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
+285 -1
View File
@@ -85,6 +85,7 @@ type
const InDev: array of string) of object;
TOnAlexChange = procedure(const A: TAlexSettings) of object;
TOnXvtrChange = procedure(const X: TXvtrSettings) of object;
TOnOCChange = procedure(const O: TOCSettings) of object;
{ TSettingsForm }
TSettingsForm = class(TForm)
@@ -320,6 +321,26 @@ type
FXvtrConfig: TXvtrSettings;
FOnXvtrChange: TOnXvtrChange;
// ---- OC Control tab (Open Collector выходы Penny/Alex, openHPSDR-only) ----
FPageOC: TScrollBox;
FNavOC: TFlatButton;
FOCSubtitle: TLabel;
FOCEnabled: TFlatCheckBox;
// HF-группа: 11 диапазонов (0=160m..10=6m) × 7 пинов, RX/TX-маски
FOCHFRx: array[0..10, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
FOCHFTx: array[0..10, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
FOCHFAction: array[0..OC_PIN_COUNT-1] of TFlatComboBox;
FOCHFGrp: TPanel;
FOCHFActGrp: TPanel;
// VHF-группа = 8 слотов XVTR × 7 пинов, RX/TX-маски
FOCXvtrRx: array[0..CFG_XVTR_COUNT-1, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
FOCXvtrTx: array[0..CFG_XVTR_COUNT-1, 0..OC_PIN_COUNT-1] of TFlatCheckBox;
FOCXvtrAction: array[0..OC_PIN_COUNT-1] of TFlatComboBox;
FOCXvtrGrp: TPanel;
FOCXvtrActGrp: TPanel;
FOCConfig: TOCSettings;
FOnOCChange: TOnOCChange;
// ---- QO-100 tab (редактирует зарезервированный слот QO100_SLOT) ----
FPageQO: TScrollBox;
FNavQO: TFlatButton;
@@ -431,6 +452,10 @@ type
procedure FireXvtrChange;
procedure OnXvtrAnyChange(Sender: TObject);
procedure UpdateXvtrRowEnabled(Idx: Integer);
procedure BuildOCTab;
procedure CollectOCFromUI;
procedure FireOCChange;
procedure OnOCAnyChange(Sender: TObject);
procedure CollectTXFromUI;
procedure FireTXChange;
procedure OnTXAnyChange(Sender: TObject);
@@ -547,6 +572,9 @@ type
procedure LoadTXSettings(const T: TTXSettings; BoardType: Integer = 0);
procedure LoadAlexSettings(const A: TAlexSettings; BoardType: Integer);
procedure LoadXvtrSettings(const X: TXvtrSettings);
procedure LoadOCSettings(const O: TOCSettings);
// Вкладка OC Control — только openHPSDR (у Pluto/AD936x нет такого выхода).
procedure SetOCBackend(IsPluto: Boolean);
procedure LoadWfAGCNF(AGC, NF: Boolean);
procedure LoadWfRender(Palette: Integer; Gamma: Double;
ResLimit, SpeedDiv: Integer);
@@ -559,6 +587,7 @@ type
property OnSliceCATChange: TOnSliceCATChange read FOnSliceCATChange write FOnSliceCATChange;
property OnAlexChange: TOnAlexChange read FOnAlexChange write FOnAlexChange;
property OnXvtrChange: TOnXvtrChange read FOnXvtrChange write FOnXvtrChange;
property OnOCChange: TOnOCChange read FOnOCChange write FOnOCChange;
property OnPAChange: TOnPASettingsChange read FOnPAChange write FOnPAChange;
property OnPlutoTxChange: TOnPlutoTxChange read FOnPlutoTxChange write FOnPlutoTxChange;
property OnAnt936xChange: TOnAnt936xChange read FOnAnt936xChange write FOnAnt936xChange;
@@ -643,6 +672,10 @@ const
PA_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
// TX pin action (OC Control) — имена в UI, значения = OCA_* из Settings.pas.
OC_ACTION_NAMES: array[0..6] of string = (
'MOX', 'TUNE', '2TON', 'MOX+TUNE', 'MOX+2TON', 'TUNE+2TON', 'MOX+TUNE+2TON');
// Раскладка вкладки Calibration — общая для построения (BuildCalibrationTab)
// и перекладки под активное железо (RelayoutCalTab).
CAL_MARGIN = 22;
@@ -818,7 +851,8 @@ begin
FNavSlices := MakeNavButton('Slices', 398);
FNavAlex := MakeNavButton('Antenna / Alex', 434);
FNavXvtr := MakeNavButton('Transverter', 470);
FNavQO := MakeNavButton('QO-100', 506);
FNavOC := MakeNavButton('OC Control', 506);
FNavQO := MakeNavButton('QO-100', 542);
FContentPanel := TPanel.Create(Self);
FContentPanel.Parent := Self;
@@ -838,11 +872,13 @@ begin
FPageSlices := MakeScrollPage;
FPageAlex := MakeScrollPage;
FPageXvtr := MakeScrollPage;
FPageOC := MakeScrollPage;
FPageQO := MakeScrollPage;
FAlexBoardType := 0;
TSettingsManager.DefaultAlex(FAlexConfig);
TSettingsManager.DefaultXvtr(FXvtrConfig);
TSettingsManager.DefaultCalibration(FCalConfig);
TSettingsManager.DefaultOC(FOCConfig);
BuildAudioTab;
BuildDisplayTab;
@@ -855,6 +891,7 @@ begin
BuildSlicesTab;
BuildAlexTab;
BuildXvtrTab;
BuildOCTab;
BuildQOTab;
FBtnClose := TFlatButton.Create(Self);
@@ -889,6 +926,7 @@ begin
else if Sender = FNavSlices then SelectPage(FPageSlices, FNavSlices)
else if Sender = FNavAlex then SelectPage(FPageAlex, FNavAlex)
else if Sender = FNavXvtr then SelectPage(FPageXvtr, FNavXvtr)
else if Sender = FNavOC then SelectPage(FPageOC, FNavOC)
else if Sender = FNavQO then SelectPage(FPageQO, FNavQO);
end;
@@ -906,6 +944,7 @@ begin
FPageSlices.Visible := APage = FPageSlices;
FPageAlex.Visible := APage = FPageAlex;
FPageXvtr.Visible := APage = FPageXvtr;
FPageOC.Visible := APage = FPageOC;
FPageQO.Visible := APage = FPageQO;
FNavAudio.Active := ANav = FNavAudio;
@@ -920,6 +959,7 @@ begin
FNavSlices.Active := ANav = FNavSlices;
FNavAlex.Active := ANav = FNavAlex;
FNavXvtr.Active := ANav = FNavXvtr;
FNavOC.Active := ANav = FNavOC;
FNavQO.Active := ANav = FNavQO;
end;
@@ -4658,6 +4698,250 @@ begin
end;
end;
// ===========================================================================
// OC Control tab (Open Collector выходы Penny/Alex, byte 1401 HP-пакета).
// Портировано из Thetis (Console/setup.cs UpdateOCBits + Console/HPSDR/
// Penny.cs), объём сокращён — см. память project_oc_control: без PA-override/
// hot-switching внешнего PA и без split-pins VFO A/B. Только openHPSDR —
// вкладка целиком прячется на Pluto/AD936x (SetOCBackend).
// HF-группа = 11 диапазонов (как Alex/PA), VHF-группа = 8 слотов XVTR.
// ===========================================================================
procedure TSettingsForm.BuildOCTab;
const
MARGIN = 22;
PAD = 12;
HDR1_H = 22;
HDR2_H = 20;
ROW_H = 24;
PIN_W = 24;
BAND_X = PAD;
RX0_X = PAD + 50;
TX0_X = RX0_X + OC_PIN_COUNT * PIN_W + 20;
GRP_W = TX0_X + OC_PIN_COUNT * PIN_W + PAD;
DATA_Y0 = GRP_TITLE_H + HDR1_H + HDR2_H + 4;
HF_GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + 11 * ROW_H + PAD;
XVTR_GRP_H = GRP_TITLE_H + HDR1_H + HDR2_H + CFG_XVTR_COUNT * ROW_H + PAD;
ACT_ROW_H = 30;
ACT_GRP_W = 260;
ACT_GRP_H = GRP_TITLE_H + PAD + OC_PIN_COUNT * ACT_ROW_H;
HF_BAND_NAMES: array[0..10] of string =
('160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m');
var
Grp, ActGrp: TPanel;
b, p: Integer;
Y: Integer;
Cmb: TFlatComboBox;
function MkChk(AParent: TWinControl; AX, AY: Integer): TFlatCheckBox;
begin
Result := TFlatCheckBox.Create(Self);
Result.Parent := AParent;
Result.Caption := '';
Result.SetBounds(AX, AY, PIN_W, 20);
Result.Font.Color := CLR_TEXT;
Result.Font.Name := UI_FONT;
Result.Font.Size := 9;
Result.OnChange := OnOCAnyChange;
end;
begin
FLoading := True;
try
with TLabel.Create(Self) do
begin
Parent := FPageOC;
Caption := 'OC Control';
SetBounds(MARGIN, 18, 500, 28);
Font.Name := UI_FONT; Font.Size := 16;
Font.Style := [fsBold]; Font.Color := CLR_TEXT;
end;
FOCSubtitle := TLabel.Create(Self);
FOCSubtitle.Parent := FPageOC;
FOCSubtitle.Caption :=
'Open Collector outputs (Penny/Alex board), per-band and per-transverter. openHPSDR only.';
FOCSubtitle.SetBounds(MARGIN, 48, 680, 20);
FOCSubtitle.Font.Name := UI_FONT;
FOCSubtitle.Font.Size := 9;
FOCSubtitle.Font.Color := CLR_TEXTDIM;
FOCEnabled := TFlatCheckBox.Create(Self);
FOCEnabled.Parent := FPageOC;
FOCEnabled.Caption := 'Enable OC Control';
FOCEnabled.SetBounds(MARGIN, 76, 220, 22);
FOCEnabled.Font.Color := CLR_TEXT;
FOCEnabled.Font.Name := UI_FONT;
FOCEnabled.Font.Size := 9;
FOCEnabled.OnChange := OnOCAnyChange;
// ---- HF band grid ----
Grp := MakeGroupPanel(FPageOC, 'HF Bands', MARGIN, 106, GRP_W, HF_GRP_H);
FOCHFGrp := Grp;
MakeLbl(Grp, 'Band', BAND_X, GRP_TITLE_H + 2, 44);
MakeLbl(Grp, 'RX pins', RX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
MakeLbl(Grp, 'TX pins', TX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
for p := 0 to OC_PIN_COUNT - 1 do
begin
MakeLbl(Grp, IntToStr(p + 1), RX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
MakeLbl(Grp, IntToStr(p + 1), TX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
end;
for b := 0 to 10 do
begin
Y := DATA_Y0 + b * ROW_H;
MakeLbl(Grp, HF_BAND_NAMES[b], BAND_X, Y + 2, 44);
for p := 0 to OC_PIN_COUNT - 1 do
begin
FOCHFRx[b, p] := MkChk(Grp, RX0_X + p * PIN_W, Y);
FOCHFTx[b, p] := MkChk(Grp, TX0_X + p * PIN_W, Y);
end;
end;
// ---- HF TX pin action ----
ActGrp := MakeGroupPanel(FPageOC, 'HF TX Pin Action',
MARGIN + GRP_W + 14, 106, ACT_GRP_W, ACT_GRP_H);
FOCHFActGrp := ActGrp;
for p := 0 to OC_PIN_COUNT - 1 do
begin
MakeLbl(ActGrp, 'Pin ' + IntToStr(p + 1), PAD, GRP_TITLE_H + PAD + p * ACT_ROW_H + 5, 44);
Cmb := MakeCombo(ActGrp, PAD + 50, GRP_TITLE_H + PAD + p * ACT_ROW_H, 190, OnOCAnyChange);
for b := 0 to 6 do Cmb.Items.Add(OC_ACTION_NAMES[b]);
Cmb.ItemIndex := OCA_MOX_TUNE_TWOTONE;
FOCHFAction[p] := Cmb;
end;
// ---- XVTR (VHF) slot grid ----
Grp := MakeGroupPanel(FPageOC, 'Transverter Slots', MARGIN, 106 + HF_GRP_H + 14,
GRP_W, XVTR_GRP_H);
FOCXvtrGrp := Grp;
MakeLbl(Grp, 'Slot', BAND_X, GRP_TITLE_H + 2, 44);
MakeLbl(Grp, 'RX pins', RX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
MakeLbl(Grp, 'TX pins', TX0_X, GRP_TITLE_H + 2, OC_PIN_COUNT * PIN_W);
for p := 0 to OC_PIN_COUNT - 1 do
begin
MakeLbl(Grp, IntToStr(p + 1), RX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
MakeLbl(Grp, IntToStr(p + 1), TX0_X + p * PIN_W + 5, GRP_TITLE_H + HDR1_H + 2, PIN_W);
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
Y := DATA_Y0 + b * ROW_H;
MakeLbl(Grp, 'XVTR' + IntToStr(b + 1), BAND_X, Y + 2, 48);
for p := 0 to OC_PIN_COUNT - 1 do
begin
FOCXvtrRx[b, p] := MkChk(Grp, RX0_X + p * PIN_W, Y);
FOCXvtrTx[b, p] := MkChk(Grp, TX0_X + p * PIN_W, Y);
end;
end;
// ---- XVTR TX pin action ----
ActGrp := MakeGroupPanel(FPageOC, 'Transverter TX Pin Action',
MARGIN + GRP_W + 14, 106 + HF_GRP_H + 14, ACT_GRP_W, ACT_GRP_H);
FOCXvtrActGrp := ActGrp;
for p := 0 to OC_PIN_COUNT - 1 do
begin
MakeLbl(ActGrp, 'Pin ' + IntToStr(p + 1), PAD, GRP_TITLE_H + PAD + p * ACT_ROW_H + 5, 44);
Cmb := MakeCombo(ActGrp, PAD + 50, GRP_TITLE_H + PAD + p * ACT_ROW_H, 190, OnOCAnyChange);
for b := 0 to 6 do Cmb.Items.Add(OC_ACTION_NAMES[b]);
Cmb.ItemIndex := OCA_MOX_TUNE_TWOTONE;
FOCXvtrAction[p] := Cmb;
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.CollectOCFromUI;
var b, p: Integer; Mask: Byte;
begin
FOCConfig.Enabled := (FOCEnabled <> nil) and FOCEnabled.Checked;
for b := 0 to 10 do
begin
Mask := 0;
for p := 0 to OC_PIN_COUNT - 1 do
if (FOCHFRx[b, p] <> nil) and FOCHFRx[b, p].Checked then Mask := Mask or (1 shl p);
FOCConfig.RxMaskHF[b] := Mask;
Mask := 0;
for p := 0 to OC_PIN_COUNT - 1 do
if (FOCHFTx[b, p] <> nil) and FOCHFTx[b, p].Checked then Mask := Mask or (1 shl p);
FOCConfig.TxMaskHF[b] := Mask;
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
begin
Mask := 0;
for p := 0 to OC_PIN_COUNT - 1 do
if (FOCXvtrRx[b, p] <> nil) and FOCXvtrRx[b, p].Checked then Mask := Mask or (1 shl p);
FOCConfig.RxMaskXvtr[b] := Mask;
Mask := 0;
for p := 0 to OC_PIN_COUNT - 1 do
if (FOCXvtrTx[b, p] <> nil) and FOCXvtrTx[b, p].Checked then Mask := Mask or (1 shl p);
FOCConfig.TxMaskXvtr[b] := Mask;
end;
for p := 0 to OC_PIN_COUNT - 1 do
begin
if (FOCHFAction[p] <> nil) and (FOCHFAction[p].ItemIndex >= 0) then
FOCConfig.ActionHF[p] := Byte(FOCHFAction[p].ItemIndex);
if (FOCXvtrAction[p] <> nil) and (FOCXvtrAction[p].ItemIndex >= 0) then
FOCConfig.ActionXvtr[p] := Byte(FOCXvtrAction[p].ItemIndex);
end;
end;
procedure TSettingsForm.FireOCChange;
begin
if FLoading then Exit;
CollectOCFromUI;
if Assigned(FOnOCChange) then
FOnOCChange(FOCConfig);
end;
procedure TSettingsForm.OnOCAnyChange(Sender: TObject);
begin
FireOCChange;
end;
procedure TSettingsForm.LoadOCSettings(const O: TOCSettings);
var b, p: Integer;
begin
FLoading := True;
try
FOCConfig := O;
if FOCEnabled <> nil then FOCEnabled.Checked := O.Enabled;
for b := 0 to 10 do
for p := 0 to OC_PIN_COUNT - 1 do
begin
if FOCHFRx[b, p] <> nil then
FOCHFRx[b, p].Checked := (O.RxMaskHF[b] and (1 shl p)) <> 0;
if FOCHFTx[b, p] <> nil then
FOCHFTx[b, p].Checked := (O.TxMaskHF[b] and (1 shl p)) <> 0;
end;
for b := 0 to CFG_XVTR_COUNT - 1 do
for p := 0 to OC_PIN_COUNT - 1 do
begin
if FOCXvtrRx[b, p] <> nil then
FOCXvtrRx[b, p].Checked := (O.RxMaskXvtr[b] and (1 shl p)) <> 0;
if FOCXvtrTx[b, p] <> nil then
FOCXvtrTx[b, p].Checked := (O.TxMaskXvtr[b] and (1 shl p)) <> 0;
end;
for p := 0 to OC_PIN_COUNT - 1 do
begin
if FOCHFAction[p] <> nil then FOCHFAction[p].ItemIndex := EnsureRange(O.ActionHF[p], 0, 6);
if FOCXvtrAction[p] <> nil then FOCXvtrAction[p].ItemIndex := EnsureRange(O.ActionXvtr[p], 0, 6);
end;
finally
FLoading := False;
end;
end;
procedure TSettingsForm.SetOCBackend(IsPluto: Boolean);
// У Pluto/AD936x нет Open Collector выхода — вкладка целиком скрывается,
// а не показывает пустое содержимое (в отличие от Antenna, где Pluto имеет
// свою таблицу разъёмов).
begin
FNavOC.Visible := not IsPluto;
if IsPluto and (FPageOC.Visible) then
SelectPage(FPageAudio, FNavAudio);
end;
// ── QO-100 page ─────────────────────────────────────────────────────────────
procedure TSettingsForm.BuildQOTab;