TX: фиксированный sample rate, per-device grid, CTUN-aware overlay, alpha-blend filter bands

Декаплинг TX от RX sample rate (как в Thetis):
- WDSPEngine.pas: константа TX_SAMPLE_RATE=192000, поля FTXSampleRate/FTXOutBufSize.
  TXA OpenChannel теперь использует FTXSampleRate как out_rate, TX-анализатор
  через SetDisplaySampleRate(TX_DISP_ID, FTXSampleRate). ProcessTXBlock работает
  с FTXOutBufSize вместо FBufSize. Public TXSampleRate для MainForm.
- MainForm.SendDUCSpecificFromSettings: DUC0Rate теперь берётся из
  FDSPEngine.TXSampleRate, а не из RX-настроек — TX всегда 192k независимо от
  выбранного RX-rate (48/96/192/384k).

DUC Specific cache + keepalive resend (HPSDRNetwork.pas):
- FCachedDUCSpec/FCachedDUCValid параллельно DDC-кэшу.
- KeepAlive thread в первые 5s после Run=1 переотправляет и DDC, и DUC
  Specific — устраняет гонку, когда DUC настройки терялись железом.
- Disconnect сбрасывает оба valid-флага.

Per-device TX grid settings (Thetis-style):
- Settings.pas: TXSpecRefLevel/TXSpecRange/TXSpecGridStep в TTXSettings
  (defaults 0.0/80.0/10.0). LoadTX/SaveTX — ключи tx_spec_ref_level/range/grid_step.
- SettingsForm.pas: новая группа \"TX Grid\" во вкладке Transmit
  (RefLevel edit, Range edit, GridStep combo). CollectTXFromUI читает значения,
  LoadTXSettings заполняет UI.
- MainForm.pas: поля FTXSpecRefLevel/Range/GridStep, ApplySpecViewGridFromState
  пушит в FSpecView либо TX, либо RX параметры в зависимости от FTransmitting.
  ApplyGridParams (RX-путь) уважает FTransmitting и не перебивает TX-grid.
  OnTXSettingsChange при активном TX моментально применяет новый grid.

TX-aware spectrum/waterfall с CTUN-выравниванием (SpectrumView.pas):
- Поля FTXMode/FTXFreq/FTXSpanHz/FTXVfoIndex. TXSpanHz=192000 при создании.
- DrawSpectrum/DrawWaterfall в FTXMode мапят данные TX-анализатора (192k вокруг
  FTXFreq) в RX-координаты FCenterFreq/FSpanHz. Формула:
    freq = FCenterFreq - FSpanHz/2 + x*FSpanHz/W
    idx  = (freq-FTXFreq + TXSpanHz/2)/TXSpanHz * (SrcCount-1)
  Вне диапазона — спектр уходит в -200 dB; водопад копирует пиксель из
  предыдущей строки (FWfPixels[X+W]) — вместо чёрной полосы продолжает
  скроллиться старая RX-картинка.
- Эффект: при CTUN ON TX-сигнал рисуется на полосе фильтра (на VFO), а не в
  центре. При RX rate ≠ 192k частотная сетка совпадает между RX и TX
  автоматически (ruler всегда в RX-координатах).
- MainForm.SyncSpecViewFreq/ApplyMOX обновляют TXFreq/TXVfoIndex/TXMode при
  каждом PTT и смене VFO; учитывается split (FSplitTxB → TXVfoIndex=1).

Alpha-blended filter bands (визуальный feedback TX/RX):
- SpectrumView: PixelFormat:=pf32bit, процедура BlendBand через ScanLine[Y]
  внутри BeginUpdate/EndUpdate. Альфа-смешение по формуле
  (a*src + (255-a)*dst) / 255 для каждого канала BGR.
- В FTXMode полоса фильтра становится полупрозрачно красной. Если split
  (FTXVfoIndex<>FActiveVfo): на RX-частоте полоса зелёная, на TX-частоте —
  красная (CalcFilterBandX вычисляет границы для произвольной VFO-freq).
- При FTXMode=False — старое поведение (сплошная заливка SpecFilter).

Build: lazbuild --ws=qt6 ewsdr.lpi — clean, 0 errors.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 14:17:49 +03:00
co-authored by Claude Opus 4.7
parent ce97ee486c
commit 96d7e0c759
6 changed files with 400 additions and 68 deletions
+21 -8
View File
@@ -121,9 +121,13 @@ type
FPortDUCIQ: Word;
FDirectIP: string; // для unicast discovery
// Кэш DDC Specific — keepalive повторно шлёт первые 5 сек после старта
// Кэш DDC/DUC Specific — keepalive повторно шлёт первые 5 сек после старта.
// duc_specific_thread эмулятора биндится на 1026 только после HP Run=1,
// поэтому первый DUC Specific может прийти раньше bind() и быть потерян.
FCachedDDCSpec: TDDCSpecificPacket;
FCachedDDCValid: Boolean;
FCachedDUCSpec: TDUCSpecificPacket;
FCachedDUCValid: Boolean;
FResendCount: Integer;
// Текущее состояние для построения HP пакетов
@@ -403,14 +407,18 @@ begin
if not FNet.FConnected then Continue;
// Отправляем полный HP с частотами и ALEX — как piHPSDR
FNet.SendFullHP;
// Повторно отправляем DDC Specific первые 5 сек после Run=1 (каждые 500 мс).
// ddc_specific_thread на эмуляторе/железе стартует ПОСЛЕ получения Run=1,
// поэтому первый ConfigureDDCs может прийти раньше чем порт 1025 откроется.
if FNet.FRunning and FNet.FCachedDDCValid then
// Повторно отправляем DDC/DUC Specific первые 5 сек после Run=1 (каждые 500 мс).
// ddc_specific_thread / duc_specific_thread на эмуляторе/железе стартуют
// ПОСЛЕ получения Run=1, поэтому первые ConfigureDDCs / SendDUCSpecific
// могут прийти раньше чем порты 1025/1026 откроются.
if FNet.FRunning then
begin
Inc(FNet.FResendCount);
if (FNet.FResendCount <= 100) and (FNet.FResendCount mod 10 = 2) then
FNet.SendDDCSpecific(FNet.FCachedDDCSpec);
begin
if FNet.FCachedDDCValid then FNet.SendDDCSpecific(FNet.FCachedDDCSpec);
if FNet.FCachedDUCValid then FNet.SendDUCSpecific(FNet.FCachedDUCSpec);
end;
end;
end;
end;
@@ -715,8 +723,10 @@ begin
FRunning := False;
StopThreads;
DoCloseSocket(FSocket);
FConnected := False;
FDevice.Valid := False;
FConnected := False;
FDevice.Valid := False;
FCachedDDCValid := False;
FCachedDUCValid := False;
end;
procedure THPSDRNetwork.StartThreads;
@@ -940,6 +950,9 @@ begin
B := Pkt;
PackSeqBytes(B.Seq, NextSeq(FSeqDUCSpec));
DoSendTo(FSocket, B, SizeOf(B), FDevice.IPAddress, FPortDUCSpec);
// Кэш для пере-отправки в keepalive (см. ResendThread)
FCachedDUCSpec := Pkt;
FCachedDUCValid := True;
end;
procedure THPSDRNetwork.SendHighPriority(const Pkt: THighPriorityPacket);