feat(fm): add universal FM RAW decoder output

This commit is contained in:
2026-07-16 18:58:28 +03:00
parent 3acd569da5
commit cd816c54d6
16 changed files with 656 additions and 148 deletions
+128 -16
View File
@@ -37,6 +37,7 @@ uses
WidebandView,
StatusBar,
PlatformUtils,
RadioModes,
WinFirewall,
BoardUtils, WisdomBuilder, UISync,
FlatEdit, FMRepeater,
@@ -67,7 +68,6 @@ const
WDSP_WISDOM_FILE = 'wdspWisdom00';
BAND_COUNT = 12; // макс. слотов среди планов (Pluto VHF/UHF = 12, HF = 11)
MODE_COUNT = 12;
// Полосы/дефолты фильтров (FILT_COUNT, FILT_*_BW, FILT_*_DEF, FILT_FM_*)
// перенесены в Settings.pas — нужны TRadioController. Здесь остаются только
// UI-подписи кнопок (NAMES).
@@ -83,6 +83,8 @@ const
('3.0k','2.7k','2.4k','2.1k','1.8k','1.5k','1.2k','900','600','300');
FILT_FM_NAMES: array[0..1] of string = ('NFM', 'FM');
FILT_WFM_NAMES: array[0..FILT_WFM_COUNT-1] of string = ('200k', '160k', '120k');
FILT_RAW_NAMES: array[0..FILT_RAW_COUNT-1] of string =
('6.25k', '10k', '12.5k', '20k', '25k');
// CTCSS tones (38 standard tones, no None — toggle via CTCSS button)
// CTCSS_COUNT и CTCSS_TONES перенесены в FMRepeater (нужны TRadioController).
@@ -105,8 +107,8 @@ const
'160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','');
MODE_NAMES: array[0..MODE_COUNT-1] of string = (
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR');
MODE_ROWS = 3; // 4 колонки; последний ряд неполный
'LSB','USB','DSB','CWL','CWU','FM','AM','SAM','DIGU','DIGL','WFM','DMR','FMRAW');
MODE_ROWS = 4; // 4 колонки; последний ряд неполный
SMETER_WIDTH_RATIO = 0.32;
SMETER_MARGIN = 3;
@@ -299,6 +301,9 @@ type
BtnFMSQ: TFlatButton;
TrkFMSQ: TFlatSlider;
LblFMSQ: TLabel;
PanelRawDev: TPanel;
TrkRawDev: TFlatSlider;
LblRawDev: TLabel;
PanelFMCTCSS: TPanel;
BtnFMCTCSS: TFlatButton;
BtnFMCTCSSTone: TFlatButton;
@@ -470,9 +475,11 @@ type
procedure Btn2TonClick(Sender: TObject);
procedure OnPSPopupChanged(Sender: TObject); // персист PS-параметров
procedure UpdatePSButtons; // стиль/доступность PS/2TON
procedure UpdateIMDMarkers; // 2TON: маркеры пиков/IMD3 на спектре
procedure UpdateFilterButtons;
procedure BtnFMSQClick(Sender: TObject);
procedure TrkFMSQChange(Sender: TObject);
procedure TrkRawDevChange(Sender: TObject);
procedure ApplyFMSquelch;
procedure BtnFMCTCSSClick(Sender: TObject);
procedure BtnFMCTCSSToneClick(Sender: TObject);
@@ -930,7 +937,7 @@ begin
FController.FVfoA := 14200000;
FController.FVfoB := 7100000;
FController.FActiveVfo := 0;
FController.FMode := 1;
FController.FMode := MODE_USB;
FController.FFilter := 5;
FController.FAGCMode := 1; // MEDIUM
FController.FAGCTop := 90; // 90 dBm
@@ -1376,6 +1383,7 @@ begin
ApplySpecViewGridFromState; // сетка спектра под активный тракт
RefreshTxIndicators; // TX-бейджи флагов (какой слайс передаёт)
UpdateTXControlAvailability;
UpdateIMDMarkers; // 2TON: маркеры IMD на TX-спектре
end; // (DUC-очередь сбрасывает SetMOX)
rfTuning:
StyleButton(BtnTUN, FController.FTuning);
@@ -1484,6 +1492,12 @@ begin
for i := 0 to FILT_COUNT - 1 do StyleButton(BtnFilter[i], i = FController.FFilter);
SyncSpecViewFreq;
end;
rfFMDeviation:
if (TrkRawDev <> nil) and (LblRawDev <> nil) then
begin
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
end;
rfAGCMode:
for i := 0 to 4 do StyleButton(BtnAGCMode[i], i = FController.FAGCMode);
rfVolume:
@@ -1524,6 +1538,7 @@ begin
// подсвечивал → визуал расходился с состоянием.
if BtnDUP <> nil then StyleButton(BtnDUP, FController.FDisplayDuplex);
UpdateRxMuteButton; // активность RX MUTE зависит от DUP
UpdateIMDMarkers; // IMD-маркеры 2TON живут только в DUP
// На передаче переключаем источник пикселей и сбрасываем буферы, чтобы
// старые TX/RX bin'ы не оставались на следующем кадре.
if FController.FTransmitting then
@@ -1550,7 +1565,7 @@ begin
begin
if FStepDropDown <> nil then FStepDropDown.SetItemIndex(FController.FFMStepIdx);
if FWebServer <> nil then FWebServer.FMStepIdx := FController.FFMStepIdx;
if (FController.FMode in [MODE_FM, MODE_DMR]) and
if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
(FSpecView <> nil) then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx];
end;
@@ -2230,6 +2245,35 @@ begin
Inc(Y, 76);
// FM RAW discriminator deviation. Slider units are 50 Hz: fine enough for
// decoder calibration, while the full 0.5..10 kHz range remains usable.
PanelRawDev := TPanel.Create(Self);
PanelRawDev.Parent := PanelLeft;
PanelRawDev.SetBounds(0, Y, LEFT_W, 50);
PanelRawDev.BevelOuter := bvNone;
PanelRawDev.Visible := False;
MakeLbl(PanelRawDev, 'DEVIATION', 4, 2);
TrkRawDev := TFlatSlider.Create(Self);
TrkRawDev.Parent := PanelRawDev;
TrkRawDev.Left := 5;
TrkRawDev.Top := 22;
TrkRawDev.Width := LEFT_W - 68;
TrkRawDev.Height := 14;
TrkRawDev.Min := RAW_FM_DEV_MIN div 50;
TrkRawDev.Max := RAW_FM_DEV_MAX div 50;
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
TrkRawDev.OnChange := TrkRawDevChange;
LblRawDev := TLabel.Create(Self);
LblRawDev.Parent := PanelRawDev;
LblRawDev.Left := LEFT_W - 61;
LblRawDev.Top := 20;
LblRawDev.Width := 57;
LblRawDev.Alignment := taRightJustify;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
LblRawDev.Font.Color := CLR_TEXT;
LblRawDev.Font.Name := 'Courier New';
LblRawDev.Font.Size := 8;
// FM Squelch panel (visible only in FM mode, above CTCSS)
PanelFMSQ := TPanel.Create(Self);
PanelFMSQ.Parent := PanelLeft;
@@ -2887,6 +2931,7 @@ begin
DP(PanelVfoA); DP(PanelVfoB); DP(PanelVfoButtons);
DP(PanelBands); DP(PanelMode); DP(PanelFilter);
if PanelFMSQ <> nil then DP(PanelFMSQ);
if PanelRawDev <> nil then DP(PanelRawDev);
if PanelFMCTCSS <> nil then DP(PanelFMCTCSS);
if PanelFMStep <> nil then DP(PanelFMStep);
if PanelFMRpt <> nil then DP(PanelFMRpt);
@@ -3262,7 +3307,7 @@ begin
FSpecView.SpanHz := ViewSpan;
FSpecView.Mode := FController.FMode;
FSpecView.FilterBW := FController.FFilterBW;
if FController.FMode in [MODE_FM, MODE_DMR] then
if FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW] then
FSpecView.FMGridStepHz := FM_STEP_HZ[FController.FFMStepIdx]
else
FSpecView.FMGridStepHz := 0;
@@ -3554,7 +3599,7 @@ var Allowed: Boolean;
begin
if (BtnMOX = nil) or (BtnTUN = nil) or (FController = nil) then Exit;
Allowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR);
not (FController.ActiveTXMode in [MODE_DMR, MODE_FMRAW]);
BtnMOX.Enabled := Allowed;
BtnTUN.Enabled := Allowed;
UpdatePSButtons;
@@ -3568,7 +3613,7 @@ var
begin
if (BtnPS = nil) or (Btn2Ton = nil) or (FController = nil) then Exit;
TXAllowed := FController.FRunning and
(FController.ActiveTXMode <> MODE_DMR);
not (FController.ActiveTXMode in [MODE_DMR, MODE_FMRAW]);
PSAllowed := TXAllowed and FController.PSAvailable;
BtnPS.Enabled := PSAllowed;
Btn2Ton.Enabled := TXAllowed;
@@ -3576,6 +3621,30 @@ begin
StyleButton(Btn2Ton, FController.FTwoTone);
if (FPSPopup <> nil) and TPureSignalPopup(FPSPopup).Visible then
TPureSignalPopup(FPSPopup).RefreshStatus;
UpdateIMDMarkers;
end;
procedure TMainForm.UpdateIMDMarkers;
// 2TON: маркеры пиков тонов и продуктов IMD3. Только в DUP — дисплей остаётся
// на RX и показывает СВОЙ сигнал после PA (настоящие плечи IMD); цифровой
// TX-спектр продуктов PA не содержит, там замер бессмыслен. Абсолютные
// частоты = TX ± аудио-оффсеты тонов (знак как у генератора: для нижней
// боковой тона лежат НИЖЕ несущей). Замер/отрисовка — в SpectrumView.
var
Sgn: Double;
begin
if (FSpecView = nil) or (FController = nil) then Exit;
if (FController.ActiveTXMode = MODE_LSB) or
(FController.ActiveTXMode = MODE_CWL) or
(FController.ActiveTXMode = MODE_DIGL) then
Sgn := -1.0
else
Sgn := 1.0;
FSpecView.SetIMDMarkers(
FController.FTwoTone and FController.FTransmitting
and FController.FDisplayDuplex,
ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq1,
ActiveTXFreqHz + Sgn * FController.FTXSettings.PSTTFreq2);
end;
procedure TMainForm.UpdateStatusField3;
@@ -4268,6 +4337,7 @@ begin
EdFMRptOffset.Text := RptFormatOffset(FController.FFMRptOffsetHz);
PanelFMRpt.Visible := True;
end;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
RelayoutBelowBands;
Exit;
end;
@@ -4279,7 +4349,7 @@ begin
if PanelFMCTCSS <> nil then PanelFMCTCSS.Visible := False;
if PanelFMStep <> nil then
begin
if FController.FMode = MODE_DMR then
if FController.FMode in [MODE_DMR, MODE_FMRAW] then
begin
StyleButton(BtnFMStep, FController.FFMStepOn);
if FStepDropDown <> nil then
@@ -4293,6 +4363,7 @@ begin
end;
end;
if PanelFMRpt <> nil then PanelFMRpt.Visible := False;
if PanelRawDev <> nil then PanelRawDev.Visible := False;
if FController.FMode = MODE_WFM then
begin
@@ -4315,6 +4386,33 @@ begin
Exit;
end;
if FController.FMode = MODE_FMRAW then
begin
CloseCTCSSPopup;
if FController.FFilter > FILT_RAW_COUNT - 1 then
FController.FFilter := FILT_RAW_DEF;
FController.FFilterBW := FILT_RAW_BW[FController.FFilter];
for i := 0 to FILT_RAW_COUNT - 1 do
begin
BtnFilter[i].Caption := FILT_RAW_NAMES[i];
BtnFilter[i].Left := LeftPanelButtonLeft(PanelFilter.Width, FILT_RAW_COUNT, i);
BtnFilter[i].Width := LeftPanelButtonWidth(PanelFilter.Width, FILT_RAW_COUNT, i);
BtnFilter[i].Top := 16;
BtnFilter[i].Visible := True;
StyleButton(BtnFilter[i], i = FController.FFilter);
end;
for i := FILT_RAW_COUNT to FILT_COUNT - 1 do BtnFilter[i].Visible := False;
PanelFilter.Height := BtnFilter[0].Top + BtnFilter[0].Height + 4;
if PanelRawDev <> nil then
begin
TrkRawDev.Position := Round(FController.FRawFMDeviation) div 50;
LblRawDev.Caption := Format('%.2f k', [FController.FRawFMDeviation / 1000.0]);
PanelRawDev.Visible := True;
end;
RelayoutBelowBands;
Exit;
end;
if FController.FMode = MODE_DMR then
begin
CloseCTCSSPopup;
@@ -4560,13 +4658,15 @@ end;
procedure TMainForm.OnPSPopupChanged(Sender: TObject);
// Правка PS-параметров в поповере: применение к WDSP делает сам поповер
// (ApplyPSSettingsToDSP), здесь — только персист per-device.
// (ApplyPSSettingsToDSP), здесь — персист per-device + live-обновление
// частот IMD-маркеров (если правили тона при активном 2TON).
begin
if FController.FDevConnected then
begin
FController.FSettings.SaveTX(FController.FDevMAC, FController.FTXSettings);
FController.FSettings.Save;
end;
UpdateIMDMarkers;
end;
// ---------------------------------------------------------------------------
@@ -4587,7 +4687,7 @@ var
ViewCenter, ViewSpan: Double;
begin
if PanelWidth <= 0 then Exit;
if (FController.FMode in [MODE_FM, MODE_DMR]) and
if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else
@@ -4612,7 +4712,7 @@ begin
if (FWidebandView = nil) or
(not FWidebandView.TryPixelToFrequency(PixelX, PanelWidth, FreqHz)) then Exit;
if (FController.FMode in [MODE_FM, MODE_DMR]) and
if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn then
StepHz := FM_STEP_HZ[FController.FFMStepIdx]
else
@@ -5094,6 +5194,14 @@ begin
SaveCurrentBand;
end;
procedure TMainForm.TrkRawDevChange(Sender: TObject);
var Hz: Integer;
begin
if FSyncingFromController then Exit;
Hz := TrkRawDev.Position * 50;
FController.SetRawFMDeviation(Hz);
end;
procedure TMainForm.CloseCTCSSPopup;
begin
if FCTCSSDropDown <> nil then FCTCSSDropDown.ClosePopup;
@@ -6183,8 +6291,8 @@ begin
Id := P.FirstSliceId;
StepHz := 100;
if ((Id <> 0) and FController.GetSlice(Id, S) and
(S.Mode = MODE_DMR)) or
((Id = 0) and (FController.FMode = MODE_DMR)) then
(S.Mode in [MODE_DMR, MODE_FMRAW])) or
((Id = 0) and (FController.FMode in [MODE_DMR, MODE_FMRAW])) then
StepHz := 12500;
Hz := Round(VC + (X / W - 0.5) * VS);
Hz := ((Hz + StepHz div 2) div StepHz) * StepHz;
@@ -7443,6 +7551,10 @@ begin
PanelFilter.Top := Y;
Y := Y + PanelFilter.Height + 2;
end;
if (PanelRawDev <> nil) and PanelRawDev.Visible then
begin
PanelRawDev.Top := Y; Y := Y + PanelRawDev.Height + 2;
end;
if (PanelFMStep <> nil) and PanelFMStep.Visible then
begin
PanelFMStep.Top := Y; Y := Y + PanelFMStep.Height + 2;
@@ -7719,7 +7831,7 @@ begin
end;
if (SliceId <> 0) and FController.GetSlice(SliceId, Sl) then
begin
if (Sl.Mode = MODE_DMR) and
if (Sl.Mode in [MODE_DMR, MODE_FMRAW]) and
not ((ssCtrl in Shift) or (ssShift in Shift)) then Step := 12500
else if (ssCtrl in Shift) and (ssShift in Shift) then Step := 1000000
else if ssCtrl in Shift then Step := 1000
@@ -7743,7 +7855,7 @@ begin
Handled := True;
Exit;
end;
if (FController.FMode in [MODE_FM, MODE_DMR]) and
if (FController.FMode in [MODE_FM, MODE_DMR, MODE_FMRAW]) and
FController.FFMStepOn and
not ((ssCtrl in Shift) or (ssShift in Shift)) then
Step := FM_STEP_HZ[FController.FFMStepIdx]