diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 385fe33..1a4c234 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -143,6 +143,21 @@ type FAlexEnabled: Boolean; FAlexConfig: TAlexSettings; // per-band antenna + routing config FStepAtten: Byte; // ADC0 step attenuator 0-31 dB + // ---- XVTR (Transverter) state ---- + // FXvtrEnable — устанавливает bit 0 байта 1400 HP-пакета: включает выход + // XVTR на трансивере (управляющий пин/реле). Обычно True, пока активный + // диапазон — трансвертер. + FXvtrEnable: Boolean; + // FXvtrRxAntOverride — если 1..3, использовать эту антенну вместо + // Alex.RxAnt[band] (per-XVTR RX antenna). 0 = не переопределять. + FXvtrRxAntOverride: Byte; + // FXvtrDisablePA — при TX подавляет бит 27 Alex (T/R relay): внутренний + // PA остаётся в RX-положении, TX-сигнал идёт через XVTR-IF. + FXvtrDisablePA: Boolean; + // FXvtrUseRxDDCIn — True если RX тракт XVTR подключён к XVTR DDC IN пину + // Alex (Alex bit 8 + bit 11). Эквивалент Alex.RxOnly=3, но включается + // динамически при выборе XVTR-band, не привязан к band-индексу. + FXvtrUseRxDDCIn: Boolean; FSendLock: TCriticalSection; // защита concurrent UDP sends @@ -194,6 +209,11 @@ type Transmitting, PAEnabled, AlexEnabled: Boolean); procedure SetStepAtten(dB: Byte); procedure SetAlexConfig(const A: TAlexSettings); + // Включение/настройка XVTR-режима. Все параметры применяются мгновенно + // (отправляется HP-пакет). Когда AEnable=False, XVTR-bit гасится, + // антенна и Alex routing возвращаются к per-band дефолту. + procedure SetXvtrMode(AEnable, AUseRxDDCIn, ADisablePA: Boolean; + ARxAnt: Byte); procedure SendFullHP; procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDUCIQ(const IData, QData: array of Integer); @@ -1095,9 +1115,20 @@ end; // Все биты active-high. Bit 27 = T/R relay (0=RX, 1=TX). // Биты 24-26: ANT1/ANT2/ANT3. Биты 20-23,29-31: LPF. Биты 1-12: HPF/BPF. // Биты 8-11: вторичный RX вход (XVTR/Ext1/Ext2/Bypass relay). +// +// XvtrRxAnt — override антенны для RX (1..3) если активен трансвертер; +// 0 = не переопределять, использовать Alex.RxAnt[band]. +// XvtrUseRxDDCIn — True: при RX подключить XVTR DDC IN (Alex bit 8 + bit 11). +// Применяется только в RX и только когда XvtrRxAnt активен. +// XvtrDisablePA — True: при TX НЕ устанавливать bit 27 (T/R relay): +// внутренний PA не активируется, IF-сигнал идёт по тракту +// XVTR без усиления. function CalcAlex0(RXFreqHz, TXFreqHz: Double; Transmitting, IsOrion2: Boolean; - const Alex: TAlexSettings): LongWord; + const Alex: TAlexSettings; + XvtrRxAnt: Byte = 0; + XvtrUseRxDDCIn: Boolean = False; + XvtrDisablePA: Boolean = False): LongWord; var txf: Double; BandIdx: Integer; @@ -1107,9 +1138,13 @@ begin // Bit 27 = T/R relay (0=RX, 1=TX). // Bit 18 = TxRx Status для Orion MkII 5.2 / 7000DLE / Saturn / G2. + // Если XvtrDisablePA=True — НЕ устанавливаем bit 27 (PA остаётся в RX + // положении). Bit 18 (TxRx status) всё равно устанавливаем — это просто + // индикатор состояния для логики платы, не управление реле. if Transmitting then begin - Result := Result or $08000000; // bit 27: T/R relay + if not XvtrDisablePA then + Result := Result or $08000000; // bit 27: T/R relay if IsOrion2 then Result := Result or $00040000; // bit 18: TxRx Status end; @@ -1155,9 +1190,14 @@ begin // Биты 24-26: ANT 1/2/3. Выбор зависит от диапазона и режима TX/RX. // Дефолт ANT1 — безопасен при любом состоянии. + // Если активен XVTR с RX antenna override (XvtrRxAnt 1..3) — он переопределяет + // RX антенну. TX антенна для XVTR всё равно берётся из Alex.TxAnt[band] — + // обычно та же что для основного HF-band за этой IF-частотой. BandIdx := FreqToBandIndex(RXFreqHz); if Transmitting then AntVal := Alex.TxAnt[BandIdx] + else if (XvtrRxAnt >= 1) and (XvtrRxAnt <= 3) then + AntVal := XvtrRxAnt else AntVal := Alex.RxAnt[BandIdx]; if AntVal < 1 then AntVal := 1; @@ -1169,6 +1209,8 @@ begin // Биты 8-11: вторичный RX вход (Bypass relay, Ext1, Ext2, XvtrDDC). // В режиме TX управляется глобальными флагами; в RX — per-band конфигом. + // Когда активен XVTR с XvtrUseRxDDCIn=True — переопределяем на XVTR DDC IN + // вне зависимости от Alex.RxOnly[band]. if Transmitting then begin if Alex.RxBypassOnTx then @@ -1177,6 +1219,11 @@ begin Result := Result or $00000200 // bit 9: Ext1 or $00000800; // bit 11: Bypass relay end + else if XvtrUseRxDDCIn then + begin + Result := Result or $00000100 // bit 8: XVTR DDC In + or $00000800; // bit 11: Bypass relay + end else begin case Alex.RxOnly[BandIdx] of @@ -1196,6 +1243,19 @@ begin if FConnected then SendFullHP; end; +procedure THPSDRNetwork.SetXvtrMode(AEnable, AUseRxDDCIn, ADisablePA: Boolean; + ARxAnt: Byte); +begin + FXvtrEnable := AEnable; + FXvtrUseRxDDCIn := AEnable and AUseRxDDCIn; + FXvtrDisablePA := AEnable and ADisablePA; + if AEnable and (ARxAnt >= 1) and (ARxAnt <= 3) then + FXvtrRxAntOverride := ARxAnt + else + FXvtrRxAntOverride := 0; + if FConnected then SendFullHP; +end; + procedure THPSDRNetwork.ConfigureDDCs(NumDDCs: Byte; SampleRate: Word; ADCSource: Byte); var @@ -1338,13 +1398,21 @@ begin if FAlexEnabled then begin Alex0 := CalcAlex0(FCurrentRXFreq, FCurrentTXFreq, - FIsTransmitting, IsOrion2, FAlexConfig); + FIsTransmitting, IsOrion2, FAlexConfig, + FXvtrRxAntOverride, FXvtrUseRxDDCIn, FXvtrDisablePA); Buf[1432] := (Alex0 shr 24) and $FF; Buf[1433] := (Alex0 shr 16) and $FF; Buf[1434] := (Alex0 shr 8) and $FF; Buf[1435] := Alex0 and $FF; end; + // XVTR enable bit (byte 1400 bit 0) + // openHPSDR Ethernet Protocol v4.3: byte 1400 — Transverter/audio enable. + // bit 0 = XVTR enable, bit 1 = Audio Codec enable (на платах с поддержкой). + // Audio codec бит оставляем 0 — мы IQ-mic не используем. + if FXvtrEnable then + Buf[1400] := Buf[1400] or $01; + // Step attenuators ADC0/ADC1 (bytes 1443/1442) // 0 dB = 0, max 31 dB Buf[1443] := FStepAtten; // ADC0 attenuation diff --git a/MainForm.pas b/MainForm.pas index 3d6c6a6..5e7a529 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -160,6 +160,8 @@ type FAudioInDevName: string; // текущее имя входного устройства (TX mic) FTXSettings: TTXSettings; // полный TX-конфиг (загружается per-MAC) FAlexSettings: TAlexSettings; // per-band antenna/routing config (Alex board) + FXvtrSettings: TXvtrSettings; // transverter slots (per-device) + FCurrentXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = active XVTR FWDSPReady: Boolean; // True когда WDSP открыт и работает // ---- State ---- @@ -317,6 +319,9 @@ type PanelBands: TPanel; BtnBand: array[0..BAND_COUNT-1] of TFlatButton; + // Динамические кнопки XVTR (создаются/удаляются по мере того, + // как пользователь enabled/disabled слоты в Settings → Transverter). + BtnXvtrBand: array[0..CFG_XVTR_COUNT-1] of TFlatButton; PanelMode: TPanel; BtnMode: array[0..MODE_COUNT-1] of TFlatButton; @@ -497,6 +502,7 @@ type procedure WebOnAttn(Idx: Integer); procedure WebOnTun(On_: Boolean); procedure WebOnMic(Samples: PSingle; Count: Integer); + procedure WebOnXvtrBand(Idx: Integer); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; @@ -504,6 +510,7 @@ type procedure SyncWebAGC; procedure SyncWebAGCTop; procedure SyncWebBand; + procedure SyncWebXvtrBand; procedure SyncWebSpan; procedure SyncWebVolume; procedure SyncWebWfAGC; @@ -562,6 +569,16 @@ type const SerBaud, SerDataBits, SerStopBits, SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer); procedure OnAlexSettingsChange(const A: TAlexSettings); + procedure OnXvtrSettingsChange(const X: TXvtrSettings); + procedure PushXvtrToWeb; + // XVTR helpers + function FindXvtrIdxForFreq(FreqHz: Double): Integer; + function XvtrTranslate(VisibleHz: Double): Double; + procedure ApplyXvtrToNetwork; + procedure RebuildXvtrButtons; + procedure BtnXvtrBandClick(Sender: TObject); + procedure ActivateXvtrBand(Idx: Integer); + procedure DeactivateXvtr; // ------------------------------------------------------------------------- procedure BtnWfAGCClick(Sender: TObject); procedure BtnWfNFClick(Sender: TObject); @@ -865,6 +882,7 @@ begin Result.WfAGCEnabled := FWfAGCEnabled; Result.WfNFEnabled := FWfNFEnabled; Result.LastBand := FCurrentBand; + Result.LastXvtr := FCurrentXvtr; Result.SampleRate := FSampleRate; // Display settings if FWDSPReady then @@ -918,6 +936,18 @@ end; procedure TMainForm.SaveCurrentBand; begin if not FDevConnected then Exit; + // В XVTR-режиме НЕ трогаем HF band-cache: FBandCache[FCurrentBand] + // относится к HF-диапазону, на котором пользователь был ДО активации + // трансвертера, и FVfoA сейчас на XVTR-частоте (144.x, 432.x и т.д.). + // Запись в FBandCache[FCurrentBand] стерла бы корректное HF-состояние. + // Вместо этого сохраняем LastFreq в XVTR-настройках. + if FCurrentXvtr >= 0 then + begin + if (FCurrentXvtr < CFG_XVTR_COUNT) then + FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA; + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); + Exit; + end; FBandCache[FCurrentBand] := MakeBandSettings; FSettings.SaveBand(FDevMAC, FCurrentBand, FBandCache[FCurrentBand]); end; @@ -997,6 +1027,10 @@ begin FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); if FDevConnected then FSettings.SaveTX(FDevMAC, FTXSettings); + // Запомнить LastFreq XVTR для следующего запуска + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then + FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA; + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); FSettings.Save; end; end; @@ -1107,7 +1141,9 @@ begin FWebServer.OnAttn := WebOnAttn; FWebServer.OnTun := WebOnTun; FWebServer.OnWebMic := WebOnMic; + FWebServer.OnXvtrBand := WebOnXvtrBand; FWebServer.Start; + PushXvtrToWeb; FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); FCATLastGlobal.CATTcpPort := 19090; for i := 0 to 3 do @@ -1175,9 +1211,11 @@ begin FNetwork.OnHPStatus := OnHPStatusCB; FNetwork.OnDDCIQ := OnDDCIQCB; FNetwork.OnMicPacket := OnMicPacketCB; - // Дефолты TX и Alex (на случай если устройство ещё не выбрано). + // Дефолты TX, Alex и XVTR (на случай если устройство ещё не выбрано). TSettingsManager.DefaultTX(FTXSettings); TSettingsManager.DefaultAlex(FAlexSettings); + TSettingsManager.DefaultXvtr(FXvtrSettings); + FCurrentXvtr := -1; BuildUI; StyleSpanButton(BtnSpan48k, FSampleRate = 48000); @@ -1242,7 +1280,7 @@ begin FMeterTimer.Enabled := False; FSpectrumTimer.Enabled := False; if FNetwork.Running then - FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); + FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); FNetwork.Disconnect; FNetwork.Free; FAudioOut.Close; @@ -1280,8 +1318,8 @@ begin begin if FRunning then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, 0, False, True, True); - FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True); + FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); FRunning := False; FTransmitting := False; if FWDSPReady then FDSPEngine.SetTXRun(False); @@ -1529,10 +1567,12 @@ begin LayoutTopVfoBlock; - // Band selector + // Band selector. Высота резервирует 2 дополнительных ряда под XVTR-кнопки + // (создаются динамически в RebuildXvtrButtons). В каждом ряду до 6 кнопок, + // итого до 12 XVTR-кнопок (наш CFG_XVTR_COUNT = 8 умещается с запасом). PanelBands := TPanel.Create(Self); PanelBands.Parent := PanelLeft; - PanelBands.SetBounds(0, Y, LEFT_W, 74); + PanelBands.SetBounds(0, Y, LEFT_W, 74 + 54); PanelBands.BevelOuter := bvNone; MakeLbl(PanelBands, 'BAND', 4, 2); W := (LEFT_W - 6) div 6; @@ -1545,8 +1585,11 @@ begin B.Tag := i; BtnBand[i] := B; end; + // XVTR кнопки создаются позже (в RebuildXvtrButtons), их Tag = -(idx+1) + for i := 0 to CFG_XVTR_COUNT - 1 do + BtnXvtrBand[i] := nil; - Inc(Y, 76); + Inc(Y, 76 + 54); // Mode selector PanelMode := TPanel.Create(Self); @@ -2669,7 +2712,7 @@ begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FRunning := False; - FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); + FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); StatusBar1.Panels[4].Text := 'ТРАНСИВЕР НЕДОСТУПЕН'; BtnMOX.Enabled := False; Exit; @@ -2680,7 +2723,7 @@ begin BtnStartStop.Caption := 'START'; StyleButton(BtnStartStop, False); FRunning := False; - FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); + FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); StatusBar1.Panels[4].Text := 'ПОТЕРЯ СВЯЗИ С ТРАНСИВЕРОМ'; BtnMOX.Enabled := False; Exit; @@ -3321,8 +3364,8 @@ begin // --- STOP --- if FRunning then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, 0, False, True, True); - FNetwork.SetRunAndFreq(False, FCenterFreq, FCenterFreq, 0); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), 0, False, True, True); + FNetwork.SetRunAndFreq(False, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), 0); FRunning := False; FTransmitting := False; if FWDSPReady then FDSPEngine.SetTXRun(False); @@ -3337,6 +3380,10 @@ begin SaveCurrentBand; FSettings.SaveGlobal(FDevMAC, MakeGlobalSettings); FSettings.SaveAlex(FDevMAC, FAlexSettings); + // XVTR LastFreq → актуальная видимая частота в момент остановки + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then + FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA; + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); end; FSettings.SaveStartupPreview(FVfoA, FVfoB, FSampleRate); FSettings.Save; @@ -3474,6 +3521,12 @@ begin // Alex-настройки per-device (антенны/маршрутизация) FSettings.LoadAlex(FDevMAC, FAlexSettings); FNetwork.SetAlexConfig(FAlexSettings); + // XVTR-настройки per-device (трансвертеры) + FSettings.LoadXvtr(FDevMAC, FXvtrSettings); + FCurrentXvtr := -1; + RebuildXvtrButtons; + ApplyXvtrToNetwork; + PushXvtrToWeb; FTXSpecRefLevel := FTXSettings.TXSpecRefLevel; FTXSpecRange := FTXSettings.TXSpecRange; if FTXSettings.TXSpecGridStep > 0 then @@ -3566,6 +3619,14 @@ begin end; RestoreBand(FCurrentBand); + // Восстанавливаем XVTR-режим, если он был активен при выходе. + // FCurrentBand уже загружен (HF-диапазон, на который пользователь зашёл + // до XVTR), его FBandCache не пострадает — ActivateXvtrBand только + // выставит FVfoA на XVTR-частоту и FCurrentXvtr. + if (G_Settings.LastXvtr >= 0) and (G_Settings.LastXvtr < CFG_XVTR_COUNT) + and FXvtrSettings.Entries[G_Settings.LastXvtr].Enabled then + ActivateXvtrBand(G_Settings.LastXvtr); + StatusBar1.Panels[0].Text := 'IP: ' + FNetwork.Device.IPAddress; StatusBar1.Panels[1].Text := 'Board: ' + BoardTypeName(FNetwork.Device.BoardType); @@ -3609,8 +3670,8 @@ begin // Сначала отправляем Run=1 — эмулятор/железо создают ddc_specific_thread // (порт 1025) и rx_thread (порт 1035+) только после получения HP Run=1. // DDC Specific и DUC Specific отправляем ПОСЛЕ, иначе порты ещё не открыты. - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, False, True, True); - FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, False, True, True); + FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel); FRunning := True; FRXStartTime := GetTickCount64; FRXLastPktTime := 0; @@ -3720,7 +3781,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; @@ -3732,7 +3793,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; @@ -3782,7 +3843,7 @@ begin if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -3804,7 +3865,7 @@ begin if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -3830,7 +3891,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; // Обновляем диапазон @@ -3914,6 +3975,15 @@ var Scrolled: Boolean; BandIdx: Integer; begin + // Клэмп в пределах XVTR-band — не даём VFO уйти за FreqBegin/FreqEnd + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled then + begin + if NewFreq < FXvtrSettings.Entries[FCurrentXvtr].FreqBegin then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqBegin); + if NewFreq > FXvtrSettings.Entries[FCurrentXvtr].FreqEnd then + NewFreq := Round(FXvtrSettings.Entries[FCurrentXvtr].FreqEnd); + end; FVfoA := NewFreq; Scrolled := False; @@ -3962,19 +4032,24 @@ begin // Обновляем VFO дисплей FreqDispA.Frequency := Round(FVfoA); - // Обновляем кнопку диапазона если частота перешла в другой диапазон - BandIdx := FreqToBandIdx(FVfoA); - if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + // Обновляем кнопку диапазона если частота перешла в другой диапазон. + // В XVTR-режиме HF band-кнопки не подсвечиваем (мы клэмпим VFO внутри + // XVTR-диапазона — FreqToBandIdx всегда вернёт -1 для VHF/UHF). + if FCurrentXvtr < 0 then begin - StyleButton(BtnBand[FCurrentBand], False); - FCurrentBand := BandIdx; - StyleButton(BtnBand[FCurrentBand], True); + BandIdx := FreqToBandIdx(FVfoA); + if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then + begin + StyleButton(BtnBand[FCurrentBand], False); + FCurrentBand := BandIdx; + StyleButton(BtnBand[FCurrentBand], True); + end; end; // DDC: передаём FCenterFreq (при CTUN OFF = FVfoA, при CTUN ON = фиксирован) if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -4011,7 +4086,7 @@ begin FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end @@ -4062,6 +4137,14 @@ end; procedure TMainForm.ApplyTUN(Active: Boolean); begin if Active = FTuning then Exit; + // Safety: те же блокировки что в ApplyMOX. TUN — это TX через WDSP PostGen, + // он тоже должен молчать на бэндах с DoNotTx или на RX-only XVTR. + if Active and FDevConnected and (FCurrentXvtr < 0) + and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then + Exit; + if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) + and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then + Exit; if Active then begin // Включаем тон ДО запуска TX-аудио, чтобы первые сэмплы уже шли с тоном @@ -4088,7 +4171,7 @@ begin FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -4131,7 +4214,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; BandIdx := FreqToBandIdx(FVfoB); @@ -4171,7 +4254,7 @@ begin end; if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpectrumDirty := True; @@ -4189,7 +4272,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpectrumDirty := True; @@ -4328,19 +4411,23 @@ var begin NewBand := (Sender as TFlatButton).Tag; if (NewBand < 0) or (NewBand >= BAND_COUNT) then Exit; - if NewBand = FCurrentBand then Exit; // уже на этом диапазоне + // Если в XVTR-режиме — DeactivateXvtr вернёт HF-состояние (RestoreBand + // на сохранённом FCurrentBand). Если NewBand совпадает с восстановленным + // FCurrentBand, дальше делать ничего не нужно. + if FCurrentXvtr >= 0 then + begin + DeactivateXvtr; + if NewBand = FCurrentBand then Exit; + end + else if NewBand = FCurrentBand then + Exit; - // Сохраняем настройки текущего диапазона перед сменой if FDevConnected then begin SaveCurrentBand; FSettings.Save; end; - - // Переключаем диапазон FCurrentBand := NewBand; - - // Восстанавливаем настройки нового диапазона RestoreBand(FCurrentBand); end; @@ -4421,7 +4508,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -4453,7 +4540,7 @@ begin if FWDSPReady then FDSPEngine.SetShift(0.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; FSpecView.InvalidateRulerCache; @@ -4490,9 +4577,13 @@ var WasTransmitting: Boolean; begin // Safety: block TX on bands marked DoNotTx in Alex config - if Active and FDevConnected + if Active and FDevConnected and (FCurrentXvtr < 0) and FAlexSettings.DoNotTx[EnsureRange(FCurrentBand, 0, 10)] then Exit; + // Safety: block TX on RX-only transverter + if Active and (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) + and FXvtrSettings.Entries[FCurrentXvtr].RXOnly then + Exit; WasTransmitting := FTransmitting; FTransmitting := Active; FDUCPendingCount := 0; @@ -4517,7 +4608,7 @@ begin if FTransmitting and FNetwork.Connected and FNetwork.Running then begin FNetwork.ClearDUCIQQueue; - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, True, True, True); FNetwork.SendFullHP; end; @@ -4563,7 +4654,7 @@ begin end; if (not FTransmitting) and FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.ClearDUCIQQueue; FNetwork.SendFullHP; @@ -4828,7 +4919,7 @@ begin FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then - FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin @@ -4967,12 +5058,40 @@ end; procedure TMainForm.SyncWebBand; begin if (FWebSyncInt < 0) or (FWebSyncInt >= BAND_COUNT) then Exit; - if FWebSyncInt = FCurrentBand then Exit; + // Выйти из XVTR при переходе на HF из web. DeactivateXvtr вернёт HF-состояние. + if FCurrentXvtr >= 0 then + begin + DeactivateXvtr; + if FWebSyncInt = FCurrentBand then Exit; + end + else if FWebSyncInt = FCurrentBand then + Exit; if FDevConnected then begin SaveCurrentBand; FSettings.Save; end; FCurrentBand := FWebSyncInt; RestoreBand(FCurrentBand); end; +procedure TMainForm.WebOnXvtrBand(Idx: Integer); +begin + FWebSyncInt := Idx; + FWebSyncM := SyncWebXvtrBand; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebXvtrBand; +begin + if FWebSyncInt < 0 then + begin + DeactivateXvtr; + Exit; + end; + if (FWebSyncInt >= CFG_XVTR_COUNT) then Exit; + if not FXvtrSettings.Entries[FWebSyncInt].Enabled then Exit; + if FWebSyncInt = FCurrentXvtr then Exit; + if FDevConnected then begin SaveCurrentBand; FSettings.Save; end; + ActivateXvtrBand(FWebSyncInt); +end; + procedure TMainForm.WebOnSpan(Hz: Integer); begin FWebSyncInt := Hz; @@ -5182,7 +5301,7 @@ begin if not FCTun then if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then - FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin @@ -5233,7 +5352,7 @@ begin else FDSPEngine.SetShift(FVfoB - FCenterFreq); end; if FNetwork.Connected and FNetwork.Running then - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FSpecView.InvalidateRulerCache; FSpectrumDirty := True; end; @@ -5322,26 +5441,53 @@ begin FDSPEngine.SetDriveLevel(TrkDrive.Position / 100.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; end; function TMainForm.CalcDriveByte: Byte; var - Cal: Double; - Pos: Integer; + Cal: Double; + Pos: Integer; + XvtrActive: Boolean; begin if (FCurrentBand >= 0) and (FCurrentBand < BAND_COUNT) and (FPABandCal[FCurrentBand] > 0.0) then Cal := FPABandCal[FCurrentBand] else Cal := 100.0; - // В режиме TUN используем отдельный per-device уровень (см. TTXSettings.TUNLevel), - // чтобы tune не зависел от Drive-слайдера. Per-band калибровка — общая для обоих. + + XvtrActive := (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled; + + // Pos — итоговый процент драйва 0..100 ДО per-band калибровки. + // + // Логика (как в Thetis): + // • Регулярный TX: + // HF → Pos = TrkDrive.Position + // XVTR → Pos = TrkDrive.Position × (XVTR.TXPower / 100) + // (TXPower работает как верхний предел драйва на трансверторе) + // • TUN (кнопка): + // HF → Pos = TUNLevel (TTXSettings.TUNLevel) + // XVTR, UseXVTRTunePower=False → Pos = TUNLevel (поведение как HF) + // XVTR, UseXVTRTunePower=True → Pos = XVTR.TXPower + // (галка ЗАМЕНЯЕТ TUNLevel на XVTR power — Thetis-семантика) if FTuning then - Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100) + begin + if XvtrActive and FXvtrSettings.UseXVTRTunePower then + Pos := EnsureRange(FXvtrSettings.Entries[FCurrentXvtr].TXPower, 0, 100) + else + Pos := EnsureRange(FTXSettings.TUNLevel, 0, 100); + end else - Pos := TrkDrive.Position; + begin + if XvtrActive then + Pos := Round(TrkDrive.Position + * FXvtrSettings.Entries[FCurrentXvtr].TXPower / 100.0) + else + Pos := TrkDrive.Position; + end; + Result := Round(Pos / 100.0 * Cal / 100.0 * 255.0); end; @@ -5392,7 +5538,7 @@ begin end; FDriveLevel := CalcDriveByte; if FNetwork.Connected and FNetwork.Running then - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, FTransmitting, True, True); + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); end; procedure TMainForm.OnTXSettingsChange(const T: TTXSettings); @@ -5434,7 +5580,7 @@ begin FDSPEngine.SetDriveLevel(FTXSettings.TUNLevel / 100.0); if FNetwork.Connected and FNetwork.Running then begin - FNetwork.UpdateState(FCenterFreq, ActiveTXFreqHz, FDriveLevel, + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), FDriveLevel, FTransmitting, True, True); FNetwork.SendFullHP; end; @@ -5460,6 +5606,251 @@ begin end; end; +// =========================================================================== +// XVTR (Transverter) integration +// =========================================================================== + +// Ищет XVTR слот, в чьё [FreqBegin..FreqEnd] попадает данная видимая частота. +// Возвращает индекс 0..CFG_XVTR_COUNT-1 или -1. +function TMainForm.FindXvtrIdxForFreq(FreqHz: Double): Integer; +var i: Integer; +begin + Result := -1; + for i := 0 to CFG_XVTR_COUNT - 1 do + with FXvtrSettings.Entries[i] do + if Enabled and (FreqHz >= FreqBegin) and (FreqHz <= FreqEnd) then + Exit(i); +end; + +// Транслирует видимую частоту в IF (то, что слышит/передаёт трансивер). +// Если активный XVTR не задан — возвращает входную частоту без изменений. +function TMainForm.XvtrTranslate(VisibleHz: Double): Double; +begin + Result := VisibleHz; + if (FCurrentXvtr < 0) or (FCurrentXvtr >= CFG_XVTR_COUNT) then Exit; + if not FXvtrSettings.Entries[FCurrentXvtr].Enabled then Exit; + // f_IF = f_visible - LOOffset + LOError + Result := VisibleHz + - FXvtrSettings.Entries[FCurrentXvtr].LOOffset + + FXvtrSettings.Entries[FCurrentXvtr].LOError; +end; + +// Передаёт XVTR-режим в сетевой слой. Вызывается при смене XVTR или его +// настроек. Управляет XVTR Enable bit, DDC IN routing, T/R relay suppression. +procedure TMainForm.ApplyXvtrToNetwork; +var + En: Boolean; + UseDDCIn: Boolean; + DisablePA: Boolean; + RxAnt: Byte; +begin + if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) and + FXvtrSettings.Entries[FCurrentXvtr].Enabled then + begin + En := True; + // XVTRRF=True означает что трансвертер использует HF тракт — не нужно + // переключаться на отдельный XVTR DDC IN на Alex. Default False → + // подключаем XVTR-IF к выделенному пину XVTR DDC IN. + UseDDCIn := not FXvtrSettings.Entries[FCurrentXvtr].XVTRRF; + DisablePA := FXvtrSettings.Entries[FCurrentXvtr].DisablePA; + RxAnt := FXvtrSettings.Entries[FCurrentXvtr].RXAntenna; + end + else + begin + En := False; UseDDCIn := False; DisablePA := False; RxAnt := 0; + end; + if FNetwork.Connected then + FNetwork.SetXvtrMode(En, UseDDCIn, DisablePA, RxAnt); +end; + +// Перестраивает динамические XVTR-кнопки в PanelBands. Удаляет старые, +// создаёт новые для enabled слотов в 3-м и 4-м ряду PanelBands. +procedure TMainForm.RebuildXvtrButtons; +const + BTN_H_LOC = 24; +var + i, Pos: Integer; + W: Integer; + B: TFlatButton; + Cap: string; +begin + // Уничтожаем все старые + for i := 0 to CFG_XVTR_COUNT - 1 do + if BtnXvtrBand[i] <> nil then + begin + BtnXvtrBand[i].Free; + BtnXvtrBand[i] := nil; + end; + if PanelBands = nil then Exit; + W := (PanelBands.Width - 6) div 6; + Pos := 0; // позиция среди enabled слотов + for i := 0 to CFG_XVTR_COUNT - 1 do + if FXvtrSettings.Entries[i].Enabled then + begin + if Pos >= 12 then Break; // 2 ряда по 6 кнопок + Cap := FXvtrSettings.Entries[i].ButtonText; + if Cap = '' then Cap := 'X' + IntToStr(i + 1); + B := TFlatButton.Create(Self); + B.Parent := PanelBands; + B.Caption := Cap; + B.Left := 2 + (Pos mod 6) * W; + B.Top := 16 + (2 + (Pos div 6)) * 27; + B.Width := W - 2; + B.Height := BTN_H_LOC; + B.OnClick := BtnXvtrBandClick; + B.Tag := -(i + 1); // отрицательное значение = XVTR индекс + BtnXvtrBand[i] := B; + StyleButton(B, i = FCurrentXvtr); + Inc(Pos); + end; +end; + +// Обработчик клика по XVTR-кнопке. Tag = -(idx+1). +procedure TMainForm.BtnXvtrBandClick(Sender: TObject); +var + TagVal, Idx: Integer; +begin + TagVal := (Sender as TFlatButton).Tag; + Idx := -TagVal - 1; + if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; + if not FXvtrSettings.Entries[Idx].Enabled then Exit; + if FCurrentXvtr = Idx then Exit; // уже на этом XVTR + // Сохраняем текущий band перед сменой (как BtnBandClick) + if FDevConnected then + begin + SaveCurrentBand; + FSettings.Save; + end; + ActivateXvtrBand(Idx); +end; + +// Активирует XVTR-band: устанавливает видимую частоту по LastFreq или +// середине [FreqBegin..FreqEnd], применяет network XVTR-режим. +procedure TMainForm.ActivateXvtrBand(Idx: Integer); +var + Vis: Double; + i: Integer; +begin + if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; + if not FXvtrSettings.Entries[Idx].Enabled then Exit; + FCurrentXvtr := Idx; + // Выбор начальной частоты: LastFreq если в диапазоне, иначе середина + Vis := FXvtrSettings.Entries[Idx].LastFreq; + if (Vis < FXvtrSettings.Entries[Idx].FreqBegin) or + (Vis > FXvtrSettings.Entries[Idx].FreqEnd) then + Vis := (FXvtrSettings.Entries[Idx].FreqBegin + + FXvtrSettings.Entries[Idx].FreqEnd) / 2.0; + // Отключаем кнопки HF band + for i := 0 to BAND_COUNT - 1 do + StyleButton(BtnBand[i], False); + // Подсвечиваем активную XVTR-кнопку + for i := 0 to CFG_XVTR_COUNT - 1 do + if BtnXvtrBand[i] <> nil then + StyleButton(BtnXvtrBand[i], i = Idx); + // Применяем XVTR режим к сети (XVTR enable bit + DDC IN + DisablePA + RX ant) + ApplyXvtrToNetwork; + // VFO на видимую частоту; FCenterFreq тоже visible (как и для HF — DDC переход + // в IF происходит только в момент UpdateState/SetRunAndFreq через XvtrTranslate). + FVfoA := Vis; + FCenterFreq := Vis; + FreqDispA.Frequency := Round(FVfoA); + // Сбросить CTUN — не имеет смысла в начальном состоянии XVTR-band + if FCTun then + begin + FCTun := False; + StyleButton(BtnCTun, False); + end; + if FWDSPReady then FDSPEngine.SetShift(0.0); + // Применяем драйв (если есть кастомный TX power для этого XVTR) + // Сделано на стороне ActiveTXFreqHz / TX path — здесь только обновляем дисплей. + // Отправляем в сеть с трансляцией + if FNetwork.Connected and FNetwork.Running then + begin + FNetwork.UpdateState(XvtrTranslate(FCenterFreq), XvtrTranslate(ActiveTXFreqHz), + FDriveLevel, FTransmitting, True, True); + FNetwork.SendFullHP; + end; + SyncSpecViewFreq; + FSpecView.DrawSpectrum; + PbSpectrum.Invalidate; + FSpecView.DrawWaterfall; + PbWaterfall.Invalidate; + if PbRuler <> nil then PbRuler.Invalidate; + PushXvtrToWeb; +end; + +// Деактивирует XVTR — возвращает к HF-режиму. +// FCurrentBand был сохранён неизменным во время XVTR (см. ActivateXvtrBand, +// ApplyVfoA, SaveCurrentBand) — он указывает на HF-диапазон, с которого +// пользователь зашёл в трансвертер. RestoreBand вернёт VFO/Mode/Filter/AGC +// /CTUN/Span этого диапазона и обновит подсветку HF band-кнопки. +procedure TMainForm.DeactivateXvtr; +var i: Integer; +begin + if FCurrentXvtr < 0 then Exit; + // Запоминаем LastFreq для bandstack + if FCurrentXvtr < CFG_XVTR_COUNT then + begin + FXvtrSettings.Entries[FCurrentXvtr].LastFreq := FVfoA; + if FDevConnected then + begin + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); + FSettings.Save; + end; + end; + FCurrentXvtr := -1; + // Сбрасываем подсветку XVTR-кнопок + for i := 0 to CFG_XVTR_COUNT - 1 do + if BtnXvtrBand[i] <> nil then + StyleButton(BtnXvtrBand[i], False); + ApplyXvtrToNetwork; + // Восстанавливаем HF band: VFO/Mode/Filter/AGC/CTUN/Span из кэша. + // RestoreBand сам обновит подсветку HF band-кнопки и сетевое состояние. + if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then + RestoreBand(FCurrentBand); + PushXvtrToWeb; +end; + +procedure TMainForm.OnXvtrSettingsChange(const X: TXvtrSettings); +begin + FXvtrSettings := X; + RebuildXvtrButtons; + // Если активный XVTR стал disabled — выйти из XVTR режима + if (FCurrentXvtr >= 0) and + ((FCurrentXvtr >= CFG_XVTR_COUNT) or + (not FXvtrSettings.Entries[FCurrentXvtr].Enabled)) then + DeactivateXvtr; + // Перепрошить XVTR в сеть (если параметры текущего изменились) + ApplyXvtrToNetwork; + PushXvtrToWeb; + if FDevConnected then + begin + FSettings.SaveXvtr(FDevMAC, FXvtrSettings); + FSettings.Save; + end; +end; + +procedure TMainForm.PushXvtrToWeb; +var + Arr: TWebXvtrArray; + i, Cnt: Integer; +begin + if FWebServer = nil then Exit; + Cnt := 0; + for i := 0 to CFG_XVTR_COUNT - 1 do + if FXvtrSettings.Entries[i].Enabled then Inc(Cnt); + SetLength(Arr, Cnt); + Cnt := 0; + for i := 0 to CFG_XVTR_COUNT - 1 do + if FXvtrSettings.Entries[i].Enabled then + begin + Arr[Cnt].Idx := i; + Arr[Cnt].Name := FXvtrSettings.Entries[i].ButtonText; + Inc(Cnt); + end; + FWebServer.SetXvtrBands(Arr, FCurrentXvtr); +end; + procedure TMainForm.TrkVolumeChange(Sender: TObject); begin FVolume := TrkVolume.Position; @@ -5745,11 +6136,13 @@ begin SF.OnThemeChange := SetLightTheme; SF.OnTXChange := OnTXSettingsChange; SF.OnAlexChange := OnAlexSettingsChange; + SF.OnXvtrChange := OnXvtrSettingsChange; end; SF := TSettingsForm(FSettingsForm); SF.LoadTXSettings(FTXSettings); SF.LoadAlexSettings(FAlexSettings, IfThen(FNetwork.Connected, FNetwork.Device.BoardType, FPendingBoardType)); + SF.LoadXvtrSettings(FXvtrSettings); SF.LoadPASettings(FPAMaxPower, FPABandCal); SF.LoadCATSettings( FCATLastGlobal.CATSerialEnabled, @@ -5996,7 +6389,7 @@ begin FCenterFreq := FVfoB; if FWDSPReady then FDSPEngine.SetShift(0.0); if FRunning then - FNetwork.SetRunAndFreq(True, FCenterFreq, FCenterFreq, FDriveLevel); + FNetwork.SetRunAndFreq(True, XvtrTranslate(FCenterFreq), XvtrTranslate(FCenterFreq), FDriveLevel); BandIdx := FreqToBandIdx(FVfoB); if (BandIdx >= 0) and (BandIdx <> FCurrentBand) then begin diff --git a/Settings.pas b/Settings.pas index 51cff0a..7336759 100644 --- a/Settings.pas +++ b/Settings.pas @@ -11,6 +11,7 @@ uses SysUtils, Classes, Math, fpJSON, jsonparser, jsonscanner; const SETTINGS_FILE = 'hpsdr_settings.json'; CFG_BAND_COUNT = 11; + CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов) CFG_BAND_DEFAULT_FREQ: array[0..CFG_BAND_COUNT-1] of Double = ( 1900000, 3750000, 5357000, 7100000, 10125000, 14200000, 18120000, 21200000, 24940000, 28500000, 50150000); @@ -103,6 +104,33 @@ type TXSpecGridStep: Double; // grid step in dB, e.g. 10 end; + // Transverter (XVTR) entry — один трансвертер. + // Видимая частота VFO в [FreqBegin..FreqEnd] транслируется в IF-частоту, + // которую слышит трансивер: f_IF = f_visible - LOOffset + LOError. + // Примеры: 2m XVTR с LO=116 MHz → visible 144.200 ⇒ IF 28.200 MHz. + TXvtrEntry = record + Enabled: Boolean; + ButtonText: string[15]; // подпись на band-кнопке (например, '2m', '70cm') + FreqBegin: Double; // Hz + FreqEnd: Double; // Hz + LOOffset: Double; // Hz, отнимается от видимой частоты для получения IF + LOError: Double; // Hz, корректировка LO (положительная — IF растёт) + RXGain: Double; // dB, компенсация усиления RX тракта + RXOnly: Boolean; // True — запрещает TX на этом трансверторе + TXPower: Integer; // 0..100 % (drive level специально для XVTR) + XVTRRF: Boolean; // True — трансвертер использует основной HF RF путь + // (а не отдельный XVTR DDC IN на Alex) + DisablePA: Boolean; // True — отключить внутренний PA при TX (XVTR имеет свой) + RXAntenna: Byte; // 0=по-умолчанию(основной band), 1=ANT1, 2=ANT2, 3=ANT3 + LastFreq: Double; // Hz — последняя tuned visible частота (BandStack) + end; + + TXvtrSettings = record + Entries: array[0..CFG_XVTR_COUNT-1] of TXvtrEntry; + // Use XVTR TUN power — кнопка TUN использует XVTR-specific power. + UseXVTRTunePower: Boolean; + end; + // Alex (Antenna + Filter board) settings per-device. // Хранятся в JSON-секции "alex" под MAC. // Безопасные дефолты: ANT1 для всех диапазонов, вторичный вход выключен. @@ -133,6 +161,7 @@ type WfAGCEnabled: Boolean; WfNFEnabled: Boolean; LastBand: Integer; + LastXvtr: Integer; // -1 = HF, 0..CFG_XVTR_COUNT-1 = активный трансвертер при выходе SampleRate: Integer; // глобальный — один для всех диапазонов WindowLeft: Integer; WindowTop: Integer; @@ -231,6 +260,10 @@ type class procedure DefaultAlex(out A: TAlexSettings); function LoadAlex(const MAC: array of Byte; out A: TAlexSettings): Boolean; procedure SaveAlex(const MAC: array of Byte; const A: TAlexSettings); + // XVTR (Transverter) settings per-device (JSON-секция "xvtr" под MAC). + class procedure DefaultXvtr(out X: TXvtrSettings); + function LoadXvtr(const MAC: array of Byte; out X: TXvtrSettings): Boolean; + procedure SaveXvtr(const MAC: array of Byte; const X: TXvtrSettings); // Тема — глобальная настройка, не привязана к устройству procedure SaveTheme(ALightTheme: Boolean); function LoadTheme: Boolean; @@ -285,6 +318,7 @@ begin G.Volume := 70; G.DriveLevel := 50; G.ActiveVfo := 0; G.NRMode := 0; G.NBMode := 0; G.SNBEnabled := False; G.ANFEnabled := False; G.AGCSlope := 0; G.AGCHangThreshold := 100; G.LastBand := 5; + G.LastXvtr := -1; G.WfAGCEnabled := True; G.WfNFEnabled := False; G.SampleRate := 192000; @@ -470,6 +504,7 @@ begin G.WfAGCEnabled := JB(GObj,'wf_agc_enabled',True); G.WfNFEnabled := JB(GObj,'wf_nf_enabled',False); G.LastBand := JI(GObj,'last_band',5); + G.LastXvtr := JI(GObj,'last_xvtr',-1); G.SampleRate := JI(GObj,'sample_rate',192000); // Display settings G.FFTSize := JI(GObj,'fft_size',131072); @@ -574,6 +609,7 @@ begin JW(O,'wf_agc_enabled',G.WfAGCEnabled); JW(O,'wf_nf_enabled',G.WfNFEnabled); JW(O,'last_band',G.LastBand); + JW(O,'last_xvtr',G.LastXvtr); JW(O,'sample_rate',G.SampleRate); // Display settings JW(O,'fft_size',G.FFTSize); @@ -994,6 +1030,113 @@ begin JW(O, 'enable_xvtr_hf', A.EnableXvtrHf); end; +// --------------------------------------------------------------------------- +// XVTR settings (per-device, JSON-секция "xvtr" под MAC) +// --------------------------------------------------------------------------- + +class procedure TSettingsManager.DefaultXvtr(out X: TXvtrSettings); +var i: Integer; +begin + FillChar(X, SizeOf(X), 0); + X.UseXVTRTunePower := False; + // Все слоты по умолчанию disabled. Первые два предзаполнены типичными + // значениями (2m с LO=116MHz, 70cm с LO=404MHz) — чтобы пользователь + // мог быстро включить. + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + X.Entries[i].Enabled := False; + X.Entries[i].ButtonText := ''; + X.Entries[i].FreqBegin := 0.0; + X.Entries[i].FreqEnd := 0.0; + X.Entries[i].LOOffset := 0.0; + X.Entries[i].LOError := 0.0; + X.Entries[i].RXGain := 0.0; + X.Entries[i].RXOnly := False; + X.Entries[i].TXPower := 100; + X.Entries[i].XVTRRF := False; + X.Entries[i].DisablePA := False; + X.Entries[i].RXAntenna := 0; + X.Entries[i].LastFreq := 0.0; + end; + // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) + X.Entries[0].ButtonText := '2m'; + X.Entries[0].FreqBegin := 144000000; + X.Entries[0].FreqEnd := 148000000; + X.Entries[0].LOOffset := 116000000; + X.Entries[0].LastFreq := 144300000; + X.Entries[1].ButtonText := '70cm'; + X.Entries[1].FreqBegin := 430000000; + X.Entries[1].FreqEnd := 440000000; + X.Entries[1].LOOffset := 404000000; + X.Entries[1].LastFreq := 432200000; +end; + +function TSettingsManager.LoadXvtr(const MAC: array of Byte; + out X: TXvtrSettings): Boolean; +var + MacStr: string; + DevObj, Root, EObj: TJSONObject; + i: Integer; +begin + DefaultXvtr(X); + MacStr := MacToStr(MAC); + Result := FRoot.Find(MacStr) <> nil; + if not Result then Exit; + DevObj := GetDevObj(MacStr); + if DevObj.Find('xvtr') = nil then Exit; + Root := EnsureObj(DevObj, 'xvtr'); + X.UseXVTRTunePower := JB(Root, 'use_tun_power', False); + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + if Root.Find('e' + IntToStr(i)) = nil then Continue; + EObj := EnsureObj(Root, 'e' + IntToStr(i)); + X.Entries[i].Enabled := JB(EObj, 'en', X.Entries[i].Enabled); + X.Entries[i].ButtonText := JS(EObj, 'name', X.Entries[i].ButtonText); + X.Entries[i].FreqBegin := JD(EObj, 'fbeg', X.Entries[i].FreqBegin); + X.Entries[i].FreqEnd := JD(EObj, 'fend', X.Entries[i].FreqEnd); + X.Entries[i].LOOffset := JD(EObj, 'lo_off', X.Entries[i].LOOffset); + X.Entries[i].LOError := JD(EObj, 'lo_err', X.Entries[i].LOError); + X.Entries[i].RXGain := JD(EObj, 'rx_gain', X.Entries[i].RXGain); + X.Entries[i].RXOnly := JB(EObj, 'rx_only', X.Entries[i].RXOnly); + X.Entries[i].TXPower := EnsureRange(JI(EObj, 'tx_pwr', X.Entries[i].TXPower), 0, 100); + X.Entries[i].XVTRRF := JB(EObj, 'xvtr_rf', X.Entries[i].XVTRRF); + X.Entries[i].DisablePA := JB(EObj, 'dis_pa', X.Entries[i].DisablePA); + X.Entries[i].RXAntenna := EnsureRange(JI(EObj, 'rx_ant', X.Entries[i].RXAntenna), 0, 3); + X.Entries[i].LastFreq := JD(EObj, 'last_freq', X.Entries[i].LastFreq); + // Валидация: LastFreq должен быть в [FreqBegin..FreqEnd] + if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or + (X.Entries[i].LastFreq > X.Entries[i].FreqEnd) then + X.Entries[i].LastFreq := (X.Entries[i].FreqBegin + X.Entries[i].FreqEnd) / 2.0; + end; +end; + +procedure TSettingsManager.SaveXvtr(const MAC: array of Byte; + const X: TXvtrSettings); +var + Root, EObj: TJSONObject; + i: Integer; +begin + Root := EnsureObj(GetDevObj(MacToStr(MAC)), 'xvtr'); + JW(Root, 'use_tun_power', X.UseXVTRTunePower); + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + EObj := EnsureObj(Root, 'e' + IntToStr(i)); + JW(EObj, 'en', X.Entries[i].Enabled); + JWS(EObj, 'name', X.Entries[i].ButtonText); + JW(EObj, 'fbeg', X.Entries[i].FreqBegin); + JW(EObj, 'fend', X.Entries[i].FreqEnd); + JW(EObj, 'lo_off', X.Entries[i].LOOffset); + JW(EObj, 'lo_err', X.Entries[i].LOError); + JW(EObj, 'rx_gain', X.Entries[i].RXGain); + JW(EObj, 'rx_only', X.Entries[i].RXOnly); + JW(EObj, 'tx_pwr', X.Entries[i].TXPower); + JW(EObj, 'xvtr_rf', X.Entries[i].XVTRRF); + JW(EObj, 'dis_pa', X.Entries[i].DisablePA); + JW(EObj, 'rx_ant', Integer(X.Entries[i].RXAntenna)); + JW(EObj, 'last_freq', X.Entries[i].LastFreq); + end; +end; + procedure TSettingsManager.SaveTheme(ALightTheme: Boolean); var O: TJSONObject; begin diff --git a/SettingsForm.pas b/SettingsForm.pas index bbb70ae..7ad0781 100644 --- a/SettingsForm.pas +++ b/SettingsForm.pas @@ -55,6 +55,7 @@ type const SerParity: array of Integer; TcpEnabled: Boolean; TcpPort: Integer) of object; TOnAlexChange = procedure(const A: TAlexSettings) of object; + TOnXvtrChange = procedure(const X: TXvtrSettings) of object; { TSettingsForm } TSettingsForm = class(TForm) @@ -200,6 +201,25 @@ type // Callback FOnAlexChange: TOnAlexChange; + // ---- XVTR (Transverter) tab controls ---- + FPageXvtr: TScrollBox; + FNavXvtr: TFlatButton; + FXvtrEn: array[0..CFG_XVTR_COUNT-1] of TCheckBox; + FXvtrName: array[0..CFG_XVTR_COUNT-1] of TEdit; + FXvtrBegin: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz + FXvtrEnd: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz + FXvtrLOOff: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // MHz + FXvtrLOErr: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // Hz (целые) + FXvtrRXGain: array[0..CFG_XVTR_COUNT-1] of TFloatSpinEdit; // dB + FXvtrRXOnly: array[0..CFG_XVTR_COUNT-1] of TCheckBox; + FXvtrPower: array[0..CFG_XVTR_COUNT-1] of TSpinEdit; // 0..100 + FXvtrRF: array[0..CFG_XVTR_COUNT-1] of TCheckBox; + FXvtrDisPA: array[0..CFG_XVTR_COUNT-1] of TCheckBox; + FXvtrRXAnt: array[0..CFG_XVTR_COUNT-1] of TComboBox; // 0=def..3=ANT3 + FChkXvtrTunPwr: TCheckBox; + FXvtrConfig: TXvtrSettings; + FOnXvtrChange: TOnXvtrChange; + // ---- CAT tab controls ---- FCATSerialEn: array[0..3] of TCheckBox; FCATSerialPort: array[0..3] of TEdit; @@ -240,6 +260,11 @@ type procedure FireAlexChange; procedure OnAlexAnyChange(Sender: TObject); procedure OnAlexRxOnlyChange(Sender: TObject); + procedure BuildXvtrTab; + procedure CollectXvtrFromUI; + procedure FireXvtrChange; + procedure OnXvtrAnyChange(Sender: TObject); + procedure UpdateXvtrRowEnabled(Idx: Integer); procedure CollectTXFromUI; procedure FireTXChange; procedure OnTXAnyChange(Sender: TObject); @@ -318,9 +343,11 @@ type TcpEnabled: Boolean; TcpPort: Integer); procedure LoadTXSettings(const T: TTXSettings); procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer); + procedure LoadXvtrSettings(const X: TXvtrSettings); property OnCATChange: TOnCATChange read FOnCATChange write FOnCATChange; property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange; + property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange; property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange; property OnDisplayChange: TOnDisplayParamChange read FOnDisplayChange write FOnDisplayChange; property OnWaterfallChange: TOnWaterfallParamChange read FOnWaterfallChange write FOnWaterfallChange; @@ -388,8 +415,8 @@ constructor TSettingsForm.Create(AOwner: TComponent); begin inherited CreateNew(AOwner); Caption := 'Settings'; - Width := 900; - Height := 660; + Width := 1180; + Height := 720; Position := poScreenCenter; BorderStyle := bsSizeable; Color := CLR_BG; @@ -540,6 +567,7 @@ begin FNavAdvanced := MakeNavButton('Advanced', 290); FNavCAT := MakeNavButton('CAT', 326); FNavAlex := MakeNavButton('Antenna / Alex', 362); + FNavXvtr := MakeNavButton('Transverter', 398); FContentPanel := TPanel.Create(Self); FContentPanel.Parent := Self; @@ -556,8 +584,10 @@ begin FPageAdvanced := MakeScrollPage; FPageCAT := MakeScrollPage; FPageAlex := MakeScrollPage; + FPageXvtr := MakeScrollPage; FAlexBoardType := 0; TSettingsManager.DefaultAlex(FAlexConfig); + TSettingsManager.DefaultXvtr(FXvtrConfig); BuildAudioTab; BuildDisplayTab; @@ -566,6 +596,7 @@ begin BuildPATab; BuildCATTab; BuildAlexTab; + BuildXvtrTab; FBtnClose := TFlatButton.Create(Self); FBtnClose.Parent := Self; @@ -595,7 +626,8 @@ begin else if Sender = FNavPA then SelectPage(FPagePA, FNavPA) else if Sender = FNavAdvanced then SelectPage(FPageAdvanced, FNavAdvanced) else if Sender = FNavCAT then SelectPage(FPageCAT, FNavCAT) - else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex); + else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex) + else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr); end; procedure TSettingsForm.SelectPage(APage: TWinControl; ANav: TFlatButton); @@ -609,6 +641,7 @@ begin FPageAdvanced.Visible := APage = FPageAdvanced; FPageCAT.Visible := APage = FPageCAT; FPageAlex.Visible := APage = FPageAlex; + FPageXvtr.Visible := APage = FPageXvtr; FNavAudio.Active := ANav = FNavAudio; FNavDisplay.Active := ANav = FNavDisplay; @@ -619,6 +652,7 @@ begin FNavAdvanced.Active := ANav = FNavAdvanced; FNavCAT.Active := ANav = FNavCAT; FNavAlex.Active := ANav = FNavAlex; + FNavXvtr.Active := ANav = FNavXvtr; end; // --------------------------------------------------------------------------- @@ -2531,4 +2565,310 @@ begin end; end; +// =========================================================================== +// XVTR (Transverter) tab +// =========================================================================== + +procedure TSettingsForm.BuildXvtrTab; +const + MARGIN = 22; + ROW_H = 30; + HDR_H = 30; // место под заголовок столбца (одна строка + отступ) + // X-координаты колонок (внутри Grp). Подобраны так чтобы помещаться + // в content-area формы (~880px при ширине 1080). + EN_X = 14; + NAME_X = 40; NAME_W = 104; // подпись на band-кнопке (макс. 15 символов) + BEG_X = 152; BEG_W = 80; // 3 десятичных, "144.000" + END_X = 240; END_W = 80; + LO_X = 328; LO_W = 90; // 6 десятичных, "116.000000" + ERR_X = 426; ERR_W = 70; // целое Hz + GAIN_X = 504; GAIN_W = 62; // 1 десятичная, dB + RXO_X = 574; RXO_W = 56; // chk центрирован + PWR_X = 638; PWR_W = 58; // целое % + RF_X = 704; RF_W = 56; + PA_X = 768; PA_W = 66; + ANT_X = 842; ANT_W = 78; // combo default/ANT1/ANT2/ANT3 + GRP_W = ANT_X + ANT_W + 14; // 934 +var + Grp, GrpOpts: TPanel; + i, Y, GrpY: Integer; + L: TLabel; + + function MakeFlt(AParent: TWinControl; AX, AY, AW: Integer; + AMin, AMax, AStep: Double; ADec: Integer): TFloatSpinEdit; + begin + Result := TFloatSpinEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Increment := AStep; + Result.DecimalPlaces := ADec; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnXvtrAnyChange; + end; + + function MakeInt(AParent: TWinControl; AX, AY, AW: Integer; + AMin, AMax: Integer): TSpinEdit; + begin + Result := TSpinEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.MinValue := AMin; + Result.MaxValue := AMax; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnXvtrAnyChange; + end; + + // Чекбокс без подписи (только квадрат). Подпись пишем отдельно над колонкой. + function MakeChk(AParent: TWinControl; AX, AY: Integer): TCheckBox; + begin + Result := TCheckBox.Create(Self); + Result.Parent := AParent; + Result.Caption := ''; + Result.SetBounds(AX, AY, 22, 20); + Result.Font.Color := CLR_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnXvtrAnyChange; + end; + + function MakeEdt(AParent: TWinControl; AX, AY, AW: Integer): TEdit; + begin + Result := TEdit.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.MaxLength := 15; + Result.OnChange := OnXvtrAnyChange; + end; + + function MakeCmb(AParent: TWinControl; AX, AY, AW: Integer): TComboBox; + begin + Result := TComboBox.Create(Self); + Result.Parent := AParent; + Result.SetBounds(AX, AY, AW, BTN_H); + Result.Style := csDropDownList; + Result.Color := CLR_INPUT; + Result.Font.Color := CLR_INPUT_TEXT; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + Result.OnChange := OnXvtrAnyChange; + end; + + // Заголовок столбца, центрирован по ширине AW от AX. + function MakeHdr(AParent: TWinControl; const Cap: string; + AX, AY, AW: Integer): TLabel; + begin + Result := TLabel.Create(Self); + Result.Parent := AParent; + Result.Caption := Cap; + Result.SetBounds(AX, AY, AW, 14); + Result.Alignment := taCenter; + Result.Font.Color := CLR_TEXTDIM; + Result.Font.Name := UI_FONT; + Result.Font.Size := 9; + end; + +begin + FLoading := True; + try + // --- Заголовок страницы --- + L := TLabel.Create(Self); + L.Parent := FPageXvtr; + L.Caption := 'Transverters'; + L.SetBounds(MARGIN, 18, 500, 28); + L.Font.Name := UI_FONT; L.Font.Size := 16; + L.Font.Style := [fsBold]; L.Font.Color := CLR_TEXT; + + L := TLabel.Create(Self); + L.Parent := FPageXvtr; + L.Caption := 'Configure up to 8 transverter (XVTR) bands. When a band is selected ' + + 'on the main form, the VFO shows the displayed (RF) frequency while ' + + 'the radio is internally tuned to the IF: f_IF = f_displayed − LO + LO Error.'; + L.SetBounds(MARGIN, 50, GRP_W, 36); + L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; + L.WordWrap := True; + + // --- Group "Slots" --- + GrpY := 100; + Grp := MakeGroupPanel(FPageXvtr, 'Slots', + MARGIN, GrpY, GRP_W, GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14); + + // Заголовки колонок (под GRP_TITLE_H + небольшой отступ) + Y := GRP_TITLE_H + 10; + MakeHdr(Grp, 'EN', EN_X - 2, Y, 28); + MakeHdr(Grp, 'Label', NAME_X, Y, NAME_W); + MakeHdr(Grp, 'Begin (MHz)',BEG_X, Y, BEG_W); + MakeHdr(Grp, 'End (MHz)', END_X, Y, END_W); + MakeHdr(Grp, 'LO (MHz)', LO_X, Y, LO_W); + MakeHdr(Grp, 'LO Error Hz',ERR_X, Y, ERR_W); + MakeHdr(Grp, 'RX Gain dB', GAIN_X, Y, GAIN_W); + MakeHdr(Grp, 'RX Only', RXO_X, Y, RXO_W); + MakeHdr(Grp, 'TX Pwr %', PWR_X, Y, PWR_W); + MakeHdr(Grp, 'XVTR RF', RF_X, Y, RF_W); + MakeHdr(Grp, 'Disable PA', PA_X, Y, PA_W); + MakeHdr(Grp, 'RX Antenna', ANT_X, Y, ANT_W); + + // Ряды слотов + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + Y := GRP_TITLE_H + HDR_H + i * ROW_H + 2; + // Чекбокс EN центрируем по 24px ширине EN-колонки + FXvtrEn[i] := MakeChk(Grp, EN_X + 5, Y + 2); + FXvtrName[i] := MakeEdt(Grp, NAME_X, Y, NAME_W); + FXvtrBegin[i] := MakeFlt(Grp, BEG_X, Y, BEG_W, 0.0, 50000.0, 0.001, 3); + FXvtrEnd[i] := MakeFlt(Grp, END_X, Y, END_W, 0.0, 50000.0, 0.001, 3); + FXvtrLOOff[i] := MakeFlt(Grp, LO_X, Y, LO_W, -50000.0, 50000.0, 0.001, 6); + FXvtrLOErr[i] := MakeFlt(Grp, ERR_X, Y, ERR_W, -99999.0, 99999.0, 1.0, 0); + FXvtrRXGain[i]:= MakeFlt(Grp, GAIN_X, Y, GAIN_W, -60.0, 60.0, 0.1, 1); + // Чекбокс RX Only центрируем + FXvtrRXOnly[i]:= MakeChk(Grp, RXO_X + (RXO_W - 22) div 2, Y + 2); + FXvtrPower[i] := MakeInt(Grp, PWR_X, Y, PWR_W, 0, 100); + FXvtrRF[i] := MakeChk(Grp, RF_X + (RF_W - 22) div 2, Y + 2); + FXvtrDisPA[i] := MakeChk(Grp, PA_X + (PA_W - 22) div 2, Y + 2); + FXvtrRXAnt[i] := MakeCmb(Grp, ANT_X, Y, ANT_W); + FXvtrRXAnt[i].Items.Add('default'); + FXvtrRXAnt[i].Items.Add('ANT1'); + FXvtrRXAnt[i].Items.Add('ANT2'); + FXvtrRXAnt[i].Items.Add('ANT3'); + FXvtrRXAnt[i].ItemIndex := 0; + FXvtrEn[i].Tag := i; + FXvtrEn[i].OnClick := OnXvtrAnyChange; + end; + + // --- Group "Options" --- + GrpY := GrpY + GRP_TITLE_H + HDR_H + CFG_XVTR_COUNT * ROW_H + 14 + 18; + GrpOpts := MakeGroupPanel(FPageXvtr, 'Options', + MARGIN, GrpY, GRP_W, GRP_TITLE_H + 36); + FChkXvtrTunPwr := TCheckBox.Create(Self); + FChkXvtrTunPwr.Parent := GrpOpts; + FChkXvtrTunPwr.Caption := 'Use XVTR power for TUN ' + + '(TUN button uses the per-slot TX Power instead of the global TUN Level)'; + FChkXvtrTunPwr.SetBounds(14, GRP_TITLE_H + 8, GRP_W - 28, 20); + FChkXvtrTunPwr.Font.Color := CLR_TEXT; + FChkXvtrTunPwr.Font.Name := UI_FONT; + FChkXvtrTunPwr.Font.Size := 9; + FChkXvtrTunPwr.OnChange := OnXvtrAnyChange; + + // --- Подсказка про колонки внизу --- + L := TLabel.Create(Self); + L.Parent := FPageXvtr; + L.Caption := + 'EN — enable the slot. Label — band button caption on the main form. ' + + 'Begin/End — VFO range (displayed frequency). LO — local oscillator (subtracted from VFO to get IF). ' + + 'LO Error — fine correction in Hz. RX Gain — gain offset of the transverter RX side. ' + + 'RX Only — block TX on this slot. TX Pwr — drive limit (used as TUN level when "Use XVTR power for TUN" is set). ' + + 'XVTR RF — transverter uses the main HF RF path (do NOT engage Alex XVTR DDC IN pin). ' + + 'Disable PA — keep the internal PA in RX position during TX (XVTR has its own amplifier). ' + + 'RX Antenna — override the per-band antenna for this slot.'; + L.SetBounds(MARGIN, GrpY + GRP_TITLE_H + 36 + 16, GRP_W, 96); + L.Font.Name := UI_FONT; L.Font.Size := 9; L.Font.Color := CLR_TEXTDIM; + L.WordWrap := True; + + // Применяем начальное состояние disabled-строк + for i := 0 to CFG_XVTR_COUNT - 1 do + UpdateXvtrRowEnabled(i); + finally + FLoading := False; + end; +end; + +procedure TSettingsForm.UpdateXvtrRowEnabled(Idx: Integer); +var En: Boolean; +begin + if (Idx < 0) or (Idx >= CFG_XVTR_COUNT) then Exit; + if FXvtrEn[Idx] = nil then Exit; + En := FXvtrEn[Idx].Checked; + if FXvtrName[Idx] <> nil then FXvtrName[Idx].Enabled := En; + if FXvtrBegin[Idx] <> nil then FXvtrBegin[Idx].Enabled := En; + if FXvtrEnd[Idx] <> nil then FXvtrEnd[Idx].Enabled := En; + if FXvtrLOOff[Idx] <> nil then FXvtrLOOff[Idx].Enabled := En; + if FXvtrLOErr[Idx] <> nil then FXvtrLOErr[Idx].Enabled := En; + if FXvtrRXGain[Idx]<> nil then FXvtrRXGain[Idx].Enabled:= En; + if FXvtrRXOnly[Idx]<> nil then FXvtrRXOnly[Idx].Enabled:= En; + if FXvtrPower[Idx] <> nil then FXvtrPower[Idx].Enabled := En; + if FXvtrRF[Idx] <> nil then FXvtrRF[Idx].Enabled := En; + if FXvtrDisPA[Idx] <> nil then FXvtrDisPA[Idx].Enabled := En; + if FXvtrRXAnt[Idx] <> nil then FXvtrRXAnt[Idx].Enabled := En; +end; + +procedure TSettingsForm.CollectXvtrFromUI; +var i: Integer; +begin + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + if FXvtrEn[i] <> nil then FXvtrConfig.Entries[i].Enabled := FXvtrEn[i].Checked; + if FXvtrName[i] <> nil then FXvtrConfig.Entries[i].ButtonText := Copy(FXvtrName[i].Text, 1, 15); + if FXvtrBegin[i] <> nil then FXvtrConfig.Entries[i].FreqBegin := FXvtrBegin[i].Value * 1.0e6; + if FXvtrEnd[i] <> nil then FXvtrConfig.Entries[i].FreqEnd := FXvtrEnd[i].Value * 1.0e6; + if FXvtrLOOff[i] <> nil then FXvtrConfig.Entries[i].LOOffset := FXvtrLOOff[i].Value * 1.0e6; + if FXvtrLOErr[i] <> nil then FXvtrConfig.Entries[i].LOError := FXvtrLOErr[i].Value; + if FXvtrRXGain[i]<> nil then FXvtrConfig.Entries[i].RXGain := FXvtrRXGain[i].Value; + if FXvtrRXOnly[i]<> nil then FXvtrConfig.Entries[i].RXOnly := FXvtrRXOnly[i].Checked; + if FXvtrPower[i] <> nil then FXvtrConfig.Entries[i].TXPower := FXvtrPower[i].Value; + if FXvtrRF[i] <> nil then FXvtrConfig.Entries[i].XVTRRF := FXvtrRF[i].Checked; + if FXvtrDisPA[i] <> nil then FXvtrConfig.Entries[i].DisablePA := FXvtrDisPA[i].Checked; + if (FXvtrRXAnt[i]<> nil) and (FXvtrRXAnt[i].ItemIndex >= 0) then + FXvtrConfig.Entries[i].RXAntenna := Byte(FXvtrRXAnt[i].ItemIndex); + end; + if FChkXvtrTunPwr <> nil then + FXvtrConfig.UseXVTRTunePower := FChkXvtrTunPwr.Checked; +end; + +procedure TSettingsForm.FireXvtrChange; +var i: Integer; +begin + if FLoading then Exit; + CollectXvtrFromUI; + // Перерисовать enabled-состояние строк (если изменилась галка En) + for i := 0 to CFG_XVTR_COUNT - 1 do + UpdateXvtrRowEnabled(i); + if Assigned(FOnXvtrChange) then + FOnXvtrChange(FXvtrConfig); +end; + +procedure TSettingsForm.OnXvtrAnyChange(Sender: TObject); +begin + FireXvtrChange; +end; + +procedure TSettingsForm.LoadXvtrSettings(const X: TXvtrSettings); +var i: Integer; +begin + FLoading := True; + try + FXvtrConfig := X; + for i := 0 to CFG_XVTR_COUNT - 1 do + begin + if FXvtrEn[i] <> nil then FXvtrEn[i].Checked := X.Entries[i].Enabled; + if FXvtrName[i] <> nil then FXvtrName[i].Text := X.Entries[i].ButtonText; + if FXvtrBegin[i] <> nil then FXvtrBegin[i].Value := X.Entries[i].FreqBegin / 1.0e6; + if FXvtrEnd[i] <> nil then FXvtrEnd[i].Value := X.Entries[i].FreqEnd / 1.0e6; + if FXvtrLOOff[i] <> nil then FXvtrLOOff[i].Value := X.Entries[i].LOOffset / 1.0e6; + if FXvtrLOErr[i] <> nil then FXvtrLOErr[i].Value := X.Entries[i].LOError; + if FXvtrRXGain[i]<> nil then FXvtrRXGain[i].Value := X.Entries[i].RXGain; + if FXvtrRXOnly[i]<> nil then FXvtrRXOnly[i].Checked:= X.Entries[i].RXOnly; + if FXvtrPower[i] <> nil then FXvtrPower[i].Value := X.Entries[i].TXPower; + if FXvtrRF[i] <> nil then FXvtrRF[i].Checked := X.Entries[i].XVTRRF; + if FXvtrDisPA[i] <> nil then FXvtrDisPA[i].Checked := X.Entries[i].DisablePA; + if FXvtrRXAnt[i] <> nil then + FXvtrRXAnt[i].ItemIndex := EnsureRange(X.Entries[i].RXAntenna, 0, 3); + UpdateXvtrRowEnabled(i); + end; + if FChkXvtrTunPwr <> nil then FChkXvtrTunPwr.Checked := X.UseXVTRTunePower; + finally + FLoading := False; + end; +end; + end. diff --git a/WebPageHtml.pas b/WebPageHtml.pas index d044c99..b84a4af 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -374,8 +374,21 @@ begin 'const BAND_N=["160m","80m","60m","40m","30m","20m","17m","15m","12m","10m","6m"];' + 'const BAND_F=[1900000,3750000,5357000,7100000,10125000,14200000,18120000,21200000,24940000,28500000,50150000];' + - '(function(){const s=q("bandSel");BAND_N.forEach((n,i)=>{const o=document.createElement("option");o.value=i;o.textContent=n;s.appendChild(o);});s.onchange=()=>ws2({cmd:"band",idx:+s.value});})();' + - 'function updBand(vfo){let bi=0,bd=1e15;BAND_F.forEach((f,i)=>{const d=Math.abs((vfo||0)-f);if(d используем + // значения "h" для HF и "x" для XVTR, чтобы различать. + 'let XVTR_BANDS=[];let XVTR_CUR=-1;' + + 'function rebuildBandSel(){const s=q("bandSel");if(!s)return;' + + 'const saved=s.value;s.innerHTML="";' + + 'BAND_N.forEach((n,i)=>{const o=document.createElement("option");o.value="h"+i;o.textContent=n;s.appendChild(o);});' + + 'XVTR_BANDS.forEach(xb=>{const o=document.createElement("option");o.value="x"+xb.idx;o.textContent=xb.name||("X"+(xb.idx+1));s.appendChild(o);});' + + // Восстановить выбор: предпочитаем "current XVTR"; иначе сохраним прежнее + 'if(XVTR_CUR>=0)s.value="x"+XVTR_CUR; else if(saved)s.value=saved;' + + '}' + + '(function(){const s=q("bandSel");BAND_N.forEach((n,i)=>{const o=document.createElement("option");o.value="h"+i;o.textContent=n;s.appendChild(o);});' + + 's.onchange=()=>{const v=s.value;if(!v)return;if(v[0]==="x")ws2({cmd:"xvtr_band",idx:+v.slice(1)});else if(v[0]==="h")ws2({cmd:"band",idx:+v.slice(1)});};' + + '})();' + + 'function updBand(vfo){if(XVTR_CUR>=0){q("bandSel").value="x"+XVTR_CUR;return;}let bi=0,bd=1e15;BAND_F.forEach((f,i)=>{const d=Math.abs((vfo||0)-f);if(d=0)s.value="x"+XVTR_CUR;}' + + '}' + // Обновляем localCenter/localSpan из сервера только когда пользователь не тянет 'if(!dragging&&Date.now()-wheelActiveTs>300){' + // В CTUN без DUP: TX-спектр WDSP центрирован по vfo_a_hz, а не по DDC LO. diff --git a/WebServer.pas b/WebServer.pas index a55bd4a..0ff60a1 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -136,6 +136,15 @@ type TWebCmdFreqA = procedure(Hz: Double) of object; TWebCmdAttn = procedure(Idx: Integer) of object; TWebCmdTun = procedure(On_: Boolean) of object; + // XVTR-band: web клиент кликнул кнопку трансвертера (Idx 0..CFG_XVTR_COUNT-1). + // Idx=-1 — выход в HF. + TWebCmdXvtrBand = procedure(Idx: Integer) of object; + // Один XVTR-слот для статуса (для отображения в web-bandSel). + TWebXvtrInfo = record + Idx: Integer; + Name: string; + end; + TWebXvtrArray = array of TWebXvtrInfo; TWebMicCB = procedure(Samples: PSingle; Count: Integer) of object; // ── Главный класс сервера ───────────────────────────────────────────────── @@ -233,6 +242,9 @@ type FOnTun: TWebCmdTun; FTuning: Boolean; FDuplex: Boolean; + FXvtrBands: TWebXvtrArray; // список enabled XVTR (для web-UI) + FCurrentXvtr: Integer; // -1 = HF, иначе индекс активного XVTR + FOnXvtrBand: TWebCmdXvtrBand; FWebClientActive: Boolean; @@ -286,7 +298,9 @@ type property OnFilter: TWebCmdFilter read FOnFilter write FOnFilter; property OnAGC: TWebCmdAGC read FOnAGC write FOnAGC; property OnAGCTop: TWebCmdAGCTop read FOnAGCTop write FOnAGCTop; - property OnBand: TWebCmdBand read FOnBand write FOnBand; + property OnBand: TWebCmdBand read FOnBand write FOnBand; + property OnXvtrBand: TWebCmdXvtrBand read FOnXvtrBand write FOnXvtrBand; + procedure SetXvtrBands(const ABands: TWebXvtrArray; ACurrent: Integer); property OnSpan: TWebCmdSpan read FOnSpan write FOnSpan; property OnVolume: TWebCmdVolume read FOnVolume write FOnVolume; property OnWfAGC: TWebCmdWfAGC read FOnWfAGC write FOnWfAGC; @@ -405,6 +419,22 @@ begin FVolume := 70; FSMeter := -120; FBandIdx := 5; + FCurrentXvtr := -1; + SetLength(FXvtrBands, 0); +end; + +procedure TWebServer.SetXvtrBands(const ABands: TWebXvtrArray; ACurrent: Integer); +var i: Integer; +begin + FStateLock.Enter; + try + SetLength(FXvtrBands, Length(ABands)); + for i := 0 to High(ABands) do + FXvtrBands[i] := ABands[i]; + FCurrentXvtr := ACurrent; + finally + FStateLock.Leave; + end; end; destructor TWebServer.Destroy; @@ -946,6 +976,12 @@ begin FStateLock.Enter; FBandIdx := Idx; FStateLock.Leave; if Assigned(FOnBand) then FOnBand(Idx); end + else if Cmd = 'xvtr_band' then + begin + // Idx 0..CFG_XVTR_COUNT-1 — активировать XVTR; -1 — выйти в HF + Idx := JsonGetInt(Json, 'idx', -1); + if Assigned(FOnXvtrBand) then FOnXvtrBand(Idx); + end else if Cmd = 'span' then begin HzI := JsonGetInt(Json, 'hz', Round(FSpanHz)); @@ -1170,11 +1206,23 @@ const ('LSB','USB','DSB','CWL','CWU','FM','AM','SAM'); var FS: TFormatSettings; + XvtrJson: string; + i: Integer; begin FS := DefaultFormatSettings; FS.DecimalSeparator := '.'; FStateLock.Enter; try + // Сборка массива xvtr_bands: [{"idx":0,"name":"2m"},...] + XvtrJson := '['; + for i := 0 to High(FXvtrBands) do + begin + if i > 0 then XvtrJson := XvtrJson + ','; + XvtrJson := XvtrJson + + Format('{"idx":%d,"name":"%s"}', + [FXvtrBands[i].Idx, FXvtrBands[i].Name], FS); + end; + XvtrJson := XvtrJson + ']'; Result := Format( '{"vfo_a_hz":%.0f,"vfo_b_hz":%.0f,"active_vfo":%d,' + '"mode":%d,"mode_name":"%s",' + @@ -1184,7 +1232,8 @@ begin '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"running":%s,"mute":%s,"ctun":%s,' + '"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%s,' + - '"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s,"dup":%s}', + '"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s,"dup":%s,' + + '"xvtr_current":%d,"xvtr_bands":%s}', [FFreq, FVfoB, FActiveVfo, FMode, MODE_N[FMode mod 8], FFilterIdx, FFilterBW, @@ -1207,7 +1256,8 @@ begin FDriveLevel, FAttnIdx, BoolToStr(FTuning, 'true', 'false'), - BoolToStr(FDuplex, 'true', 'false') + BoolToStr(FDuplex, 'true', 'false'), + FCurrentXvtr, XvtrJson ], FS); finally FStateLock.Leave;