diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 1e98e91..1197059 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -19,7 +19,7 @@ uses {$ELSE} Sockets, BaseUnix, {$ENDIF} - HPSDRProtocol, SyncObjs, Settings, RadioBackend, TxHealth, PlatformUtils; + HPSDRProtocol, SyncObjs, Settings, RadioBackend; const DUC_TX_QUEUE_SIZE = 256; // power of two, bounded latency on TX underrun/overrun @@ -497,7 +497,6 @@ const DUC_WIN_SPIN_US = 350; // final part of 1.25 ms interval: avoid Sleep jitter var Pkt: TDUCIQPacket; - DryFrom: Int64; // детектор: начало простоя очереди, мкс (см. TxHealth) {$IFDEF WINDOWS} QPCFreq: Int64; QPCValue: Int64; @@ -552,15 +551,11 @@ begin if not QueryPerformanceFrequency(QPCFreq) or (QPCFreq <= 0) then QPCFreq := 1000; NextDueUs := ClockUs; - DryFrom := 0; while not Terminated do begin if not FNet.DequeueDUCIQ(Pkt) then begin - // Детектор голодания: очередь пуста на передаче. Стоимость — одно чтение - // монотонных часов на ПЕРЕХОД, ни файлов, ни локов (см. TxHealth.pas). - if FNet.FIsTransmitting and (DryFrom = 0) then DryFrom := MonotonicUs; RTLEventWaitFor(FNet.FDUCIQSem, 20); NowUs := ClockUs; if NextDueUs < NowUs - DUC_PACKET_INTERVAL_US then @@ -573,12 +568,6 @@ begin if not FNet.FIsTransmitting then Continue; - if DryFrom <> 0 then - begin - TxHealthDrought(DryFrom, MonotonicUs, FNet.FDUCIQCount); - DryFrom := 0; - end; - FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, FNet.FPortDUCIQ); @@ -591,16 +580,12 @@ begin {$ELSE} VirtualSamples := 0; LastTick := GetTickCount64; - DryFrom := 0; while not Terminated do begin if not FNet.DequeueDUCIQ(Pkt) then begin DrainVirtualFIFO; - // Детектор голодания: очередь пуста на передаче. Стоимость — одно чтение - // монотонных часов на ПЕРЕХОД, ни файлов, ни локов (см. TxHealth.pas). - if FNet.FIsTransmitting and (DryFrom = 0) then DryFrom := MonotonicUs; // Очередь пуста. НЕ инжектируем нулевые пакеты — 1.25мс тишины прямо // в поток сэмплов = разрыв данных = всплеск на водопаде/в эфире. Как // dl1ycf pihpsdr (txiq_thread: пустая очередь → просто sem_wait): ждём @@ -619,12 +604,6 @@ begin if not FNet.FIsTransmitting then Continue; - if DryFrom <> 0 then - begin - TxHealthDrought(DryFrom, MonotonicUs, FNet.FDUCIQCount); - DryFrom := 0; - end; - FNet.PackSeqBytes(Pkt.Seq, FNet.NextSeq(FNet.FSeqDUCIQ)); FNet.DoSendTo(FNet.FSocket, Pkt, SizeOf(Pkt), FNet.FDevice.IPAddress, FNet.FPortDUCIQ); diff --git a/MainForm.pas b/MainForm.pas index 1f7f80f..b3d20f9 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -40,7 +40,7 @@ uses PanZoomBar, PanafallPanel, PanDisplayPopup, PureSignalPopup, FlatPopupMenu, WidebandView, StatusBar, - PlatformUtils, TxHealth, + PlatformUtils, RadioModes, WinFirewall, BoardUtils, WisdomBuilder, UISync, @@ -3822,14 +3822,9 @@ var PeakTarget, MinTarget: Double; PeakDiff, MinDiff: Double; PeakAlpha, MinAlpha: Double; - TxhSuffix: string; begin // Затухание спотов не зависит от того, крутится ли приём. ServiceDXCluster; - // Детектор голодания очереди TX: сводки за прошедшие передачи → журнал. - // ★Это ЕДИНСТВЕННОЕ место, где прибор трогает диск, и оно намеренно вынесено - // из тракта передачи в таймер UI (см. TxHealth.pas). - TxHealthService; if not FController.FRunning then begin @@ -3882,24 +3877,17 @@ begin if FController.FWDSPReady then begin - // Опасные простои очереди TX за сеанс: столько раз очередь пустела дольше - // подушки отправителя, то есть столько раз в эфир могла уйти голая несущая. - // Ноль — значит всплеск, если он был виден, родился НЕ в нашем тракте. - if TxHealthDangerTotal > 0 then - TxhSuffix := Format(' ·сухо %d', [TxHealthDangerTotal]) - else - TxhSuffix := ''; if FController.FTransmitting then begin case FController.FDSPEngine.TXMicSource of - txmsSoundCard: SetStatusText(5, 'TX sound card' + TxhSuffix); - txmsWeb: SetStatusText(5, 'TX web mic' + TxhSuffix); + txmsSoundCard: SetStatusText(5, 'TX sound card'); + txmsWeb: SetStatusText(5, 'TX web mic'); else - SetStatusText(5, 'TX radio mic' + TxhSuffix); + SetStatusText(5, 'TX radio mic'); end; end else - SetStatusText(5, 'TX idle' + TxhSuffix); + SetStatusText(5, 'TX idle'); end; end; diff --git a/RadioController.pas b/RadioController.pas index f918c75..babcc53 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -51,7 +51,7 @@ uses HPSDRProtocol, HPSDRNetwork, RadioBackend, PlutoBackend, IIOBindings, WDSPEngine, AudioOutput, AudioInput, BeaconDecoder, BeaconFEC, DMRDecoder, Settings, ChannelStore, FMRepeater, BoardUtils, DeviceStore, CWMorse, CWKeyer, - CWDecoder, TxHealth; + CWDecoder; // Панадаптеры (этап 3): потолок MAX_PANS живёт в WDSPEngine (общий для // движка/контроллера/UI). Фактический лимит бэкенда — Caps.MaxPans (Pluto=1). @@ -1864,11 +1864,6 @@ begin FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0; FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0; FHPpOverload := Status.ADCOverload; - // ★Жалоба самого радио: бит «DUC FIFO пуст». Поле ГЛУБИНЫ FIFO (байты 35-36) - // на этой прошивке статично и веры ему нет, а бит мы не читали вовсе — - // если прошивка его заполняет, это независимый свидетель голодания. - if FTransmitting and ((Status.StatusBits and HPS_FIFO_EMPTY) <> 0) then - TxHealthFifoEmpty; // Состояние ключа/манипулятора в разъёме трансивера (сетевой поток). // Касание обрывает программную передачу — как в Thetis/pihpsdr («key hit» // останавливает CAT CW), иначе оператор не может вклиниться. ★Обрываем по @@ -6297,7 +6292,6 @@ end; procedure TRadioController.SetMOX(On_: Boolean); var WasTransmitting: Boolean; - TxhSrc: Integer; begin // DMR support is receive-only. This guard also covers CAT, web and hardware // PTT. FM RAW is a flat external-modem transmit path. @@ -6432,19 +6426,6 @@ begin if FTransmitting and FDisplayDuplex then FDSPEngine.SetDisplaySourceTX(False); end; - // Детектор голодания очереди TX: фронт «в эфир» открывает передачу для - // счётчиков. Стоит ПОСЛЕ блока движка — источник модуляции решается там. - // Ввода-вывода тут нет и быть не может: журнал пишет потребитель (таймер UI / - // цикл демона), см. TxHealth.pas. - if FTransmitting and (not WasTransmitting) then - begin - if FTCIMicActive then TxhSrc := txhSrcTCI - else if FWebMicActive then TxhSrc := txhSrcWeb - else if Assigned(FDSPEngine) and (FDSPEngine.TXMicSource = txmsSoundCard) then - TxhSrc := txhSrcCard - else TxhSrc := txhSrcRadio; - TxHealthPtt(True, TxhSrc); - end; // Сетевая PTT-последовательность TX-off: сначала снимаем PTT, затем реле RX. if (not FTransmitting) and Assigned(FNetwork) and FNetwork.Connected and FNetwork.Running then begin @@ -6462,9 +6443,6 @@ begin // (оператор держит T/R руками) — вооружение обязано пойти за этой кнопкой. SyncCWKeyer; Changed(rfTransmitting); - // Передача кончилась — складываем сводку в память. Диск не трогаем (★прошлый - // прибор писал дамп ровно здесь и тем самым прикрывал гонку в пейсинге). - if WasTransmitting and (not FTransmitting) then TxHealthPtt(False, 0); end; procedure TRadioController.SetTune(On_: Boolean); diff --git a/TCIAdapter.pas b/TCIAdapter.pas index c444956..891a347 100644 --- a/TCIAdapter.pas +++ b/TCIAdapter.pas @@ -44,7 +44,7 @@ interface uses Classes, SysUtils, DateUtils, Math, SyncObjs, RadioController, RadioBackend, WDSPEngine, Settings, PlatformUtils, - DXSpotStore, TCIProtocol, TCIServer, TCIStreams, TxHealth; + DXSpotStore, TCIProtocol, TCIServer, TCIStreams; const // ★Приёмник TCI: 0 — главный тракт (каналы A/B = VFO A/B), N — слайс СЛОТА @@ -3997,12 +3997,6 @@ begin TxNoteRequest(NowUs); Inc(Sent); end; - // Снимок пейсинга для детектора голодания: поток отправителя DUC приложит - // его к очередному опасному простою и тем ответит, чей это простой — - // аванса, зернистости клиента или планировщика. Простые записи, без лока и - // без ввода-вывода (см. TxHealth.pas). - TxHealthPublishTci(FTxLead, Round(FTxOwed), Round(FTxInFlight), FTxQuantum, - Integer(PeriodUs), Round(FTxGapUs)); finally FTxLock.Leave; end; diff --git a/TxHealth.pas b/TxHealth.pas deleted file mode 100644 index 045fe97..0000000 --- a/TxHealth.pas +++ /dev/null @@ -1,358 +0,0 @@ -unit TxHealth; - -{ - TxHealth.pas — тихий детектор голодания очереди TX. - - ЗАЧЕМ. Всплеск своего сигнала, рождённый НАШИМ трактом, физически невозможен - без одного события: очередь DUC простаивает дольше подушки отправителя - (DUC_FIFO_THROTTLE = 2000 отсчётов при 192 кГц = 10.42 мс). Доказано pcap- - съёмом: шесть всплесков в дампе — шесть простоев 10.3-23.5 мс, а паузы 8 мс и - короче не дали ни одного. Значит счётчик таких простоев — ПОЛНЫЙ детектор: - молчит он — всплеск не наш (смотреть приёмный тракт, PA, реле, спуры), - сработал — наш, и момент известен с точностью до микросекунд. - - ★ГЛАВНОЕ ПРАВИЛО, оплаченное регрессом. Прошлый прибор (TxTrace) писал дамп - СИНХРОННО в хвосте SetMOX — десятки миллисекунд ровно в том окне, где жила - гонка FTxRunning, и тем самым ЧИНИЛ дефект собой: с зондами всплесков не было, - без них они возвращались. Поэтому здесь в тракте передачи не делается НИЧЕГО, - кроме записи в память: - * потоки передачи (отправитель DUC, сетевой поток статуса, планировщик TCI) - только пишут в кольцо и в счётчики — ни файлов, ни локов, ни форматирования; - * ВСЮ работу с диском делает ПОТРЕБИТЕЛЬ (таймер UI 100 мс / цикл демона), - вызывая TxHealthService. Своих потоков юнит не заводит. - - Детектор включён ВСЕГДА: переменной окружения нет намеренно — прибор, который - надо не забыть включить, к моменту дефекта выключен. -} - -{$IFDEF FPC} - {$MODE Delphi} -{$ENDIF} - -interface - -const - // Подушка отправителя: DUC_FIFO_THROTTLE = 2000 отсчётов при DUC_SAMPLE_RATE - // = 192000 (копия из HPSDRNetwork.pas — юнит намеренно ни от чего не зависит). - // Простой ДЛИННЕЕ неё гарантированно осушает FIFO радио: в эфире остаётся - // голая несущая гетеродина DUC. - TXH_CUSHION_US = 10417; - - // Источник модуляции в сводке (что именно кормило передатчик). - txhSrcRadio = 0; - txhSrcCard = 1; - txhSrcWeb = 2; - txhSrcTCI = 3; - -type - // Один опасный простой очереди — с контекстом пейсинга TCI на тот момент. - TTxDrought = record - Session: LongInt; // к какой передаче относится - AtMs: Integer; // от фронта PTT - LenUs: Integer; // длительность простоя - Queue: Integer; // глубина очереди DUC на выходе из простоя - Lead: Integer; // аванс, кадров (0 — модуляция не из TCI) - Owed: Integer; // долг, кадров - InFlight: Integer; // в полёте, кадров - Quantum: Integer; // квант запроса, кадров - StepUs: Integer; // шаг планировщика - GapUs: Integer; // период между пачками клиента - end; - - // Сводка по одной передаче. - TTxSession = record - Id: LongInt; - StartedAt: TDateTime; - LenMs: Integer; - Source: Integer; // txhSrc* - Droughts: Integer; // простоев всего - Dangerous: Integer; // из них длиннее подушки - LongestUs: Integer; - FirstDangerMs: Integer; // -1 — опасных не было - FifoEmpty: Integer; // HP-статусов с битом «DUC FIFO пуст» - end; - -// ── Пишут потоки передачи (только память) ─────────────────────────────────── -procedure TxHealthPtt(On_: Boolean; Source: Integer); -procedure TxHealthDrought(FromUs, ToUs: Int64; QueueDepth: Integer); -procedure TxHealthFifoEmpty; -procedure TxHealthPublishTci(Lead, Owed, InFlight, Quantum, StepUs, - GapUs: Integer); - -// ── Зовёт потребитель (UI-таймер / цикл демона) ───────────────────────────── -{ Забрать готовые сводки и дописать их в ~/.config/ewsdr/txhealth.log. - Возвращает число записанных передач. Работа с диском — только здесь. } -function TxHealthService: Integer; -{ Снять следующую сводку, не трогая диск (Service стоит на этих двух; стенду и - любому иному потребителю — тоже). } -function TxHealthTake(out S: TTxSession): Boolean; -{ Index-й опасный простой этой передачи, пока он ещё жив в кольце. } -function TxHealthDetail(Id: LongInt; Index: Integer; out D: TTxDrought): Boolean; -{ Опасных простоев за весь сеанс — для индикации в UI. } -function TxHealthDangerTotal: Integer; -{ Последняя записанная сводка (для UI/стенда), False — сводок ещё не было. } -function TxHealthLastSession(out S: TTxSession): Boolean; -{ Путь журнала. } -function TxHealthLogPath: string; - -implementation - -uses - SysUtils, Math, PlatformUtils; - -const - RING_SIZE = 256; // опасных простоев в памяти (хвост важнее) - RING_MASK = RING_SIZE - 1; - SESS_SIZE = 32; // сводок, ждущих потребителя - SESS_MASK = SESS_SIZE - 1; - MAX_DETAIL = 24; // строк подробностей на передачу в журнале - -var - // Кольцо опасных простоев: пишет поток отправителя DUC, читает потребитель. - Ring: array[0..RING_SIZE - 1] of TTxDrought; - RingSeq: LongInt = 0; - - // Кольцо сводок: пишет поток, снимающий PTT, читает потребитель. - Sess: array[0..SESS_SIZE - 1] of TTxSession; - SessSeq: LongInt = 0; // сколько сводок сложено - SessRead: LongInt = 0; // сколько забрал потребитель - - // Текущая передача. - CurId: LongInt = 0; - CurStartUs: Int64 = 0; - CurStartAt: TDateTime = 0; - CurSource: Integer = txhSrcRadio; - CurOn: Boolean = False; - CurDroughts, CurDanger, CurLongest, CurFirstMs, CurFifoEmpty: Integer; - - // Снимок пейсинга TCI — публикует планировщик, читает поток отправителя. - // Простые записи без лока: числа диагностические, рвань в них безвредна, а - // лок в потоке отправителя DUC — недопустимый риск для тайминга. - PubLead, PubOwed, PubFlight, PubQuantum, PubStep, PubGap: Integer; - - DangerTotal: Integer = 0; - LastSess: TTxSession; - HaveLast: Boolean = False; - -procedure TxHealthPtt(On_: Boolean; Source: Integer); -// Фронт PTT. Только память: несколько записей, никакого ввода-вывода. -var - S: TTxSession; - Idx: LongInt; -begin - if On_ then - begin - if CurOn then Exit; - InterlockedIncrement(CurId); - CurStartUs := MonotonicUs; - CurStartAt := Now; - CurSource := Source; - CurDroughts := 0; - CurDanger := 0; - CurLongest := 0; - CurFirstMs := -1; - CurFifoEmpty := 0; - CurOn := True; - Exit; - end; - - if not CurOn then Exit; - CurOn := False; - S.Id := CurId; - S.StartedAt := CurStartAt; - S.LenMs := Integer((MonotonicUs - CurStartUs) div 1000); - S.Source := CurSource; - S.Droughts := CurDroughts; - S.Dangerous := CurDanger; - S.LongestUs := CurLongest; - S.FirstDangerMs := CurFirstMs; - S.FifoEmpty := CurFifoEmpty; - Idx := InterlockedIncrement(SessSeq) - 1; - Sess[Idx and SESS_MASK] := S; -end; - -procedure TxHealthDrought(FromUs, ToUs: Int64; QueueDepth: Integer); -// Поток отправителя DUC, на выходе очереди из простоя. Дороже всего здесь одно -// вычитание: время простоя измерил вызывающий, он же держит его границы. -var - LenUs, AtMs: Integer; - Idx: LongInt; - P: ^TTxDrought; -begin - if not CurOn then Exit; - LenUs := Integer(ToUs - FromUs); - if LenUs < 0 then Exit; - Inc(CurDroughts); - if LenUs > CurLongest then CurLongest := LenUs; - if LenUs <= TXH_CUSHION_US then Exit; - - Inc(CurDanger); - InterlockedIncrement(DangerTotal); - AtMs := Integer((FromUs - CurStartUs) div 1000); - if CurFirstMs < 0 then CurFirstMs := AtMs; - - Idx := InterlockedIncrement(RingSeq) - 1; - P := @Ring[Idx and RING_MASK]; - P^.Session := CurId; - P^.AtMs := AtMs; - P^.LenUs := LenUs; - P^.Queue := QueueDepth; - P^.Lead := PubLead; - P^.Owed := PubOwed; - P^.InFlight := PubFlight; - P^.Quantum := PubQuantum; - P^.StepUs := PubStep; - P^.GapUs := PubGap; -end; - -procedure TxHealthFifoEmpty; -// Сетевой поток, разбор HP-статуса: радио говорит, что его DUC FIFO пуст. -// ★На этой прошивке поле ГЛУБИНЫ FIFO статично (всегда 256) и веры ему нет — -// а вот бит в байте статуса мы раньше не читали вовсе. Если прошивка его -// заполняет, получаем жалобу самого железа как независимого свидетеля. -begin - if CurOn then Inc(CurFifoEmpty); -end; - -procedure TxHealthPublishTci(Lead, Owed, InFlight, Quantum, StepUs, - GapUs: Integer); -// Планировщик TCI. Публикация состояния пейсинга для контекста простоев. -begin - PubLead := Lead; - PubOwed := Owed; - PubFlight := InFlight; - PubQuantum := Quantum; - PubStep := StepUs; - PubGap := GapUs; -end; - -function TxHealthDangerTotal: Integer; -begin - Result := DangerTotal; -end; - -function TxHealthLastSession(out S: TTxSession): Boolean; -begin - Result := HaveLast; - if Result then S := LastSess; -end; - -function TxHealthLogPath: string; -begin - Result := GetAppCfgDir + 'txhealth.log'; -end; - -function SourceName(Src: Integer): string; -begin - case Src of - txhSrcTCI: Result := 'TCI'; - txhSrcWeb: Result := 'web'; - txhSrcCard: Result := 'звуковая карта'; - else - Result := 'микрофон радио'; - end; -end; - -function TxHealthTake(out S: TTxSession): Boolean; -var Have: LongInt; -begin - Have := SessSeq; - if Have <= SessRead then Exit(False); - // Сводки, которые кольцо успело затереть, пропускаем: журнал честно отражает - // хвост, а отстать на SESS_SIZE передач потребитель может лишь простояв - // секунды — чего с таймером 100 мс не бывает. - if Have - SessRead > SESS_SIZE then SessRead := Have - SESS_SIZE; - S := Sess[SessRead and SESS_MASK]; - Inc(SessRead); - LastSess := S; - HaveLast := True; - Result := True; -end; - -function TxHealthDetail(Id: LongInt; Index: Integer; out D: TTxDrought): Boolean; -var - i, Seen, Total: LongInt; -begin - Result := False; - Total := RingSeq; - Seen := 0; - for i := Max(0, Total - RING_SIZE) to Total - 1 do - begin - if Ring[i and RING_MASK].Session <> Id then Continue; - if Seen = Index then - begin - D := Ring[i and RING_MASK]; - Exit(True); - end; - Inc(Seen); - end; -end; - -procedure WriteSession(var F: TextFile; const S: TTxSession); -// Одна передача: строка сводки плюс подробности по опасным простоям. -var - Shown: Integer; - R: TTxDrought; - Verdict: string; -begin - if S.Dangerous > 0 then - Verdict := 'ОПАСНО' - else - Verdict := 'чисто'; - if S.FirstDangerMs >= 0 then - Verdict := Verdict + Format(' (первое на %d мс)', [S.FirstDangerMs]); - WriteLn(F, Format('%s %s передача %.1f с, источник %s: осушений %d, ' + - 'опасных %d, самое длинное %.1f мс, FIFO-EMPTY %d', - [FormatDateTime('yyyy-mm-dd hh:nn:ss', S.StartedAt), Verdict, - S.LenMs / 1000.0, SourceName(S.Source), S.Droughts, S.Dangerous, - S.LongestUs / 1000.0, S.FifoEmpty])); - if S.Dangerous <= 0 then Exit; - - Shown := 0; - while TxHealthDetail(S.Id, Shown, R) do - begin - Inc(Shown); - if Shown > MAX_DETAIL then - begin - WriteLn(F, ' … остальные опущены'); - Break; - end; - WriteLn(F, Format(' опасный #%d: на %d мс от PTT, %.1f мс, очередь %d, ' + - 'аванс %d, долг %d, в полёте %d, квант %d, шаг %d мкс, ' + - 'пачка клиента %.1f мс', - [Shown, R.AtMs, R.LenUs / 1000.0, R.Queue, R.Lead, R.Owed, - R.InFlight, R.Quantum, R.StepUs, R.GapUs / 1000.0])); - end; -end; - -function TxHealthService: Integer; -// ★Единственное место с диском, и зовут его только потребители (таймер UI, -// цикл демона) — никогда не поток передачи. -var - F: TextFile; - S: TTxSession; -begin - Result := 0; - if SessSeq <= SessRead then Exit; - - AssignFile(F, TxHealthLogPath); - try - if FileExists(TxHealthLogPath) then Append(F) else Rewrite(F); - try - while TxHealthTake(S) do - begin - WriteSession(F, S); - Inc(Result); - end; - finally - CloseFile(F); - end; - except - // Журнал — вспомогательный: не пишется, так и ладно. Сводки при этом - // считаем забранными, иначе они копились бы и пытались писаться каждые - // 100 мс. - on E: Exception do SessRead := SessSeq; - end; -end; - -end. diff --git a/ewsdrd.lpr b/ewsdrd.lpr index beea708..f04e51c 100644 --- a/ewsdrd.lpr +++ b/ewsdrd.lpr @@ -28,7 +28,7 @@ uses {$IFDEF UNIX}cthreads, BaseUnix,{$ENDIF} Classes, SysUtils, Math, RadioController, WebServer, WebAdapter, WDSPEngine, - HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils, TxHealth; + HPSDRNetwork, Settings, WDSP, PlatformUtils, DeviceStore, BoardUtils; const PUSH_INTERVAL_MS = 50; // период исходящего web-зеркала (как GUI-таймер) @@ -470,9 +470,6 @@ begin // Телеграф: фронты «железо в эфире» (у Pluto HP-статуса нет вовсе, и // отпустить реле T/R после выдержки больше некому). FCtrl.ServiceCWKeyed; - // Детектор голодания очереди TX: сводки за прошедшие передачи → журнал. - // Единственное место, где прибор трогает диск (см. TxHealth.pas). - TxHealthService; lastBeacon := nowMs; end; if nowMs - lastPush >= PUSH_INTERVAL_MS then diff --git a/test/hpsdr/README.md b/test/hpsdr/README.md index aa2ed62..8030397 100644 --- a/test/hpsdr/README.md +++ b/test/hpsdr/README.md @@ -105,54 +105,31 @@ TX голодала, в эфире дыры. Тут же диапазон глу * `capture.sh` — кольцевой съём tcpdump + лог потерь ядра. * `pcap_tx_scan.py` — разбор pcap, чистый Python без зависимостей (~40 МБ/с). -## Тихий детектор голодания очереди TX (включён всегда) +## Тихий детектор голодания очереди TX (снят, вернуть из `b173b7f`) -Всплеск, рождённый НАШИМ трактом, невозможен без одного события: очередь DUC -простаивает дольше подушки отправителя (`DUC_FIFO_THROTTLE` = 2000 отсчётов при -192 кГц = **10.42 мс**). Это доказано съёмом 2026-08-21: шесть всплесков — шесть -простоев 10.3–23.5 мс, паузы 8 мс и короче не дали ни одного. Поэтому счётчик -таких простоев — **полный** детектор, и он теперь живёт в самой программе -(`TxHealth.pas`), без переменных окружения: прибор, который надо не забыть -включить, к моменту дефекта выключен. +Опора детектора: всплеск, рождённый НАШИМ трактом, невозможен без простоя +очереди DUC длиннее подушки отправителя (`DUC_FIFO_THROTTLE` = 2000 отсчётов при +192 кГц = **10.42 мс**) — съём 2026-08-21 дал шесть всплесков на шесть простоев +10.3–23.5 мс, а паузы 8 мс и короче не дали ни одного. Значит счётчик таких +простоев отвечает на вопрос «наш всплеск или не наш» полностью. -Что делает: +Прибор (`TxHealth.pas`) был включён всегда, без переменных окружения: поток +отправителя DUC мерил простои, планировщик TCI публиковал рядом контекст +пейсинга (аванс, долг, в полёте, квант, шаг, период пачек клиента), сетевой +поток считал бит `HPS_FIFO_EMPTY`. ★Журнал (`~/.config/ewsdr/txhealth.log`, +строка на передачу: «чисто» / «ОПАСНО (первое на N мс)» плюс подробности) писал +ТОЛЬКО потребитель — таймер UI и цикл демона; в тракте передачи диска не было по +построению. В статусной строке показывался счётчик `·сухо N` за сеанс. -* поток отправителя DUC мерит каждый простой очереди на передаче — одно чтение - монотонных часов на переход, ни файлов, ни локов; -* планировщик TCI публикует рядом состояние пейсинга (аванс, долг, в полёте, - квант, шаг, период пачек клиента) — простыми записями; -* сетевой поток считает биты «DUC FIFO пуст» из HP-статуса радио (поле ГЛУБИНЫ - на этой прошивке статично и веры ему нет, а бит раньше не читался вовсе); -* журнал пишет **потребитель** — таймер UI (100 мс) или цикл демона, никогда не - тракт передачи. +Вернуть целиком: -★Последнее — правило, оплаченное регрессом: прошлый прибор (`TxTrace`) сбрасывал -дамп синхронно в хвосте `SetMOX`, и эти десятки миллисекунд закрывали гонку в -пейсинге TCI. С зондами всплесков не было, без них они возвращались — -инструмент чинил дефект собой (лечение: коммит `fa38fa7`). + git revert <коммит снятия> # он же и есть реверс b173b7f -Журнал: `~/.config/ewsdr/txhealth.log`, по строке на передачу. - -``` -2026-08-24 19:08:03 ОПАСНО (первое на 100 мс) передача 12.4 с, источник TCI: осушений 17, опасных 1, самое длинное 18.0 мс, FIFO-EMPTY 0 - опасный #1: на 100 мс от PTT, 18.0 мс, очередь 0, аванс 3584, долг 2200, в полёте 1024, квант 512, шаг 5333 мкс, пачка клиента 44.1 мс -``` - -Как читать: - -* **`чисто`** — за эту передачу очередь ни разу не пересыхала дольше подушки. - Значит увиденный всплеск родился НЕ в нашем тракте: смотреть приёмный тракт - (дыра в DDC — на экране есть, в эфире нет), PA, реле, спуры. -* **`ОПАСНО`** — простой был, и подробная строка называет виновника по контексту: - пачка клиента длиннее аванса (`пачка клиента` > `аванс`/48 мс) — зернистость - MSHV; `долг` у потолка при малом `в полёте` — планировщик не успевает; - `шаг` заметно больше половины кванта — проспал поток планировщика. -* Строка статуса в UI показывает `·сухо N` — сколько опасных простоев набралось - за сеанс, чтобы не приходилось помнить, какая посылка была плохой. - -Правило разбора то же, что и с pcap: **дальше сопоставлять**. Детектор говорит -«очередь сохла в такой-то момент», `pcap_tx_scan.py` по тому же куску времени -скажет, ушёл ли всплеск в эфир или это артефакт приёма. +★Правило, ради которого прибор так и построен: предыдущий инструмент +(`TxTrace`) сбрасывал дамп синхронно в хвосте `SetMOX` и этими десятками +миллисекунд закрывал гонку в пейсинге TCI — с зондами всплесков не было, без них +они возвращались (лечение гонки — `fa38fa7`). Инструмент не имеет права ничего +делать в тракте передачи. ## Приборы внутри процесса: кольцевая трассировка TX-тракта diff --git a/test/tci/tcitest.pas b/test/tci/tcitest.pas index bb292e4..44aaf42 100644 --- a/test/tci/tcitest.pas +++ b/test/tci/tcitest.pas @@ -28,7 +28,7 @@ program tcitest; uses cthreads, Classes, SysUtils, Math, SyncObjs, Sockets, BaseUnix, TCIProtocol, TCIStreams, TCIServer, TCIAdapter, RadioController, - WsClient, WebUtils, WDSPEngine, Settings, CWMorse, TxHealth, PlatformUtils; + WsClient, WebUtils, WDSPEngine, Settings, CWMorse; var Passed, Failed: Integer; @@ -2207,83 +2207,6 @@ begin end; end; -procedure TestTxHealth; -// Детектор голодания очереди TX (TxHealth.pas). Стенд проверяет его отдельно от -// сети: порог «опасного» простоя, сводку по передаче, контекст пейсинга рядом с -// событием и то, что вне передачи прибор молчит. -// ★Диска здесь нет намеренно: TxHealthService писал бы в НАСТОЯЩИЙ журнал -// пользователя, поэтому стенд берёт сводки через TxHealthTake. -var - Base: Int64; - S: TTxSession; - D: TTxDrought; - Was: Integer; -begin - WriteLn('G. Детектор голодания очереди TX'); - - // Сводки, накопленные прошлыми частями стенда (они гоняют SetMOX), забираем, - // чтобы мерить своё. - while TxHealthTake(S) do ; - Was := TxHealthDangerTotal; - - // Вне передачи прибор обязан молчать: простой на приёме — это норма, DUC-поток - // тогда не шлёт вообще ничего. - Base := MonotonicUs; - TxHealthDrought(Base, Base + 50000, 0); - TxHealthFifoEmpty; - Check('детектор: вне передачи простой не считается', - TxHealthDangerTotal = Was, IntToStr(TxHealthDangerTotal - Was)); - - TxHealthPtt(True, txhSrcTCI); - TxHealthPublishTci(3584, 2200, 1024, 512, 5333, 44100); - Base := MonotonicUs; - // Короткий простой: подушка отправителя его покрывает, в эфир он не выходит. - TxHealthDrought(Base, Base + 8000, 7); - Check('детектор: простой короче подушки не опасен', - TxHealthDangerTotal = Was, IntToStr(TxHealthDangerTotal - Was)); - // Длинный: FIFO радио сохнет, в эфире голая несущая. - TxHealthDrought(Base + 100000, Base + 118000, 0); - TxHealthFifoEmpty; - Check('детектор: простой длиннее подушки опасен', - TxHealthDangerTotal = Was + 1, IntToStr(TxHealthDangerTotal - Was)); - TxHealthPtt(False, 0); - - Check('детектор: сводка передачи снялась', TxHealthTake(S)); - Check('детектор: простоев посчитано верно', S.Droughts = 2, IntToStr(S.Droughts)); - Check('детектор: опасных посчитано верно', S.Dangerous = 1, IntToStr(S.Dangerous)); - Check('детектор: самое длинное — длиннее подушки', - S.LongestUs = 18000, IntToStr(S.LongestUs)); - Check('детектор: момент первого опасного — от фронта PTT', - (S.FirstDangerMs >= 90) and (S.FirstDangerMs <= 130), - IntToStr(S.FirstDangerMs)); - Check('детектор: источник модуляции в сводке', S.Source = txhSrcTCI); - Check('детектор: жалоба радио (FIFO EMPTY) считается только на передаче', - S.FifoEmpty = 1, IntToStr(S.FifoEmpty)); - - // ★Ради этого прибор и делался: рядом с опасным простоем лежит состояние - // пейсинга — по нему видно, чей простой (аванса, зернистости клиента или - // планировщика), и оператору не нужно называть интервал на глаз. - Check('детектор: контекст пейсинга лежит рядом с событием', - TxHealthDetail(S.Id, 0, D)); - Check('детектор: в контексте аванс и пачка клиента', - (D.Lead = 3584) and (D.GapUs = 44100) and (D.Quantum = 512) and - (D.LenUs = 18000), - Format('аванс %d, пачка %d, квант %d, простой %d', - [D.Lead, D.GapUs, D.Quantum, D.LenUs])); - Check('детектор: второго опасного в этой передаче нет', - not TxHealthDetail(S.Id, 1, D)); - - // Чистая передача: сводка обязана прийти с честным «опасных 0» — именно она и - // снимает подозрение с нашего тракта. - TxHealthPtt(True, txhSrcCard); - Base := MonotonicUs; - TxHealthDrought(Base, Base + 3000, 9); - TxHealthPtt(False, 0); - Check('детектор: чистая передача помечена как чистая', - TxHealthTake(S) and (S.Dangerous = 0) and (S.Droughts = 1) and - (S.FirstDangerMs < 0), Format('опасных %d', [S.Dangerous])); -end; - procedure TestEndToEnd; const PORT = 40098; @@ -2734,7 +2657,6 @@ begin TestServer; TestEndToEnd; TestTxPacing; - TestTxHealth; WriteLn; WriteLn(Format('Итого: %d проверок, провалено %d', [Passed + Failed, Failed])); if Failed > 0 then Halt(1);