feature(rx): плавный аттенюатор 0..31 дБ в UI, память на диапазон, компенсация уровней

Аттенюатор был только в web и только тремя ступенями (0/-10/-20), а его дБ
никуда не возвращались — при ATT 20 дБ S-метр и спектр уезжали на 20 дБ, и
свежая калибровка уровней была верна лишь при ATT=0.

- RadioController: SetAtten(Db) 0..31 (как умеет железо) вместо индекса *10.
  AttenCalDB входит в SMeterCalFor и DispCalFor — стрелка, спектр, водопад и
  паны компенсируются, шкала dBm остаётся абсолютной (как Thetis/piHPSDR).
  У AD936x аттенюатора нет, там поправка 0 (усиление тракта — hardwaregain).
- Память на диапазон/слот трансвертера по образцу RxGainMode/RxGainDb:
  TBandSettings.AttenDB (band 'atten_db') и TXvtrEntry.LastAttenDB
  ('last_atten_db'); восстановление и отправка в железо — в ApplyBandDSP,
  сбор — в MakeBandSettings/SaveCurrentBand, вход в слот — через B.AttenDB.
  SetAtten сразу кладёт значение в кэш диапазона/слота (JSON пишется общим
  сохранением, а не на каждый шаг ползунка — как у drive).
- MainForm: ряд «ATT [ползунок] NNdB» в RX-блоке левой панели, отступы как у
  VOL; на Pluto ряд скрыт и RX-блок ужимается (LayoutLeftPanel двигает
  панели ниже). Внешние изменения приходят по rfAtten.
- Web: select → ползунок 0..31 с цифрой, attn_idx -> attn_db, cmd attn {db:}.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 11:41:21 +03:00
co-authored by Claude Opus 4.8
parent 366d4c9821
commit 6de8c00689
5 changed files with 127 additions and 22 deletions
+68 -2
View File
@@ -347,6 +347,11 @@ type
FPSPopup: TObject; // TPureSignalPopup (cast при использовании) FPSPopup: TObject; // TPureSignalPopup (cast при использовании)
PanelRXBlock: TPanel; PanelRXBlock: TPanel;
// Шаговый аттенюатор ADC (openHPSDR): плавные 0..31 дБ. На Pluto ряд
// скрыт — там усиление тракта живёт в блоке RF AGC.
LblAtt: TLabel;
LblAttVal: TLabel;
TrkAtt: TFlatSlider;
BtnChannels: TFlatButton; BtnChannels: TFlatButton;
FChannelsDropDown: TFlatDropDown; FChannelsDropDown: TFlatDropDown;
FChannelsForm: TObject; // TChannelsForm (cast при использовании) FChannelsForm: TObject; // TChannelsForm (cast при использовании)
@@ -706,6 +711,8 @@ type
procedure BtnAGCModeClick(Sender: TObject); procedure BtnAGCModeClick(Sender: TObject);
procedure TrkAGCChange(Sender: TObject); procedure TrkAGCChange(Sender: TObject);
procedure TrkRxGainChange(Sender: TObject); procedure TrkRxGainChange(Sender: TObject);
procedure TrkAttChange(Sender: TObject);
procedure UpdateAttLabel;
// (BtnPlutoGainClick объявлен выше) // (BtnPlutoGainClick объявлен выше)
procedure MeterTimerTick(Sender: TObject); procedure MeterTimerTick(Sender: TObject);
procedure SpectrumTimerTick(Sender: TObject); procedure SpectrumTimerTick(Sender: TObject);
@@ -762,6 +769,13 @@ uses SettingsForm;
{$R *.lfm} {$R *.lfm}
const
// Высота RX-блока левой панели: с рядом ATT (openHPSDR) и без него (Pluto —
// там усиление тракта в блоке RF AGC). Переключает LayoutLeftPanel, он же
// двигает панели ниже.
RX_BLOCK_H = 96;
RX_BLOCK_H_ATT = 120;
function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount, function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount,
ColIndex: Integer): Integer; ColIndex: Integer): Integer;
var var
@@ -1505,6 +1519,12 @@ begin
TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор
rfRxGain: rfRxGain:
UpdateRxGainUI; UpdateRxGainUI;
rfAtten:
if TrkAtt <> nil then
begin
TrkAtt.Position := FController.FAtten;
UpdateAttLabel;
end;
rfDrive: rfDrive:
begin begin
TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive%
@@ -2028,7 +2048,7 @@ begin
// RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE // RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE
PanelRXBlock := TPanel.Create(Self); PanelRXBlock := TPanel.Create(Self);
PanelRXBlock.Parent := PanelLeft; PanelRXBlock.Parent := PanelLeft;
PanelRXBlock.SetBounds(0, Y, LEFT_W, 96); PanelRXBlock.SetBounds(0, Y, LEFT_W, RX_BLOCK_H_ATT);
PanelRXBlock.BevelOuter := bvNone; PanelRXBlock.BevelOuter := bvNone;
MakeLbl(PanelRXBlock, 'RX', 4, 2); MakeLbl(PanelRXBlock, 'RX', 4, 2);
@@ -2084,7 +2104,29 @@ begin
LeftPanelButtonLeft(LEFT_W, 5, 4), 70, LeftPanelButtonLeft(LEFT_W, 5, 4), 70,
LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick); LeftPanelButtonWidth(LEFT_W, 5, 4), BTN_SM, BtnMuteClick);
Inc(Y, 100); // Шаговый аттенюатор ADC — плавный 0..31 дБ (железо openHPSDR так и умеет,
// ступени 0/10/20 были только в web-UI). Показания S-метра/спектра на эти
// дБ компенсируются в контроллере, шкала dBm остаётся абсолютной.
LblAtt := TLabel.Create(Self);
LblAtt.Parent := PanelRXBlock; LblAtt.Left := 4; LblAtt.Top := 106;
LblAtt.Caption := 'ATT'; LblAtt.Font.Color := CLR_TEXTDIM;
LblAtt.Font.Name := 'Courier New'; LblAtt.Font.Size := 7;
TrkAtt := TFlatSlider.Create(Self);
TrkAtt.Parent := PanelRXBlock; TrkAtt.Left := 34; TrkAtt.Top := 102;
TrkAtt.Width := LEFT_W - 76; TrkAtt.Height := 16;
TrkAtt.Min := 0; TrkAtt.Max := 31; // ADC step attenuator, дБ
TrkAtt.Position := FController.FAtten;
TrkAtt.OnChange := TrkAttChange;
LblAttVal := TLabel.Create(Self);
LblAttVal.Parent := PanelRXBlock;
LblAttVal.Left := LEFT_W - 38; LblAttVal.Top := 106;
LblAttVal.Font.Color := CLR_TEXT; LblAttVal.Font.Name := 'Courier New';
LblAttVal.Font.Size := 7;
UpdateAttLabel;
Inc(Y, RX_BLOCK_H_ATT + 4);
// AGC controls // AGC controls
PanelAGC := TPanel.Create(Self); PanelAGC := TPanel.Create(Self);
@@ -7933,6 +7975,19 @@ begin
FController.SetRxGain(TrkRxGain.Position); // переводит в manual FController.SetRxGain(TrkRxGain.Position); // переводит в manual
end; end;
procedure TMainForm.TrkAttChange(Sender: TObject);
begin
if FSyncingFromController then Exit;
FController.SetAtten(TrkAtt.Position);
UpdateAttLabel;
end;
procedure TMainForm.UpdateAttLabel;
begin
if LblAttVal <> nil then
LblAttVal.Caption := Format('%ddB', [FController.FAtten]);
end;
procedure TMainForm.BtnPlutoGainClick(Sender: TObject); procedure TMainForm.BtnPlutoGainClick(Sender: TObject);
begin begin
if FSyncingFromController then Exit; if FSyncingFromController then Exit;
@@ -7966,6 +8021,17 @@ procedure TMainForm.LayoutLeftPanel(Pluto: Boolean);
var baseTop: Integer; var baseTop: Integer;
begin begin
if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit; if (PanelPlutoAGC = nil) or (PanelAGC = nil) or (PanelBands = nil) then Exit;
// Шаговый аттенюатор — только у openHPSDR; у AD936x усиление тракта в блоке
// RF AGC ниже, поэтому ряд ATT убираем и RX-блок ужимаем.
if (TrkAtt <> nil) and (LblAtt <> nil) and (LblAttVal <> nil) then
begin
TrkAtt.Visible := not Pluto;
LblAtt.Visible := not Pluto;
LblAttVal.Visible := not Pluto;
if Pluto then PanelRXBlock.Height := RX_BLOCK_H
else PanelRXBlock.Height := RX_BLOCK_H_ATT;
PanelAGC.Top := PanelRXBlock.Top + PanelRXBlock.Height + 4;
end;
baseTop := PanelAGC.Top + PanelAGC.Height + 4; baseTop := PanelAGC.Top + PanelAGC.Height + 4;
PanelPlutoAGC.Top := baseTop; PanelPlutoAGC.Top := baseTop;
PanelPlutoAGC.Visible := Pluto; PanelPlutoAGC.Visible := Pluto;
+35 -7
View File
@@ -601,7 +601,7 @@ type
procedure ToggleMute; procedure ToggleMute;
procedure SetDrive(V: Integer); procedure SetDrive(V: Integer);
procedure DriveBy(Delta: Integer); procedure DriveBy(Delta: Integer);
procedure SetAtten(Idx: Integer); procedure SetAtten(Db: Integer); // ADC step attenuator, дБ (0..31)
// ---- Мультислайсы (софт-fan-out) ---- // ---- Мультислайсы (софт-fan-out) ----
// Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход // Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход
@@ -635,6 +635,8 @@ type
// Офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, 1..N — пан. // Офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, 1..N — пан.
// Колбэк движка (display-поток), см. TOnDispCalQuery. // Колбэк движка (display-поток), см. TOnDispCalQuery.
function DispCalFor(PanId: Integer): Double; function DispCalFor(PanId: Integer): Double;
// Поправка на шаговый аттенюатор ADC (дБ) — входит в оба офсета выше.
function AttenCalDB: Double;
procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string); procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string);
function SliceCount: Integer; function SliceCount: Integer;
function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean;
@@ -2094,7 +2096,17 @@ end;
function TRadioController.SMeterCalFor(VisibleHz: Double): Double; function TRadioController.SMeterCalFor(VisibleHz: Double): Double;
begin begin
Result := CalLookup(VisibleHz, FCal.SMeterBandDB, FCal.SMeterXvtrDB); Result := CalLookup(VisibleHz, FCal.SMeterBandDB, FCal.SMeterXvtrDB)
+ AttenCalDB;
end;
function TRadioController.AttenCalDB: Double;
// Шаговый аттенюатор ADC режет вход — чтобы шкала dBm оставалась абсолютной,
// его дБ возвращаются в показания (так же делают Thetis и piHPSDR). У AD936x
// такого аттенюатора нет: там усиление тракта — hardwaregain.
begin
if IsPluto then Result := 0.0
else Result := FAtten;
end; end;
function TRadioController.DispCalFor(PanId: Integer): Double; function TRadioController.DispCalFor(PanId: Integer): Double;
@@ -2105,7 +2117,7 @@ begin
if (PanId > 0) and (PanId < MAX_PANS) then Hz := FPanFreqHz[PanId] if (PanId > 0) and (PanId < MAX_PANS) then Hz := FPanFreqHz[PanId]
else Hz := FCenterFreq; else Hz := FCenterFreq;
if Hz <= 0 then Exit(0.0); if Hz <= 0 then Exit(0.0);
Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB); Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB) + AttenCalDB;
end; end;
procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer; procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer;
@@ -2808,6 +2820,7 @@ begin
// Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100). // Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100).
Result.RxGainMode := FRxGainMode; Result.RxGainMode := FRxGainMode;
Result.RxGainDb := FRxGainDb; Result.RxGainDb := FRxGainDb;
Result.AttenDB := FAtten; // шаговый аттенюатор ADC — тоже на диапазон
// Waterfall AGC/NF — глобальные (не диапазонные) // Waterfall AGC/NF — глобальные (не диапазонные)
end; end;
@@ -2846,6 +2859,7 @@ begin
FXvtrSettings.Entries[i].LastPanSlider := FPanSlider; FXvtrSettings.Entries[i].LastPanSlider := FPanSlider;
FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode; FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode;
FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb; FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb;
FXvtrSettings.Entries[i].LastAttenDB := FAtten;
end; end;
FSettings.SaveXvtr(FDevMAC, FXvtrSettings); FSettings.SaveXvtr(FDevMAC, FXvtrSettings);
Exit; Exit;
@@ -2975,6 +2989,11 @@ begin
ApplyRxGainToBackend; ApplyRxGainToBackend;
Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок Changed(rfRxGain); // UI: кнопки FAST/SLOW/HYB/MAN + ползунок
// --- Шаговый аттенюатор ADC (openHPSDR) — на диапазон/слот трансвертера.
FAtten := EnsureRange(B.AttenDB, 0, 31);
if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
Changed(rfAtten);
// --- FM squelch / CTCSS / step / repeater --- // --- FM squelch / CTCSS / step / repeater ---
FFMSQOn := B.FMSQOn; FFMSQOn := B.FMSQOn;
FFMSQLevel := B.FMSQLevel; FFMSQLevel := B.FMSQLevel;
@@ -3646,12 +3665,20 @@ begin
else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0); else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0);
end; end;
procedure TRadioController.SetAtten(Idx: Integer); procedure TRadioController.SetAtten(Db: Integer);
// Шаговый аттенюатор ADC — плавный 0..31 дБ (как в железе openHPSDR), а не
// три фиксированные ступени. Уровни S-метра/спектра компенсируются на ту же
// величину (CalLookup), поэтому шкала dBm остаётся абсолютной.
begin begin
if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2; FAtten := EnsureRange(Db, 0, 31);
FAtten := Idx; if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten));
if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10);
Changed(rfAtten); Changed(rfAtten);
// Персист — вместе с диапазоном/слотом (SaveCurrentBand на выходе из них),
// писать JSON на каждый шаг ползунка незачем, как и у drive.
if (FCurrentXvtr >= 0) and (FCurrentXvtr < CFG_XVTR_COUNT) then
FXvtrSettings.Entries[FCurrentXvtr].LastAttenDB := FAtten
else if (FCurrentBand >= 0) and (FCurrentBand < CFG_BAND_COUNT) then
FBandCache[FCurrentBand].AttenDB := FAtten;
end; end;
procedure TRadioController.SetBand(Idx: Integer); procedure TRadioController.SetBand(Idx: Integer);
@@ -3742,6 +3769,7 @@ begin
B.FMRptOffsetHz := E.LastFMRptOffsetHz; B.FMRptOffsetHz := E.LastFMRptOffsetHz;
B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x) B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x)
B.RxGainDb := E.LastRxGainDb; B.RxGainDb := E.LastRxGainDb;
B.AttenDB := E.LastAttenDB; // аттенюатор ADC слота
B.VfoA := Vis; B.VfoA := Vis;
B.VfoB := FVfoB; B.VfoB := FVfoB;
ApplyBandDSP(B); ApplyBandDSP(B);
+11
View File
@@ -103,6 +103,9 @@ type
// сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band. // сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band.
RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
RxGainDb: Integer; // manual gain, дБ (0..73) RxGainDb: Integer; // manual gain, дБ (0..73)
// Шаговый аттенюатор ADC (openHPSDR) — на диапазон: на 40м ночью нужен,
// на 10м нет. 0..31 дБ.
AttenDB: Integer;
end; end;
// Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается
@@ -295,6 +298,7 @@ type
// Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта). // Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта).
LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid
LastRxGainDb: Integer; // manual gain, дБ LastRxGainDb: Integer; // manual gain, дБ
LastAttenDB: Integer; // шаговый аттенюатор ADC слота, дБ (0..31)
end; end;
TXvtrSettings = record TXvtrSettings = record
@@ -558,6 +562,7 @@ begin
B.SpanHz := 192000; B.SpanHz := 192000;
B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto) B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto)
B.RxGainDb := 40; B.RxGainDb := 40;
B.AttenDB := 0;
B.FMSQOn := False; B.FMSQOn := False;
B.FMSQLevel := 30; B.FMSQLevel := 30;
B.CTCSSOn := False; B.CTCSSOn := False;
@@ -944,6 +949,7 @@ begin
Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz); Bands[i].FMRptOffsetHz := JD(BObj,'fmrpt_offset', Bands[i].FMRptOffsetHz);
Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3); Bands[i].RxGainMode := EnsureRange(JI(BObj,'rx_gain_mode', Bands[i].RxGainMode), 0, 3);
Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73); Bands[i].RxGainDb := EnsureRange(JI(BObj,'rx_gain_db', Bands[i].RxGainDb), 0, 73);
Bands[i].AttenDB := EnsureRange(JI(BObj,'atten_db', Bands[i].AttenDB), 0, 31);
end; end;
end; end;
@@ -1060,6 +1066,7 @@ begin
JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz); JW(O,'fmrpt_dir',B.FMRptDir); JW(O,'fmrpt_offset',B.FMRptOffsetHz);
JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider); JW(O,'zoom_factor',B.ZoomFactor); JW(O,'pan_slider',B.PanSlider);
JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb); JW(O,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb);
JW(O,'atten_db',B.AttenDB);
end; end;
function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer;
@@ -1091,6 +1098,7 @@ begin
B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz); B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz);
B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3); B.RxGainMode := EnsureRange(JI(O,'rx_gain_mode', B.RxGainMode), 0, 3);
B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73); B.RxGainDb := EnsureRange(JI(O,'rx_gain_db', B.RxGainDb), 0, 73);
B.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31);
B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0); B.ZoomFactor := EnsureRange(JD(O,'zoom_factor', B.ZoomFactor), 0.0, 1.0);
B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0); B.PanSlider := EnsureRange(JD(O,'pan_slider', B.PanSlider), 0.0, 1.0);
end; end;
@@ -1617,6 +1625,7 @@ begin
X.Entries[i].LastPanSlider := 0.5; X.Entries[i].LastPanSlider := 0.5;
X.Entries[i].LastRxGainMode := 2; // slow_attack X.Entries[i].LastRxGainMode := 2; // slow_attack
X.Entries[i].LastRxGainDb := 40; X.Entries[i].LastRxGainDb := 40;
X.Entries[i].LastAttenDB := 0;
end; end;
// Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились)
X.Entries[0].ButtonText := '2m'; X.Entries[0].ButtonText := '2m';
@@ -1698,6 +1707,7 @@ begin
X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0); X.Entries[i].LastPanSlider := EnsureRange(JD(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider), 0.0, 1.0);
X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3); X.Entries[i].LastRxGainMode := EnsureRange(JI(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode), 0, 3);
X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73); X.Entries[i].LastRxGainDb := EnsureRange(JI(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb), 0, 73);
X.Entries[i].LastAttenDB := EnsureRange(JI(EObj, 'last_atten_db', X.Entries[i].LastAttenDB), 0, 31);
// Валидация: Last freqs должны быть в [FreqBegin..FreqEnd] // Валидация: Last freqs должны быть в [FreqBegin..FreqEnd]
if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or 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].FreqEnd) then
@@ -1798,6 +1808,7 @@ begin
JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider); JW(EObj, 'last_pan_slider', X.Entries[i].LastPanSlider);
JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode); JW(EObj, 'last_rx_gain_mode', X.Entries[i].LastRxGainMode);
JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb); JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb);
JW(EObj, 'last_atten_db', X.Entries[i].LastAttenDB);
end; end;
end; end;
+4 -4
View File
@@ -199,7 +199,7 @@ begin
'<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' + '<option value="10">10 Hz</option><option value="100" selected>100 Hz</option>' +
'<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' + '<option value="1000">1 kHz</option><option value="10000">10 kHz</option>' +
'</select>' + '</select>' +
'<select id="attSel"><option value="0" selected>0 dB</option><option value="1">-10 dB</option><option value="2">-20 dB</option></select>' + '<div class="sl-g"><span class="sl-lb">ATT</span><input type="range" id="attSl" min="0" max="31" value="0"><span class="sl-val" id="attVal">0</span></div>' +
'<button id="nbBtn">NB</button>' + '<button id="nbBtn">NB</button>' +
'<button id="snbBtn">SNB</button>' + '<button id="snbBtn">SNB</button>' +
'<button id="anfBtn">ANF</button>' + '<button id="anfBtn">ANF</button>' +
@@ -311,7 +311,7 @@ begin
'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' + 'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;syncModGrp(defaultMode);pend("mode");ws2({cmd:"mode",mode:defaultMode});};' +
'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' + 'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;syncModGrp(next);pend("mode");ws2({cmd:"mode",mode:next});};' +
'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' + 'q("agcSel").onchange=function(e){pend("agc");ws2({cmd:"agc",mode:+e.target.value});};' +
'q("attSel").onchange=function(e){pend("attn");ws2({cmd:"attn",idx:+e.target.value});};' + 'q("attSl").oninput=function(e){var v=+e.target.value;var lbl=q("attVal");if(lbl)lbl.textContent=v;pend("attn");ws2({cmd:"attn",db:v});};' +
// FNP — фильтровый оверлей // FNP — фильтровый оверлей
'var FNP_N={ssb:["5.0k","4.4k","3.8k","3.3k","2.9k","2.7k","2.4k","2.1k","1.8k","1.0k"],' + 'var FNP_N={ssb:["5.0k","4.4k","3.8k","3.3k","2.9k","2.7k","2.4k","2.1k","1.8k","1.0k"],' +
@@ -732,7 +732,7 @@ begin
'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' +
// Маппинг: server JSON field → pend key // Маппинг: server JSON field → pend key
'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon",rx_mute_on:"rxmute",rx_gain_mode:"rxgain",rx_gain_db:"rxgain"};' + 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_db:"attn",tuning:"tun",filter_bw:"filter",mute:"mute",beacon_on:"beacon",rx_mute_on:"rxmute",rx_gain_mode:"rxgain",rx_gain_db:"rxgain"};' +
'function applyState(msg){' + 'function applyState(msg){' +
'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' +
@@ -786,7 +786,7 @@ begin
'if(!isPending("mute"))updMuteIc();' + 'if(!isPending("mute"))updMuteIc();' +
'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' + 'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' +
'if(!isPending("mode")){syncModGrp(cur.mode||0);syncStepSel();}' + 'if(!isPending("mode")){syncModGrp(cur.mode||0);syncStepSel();}' +
'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||0;}' + 'if(!isPending("attn")){const at=q("attSl");if(at&&!at.matches(":active")){at.value=cur.attn_db||0;var av=q("attVal");if(av)av.textContent=cur.attn_db||0;}}' +
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
'paintSM(Number(cur.smeter_dbm||-130));' + 'paintSM(Number(cur.smeter_dbm||-130));' +
'updStatusBar();' + 'updStatusBar();' +
+9 -9
View File
@@ -296,7 +296,7 @@ type
FDriveLevel: Integer; FDriveLevel: Integer;
FOnFreqA: TWebCmdFreqA; FOnFreqA: TWebCmdFreqA;
FOnAttn: TWebCmdAttn; FOnAttn: TWebCmdAttn;
FAttnIdx: Integer; FAttnDB: Integer; // шаговый аттенюатор ADC, дБ (0..31)
FOnTun: TWebCmdTun; FOnTun: TWebCmdTun;
FOnFMStep: TWebCmdFMStep; FOnFMStep: TWebCmdFMStep;
FTuning: Boolean; FTuning: Boolean;
@@ -406,7 +406,7 @@ type
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer; VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer; Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double; FwdW, SWRV, PAMaxPower: Double;
const StatusText, BoardText, IPText, SupplyText, PLLText, const StatusText, BoardText, IPText, SupplyText, PLLText,
RXText, TXText, SeqText: string); RXText, TXText, SeqText: string);
@@ -1572,10 +1572,10 @@ begin
end end
else if Cmd = 'attn' then else if Cmd = 'attn' then
begin begin
Idx := JsonGetInt(Json, 'idx', FAttnIdx); Idx := JsonGetInt(Json, 'db', FAttnDB);
if Idx < 0 then Idx := 0; if Idx < 0 then Idx := 0;
if Idx > 2 then Idx := 2; if Idx > 31 then Idx := 31;
FStateLock.Enter; FAttnIdx := Idx; FStateLock.Leave; FStateLock.Enter; FAttnDB := Idx; FStateLock.Leave;
if Assigned(FOnAttn) then FOnAttn(Idx); if Assigned(FOnAttn) then FOnAttn(Idx);
end end
else if Cmd = 'fm_step' then else if Cmd = 'fm_step' then
@@ -1778,7 +1778,7 @@ begin
'"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' +
'"running":%s,"mute":%s,"ctun":%s,' + '"running":%s,"mute":%s,"ctun":%s,' +
'"nr_mode":%d,"nr":%s,"nb_mode":%d,"nb":%s,"snb":%s,"anf":%s,"connected":%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_db":%d,"tuning":%s,"dup":%s,' +
'"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' + '"fwd_w":%.1f,"swr":%.2f,"pa_max_power":%.0f,' +
'"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' + '"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' +
'"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' + '"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' +
@@ -1809,7 +1809,7 @@ begin
BoolToStr(FConnected, 'true', 'false'), BoolToStr(FConnected, 'true', 'false'),
BoolToStr(FTransmitting, 'true', 'false'), BoolToStr(FTransmitting, 'true', 'false'),
FDriveLevel, FDriveLevel,
FAttnIdx, FAttnDB,
BoolToStr(FTuning, 'true', 'false'), BoolToStr(FTuning, 'true', 'false'),
BoolToStr(FDuplex, 'true', 'false'), BoolToStr(FDuplex, 'true', 'false'),
FFwdW, FSWR, FPAMaxPower, FFwdW, FSWR, FPAMaxPower,
@@ -1889,7 +1889,7 @@ procedure TWebServer.PushSpectrum(
CenterHz: Double; FilterIdx: Integer; CenterHz: Double; FilterIdx: Integer;
VfoB: Double; ActiveVfo: Integer; VfoB: Double; ActiveVfo: Integer;
Transmitting: Boolean; DriveLevel: Integer; Transmitting: Boolean; DriveLevel: Integer;
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; AttnDB: Integer; Tuning: Boolean; Duplex: Boolean;
FwdW, SWRV, PAMaxPower: Double; FwdW, SWRV, PAMaxPower: Double;
const StatusText, BoardText, IPText, SupplyText, PLLText, const StatusText, BoardText, IPText, SupplyText, PLLText,
RXText, TXText, SeqText: string); RXText, TXText, SeqText: string);
@@ -1926,7 +1926,7 @@ begin
FActiveVfo := ActiveVfo; FActiveVfo := ActiveVfo;
FTransmitting := Transmitting; FTransmitting := Transmitting;
FDriveLevel := DriveLevel; FDriveLevel := DriveLevel;
FAttnIdx := AttnIdx; FAttnDB := AttnDB;
FTuning := Tuning; FTuning := Tuning;
FDuplex := Duplex; FDuplex := Duplex;
FFwdW := FwdW; FFwdW := FwdW;