feat(oc): OC Control — Open Collector выходы Penny/Alex (эталон Thetis)

Byte 1401 High Priority packet: per-band (HF 160m-6m) и per-slot (XVTR)
RX/TX-маски на 7 пинов + TX pin action (MOX/TUNE/2TON и комбинации).
Только openHPSDR — у Pluto/AD936x такого выхода нет, вкладка Settings
скрывается на этом бэкенде. Настройки — вкладка "OC Control" в SettingsForm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 13:47:04 +03:00
co-authored by Claude Sonnet 5
parent b6d2256416
commit 041bc6ba7a
8 changed files with 547 additions and 26 deletions
+108
View File
@@ -13,6 +13,16 @@ const
SETTINGS_FILE = 'hpsdr_settings.json';
CFG_BAND_COUNT = 12;
CFG_XVTR_COUNT = 8; // количество XVTR-слотов (трансверторов)
OC_PIN_COUNT = 7; // количество Open Collector выходов (Penny/Alex)
// TX pin action (byte-константы, как TXPinActions в Thetis Penny.cs):
// при каком состоянии передачи пин активен.
OCA_MOX = 0; // только обычный TX (не TUNE, не 2TON)
OCA_TUNE = 1; // только TUNE
OCA_TWOTONE = 2; // только 2TON
OCA_MOX_TUNE = 3; // TX или TUNE
OCA_MOX_TWOTONE = 4; // TX или 2TON
OCA_TUNE_TWOTONE = 5; // TUNE или 2TON
OCA_MOX_TUNE_TWOTONE = 6; // любое из трёх (дефолт)
CFG_MODE_MIN = RadioModes.MODE_MIN;
CFG_MODE_MAX = RadioModes.MODE_MAX;
CFG_PANS_MAX = 4; // потолок панадаптеров (пан 0 + доп.) — = WDSPEngine.MAX_PANS
@@ -341,6 +351,26 @@ type
EnableXvtrHf: Boolean; // режим XVTR HF
end;
// OC Control (Open Collector выходы Penny/Alex, byte 1401 HP packet).
// Только openHPSDR — у AD936x (Pluto/LibreSDR) такого выхода нет.
// Хранятся в JSON-секции "oc" под MAC. Портировано из Thetis
// (Console/setup.cs UpdateOCBits + Console/HPSDR/Penny.cs), объём сокращён:
// без PA-override/hot-switching внешнего PA (в EWSDR нет такой концепции
// на панели) и без split-pins VFO A/B (у EWSDR мультислайсы).
// HF-группа — 11 диапазонов (0=160m..10=6m, как в TAlexSettings).
// VHF-группа — 8 слотов XVTR (аналог Thetis VHF0-13, но по нашим слотам).
TOCPinActions = array[0..OC_PIN_COUNT-1] of Byte; // OCA_* на пин
TOCSettings = record
Enabled: Boolean;
RxMaskHF: array[0..CFG_BAND_COUNT-1] of Byte; // 7-бит маска, биты 0..6 = пины 1..7
TxMaskHF: array[0..CFG_BAND_COUNT-1] of Byte;
ActionHF: TOCPinActions; // для HF TX-маски
RxMaskXvtr: array[0..CFG_XVTR_COUNT-1] of Byte;
TxMaskXvtr: array[0..CFG_XVTR_COUNT-1] of Byte;
ActionXvtr: TOCPinActions; // для XVTR TX-маски
end;
// Калибровки (вкладка Calibration): детектор мощности/КСВ Alex-моста,
// напряжение/ток питания, S-метр. Модель детектора задаёт константы моста
// (Vref/bridge_volt/ADC-offset, как в Thetis computeAlexFwdPower).
@@ -526,6 +556,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);
// OC Control settings per-device (JSON-секция "oc" под MAC).
class procedure DefaultOC(out O: TOCSettings);
function LoadOC(const MAC: array of Byte; out O: TOCSettings): Boolean;
procedure SaveOC(const MAC: array of Byte; const O: TOCSettings);
// 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;
@@ -1567,6 +1601,80 @@ begin
A.EnableXvtrHf := JB(O, 'enable_xvtr_hf', False);
end;
class procedure TSettingsManager.DefaultOC(out O: TOCSettings);
var i: Integer;
begin
FillChar(O, SizeOf(O), 0);
O.Enabled := False;
for i := 0 to CFG_BAND_COUNT - 1 do
begin
O.RxMaskHF[i] := 0;
O.TxMaskHF[i] := 0;
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
O.RxMaskXvtr[i] := 0;
O.TxMaskXvtr[i] := 0;
end;
for i := 0 to OC_PIN_COUNT - 1 do
begin
O.ActionHF[i] := OCA_MOX_TUNE_TWOTONE;
O.ActionXvtr[i] := OCA_MOX_TUNE_TWOTONE;
end;
end;
function TSettingsManager.LoadOC(const MAC: array of Byte;
out O: TOCSettings): Boolean;
var MacStr: string; DevObj, Obj: TJSONObject; i: Integer;
begin
DefaultOC(O);
MacStr := MacToStr(MAC);
Result := FRoot.Find(MacStr) <> nil;
if not Result then Exit;
DevObj := GetDevObj(MacStr);
if DevObj.Find('oc') = nil then begin Result := False; Exit; end;
Obj := EnsureObj(DevObj, 'oc');
O.Enabled := JB(Obj, 'enabled', False);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
O.RxMaskHF[i] := EnsureRange(JI(Obj, 'rx_hf_' + IntToStr(i), 0), 0, 127);
O.TxMaskHF[i] := EnsureRange(JI(Obj, 'tx_hf_' + IntToStr(i), 0), 0, 127);
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
O.RxMaskXvtr[i] := EnsureRange(JI(Obj, 'rx_xvtr_' + IntToStr(i), 0), 0, 127);
O.TxMaskXvtr[i] := EnsureRange(JI(Obj, 'tx_xvtr_' + IntToStr(i), 0), 0, 127);
end;
for i := 0 to OC_PIN_COUNT - 1 do
begin
O.ActionHF[i] := EnsureRange(JI(Obj, 'action_hf_' + IntToStr(i), OCA_MOX_TUNE_TWOTONE), 0, 6);
O.ActionXvtr[i] := EnsureRange(JI(Obj, 'action_xvtr_' + IntToStr(i), OCA_MOX_TUNE_TWOTONE), 0, 6);
end;
end;
procedure TSettingsManager.SaveOC(const MAC: array of Byte;
const O: TOCSettings);
var Obj: TJSONObject; i: Integer;
begin
Obj := EnsureObj(GetDevObj(MacToStr(MAC)), 'oc');
JW(Obj, 'enabled', O.Enabled);
for i := 0 to CFG_BAND_COUNT - 1 do
begin
JW(Obj, 'rx_hf_' + IntToStr(i), Integer(O.RxMaskHF[i]));
JW(Obj, 'tx_hf_' + IntToStr(i), Integer(O.TxMaskHF[i]));
end;
for i := 0 to CFG_XVTR_COUNT - 1 do
begin
JW(Obj, 'rx_xvtr_' + IntToStr(i), Integer(O.RxMaskXvtr[i]));
JW(Obj, 'tx_xvtr_' + IntToStr(i), Integer(O.TxMaskXvtr[i]));
end;
for i := 0 to OC_PIN_COUNT - 1 do
begin
JW(Obj, 'action_hf_' + IntToStr(i), Integer(O.ActionHF[i]));
JW(Obj, 'action_xvtr_' + IntToStr(i), Integer(O.ActionXvtr[i]));
end;
end;
class procedure TSettingsManager.DefaultAnt936x(out A: TAD936xAnt);
var i: Integer;
begin