From c1da506f73a5234ff4101614d574a284f54a86d7 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Fri, 5 Jun 2026 23:35:35 +0300 Subject: [PATCH] Phase 5 (batch 25b): move HP-status callback into the controller OnHPStatusCB + DoUpdateStatus move into TRadioController as OnHPStatus (network thread: board-specific supply/power/SWR decode -> pending fields -> Invoke) and ApplyHPStatus (marshalled: PEP-style fwd/SWR ballistics + supply EMA into FLast*, ADC-overload via Changed(rfADCOverload), and the HW-PTT edge -> SetMOX). FNetwork.OnHPStatus is wired to the controller; the ADC-overload indicator renders via OnControllerState(rfADCOverload). The HWPTT->TX path is now headless-capable and reads an always-current FWebClientActive for mic-source. TStatusUISync is left dead in UISync.pas (unit still used for TDDCSeqSync) pending a separate cleanup. Co-Authored-By: Claude Opus 4.8 --- MainForm.pas | 175 +++----------------------------------------- RadioController.pas | 145 ++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 165 deletions(-) diff --git a/MainForm.pas b/MainForm.pas index 1630e70..6deb519 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -351,7 +351,6 @@ type // Network callbacks procedure OnDeviceFound(const Dev: THPSDRDevice); - procedure OnHPStatusCB(const Status: THighPriorityStatus); procedure OnWidebandCB(ADCIndex: Integer; const Samples: array of SmallInt; Count: Integer); procedure OnWDSPOpenDone(Success: Boolean); @@ -364,8 +363,6 @@ type // Public UI update (called from sync objects) procedure DoAddDevice(const Dev: THPSDRDevice; const Entry: string); - procedure DoUpdateStatus(FwdW, SWRV, SupplyV, SupplyA: Double; PLLLock, HWPTT: Boolean; - ADCOverload: Byte); procedure UpdateTXMeters; procedure DoUpdateDDCSeq(DDCIdx: Integer; Seq: LongWord); procedure DoUpdateDDCSeqOrNoDevice(DDCIdx: Integer; Seq: LongWord); @@ -1139,7 +1136,7 @@ begin // startup-preview выше, поэтому FDSPEngine рождается с нужной частотой. FController.CreateEngines(FController.FSampleRate); FController.FNetwork.OnDeviceFound := OnDeviceFound; - FController.FNetwork.OnHPStatus := OnHPStatusCB; + FController.FNetwork.OnHPStatus := FController.OnHPStatus; FController.FNetwork.OnDDCIQ := FController.OnDDCIQ; FController.FNetwork.OnMicPacket := FController.OnMicPacket; FController.FNetwork.OnWideband := OnWidebandCB; @@ -1317,6 +1314,12 @@ begin end; rfTuning: StyleButton(BtnTUN, FController.FTuning); + rfADCOverload: + if FSpecView <> nil then + begin + FSpecView.ADCOverloadVisible := FController.FADCOverloadActive; + if PbSpectrum <> nil then PbSpectrum.Invalidate; + end; rfBand: // В XVTR-режиме HF-кнопки гасим (активна XVTR-кнопка, см. rfXvtr). for i := 0 to BAND_COUNT - 1 do @@ -3281,169 +3284,11 @@ begin SetStatusText(2, Format('Found: %s', [Dev.IPAddress])); end; -procedure TMainForm.OnHPStatusCB(const Status: THighPriorityStatus); -const - // Порог fwd-мощности (W), ниже которого SWR не вычисляем — там всё ADC-шум. - SWR_MIN_FWD_W = 1.0; - // Верхняя граница SWR (выше — это всё равно «беда»). - SWR_CAP = 9.9; -var - ExcPwr, FwdPwr, RevPwr: Word; - SupplyV, SupplyA, FwdW, SWRV, Rho: Double; - IsTx: Boolean; - Sync: TStatusUISync; - M: TThreadMethod; -begin - ExcPwr := (Status.ExciterPwr0Hi shl 8) or Status.ExciterPwr0Lo; - FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; - RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; - SupplyV := -1.0; - SupplyA := -1.0; - - // Board 5 (Orion MkII): voltage on UserADC0 (bytes 57-58), - // current on UserADC1 (bytes 55-56) — matching Thetis behaviour. - // Other supported boards report voltage in SupplyVolts bytes and no current. - if BoardSupportsSupplyVoltage(FController.FNetwork.Device.BoardType) then - begin - if FController.FNetwork.Device.BoardType = 5 then - SupplyV := ADCToSupplyVolts( - (Status.UserADC0Hi shl 8) or Status.UserADC0Lo, - FController.FNetwork.Device.BoardType) - else - SupplyV := ADCToSupplyVolts( - (Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo, - FController.FNetwork.Device.BoardType); - - if BoardSupportsSupplyCurrent(FController.FNetwork.Device.BoardType) then - SupplyA := ADCToSupplyCurrent( - (Status.UserADC1Hi shl 8) or Status.UserADC1Lo, - FController.FNetwork.Device.BoardType); - end; - // FController.FLastSMeter обновляется только из WDSP (GetSMeterDBm) в SpectrumTimerTick — - // ExciterPwr это мощность TX, не уровень принятого сигнала. - FwdW := ADCToWatts100(FwdPwr); - // FController.FTransmitting — наш программный флаг (MOX/TUN/HW-PTT через ApplyMOX). - // HPS_PTT-бит в StatusBits — это вход аппаратного PTT (footswitch/mic), - // а не «трансивер сейчас передаёт», поэтому только на него полагаться нельзя. - IsTx := FController.FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); - // SWR: правильная формула (1+ρ)/(1-ρ), считаем только когда есть значимая - // прямая мощность — иначе ADC-шум RevPwr даёт случайные «прыжки» SWR. - // Не на передаче — SWR не показываем (фиксируем 1.0), Fwd принудительно 0. - if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then - begin - // ρ = sqrt(Pr/Pf). Поскольку P=V²/R и V пропорционально ADC, то - // sqrt(Pr/Pf) = adc_r/adc_f напрямую — лишнего sqrt не нужно. - Rho := RevPwr / FwdPwr; - if Rho >= 0.99 then - SWRV := SWR_CAP - else - begin - SWRV := (1.0 + Rho) / (1.0 - Rho); - if SWRV > SWR_CAP then SWRV := SWR_CAP; - if SWRV < 1.0 then SWRV := 1.0; - end; - end else - SWRV := 1.0; - if not IsTx then FwdW := 0; - Sync := TStatusUISync.Create(DoUpdateStatus, FwdW, SWRV, SupplyV, SupplyA, - (Status.StatusBits and HPS_PLL_LOCKED) <> 0, - (Status.StatusBits and HPS_PTT) <> 0, - Status.ADCOverload); - try - M := Sync.Execute; - TThread.Synchronize(nil, M); - finally - Sync.Free; - end; -end; - -procedure TMainForm.DoUpdateStatus(FwdW, SWRV, SupplyV, SupplyA: Double; PLLLock, HWPTT: Boolean; - ADCOverload: Byte); -const - // Thetis/piHPSDR PEP-style баллистика для FwdW и SWR: атака мгновенная - // (peak-hold), спад медленный — стрелка «висит» на пике и плавно опускается. - // Это маскирует огибающую SSB-голоса, которая на ADC прыгает на 10-20 дБ - // внутри слога. На rate ~150 пакетов/с эти α дают τ_decay ≈ 350 мс / 500 мс. - DECAY_FWD = 0.02; - DECAY_SWR = 0.015; - // Напряжение питания меняется медленно — обычное симметричное EMA. - ALPHA_SUPPLY = 0.05; -begin - // Запоминаем значения; вывод в UI делает MeterTimerTick на 10 Гц. - // Сама обработка здесь идёт на UI-потоке через Synchronize. - // FwdW=0 / SWRV=1 — это «не на передаче»: снапим без decay, - // чтобы метры моментально падали при отпускании PTT. - if FwdW <= 0 then - FController.FLastFwdW := 0 - else if FwdW > FController.FLastFwdW then - FController.FLastFwdW := FwdW // attack: instant - else - FController.FLastFwdW := DECAY_FWD * FwdW + (1.0 - DECAY_FWD) * FController.FLastFwdW; - if SWRV <= 1.0 then - FController.FLastSWR := 1.0 - else if SWRV > FController.FLastSWR then - FController.FLastSWR := SWRV // attack: instant - else - FController.FLastSWR := DECAY_SWR * SWRV + (1.0 - DECAY_SWR) * FController.FLastSWR; - if SupplyV >= 0 then - begin - if FController.FLastSupplyV < 0 then - FController.FLastSupplyV := SupplyV // первое значение — без сглаживания - else - FController.FLastSupplyV := ALPHA_SUPPLY * SupplyV + (1.0 - ALPHA_SUPPLY) * FController.FLastSupplyV; - end else - FController.FLastSupplyV := -1.0; - - if SupplyA >= 0 then - begin - if FController.FLastSupplyA < 0 then - FController.FLastSupplyA := SupplyA - else - FController.FLastSupplyA := ALPHA_SUPPLY * SupplyA + (1.0 - ALPHA_SUPPLY) * FController.FLastSupplyA; - end else - FController.FLastSupplyA := -1.0; - FController.FLastPLLLock := PLLLock; - - if (FSpecView <> nil) and (FSpecView.ADCOverloadVisible <> (ADCOverload <> 0)) then - begin - FSpecView.ADCOverloadVisible := ADCOverload <> 0; - if PbSpectrum <> nil then - PbSpectrum.Invalidate; - end; - - // Hardware PTT (foot switch / mic PTT) — обнаружение фронта (нужно делать - // на rate HP Status, а не таймера, чтобы не пропустить короткое нажатие). - if HWPTT <> FController.FHWPTTActive then - begin - FController.FHWPTTActive := HWPTT; - if not FController.FTuning then - begin - if HWPTT then - begin - // Press: не перекрываем ручной MOX; запоминаем что мы включили TX. - if not BtnMOX.Active then - begin - ApplyMOX(True); - FController.FHWPTTStartedTX := True; - end; - end else - begin - // Release: выключаем TX только если он был включён через HWPTT. - // Нельзя проверять BtnMOX.Active — ApplyMOX(True) сам его выставляет. - if FController.FHWPTTStartedTX then - begin - FController.FHWPTTStartedTX := False; - ApplyMOX(False); - end; - end; - end; - end; -end; procedure TMainForm.UpdateTXMeters; -// Вызывается из MeterTimerTick (10 Гц). Использует FController.FLastFwdW/FController.FLastSWR/ -// FController.FLastSupplyV/FController.FLastSupplyA обновляются на rate HP Status (~150/с) -// в DoUpdateStatus. +// Вызывается из MeterTimerTick (10 Гц). FController.FLastFwdW/FLastSWR/ +// FLastSupplyV/FLastSupplyA обновляются на rate HP Status (~150/с) +// в TRadioController.ApplyHPStatus. begin if not FController.FRunning then begin diff --git a/RadioController.pas b/RadioController.pas index 069c824..75ab676 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -69,6 +69,7 @@ type rfFMStep, rfFMStepIdx, rfFMRpt, rfChannel, rfSMeter, rfPower, rfSWR, rfSupply, rfPLL, // телеметрия + rfADCOverload, // ADC overload (рендер индикатора) rfDevice // подключённое устройство сменилось ); @@ -213,6 +214,12 @@ type FLastSeqErrorDDC: Integer; FLastSeqErrorDelta: Int64; FSeqOkStreak: Integer; + FADCOverloadActive: Boolean; // ADC overload (рендерится по rfADCOverload) + // Pending-значения декода HP-статуса (сетевой поток → ApplyHPStatus через + // Invoke; Synchronize блокирующий и последовательный, гонок нет). + FHPpFwdW, FHPpSWR, FHPpSupplyV, FHPpSupplyA: Double; + FHPpPLL, FHPpHWPTT: Boolean; + FHPpOverload: Byte; constructor Create; destructor Destroy; override; @@ -230,6 +237,8 @@ type procedure OnMicPacket(const Data: TMicDataPacket); function PullSoundCardMic(MaxN: Integer): Integer; procedure OnDDCIQ(DDCIndex: Integer; const Data: TDDCIQPacket); + procedure OnHPStatus(const Status: THighPriorityStatus); + procedure ApplyHPStatus; // маршалится из OnHPStatus (сглаживание/overload/HWPTT) // Маршалинг команды в поток контроллера (GUI: TThread.Synchronize). procedure Invoke(M: TThreadMethod); @@ -499,6 +508,142 @@ begin FLastDDCIndex := DDCIndex; end; +procedure TRadioController.OnHPStatus(const Status: THighPriorityStatus); +// Сетевой поток: декод HP-статуса (board-specific supply/power/SWR) → pending → +// маршалинг в ApplyHPStatus (сглаживание + overload + HWPTT уже на потоке UI/ядра). +const + SWR_MIN_FWD_W = 1.0; // ниже — fwd это ADC-шум, SWR не считаем + SWR_CAP = 9.9; +var + FwdPwr, RevPwr: Word; + SupplyV, SupplyA, FwdW, SWRV, Rho: Double; + IsTx: Boolean; +begin + FwdPwr := (Status.FwdPwrAlex0Hi shl 8) or Status.FwdPwrAlex0Lo; + RevPwr := (Status.RevPwrAlex0Hi shl 8) or Status.RevPwrAlex0Lo; + SupplyV := -1.0; + SupplyA := -1.0; + + // Board 5 (Orion MkII): напряжение на UserADC0, ток на UserADC1 (как Thetis). + // Прочие платы — в SupplyVolts-байтах, без тока. + if BoardSupportsSupplyVoltage(FNetwork.Device.BoardType) then + begin + if FNetwork.Device.BoardType = 5 then + SupplyV := ADCToSupplyVolts( + (Status.UserADC0Hi shl 8) or Status.UserADC0Lo, + FNetwork.Device.BoardType) + else + SupplyV := ADCToSupplyVolts( + (Status.SupplyVoltsHi shl 8) or Status.SupplyVoltsLo, + FNetwork.Device.BoardType); + + if BoardSupportsSupplyCurrent(FNetwork.Device.BoardType) then + SupplyA := ADCToSupplyCurrent( + (Status.UserADC1Hi shl 8) or Status.UserADC1Lo, + FNetwork.Device.BoardType); + end; + FwdW := ADCToWatts100(FwdPwr); + // FTransmitting — программный флаг; HPS_PTT — вход HW-PTT (footswitch/mic). + IsTx := FTransmitting or ((Status.StatusBits and HPS_PTT) <> 0); + // SWR = (1+ρ)/(1-ρ), ρ = adc_r/adc_f; только при значимой fwd-мощности. + if IsTx and (FwdW >= SWR_MIN_FWD_W) and (FwdPwr > 0) then + begin + Rho := RevPwr / FwdPwr; + if Rho >= 0.99 then + SWRV := SWR_CAP + else + begin + SWRV := (1.0 + Rho) / (1.0 - Rho); + if SWRV > SWR_CAP then SWRV := SWR_CAP; + if SWRV < 1.0 then SWRV := 1.0; + end; + end else + SWRV := 1.0; + if not IsTx then FwdW := 0; + + FHPpFwdW := FwdW; + FHPpSWR := SWRV; + FHPpSupplyV := SupplyV; + FHPpSupplyA := SupplyA; + FHPpPLL := (Status.StatusBits and HPS_PLL_LOCKED) <> 0; + FHPpHWPTT := (Status.StatusBits and HPS_PTT) <> 0; + FHPpOverload := Status.ADCOverload; + Invoke(ApplyHPStatus); +end; + +procedure TRadioController.ApplyHPStatus; +// Маршалится из OnHPStatus. PEP-style баллистика (мгновенная атака, медленный +// спад) для FwdW/SWR; EMA для supply. + overload-событие + HWPTT-фронт → SetMOX. +const + DECAY_FWD = 0.02; + DECAY_SWR = 0.015; + ALPHA_SUPPLY = 0.05; +begin + if FHPpFwdW <= 0 then + FLastFwdW := 0 + else if FHPpFwdW > FLastFwdW then + FLastFwdW := FHPpFwdW // attack: instant + else + FLastFwdW := DECAY_FWD * FHPpFwdW + (1.0 - DECAY_FWD) * FLastFwdW; + if FHPpSWR <= 1.0 then + FLastSWR := 1.0 + else if FHPpSWR > FLastSWR then + FLastSWR := FHPpSWR // attack: instant + else + FLastSWR := DECAY_SWR * FHPpSWR + (1.0 - DECAY_SWR) * FLastSWR; + if FHPpSupplyV >= 0 then + begin + if FLastSupplyV < 0 then + FLastSupplyV := FHPpSupplyV // первое значение — без сглаживания + else + FLastSupplyV := ALPHA_SUPPLY * FHPpSupplyV + (1.0 - ALPHA_SUPPLY) * FLastSupplyV; + end else + FLastSupplyV := -1.0; + if FHPpSupplyA >= 0 then + begin + if FLastSupplyA < 0 then + FLastSupplyA := FHPpSupplyA + else + FLastSupplyA := ALPHA_SUPPLY * FHPpSupplyA + (1.0 - ALPHA_SUPPLY) * FLastSupplyA; + end else + FLastSupplyA := -1.0; + FLastPLLLock := FHPpPLL; + + // ADC overload — индикатор рисует UI по rfADCOverload (только на изменении). + if FADCOverloadActive <> (FHPpOverload <> 0) then + begin + FADCOverloadActive := FHPpOverload <> 0; + Changed(rfADCOverload); + end; + + // Hardware PTT (footswitch / mic PTT): фронт на rate HP Status — не пропустить + // короткое нажатие. HWPTT не перекрывает ручной MOX; выключаем TX только если + // его включил именно HWPTT. + if FHPpHWPTT <> FHWPTTActive then + begin + FHWPTTActive := FHPpHWPTT; + if not FTuning then + begin + if FHPpHWPTT then + begin + if not FTransmitting then + begin + SetMOX(True); + FHWPTTStartedTX := True; + end; + end + else + begin + if FHWPTTStartedTX then + begin + FHWPTTStartedTX := False; + SetMOX(False); + end; + end; + end; + end; +end; + procedure TRadioController.OnMicPacket(const Data: TMicDataPacket); // HW mic-стрим уходит в WDSP только если выбран источник Radio; иначе сэмплы // игнорируются (mic берётся со звуковой карты).