diff --git a/MainForm.pas b/MainForm.pas index eb1225f..394da30 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -347,6 +347,11 @@ type FPSPopup: TObject; // TPureSignalPopup (cast при использовании) PanelRXBlock: TPanel; + // Шаговый аттенюатор ADC (openHPSDR): плавные 0..31 дБ. На Pluto ряд + // скрыт — там усиление тракта живёт в блоке RF AGC. + LblAtt: TLabel; + LblAttVal: TLabel; + TrkAtt: TFlatSlider; BtnChannels: TFlatButton; FChannelsDropDown: TFlatDropDown; FChannelsForm: TObject; // TChannelsForm (cast при использовании) @@ -706,6 +711,8 @@ type procedure BtnAGCModeClick(Sender: TObject); procedure TrkAGCChange(Sender: TObject); procedure TrkRxGainChange(Sender: TObject); + procedure TrkAttChange(Sender: TObject); + procedure UpdateAttLabel; // (BtnPlutoGainClick объявлен выше) procedure MeterTimerTick(Sender: TObject); procedure SpectrumTimerTick(Sender: TObject); @@ -762,6 +769,13 @@ uses SettingsForm; {$R *.lfm} +const + // Высота RX-блока левой панели: с рядом ATT (openHPSDR) и без него (Pluto — + // там усиление тракта в блоке RF AGC). Переключает LayoutLeftPanel, он же + // двигает панели ниже. + RX_BLOCK_H = 96; + RX_BLOCK_H_ATT = 120; + function TMainForm.LeftPanelButtonWidth(PanelWidth, ColCount, ColIndex: Integer): Integer; var @@ -1505,6 +1519,12 @@ begin TrkVolume.Position := FController.ActiveVolume; // RX или TX-монитор rfRxGain: UpdateRxGainUI; + rfAtten: + if TrkAtt <> nil then + begin + TrkAtt.Position := FController.FAtten; + UpdateAttLabel; + end; rfDrive: begin TrkDrive.Position := FController.FDrivePercent; // слайдер ← drive% @@ -2028,7 +2048,7 @@ begin // RX block: CTUN + VOL + NR/NB/SNB/ANF/MUTE PanelRXBlock := TPanel.Create(Self); PanelRXBlock.Parent := PanelLeft; - PanelRXBlock.SetBounds(0, Y, LEFT_W, 96); + PanelRXBlock.SetBounds(0, Y, LEFT_W, RX_BLOCK_H_ATT); PanelRXBlock.BevelOuter := bvNone; MakeLbl(PanelRXBlock, 'RX', 4, 2); @@ -2084,7 +2104,29 @@ begin LeftPanelButtonLeft(LEFT_W, 5, 4), 70, 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 PanelAGC := TPanel.Create(Self); @@ -7933,6 +7975,19 @@ begin FController.SetRxGain(TrkRxGain.Position); // переводит в manual 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); begin if FSyncingFromController then Exit; @@ -7966,6 +8021,17 @@ procedure TMainForm.LayoutLeftPanel(Pluto: Boolean); var baseTop: Integer; begin 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; PanelPlutoAGC.Top := baseTop; PanelPlutoAGC.Visible := Pluto; diff --git a/RadioController.pas b/RadioController.pas index a5147c2..04c3c28 100644 --- a/RadioController.pas +++ b/RadioController.pas @@ -601,7 +601,7 @@ type procedure ToggleMute; procedure SetDrive(V: Integer); procedure DriveBy(Delta: Integer); - procedure SetAtten(Idx: Integer); + procedure SetAtten(Db: Integer); // ADC step attenuator, дБ (0..31) // ---- Мультислайсы (софт-fan-out) ---- // Создаёт слайс на TargetHz: заводит WDSP-канал в движке + свой аудио-выход @@ -635,6 +635,8 @@ type // Офсет спектра/водопада (дБ) для дисплея: PanId 0 = главный, 1..N — пан. // Колбэк движка (display-поток), см. TOnDispCalQuery. function DispCalFor(PanId: Integer): Double; + // Поправка на шаговый аттенюатор ADC (дБ) — входит в оба офсета выше. + function AttenCalDB: Double; procedure SetSliceDevice(Id, DevIndex: Integer; const DevName: string); function SliceCount: Integer; function GetSlice(Id: Integer; out S: TCtrlSlice): Boolean; @@ -2094,7 +2096,17 @@ end; function TRadioController.SMeterCalFor(VisibleHz: Double): Double; 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; function TRadioController.DispCalFor(PanId: Integer): Double; @@ -2105,7 +2117,7 @@ begin if (PanId > 0) and (PanId < MAX_PANS) then Hz := FPanFreqHz[PanId] else Hz := FCenterFreq; if Hz <= 0 then Exit(0.0); - Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB); + Result := CalLookup(Hz, FCal.DispBandDB, FCal.DispXvtrDB) + AttenCalDB; end; procedure TRadioController.SetSliceDevice(Id, DevIndex: Integer; @@ -2808,6 +2820,7 @@ begin // Аппаратный AGC AD936x — на диапазон (усиление тракта разное на 2м и QO-100). Result.RxGainMode := FRxGainMode; Result.RxGainDb := FRxGainDb; + Result.AttenDB := FAtten; // шаговый аттенюатор ADC — тоже на диапазон // Waterfall AGC/NF — глобальные (не диапазонные) end; @@ -2846,6 +2859,7 @@ begin FXvtrSettings.Entries[i].LastPanSlider := FPanSlider; FXvtrSettings.Entries[i].LastRxGainMode := FRxGainMode; FXvtrSettings.Entries[i].LastRxGainDb := FRxGainDb; + FXvtrSettings.Entries[i].LastAttenDB := FAtten; end; FSettings.SaveXvtr(FDevMAC, FXvtrSettings); Exit; @@ -2975,6 +2989,11 @@ begin ApplyRxGainToBackend; 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 --- FFMSQOn := B.FMSQOn; FFMSQLevel := B.FMSQLevel; @@ -3646,12 +3665,20 @@ begin else FDSPEngine.SetDriveLevel(EnsureRange(Pct, 0, 100) / 100.0); end; -procedure TRadioController.SetAtten(Idx: Integer); +procedure TRadioController.SetAtten(Db: Integer); +// Шаговый аттенюатор ADC — плавный 0..31 дБ (как в железе openHPSDR), а не +// три фиксированные ступени. Уровни S-метра/спектра компенсируются на ту же +// величину (CalLookup), поэтому шкала dBm остаётся абсолютной. begin - if Idx < 0 then Idx := 0 else if Idx > 2 then Idx := 2; - FAtten := Idx; - if Assigned(FNetwork) then FNetwork.SetStepAtten(FAtten * 10); + FAtten := EnsureRange(Db, 0, 31); + if Assigned(FNetwork) then FNetwork.SetStepAtten(Byte(FAtten)); 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; procedure TRadioController.SetBand(Idx: Integer); @@ -3742,6 +3769,7 @@ begin B.FMRptOffsetHz := E.LastFMRptOffsetHz; B.RxGainMode := E.LastRxGainMode; // hw-AGC слота (AD936x) B.RxGainDb := E.LastRxGainDb; + B.AttenDB := E.LastAttenDB; // аттенюатор ADC слота B.VfoA := Vis; B.VfoB := FVfoB; ApplyBandDSP(B); diff --git a/Settings.pas b/Settings.pas index bb96fda..74cb5fe 100644 --- a/Settings.pas +++ b/Settings.pas @@ -103,6 +103,9 @@ type // сильно разное на 2м и на QO-100, поэтому режим и manual-гейн per-band. RxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid RxGainDb: Integer; // manual gain, дБ (0..73) + // Шаговый аттенюатор ADC (openHPSDR) — на диапазон: на 40м ночью нужен, + // на 10м нет. 0..31 дБ. + AttenDB: Integer; end; // Персист панадаптеров и слайсов (секция "pans" под MAC). Восстанавливается @@ -295,6 +298,7 @@ type // Аппаратный AGC AD936x на слот трансвертера (у QO-100 своё усиление тракта). LastRxGainMode: Integer; // 0=manual,1=fast,2=slow,3=hybrid LastRxGainDb: Integer; // manual gain, дБ + LastAttenDB: Integer; // шаговый аттенюатор ADC слота, дБ (0..31) end; TXvtrSettings = record @@ -558,6 +562,7 @@ begin B.SpanHz := 192000; B.RxGainMode := 2; // slow_attack (как глобальный дефолт Pluto) B.RxGainDb := 40; + B.AttenDB := 0; B.FMSQOn := False; B.FMSQLevel := 30; B.CTCSSOn := False; @@ -944,6 +949,7 @@ begin 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].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; @@ -1060,6 +1066,7 @@ begin 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,'rx_gain_mode',B.RxGainMode); JW(O,'rx_gain_db',B.RxGainDb); + JW(O,'atten_db',B.AttenDB); end; function TSettingsManager.LoadBand(const MAC: array of Byte; BandIdx: Integer; @@ -1091,6 +1098,7 @@ begin B.FMRptOffsetHz := JD(O,'fmrpt_offset', B.FMRptOffsetHz); 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.AttenDB := EnsureRange(JI(O,'atten_db', B.AttenDB), 0, 31); 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); end; @@ -1617,6 +1625,7 @@ begin X.Entries[i].LastPanSlider := 0.5; X.Entries[i].LastRxGainMode := 2; // slow_attack X.Entries[i].LastRxGainDb := 40; + X.Entries[i].LastAttenDB := 0; end; // Шаблоны (пользователь должен сам Enable, чтобы трансверторы появились) 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].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].LastAttenDB := EnsureRange(JI(EObj, 'last_atten_db', X.Entries[i].LastAttenDB), 0, 31); // Валидация: Last freqs должны быть в [FreqBegin..FreqEnd] if (X.Entries[i].LastFreq < X.Entries[i].FreqBegin) or (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_rx_gain_mode', X.Entries[i].LastRxGainMode); JW(EObj, 'last_rx_gain_db', X.Entries[i].LastRxGainDb); + JW(EObj, 'last_atten_db', X.Entries[i].LastAttenDB); end; end; diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 360d598..77b49b7 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -199,7 +199,7 @@ begin '' + '' + '' + - '' + + '
ATT0
' + '' + '' + '' + @@ -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("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("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 — фильтровый оверлей '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());}' + // Маппинг: 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){' + '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("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("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);' + 'paintSM(Number(cur.smeter_dbm||-130));' + 'updStatusBar();' + diff --git a/WebServer.pas b/WebServer.pas index 7c4f96a..ba8e098 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -296,7 +296,7 @@ type FDriveLevel: Integer; FOnFreqA: TWebCmdFreqA; FOnAttn: TWebCmdAttn; - FAttnIdx: Integer; + FAttnDB: Integer; // шаговый аттенюатор ADC, дБ (0..31) FOnTun: TWebCmdTun; FOnFMStep: TWebCmdFMStep; FTuning: Boolean; @@ -406,7 +406,7 @@ type CenterHz: Double; FilterIdx: Integer; VfoB: Double; ActiveVfo: Integer; Transmitting: Boolean; DriveLevel: Integer; - AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; + AttnDB: Integer; Tuning: Boolean; Duplex: Boolean; FwdW, SWRV, PAMaxPower: Double; const StatusText, BoardText, IPText, SupplyText, PLLText, RXText, TXText, SeqText: string); @@ -1572,10 +1572,10 @@ begin end else if Cmd = 'attn' then begin - Idx := JsonGetInt(Json, 'idx', FAttnIdx); + Idx := JsonGetInt(Json, 'db', FAttnDB); if Idx < 0 then Idx := 0; - if Idx > 2 then Idx := 2; - FStateLock.Enter; FAttnIdx := Idx; FStateLock.Leave; + if Idx > 31 then Idx := 31; + FStateLock.Enter; FAttnDB := Idx; FStateLock.Leave; if Assigned(FOnAttn) then FOnAttn(Idx); end else if Cmd = 'fm_step' then @@ -1778,7 +1778,7 @@ begin '"wf_agc":%s,"wf_nf":%s,"band_idx":%d,"smeter_dbm":%.1f,' + '"running":%s,"mute":%s,"ctun":%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,' + '"status_text":"%s","board_text":"%s","ip_text":"%s","supply_text":"%s",' + '"pll_text":"%s","rx_text":"%s","tx_text":"%s","seq_text":"%s",' + @@ -1809,7 +1809,7 @@ begin BoolToStr(FConnected, 'true', 'false'), BoolToStr(FTransmitting, 'true', 'false'), FDriveLevel, - FAttnIdx, + FAttnDB, BoolToStr(FTuning, 'true', 'false'), BoolToStr(FDuplex, 'true', 'false'), FFwdW, FSWR, FPAMaxPower, @@ -1889,7 +1889,7 @@ procedure TWebServer.PushSpectrum( CenterHz: Double; FilterIdx: Integer; VfoB: Double; ActiveVfo: Integer; Transmitting: Boolean; DriveLevel: Integer; - AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean; + AttnDB: Integer; Tuning: Boolean; Duplex: Boolean; FwdW, SWRV, PAMaxPower: Double; const StatusText, BoardText, IPText, SupplyText, PLLText, RXText, TXText, SeqText: string); @@ -1926,7 +1926,7 @@ begin FActiveVfo := ActiveVfo; FTransmitting := Transmitting; FDriveLevel := DriveLevel; - FAttnIdx := AttnIdx; + FAttnDB := AttnDB; FTuning := Tuning; FDuplex := Duplex; FFwdW := FwdW;