From f1726785606d9fc28fc528e8c706bf4802e5adb2 Mon Sep 17 00:00:00 2001 From: rouggy Date: Sat, 10 Jan 2026 11:01:40 +0100 Subject: [PATCH] ultrabeam --- cmd/server/web/dist/assets/index-8_72Rq0c.js | 11 + cmd/server/web/dist/assets/index-Ml--d1Bc.css | 1 + cmd/server/web/dist/index.html | 4 +- configs/config.example.yaml | 4 + internal/api/device_manager.go | 33 ++ internal/api/handlers.go | 43 ++ internal/config/config.go | 6 + internal/devices/ultrabeam/ultrabeam.go | 457 +++++++++++++++ web/src/App.svelte | 5 + web/src/components/Ultrabeam.svelte | 547 ++++++++++++++++++ web/src/lib/api.js | 9 + 11 files changed, 1118 insertions(+), 2 deletions(-) create mode 100644 cmd/server/web/dist/assets/index-8_72Rq0c.js create mode 100644 cmd/server/web/dist/assets/index-Ml--d1Bc.css create mode 100644 internal/devices/ultrabeam/ultrabeam.go create mode 100644 web/src/components/Ultrabeam.svelte diff --git a/cmd/server/web/dist/assets/index-8_72Rq0c.js b/cmd/server/web/dist/assets/index-8_72Rq0c.js new file mode 100644 index 0000000..9e77921 --- /dev/null +++ b/cmd/server/web/dist/assets/index-8_72Rq0c.js @@ -0,0 +1,11 @@ +var Cl=Object.defineProperty;var Fl=(t,n,s)=>n in t?Cl(t,n,{enumerable:!0,configurable:!0,writable:!0,value:s}):t[n]=s;var Ht=(t,n,s)=>Fl(t,typeof n!="symbol"?n+"":n,s);(function(){const n=document.createElement("link").relList;if(n&&n.supports&&n.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))a(o);new MutationObserver(o=>{for(const r of o)if(r.type==="childList")for(const g of r.addedNodes)g.tagName==="LINK"&&g.rel==="modulepreload"&&a(g)}).observe(document,{childList:!0,subtree:!0});function s(o){const r={};return o.integrity&&(r.integrity=o.integrity),o.referrerPolicy&&(r.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?r.credentials="include":o.crossOrigin==="anonymous"?r.credentials="omit":r.credentials="same-origin",r}function a(o){if(o.ep)return;o.ep=!0;const r=s(o);fetch(o.href,r)}})();function Re(){}function _l(t){return t()}function $t(){return Object.create(null)}function ot(t){t.forEach(_l)}function bl(t){return typeof t=="function"}function mt(t,n){return t!=t?n==n:t!==n||t&&typeof t=="object"||typeof t=="function"}function Tl(t){return Object.keys(t).length===0}function e(t,n){t.appendChild(n)}function je(t,n,s){t.insertBefore(n,s||null)}function we(t){t.parentNode&&t.parentNode.removeChild(t)}function It(t,n){for(let s=0;st.removeEventListener(n,s,a)}function l(t,n,s){s==null?t.removeAttribute(n):t.getAttribute(n)!==s&&t.setAttribute(n,s)}function Ot(t){return t===""?null:+t}function Sl(t){return Array.from(t.childNodes)}function P(t,n){n=""+n,t.data!==n&&(t.data=n)}function Qe(t,n){t.value=n??""}function de(t,n,s,a){s==null?t.style.removeProperty(n):t.style.setProperty(n,s,"")}function Pt(t,n,s){for(let a=0;at.indexOf(a)===-1?n.push(a):s.push(a)),s.forEach(a=>a()),Mt=n}const Dt=new Set;let Ll;function gt(t,n){t&&t.i&&(Dt.delete(t),t.i(n))}function Ct(t,n,s,a){if(t&&t.o){if(Dt.has(t))return;Dt.add(t),Ll.c.push(()=>{Dt.delete(t)}),t.o(n)}}function dt(t){return(t==null?void 0:t.length)!==void 0?t:Array.from(t)}function Ft(t){t&&t.c()}function _t(t,n,s){const{fragment:a,after_update:o}=t.$$;a&&a.m(n,s),qt(()=>{const r=t.$$.on_mount.map(_l).filter(bl);t.$$.on_destroy?t.$$.on_destroy.push(...r):ot(r),t.$$.on_mount=[]}),o.forEach(qt)}function bt(t,n){const s=t.$$;s.fragment!==null&&(El(s.after_update),ot(s.on_destroy),s.fragment&&s.fragment.d(n),s.on_destroy=s.fragment=null,s.ctx=[])}function ql(t,n){t.$$.dirty[0]===-1&&(St.push(t),Al(),t.$$.dirty.fill(0)),t.$$.dirty[n/31|0]|=1<{const k=p.length?p[0]:b;return c.ctx&&o(c.ctx[h],c.ctx[h]=k)&&(!c.skip_bound&&c.bound[h]&&c.bound[h](k),_&&ql(t,h)),b}):[],c.update(),_=!0,ot(c.before_update),c.fragment=a?a(c.ctx):!1,n.target){if(n.hydrate){const h=Sl(n.target);c.fragment&&c.fragment.l(h),h.forEach(we)}else c.fragment&&c.fragment.c();n.intro&>(t.$$.fragment),_t(t,n.target,n.anchor),jl()}Et(m)}class yt{constructor(){Ht(this,"$$");Ht(this,"$$set")}$destroy(){bt(this,1),this.$destroy=Re}$on(n,s){if(!bl(s))return Re;const a=this.$$.callbacks[n]||(this.$$.callbacks[n]=[]);return a.push(s),()=>{const o=a.indexOf(s);o!==-1&&a.splice(o,1)}}$set(n){this.$$set&&!Tl(n)&&(this.$$.skip_bound=!0,this.$$set(n),this.$$.skip_bound=!1)}}const Il="4";typeof window<"u"&&(window.__svelte||(window.__svelte={v:new Set})).v.add(Il);const Tt=[];function Rt(t,n=Re){let s;const a=new Set;function o(d){if(mt(t,d)&&(t=d,s)){const m=!Tt.length;for(const c of a)c[1](),Tt.push(c,t);if(m){for(let c=0;c{a.delete(c),a.size===0&&s&&(s(),s=null)}}return{set:o,update:r,subscribe:g}}const Bt=Rt(!1),kl=Rt(null),Dl=Rt(null);class Hl{constructor(){this.ws=null,this.reconnectTimeout=null,this.reconnectDelay=3e3}connect(){const s=`${window.location.protocol==="https:"?"wss:":"ws:"}//${window.location.host}/ws`;try{this.ws=new WebSocket(s),this.ws.onopen=()=>{console.log("WebSocket connected"),Bt.set(!0)},this.ws.onmessage=a=>{try{const o=JSON.parse(a.data);o.type==="update"&&(console.log("System status updated:",o.data),kl.set(o.data),Dl.set(new Date(o.timestamp)))}catch(o){console.error("Error parsing message:",o)}},this.ws.onerror=a=>{console.error("WebSocket error:",a)},this.ws.onclose=()=>{console.log("WebSocket disconnected"),Bt.set(!1),this.scheduleReconnect()}}catch(a){console.error("Error creating WebSocket:",a),this.scheduleReconnect()}}scheduleReconnect(){this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.reconnectTimeout=setTimeout(()=>{console.log("Attempting to reconnect..."),this.connect()},this.reconnectDelay)}send(n){this.ws&&this.ws.readyState===WebSocket.OPEN&&this.ws.send(JSON.stringify(n))}disconnect(){this.reconnectTimeout&&clearTimeout(this.reconnectTimeout),this.ws&&this.ws.close()}}const ll=new Hl,xl="/api";async function Fe(t,n={}){try{const s=await fetch(`${xl}${t}`,{...n,headers:{"Content-Type":"application/json",...n.headers}});if(!s.ok)throw new Error(`HTTP error! status: ${s.status}`);return await s.json()}catch(s){throw console.error("API request failed:",s),s}}const Ue={getStatus:()=>Fe("/status"),getConfig:()=>Fe("/config"),webswitch:{relayOn:t=>Fe(`/webswitch/relay/on?relay=${t}`,{method:"POST"}),relayOff:t=>Fe(`/webswitch/relay/off?relay=${t}`,{method:"POST"}),allOn:()=>Fe("/webswitch/all/on",{method:"POST"}),allOff:()=>Fe("/webswitch/all/off",{method:"POST"})},rotator:{move:(t,n)=>Fe("/rotator/move",{method:"POST",body:JSON.stringify({rotator:t,azimuth:n})}),cw:t=>Fe(`/rotator/cw?rotator=${t}`,{method:"POST"}),ccw:t=>Fe(`/rotator/ccw?rotator=${t}`,{method:"POST"}),stop:()=>Fe("/rotator/stop",{method:"POST"})},tuner:{setOperate:t=>Fe("/tuner/operate",{method:"POST",body:JSON.stringify({value:t})}),setBypass:t=>Fe("/tuner/bypass",{method:"POST",body:JSON.stringify({value:t})}),autoTune:()=>Fe("/tuner/autotune",{method:"POST"})},antenna:{selectAntenna:(t,n)=>Fe("/antenna/select",{method:"POST",body:JSON.stringify({port:t,antenna:n})}),reboot:()=>Fe("/antenna/reboot",{method:"POST"})},power:{setFanMode:t=>Fe("/power/fanmode",{method:"POST",body:JSON.stringify({mode:t})}),setOperate:t=>Fe("/power/operate",{method:"POST",body:JSON.stringify({value:t})})},rotator:{setHeading:t=>Fe("/rotator/heading",{method:"POST",body:JSON.stringify({heading:t})}),rotateCW:()=>Fe("/rotator/cw",{method:"POST"}),rotateCCW:()=>Fe("/rotator/ccw",{method:"POST"}),stop:()=>Fe("/rotator/stop",{method:"POST"})},ultrabeam:{setFrequency:(t,n)=>Fe("/ultrabeam/frequency",{method:"POST",body:JSON.stringify({frequency:t,direction:n})}),retract:()=>Fe("/ultrabeam/retract",{method:"POST"})}};function nl(t,n,s){var g;const a=t.slice();a[10]=n[s];const o=a[1].find(function(...m){return t[9](a[10],...m)});a[11]=o;const r=((g=a[11])==null?void 0:g.state)||!1;return a[12]=r,a}function sl(t){let n,s,a,o,r,g,d=t[12]?"ON":"OFF",m,c,_,h,b,p,k,j;function f(){return t[8](t[10])}return{c(){n=i("div"),s=i("div"),a=i("div"),o=i("div"),o.textContent=`${t[3][t[10]]}`,r=u(),g=i("div"),m=w(d),c=u(),_=i("button"),h=i("div"),h.innerHTML='
',p=u(),l(o,"class","relay-name svelte-z2csmj"),l(g,"class","relay-status svelte-z2csmj"),l(a,"class","relay-details svelte-z2csmj"),l(s,"class","relay-info svelte-z2csmj"),l(h,"class","toggle-track svelte-z2csmj"),l(_,"class","relay-toggle svelte-z2csmj"),_.disabled=b=t[0][t[10]],E(_,"active",t[12]),E(_,"loading",t[0][t[10]]),l(n,"class","relay-card svelte-z2csmj"),E(n,"relay-on",t[12])},m(C,y){je(C,n,y),e(n,s),e(s,a),e(a,o),e(a,r),e(a,g),e(g,m),e(n,c),e(n,_),e(_,h),e(n,p),k||(j=ye(_,"click",f),k=!0)},p(C,y){t=C,y&2&&d!==(d=t[12]?"ON":"OFF")&&P(m,d),y&1&&b!==(b=t[0][t[10]])&&(_.disabled=b),y&2&&E(_,"active",t[12]),y&1&&E(_,"loading",t[0][t[10]]),y&2&&E(n,"relay-on",t[12])},d(C){C&&we(n),k=!1,j()}}}function zl(t){let n,s,a,o,r,g,d,m,c,_,h,b,p,k,j,f=dt([1,2,3,4,5]),C=[];for(let y=0;y<5;y+=1)C[y]=sl(nl(t,f,y));return{c(){n=i("div"),s=i("div"),a=i("h2"),a.textContent="WebSwitch",o=u(),r=i("span"),g=u(),d=i("div"),m=i("div");for(let y=0;y<5;y+=1)C[y].c();c=u(),_=i("div"),h=i("button"),h.innerHTML=` + ALL ON`,b=u(),p=i("button"),p.innerHTML=` + ALL OFF`,l(a,"class","svelte-z2csmj"),l(r,"class","status-dot svelte-z2csmj"),E(r,"disconnected",!t[2]),l(s,"class","card-header svelte-z2csmj"),l(m,"class","relays svelte-z2csmj"),l(h,"class","control-btn all-on svelte-z2csmj"),l(p,"class","control-btn all-off svelte-z2csmj"),l(_,"class","controls svelte-z2csmj"),l(d,"class","metrics svelte-z2csmj"),l(n,"class","card svelte-z2csmj")},m(y,v){je(y,n,v),e(n,s),e(s,a),e(s,o),e(s,r),e(n,g),e(n,d),e(d,m);for(let F=0;F<5;F+=1)C[F]&&C[F].m(m,null);e(d,c),e(d,_),e(_,h),e(_,b),e(_,p),k||(j=[ye(h,"click",t[5]),ye(p,"click",t[6])],k=!0)},p(y,[v]){if(v&4&&E(r,"disconnected",!y[2]),v&27){f=dt([1,2,3,4,5]);let F;for(F=0;F<5;F+=1){const T=nl(y,f,F);C[F]?C[F].p(T,v):(C[F]=sl(T),C[F].c(),C[F].m(m,null))}for(;F<5;F+=1)C[F].d(1)}},i:Re,o:Re,d(y){y&&we(n),It(C,y),k=!1,ot(j)}}}function Bl(t,n,s){let a,o,{status:r}=n;const g={1:"Power Supply",2:"PGXL",3:"TGXL",4:"Flex Radio Start",5:"Reserve"};let d={};async function m(p){const k=a.find(f=>f.number===p),j=(k==null?void 0:k.state)||!1;s(0,d[p]=!0,d);try{j?await Ue.webswitch.relayOff(p):await Ue.webswitch.relayOn(p)}catch(f){console.error("Failed to toggle relay:",f),alert("Failed to control relay")}finally{s(0,d[p]=!1,d)}}async function c(){try{await Ue.webswitch.allOn()}catch(p){console.error("Failed to turn all on:",p)}}async function _(){try{await Ue.webswitch.allOff()}catch(p){console.error("Failed to turn all off:",p)}}const h=p=>m(p),b=(p,k)=>k.number===p;return t.$$set=p=>{"status"in p&&s(7,r=p.status)},t.$$.update=()=>{t.$$.dirty&128&&s(1,a=(r==null?void 0:r.relays)||[]),t.$$.dirty&128&&s(2,o=(r==null?void 0:r.connected)||!1)},[d,a,o,g,m,c,_,r,h,b]}class Rl extends yt{constructor(n){super(),wt(this,n,Bl,zl,mt,{status:7})}}function Wl(t){let n;return{c(){n=i("span"),n.textContent="High!",l(n,"class","status-text danger svelte-utvwj6")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Gl(t){let n;return{c(){n=i("span"),n.textContent="Caution",l(n,"class","status-text warning svelte-utvwj6")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Ul(t){let n;return{c(){n=i("span"),n.textContent="Good",l(n,"class","status-text ok svelte-utvwj6")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Jl(t){let n;return{c(){n=i("span"),n.textContent="Excellent",l(n,"class","status-text good svelte-utvwj6")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Xl(t){let n,s,a,o,r,g,d,m,c,_,h,b,p,k,j=t[1].toFixed(0)+"",f,C,y,v,F,T,S,L,R,X,ae,Y,N,I,te=t[2].toFixed(2)+"",Q,Te,K,D,M,ee,O,x,ke,ge=t[3].toFixed(1)+"",Ae,oe,xe,ue,re,Ne,G,Z,Ie,z,Ce=t[12].toFixed(1)+"",ve,Ee,De,Se,He,_e,fe,Ye,le,$,B,ne,U,Me=t[15].toFixed(0)+"",We,Ke,Oe,Je,et,Ze,ze=t[14].toFixed(1)+"",Le,Be,Ge,pe,Xe,V,me=t[13].toFixed(1)+"",A,ce,W,qe,Ve,$e,H,be,rt,ut,nt,ct,ht,st,At,it,tt,Pe,lt,vt,ft,pt,jt,Nt;function q(J,ie){return J[2]<1.5?Jl:J[2]<2?Ul:J[2]<3?Gl:Wl}let se=q(t),at=se(t);return{c(){n=i("div"),s=i("div"),a=i("h2"),a.textContent="Power Genius XL",o=u(),r=i("div"),g=i("button"),d=w(t[7]),m=u(),c=i("span"),_=u(),h=i("div"),b=i("div"),p=i("div"),k=i("div"),f=w(j),C=i("span"),C.textContent="W",y=u(),v=i("div"),v.textContent="Forward Power",F=u(),T=i("div"),S=i("div"),L=i("div"),R=u(),X=i("div"),X.innerHTML="0 1000 2000",ae=u(),Y=i("div"),N=i("div"),I=i("div"),Q=w(te),Te=u(),K=i("div"),K.textContent="SWR",D=u(),M=i("div"),at.c(),ee=u(),O=i("div"),x=i("div"),ke=i("div"),Ae=w(ge),oe=w("°"),xe=u(),ue=i("div"),ue.textContent="PA Temp",re=u(),Ne=i("div"),G=i("div"),Z=u(),Ie=i("div"),z=i("div"),ve=w(Ce),Ee=w("°"),De=u(),Se=i("div"),Se.textContent="HL Temp",He=u(),_e=i("div"),fe=i("div"),Ye=u(),le=i("div"),$=i("div"),B=i("div"),B.textContent="VAC",ne=u(),U=i("div"),We=w(Me),Ke=u(),Oe=i("div"),Je=i("div"),Je.textContent="VDD",et=u(),Ze=i("div"),Le=w(ze),Be=u(),Ge=i("div"),pe=i("div"),pe.textContent="ID Peak",Xe=u(),V=i("div"),A=w(me),ce=u(),W=i("div"),qe=i("div"),Ve=i("span"),Ve.textContent="Band A",$e=u(),H=i("span"),be=w(t[10]),rt=u(),ut=i("div"),nt=i("span"),nt.textContent="Band B",ct=u(),ht=i("span"),st=w(t[9]),At=u(),it=i("div"),tt=i("label"),tt.textContent="Fan Mode",Pe=u(),lt=i("select"),vt=i("option"),vt.textContent="Standard",ft=i("option"),ft.textContent="Contest",pt=i("option"),pt.textContent="Broadcast",l(a,"class","svelte-utvwj6"),l(g,"class","state-badge svelte-utvwj6"),E(g,"idle",t[0]==="IDLE"),E(g,"transmit",t[0].includes("TRANSMIT")),l(c,"class","status-dot svelte-utvwj6"),E(c,"disconnected",!t[8]),l(r,"class","header-right svelte-utvwj6"),l(s,"class","card-header svelte-utvwj6"),l(C,"class","unit svelte-utvwj6"),l(k,"class","power-value svelte-utvwj6"),l(v,"class","power-label svelte-utvwj6"),l(p,"class","power-main svelte-utvwj6"),l(L,"class","power-bar-glow svelte-utvwj6"),l(S,"class","power-bar-fill svelte-utvwj6"),de(S,"width",t[4]+"%"),l(X,"class","power-scale svelte-utvwj6"),l(T,"class","power-bar svelte-utvwj6"),l(b,"class","power-display svelte-utvwj6"),l(I,"class","swr-value svelte-utvwj6"),l(K,"class","swr-label svelte-utvwj6"),l(N,"class","swr-circle svelte-utvwj6"),de(N,"--swr-color",t[5]),l(M,"class","swr-status svelte-utvwj6"),l(Y,"class","swr-container svelte-utvwj6"),l(ke,"class","temp-value svelte-utvwj6"),de(ke,"color",t[6]),l(ue,"class","temp-label svelte-utvwj6"),l(G,"class","temp-mini-fill svelte-utvwj6"),de(G,"width",t[3]/80*100+"%"),de(G,"background",t[6]),l(Ne,"class","temp-mini-bar svelte-utvwj6"),l(x,"class","temp-item svelte-utvwj6"),l(z,"class","temp-value svelte-utvwj6"),de(z,"color",t[6]),l(Se,"class","temp-label svelte-utvwj6"),l(fe,"class","temp-mini-fill svelte-utvwj6"),de(fe,"width",t[12]/80*100+"%"),de(fe,"background",t[6]),l(_e,"class","temp-mini-bar svelte-utvwj6"),l(Ie,"class","temp-item svelte-utvwj6"),l(O,"class","temp-group svelte-utvwj6"),l(B,"class","param-label svelte-utvwj6"),l(U,"class","param-value svelte-utvwj6"),l($,"class","param-box svelte-utvwj6"),l(Je,"class","param-label svelte-utvwj6"),l(Ze,"class","param-value svelte-utvwj6"),l(Oe,"class","param-box svelte-utvwj6"),l(pe,"class","param-label svelte-utvwj6"),l(V,"class","param-value svelte-utvwj6"),l(Ge,"class","param-box svelte-utvwj6"),l(le,"class","params-grid svelte-utvwj6"),l(Ve,"class","band-label svelte-utvwj6"),l(H,"class","band-value svelte-utvwj6"),l(qe,"class","band-item svelte-utvwj6"),l(nt,"class","band-label svelte-utvwj6"),l(ht,"class","band-value svelte-utvwj6"),l(ut,"class","band-item svelte-utvwj6"),l(W,"class","band-display svelte-utvwj6"),l(tt,"class","control-label svelte-utvwj6"),vt.__value="STANDARD",Qe(vt,vt.__value),ft.__value="CONTEST",Qe(ft,ft.__value),pt.__value="BROADCAST",Qe(pt,pt.__value),l(lt,"class","svelte-utvwj6"),l(it,"class","fan-control svelte-utvwj6"),l(h,"class","metrics svelte-utvwj6"),l(n,"class","card svelte-utvwj6")},m(J,ie){je(J,n,ie),e(n,s),e(s,a),e(s,o),e(s,r),e(r,g),e(g,d),e(r,m),e(r,c),e(n,_),e(n,h),e(h,b),e(b,p),e(p,k),e(k,f),e(k,C),e(p,y),e(p,v),e(b,F),e(b,T),e(T,S),e(S,L),e(T,R),e(T,X),e(h,ae),e(h,Y),e(Y,N),e(N,I),e(I,Q),e(N,Te),e(N,K),e(Y,D),e(Y,M),at.m(M,null),e(h,ee),e(h,O),e(O,x),e(x,ke),e(ke,Ae),e(ke,oe),e(x,xe),e(x,ue),e(x,re),e(x,Ne),e(Ne,G),e(O,Z),e(O,Ie),e(Ie,z),e(z,ve),e(z,Ee),e(Ie,De),e(Ie,Se),e(Ie,He),e(Ie,_e),e(_e,fe),e(h,Ye),e(h,le),e(le,$),e($,B),e($,ne),e($,U),e(U,We),e(le,Ke),e(le,Oe),e(Oe,Je),e(Oe,et),e(Oe,Ze),e(Ze,Le),e(le,Be),e(le,Ge),e(Ge,pe),e(Ge,Xe),e(Ge,V),e(V,A),e(h,ce),e(h,W),e(W,qe),e(qe,Ve),e(qe,$e),e(qe,H),e(H,be),e(W,rt),e(W,ut),e(ut,nt),e(ut,ct),e(ut,ht),e(ht,st),e(h,At),e(h,it),e(it,tt),e(it,Pe),e(it,lt),e(lt,vt),e(lt,ft),e(lt,pt),Pt(lt,t[11]),jt||(Nt=[ye(g,"click",t[17]),ye(lt,"change",t[19])],jt=!0)},p(J,[ie]){ie&128&&P(d,J[7]),ie&1&&E(g,"idle",J[0]==="IDLE"),ie&1&&E(g,"transmit",J[0].includes("TRANSMIT")),ie&256&&E(c,"disconnected",!J[8]),ie&2&&j!==(j=J[1].toFixed(0)+"")&&P(f,j),ie&16&&de(S,"width",J[4]+"%"),ie&4&&te!==(te=J[2].toFixed(2)+"")&&P(Q,te),ie&32&&de(N,"--swr-color",J[5]),se!==(se=q(J))&&(at.d(1),at=se(J),at&&(at.c(),at.m(M,null))),ie&8&&ge!==(ge=J[3].toFixed(1)+"")&&P(Ae,ge),ie&64&&de(ke,"color",J[6]),ie&8&&de(G,"width",J[3]/80*100+"%"),ie&64&&de(G,"background",J[6]),ie&4096&&Ce!==(Ce=J[12].toFixed(1)+"")&&P(ve,Ce),ie&64&&de(z,"color",J[6]),ie&4096&&de(fe,"width",J[12]/80*100+"%"),ie&64&&de(fe,"background",J[6]),ie&32768&&Me!==(Me=J[15].toFixed(0)+"")&&P(We,Me),ie&16384&&ze!==(ze=J[14].toFixed(1)+"")&&P(Le,ze),ie&8192&&me!==(me=J[13].toFixed(1)+"")&&P(A,me),ie&1024&&P(be,J[10]),ie&512&&P(st,J[9]),ie&2048&&Pt(lt,J[11])},i:Re,o:Re,d(J){J&&we(n),at.d(),jt=!1,ot(Nt)}}}function Vl(t,n,s){let a,o,r,g,d,m,c,_,h,b,p,k,j,f,C,y,{status:v}=n;async function F(L){try{await Ue.power.setFanMode(L)}catch(R){console.error("Failed to set fan mode:",R),alert("Failed to set fan mode")}}async function T(){try{const L=h==="IDLE"?0:1;await Ue.power.setOperate(L)}catch(L){console.error("Failed to toggle operate:",L),alert("Failed to toggle operate mode")}}const S=L=>F(L.target.value);return t.$$set=L=>{"status"in L&&s(18,v=L.status)},t.$$.update=()=>{t.$$.dirty&262144&&s(1,a=(v==null?void 0:v.power_forward)||0),t.$$.dirty&262144&&v!=null&&v.power_reflected,t.$$.dirty&262144&&s(2,o=(v==null?void 0:v.swr)||1),t.$$.dirty&262144&&s(15,r=(v==null?void 0:v.voltage)||0),t.$$.dirty&262144&&s(14,g=(v==null?void 0:v.vdd)||0),t.$$.dirty&262144&&v!=null&&v.current,t.$$.dirty&262144&&s(13,d=(v==null?void 0:v.peak_current)||0),t.$$.dirty&262144&&s(3,m=(v==null?void 0:v.temperature)||0),t.$$.dirty&262144&&s(12,c=(v==null?void 0:v.harmonic_load_temp)||0),t.$$.dirty&262144&&s(11,_=(v==null?void 0:v.fan_mode)||"CONTEST"),t.$$.dirty&262144&&s(0,h=(v==null?void 0:v.state)||"IDLE"),t.$$.dirty&262144&&s(10,b=(v==null?void 0:v.band_a)||"0"),t.$$.dirty&262144&&s(9,p=(v==null?void 0:v.band_b)||"0"),t.$$.dirty&262144&&s(8,k=(v==null?void 0:v.connected)||!1),t.$$.dirty&1&&s(7,j=h.replace("TRANSMIT_A","TRANSMIT").replace("TRANSMIT_B","TRANSMIT")),t.$$.dirty&8&&s(6,f=m<40?"#4caf50":m<60?"#ffc107":m<75?"#ff9800":"#f44336"),t.$$.dirty&4&&s(5,C=o<1.5?"#4caf50":o<2?"#ffc107":o<3?"#ff9800":"#f44336"),t.$$.dirty&2&&s(4,y=Math.min(a/2e3*100,100))},[h,a,o,m,y,C,f,j,k,p,b,_,c,d,g,r,F,T,v,S]}class Yl extends yt{constructor(n){super(),wt(this,n,Vl,Xl,mt,{status:18})}}function Kl(t){let n;return{c(){n=i("span"),n.textContent="High!",l(n,"class","status-text danger svelte-j97t2j")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Zl(t){let n;return{c(){n=i("span"),n.textContent="Caution",l(n,"class","status-text warning svelte-j97t2j")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function $l(t){let n;return{c(){n=i("span"),n.textContent="Good",l(n,"class","status-text ok svelte-j97t2j")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function Ql(t){let n;return{c(){n=i("span"),n.textContent="Excellent",l(n,"class","status-text good svelte-j97t2j")},m(s,a){je(s,n,a)},d(s){s&&we(n)}}}function en(t){let n,s,a,o,r,g,d,m,c,_,h,b,p,k,j=t[0].toFixed(0)+"",f,C,y,v,F,T,S,L,R,X,ae,Y,N,I,te=t[1].toFixed(2)+"",Q,Te,K,D,M,ee,O,x,ke,ge,Ae,oe,xe,ue,re,Ne,G,Z,Ie,z,Ce,ve,Ee,De,Se,He,_e,fe,Ye,le,$=(t[11]/1e3).toFixed(3)+"",B,ne,U,Me,We,Ke,Oe,Je=(t[10]/1e3).toFixed(3)+"",et,Ze,ze,Le,Be,Ge=t[8]===1?"OPERATE":"STANDBY",pe,Xe,V,me,A,ce,W;function qe(H,be){return H[1]<1.5?Ql:H[1]<2?$l:H[1]<3?Zl:Kl}let Ve=qe(t),$e=Ve(t);return{c(){n=i("div"),s=i("div"),a=i("h2"),a.textContent="Tuner Genius XL",o=u(),r=i("div"),g=i("span"),d=w(t[12]),m=u(),c=i("span"),_=u(),h=i("div"),b=i("div"),p=i("div"),k=i("div"),f=w(j),C=i("span"),C.textContent="W",y=u(),v=i("div"),v.textContent="Forward Power",F=u(),T=i("div"),S=i("div"),L=i("div"),R=u(),X=i("div"),X.innerHTML="0 1000 2000",ae=u(),Y=i("div"),N=i("div"),I=i("div"),Q=w(te),Te=u(),K=i("div"),K.textContent="SWR",D=u(),M=i("div"),$e.c(),ee=u(),O=i("div"),x=i("div"),ke=i("div"),ge=w(t[7]),Ae=u(),oe=i("div"),oe.textContent="C1",xe=u(),ue=i("div"),re=i("div"),Ne=w(t[6]),G=u(),Z=i("div"),Z.textContent="L",Ie=u(),z=i("div"),Ce=i("div"),ve=w(t[5]),Ee=u(),De=i("div"),De.textContent="C2",Se=u(),He=i("div"),_e=i("div"),fe=i("div"),fe.textContent="Freq A",Ye=u(),le=i("div"),B=w($),ne=i("span"),ne.textContent="MHz",U=u(),Me=i("div"),We=i("div"),We.textContent="Freq B",Ke=u(),Oe=i("div"),et=w(Je),Ze=i("span"),Ze.textContent="MHz",ze=u(),Le=i("div"),Be=i("button"),pe=w(Ge),Xe=u(),V=i("button"),V.textContent="BYPASS",me=u(),A=i("button"),A.innerHTML=` + AUTO TUNE`,l(a,"class","svelte-j97t2j"),l(g,"class","tuning-badge svelte-j97t2j"),E(g,"tuning",t[12]==="TUNING"),l(c,"class","status-dot svelte-j97t2j"),E(c,"disconnected",!t[4]),l(r,"class","header-right svelte-j97t2j"),l(s,"class","card-header svelte-j97t2j"),l(C,"class","unit svelte-j97t2j"),l(k,"class","power-value svelte-j97t2j"),l(v,"class","power-label svelte-j97t2j"),l(p,"class","power-main svelte-j97t2j"),l(L,"class","power-bar-glow svelte-j97t2j"),l(S,"class","power-bar-fill svelte-j97t2j"),de(S,"width",t[2]+"%"),l(X,"class","power-scale svelte-j97t2j"),l(T,"class","power-bar svelte-j97t2j"),l(b,"class","power-display svelte-j97t2j"),l(I,"class","swr-value svelte-j97t2j"),l(K,"class","swr-label svelte-j97t2j"),l(N,"class","swr-circle svelte-j97t2j"),de(N,"--swr-color",t[3]),l(M,"class","swr-status svelte-j97t2j"),l(Y,"class","swr-container svelte-j97t2j"),l(ke,"class","cap-value svelte-j97t2j"),l(oe,"class","cap-label svelte-j97t2j"),l(x,"class","cap-item svelte-j97t2j"),l(re,"class","cap-value svelte-j97t2j"),l(Z,"class","cap-label svelte-j97t2j"),l(ue,"class","cap-item svelte-j97t2j"),l(Ce,"class","cap-value svelte-j97t2j"),l(De,"class","cap-label svelte-j97t2j"),l(z,"class","cap-item svelte-j97t2j"),l(O,"class","capacitors svelte-j97t2j"),l(fe,"class","freq-label svelte-j97t2j"),l(ne,"class","freq-unit svelte-j97t2j"),l(le,"class","freq-value svelte-j97t2j"),l(_e,"class","freq-item svelte-j97t2j"),l(We,"class","freq-label svelte-j97t2j"),l(Ze,"class","freq-unit svelte-j97t2j"),l(Oe,"class","freq-value svelte-j97t2j"),l(Me,"class","freq-item svelte-j97t2j"),l(He,"class","freq-display svelte-j97t2j"),l(Be,"class","control-btn operate svelte-j97t2j"),E(Be,"active",t[8]===1),l(V,"class","control-btn bypass svelte-j97t2j"),E(V,"active",t[9]),l(Le,"class","controls svelte-j97t2j"),l(A,"class","tune-btn svelte-j97t2j"),l(h,"class","metrics svelte-j97t2j"),l(n,"class","card svelte-j97t2j")},m(H,be){je(H,n,be),e(n,s),e(s,a),e(s,o),e(s,r),e(r,g),e(g,d),e(r,m),e(r,c),e(n,_),e(n,h),e(h,b),e(b,p),e(p,k),e(k,f),e(k,C),e(p,y),e(p,v),e(b,F),e(b,T),e(T,S),e(S,L),e(T,R),e(T,X),e(h,ae),e(h,Y),e(Y,N),e(N,I),e(I,Q),e(N,Te),e(N,K),e(Y,D),e(Y,M),$e.m(M,null),e(h,ee),e(h,O),e(O,x),e(x,ke),e(ke,ge),e(x,Ae),e(x,oe),e(O,xe),e(O,ue),e(ue,re),e(re,Ne),e(ue,G),e(ue,Z),e(O,Ie),e(O,z),e(z,Ce),e(Ce,ve),e(z,Ee),e(z,De),e(h,Se),e(h,He),e(He,_e),e(_e,fe),e(_e,Ye),e(_e,le),e(le,B),e(le,ne),e(He,U),e(He,Me),e(Me,We),e(Me,Ke),e(Me,Oe),e(Oe,et),e(Oe,Ze),e(h,ze),e(h,Le),e(Le,Be),e(Be,pe),e(Le,Xe),e(Le,V),e(h,me),e(h,A),ce||(W=[ye(Be,"click",t[17]),ye(V,"click",t[18]),ye(A,"click",t[13])],ce=!0)},p(H,[be]){be&4096&&P(d,H[12]),be&4096&&E(g,"tuning",H[12]==="TUNING"),be&16&&E(c,"disconnected",!H[4]),be&1&&j!==(j=H[0].toFixed(0)+"")&&P(f,j),be&4&&de(S,"width",H[2]+"%"),be&2&&te!==(te=H[1].toFixed(2)+"")&&P(Q,te),be&8&&de(N,"--swr-color",H[3]),Ve!==(Ve=qe(H))&&($e.d(1),$e=Ve(H),$e&&($e.c(),$e.m(M,null))),be&128&&P(ge,H[7]),be&64&&P(Ne,H[6]),be&32&&P(ve,H[5]),be&2048&&$!==($=(H[11]/1e3).toFixed(3)+"")&&P(B,$),be&1024&&Je!==(Je=(H[10]/1e3).toFixed(3)+"")&&P(et,Je),be&256&&Ge!==(Ge=H[8]===1?"OPERATE":"STANDBY")&&P(pe,Ge),be&256&&E(Be,"active",H[8]===1),be&512&&E(V,"active",H[9])},i:Re,o:Re,d(H){H&&we(n),$e.d(),ce=!1,ot(W)}}}function tn(t,n,s){let a,o,r,g,d,m,c,_,h,b,p,k,j,{status:f}=n;async function C(){try{await Ue.tuner.autoTune()}catch(S){console.error("Failed to tune:",S),alert("Failed to start tuning")}}async function y(S){try{await Ue.tuner.setBypass(S)}catch(L){console.error("Failed to set bypass:",L),alert("Failed to set bypass")}}async function v(S){try{await Ue.tuner.setOperate(S)}catch(L){console.error("Failed to set operate:",L),alert("Failed to set operate")}}const F=()=>v(c===1?0:1),T=()=>y(m?0:1);return t.$$set=S=>{"status"in S&&s(16,f=S.status)},t.$$.update=()=>{t.$$.dirty&65536&&s(0,a=(f==null?void 0:f.power_forward)||0),t.$$.dirty&65536&&s(1,o=(f==null?void 0:f.swr)||1),t.$$.dirty&65536&&s(12,r=(f==null?void 0:f.tuning_status)||"READY"),t.$$.dirty&65536&&s(11,g=(f==null?void 0:f.frequency_a)||0),t.$$.dirty&65536&&s(10,d=(f==null?void 0:f.frequency_b)||0),t.$$.dirty&65536&&s(9,m=(f==null?void 0:f.bypass)||!1),t.$$.dirty&65536&&s(8,c=(f==null?void 0:f.state)||0),t.$$.dirty&65536&&s(7,_=(f==null?void 0:f.c1)||0),t.$$.dirty&65536&&s(6,h=(f==null?void 0:f.l)||0),t.$$.dirty&65536&&s(5,b=(f==null?void 0:f.c2)||0),t.$$.dirty&65536&&s(4,p=(f==null?void 0:f.connected)||!1),t.$$.dirty&2&&s(3,k=o<1.5?"#4caf50":o<2?"#ffc107":o<3?"#ff9800":"#f44336"),t.$$.dirty&1&&s(2,j=Math.min(a/2e3*100,100))},[a,o,j,k,p,b,h,_,c,m,d,g,r,C,y,v,f,F,T]}class ln extends yt{constructor(n){super(),wt(this,n,tn,en,mt,{status:16})}}function il(t,n,s){const a=t.slice();a[12]=n[s];const o=a[1].tx&&a[1].tx_ant===a[12].number;a[13]=o;const r=a[0].tx&&a[0].tx_ant===a[12].number;a[14]=r;const g=!a[1].tx&&a[1].rx_ant===a[12].number;a[15]=g;const d=!a[0].tx&&a[0].rx_ant===a[12].number;a[16]=d;const m=a[13]||a[14];a[17]=m;const c=a[15]||a[13];a[18]=c;const _=a[16]||a[14];return a[19]=_,a}function al(t){let n,s,a=t[12].name+"",o,r,g,d,m,c,_,h,b;function p(){return t[9](t[12])}function k(){return t[10](t[12])}return{c(){n=i("div"),s=i("div"),o=w(a),r=u(),g=i("div"),d=i("button"),d.textContent="A",m=u(),c=i("button"),c.textContent="B",_=u(),l(s,"class","antenna-name svelte-1r71rs8"),l(d,"class","port-btn svelte-1r71rs8"),E(d,"active",t[18]),l(c,"class","port-btn svelte-1r71rs8"),E(c,"active",t[19]),l(g,"class","antenna-ports svelte-1r71rs8"),l(n,"class","antenna-card svelte-1r71rs8"),E(n,"tx",t[17]),E(n,"active-a",t[18]),E(n,"active-b",t[19])},m(j,f){je(j,n,f),e(n,s),e(s,o),e(n,r),e(n,g),e(g,d),e(g,m),e(g,c),e(n,_),h||(b=[ye(d,"click",p),ye(c,"click",k)],h=!0)},p(j,f){t=j,f&16&&a!==(a=t[12].name+"")&&P(o,a),f&18&&E(d,"active",t[18]),f&17&&E(c,"active",t[19]),f&19&&E(n,"tx",t[17]),f&18&&E(n,"active-a",t[18]),f&17&&E(n,"active-b",t[19])},d(j){j&&we(n),h=!1,ot(b)}}}function nn(t){let n,s,a,o,r,g,d,m,c,_,h=(t[1].source||"FLEX")+"",b,p,k,j,f=(t[0].source||"FLEX")+"",C,y,v,F,T,S,L,R,X,ae,Y,N,I,te,Q,Te,K=dt(t[4]),D=[];for(let M=0;M🔄 + REBOOT`,l(a,"class","svelte-1r71rs8"),l(r,"class","status-dot svelte-1r71rs8"),E(r,"disconnected",!t[5]),l(s,"class","card-header svelte-1r71rs8"),l(_,"class","source-label svelte-1r71rs8"),l(c,"class","source-item svelte-1r71rs8"),l(j,"class","source-label svelte-1r71rs8"),l(k,"class","source-item svelte-1r71rs8"),l(m,"class","sources svelte-1r71rs8"),l(T,"class","band-value svelte-1r71rs8"),l(F,"class","band-item svelte-1r71rs8"),l(X,"class","band-value svelte-1r71rs8"),l(R,"class","band-item svelte-1r71rs8"),l(v,"class","bands svelte-1r71rs8"),l(N,"class","antennas svelte-1r71rs8"),l(te,"class","reboot-btn svelte-1r71rs8"),l(d,"class","metrics svelte-1r71rs8"),l(n,"class","card svelte-1r71rs8")},m(M,ee){je(M,n,ee),e(n,s),e(s,a),e(s,o),e(s,r),e(n,g),e(n,d),e(d,m),e(m,c),e(c,_),e(_,b),e(m,p),e(m,k),e(k,j),e(j,C),e(d,y),e(d,v),e(v,F),e(F,T),e(T,S),e(v,L),e(v,R),e(R,X),e(X,ae),e(d,Y),e(d,N);for(let O=0;Oh(1,j.number),k=j=>h(2,j.number);return t.$$set=j=>{"status"in j&&s(8,c=j.status)},t.$$.update=()=>{t.$$.dirty&256&&s(5,a=(c==null?void 0:c.connected)||!1),t.$$.dirty&256&&s(1,o=(c==null?void 0:c.port_a)||{}),t.$$.dirty&256&&s(0,r=(c==null?void 0:c.port_b)||{}),t.$$.dirty&256&&s(4,g=(c==null?void 0:c.antennas)||[]),t.$$.dirty&2&&s(3,d=_[o.band]||"None"),t.$$.dirty&1&&s(2,m=_[r.band]||"None")},[r,o,m,d,g,a,h,b,c,p,k]}class an extends yt{constructor(n){super(),wt(this,n,sn,nn,mt,{status:8})}}function ol(t,n,s){const a=t.slice();a[9]=n[s];const o=150+125*Math.sin(a[9]*Math.PI/180);a[10]=o;const r=150-125*Math.cos(a[9]*Math.PI/180);return a[11]=r,a}function rl(t){let n,s,a;return{c(){n=he("text"),s=w(t[9]),a=w("°"),l(n,"x",t[10]),l(n,"y",t[11]),l(n,"text-anchor","middle"),l(n,"dominant-baseline","middle"),l(n,"class","degree-label svelte-1sgx2se")},m(o,r){je(o,n,r),e(n,s),e(n,a)},p:Re,d(o){o&&we(n)}}}function on(t){let n,s,a,o,r,g,d,m,c,_,h,b,p,k,j,f,C,y,v,F,T,S,L,R,X,ae,Y,N,I,te,Q,Te,K,D,M,ee,O,x,ke,ge,Ae,oe,xe,ue,re,Ne,G,Z,Ie,z,Ce,ve,Ee,De,Se,He,_e,fe,Ye,le=dt([45,135,225,315]),$=[];for(let B=0;B<4;B+=1)$[B]=rl(ol(t,le,B));return{c(){n=i("div"),s=i("div"),a=i("h2"),a.textContent="Rotator Genius",o=u(),r=i("span"),g=u(),d=i("div"),m=i("div"),c=i("div"),c.textContent="CURRENT HEADING",_=u(),h=i("div"),b=w(t[0]),p=w("°"),k=u(),j=i("div"),f=he("svg"),C=he("defs"),y=he("radialGradient"),v=he("stop"),F=he("stop"),T=he("circle"),S=he("circle"),L=he("circle"),R=he("circle"),X=he("g"),ae=he("g"),Y=he("path"),N=he("line"),I=he("line"),te=he("g"),Q=he("polygon"),K=he("circle"),D=he("animate"),M=he("circle"),ee=he("animate"),O=he("animate"),x=he("text"),ke=w("N"),ge=he("text"),Ae=w("E"),oe=he("text"),xe=w("S"),ue=he("text"),re=w("W");for(let B=0;B<4;B+=1)$[B].c();Ne=u(),G=i("div"),Z=i("input"),Ie=u(),z=i("button"),z.textContent="GO",Ce=u(),ve=i("div"),Ee=i("button"),Ee.innerHTML=` + CCW`,De=u(),Se=i("button"),Se.textContent="STOP",He=u(),_e=i("button"),_e.innerHTML=` + CW`,l(a,"class","svelte-1sgx2se"),l(r,"class","status-dot svelte-1sgx2se"),E(r,"disconnected",!t[1]),l(s,"class","card-header svelte-1sgx2se"),l(c,"class","heading-label svelte-1sgx2se"),l(h,"class","heading-value svelte-1sgx2se"),l(m,"class","heading-display svelte-1sgx2se"),l(v,"offset","0%"),de(v,"stop-color","rgba(79, 195, 247, 0.7)"),de(v,"stop-opacity","1"),l(F,"offset","100%"),de(F,"stop-color","rgba(79, 195, 247, 0)"),de(F,"stop-opacity","0"),l(y,"id","beamGradient"),l(T,"cx","150"),l(T,"cy","150"),l(T,"r","140"),l(T,"fill","rgba(30, 64, 175, 0.15)"),l(T,"stroke","rgba(79, 195, 247, 0.4)"),l(T,"stroke-width","2"),l(S,"cx","150"),l(S,"cy","150"),l(S,"r","105"),l(S,"fill","none"),l(S,"stroke","rgba(79,195,247,0.2)"),l(S,"stroke-width","1"),l(S,"stroke-dasharray","3,3"),l(L,"cx","150"),l(L,"cy","150"),l(L,"r","70"),l(L,"fill","none"),l(L,"stroke","rgba(79,195,247,0.2)"),l(L,"stroke-width","1"),l(L,"stroke-dasharray","3,3"),l(R,"cx","150"),l(R,"cy","150"),l(R,"r","35"),l(R,"fill","none"),l(R,"stroke","rgba(79,195,247,0.2)"),l(R,"stroke-width","1"),l(R,"stroke-dasharray","3,3"),l(Y,"d","M 0,0 L "+-Math.sin(15*Math.PI/180)*130+","+-Math.cos(15*Math.PI/180)*130+` \r + A 130,130 0 0,1 `+Math.sin(15*Math.PI/180)*130+","+-Math.cos(15*Math.PI/180)*130+" Z"),l(Y,"fill","url(#beamGradient)"),l(Y,"opacity","0.85"),l(N,"x1","0"),l(N,"y1","0"),l(N,"x2",-Math.sin(15*Math.PI/180)*130),l(N,"y2",-Math.cos(15*Math.PI/180)*130),l(N,"stroke","#4fc3f7"),l(N,"stroke-width","2"),l(N,"opacity","0.9"),l(I,"x1","0"),l(I,"y1","0"),l(I,"x2",Math.sin(15*Math.PI/180)*130),l(I,"y2",-Math.cos(15*Math.PI/180)*130),l(I,"stroke","#4fc3f7"),l(I,"stroke-width","2"),l(I,"opacity","0.9"),l(Q,"points","0,-20 -8,5 0,0 8,5"),l(Q,"fill","#4fc3f7"),l(Q,"stroke","#0288d1"),l(Q,"stroke-width","2"),de(Q,"filter","drop-shadow(0 0 10px rgba(79, 195, 247, 1))"),l(te,"transform","translate(0, -110)"),l(ae,"transform",Te="rotate("+t[0]+")"),l(D,"attributeName","r"),l(D,"values","5;7;5"),l(D,"dur","2s"),l(D,"repeatCount","indefinite"),l(K,"cx","0"),l(K,"cy","0"),l(K,"r","5"),l(K,"fill","#f44336"),l(K,"stroke","#fff"),l(K,"stroke-width","2"),l(ee,"attributeName","r"),l(ee,"values","10;16;10"),l(ee,"dur","2s"),l(ee,"repeatCount","indefinite"),l(O,"attributeName","opacity"),l(O,"values","0.5;0;0.5"),l(O,"dur","2s"),l(O,"repeatCount","indefinite"),l(M,"cx","0"),l(M,"cy","0"),l(M,"r","10"),l(M,"fill","none"),l(M,"stroke","#f44336"),l(M,"stroke-width","1.5"),l(M,"opacity","0.5"),l(X,"transform","translate(150, 150)"),l(x,"x","150"),l(x,"y","20"),l(x,"text-anchor","middle"),l(x,"class","cardinal svelte-1sgx2se"),l(ge,"x","280"),l(ge,"y","155"),l(ge,"text-anchor","middle"),l(ge,"class","cardinal svelte-1sgx2se"),l(oe,"x","150"),l(oe,"y","285"),l(oe,"text-anchor","middle"),l(oe,"class","cardinal svelte-1sgx2se"),l(ue,"x","20"),l(ue,"y","155"),l(ue,"text-anchor","middle"),l(ue,"class","cardinal svelte-1sgx2se"),l(f,"viewBox","0 0 300 300"),l(f,"class","map-svg svelte-1sgx2se"),l(j,"class","map-container svelte-1sgx2se"),l(Z,"type","number"),l(Z,"min","0"),l(Z,"max","359"),l(Z,"placeholder","Enter heading"),l(Z,"class","heading-input svelte-1sgx2se"),l(z,"class","go-btn svelte-1sgx2se"),l(G,"class","goto-container svelte-1sgx2se"),l(Ee,"class","control-btn ccw svelte-1sgx2se"),l(Se,"class","control-btn stop svelte-1sgx2se"),l(_e,"class","control-btn cw svelte-1sgx2se"),l(ve,"class","controls svelte-1sgx2se"),l(d,"class","metrics svelte-1sgx2se"),l(n,"class","card svelte-1sgx2se")},m(B,ne){je(B,n,ne),e(n,s),e(s,a),e(s,o),e(s,r),e(n,g),e(n,d),e(d,m),e(m,c),e(m,_),e(m,h),e(h,b),e(h,p),e(d,k),e(d,j),e(j,f),e(f,C),e(C,y),e(y,v),e(y,F),e(f,T),e(f,S),e(f,L),e(f,R),e(f,X),e(X,ae),e(ae,Y),e(ae,N),e(ae,I),e(ae,te),e(te,Q),e(X,K),e(K,D),e(X,M),e(M,ee),e(M,O),e(f,x),e(x,ke),e(f,ge),e(ge,Ae),e(f,oe),e(oe,xe),e(f,ue),e(ue,re);for(let U=0;U<4;U+=1)$[U]&&$[U].m(f,null);e(d,Ne),e(d,G),e(G,Z),Qe(Z,t[2]),e(G,Ie),e(G,z),e(d,Ce),e(d,ve),e(ve,Ee),e(ve,De),e(ve,Se),e(ve,He),e(ve,_e),fe||(Ye=[ye(Z,"input",t[8]),ye(z,"click",t[3]),ye(Ee,"click",t[5]),ye(Se,"click",t[6]),ye(_e,"click",t[4])],fe=!0)},p(B,[ne]){if(ne&2&&E(r,"disconnected",!B[1]),ne&1&&P(b,B[0]),ne&1&&Te!==(Te="rotate("+B[0]+")")&&l(ae,"transform",Te),ne&0){le=dt([45,135,225,315]);let U;for(U=0;U<4;U+=1){const Me=ol(B,le,U);$[U]?$[U].p(Me,ne):($[U]=rl(Me),$[U].c(),$[U].m(f,null))}for(;U<4;U+=1)$[U].d(1)}ne&4&&Ot(Z.value)!==B[2]&&Qe(Z,B[2])},i:Re,o:Re,d(B){B&&we(n),It($,B),fe=!1,ot(Ye)}}}function rn(t,n,s){let{status:a}=n,o=0,r=!1,g=0;async function d(){if(g<0||g>359){alert("Heading must be between 0 and 359");return}try{const b=(g-10+360)%360;await Ue.rotator.setHeading(b)}catch(b){console.error("Failed to set heading:",b),alert("Failed to rotate")}}async function m(){try{await Ue.rotator.rotateCW()}catch(b){console.error("Failed to rotate CW:",b)}}async function c(){try{await Ue.rotator.rotateCCW()}catch(b){console.error("Failed to rotate CCW:",b)}}async function _(){try{await Ue.rotator.stop()}catch(b){console.error("Failed to stop:",b)}}function h(){g=Ot(this.value),s(2,g)}return t.$$set=b=>{"status"in b&&s(7,a=b.status)},t.$$.update=()=>{t.$$.dirty&128&&(a==null?void 0:a.heading)!==void 0&&(a==null?void 0:a.heading)!==null&&s(0,o=a.heading),t.$$.dirty&128&&s(1,r=(a==null?void 0:a.connected)||!1)},[o,r,g,d,m,c,_,a,h]}class cn extends yt{constructor(n){super(),wt(this,n,rn,on,mt,{status:7})}}function cl(t,n,s){const a=t.slice();return a[26]=n[s],a[28]=s,a}function dl(t,n,s){const a=t.slice();return a[26]=n[s],a[28]=s,a}function ul(t){let n,s,a,o,r,g,d,m,c,_=t[6].toFixed(0)+"",h,b;return{c(){n=i("div"),s=i("h3"),s.textContent="Motors Moving...",a=u(),o=i("div"),r=i("div"),g=u(),d=i("div"),m=w(t[0]),c=w(" / 60 ("),h=w(_),b=w("%)"),l(s,"class","svelte-1knxynm"),l(r,"class","progress-fill svelte-1knxynm"),de(r,"width",t[6]+"%"),l(o,"class","progress-bar svelte-1knxynm"),l(d,"class","progress-text svelte-1knxynm"),l(n,"class","progress-section svelte-1knxynm")},m(p,k){je(p,n,k),e(n,s),e(n,a),e(n,o),e(o,r),e(n,g),e(n,d),e(d,m),e(d,c),e(d,h),e(d,b)},p(p,k){k&64&&de(r,"width",p[6]+"%"),k&1&&P(m,p[0]),k&64&&_!==(_=p[6].toFixed(0)+"")&&P(h,_)},d(p){p&&we(n)}}}function vl(t){let n,s,a,o,r=t[26]+"",g,d,m;return{c(){n=i("div"),s=i("div"),s.textContent=`Element ${t[28]+1}`,a=u(),o=i("div"),g=w(r),d=w(" mm"),m=u(),l(s,"class","element-label svelte-1knxynm"),l(o,"class","element-value svelte-1knxynm"),l(n,"class","element-item svelte-1knxynm")},m(c,_){je(c,n,_),e(n,s),e(n,a),e(n,o),e(o,g),e(o,d),e(n,m)},p(c,_){_&128&&r!==(r=c[26]+"")&&P(g,r)},d(c){c&&we(n)}}}function fl(t){let n,s=t[26]>0&&vl(t);return{c(){s&&s.c(),n=wl()},m(a,o){s&&s.m(a,o),je(a,n,o)},p(a,o){a[26]>0?s?s.p(a,o):(s=vl(a),s.c(),s.m(n.parentNode,n)):s&&(s.d(1),s=null)},d(a){a&&we(n),s&&s.d(a)}}}function pl(t){let n,s,a,o,r,g,d,m,c,_,h,b,p,k,j,f,C=dt(t[7]),y=[];for(let v=0;v⚙️ + Apply Adjustment`,p=u(),k=i("p"),k.textContent="⚠️ Calibration changes are saved after 12 seconds. Do not turn off during this time.",l(a,"for","element-select"),l(a,"class","svelte-1knxynm"),l(r,"id","element-select"),l(r,"class","svelte-1knxynm"),t[4]===void 0&&qt(()=>t[24].call(r)),l(s,"class","input-group svelte-1knxynm"),l(m,"for","adjustment"),l(m,"class","svelte-1knxynm"),l(_,"id","adjustment"),l(_,"type","number"),l(_,"step","1"),l(_,"placeholder","±10"),l(_,"class","svelte-1knxynm"),l(d,"class","input-group svelte-1knxynm"),l(b,"class","btn-caution svelte-1knxynm"),l(k,"class","warning-text svelte-1knxynm"),l(n,"class","calibration-controls svelte-1knxynm")},m(v,F){je(v,n,F),e(n,s),e(s,a),e(s,o),e(s,r);for(let T=0;T0&&ml(t);return{c(){s&&s.c(),n=wl()},m(a,o){s&&s.m(a,o),je(a,n,o)},p(a,o){a[26]>0?s?s.p(a,o):(s=ml(a),s.c(),s.m(n.parentNode,n)):s&&(s.d(1),s=null)},d(a){a&&we(n),s&&s.d(a)}}}function dn(t){let n,s,a,o,r,g,d,m,c,_,h,b,p=(t[12]/1e3).toFixed(3)+"",k,j,f,C,y,v,F,T=(t[14][t[11]]||"Unknown")+"",S,L,R,X,ae,Y,N=t[15][t[10]]+"",I,te,Q,Te,K,D,M,ee,O,x,ke,ge,Ae,oe,xe,ue,re,Ne,G,Z,Ie,z,Ce,ve,Ee,De,Se,He,_e,fe,Ye,le,$,B,ne,U,Me,We,Ke,Oe=t[3]?"Hide":"Show",Je,et,Ze,ze,Le,Be,Ge,pe=t[9]>0&&ul(t),Xe=dt(t[7]),V=[];for(let A=0;A📡 + Set Frequency`,He=u(),pe&&pe.c(),_e=u(),fe=i("div"),Ye=i("h3"),Ye.textContent="Element Lengths (mm)",le=u(),$=i("div");for(let A=0;A↓ + Retract Elements`,l(a,"class","svelte-1knxynm"),l(r,"class","status-dot svelte-1knxynm"),E(r,"disconnected",!t[13]),l(s,"class","card-header svelte-1knxynm"),l(_,"class","status-label svelte-1knxynm"),l(b,"class","status-value freq svelte-1knxynm"),l(c,"class","status-item svelte-1knxynm"),l(y,"class","status-label svelte-1knxynm"),l(F,"class","status-value band svelte-1knxynm"),l(C,"class","status-item svelte-1knxynm"),l(X,"class","status-label svelte-1knxynm"),l(Y,"class","status-value direction svelte-1knxynm"),l(R,"class","status-item svelte-1knxynm"),l(Te,"class","status-label svelte-1knxynm"),l(D,"class","status-value fw svelte-1knxynm"),l(Q,"class","status-item svelte-1knxynm"),l(m,"class","status-grid svelte-1knxynm"),l(ke,"class","svelte-1knxynm"),l(xe,"for","target-freq"),l(xe,"class","svelte-1knxynm"),l(re,"id","target-freq"),l(re,"type","number"),l(re,"min","1800"),l(re,"max","30000"),l(re,"step","1"),l(re,"placeholder","e.g. 14200"),l(re,"class","svelte-1knxynm"),l(oe,"class","input-group svelte-1knxynm"),l(Z,"for","target-dir"),l(Z,"class","svelte-1knxynm"),Ce.__value=0,Qe(Ce,Ce.__value),ve.__value=1,Qe(ve,ve.__value),Ee.__value=2,Qe(Ee,Ee.__value),l(z,"id","target-dir"),l(z,"class","svelte-1knxynm"),t[2]===void 0&&qt(()=>t[22].call(z)),l(G,"class","input-group svelte-1knxynm"),l(Se,"class","btn-primary svelte-1knxynm"),l(Ae,"class","freq-control svelte-1knxynm"),l(x,"class","control-section svelte-1knxynm"),l(Ye,"class","svelte-1knxynm"),l($,"class","elements-grid svelte-1knxynm"),l(fe,"class","elements-section svelte-1knxynm"),l(Me,"class","svelte-1knxynm"),l(Ke,"class","btn-toggle svelte-1knxynm"),E(Ke,"active",t[3]),l(U,"class","section-header svelte-1knxynm"),l(ne,"class","calibration-section svelte-1knxynm"),l(Le,"class","btn-danger svelte-1knxynm"),l(ze,"class","actions svelte-1knxynm"),l(d,"class","metrics svelte-1knxynm"),l(n,"class","card svelte-1knxynm")},m(A,ce){je(A,n,ce),e(n,s),e(s,a),e(s,o),e(s,r),e(n,g),e(n,d),e(d,m),e(m,c),e(c,_),e(c,h),e(c,b),e(b,k),e(b,j),e(m,f),e(m,C),e(C,y),e(C,v),e(C,F),e(F,S),e(m,L),e(m,R),e(R,X),e(R,ae),e(R,Y),e(Y,I),e(m,te),e(m,Q),e(Q,Te),e(Q,K),e(Q,D),e(D,M),e(D,ee),e(d,O),e(d,x),e(x,ke),e(x,ge),e(x,Ae),e(Ae,oe),e(oe,xe),e(oe,ue),e(oe,re),Qe(re,t[1]),e(Ae,Ne),e(Ae,G),e(G,Z),e(G,Ie),e(G,z),e(z,Ce),e(z,ve),e(z,Ee),Pt(z,t[2],!0),e(Ae,De),e(Ae,Se),e(d,He),pe&&pe.m(d,null),e(d,_e),e(d,fe),e(fe,Ye),e(fe,le),e(fe,$);for(let W=0;W0?pe?pe.p(A,ce):(pe=ul(A),pe.c(),pe.m(d,_e)):pe&&(pe.d(1),pe=null),ce&128){Xe=dt(A[7]);let W;for(W=0;W3e4){alert("Frequency must be between 1.8 MHz and 30 MHz");return}try{await Ue.ultrabeam.setFrequency(f,C)}catch(I){console.error("Failed to set frequency:",I),alert("Failed to set frequency")}}async function S(){if(confirm("Retract all antenna elements?"))try{await Ue.ultrabeam.retract()}catch(I){console.error("Failed to retract:",I),alert("Failed to retract")}}async function L(){try{const I=_[v]+F;alert(`Would adjust element ${v} by ${F}mm to ${I}mm`),s(5,F=0)}catch(I){console.error("Failed to adjust element:",I),alert("Failed to adjust element")}}function R(){f=Ot(this.value),s(1,f)}function X(){C=Qt(this),s(2,C)}const ae=()=>s(3,y=!y);function Y(){v=Qt(this),s(4,v)}function N(){F=Ot(this.value),s(5,F)}return t.$$set=I=>{"status"in I&&s(19,p=I.status)},t.$$.update=()=>{t.$$.dirty&524288&&s(13,a=(p==null?void 0:p.connected)||!1),t.$$.dirty&524288&&s(12,o=(p==null?void 0:p.frequency)||0),t.$$.dirty&524288&&s(11,r=(p==null?void 0:p.band)||0),t.$$.dirty&524288&&s(10,g=(p==null?void 0:p.direction)||0),t.$$.dirty&524288&&s(9,d=(p==null?void 0:p.motors_moving)||0),t.$$.dirty&524288&&s(20,m=(p==null?void 0:p.progress_total)||0),t.$$.dirty&524288&&s(0,c=(p==null?void 0:p.progress_current)||0),t.$$.dirty&524288&&s(7,_=(p==null?void 0:p.element_lengths)||[]),t.$$.dirty&524288&&s(8,h=p?`${p.firmware_major}.${p.firmware_minor}`:"0.0"),t.$$.dirty&1048577&&s(6,b=m>0?c/60*100:0)},[c,f,C,y,v,F,b,_,h,d,g,r,o,a,k,j,T,S,L,p,m,R,X,ae,Y,N]}class vn extends yt{constructor(n){super(),wt(this,n,un,dn,mt,{status:19})}}function fn(t){var lt,vt,ft,pt,jt,Nt;let n,s,a,o,r,g,d,m,c,_,h=t[1]?"Connected":"Disconnected",b,p,k,j,f,C,y,v=t[5].sfi+"",F,T,S,L,R,X=t[5].sunspots+"",ae,Y,N,I,te,Q=t[5].a_index+"",Te,K,D,M,ee,O=t[5].k_index+"",x,ke,ge,Ae,oe,xe=t[5].geomag+"",ue,re,Ne,G,Z,Ie,z=t[4].wind_speed.toFixed(1)+"",Ce,ve,Ee,De,Se,He=t[4].wind_gust.toFixed(1)+"",_e,fe,Ye,le,$,B=t[4].temp.toFixed(1)+"",ne,U,Me,We,Ke,Oe=t[4].feels_like.toFixed(1)+"",Je,et,Ze,ze,Le,Be=gl(t[2])+"",Ge,pe,Xe,V=t[2].toLocaleDateString()+"",me,A,ce,W,qe,Ve,$e,H,be,rt,ut,nt,ct,ht,st,At,it,tt,Pe;return Ve=new Rl({props:{status:(lt=t[0])==null?void 0:lt.webswitch}}),H=new Yl({props:{status:(vt=t[0])==null?void 0:vt.power_genius}}),rt=new ln({props:{status:(ft=t[0])==null?void 0:ft.tuner_genius}}),ct=new an({props:{status:(pt=t[0])==null?void 0:pt.antenna_genius}}),st=new cn({props:{status:(jt=t[0])==null?void 0:jt.rotator_genius}}),tt=new vn({props:{status:(Nt=t[0])==null?void 0:Nt.ultrabeam}}),{c(){n=i("div"),s=i("header"),a=i("div"),o=i("h1"),r=w(t[3]),g=w(" Shack"),d=u(),m=i("div"),c=i("span"),_=u(),b=w(h),p=u(),k=i("div"),j=i("div"),f=i("span"),C=w("SFI "),y=i("span"),F=w(v),T=u(),S=i("span"),L=w("Spots "),R=i("span"),ae=w(X),Y=u(),N=i("span"),I=w("A "),te=i("span"),Te=w(Q),K=u(),D=i("span"),M=w("K "),ee=i("span"),x=w(O),ke=u(),ge=i("span"),Ae=w("G "),oe=i("span"),ue=w(xe),re=u(),Ne=i("div"),G=i("div"),Z=i("span"),Ie=w("🌬️ "),Ce=w(z),ve=w("m/s"),Ee=u(),De=i("span"),Se=w("💨 "),_e=w(He),fe=w("m/s"),Ye=u(),le=i("span"),$=w("🌡️ "),ne=w(B),U=w("°C"),Me=u(),We=i("span"),Ke=w("→ "),Je=w(Oe),et=w("°C"),Ze=u(),ze=i("div"),Le=i("span"),Ge=w(Be),pe=u(),Xe=i("span"),me=w(V),A=u(),ce=i("main"),W=i("div"),qe=i("div"),Ft(Ve.$$.fragment),$e=u(),Ft(H.$$.fragment),be=u(),Ft(rt.$$.fragment),ut=u(),nt=i("div"),Ft(ct.$$.fragment),ht=u(),Ft(st.$$.fragment),At=u(),it=i("div"),Ft(tt.$$.fragment),l(o,"class","svelte-1hrhory"),l(c,"class","status-indicator"),E(c,"status-online",t[1]),E(c,"status-offline",!t[1]),l(m,"class","connection-status svelte-1hrhory"),l(a,"class","header-left svelte-1hrhory"),l(y,"class","value svelte-1hrhory"),l(f,"class","solar-item svelte-1hrhory"),l(R,"class","value svelte-1hrhory"),l(S,"class","solar-item svelte-1hrhory"),l(te,"class","value svelte-1hrhory"),l(N,"class","solar-item svelte-1hrhory"),l(ee,"class","value svelte-1hrhory"),l(D,"class","solar-item svelte-1hrhory"),l(oe,"class","value svelte-1hrhory"),l(ge,"class","solar-item svelte-1hrhory"),l(j,"class","solar-info svelte-1hrhory"),l(k,"class","header-center svelte-1hrhory"),l(Z,"title","Wind"),l(De,"title","Gust"),l(le,"title","Temperature"),l(We,"title","Feels like"),l(G,"class","weather-info svelte-1hrhory"),l(Le,"class","time svelte-1hrhory"),l(Xe,"class","date svelte-1hrhory"),l(ze,"class","clock svelte-1hrhory"),l(Ne,"class","header-right svelte-1hrhory"),l(s,"class","svelte-1hrhory"),l(qe,"class","row svelte-1hrhory"),l(nt,"class","row svelte-1hrhory"),l(it,"class","row svelte-1hrhory"),l(W,"class","dashboard-grid svelte-1hrhory"),l(ce,"class","svelte-1hrhory"),l(n,"class","app svelte-1hrhory")},m(q,se){je(q,n,se),e(n,s),e(s,a),e(a,o),e(o,r),e(o,g),e(a,d),e(a,m),e(m,c),e(m,_),e(m,b),e(s,p),e(s,k),e(k,j),e(j,f),e(f,C),e(f,y),e(y,F),e(j,T),e(j,S),e(S,L),e(S,R),e(R,ae),e(j,Y),e(j,N),e(N,I),e(N,te),e(te,Te),e(j,K),e(j,D),e(D,M),e(D,ee),e(ee,x),e(j,ke),e(j,ge),e(ge,Ae),e(ge,oe),e(oe,ue),e(s,re),e(s,Ne),e(Ne,G),e(G,Z),e(Z,Ie),e(Z,Ce),e(Z,ve),e(G,Ee),e(G,De),e(De,Se),e(De,_e),e(De,fe),e(G,Ye),e(G,le),e(le,$),e(le,ne),e(le,U),e(G,Me),e(G,We),e(We,Ke),e(We,Je),e(We,et),e(Ne,Ze),e(Ne,ze),e(ze,Le),e(Le,Ge),e(ze,pe),e(ze,Xe),e(Xe,me),e(n,A),e(n,ce),e(ce,W),e(W,qe),_t(Ve,qe,null),e(qe,$e),_t(H,qe,null),e(qe,be),_t(rt,qe,null),e(W,ut),e(W,nt),_t(ct,nt,null),e(nt,ht),_t(st,nt,null),e(W,At),e(W,it),_t(tt,it,null),Pe=!0},p(q,[se]){var Jt,Xt,Vt,Yt,Kt,Zt;(!Pe||se&8)&&P(r,q[3]),(!Pe||se&2)&&E(c,"status-online",q[1]),(!Pe||se&2)&&E(c,"status-offline",!q[1]),(!Pe||se&2)&&h!==(h=q[1]?"Connected":"Disconnected")&&P(b,h),(!Pe||se&32)&&v!==(v=q[5].sfi+"")&&P(F,v),(!Pe||se&32)&&X!==(X=q[5].sunspots+"")&&P(ae,X),(!Pe||se&32)&&Q!==(Q=q[5].a_index+"")&&P(Te,Q),(!Pe||se&32)&&O!==(O=q[5].k_index+"")&&P(x,O),(!Pe||se&32)&&xe!==(xe=q[5].geomag+"")&&P(ue,xe),(!Pe||se&16)&&z!==(z=q[4].wind_speed.toFixed(1)+"")&&P(Ce,z),(!Pe||se&16)&&He!==(He=q[4].wind_gust.toFixed(1)+"")&&P(_e,He),(!Pe||se&16)&&B!==(B=q[4].temp.toFixed(1)+"")&&P(ne,B),(!Pe||se&16)&&Oe!==(Oe=q[4].feels_like.toFixed(1)+"")&&P(Je,Oe),(!Pe||se&4)&&Be!==(Be=gl(q[2])+"")&&P(Ge,Be),(!Pe||se&4)&&V!==(V=q[2].toLocaleDateString()+"")&&P(me,V);const at={};se&1&&(at.status=(Jt=q[0])==null?void 0:Jt.webswitch),Ve.$set(at);const J={};se&1&&(J.status=(Xt=q[0])==null?void 0:Xt.power_genius),H.$set(J);const ie={};se&1&&(ie.status=(Vt=q[0])==null?void 0:Vt.tuner_genius),rt.$set(ie);const Wt={};se&1&&(Wt.status=(Yt=q[0])==null?void 0:Yt.antenna_genius),ct.$set(Wt);const Gt={};se&1&&(Gt.status=(Kt=q[0])==null?void 0:Kt.rotator_genius),st.$set(Gt);const Ut={};se&1&&(Ut.status=(Zt=q[0])==null?void 0:Zt.ultrabeam),tt.$set(Ut)},i(q){Pe||(gt(Ve.$$.fragment,q),gt(H.$$.fragment,q),gt(rt.$$.fragment,q),gt(ct.$$.fragment,q),gt(st.$$.fragment,q),gt(tt.$$.fragment,q),Pe=!0)},o(q){Ct(Ve.$$.fragment,q),Ct(H.$$.fragment,q),Ct(rt.$$.fragment,q),Ct(ct.$$.fragment,q),Ct(st.$$.fragment,q),Ct(tt.$$.fragment,q),Pe=!1},d(q){q&&we(n),bt(Ve),bt(H),bt(rt),bt(ct),bt(st),bt(tt)}}}function gl(t){return t.toTimeString().slice(0,8)}function pn(t,n,s){let a,o,r=null,g=!1,d=new Date,m="F4BPO";const c=kl.subscribe(h=>{s(0,r=h)}),_=Bt.subscribe(h=>{s(1,g=h)});return Ml(async()=>{ll.connect();try{const b=await Ue.getConfig();b.callsign&&s(3,m=b.callsign)}catch(b){console.error("Failed to fetch config:",b)}const h=setInterval(()=>{s(2,d=new Date)},1e3);return()=>{clearInterval(h)}}),Ol(()=>{ll.disconnect(),c(),_()}),t.$$.update=()=>{t.$$.dirty&1&&s(5,a=(r==null?void 0:r.solar)||{sfi:0,sunspots:0,a_index:0,k_index:0,geomag:"Unknown"}),t.$$.dirty&1&&s(4,o=(r==null?void 0:r.weather)||{wind_speed:0,wind_gust:0,temp:0,feels_like:0})},[r,g,d,m,o,a]}class mn extends yt{constructor(n){super(),wt(this,n,pn,fn,mt,{})}}new mn({target:document.getElementById("app")}); diff --git a/cmd/server/web/dist/assets/index-Ml--d1Bc.css b/cmd/server/web/dist/assets/index-Ml--d1Bc.css new file mode 100644 index 0000000..f33f146 --- /dev/null +++ b/cmd/server/web/dist/assets/index-Ml--d1Bc.css @@ -0,0 +1 @@ +.card.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-z2csmj.svelte-z2csmj{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-z2csmj.svelte-z2csmj{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-z2csmj.svelte-z2csmj{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-z2csmj.svelte-z2csmj{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-z2csmj.svelte-z2csmj{padding:16px;display:flex;flex-direction:column;gap:12px}.relays.svelte-z2csmj.svelte-z2csmj{display:flex;flex-direction:column;gap:8px}.relay-card.svelte-z2csmj.svelte-z2csmj{display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--bg-tertiary);border-radius:6px;border:1px solid var(--border-color);transition:all .3s}.relay-card.relay-on.svelte-z2csmj.svelte-z2csmj{background:#4caf501a;border-color:#4caf504d;box-shadow:0 0 15px #4caf5033}.relay-info.svelte-z2csmj.svelte-z2csmj{display:flex;align-items:center}.relay-details.svelte-z2csmj.svelte-z2csmj{display:flex;flex-direction:column;gap:2px}.relay-name.svelte-z2csmj.svelte-z2csmj{font-size:12px;color:var(--text-primary);font-weight:500}.relay-status.svelte-z2csmj.svelte-z2csmj{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.relay-card.relay-on.svelte-z2csmj .relay-status.svelte-z2csmj{color:#4caf50;font-weight:600}.relay-toggle.svelte-z2csmj.svelte-z2csmj{padding:0;background:transparent;border:none;cursor:pointer}.toggle-track.svelte-z2csmj.svelte-z2csmj{width:52px;height:28px;background:var(--bg-primary);border:2px solid var(--border-color);border-radius:14px;position:relative;transition:all .3s}.relay-toggle.svelte-z2csmj:hover .toggle-track.svelte-z2csmj{border-color:var(--accent-cyan)}.relay-toggle.active.svelte-z2csmj .toggle-track.svelte-z2csmj{background:linear-gradient(135deg,#4caf50,#66bb6a);border-color:#4caf50;box-shadow:0 0 15px #4caf5080}.toggle-thumb.svelte-z2csmj.svelte-z2csmj{width:20px;height:20px;background:#fff;border-radius:50%;position:absolute;top:2px;left:2px;transition:all .3s;box-shadow:0 2px 4px #0000004d}.relay-toggle.active.svelte-z2csmj .toggle-thumb.svelte-z2csmj{transform:translate(24px)}.relay-toggle.svelte-z2csmj.svelte-z2csmj:disabled{opacity:.5;cursor:not-allowed}.controls.svelte-z2csmj.svelte-z2csmj{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:8px}.control-btn.svelte-z2csmj.svelte-z2csmj{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:6px}.control-btn.svelte-z2csmj.svelte-z2csmj:hover{transform:translateY(-2px)}.control-btn.svelte-z2csmj.svelte-z2csmj:active{transform:translateY(0)}.btn-icon.svelte-z2csmj.svelte-z2csmj{font-size:16px}.all-on.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#4caf50,#66bb6a);color:#fff;box-shadow:0 4px 12px #4caf5066}.all-on.svelte-z2csmj.svelte-z2csmj:hover{box-shadow:0 6px 16px #4caf5080}.all-off.svelte-z2csmj.svelte-z2csmj{background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff;box-shadow:0 4px 12px #f4433666}.all-off.svelte-z2csmj.svelte-z2csmj:hover{box-shadow:0 6px 16px #f4433680}.card.svelte-utvwj6.svelte-utvwj6{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.header-right.svelte-utvwj6.svelte-utvwj6{display:flex;align-items:center;gap:8px}.state-badge.svelte-utvwj6.svelte-utvwj6{padding:4px 12px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;transition:all .2s}.state-badge.idle.svelte-utvwj6.svelte-utvwj6{background:#4caf5033;color:#4caf50}.state-badge.transmit.svelte-utvwj6.svelte-utvwj6{background:#ff980033;color:#ff9800;animation:svelte-utvwj6-pulse 1s infinite}@keyframes svelte-utvwj6-pulse{0%,to{opacity:1}50%{opacity:.7}}.status-dot.svelte-utvwj6.svelte-utvwj6{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-utvwj6.svelte-utvwj6{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-utvwj6.svelte-utvwj6{padding:16px;display:flex;flex-direction:column;gap:16px}.power-display.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:8px}.power-main.svelte-utvwj6.svelte-utvwj6{text-align:center}.power-value.svelte-utvwj6.svelte-utvwj6{font-size:48px;font-weight:200;color:var(--accent-cyan);line-height:1;text-shadow:0 0 20px rgba(79,195,247,.5)}.power-value.svelte-utvwj6 .unit.svelte-utvwj6{font-size:24px;color:var(--text-secondary);margin-left:4px}.power-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px}.power-bar.svelte-utvwj6.svelte-utvwj6{position:relative;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.power-bar-fill.svelte-utvwj6.svelte-utvwj6{position:relative;height:100%;background:linear-gradient(90deg,#4caf50,#ffc107,#ff9800,#f44336);border-radius:4px;transition:width .3s ease}.power-bar-glow.svelte-utvwj6.svelte-utvwj6{position:absolute;top:0;right:0;width:20px;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.5));animation:svelte-utvwj6-shimmer 2s infinite}@keyframes svelte-utvwj6-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}.power-scale.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);margin-top:4px}.swr-container.svelte-utvwj6.svelte-utvwj6{display:flex;align-items:center;gap:16px}.swr-circle.svelte-utvwj6.svelte-utvwj6{width:80px;height:80px;border-radius:50%;background:radial-gradient(circle,rgba(79,195,247,.1),transparent);border:3px solid var(--swr-color);display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 20px var(--swr-color)}.swr-value.svelte-utvwj6.svelte-utvwj6{font-size:24px;font-weight:300;color:var(--swr-color)}.swr-label.svelte-utvwj6.svelte-utvwj6{font-size:10px;color:var(--text-muted);text-transform:uppercase}.swr-status.svelte-utvwj6.svelte-utvwj6{flex:1}.status-text.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.status-text.good.svelte-utvwj6.svelte-utvwj6{color:#4caf50}.status-text.ok.svelte-utvwj6.svelte-utvwj6{color:#ffc107}.status-text.warning.svelte-utvwj6.svelte-utvwj6{color:#ff9800}.status-text.danger.svelte-utvwj6.svelte-utvwj6{color:#f44336}.temp-group.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:1fr 1fr;gap:12px}.temp-item.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:4px;padding:12px;background:var(--bg-tertiary);border-radius:6px}.temp-value.svelte-utvwj6.svelte-utvwj6{font-size:32px;font-weight:300;line-height:1}.temp-label.svelte-utvwj6.svelte-utvwj6{font-size:10px;color:var(--text-muted);text-transform:uppercase}.temp-mini-bar.svelte-utvwj6.svelte-utvwj6{height:4px;background:#ffffff1a;border-radius:2px;overflow:hidden;margin-top:4px}.temp-mini-fill.svelte-utvwj6.svelte-utvwj6{height:100%;border-radius:2px;transition:width .3s ease}.params-grid.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}.param-box.svelte-utvwj6.svelte-utvwj6{padding:8px;background:var(--bg-tertiary);border-radius:4px;text-align:center}.param-label.svelte-utvwj6.svelte-utvwj6{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.param-value.svelte-utvwj6.svelte-utvwj6{font-size:18px;font-weight:300;color:var(--text-primary);margin-top:2px}.band-display.svelte-utvwj6.svelte-utvwj6{display:grid;grid-template-columns:1fr 1fr;gap:8px;padding:8px;background:#4fc3f70d;border-radius:6px}.band-item.svelte-utvwj6.svelte-utvwj6{display:flex;justify-content:space-between;align-items:center}.band-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted)}.band-value.svelte-utvwj6.svelte-utvwj6{font-size:14px;font-weight:600;color:var(--accent-cyan)}.fan-control.svelte-utvwj6.svelte-utvwj6{display:flex;flex-direction:column;gap:6px}.control-label.svelte-utvwj6.svelte-utvwj6{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}select.svelte-utvwj6.svelte-utvwj6{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:8px;font-size:12px;cursor:pointer;outline:none;transition:all .2s}select.svelte-utvwj6.svelte-utvwj6:hover{border-color:var(--accent-cyan)}select.svelte-utvwj6.svelte-utvwj6:focus{border-color:var(--accent-cyan);box-shadow:0 0 0 2px #4fc3f733}.card.svelte-j97t2j.svelte-j97t2j{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-j97t2j.svelte-j97t2j{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-j97t2j.svelte-j97t2j{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.header-right.svelte-j97t2j.svelte-j97t2j{display:flex;align-items:center;gap:8px}.tuning-badge.svelte-j97t2j.svelte-j97t2j{padding:4px 12px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;background:#4caf5033;color:#4caf50}.tuning-badge.tuning.svelte-j97t2j.svelte-j97t2j{background:#ff980033;color:#ff9800;animation:svelte-j97t2j-pulse 1s infinite}@keyframes svelte-j97t2j-pulse{0%,to{opacity:1}50%{opacity:.7}}.status-dot.svelte-j97t2j.svelte-j97t2j{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-j97t2j.svelte-j97t2j{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-j97t2j.svelte-j97t2j{padding:16px;display:flex;flex-direction:column;gap:16px}.power-display.svelte-j97t2j.svelte-j97t2j{display:flex;flex-direction:column;gap:8px}.power-main.svelte-j97t2j.svelte-j97t2j{text-align:center}.power-value.svelte-j97t2j.svelte-j97t2j{font-size:48px;font-weight:200;color:var(--accent-cyan);line-height:1;text-shadow:0 0 20px rgba(79,195,247,.5)}.power-value.svelte-j97t2j .unit.svelte-j97t2j{font-size:24px;color:var(--text-secondary);margin-left:4px}.power-label.svelte-j97t2j.svelte-j97t2j{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-top:4px}.power-bar.svelte-j97t2j.svelte-j97t2j{position:relative;height:8px;background:var(--bg-tertiary);border-radius:4px;overflow:hidden}.power-bar-fill.svelte-j97t2j.svelte-j97t2j{position:relative;height:100%;background:linear-gradient(90deg,#4caf50,#ffc107,#ff9800,#f44336);border-radius:4px;transition:width .3s ease}.power-bar-glow.svelte-j97t2j.svelte-j97t2j{position:absolute;top:0;right:0;width:20px;height:100%;background:linear-gradient(90deg,transparent,rgba(255,255,255,.5));animation:svelte-j97t2j-shimmer 2s infinite}@keyframes svelte-j97t2j-shimmer{0%{transform:translate(-100%)}to{transform:translate(100%)}}.power-scale.svelte-j97t2j.svelte-j97t2j{display:flex;justify-content:space-between;font-size:9px;color:var(--text-muted);margin-top:4px}.swr-container.svelte-j97t2j.svelte-j97t2j{display:flex;align-items:center;gap:16px}.swr-circle.svelte-j97t2j.svelte-j97t2j{width:80px;height:80px;border-radius:50%;background:radial-gradient(circle,rgba(79,195,247,.1),transparent);border:3px solid var(--swr-color);display:flex;flex-direction:column;align-items:center;justify-content:center;box-shadow:0 0 20px var(--swr-color)}.swr-value.svelte-j97t2j.svelte-j97t2j{font-size:24px;font-weight:300;color:var(--swr-color)}.swr-label.svelte-j97t2j.svelte-j97t2j{font-size:10px;color:var(--text-muted);text-transform:uppercase}.swr-status.svelte-j97t2j.svelte-j97t2j{flex:1}.status-text.svelte-j97t2j.svelte-j97t2j{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.status-text.good.svelte-j97t2j.svelte-j97t2j{color:#4caf50}.status-text.ok.svelte-j97t2j.svelte-j97t2j{color:#ffc107}.status-text.warning.svelte-j97t2j.svelte-j97t2j{color:#ff9800}.status-text.danger.svelte-j97t2j.svelte-j97t2j{color:#f44336}.capacitors.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;padding:16px;background:#4fc3f70d;border-radius:6px;border:1px solid rgba(79,195,247,.2)}.cap-item.svelte-j97t2j.svelte-j97t2j{display:flex;flex-direction:column;align-items:center;gap:4px}.cap-value.svelte-j97t2j.svelte-j97t2j{font-size:32px;font-weight:300;color:var(--accent-cyan);text-shadow:0 0 15px rgba(79,195,247,.5)}.cap-label.svelte-j97t2j.svelte-j97t2j{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px}.freq-display.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:1fr 1fr;gap:12px}.freq-item.svelte-j97t2j.svelte-j97t2j{padding:10px;background:var(--bg-tertiary);border-radius:6px;display:flex;flex-direction:column;gap:4px}.freq-label.svelte-j97t2j.svelte-j97t2j{font-size:9px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px}.freq-value.svelte-j97t2j.svelte-j97t2j{font-size:16px;font-weight:300;color:var(--text-primary)}.freq-unit.svelte-j97t2j.svelte-j97t2j{font-size:11px;color:var(--text-secondary);margin-left:2px}.controls.svelte-j97t2j.svelte-j97t2j{display:grid;grid-template-columns:1fr 1fr;gap:8px}.control-btn.svelte-j97t2j.svelte-j97t2j{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-tertiary);color:var(--text-primary);transition:all .2s}.control-btn.svelte-j97t2j.svelte-j97t2j:hover{border-color:var(--accent-cyan);transform:translateY(-1px)}.control-btn.active.svelte-j97t2j.svelte-j97t2j{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000;box-shadow:0 0 15px #4fc3f780}.tune-btn.svelte-j97t2j.svelte-j97t2j{width:100%;padding:14px;border-radius:6px;font-size:13px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:8px;box-shadow:0 4px 12px #f4433666}.tune-btn.svelte-j97t2j.svelte-j97t2j:hover{transform:translateY(-2px);box-shadow:0 6px 16px #f4433680}.tune-btn.svelte-j97t2j.svelte-j97t2j:active{transform:translateY(0)}.tune-icon.svelte-j97t2j.svelte-j97t2j{font-size:18px}.card.svelte-1r71rs8{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-1r71rs8{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-1r71rs8{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-1r71rs8{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-1r71rs8{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-1r71rs8{padding:16px;display:flex;flex-direction:column;gap:12px}.sources.svelte-1r71rs8{display:grid;grid-template-columns:1fr 1fr;gap:8px}.source-item.svelte-1r71rs8{padding:8px;background:var(--bg-tertiary);border-radius:4px;text-align:center}.source-label.svelte-1r71rs8{font-size:12px;font-weight:600;color:var(--text-primary);text-transform:uppercase}.bands.svelte-1r71rs8{display:grid;grid-template-columns:1fr 1fr;gap:8px}.band-item.svelte-1r71rs8{padding:10px;background:#4fc3f71a;border:1px solid rgba(79,195,247,.3);border-radius:4px;text-align:center}.band-value.svelte-1r71rs8{font-size:16px;font-weight:600;color:var(--accent-cyan)}.antennas.svelte-1r71rs8{display:flex;flex-direction:column;gap:8px}.antenna-card.svelte-1r71rs8{display:flex;justify-content:space-between;align-items:center;padding:12px;background:var(--bg-tertiary);border:2px solid var(--border-color);border-radius:6px;transition:all .3s}.antenna-card.tx.svelte-1r71rs8{background:#f4433633;border-color:#f44336;box-shadow:0 0 20px #f4433666}.antenna-card.active-a.svelte-1r71rs8{background:#4caf5033;border-color:#4caf50;box-shadow:0 0 20px #4caf504d}.antenna-card.active-b.svelte-1r71rs8{background:#2196f333;border-color:#2196f3;box-shadow:0 0 20px #2196f34d}.antenna-name.svelte-1r71rs8{font-size:14px;font-weight:500;color:var(--text-primary)}.antenna-ports.svelte-1r71rs8{display:flex;gap:6px}.port-btn.svelte-1r71rs8{width:36px;height:36px;border-radius:4px;font-size:14px;font-weight:600;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-primary);color:var(--text-secondary);transition:all .2s}.port-btn.svelte-1r71rs8:hover{border-color:var(--accent-cyan);transform:scale(1.05)}.port-btn.active.svelte-1r71rs8{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000;box-shadow:0 0 12px #4fc3f780}.reboot-btn.svelte-1r71rs8{width:100%;padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;cursor:pointer;border:none;background:linear-gradient(135deg,#ff9800,#f57c00);color:#fff;transition:all .2s;display:flex;align-items:center;justify-content:center;gap:6px;box-shadow:0 4px 12px #ff980066;margin-top:8px}.reboot-btn.svelte-1r71rs8:hover{transform:translateY(-2px);box-shadow:0 6px 16px #ff980080}.reboot-btn.svelte-1r71rs8:active{transform:translateY(0)}.reboot-icon.svelte-1r71rs8{font-size:16px}.card.svelte-1sgx2se{background:linear-gradient(135deg,#1a2332,#0f1923);border:1px solid #2d3748;border-radius:8px;padding:0;overflow:hidden;box-shadow:0 4px 6px #0000004d}.card-header.svelte-1sgx2se{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#4fc3f70d;border-bottom:1px solid #2d3748}h2.svelte-1sgx2se{font-size:14px;font-weight:600;color:var(--accent-cyan);margin:0;letter-spacing:.5px}.status-dot.svelte-1sgx2se{width:8px;height:8px;border-radius:50%;background:#4caf50;box-shadow:0 0 8px #4caf50}.status-dot.disconnected.svelte-1sgx2se{background:#f44336;box-shadow:0 0 8px #f44336}.metrics.svelte-1sgx2se{padding:16px;display:flex;flex-direction:column;gap:16px}.heading-display.svelte-1sgx2se{text-align:center;padding:12px;background:#4fc3f71a;border-radius:6px;border:1px solid rgba(79,195,247,.3)}.heading-label.svelte-1sgx2se{font-size:10px;color:var(--text-muted);text-transform:uppercase;letter-spacing:1px;margin-bottom:4px}.heading-value.svelte-1sgx2se{font-size:42px;font-weight:200;color:var(--accent-cyan);text-shadow:0 0 20px rgba(79,195,247,.5)}.map-container.svelte-1sgx2se{display:flex;justify-content:center;padding:10px;background:#0a162899;border-radius:8px}.map-svg.svelte-1sgx2se{width:100%;max-width:300px;height:auto}.cardinal.svelte-1sgx2se{fill:var(--accent-cyan);font-size:18px;font-weight:700;text-shadow:0 0 10px rgba(79,195,247,.8)}.degree-label.svelte-1sgx2se{fill:#4fc3f7b3;font-size:12px;font-weight:600}.goto-container.svelte-1sgx2se{display:grid;grid-template-columns:1fr auto;gap:8px}.heading-input.svelte-1sgx2se{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:10px 12px;font-size:14px;outline:none;transition:all .2s}.heading-input.svelte-1sgx2se:focus{border-color:var(--accent-cyan);box-shadow:0 0 0 2px #4fc3f733}.go-btn.svelte-1sgx2se{padding:10px 24px;border-radius:4px;font-size:13px;font-weight:600;text-transform:uppercase;cursor:pointer;border:none;background:linear-gradient(135deg,var(--accent-cyan),#0288d1);color:#000;transition:all .2s;box-shadow:0 4px 12px #4fc3f766}.go-btn.svelte-1sgx2se:hover{transform:translateY(-2px);box-shadow:0 6px 16px #4fc3f780}.controls.svelte-1sgx2se{display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px}.control-btn.svelte-1sgx2se{padding:12px;border-radius:6px;font-size:12px;font-weight:600;text-transform:uppercase;cursor:pointer;border:1px solid var(--border-color);background:var(--bg-tertiary);color:var(--text-primary);transition:all .2s;display:flex;flex-direction:column;align-items:center;gap:4px}.control-btn.svelte-1sgx2se:hover{border-color:var(--accent-cyan);transform:translateY(-1px)}.control-btn.stop.svelte-1sgx2se{background:linear-gradient(135deg,#f44336,#d32f2f);border-color:#f44336;color:#fff;box-shadow:0 4px 12px #f4433666}.control-btn.stop.svelte-1sgx2se:hover{box-shadow:0 6px 16px #f4433680}.arrow.svelte-1sgx2se{font-size:20px;line-height:1}.card.svelte-1knxynm.svelte-1knxynm{background:linear-gradient(135deg,#1e293bf2,#0f172afa);border-radius:16px;padding:24px;box-shadow:0 8px 32px #0006;border:1px solid rgba(79,195,247,.2)}.card-header.svelte-1knxynm.svelte-1knxynm{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;padding-bottom:16px;border-bottom:2px solid rgba(79,195,247,.3)}h2.svelte-1knxynm.svelte-1knxynm{margin:0;font-size:24px;font-weight:600;background:linear-gradient(135deg,#4fc3f7,#03a9f4);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-shadow:0 0 20px rgba(79,195,247,.5)}h3.svelte-1knxynm.svelte-1knxynm{margin:0 0 12px;font-size:16px;font-weight:500;color:#4fc3f7}.status-dot.svelte-1knxynm.svelte-1knxynm{width:12px;height:12px;border-radius:50%;background:#4caf50;box-shadow:0 0 12px #4caf50cc;animation:svelte-1knxynm-pulse 2s ease-in-out infinite}.status-dot.disconnected.svelte-1knxynm.svelte-1knxynm{background:#666;box-shadow:none;animation:none}@keyframes svelte-1knxynm-pulse{0%,to{opacity:1}50%{opacity:.6}}.metrics.svelte-1knxynm.svelte-1knxynm{display:flex;flex-direction:column;gap:24px}.status-grid.svelte-1knxynm.svelte-1knxynm{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:16px}.status-item.svelte-1knxynm.svelte-1knxynm{background:#0f172a99;padding:16px;border-radius:12px;border:1px solid rgba(79,195,247,.2)}.status-label.svelte-1knxynm.svelte-1knxynm{font-size:12px;color:#fff9;text-transform:uppercase;letter-spacing:1px;margin-bottom:8px}.status-value.svelte-1knxynm.svelte-1knxynm{font-size:20px;font-weight:600;color:#4fc3f7}.status-value.freq.svelte-1knxynm.svelte-1knxynm{color:#66bb6a;font-size:24px}.status-value.band.svelte-1knxynm.svelte-1knxynm{color:#ffa726}.status-value.direction.svelte-1knxynm.svelte-1knxynm{color:#ab47bc}.control-section.svelte-1knxynm.svelte-1knxynm{background:#0f172a66;padding:20px;border-radius:12px;border:1px solid rgba(79,195,247,.2)}.freq-control.svelte-1knxynm.svelte-1knxynm{display:grid;grid-template-columns:2fr 1fr auto;gap:12px;align-items:end}.input-group.svelte-1knxynm.svelte-1knxynm{display:flex;flex-direction:column;gap:8px}.input-group.svelte-1knxynm label.svelte-1knxynm{font-size:12px;color:#ffffffb3;text-transform:uppercase;letter-spacing:.5px}input[type=number].svelte-1knxynm.svelte-1knxynm,select.svelte-1knxynm.svelte-1knxynm{background:#0f172acc;border:1px solid rgba(79,195,247,.3);border-radius:8px;padding:10px 12px;color:#fff;font-size:16px;transition:all .2s}input[type=number].svelte-1knxynm.svelte-1knxynm:focus,select.svelte-1knxynm.svelte-1knxynm:focus{outline:none;border-color:#4fc3f7;box-shadow:0 0 12px #4fc3f74d}button.svelte-1knxynm.svelte-1knxynm{padding:12px 20px;border-radius:8px;border:none;font-weight:600;font-size:14px;cursor:pointer;transition:all .2s;display:flex;align-items:center;gap:8px;justify-content:center}.btn-primary.svelte-1knxynm.svelte-1knxynm{background:linear-gradient(135deg,#4fc3f7,#03a9f4);color:#fff;box-shadow:0 4px 16px #4fc3f766}.btn-primary.svelte-1knxynm.svelte-1knxynm:hover{transform:translateY(-2px);box-shadow:0 6px 20px #4fc3f799}.btn-danger.svelte-1knxynm.svelte-1knxynm{background:linear-gradient(135deg,#f44336,#d32f2f);color:#fff;box-shadow:0 4px 16px #f4433666;width:100%}.btn-danger.svelte-1knxynm.svelte-1knxynm:hover{transform:translateY(-2px);box-shadow:0 6px 20px #f4433699}.btn-caution.svelte-1knxynm.svelte-1knxynm{background:linear-gradient(135deg,#ffa726,#fb8c00);color:#fff;box-shadow:0 4px 16px #ffa72666;width:100%}.btn-caution.svelte-1knxynm.svelte-1knxynm:hover{transform:translateY(-2px);box-shadow:0 6px 20px #ffa72699}.btn-toggle.svelte-1knxynm.svelte-1knxynm{background:#4fc3f71a;color:#4fc3f7;border:1px solid rgba(79,195,247,.3);padding:6px 12px;font-size:12px}.btn-toggle.active.svelte-1knxynm.svelte-1knxynm{background:#4fc3f733}.icon.svelte-1knxynm.svelte-1knxynm{font-size:16px}.progress-section.svelte-1knxynm.svelte-1knxynm{background:#0f172a66;padding:20px;border-radius:12px;border:1px solid rgba(255,193,7,.3)}.progress-bar.svelte-1knxynm.svelte-1knxynm{width:100%;height:24px;background:#0f172acc;border-radius:12px;overflow:hidden;margin:12px 0;border:1px solid rgba(79,195,247,.3)}.progress-fill.svelte-1knxynm.svelte-1knxynm{height:100%;background:linear-gradient(90deg,#4fc3f7,#66bb6a);transition:width .3s ease;box-shadow:0 0 12px #4fc3f799}.progress-text.svelte-1knxynm.svelte-1knxynm{text-align:center;color:#4fc3f7;font-weight:600}.elements-section.svelte-1knxynm.svelte-1knxynm{background:#0f172a66;padding:20px;border-radius:12px;border:1px solid rgba(79,195,247,.2)}.elements-grid.svelte-1knxynm.svelte-1knxynm{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:12px}.element-item.svelte-1knxynm.svelte-1knxynm{background:#0f172a99;padding:12px;border-radius:8px;border:1px solid rgba(79,195,247,.2);text-align:center}.element-label.svelte-1knxynm.svelte-1knxynm{font-size:11px;color:#fff9;margin-bottom:4px}.element-value.svelte-1knxynm.svelte-1knxynm{font-size:16px;font-weight:600;color:#66bb6a}.calibration-section.svelte-1knxynm.svelte-1knxynm{background:#ff98000d;padding:20px;border-radius:12px;border:1px solid rgba(255,152,0,.3)}.section-header.svelte-1knxynm.svelte-1knxynm{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px}.calibration-controls.svelte-1knxynm.svelte-1knxynm{display:flex;flex-direction:column;gap:16px}.warning-text.svelte-1knxynm.svelte-1knxynm{margin:0;padding:12px;background:#ff98001a;border-radius:8px;border-left:3px solid #ffa726;color:#ffa726;font-size:13px}.actions.svelte-1knxynm.svelte-1knxynm{display:flex;gap:12px}@media (max-width: 768px){.freq-control.svelte-1knxynm.svelte-1knxynm{grid-template-columns:1fr}.elements-grid.svelte-1knxynm.svelte-1knxynm{grid-template-columns:repeat(auto-fill,minmax(100px,1fr))}}.app.svelte-1hrhory.svelte-1hrhory{min-height:100vh;display:flex;flex-direction:column}header.svelte-1hrhory.svelte-1hrhory{background:linear-gradient(135deg,#1e3c72,#2a5298);padding:16px 24px;display:flex;justify-content:space-between;align-items:center;box-shadow:0 2px 8px #0000004d;flex-wrap:wrap;gap:16px}.header-left.svelte-1hrhory.svelte-1hrhory{display:flex;align-items:center;gap:16px}h1.svelte-1hrhory.svelte-1hrhory{font-size:24px;font-weight:500;margin:0;color:#fff}.connection-status.svelte-1hrhory.svelte-1hrhory{display:flex;align-items:center;gap:8px;font-size:14px;padding:6px 12px;background:#0000004d;border-radius:16px}.header-center.svelte-1hrhory.svelte-1hrhory{flex:1;display:flex;justify-content:center}.solar-info.svelte-1hrhory.svelte-1hrhory{display:flex;gap:20px;font-size:14px}.solar-item.svelte-1hrhory.svelte-1hrhory{color:#fffc}.solar-item.svelte-1hrhory .value.svelte-1hrhory{color:var(--accent-teal);font-weight:500;margin-left:4px}.header-right.svelte-1hrhory.svelte-1hrhory{display:flex;gap:20px;align-items:center}.weather-info.svelte-1hrhory.svelte-1hrhory{display:flex;gap:12px;font-size:14px;color:#ffffffe6}.clock.svelte-1hrhory.svelte-1hrhory{display:flex;flex-direction:column;align-items:flex-end}.time.svelte-1hrhory.svelte-1hrhory{font-size:18px;font-weight:500;color:#fff}.date.svelte-1hrhory.svelte-1hrhory{font-size:12px;color:#ffffffb3}main.svelte-1hrhory.svelte-1hrhory{flex:1;padding:24px;overflow-y:auto}.dashboard-grid.svelte-1hrhory.svelte-1hrhory{display:flex;flex-direction:column;gap:24px;max-width:1800px;margin:0 auto}.row.svelte-1hrhory.svelte-1hrhory{display:flex;gap:24px;flex-wrap:wrap}.row.svelte-1hrhory>*{flex:1;min-width:300px}@media (max-width: 1200px){.row.svelte-1hrhory.svelte-1hrhory{flex-direction:column}}@media (max-width: 768px){header.svelte-1hrhory.svelte-1hrhory{flex-direction:column;align-items:flex-start}.header-center.svelte-1hrhory.svelte-1hrhory,.header-right.svelte-1hrhory.svelte-1hrhory{width:100%;justify-content:flex-start}.solar-info.svelte-1hrhory.svelte-1hrhory{flex-wrap:wrap}}:root{--bg-primary: #0a1628;--bg-secondary: #1a2332;--bg-tertiary: #243447;--bg-hover: #2a3f5f;--text-primary: #e0e6ed;--text-secondary: #a0aec0;--text-muted: #718096;--accent-cyan: #4fc3f7;--accent-blue: #2196f3;--accent-green: #4caf50;--accent-orange: #ff9800;--accent-red: #f44336;--accent-purple: #9c27b0;--accent-yellow: #ffc107;--border-color: #2d3748;--border-light: #374151;--card-shadow: 0 1px 3px rgba(0, 0, 0, .3);--card-radius: 6px;--header-height: 56px;--spacing-xs: 4px;--spacing-sm: 8px;--spacing-md: 12px;--spacing-lg: 16px;--spacing-xl: 20px}*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background:var(--bg-primary);color:var(--text-primary);font-size:13px;line-height:1.4;overflow-x:hidden}.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}header{height:var(--header-height);background:var(--bg-secondary);border-bottom:1px solid var(--border-color);display:flex;align-items:center;justify-content:space-between;padding:0 var(--spacing-lg);flex-shrink:0}.header-left{display:flex;align-items:center;gap:var(--spacing-lg)}.header-left h1{font-size:16px;font-weight:600;color:var(--accent-cyan);letter-spacing:.5px}.connection-status{display:flex;align-items:center;gap:var(--spacing-sm);font-size:12px;color:var(--text-secondary)}.status-indicator{width:8px;height:8px;border-radius:50%;background:var(--accent-red);transition:background .3s}.status-indicator.status-online{background:var(--accent-green);box-shadow:0 0 8px var(--accent-green)}.header-center{display:flex;gap:var(--spacing-xl)}.solar-info{display:flex;gap:var(--spacing-md);font-size:12px}.solar-item{color:var(--text-secondary)}.solar-item .value{color:var(--accent-cyan);font-weight:600;margin-left:var(--spacing-xs)}.header-right{display:flex;align-items:center;gap:var(--spacing-lg)}.weather-info{display:flex;gap:var(--spacing-md);font-size:12px;color:var(--text-secondary)}.clock{display:flex;flex-direction:column;align-items:flex-end;font-size:11px}.clock .time{font-size:14px;font-weight:600;color:var(--text-primary)}.clock .date{color:var(--text-secondary)}main{flex:1;overflow-y:auto;padding:var(--spacing-md)}.dashboard-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:var(--spacing-md);max-width:1800px;margin:0 auto}.card{background:var(--bg-secondary);border:1px solid var(--border-color);border-radius:var(--card-radius);padding:var(--spacing-md);box-shadow:var(--card-shadow);transition:border-color .2s}.card:hover{border-color:var(--border-light)}.card h2{font-size:14px;font-weight:600;color:var(--accent-cyan);margin-bottom:var(--spacing-md);display:flex;align-items:center;gap:var(--spacing-sm);letter-spacing:.5px}.card h2:before{content:"";width:3px;height:14px;background:var(--accent-cyan);border-radius:2px}.status-dot{width:8px;height:8px;border-radius:50%;background:var(--accent-green)}.status-dot.disconnected{background:var(--accent-red)}.label{font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:var(--spacing-xs)}.value{font-size:18px;font-weight:300;color:var(--text-primary)}button,.button{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:var(--spacing-sm) var(--spacing-md);font-size:12px;font-weight:500;cursor:pointer;transition:all .2s;text-transform:uppercase;letter-spacing:.5px}button:hover,.button:hover{background:var(--bg-hover);border-color:var(--border-light)}button:active,.button:active{transform:scale(.98)}button.primary{background:var(--accent-cyan);border-color:var(--accent-cyan);color:#000}button.primary:hover{background:#29b6f6;border-color:#29b6f6}button.success{background:var(--accent-green);border-color:var(--accent-green);color:#fff}button.danger{background:var(--accent-red);border-color:var(--accent-red);color:#fff}button:disabled{opacity:.5;cursor:not-allowed}select{background:var(--bg-tertiary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;padding:var(--spacing-sm);font-size:12px;cursor:pointer;outline:none;transition:all .2s}select:hover{border-color:var(--border-light)}select:focus{border-color:var(--accent-cyan)}.badge{display:inline-block;padding:4px 10px;border-radius:12px;font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px}.badge.green{background:#4caf5033;color:var(--accent-green)}.badge.red{background:#f4433633;color:var(--accent-red)}.badge.orange{background:#ff980033;color:var(--accent-orange)}.badge.cyan{background:#4fc3f733;color:var(--accent-cyan)}.badge.purple{background:#9c27b033;color:var(--accent-purple)}.bar{width:100%;height:6px;background:var(--bg-tertiary);border-radius:3px;overflow:hidden;margin:var(--spacing-xs) 0}.bar-fill{height:100%;background:linear-gradient(90deg,var(--accent-green),var(--accent-orange),var(--accent-red));transition:width .3s ease;border-radius:3px}.scale{display:flex;justify-content:space-between;font-size:10px;color:var(--text-muted);margin-top:var(--spacing-xs)}.metrics{display:flex;flex-direction:column;gap:var(--spacing-md)}.metric{display:flex;flex-direction:column;gap:var(--spacing-xs)}.metric-row{display:grid;grid-template-columns:repeat(3,1fr);gap:var(--spacing-md)}.metric.small{min-width:0}.metric-value{display:flex;justify-content:space-between;align-items:baseline;flex-wrap:wrap}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--bg-primary)}::-webkit-scrollbar-thumb{background:var(--bg-tertiary);border-radius:4px}::-webkit-scrollbar-thumb:hover{background:var(--bg-hover)}@media (max-width: 1400px){.dashboard-grid{grid-template-columns:repeat(auto-fit,minmax(250px,1fr))}}@media (max-width: 768px){header{flex-direction:column;height:auto;padding:var(--spacing-sm);gap:var(--spacing-sm)}.dashboard-grid{grid-template-columns:1fr}.header-center{order:3;width:100%}} diff --git a/cmd/server/web/dist/index.html b/cmd/server/web/dist/index.html index 0723fc2..b28a366 100644 --- a/cmd/server/web/dist/index.html +++ b/cmd/server/web/dist/index.html @@ -7,8 +7,8 @@ - - + +
diff --git a/configs/config.example.yaml b/configs/config.example.yaml index 3400a53..91ed156 100644 --- a/configs/config.example.yaml +++ b/configs/config.example.yaml @@ -21,6 +21,10 @@ devices: rotator_genius: host: "10.10.10.121" port: 9006 + + ultrabeam: + host: "10.10.10.124" + port: 4210 weather: openweathermap_api_key: "YOUR_API_KEY_HERE" diff --git a/internal/api/device_manager.go b/internal/api/device_manager.go index fd53fa0..bb518e4 100644 --- a/internal/api/device_manager.go +++ b/internal/api/device_manager.go @@ -10,6 +10,7 @@ import ( "git.rouggy.com/rouggy/ShackMaster/internal/devices/powergenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/rotatorgenius" "git.rouggy.com/rouggy/ShackMaster/internal/devices/tunergenius" + "git.rouggy.com/rouggy/ShackMaster/internal/devices/ultrabeam" "git.rouggy.com/rouggy/ShackMaster/internal/devices/webswitch" "git.rouggy.com/rouggy/ShackMaster/internal/services/solar" "git.rouggy.com/rouggy/ShackMaster/internal/services/weather" @@ -23,6 +24,7 @@ type DeviceManager struct { tunerGenius *tunergenius.Client antennaGenius *antennagenius.Client rotatorGenius *rotatorgenius.Client + ultrabeam *ultrabeam.Client solarClient *solar.Client weatherClient *weather.Client @@ -40,6 +42,7 @@ type SystemStatus struct { TunerGenius *tunergenius.Status `json:"tuner_genius"` AntennaGenius *antennagenius.Status `json:"antenna_genius"` RotatorGenius *rotatorgenius.Status `json:"rotator_genius"` + Ultrabeam *ultrabeam.Status `json:"ultrabeam"` Solar *solar.SolarData `json:"solar"` Weather *weather.WeatherData `json:"weather"` Timestamp time.Time `json:"timestamp"` @@ -85,6 +88,13 @@ func (dm *DeviceManager) Initialize() error { dm.config.Devices.RotatorGenius.Port, ) + // Initialize Ultrabeam + log.Printf("Initializing Ultrabeam: host=%s port=%d", dm.config.Devices.Ultrabeam.Host, dm.config.Devices.Ultrabeam.Port) + dm.ultrabeam = ultrabeam.New( + dm.config.Devices.Ultrabeam.Host, + dm.config.Devices.Ultrabeam.Port, + ) + // Initialize Solar data client dm.solarClient = solar.New() @@ -123,6 +133,15 @@ func (dm *DeviceManager) Initialize() error { }() log.Println("RotatorGenius goroutine launched") + log.Println("About to launch Ultrabeam goroutine...") + go func() { + log.Println("Starting Ultrabeam polling goroutine...") + if err := dm.ultrabeam.Start(); err != nil { + log.Printf("Warning: Failed to start Ultrabeam polling: %v", err) + } + }() + log.Println("Ultrabeam goroutine launched") + log.Println("Device manager initialized") return nil } @@ -150,6 +169,9 @@ func (dm *DeviceManager) Stop() { if dm.rotatorGenius != nil { dm.rotatorGenius.Close() } + if dm.ultrabeam != nil { + dm.ultrabeam.Stop() + } } func (dm *DeviceManager) monitorDevices() { @@ -207,6 +229,13 @@ func (dm *DeviceManager) updateStatus() { log.Printf("Rotator Genius error: %v", err) } + // Ultrabeam + if ubStatus, err := dm.ultrabeam.GetStatus(); err == nil { + status.Ultrabeam = ubStatus + } else { + log.Printf("Ultrabeam error: %v", err) + } + // Solar Data (fetched every 15 minutes, cached) if solarData, err := dm.solarClient.GetSolarData(); err == nil { status.Solar = solarData @@ -265,3 +294,7 @@ func (dm *DeviceManager) AntennaGenius() *antennagenius.Client { func (dm *DeviceManager) RotatorGenius() *rotatorgenius.Client { return dm.rotatorGenius } + +func (dm *DeviceManager) Ultrabeam() *ultrabeam.Client { + return dm.ultrabeam +} diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 9605554..d0bf63a 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -54,6 +54,10 @@ func (s *Server) SetupRoutes() *http.ServeMux { mux.HandleFunc("/api/rotator/ccw", s.handleRotatorCCW) mux.HandleFunc("/api/rotator/stop", s.handleRotatorStop) + // Ultrabeam endpoints + mux.HandleFunc("/api/ultrabeam/frequency", s.handleUltrabeamFrequency) + mux.HandleFunc("/api/ultrabeam/retract", s.handleUltrabeamRetract) + // Tuner endpoints mux.HandleFunc("/api/tuner/operate", s.handleTunerOperate) mux.HandleFunc("/api/tuner/bypass", s.handleTunerBypass) @@ -392,6 +396,45 @@ func (s *Server) handlePowerOperate(w http.ResponseWriter, r *http.Request) { s.sendJSON(w, map[string]string{"status": "ok"}) } +// Ultrabeam handlers +func (s *Server) handleUltrabeamFrequency(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var req struct { + Frequency int `json:"frequency"` // KHz + Direction int `json:"direction"` // 0=normal, 1=180°, 2=bi-dir + } + + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, "Invalid request body", http.StatusBadRequest) + return + } + + if err := s.deviceManager.Ultrabeam().SetFrequency(req.Frequency, req.Direction); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + s.sendJSON(w, map[string]string{"status": "ok"}) +} + +func (s *Server) handleUltrabeamRetract(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + if err := s.deviceManager.Ultrabeam().Retract(); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + s.sendJSON(w, map[string]string{"status": "ok"}) +} + func (s *Server) sendJSON(w http.ResponseWriter, data interface{}) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(data) diff --git a/internal/config/config.go b/internal/config/config.go index fe57a83..e1c2752 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -25,6 +25,7 @@ type DevicesConfig struct { TunerGenius TunerGeniusConfig `yaml:"tuner_genius"` AntennaGenius AntennaGeniusConfig `yaml:"antenna_genius"` RotatorGenius RotatorGeniusConfig `yaml:"rotator_genius"` + Ultrabeam UltrabeamConfig `yaml:"ultrabeam"` } type WebSwitchConfig struct { @@ -51,6 +52,11 @@ type RotatorGeniusConfig struct { Port int `yaml:"port"` } +type UltrabeamConfig struct { + Host string `yaml:"host"` + Port int `yaml:"port"` +} + type WeatherConfig struct { OpenWeatherMapAPIKey string `yaml:"openweathermap_api_key"` LightningEnabled bool `yaml:"lightning_enabled"` diff --git a/internal/devices/ultrabeam/ultrabeam.go b/internal/devices/ultrabeam/ultrabeam.go new file mode 100644 index 0000000..d7f0895 --- /dev/null +++ b/internal/devices/ultrabeam/ultrabeam.go @@ -0,0 +1,457 @@ +package ultrabeam + +import ( + "bufio" + "fmt" + "log" + "net" + "sync" + "time" +) + +// Protocol constants +const ( + STX byte = 0xF5 // 245 decimal + ETX byte = 0xFA // 250 decimal + DLE byte = 0xF6 // 246 decimal +) + +// Command codes +const ( + CMD_STATUS byte = 1 // General status query + CMD_RETRACT byte = 2 // Retract elements + CMD_FREQ byte = 3 // Change frequency + CMD_READ_BANDS byte = 9 // Read current band adjustments + CMD_PROGRESS byte = 10 // Read progress bar + CMD_MODIFY_ELEM byte = 12 // Modify element length +) + +// Reply codes +const ( + UB_OK byte = 0 // Normal execution + UB_BAD byte = 1 // Invalid command + UB_PAR byte = 2 // Bad parameters + UB_ERR byte = 3 // Error executing command +) + +// Direction modes +const ( + DIR_NORMAL byte = 0 + DIR_180 byte = 1 + DIR_BIDIR byte = 2 +) + +type Client struct { + host string + port int + conn net.Conn + connMu sync.Mutex + reader *bufio.Reader + lastStatus *Status + statusMu sync.RWMutex + stopChan chan struct{} + running bool + seqNum byte + seqMu sync.Mutex +} + +type Status struct { + FirmwareMinor int `json:"firmware_minor"` + FirmwareMajor int `json:"firmware_major"` + CurrentOperation int `json:"current_operation"` + Frequency int `json:"frequency"` // KHz + Band int `json:"band"` + Direction int `json:"direction"` // 0=normal, 1=180°, 2=bi-dir + OffState bool `json:"off_state"` + MotorsMoving int `json:"motors_moving"` // Bitmask + FreqMin int `json:"freq_min"` // MHz + FreqMax int `json:"freq_max"` // MHz + ElementLengths []int `json:"element_lengths"` // mm + ProgressTotal int `json:"progress_total"` // mm + ProgressCurrent int `json:"progress_current"` // 0-60 + Connected bool `json:"connected"` +} + +func New(host string, port int) *Client { + return &Client{ + host: host, + port: port, + stopChan: make(chan struct{}), + seqNum: 0, + } +} + +func (c *Client) Start() error { + c.running = true + go c.pollLoop() + return nil +} + +func (c *Client) Stop() { + if !c.running { + return + } + c.running = false + close(c.stopChan) + + c.connMu.Lock() + if c.conn != nil { + c.conn.Close() + c.conn = nil + } + c.connMu.Unlock() +} + +func (c *Client) pollLoop() { + ticker := time.NewTicker(500 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + // Try to connect if not connected + c.connMu.Lock() + if c.conn == nil { + conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second) + if err != nil { + c.connMu.Unlock() + + // Mark as disconnected + c.statusMu.Lock() + c.lastStatus = &Status{Connected: false} + c.statusMu.Unlock() + continue + } + c.conn = conn + c.reader = bufio.NewReader(c.conn) + log.Printf("Ultrabeam: Connected to %s:%d", c.host, c.port) + } + c.connMu.Unlock() + + // Query status + status, err := c.queryStatus() + if err != nil { + log.Printf("Ultrabeam: Failed to query status: %v", err) + // Close connection and retry + c.connMu.Lock() + if c.conn != nil { + c.conn.Close() + c.conn = nil + c.reader = nil + } + c.connMu.Unlock() + + // Mark as disconnected + c.statusMu.Lock() + c.lastStatus = &Status{Connected: false} + c.statusMu.Unlock() + continue + } + + // Mark as connected + status.Connected = true + + // Query element lengths + lengths, err := c.queryElementLengths() + if err == nil { + status.ElementLengths = lengths + } + + // Query progress if motors moving + if status.MotorsMoving != 0 { + progress, err := c.queryProgress() + if err == nil { + status.ProgressTotal = progress[0] + status.ProgressCurrent = progress[1] + } + } + + c.statusMu.Lock() + c.lastStatus = status + c.statusMu.Unlock() + + case <-c.stopChan: + return + } + } +} + +func (c *Client) GetStatus() (*Status, error) { + c.statusMu.RLock() + defer c.statusMu.RUnlock() + + if c.lastStatus == nil { + return &Status{Connected: false}, nil + } + + return c.lastStatus, nil +} + +// getNextSeq returns the next sequence number +func (c *Client) getNextSeq() byte { + c.seqMu.Lock() + defer c.seqMu.Unlock() + + seq := c.seqNum + c.seqNum = (c.seqNum + 1) % 128 + return seq +} + +// calculateChecksum calculates the checksum for a packet +func calculateChecksum(data []byte) byte { + chk := byte(0x55) + for _, b := range data { + chk ^= b + chk++ + } + return chk +} + +// quoteByte handles DLE escaping +func quoteByte(b byte) []byte { + if b == STX || b == ETX || b == DLE { + return []byte{DLE, b & 0x7F} // Clear MSB + } + return []byte{b} +} + +// buildPacket creates a complete packet with checksum and escaping +func (c *Client) buildPacket(cmd byte, data []byte) []byte { + seq := c.getNextSeq() + + // Calculate checksum on unquoted data + payload := append([]byte{seq, cmd}, data...) + chk := calculateChecksum(payload) + + // Build packet with quoting + packet := []byte{STX} + + // Add quoted SEQ + packet = append(packet, quoteByte(seq)...) + + // Add quoted CMD + packet = append(packet, quoteByte(cmd)...) + + // Add quoted data + for _, b := range data { + packet = append(packet, quoteByte(b)...) + } + + // Add quoted checksum + packet = append(packet, quoteByte(chk)...) + + // Add ETX + packet = append(packet, ETX) + + return packet +} + +// parsePacket parses a received packet, handling DLE unescaping +func parsePacket(data []byte) (seq byte, cmd byte, payload []byte, err error) { + if len(data) < 5 { // STX + SEQ + CMD + CHK + ETX + return 0, 0, nil, fmt.Errorf("packet too short") + } + + if data[0] != STX { + return 0, 0, nil, fmt.Errorf("missing STX") + } + + if data[len(data)-1] != ETX { + return 0, 0, nil, fmt.Errorf("missing ETX") + } + + // Unquote the data + var unquoted []byte + dle := false + for i := 1; i < len(data)-1; i++ { + b := data[i] + if b == DLE { + dle = true + continue + } + if dle { + b |= 0x80 // Set MSB + dle = false + } + unquoted = append(unquoted, b) + } + + if len(unquoted) < 3 { + return 0, 0, nil, fmt.Errorf("unquoted packet too short") + } + + seq = unquoted[0] + cmd = unquoted[1] + chk := unquoted[len(unquoted)-1] + payload = unquoted[2 : len(unquoted)-1] + + // Verify checksum + calcChk := calculateChecksum(unquoted[:len(unquoted)-1]) + if calcChk != chk { + return 0, 0, nil, fmt.Errorf("checksum mismatch: got %02X, expected %02X", chk, calcChk) + } + + return seq, cmd, payload, nil +} + +// sendCommand sends a command and waits for reply +func (c *Client) sendCommand(cmd byte, data []byte) ([]byte, error) { + c.connMu.Lock() + defer c.connMu.Unlock() + + if c.conn == nil || c.reader == nil { + return nil, fmt.Errorf("not connected") + } + + // Build and send packet + packet := c.buildPacket(cmd, data) + + _, err := c.conn.Write(packet) + if err != nil { + return nil, fmt.Errorf("failed to write: %w", err) + } + + // Read reply with timeout + c.conn.SetReadDeadline(time.Now().Add(2 * time.Second)) + + // Read until we get a complete packet + var buffer []byte + for { + b, err := c.reader.ReadByte() + if err != nil { + return nil, fmt.Errorf("failed to read: %w", err) + } + + buffer = append(buffer, b) + + // Check if we have a complete packet + if b == ETX && len(buffer) > 0 && buffer[0] == STX { + break + } + + // Prevent infinite loop + if len(buffer) > 256 { + return nil, fmt.Errorf("packet too long") + } + } + + // Parse reply + _, replyCmd, payload, err := parsePacket(buffer) + if err != nil { + return nil, fmt.Errorf("failed to parse reply: %w", err) + } + + // Check for errors + switch replyCmd { + case UB_BAD: + return nil, fmt.Errorf("invalid command") + case UB_PAR: + return nil, fmt.Errorf("bad parameters") + case UB_ERR: + return nil, fmt.Errorf("execution error") + case UB_OK: + return payload, nil + default: + return nil, fmt.Errorf("unknown reply code: %d", replyCmd) + } +} + +// queryStatus queries general status (command 1) +func (c *Client) queryStatus() (*Status, error) { + reply, err := c.sendCommand(CMD_STATUS, nil) + if err != nil { + return nil, err + } + + if len(reply) < 12 { + return nil, fmt.Errorf("status reply too short: %d bytes", len(reply)) + } + + status := &Status{ + FirmwareMinor: int(reply[0]), + FirmwareMajor: int(reply[1]), + CurrentOperation: int(reply[2]), + Frequency: int(reply[3]) | (int(reply[4]) << 8), + Band: int(reply[5]), + Direction: int(reply[6] & 0x0F), + OffState: (reply[7] & 0x02) != 0, + MotorsMoving: int(reply[9]), + FreqMin: int(reply[10]), + FreqMax: int(reply[11]), + } + + return status, nil +} + +// queryElementLengths queries element lengths (command 9) +func (c *Client) queryElementLengths() ([]int, error) { + reply, err := c.sendCommand(CMD_READ_BANDS, nil) + if err != nil { + return nil, err + } + + if len(reply) < 12 { + return nil, fmt.Errorf("element lengths reply too short") + } + + lengths := make([]int, 6) + for i := 0; i < 6; i++ { + lo := int(reply[i*2]) + hi := int(reply[i*2+1]) + lengths[i] = lo | (hi << 8) + } + + return lengths, nil +} + +// queryProgress queries motor progress (command 10) +func (c *Client) queryProgress() ([]int, error) { + reply, err := c.sendCommand(CMD_PROGRESS, nil) + if err != nil { + return nil, err + } + + if len(reply) < 4 { + return nil, fmt.Errorf("progress reply too short") + } + + total := int(reply[0]) | (int(reply[1]) << 8) + current := int(reply[2]) | (int(reply[3]) << 8) + + return []int{total, current}, nil +} + +// SetFrequency changes frequency and optional direction (command 3) +func (c *Client) SetFrequency(freqKhz int, direction int) error { + data := []byte{ + byte(freqKhz & 0xFF), + byte((freqKhz >> 8) & 0xFF), + byte(direction), + } + + _, err := c.sendCommand(CMD_FREQ, data) + return err +} + +// Retract retracts all elements (command 2) +func (c *Client) Retract() error { + _, err := c.sendCommand(CMD_RETRACT, nil) + return err +} + +// ModifyElement modifies element length (command 12) +func (c *Client) ModifyElement(elementNum int, lengthMm int) error { + if elementNum < 0 || elementNum > 5 { + return fmt.Errorf("invalid element number: %d", elementNum) + } + + data := []byte{ + byte(elementNum), + 0, // Reserved + byte(lengthMm & 0xFF), + byte((lengthMm >> 8) & 0xFF), + } + + _, err := c.sendCommand(CMD_MODIFY_ELEM, data) + return err +} diff --git a/web/src/App.svelte b/web/src/App.svelte index c6df217..d519b09 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -7,6 +7,7 @@ import TunerGenius from './components/TunerGenius.svelte'; import AntennaGenius from './components/AntennaGenius.svelte'; import RotatorGenius from './components/RotatorGenius.svelte'; + import Ultrabeam from './components/Ultrabeam.svelte'; let status = null; let isConnected = false; @@ -118,6 +119,10 @@ + +
+ +
diff --git a/web/src/components/Ultrabeam.svelte b/web/src/components/Ultrabeam.svelte new file mode 100644 index 0000000..75d4a93 --- /dev/null +++ b/web/src/components/Ultrabeam.svelte @@ -0,0 +1,547 @@ + + +
+
+

Ultrabeam VL2.3

+ +
+ +
+ +
+
+
Frequency
+
{(frequency / 1000).toFixed(3)} MHz
+
+ +
+
Band
+
{bandNames[band] || 'Unknown'}
+
+ +
+
Direction
+
{directionNames[direction]}
+
+ +
+
Firmware
+
v{firmwareVersion}
+
+
+ + +
+

Frequency Control

+
+
+ + +
+ +
+ + +
+ + +
+
+ + + {#if motorsMoving > 0} +
+

Motors Moving...

+
+
+
+
{progressCurrent} / 60 ({progressPercent.toFixed(0)}%)
+
+ {/if} + + +
+

Element Lengths (mm)

+
+ {#each elementLengths as length, i} + {#if length > 0} +
+
Element {i + 1}
+
{length} mm
+
+ {/if} + {/each} +
+
+ + +
+
+

Calibration

+ +
+ + {#if calibrationMode} +
+
+ + +
+ +
+ + +
+ + + +

+ ⚠️ Calibration changes are saved after 12 seconds. Do not turn off during this time. +

+
+ {/if} +
+ + +
+ +
+
+
+ + \ No newline at end of file diff --git a/web/src/lib/api.js b/web/src/lib/api.js index 3bdcde9..21c971b 100644 --- a/web/src/lib/api.js +++ b/web/src/lib/api.js @@ -89,4 +89,13 @@ export const api = { rotateCCW: () => request('/rotator/ccw', { method: 'POST' }), stop: () => request('/rotator/stop', { method: 'POST' }), }, + + // Ultrabeam + ultrabeam: { + setFrequency: (frequency, direction) => request('/ultrabeam/frequency', { + method: 'POST', + body: JSON.stringify({ frequency, direction }), + }), + retract: () => request('/ultrabeam/retract', { method: 'POST' }), + }, }; \ No newline at end of file