add trancverter

This commit is contained in:
2026-05-14 20:24:50 +03:00
parent aea9f02df8
commit 020b9422e0
6 changed files with 1083 additions and 66 deletions
+143
View File
@@ -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