280 lines
13 KiB
HTML
280 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
|
<title>ID-5100 · CI-V Remote</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Saira+Condensed:wght@400;600;800&family=Chivo+Mono:wght@400;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
@font-face{
|
|
font-family:'DSEG7';
|
|
src:url('https://cdn.jsdelivr.net/npm/dseg@0.46.0/fonts/DSEG7-Classic/DSEG7Classic-Bold.woff2') format('woff2');
|
|
font-weight:700;font-display:swap;
|
|
}
|
|
:root{
|
|
--bg:#0b0a08; --metal-hi:#34302a; --metal-lo:#17150f; --groove:#0c0b08;
|
|
--amber:#ffb22e; --amber-soft:#c98a2a; --amber-glow:rgba(255,178,46,.55);
|
|
--accent:#d35400; --accent-hi:#ff7a1a;
|
|
--led-on:#54d171; --led-off:#243126; --red:#ff5436;
|
|
--ink:#cdbfa8; --ink-dim:#7c715c; --glass:#0a0d0a;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
html,body{height:100%}
|
|
body{
|
|
background:
|
|
radial-gradient(120% 90% at 50% -10%, #211d16 0%, var(--bg) 55%, #050403 100%);
|
|
font-family:'Chivo Mono',monospace; color:var(--ink);
|
|
min-height:100%; display:grid; place-items:center; padding:26px 14px;
|
|
-webkit-tap-highlight-color:transparent;
|
|
}
|
|
body::after{ /* grain */
|
|
content:""; position:fixed; inset:0; pointer-events:none; opacity:.05; mix-blend-mode:overlay;
|
|
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
.unit{
|
|
width:min(680px,100%);
|
|
background:linear-gradient(168deg,var(--metal-hi),var(--metal-lo) 60%,#100e09);
|
|
border-radius:16px; padding:22px;
|
|
border:1px solid #3c372e;
|
|
box-shadow:
|
|
0 1px 0 rgba(255,225,170,.10) inset,
|
|
0 -22px 50px rgba(0,0,0,.55) inset,
|
|
0 30px 70px rgba(0,0,0,.65),
|
|
0 2px 0 #000;
|
|
position:relative;
|
|
animation:power 1s cubic-bezier(.2,.8,.2,1) both;
|
|
}
|
|
@keyframes power{from{opacity:0;transform:translateY(14px) scale(.985);filter:brightness(.4)}to{opacity:1;transform:none;filter:none}}
|
|
.unit::before,.unit::after{ /* corner screws */
|
|
content:""; position:absolute; width:11px; height:11px; border-radius:50%;
|
|
background:radial-gradient(circle at 35% 30%,#5b5347,#181610 70%);
|
|
box-shadow:0 1px 1px rgba(0,0,0,.8), 0 0 0 1px #0c0b08;
|
|
}
|
|
.unit::before{top:12px;left:12px} .unit::after{top:12px;right:12px}
|
|
|
|
.topbar{display:flex;align-items:center;justify-content:center;gap:14px;
|
|
position:relative; padding:2px 26px 14px; }
|
|
.brand{font-family:'Saira Condensed',sans-serif;font-weight:800;
|
|
letter-spacing:.26em; font-size:15px; color:#e8dcc4; text-transform:uppercase}
|
|
.brand b{color:var(--accent-hi)}
|
|
.link{position:absolute;right:26px;top:0;display:flex;align-items:center;gap:7px;
|
|
font-size:10px;letter-spacing:.18em;color:var(--ink-dim);text-transform:uppercase}
|
|
.dot{width:9px;height:9px;border-radius:50%;background:var(--led-off);
|
|
box-shadow:0 0 0 1px #000 inset; transition:.3s}
|
|
.dot.on{background:var(--led-on);box-shadow:0 0 9px var(--led-on),0 0 0 1px #0a3 inset}
|
|
.dot.off{background:var(--red);box-shadow:0 0 9px var(--red)}
|
|
|
|
/* ---- VFD display ---- */
|
|
.display{
|
|
margin:6px 4px 16px; padding:18px 22px; border-radius:10px;
|
|
background:
|
|
linear-gradient(180deg,#060806,#0c100c) padding-box,
|
|
var(--glass);
|
|
border:1px solid #000;
|
|
box-shadow:0 0 0 1px #2c2820, 0 14px 26px rgba(0,0,0,.6) inset, 0 2px 0 #2a261e;
|
|
position:relative; overflow:hidden;
|
|
}
|
|
.display::after{ /* scanlines */
|
|
content:"";position:absolute;inset:0;pointer-events:none;opacity:.35;
|
|
background:repeating-linear-gradient(180deg,transparent 0 2px,rgba(0,0,0,.5) 2px 3px);
|
|
}
|
|
.row1{display:flex;align-items:baseline;justify-content:space-between;gap:12px}
|
|
.freq{
|
|
font-family:'DSEG7','Chivo Mono',monospace; font-weight:700;
|
|
font-size:clamp(38px,11vw,68px); line-height:1; color:var(--amber);
|
|
text-shadow:0 0 6px var(--amber-glow),0 0 22px rgba(255,150,20,.35);
|
|
letter-spacing:2px; animation:flick 6s infinite steps(1);
|
|
}
|
|
@keyframes flick{0%,97%,100%{opacity:1}98%{opacity:.86}}
|
|
.unitlbl{font-family:'Saira Condensed',sans-serif;font-weight:600;letter-spacing:.2em;
|
|
color:var(--amber-soft);font-size:15px;text-shadow:0 0 8px var(--amber-glow)}
|
|
.row2{display:flex;align-items:center;justify-content:space-between;margin-top:12px}
|
|
.modebadge{font-family:'Saira Condensed',sans-serif;font-weight:800;letter-spacing:.18em;
|
|
font-size:20px;color:var(--accent-hi);text-shadow:0 0 10px rgba(255,122,26,.5);min-width:54px}
|
|
.busy{display:flex;align-items:center;gap:8px;font-size:11px;letter-spacing:.18em;
|
|
color:var(--ink-dim);text-transform:uppercase}
|
|
.busy .dot.on{background:var(--accent-hi);box-shadow:0 0 10px var(--accent-hi)}
|
|
|
|
/* ---- S meter ---- */
|
|
.meter{margin:4px 4px 18px;padding:0 2px}
|
|
.bars{display:flex;gap:3px;height:26px;align-items:stretch}
|
|
.seg{flex:1;border-radius:2px;background:var(--led-off);
|
|
box-shadow:0 0 0 1px #000 inset; transition:background .12s, box-shadow .12s}
|
|
.seg.on{background:var(--amber);box-shadow:0 0 7px var(--amber-glow),0 0 0 1px #000 inset}
|
|
.seg.on.hot{background:var(--red);box-shadow:0 0 8px var(--red),0 0 0 1px #000 inset}
|
|
.scale{display:flex;justify-content:space-between;margin-top:6px;
|
|
font-size:9.5px;letter-spacing:.08em;color:var(--ink-dim)}
|
|
|
|
/* ---- controls ---- */
|
|
.panel{display:grid;gap:16px}
|
|
.card{background:linear-gradient(180deg,#221f19,#16140f);border:1px solid #332e26;
|
|
border-radius:10px;padding:14px 14px 16px;
|
|
box-shadow:0 1px 0 rgba(255,220,160,.06) inset,0 6px 16px rgba(0,0,0,.4)}
|
|
.label{font-family:'Saira Condensed',sans-serif;font-weight:600;letter-spacing:.22em;
|
|
text-transform:uppercase;font-size:11px;color:var(--ink-dim);margin-bottom:11px}
|
|
.freqset{display:flex;gap:8px;flex-wrap:wrap}
|
|
input[type=number]{flex:1;min-width:120px;background:#0a0907;border:1px solid #3a352b;
|
|
color:var(--amber);font-family:'Chivo Mono';font-size:18px;padding:9px 12px;border-radius:7px;
|
|
outline:none}
|
|
input[type=number]:focus{border-color:var(--accent);box-shadow:0 0 0 2px rgba(211,84,0,.25)}
|
|
button{font-family:'Saira Condensed',sans-serif;font-weight:800;letter-spacing:.1em;
|
|
text-transform:uppercase;cursor:pointer;border:none;color:#1a160f;
|
|
background:linear-gradient(180deg,var(--accent-hi),var(--accent));border-radius:7px;
|
|
padding:9px 16px;font-size:14px;box-shadow:0 2px 0 #5a2400,0 6px 12px rgba(0,0,0,.4);
|
|
transition:transform .06s, box-shadow .06s}
|
|
button:active{transform:translateY(2px);box-shadow:0 0 0 #5a2400}
|
|
.chips{display:flex;gap:7px;flex-wrap:wrap;margin-top:10px}
|
|
.chip{background:#0c0b08;border:1px solid #3a352b;color:var(--ink);
|
|
padding:7px 11px;border-radius:20px;font-size:12px;box-shadow:none}
|
|
.chip:active{transform:translateY(1px)}
|
|
.modes{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}
|
|
.mode{padding:13px 0;border-radius:8px;background:#0d0c09;border:1px solid #34302a;
|
|
color:var(--ink-dim);font-size:16px;letter-spacing:.16em;box-shadow:0 2px 0 #000 inset}
|
|
.mode.active{color:#1a160f;background:linear-gradient(180deg,var(--amber),var(--amber-soft));
|
|
border-color:#000;box-shadow:0 0 16px var(--amber-glow),0 0 0 1px #000}
|
|
.fader{display:flex;align-items:center;gap:13px}
|
|
.fader .val{font-family:'DSEG7','Chivo Mono';color:var(--amber);font-size:20px;min-width:54px;
|
|
text-align:right;text-shadow:0 0 7px var(--amber-glow)}
|
|
input[type=range]{-webkit-appearance:none;appearance:none;flex:1;height:8px;border-radius:6px;
|
|
background:linear-gradient(90deg,var(--accent) 0%,var(--accent) var(--p,0%),#0c0b08 var(--p,0%),#0c0b08 100%);
|
|
border:1px solid #000;outline:none}
|
|
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:22px;height:26px;border-radius:5px;
|
|
background:linear-gradient(180deg,#5b5346,#211d16);border:1px solid #000;cursor:grab;
|
|
box-shadow:0 2px 5px rgba(0,0,0,.6),0 1px 0 rgba(255,230,180,.25) inset}
|
|
input[type=range]::-moz-range-thumb{width:22px;height:26px;border-radius:5px;
|
|
background:linear-gradient(180deg,#5b5346,#211d16);border:1px solid #000;cursor:grab}
|
|
.stack{display:grid;gap:13px}
|
|
.err{color:var(--red);font-size:11px;letter-spacing:.05em;min-height:14px;text-align:center;
|
|
text-transform:uppercase}
|
|
.reveal{animation:rise .7s both}
|
|
@keyframes rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="unit">
|
|
<div class="topbar">
|
|
<div class="brand">ICOM <b>ID-5100</b></div>
|
|
<div class="link"><span id="led" class="dot off"></span><span id="linktxt">link…</span></div>
|
|
</div>
|
|
|
|
<div class="display reveal" style="animation-delay:.05s">
|
|
<div class="row1">
|
|
<div id="freq" class="freq">---.-----</div>
|
|
<div class="unitlbl">MHz</div>
|
|
</div>
|
|
<div class="row2">
|
|
<div id="mode" class="modebadge">--</div>
|
|
<div class="busy"><span id="busyled" class="dot"></span><span id="busytxt">SQ CLOSED</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="meter reveal" style="animation-delay:.12s">
|
|
<div id="bars" class="bars"></div>
|
|
<div class="scale"><span>S1</span><span>3</span><span>5</span><span>7</span><span>9</span><span>+20</span><span>+40</span><span>+60</span></div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="card reveal" style="animation-delay:.19s">
|
|
<div class="label">Frequency · MHz</div>
|
|
<div class="freqset">
|
|
<input id="finp" type="number" step="0.005" inputmode="decimal" placeholder="146.520">
|
|
<button id="fset">Set</button>
|
|
</div>
|
|
<div class="chips" id="chips"></div>
|
|
</div>
|
|
|
|
<div class="card reveal" style="animation-delay:.26s">
|
|
<div class="label">Mode</div>
|
|
<div class="modes" id="modes"></div>
|
|
</div>
|
|
|
|
<div class="card reveal" style="animation-delay:.33s">
|
|
<div class="label">Levels</div>
|
|
<div class="stack">
|
|
<div class="fader"><input id="vol" type="range" min="0" max="255"><span id="volv" class="val">--</span></div>
|
|
<div class="fader"><input id="sql" type="range" min="0" max="255"><span id="sqlv" class="val">--</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div id="err" class="err"></div>
|
|
</div>
|
|
|
|
<script>
|
|
const $=s=>document.querySelector(s);
|
|
const SEGS=32, MODES=['FM','AM','DV'];
|
|
const CHIPS=[['2m call','146.520'],['70cm call','446.000'],['Nat. simplex','147.300'],['70cm','446.500']];
|
|
|
|
// build segments
|
|
const bars=$('#bars');
|
|
for(let i=0;i<SEGS;i++){const d=document.createElement('div');d.className='seg';bars.appendChild(d);}
|
|
// build mode buttons
|
|
const modesEl=$('#modes');
|
|
MODES.forEach(m=>{const b=document.createElement('div');b.className='mode';b.textContent=m;
|
|
b.onclick=()=>post('/api/mode',{mode:m});modesEl.appendChild(b);});
|
|
// build chips
|
|
const chipsEl=$('#chips');
|
|
CHIPS.forEach(([lbl,mhz])=>{const c=document.createElement('button');c.className='chip';
|
|
c.textContent=lbl+' · '+mhz;c.onclick=()=>post('/api/freq',{mhz:+mhz});chipsEl.appendChild(c);});
|
|
|
|
let dragging=null;
|
|
function err(m){$('#err').textContent=m||'';}
|
|
|
|
async function post(path,body){
|
|
try{const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)});
|
|
const j=await r.json(); if(!j.ok){err(j.error||'rejected');} else {err('');} refresh();
|
|
}catch(e){err('send failed');}
|
|
}
|
|
|
|
function fmtFreq(mhz){return Number(mhz).toFixed(5);}
|
|
|
|
function render(s){
|
|
$('#led').className='dot on'; $('#linktxt').textContent='online';
|
|
$('#freq').textContent=fmtFreq(s.freq_mhz);
|
|
$('#mode').textContent=s.mode;
|
|
// s-meter
|
|
const lit=Math.round(s.smeter/255*SEGS);
|
|
bars.querySelectorAll('.seg').forEach((seg,i)=>{
|
|
const on=i<lit; seg.className='seg'+(on?' on':'')+(on&&i>=SEGS*0.72?' hot':'');});
|
|
// squelch
|
|
const busy=s.squelch_open;
|
|
$('#busyled').className='dot'+(busy?' on':'');
|
|
$('#busytxt').innerHTML=busy?'SQ OPEN':'SQ CLOSED';
|
|
// modes active
|
|
modesEl.querySelectorAll('.mode').forEach(b=>b.classList.toggle('active',b.textContent===s.mode));
|
|
// faders (don't fight the user mid-drag)
|
|
if(dragging!=='vol'){const v=$('#vol');v.value=s.volume;v.style.setProperty('--p',(s.volume/255*100)+'%');$('#volv').textContent=s.volume;}
|
|
if(dragging!=='sql'){const v=$('#sql');v.value=s.squelch_level;v.style.setProperty('--p',(s.squelch_level/255*100)+'%');$('#sqlv').textContent=s.squelch_level;}
|
|
}
|
|
|
|
function offline(msg){$('#led').className='dot off';$('#linktxt').textContent='offline';err(msg||'no radio');}
|
|
|
|
async function refresh(){
|
|
try{const r=await fetch('/api/status');const s=await r.json();
|
|
if(s.ok){render(s);} else {offline(s.error);}
|
|
}catch(e){offline('server unreachable');}
|
|
}
|
|
|
|
// fader wiring with debounce
|
|
function wireFader(id,path,valEl){
|
|
const el=$('#'+id);
|
|
el.addEventListener('input',()=>{dragging=id;el.style.setProperty('--p',(el.value/255*100)+'%');$(valEl).textContent=el.value;});
|
|
let t;
|
|
const commit=()=>{clearTimeout(t);t=setTimeout(()=>{post(path,{value:+el.value});dragging=null;},120);};
|
|
el.addEventListener('change',commit);
|
|
el.addEventListener('pointerup',commit);
|
|
}
|
|
wireFader('vol','/api/vol','#volv');
|
|
wireFader('sql','/api/sql','#sqlv');
|
|
|
|
$('#fset').onclick=()=>{const v=parseFloat($('#finp').value);if(!isNaN(v))post('/api/freq',{mhz:v});};
|
|
$('#finp').addEventListener('keydown',e=>{if(e.key==='Enter')$('#fset').click();});
|
|
|
|
refresh();
|
|
setInterval(refresh,900);
|
|
</script>
|
|
</body>
|
|
</html>
|