diff --git a/HPSDRNetwork.pas b/HPSDRNetwork.pas index 1ac61e2..1b1e644 100644 --- a/HPSDRNetwork.pas +++ b/HPSDRNetwork.pas @@ -141,6 +141,7 @@ type FIsTransmitting: Boolean; FPAEnabled: Boolean; FAlexEnabled: Boolean; + FStepAtten: Byte; // ADC0 step attenuator 0-31 dB FSendLock: TCriticalSection; // защита concurrent UDP sends @@ -190,6 +191,7 @@ type DriveLevel: Byte = 100); procedure UpdateState(RXFreqHz, TXFreqHz: Double; DriveLevel: Byte; Transmitting, PAEnabled, AlexEnabled: Boolean); + procedure SetStepAtten(dB: Byte); procedure SendFullHP; procedure SendDDCAudio(const LeftRight: array of SmallInt); procedure SendDUCIQ(const IData, QData: array of Integer); @@ -1208,6 +1210,13 @@ begin FAlexEnabled := AlexEnabled; end; +procedure THPSDRNetwork.SetStepAtten(dB: Byte); +begin + if dB > 31 then dB := 31; + FStepAtten := dB; + if FConnected then SendFullHP; +end; + procedure THPSDRNetwork.SendFullHP; var Buf: array[0..1443] of Byte; @@ -1274,8 +1283,8 @@ begin // Step attenuators ADC0/ADC1 (bytes 1443/1442) // 0 dB = 0, max 31 dB - Buf[1443] := 0; // ADC0 attenuation - Buf[1442] := 0; // ADC1 attenuation + Buf[1443] := FStepAtten; // ADC0 attenuation + Buf[1442] := FStepAtten; // ADC1 attenuation DoSendTo(FSocket, Buf, SizeOf(Buf), FDevice.IPAddress, FPortHPFromPC); end; diff --git a/MainForm.pas b/MainForm.pas index ce72ce0..48d0c5f 100644 --- a/MainForm.pas +++ b/MainForm.pas @@ -197,6 +197,7 @@ type FHWPTTActive: Boolean; // True = hardware PTT нажата FMuted: Boolean; FVolume: Integer; + FAtten: Integer; // RX step attenuator index: 0=0dB 1=10dB 2=20dB FLastSMeter: Double; FSMeterPeak: Double; // верхняя граница светлой зоны FSMeterMin: Double; // нижняя граница светлой зоны @@ -489,6 +490,9 @@ type procedure WebOnCenter(Hz: Double); procedure WebOnMOX(On_: Boolean); procedure WebOnDrive(V: Integer); + procedure WebOnFreqA(Hz: Double); + procedure WebOnAttn(Idx: Integer); + procedure WebOnTun(On_: Boolean); // Synchronize-обёртки (выполняются в UI-потоке) procedure SyncWebFreq; procedure SyncWebMode; @@ -512,6 +516,9 @@ type procedure SyncWebCenter; procedure SyncWebMOX; procedure SyncWebDrive; + procedure SyncWebFreqA; + procedure SyncWebAttn; + procedure SyncWebTun; // --- CAT callbacks ------------------------------------------------------- function CATGetVfoA: Double; function CATGetVfoB: Double; @@ -1091,6 +1098,9 @@ begin FWebServer.OnCenter := WebOnCenter; FWebServer.OnMOX := WebOnMOX; FWebServer.OnDrive := WebOnDrive; + FWebServer.OnFreqA := WebOnFreqA; + FWebServer.OnAttn := WebOnAttn; + FWebServer.OnTun := WebOnTun; FWebServer.Start; FillChar(FCATLastGlobal, SizeOf(FCATLastGlobal), 0); FCATLastGlobal.CATTcpPort := 19090; @@ -2765,7 +2775,8 @@ begin BtnNR.Tag, BtnNB.Tag, BtnSNB.Tag <> 0, BtnANF.Tag <> 0, FCenterFreq, FFilter, FVfoB, FActiveVfo, - FTransmitting, TrkDrive.Position); + FTransmitting, TrkDrive.Position, + FAtten, FTuning); end; // =========================================================================== @@ -5204,6 +5215,43 @@ begin TrkDrive.Position := FWebSyncInt; end; +procedure TMainForm.WebOnFreqA(Hz: Double); +begin + FWebSyncFreq := Hz; + FWebSyncM := SyncWebFreqA; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebFreqA; +begin + ApplyVfoA(Round(FWebSyncFreq)); +end; + +procedure TMainForm.WebOnAttn(Idx: Integer); +begin + FWebSyncInt := Idx; + FWebSyncM := SyncWebAttn; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebAttn; +begin + FAtten := Max(0, Min(2, FWebSyncInt)); + FNetwork.SetStepAtten(FAtten * 10); +end; + +procedure TMainForm.WebOnTun(On_: Boolean); +begin + FWebSyncBool := On_; + FWebSyncM := SyncWebTun; + TThread.Synchronize(nil, FWebSyncM); +end; + +procedure TMainForm.SyncWebTun; +begin + ApplyTUN(FWebSyncBool); +end; + procedure TMainForm.TrkDriveChange(Sender: TObject); begin FDriveLevel := CalcDriveByte; diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 8ee627e..af6416a 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -205,12 +205,67 @@ begin 'q("modGrpSel").onchange=function(){const grp=GRP_MAP[modGrp()];const defaultMode=grp[0];cur.mode=defaultMode;updModTglLabel();pend("mode");ws2({cmd:"mode",mode:defaultMode});};' + 'q("modTglBtn").onclick=function(){audioKick();const next=modTglTarget();cur.mode=next;updModTglLabel();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});};' + + + // 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"],' + + 'cw:["1.0k","800","750","600","500","400","250","100","50","25"],' + + 'am:["16k","12k","10k","8k","6.6k","5.2k","4.0k","3.1k","2.9k","2.4k"],' + + 'fm:["20k","15k","12k","10k","8k","6k","5k","4k","3k","2k"]};' + + 'var FNP_BW={ssb:[5000,4400,3800,3300,2900,2700,2400,2100,1800,1000],' + + 'cw:[1000,800,750,600,500,400,250,100,50,25],' + + 'am:[16000,12000,10000,8000,6600,5200,4000,3100,2900,2400],' + + 'fm:[20000,15000,12000,10000,8000,6000,5000,4000,3000,2000]};' + + 'var fnpOv=null;' + + 'function fnpGrp(){var m=cur.mode|0;return(m===3||m===4)?"cw":(m===5)?"fm":(m===2||m===6||m===7)?"am":"ssb";}' + + 'function closeFnp(){if(fnpOv){fnpOv.remove();fnpOv=null;}document.removeEventListener("click",fnpOutside);}' + + 'function fnpOutside(e){if(fnpOv&&!fnpOv.contains(e.target)&&e.target!==q("fnpBtn"))closeFnp();}' + + 'function showFnp(){' + + 'if(fnpOv){closeFnp();return;}' + + 'var g=fnpGrp(),ns=FNP_N[g],bws=FNP_BW[g],cb=cur.filter_bw||2700;' + + 'fnpOv=document.createElement("div");' + + 'fnpOv.style.cssText="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);' + + 'background:rgba(6,14,6,0.93);border:1px solid rgba(70,120,70,0.65);border-radius:8px;' + + 'padding:12px 14px;z-index:2000;backdrop-filter:blur(6px);box-shadow:0 4px 24px rgba(0,0,0,0.7);min-width:260px;";' + + // заголовок + 'var hd=document.createElement("div");' + + 'hd.style.cssText="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;";' + + 'var tl=document.createElement("span");' + + 'tl.textContent="Filter — "+g.toUpperCase();' + + 'tl.style.cssText="color:#80b880;font-family:monospace;font-size:12px;letter-spacing:1px;";' + + 'var xb=document.createElement("button");' + + 'xb.textContent="×";' + + 'xb.style.cssText="background:none;border:none;color:#6a9a6a;font-size:16px;cursor:pointer;padding:0 2px;line-height:1;";' + + 'xb.onclick=closeFnp;' + + 'hd.appendChild(tl);hd.appendChild(xb);fnpOv.appendChild(hd);' + + // сетка кнопок + 'var gd=document.createElement("div");' + + 'gd.style.cssText="display:grid;grid-template-columns:repeat(5,1fr);gap:5px;";' + + 'ns.forEach(function(n,i){' + + 'var b=document.createElement("button"),act=bws[i]===cb;' + + 'b.textContent=n;' + + 'b.style.cssText="background:"+(act?"rgba(25,70,25,0.95)":"rgba(12,28,12,0.85)")+";' + + 'color:"+(act?"#b0ffb0":"#7aaa7a")+";border:1px solid "+(act?"rgba(120,220,120,0.7)":"rgba(55,95,55,0.45)")+";' + + 'border-radius:4px;padding:5px 0;font-family:monospace;font-size:12px;cursor:pointer;' + + 'transition:background 0.1s;";' + + 'b.onmouseenter=function(){if(!act)this.style.background="rgba(20,50,20,0.95)";}; ' + + 'b.onmouseleave=function(){if(!act)this.style.background="rgba(12,28,12,0.85)";}; ' + + 'b.onclick=function(){cur.filter_bw=bws[i];pend("filter");ws2({cmd:"filter",bw:bws[i]});closeFnp();};' + + 'gd.appendChild(b);' + + '});' + + 'fnpOv.appendChild(gd);' + + 'document.body.appendChild(fnpOv);' + + 'setTimeout(function(){document.addEventListener("click",fnpOutside);},0);' + + '}' + + 'q("fnpBtn").onclick=showFnp;' + 'q("volSl").oninput=function(e){ws2({cmd:"volume",v:+e.target.value});var v=q("volVal");if(v)v.textContent=e.target.value;};' + 'q("rfSl").oninput=function(e){ws2({cmd:"agctop",db:+e.target.value});var v=q("rfVal");if(v)v.textContent=e.target.value+"dB";};' + 'q("txSl").oninput=function(e){var v=+e.target.value;var lbl=q("txVal");if(lbl)lbl.textContent=v;pend("drive");ws2({cmd:"drive",v:v});};' + 'function updMoxBtn(tx){var b=q("moxBtn");if(!b)return;b.textContent=tx?"TX":"MOX";b.classList.toggle("mox-on",!!tx);}' + 'q("moxBtn").onclick=function(){audioKick();cur.transmitting=!cur.transmitting;pend("mox");updMoxBtn(cur.transmitting);cmd("set_mox",cur.transmitting);};' + + 'function updToneBtn(t){var b=q("toneBtn");if(!b)return;b.classList.toggle("on",!!t);}' + + 'q("toneBtn").onclick=function(){audioKick();cur.tuning=!cur.tuning;pend("tun");updToneBtn(cur.tuning);cmd("set_tun",cur.tuning);};' + // DSP кнопки — cur обновляется локально сразу, не ждём ответа сервера 'q("nrBtn").onclick=function(){cur.nr_mode=((cur.nr_mode|0)+1)%5;cur.nr=cur.nr_mode!==0;updNrBtn();pend("nr");ws2({cmd:"set_nr_mode",mode:cur.nr_mode});};' + @@ -274,18 +329,12 @@ begin 'q("vBtnA").onclick=()=>{if(activeVfo!=="A")selVfo("A");};' + 'q("vBtnB").onclick=()=>{if(activeVfo!=="B")selVfo("B");};' + // B=A: copy A freq into B - 'q("btnBA").onclick=()=>{vfoB=vfoA;renderVfos();ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' + - // A=B: copy B freq into A - 'q("btnAB").onclick=()=>{vfoA=vfoB;renderVfos();' + - 'if(activeVfo==="A")ws2({cmd:"freq",hz:Math.round(vfoA)});' + - 'else ws2({cmd:"freq_b",hz:Math.round(vfoA)});' + - // Actually A=B means set vfoA=vfoB, so send freq_a regardless of active - '};' + - // SWAP - 'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();' + - 'ws2({cmd:"freq",hz:Math.round(activeVfo==="A"?vfoA:vfoB)});' + - 'ws2({cmd:"freq_b",hz:Math.round(activeVfo==="A"?vfoB:vfoA)});' + - '};' + + 'q("btnBA").onclick=()=>{vfoB=vfoA;renderVfos();pend("freq");ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' + + // A=B: copy B freq into A — always freq_a (sets VFO A regardless of active VFO) + 'q("btnAB").onclick=()=>{vfoA=vfoB;renderVfos();pend("freq");ws2({cmd:"freq_a",hz:Math.round(vfoA)});};' + + // SWAP — freq_a always sets VFO A, freq_b always sets VFO B + 'q("btnSwap").onclick=()=>{const t=vfoA;vfoA=vfoB;vfoB=t;renderVfos();pend("freq");' + + 'ws2({cmd:"freq_a",hz:Math.round(vfoA)});ws2({cmd:"freq_b",hz:Math.round(vfoB)});};' + // SPAN кнопки: только 48K..1536K 'const SPANS=[48000,96000,192000,384000,768000,1536000];' + @@ -355,7 +404,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"};' + + '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"};' + 'function applyState(msg){' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + @@ -381,8 +430,10 @@ begin 'if(!isPending("snb"))updSnbBtn();if(!isPending("anf"))updAnfBtn();' + 'if(!isPending("run"))updRunBtn(!!cur.running);' + 'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' + + 'if(!isPending("tun"))updToneBtn(!!cur.tuning);' + '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);}' + + 'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||0;}' + 'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' + 'paintSM(Number(cur.smeter_dbm||-130));' + 'const mn=cur.mode_name||(MODE_N[cur.mode||0]||"?");' + diff --git a/WebServer.pas b/WebServer.pas index fbeb370..da60032 100644 --- a/WebServer.pas +++ b/WebServer.pas @@ -126,6 +126,9 @@ type TWebCmdCenter = procedure(Hz: Double) of object; TWebCmdMOX = procedure(On_: Boolean) of object; TWebCmdDrive = procedure(V: Integer) of object; + TWebCmdFreqA = procedure(Hz: Double) of object; + TWebCmdAttn = procedure(Idx: Integer) of object; + TWebCmdTun = procedure(On_: Boolean) of object; // ── Главный класс сервера ───────────────────────────────────────────────── TWebServer = class @@ -210,6 +213,11 @@ type FOnDrive: TWebCmdDrive; FTransmitting: Boolean; FDriveLevel: Integer; + FOnFreqA: TWebCmdFreqA; + FOnAttn: TWebCmdAttn; + FAttnIdx: Integer; + FOnTun: TWebCmdTun; + FTuning: Boolean; FWebClientActive: Boolean; @@ -253,7 +261,8 @@ type NRMode, NBMode: Integer; SNB, ANF: Boolean; CenterHz: Double; FilterIdx: Integer; VfoB: Double; ActiveVfo: Integer; - Transmitting: Boolean; DriveLevel: Integer); + Transmitting: Boolean; DriveLevel: Integer; + AttnIdx: Integer; Tuning: Boolean); property WebClientActive: Boolean read FWebClientActive; @@ -279,6 +288,9 @@ type property OnCenter: TWebCmdCenter read FOnCenter write FOnCenter; property OnMOX: TWebCmdMOX read FOnMOX write FOnMOX; property OnDrive: TWebCmdDrive read FOnDrive write FOnDrive; + property OnFreqA: TWebCmdFreqA read FOnFreqA write FOnFreqA; + property OnAttn: TWebCmdAttn read FOnAttn write FOnAttn; + property OnTun: TWebCmdTun read FOnTun write FOnTun; end; implementation @@ -1004,6 +1016,26 @@ begin if V > 100 then V := 100; FStateLock.Enter; FDriveLevel := V; FStateLock.Leave; if Assigned(FOnDrive) then FOnDrive(V); + end + else if Cmd = 'set_tun' then + begin + On_ := JsonGetBool(Json, 'on', FTuning); + FStateLock.Enter; FTuning := On_; FStateLock.Leave; + if Assigned(FOnTun) then FOnTun(On_); + end + else if Cmd = 'freq_a' then + begin + HzF := JsonGetFloat(Json, 'hz', FFreq); + FStateLock.Enter; FFreq := HzF; FStateLock.Leave; + if Assigned(FOnFreqA) then FOnFreqA(HzF); + end + else if Cmd = 'attn' then + begin + Idx := JsonGetInt(Json, 'idx', FAttnIdx); + if Idx < 0 then Idx := 0; + if Idx > 2 then Idx := 2; + FStateLock.Enter; FAttnIdx := Idx; FStateLock.Leave; + if Assigned(FOnAttn) then FOnAttn(Idx); end; end; @@ -1101,7 +1133,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}', + '"transmitting":%s,"drive":%d,"attn_idx":%d,"tuning":%s}', [FFreq, FVfoB, FActiveVfo, FMode, MODE_N[FMode mod 8], FFilterIdx, FFilterBW, @@ -1121,7 +1153,9 @@ begin BoolToStr(FANF, 'true', 'false'), BoolToStr(FConnected, 'true', 'false'), BoolToStr(FTransmitting, 'true', 'false'), - FDriveLevel + FDriveLevel, + FAttnIdx, + BoolToStr(FTuning, 'true', 'false') ]); finally FStateLock.Leave; @@ -1187,7 +1221,8 @@ procedure TWebServer.PushSpectrum( NRMode, NBMode: Integer; SNB, ANF: Boolean; CenterHz: Double; FilterIdx: Integer; VfoB: Double; ActiveVfo: Integer; - Transmitting: Boolean; DriveLevel: Integer); + Transmitting: Boolean; DriveLevel: Integer; + AttnIdx: Integer; Tuning: Boolean); var N, i: Integer; begin if not FRunning then Exit; @@ -1221,6 +1256,8 @@ begin FActiveVfo := ActiveVfo; FTransmitting := Transmitting; FDriveLevel := DriveLevel; + FAttnIdx := AttnIdx; + FTuning := Tuning; finally FStateLock.Leave; end;