Add configurable VFO frequency range with GHz support

- Settings > Display: new "VFO Display" control to select max frequency
  range (999 MHz / 9.999 GHz / 99.999 GHz), persisted as freq_mhz_digits
- FreqDisplay: extend digit map [0..10] to support up to 11 digit positions;
  keyboard navigation adapts to MinMhzDigits dynamically
- MainForm/LayoutTopVfoBlock: VFO panel width scales automatically with
  selected digit count using dynamic text measurement
- Web VFO: fix broken HTML (class attr not closed before data-s — wheel and
  hover were completely non-functional); rewrite fmtVfo to use variable MHz
  digit count synced from server state; fix missing </span> for .vfo-hz
- WebServer: publish freq_mhz_digits in state JSON; updated from
  ApplyFreqMhzDigits so desktop and web stay in sync
- Fix Int32 overflow on frequencies above ~2.15 GHz: CATEngine.Pad V param
  Int64, FormatFreq/FormatFreqSV Mhz var Int64 (Mhz*1000000 overflowed)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-15 13:13:52 +03:00
co-authored by Claude Sonnet 4.6
parent 5ac7ddc976
commit efc21b92e6
8 changed files with 105 additions and 35 deletions
+2 -2
View File
@@ -88,7 +88,7 @@ type
FCtx: TCATContext;
FLock: TCriticalSection;
function Pad(V, W: Integer): string;
function Pad(V: Int64; W: Integer): string;
function FreqToStr(Hz: Double): string;
function StrToFreq(const S: string): Double;
function ModeToK(M: Integer): Integer;
@@ -311,7 +311,7 @@ begin if B then Result := '1' else Result := '0'; end;
{ ── Helpers ──────────────────────────────────────────────────────────────── }
function TCATEngine.Pad(V, W: Integer): string;
function TCATEngine.Pad(V: Int64; W: Integer): string;
begin
Result := IntToStr(Abs(V));
while Length(Result) < W do Result := '0' + Result;
+9 -9
View File
@@ -36,8 +36,8 @@ type
FCenterText: Boolean;
FOnChange: TFreqChangeEvent;
FHoverDigit: Integer; // 0=единицы .. 8=100МГц, -1=нет
FDigitX: array[0..8] of Integer; // X левого края каждой цифры
FHoverDigit: Integer; // 0=единицы .. 10=10ГГц, -1=нет
FDigitX: array[0..10] of Integer; // X левого края каждой цифры
FDigitW: Integer;
FCharH: Integer;
@@ -125,7 +125,7 @@ var
begin
Result := -1;
if FDigitW = 0 then Exit;
for i := 0 to 8 do
for i := 0 to 10 do
if (X >= FDigitX[i]) and (X < FDigitX[i] + FDigitW) then
begin
Result := i;
@@ -137,14 +137,14 @@ end;
// Цифры нумеруются справа налево: digit0=единицы, digit8=сотни млн
procedure TFreqDisplay.BuildDigitMap(const S: string; StartX: Integer);
var
Xs: array[0..11] of Integer; // X каждого символа строки (макс 12 симв)
Xs: array[0..13] of Integer; // X каждого символа строки (макс 14 симв)
ci: Integer;
xi: Integer;
dIdx: Integer;
len: Integer;
begin
len := Length(S);
if len > 12 then len := 12;
if len > 13 then len := 13;
xi := StartX;
for ci := 0 to len - 1 do
@@ -159,7 +159,7 @@ begin
begin
if S[ci + 1] <> '.' then
begin
if dIdx <= 8 then
if dIdx <= 10 then
FDigitX[dIdx] := Xs[ci];
Inc(dIdx);
end;
@@ -185,7 +185,7 @@ var
ch: Char;
dIdx: Integer;
col: TColor;
DiChar: array[0..11] of Integer;
DiChar: array[0..13] of Integer;
tmpIdx: Integer;
len: Integer;
begin
@@ -234,7 +234,7 @@ begin
// Строим обратную карту символ → digit index
len := Length(S);
if len > 12 then len := 12;
if len > 13 then len := 13;
tmpIdx := 0;
for ci := len - 1 downto 0 do
begin
@@ -385,7 +385,7 @@ begin
VK_DOWN: begin ChangeByDigit(d, -1); Key := 0; end;
VK_LEFT: begin
if FHoverDigit < 0 then FHoverDigit := 3;
if FHoverDigit < 8 then Inc(FHoverDigit);
if FHoverDigit < FMinMhzDigits + 5 then Inc(FHoverDigit);
Invalidate; Key := 0;
end;
VK_RIGHT: begin
+28 -5
View File
@@ -278,6 +278,7 @@ type
FBandCache: array[0..CFG_BAND_COUNT-1] of TBandSettings; // кэш диапазонов
FDevConnected: Boolean; // True после первого подключения
FLightTheme: Boolean; // True = светлая тема
FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz
FCenterFreq: Double; // центр спектра = LO (DDC). При CTUN ON не меняется при кручении VFO
FSpanHz: Double;
FSampleRate: Integer; // текущий DDC sample rate (Hz)
@@ -630,6 +631,7 @@ type
function BuildMicLineSelectByte: Byte;
procedure ApplyVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure ApplyFPS(FPS: Integer);
procedure ApplyFreqMhzDigits(Digits: Integer);
published
// Обработчики событий формы — должны быть в published для RTTI/LFM
@@ -919,6 +921,7 @@ begin
Result.DisplayDuplex := FDisplayDuplex;
Result.DisplayFPS := FDisplayFPS;
Result.LightTheme := FLightTheme;
Result.FreqMhzDigits := FFreqMhzDigits;
// Audio device names
Result.AudioOutDevice := FAudioOutDevName;
Result.AudioInDevice := FAudioInDevName;
@@ -1182,6 +1185,7 @@ begin
FShowSpectrum := True;
FShowWaterfall := True;
FDisplayFPS := 60;
FFreqMhzDigits := 3;
FSplitterRatio := 0.40;
FSplitterDrag := False;
@@ -1992,7 +1996,7 @@ begin
Canvas.Font.Style := [fsBold];
FreqDispA.Left := VFO_BTN_W + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
FreqDispB.Left := VFO_B_LEFT_PAD + VFO_BTN_W + VFO_BTN_FREQ_GAP - VFO_FREQ_LEFT_PAD;
FreqTextW := Canvas.TextWidth('000.000.000');
FreqTextW := Canvas.TextWidth(StringOfChar('0', Max(3, FFreqMhzDigits)) + '.000.000');
NeedFreqAW := FreqDispA.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
NeedFreqBW := FreqDispB.Left + FreqTextW + VFO_FREQ_LEFT_PAD + VFO_FREQ_RIGHT_PAD;
TopVfoAW := NeedFreqAW;
@@ -2448,12 +2452,13 @@ end;
function TMainForm.FormatFreq(Hz: Double): string;
var
Mhz, KHz, Rest: Integer;
Mhz: Int64;
KHz, Rest: Integer;
begin
Mhz := Trunc(Hz / 1000000);
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
Rest := Trunc(Hz) mod 1000;
Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
procedure TMainForm.UpdateVfoDisplay;
@@ -3568,6 +3573,7 @@ begin
if BtnDUP <> nil then StyleButton(BtnDUP, FDisplayDuplex);
if G_Settings.DisplayFPS > 0 then
ApplyFPS(G_Settings.DisplayFPS);
ApplyFreqMhzDigits(EnsureRange(G_Settings.FreqMhzDigits, 3, 5));
// Настройки дисплея и сетки
if G_Settings.FFTSize > 0 then
begin
@@ -6116,6 +6122,21 @@ begin
FDSPEngine.SetDisplayFPS(FPS);
end;
procedure TMainForm.ApplyFreqMhzDigits(Digits: Integer);
const
MaxFreqs: array[3..5] of Int64 = (999999999, 9999999999, 99999999999);
begin
Digits := EnsureRange(Digits, 3, 5);
FFreqMhzDigits := Digits;
FreqDispA.MinMhzDigits := Digits;
FreqDispB.MinMhzDigits := Digits;
FreqDispA.MaxFreq := MaxFreqs[Digits];
FreqDispB.MaxFreq := MaxFreqs[Digits];
if Assigned(FWebServer) then
FWebServer.FreqMhzDigits := Digits;
LayoutTopVfoBlock;
end;
procedure TMainForm.BtnSettingsClick(Sender: TObject);
var
SF: TSettingsForm;
@@ -6133,8 +6154,9 @@ begin
SF.OnFPSChange := ApplyFPS;
SF.OnPAChange := OnPASettingsChange;
SF.OnCATChange := OnCATSettingsChange;
SF.OnThemeChange := SetLightTheme;
SF.OnTXChange := OnTXSettingsChange;
SF.OnThemeChange := SetLightTheme;
SF.OnFreqMhzDigitsChange := ApplyFreqMhzDigits;
SF.OnTXChange := OnTXSettingsChange;
SF.OnAlexChange := OnAlexSettingsChange;
SF.OnXvtrChange := OnXvtrSettingsChange;
end;
@@ -6158,6 +6180,7 @@ begin
SF.LoadVisibility(FShowSpectrum, FShowWaterfall);
SF.LoadFPS(FDisplayFPS);
SF.LoadLightTheme(FLightTheme);
SF.LoadFreqMhzDigits(FFreqMhzDigits);
// Загружаем текущие значения
if FWDSPReady then
SF.LoadValues(
+5
View File
@@ -194,6 +194,8 @@ type
DisplayFPS: Integer; // spectrum/waterfall timer fps (1..100)
// --- Theme ---
LightTheme: Boolean; // False = dark (default), True = light
// --- VFO display digits ---
FreqMhzDigits: Integer; // minimum MHz digit count: 3=999MHz, 4=9.999GHz, 5=99.999GHz
// --- PA (Power Amplifier) settings ---
PAMaxPower: Double; // максимальная выходная мощность, Вт (5..200)
PABandCal: array[0..CFG_BAND_COUNT-1] of Double; // калибровка на диапазон 38.8..100.0
@@ -346,6 +348,7 @@ begin
G.ShowWaterfall := True;
G.DisplayDuplex := False;
G.DisplayFPS := 60;
G.FreqMhzDigits := 3;
// CAT defaults
for i := 0 to 3 do begin
G.CATSerialEnabled[i] := False;
@@ -557,6 +560,7 @@ begin
G.DisplayDuplex := JB(GObj,'display_duplex',False);
G.DisplayFPS := JI(GObj,'display_fps',60);
G.LightTheme := JB(GObj,'light_theme',False);
G.FreqMhzDigits := EnsureRange(JI(GObj,'freq_mhz_digits',3), 3, 5);
// PA settings
G.PAMaxPower := JD(GObj,'pa_max_power',100.0);
for i := 0 to CFG_BAND_COUNT-1 do
@@ -636,6 +640,7 @@ begin
JW(O,'display_duplex',G.DisplayDuplex);
JW(O,'display_fps',G.DisplayFPS);
JW(O,'light_theme',G.LightTheme);
JW(O,'freq_mhz_digits',G.FreqMhzDigits);
// PA settings
JW(O,'pa_max_power',G.PAMaxPower);
for i := 0 to CFG_BAND_COUNT-1 do
+34 -2
View File
@@ -36,6 +36,7 @@ type
// TX-настройки целиком (любая правка → один callback с актуальной структурой)
TOnTXSettingsChange = procedure(const T: TTXSettings) of object;
TOnFreqMhzDigitsChange = procedure(Digits: Integer) of object;
TOnDisplayParamChange = procedure(FFTSize, WindowType, SpecDetector,
SpecAvgMode: Integer; SpecAvgTimeMS: Double) of object;
TOnWaterfallParamChange = procedure(WfDetector, WfAvgMode: Integer;
@@ -109,6 +110,7 @@ type
FChkShowWaterfall: TCheckBox;
FCmbFPS: TComboBox;
FChkLightTheme: TCheckBox;
FCmbFreqMhzDigits: TComboBox;
// ---- PA Settings tab controls ----
FEdMaxPower: TSpinEdit;
@@ -243,8 +245,9 @@ type
FOnAudioInDevChange: TOnAudioInDevChange;
FOnVisibilityChange: TOnVisibilityChange;
FOnFPSChange: TOnFPSChange;
FOnThemeChange: TOnThemeChange;
FOnTXChange: TOnTXSettingsChange;
FOnThemeChange: TOnThemeChange;
FOnFreqMhzDigitsChange: TOnFreqMhzDigitsChange;
FOnTXChange: TOnTXSettingsChange;
FLoading: Boolean;
@@ -298,6 +301,7 @@ type
procedure OnVisibilityChkChange(Sender: TObject);
procedure OnFPSCmbChange(Sender: TObject);
procedure OnLightThemeChkChange(Sender: TObject);
procedure OnFreqMhzDigitsCmbChange(Sender: TObject);
procedure OnMaxPowerChange(Sender: TObject);
procedure OnBandCalChange(Sender: TObject);
procedure OnCATSerialChange(Sender: TObject);
@@ -335,6 +339,7 @@ type
procedure LoadVisibility(ShowSpectrum, ShowWaterfall: Boolean);
procedure LoadFPS(FPS: Integer);
procedure LoadLightTheme(ALight: Boolean);
procedure LoadFreqMhzDigits(Digits: Integer);
procedure LoadPASettings(MaxPower: Double; const BandCal: array of Double);
procedure LoadCATSettings(
const SerEnabled: array of Boolean;
@@ -357,6 +362,7 @@ type
property OnVisibilityChange: TOnVisibilityChange read FOnVisibilityChange write FOnVisibilityChange;
property OnFPSChange: TOnFPSChange read FOnFPSChange write FOnFPSChange;
property OnThemeChange: TOnThemeChange read FOnThemeChange write FOnThemeChange;
property OnFreqMhzDigitsChange: TOnFreqMhzDigitsChange read FOnFreqMhzDigitsChange write FOnFreqMhzDigitsChange;
property OnTXChange: TOnTXSettingsChange read FOnTXChange write FOnTXChange;
end;
@@ -783,6 +789,15 @@ begin
FChkLightTheme.Font.Name := UI_FONT;
FChkLightTheme.Font.Size := 9;
FChkLightTheme.OnChange := OnLightThemeChkChange;
Grp := MakeGroupPanel(FPageDisplay, 'VFO Display', MARGIN, 378, 650, 92);
MakeLbl(Grp, 'Frequency range', PAD, R1 + 5, LW);
FCmbFreqMhzDigits := MakeCombo(Grp, CX, R1, 240, OnFreqMhzDigitsCmbChange);
FCmbFreqMhzDigits.Items.Add('up to 999 MHz (3 digits)');
FCmbFreqMhzDigits.Items.Add('up to 9.999 GHz (4 digits)');
FCmbFreqMhzDigits.Items.Add('up to 99.999 GHz (5 digits)');
FCmbFreqMhzDigits.ItemIndex := 0;
end;
// ---------------------------------------------------------------------------
@@ -2014,6 +2029,23 @@ begin
else ApplyTheme(DarkTheme);
end;
procedure TSettingsForm.LoadFreqMhzDigits(Digits: Integer);
begin
FLoading := True;
try
FCmbFreqMhzDigits.ItemIndex := EnsureRange(Digits - 3, 0, 2);
finally
FLoading := False;
end;
end;
procedure TSettingsForm.OnFreqMhzDigitsCmbChange(Sender: TObject);
begin
if FLoading then Exit;
if Assigned(FOnFreqMhzDigitsChange) then
FOnFreqMhzDigitsChange(FCmbFreqMhzDigits.ItemIndex + 3);
end;
procedure TSettingsForm.LoadPASettings(MaxPower: Double;
const BandCal: array of Double);
var i, n: Integer;
+2 -2
View File
@@ -371,12 +371,12 @@ begin
end;
function FormatFreqSV(Hz: Double): string;
var Mhz, KHz, Rest: Integer;
var Mhz: Int64; KHz, Rest: Integer;
begin
Mhz := Trunc(Hz / 1000000);
KHz := Trunc((Hz - Mhz * 1000000) / 1000);
Rest := Trunc(Hz) mod 1000;
Result := Format('%3d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
Result := Format('%d.%3.3d.%3.3d', [Mhz, KHz, Rest]);
end;
// ════════════════════════════════════════════════════════════════════════════
+18 -11
View File
@@ -323,22 +323,28 @@ begin
'q("muteIc").onclick=()=>{cur.mute=!cur.mute;q("muteIc").textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";cmd("set_mute",cur.mute);};' +
'let stA=0,stAmem=100,stB=0,stBmem=100;' +
'let freqMhzDigits=3,freqMaxHz=999999999;' +
'function fmtVfo(hz,step){' +
'const s=String(Math.max(0,Math.round(hz||0))).padStart(9,"0");' +
'const N=freqMhzDigits+6;' +
'const s=String(Math.max(0,Math.round(hz||0))).padStart(N,"0");' +
'let h="",seenNZ=false;' +
'for(let i=0;i<6;i++){' +
'const st=Math.pow(10,8-i);' +
'for(let i=0;i<freqMhzDigits;i++){' +
'const st=Math.pow(10,freqMhzDigits+5-i);' +
'if(s[i]!=="0")seenNZ=true;' +
'const lead=(!seenNZ)?" lead":"";' +
'const sel=(step>0&&st===step)?" sel":"";' +
'h+="<span class=\"dg"+lead+sel+" data-s=\""+st+"\">"+s[i]+"</span>";' +
'if(i===2)h+="<span class=\"sp\">.</span>";}' +
'h+="<span class=\"dg"+lead+sel+"\" data-s=\""+st+"\">"+s[i]+"</span>";}' +
'h+="<span class=\"sp\">.</span>";' +
'h+="<span class=\"vfo-hz\">";' +
'for(let i=6;i<9;i++){const st=Math.pow(10,8-i);' +
'for(let i=freqMhzDigits;i<freqMhzDigits+3;i++){' +
'const st=Math.pow(10,freqMhzDigits+5-i);' +
'const sel=(step>0&&st===step)?" sel":"";' +
'h+="<span class=\"dg"+sel+" data-s=\""+st+"\">"+s[i]+"</span>";}' +
'return h;}' +
'h+="<span class=\"dg"+sel+"\" data-s=\""+st+"\">"+s[i]+"</span>";}' +
'h+="<span class=\"sp\">.</span><span class=\"vfo-hz\">";' +
'for(let i=freqMhzDigits+3;i<N;i++){' +
'const st=Math.pow(10,freqMhzDigits+5-i);' +
'const sel=(step>0&&st===step)?" sel":"";' +
'h+="<span class=\"dg"+sel+"\" data-s=\""+st+"\">"+s[i]+"</span>";}' +
'h+="</span>";return h;}' +
'function renderVfos(){q("vDispA").innerHTML=fmtVfo(vfoA,stA);q("vDispB").innerHTML=fmtVfo(vfoB,stB);}' +
'function bindVfo(dispId,getHz,setHz,getSt,setSt,getMem,setMem,onSend){' +
@@ -348,12 +354,12 @@ begin
'el.addEventListener("mouseleave",()=>{if(getSt()!==0){setSt(0);renderVfos();}});' +
'el.addEventListener("wheel",e=>{e.preventDefault();e.stopPropagation();audioKick();' +
'const dir=e.deltaY<0?1:-1,st=getSt()||getMem()||100;' +
'const nf=cl(Math.round(getHz())+st*dir,30000,60000000);' +
'const nf=cl(Math.round(getHz())+st*dir,30000,freqMaxHz);' +
'setHz(nf);onSend(nf);},{passive:false});' +
'el.addEventListener("dblclick",()=>{' +
'const s=prompt("Set Hz",String(Math.round(getHz())));if(!s)return;' +
'const f=parseInt(s.replace(/[^0-9]/g,""),10);if(!isFinite(f))return;' +
'const nf=cl(f,30000,60000000);setHz(nf);renderVfos();onSend(nf);' +
'const nf=cl(f,30000,freqMaxHz);setHz(nf);renderVfos();onSend(nf);' +
'});}' +
'bindVfo("vDispA",()=>vfoA,v=>{vfoA=v;},()=>stA,v=>{stA=v;},()=>stAmem,v=>{stAmem=v;},' +
'function(nf){renderVfos();pend("freq");if(activeVfo==="A"){localCenter=nf;ws2({cmd:"freq",hz:nf});}else ws2({cmd:"freq_b",hz:nf});});' +
@@ -483,6 +489,7 @@ begin
'}' +
'if(!isPending("freq")){vfoA=cur.vfo_a_hz||vfoA;}' +
'if(msg.vfo_b_hz!=null)vfoB=msg.vfo_b_hz;' +
'if(msg.freq_mhz_digits!=null){freqMhzDigits=Math.max(3,Math.min(5,msg.freq_mhz_digits|0));freqMaxHz=Math.pow(10,freqMhzDigits+6)-1;}' +
'if(msg.active_vfo!=null&&!isPending("freq")){' +
'const sv=msg.active_vfo===0?"A":"B";' +
'if(sv!==activeVfo){activeVfo=sv;' +
+7 -4
View File
@@ -241,7 +241,8 @@ type
FAttnIdx: Integer;
FOnTun: TWebCmdTun;
FTuning: Boolean;
FDuplex: Boolean;
FDuplex: Boolean;
FFreqMhzDigits: Integer; // 3=999MHz, 4=9.999GHz, 5=99.999GHz
FXvtrBands: TWebXvtrArray; // список enabled XVTR (для web-UI)
FCurrentXvtr: Integer; // -1 = HF, иначе индекс активного XVTR
FOnXvtrBand: TWebCmdXvtrBand;
@@ -292,6 +293,7 @@ type
AttnIdx: Integer; Tuning: Boolean; Duplex: Boolean);
property WebClientActive: Boolean read FWebClientActive;
property FreqMhzDigits: Integer read FFreqMhzDigits write FFreqMhzDigits;
property OnFreq: TWebCmdFreq read FOnFreq write FOnFreq;
property OnMode: TWebCmdMode read FOnMode write FOnMode;
@@ -419,7 +421,8 @@ begin
FVolume := 70;
FSMeter := -120;
FBandIdx := 5;
FCurrentXvtr := -1;
FCurrentXvtr := -1;
FFreqMhzDigits := 3;
SetLength(FXvtrBands, 0);
end;
@@ -1233,7 +1236,7 @@ begin
'"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,' +
'"xvtr_current":%d,"xvtr_bands":%s}',
'"xvtr_current":%d,"xvtr_bands":%s,"freq_mhz_digits":%d}',
[FFreq, FVfoB, FActiveVfo,
FMode, MODE_N[FMode mod 8],
FFilterIdx, FFilterBW,
@@ -1257,7 +1260,7 @@ begin
FAttnIdx,
BoolToStr(FTuning, 'true', 'false'),
BoolToStr(FDuplex, 'true', 'false'),
FCurrentXvtr, XvtrJson
FCurrentXvtr, XvtrJson, FFreqMhzDigits
], FS);
finally
FStateLock.Leave;