Phase 3 (batch 33+34): web device discovery/connect parity

Move discovery into the controller and add a full web device overlay
(discovery + connect + saved-device CRUD), reaching parity with the
desktop device dialog.

- RadioController: OnDeviceFound/Discover/DiscoverFinishedNone +
  rfDeviceList; TDiscoverThread moved in from MainForm.
- WebServer: device protocol (discover/connect/disconnect/dev_add/
  dev_remove/dev_autostart) + "devices" in BuildStateJson.
- MainForm: web handlers (marshalled), PushDeviceListToWeb, extracted
  DoStopAndDisconnect, detached WebOnRun from BtnStartStopClick;
  web connect resolves board type from discovered then saved by IP.
- DeviceForm: RefreshFound renders the found list from the store.
- WebPageHtml: DEV overlay; START opens it, STOP disconnects.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Uladzimir Karpenka
2026-06-08 15:30:03 +03:00
co-authored by Claude Opus 4.8
parent 9dec47a6ca
commit c4981fcb19
5 changed files with 488 additions and 135 deletions
+76 -2
View File
@@ -7,7 +7,9 @@ unit WebPageHtml;
HTML-страницу (одним файлом) для отправки клиенту по HTTP GET /.
ИСПРАВЛЕНИЯ:
- Кнопка START/STOP добавлена в тулбар 1 (шлёт set_run, зелёная/красная)
- Device-overlay (DEV): discovery/connect/saved-CRUD, паритет с десктоп-диалогом.
START теперь открывает overlay (если не подключены); STOP шлёт disconnect.
- Кнопка START/STOP добавлена в тулбар 1 (зелёная/красная)
- Кнопка RX/TX удалена из тулбара 2
- NR/NB/ANF: cur обновляется локально сразу при клике (иначе повторный
клик инвертировал уже отправленное значение)
@@ -202,6 +204,7 @@ begin
'<button id="anfBtn">ANF</button>' +
'<button id="nrBtn">NR</button>' +
'<span class="sl-sep">|</span>' +
'<button id="devBtn" title="Devices">DEV</button>' +
'<button id="startStopBtn" class="on">START</button>' +
'</div>' +
@@ -352,6 +355,74 @@ begin
'}' +
'q("fnpBtn").onclick=showFnp;' +
// ── Device discovery / connect / saved-CRUD overlay (паритет с десктоп-диалогом) ──
'var devOv=null,devList=null;' +
'function closeDev(){if(devOv){devOv.remove();devOv=null;devList=null;}}' +
'function devBtnS(bg,fg,bd){return "background:"+bg+";color:"+fg+";border:1px solid "+bd+";border-radius:4px;padding:4px 8px;font-family:monospace;font-size:11px;cursor:pointer;margin-left:4px;";}' +
'function renderDevList(){if(!devList)return;devList.innerHTML="";' +
'var devs=cur.devices||[];var conn=!!cur.connected;' +
'function mkRow(d,sIdx){' +
'var row=document.createElement("div");' +
'row.style.cssText="display:flex;align-items:center;justify-content:space-between;gap:6px;padding:5px 6px;border:1px solid rgba(55,95,55,0.4);border-radius:4px;margin-bottom:4px;background:rgba(12,28,12,0.7);";' +
'var lbl=document.createElement("span");' +
'lbl.textContent=d.saved?((d.autostart?"★ ":"")+(d.name||"HPSDR")+" "+d.ip):d.name;' +
'lbl.style.cssText="color:#9cc99c;font-family:monospace;font-size:11px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;";' +
'var btns=document.createElement("span");btns.style.cssText="display:flex;flex:none;";' +
'var cb=document.createElement("button");cb.textContent="Connect";' +
'cb.style.cssText=devBtnS("rgba(25,70,25,0.95)","#b0ffb0","rgba(120,220,120,0.7)");' +
'cb.onclick=function(){ws2({cmd:"connect",ip:d.ip});closeDev();};if(conn)cb.disabled=true;' +
'btns.appendChild(cb);' +
'if(d.saved){' +
'var ab=document.createElement("button");ab.textContent="★";ab.title="Auto-start";' +
'ab.style.cssText=devBtnS("rgba(12,28,12,0.85)",d.autostart?"#ffd060":"#7aaa7a","rgba(55,95,55,0.45)");' +
'ab.onclick=function(){ws2({cmd:"dev_autostart",idx:sIdx});};' +
'var rb=document.createElement("button");rb.textContent="✕";rb.title="Remove";' +
'rb.style.cssText=devBtnS("rgba(40,12,12,0.85)","#cc8080","rgba(95,55,55,0.45)");' +
'rb.onclick=function(){ws2({cmd:"dev_remove",idx:sIdx});};' +
'btns.appendChild(ab);btns.appendChild(rb);' +
'}else{' +
'var sv=document.createElement("button");sv.textContent="Save";' +
'sv.style.cssText=devBtnS("rgba(12,28,12,0.85)","#7aaa7a","rgba(55,95,55,0.45)");' +
'sv.onclick=function(){ws2({cmd:"dev_add",name:d.name||"",ip:d.ip});};' +
'btns.appendChild(sv);' +
'}' +
'row.appendChild(lbl);row.appendChild(btns);return row;' +
'}' +
'var si=-1,hasSaved=false,hasFound=false;' +
'devs.forEach(function(d){if(!d.saved)return;si++;' +
'if(!hasSaved){var h=document.createElement("div");h.textContent="Saved";h.style.cssText="color:#6a9a6a;font-size:10px;margin:2px 0 4px;letter-spacing:1px;";devList.appendChild(h);hasSaved=true;}' +
'devList.appendChild(mkRow(d,si));});' +
'devs.forEach(function(d){if(d.saved)return;' +
'if(!hasFound){var h=document.createElement("div");h.textContent="Discovered";h.style.cssText="color:#6a9a6a;font-size:10px;margin:8px 0 4px;letter-spacing:1px;";devList.appendChild(h);hasFound=true;}' +
'devList.appendChild(mkRow(d,-1));});' +
'if(!hasSaved&&!hasFound){var e=document.createElement("div");e.textContent="No devices — Discover or add manually";e.style.cssText="color:#6a8a6a;font-size:11px;font-style:italic;padding:6px 2px;";devList.appendChild(e);}' +
'}' +
'function showDev(){' +
'if(devOv){closeDev();return;}' +
'devOv=document.createElement("div");' +
'devOv.style.cssText="position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(6,14,6,0.95);border:1px solid rgba(70,120,70,0.65);border-radius:8px;padding:14px 16px;z-index:2000;backdrop-filter:blur(6px);box-shadow:0 4px 24px rgba(0,0,0,0.7);width:340px;max-width:92vw;max-height:80vh;overflow:auto;";' +
'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="Devices";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=closeDev;' +
'hd.appendChild(tl);hd.appendChild(xb);devOv.appendChild(hd);' +
'var ar=document.createElement("div");ar.style.cssText="display:flex;gap:6px;margin-bottom:10px;";' +
'var db=document.createElement("button");db.textContent="Discover";db.style.cssText=devBtnS("rgba(25,55,80,0.95)","#a0d0ff","rgba(90,150,210,0.7)")+"margin-left:0;flex:1;";db.onclick=function(){ws2({cmd:"discover"});};ar.appendChild(db);' +
'var dc=document.createElement("button");dc.textContent="Disconnect";dc.style.cssText=devBtnS("rgba(60,20,20,0.9)","#e0a0a0","rgba(150,80,80,0.6)")+"margin-left:0;flex:1;";dc.onclick=function(){ws2({cmd:"disconnect"});closeDev();};if(!cur.connected)dc.disabled=true;ar.appendChild(dc);' +
'devOv.appendChild(ar);' +
'devList=document.createElement("div");devOv.appendChild(devList);' +
'var mf=document.createElement("div");mf.style.cssText="margin-top:12px;border-top:1px solid rgba(55,95,55,0.35);padding-top:10px;display:flex;flex-direction:column;gap:6px;";' +
'var inS="background:rgba(0,0,0,0.4);border:1px solid rgba(55,95,55,0.5);border-radius:4px;color:#b0e0b0;font-family:monospace;font-size:11px;padding:5px 6px;width:100%;box-sizing:border-box;";' +
'var nm=document.createElement("input");nm.placeholder="Name (optional)";nm.style.cssText=inS;' +
'var ip=document.createElement("input");ip.placeholder="IP address";ip.style.cssText=inS;' +
'mf.appendChild(nm);mf.appendChild(ip);' +
'var mb=document.createElement("div");mb.style.cssText="display:flex;gap:6px;";' +
'var cob=document.createElement("button");cob.textContent="Connect";cob.style.cssText=devBtnS("rgba(25,70,25,0.95)","#b0ffb0","rgba(120,220,120,0.7)")+"margin-left:0;flex:1;";cob.onclick=function(){var v=ip.value.trim();if(v){ws2({cmd:"connect",ip:v});closeDev();}};' +
'var svb=document.createElement("button");svb.textContent="Save";svb.style.cssText=devBtnS("rgba(12,28,12,0.85)","#7aaa7a","rgba(55,95,55,0.45)")+"margin-left:0;flex:1;";svb.onclick=function(){var v=ip.value.trim();if(v){ws2({cmd:"dev_add",name:nm.value.trim(),ip:v});nm.value="";ip.value="";}};' +
'mb.appendChild(cob);mb.appendChild(svb);mf.appendChild(mb);devOv.appendChild(mf);' +
'renderDevList();document.body.appendChild(devOv);' +
'}' +
'q("devBtn").onclick=showDev;' +
'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});};' +
@@ -375,7 +446,9 @@ begin
// START/STOP
'function updRunBtn(r){var b=q("startStopBtn");if(!b)return;b.textContent=r?"STOP":"START";b.classList.toggle("rx-tx",!!r);b.classList.toggle("on",!r);}' +
'q("startStopBtn").onclick=function(){audioKick();cur.running=!cur.running;pend("run");updRunBtn(cur.running);cmd("set_run",cur.running);};' +
// Подключённы → STOP (disconnect). Иначе → device-overlay (выбор/подключение).
// set_run:true больше не открывает устройство (web стартует через connect).
'q("startStopBtn").onclick=function(){audioKick();if(cur.connected||cur.running){ws2({cmd:"disconnect"});cur.running=false;pend("run");updRunBtn(false);}else{showDev();}};' +
'function updMuteIc(){var mi=q("muteIc");if(mi)mi.textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";}' +
'q("muteIc").onclick=()=>{cur.mute=!cur.mute;updMuteIc();pend("mute");cmd("set_mute",cur.mute);};' +
@@ -645,6 +718,7 @@ begin
'updBand(activeVfo==="A"?(cur.vfo_a_hz||0):(vfoB||0));updSpan(cur.span_hz||192000);' +
'paintSM(Number(cur.smeter_dbm||-130));' +
'updStatusBar();' +
'if(devList)renderDevList();' +
'const nowTx=!!cur.transmitting;if(nowTx!==prevTx){prevTx=nowTx;if(nowTx)startTXMic();else stopTXMic();}}' +
'function pal(db){const v=Math.round(cl((db-wfLo)/Math.max(1,wfHi-wfLo),0,1)*255);const c=PAL[v]|0;return[(c>>16)&255,(c>>8)&255,c&255];}' +