|
|
@@ -312,6 +312,7 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 |
|
|
<div class="k">RDS Inj.</div><div class="v" id="info-rdsinj">--</div> |
|
|
<div class="k">RDS Inj.</div><div class="v" id="info-rdsinj">--</div> |
|
|
<div class="k">MPX Gain</div><div class="v" id="info-mpxgain">--</div> |
|
|
<div class="k">MPX Gain</div><div class="v" id="info-mpxgain">--</div> |
|
|
<div class="k">BS.412</div><div class="v" id="info-bs412">--</div> |
|
|
<div class="k">BS.412</div><div class="v" id="info-bs412">--</div> |
|
|
|
|
|
<div class="k">Comp. Clip</div><div class="v" id="info-compclip">--</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
@@ -425,6 +426,31 @@ input.input-error{border-color:var(--red);box-shadow:0 0 0 3px rgba(176,48,48,.1 |
|
|
<div class="section-note reset-hint">Compliance changes are persisted to config and require TX restart before they affect the modulation chain.</div> |
|
|
<div class="section-note reset-hint">Compliance changes are persisted to config and require TX restart before they affect the modulation chain.</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<!-- Composite Clipper --> |
|
|
|
|
|
<div class="card panel" data-panel-key="compclip"> |
|
|
|
|
|
<div class="panel-head" data-panel><h2>Composite Clipper</h2><div class="meta" id="compclip-meta">SM.1268</div><span class="chevron">▼</span></div> |
|
|
|
|
|
<div class="panel-body"> |
|
|
|
|
|
<div class="section-note">ITU-R SM.1268 iterative composite clipper. Enable/disable is live. Iterations, knee, and look-ahead require TX restart.</div> |
|
|
|
|
|
<div class="toggle-row"> |
|
|
|
|
|
<div class="toggle-copy"><div class="title">Composite Clipper</div><div class="sub">Iterative clip-filter-clip + look-ahead <span class="tag tag-live">live</span></div></div> |
|
|
|
|
|
<div class="toggle-ctl"><div class="toggle" id="tog-compclip" data-toggle="compositeClipperEnabled" role="switch" aria-checked="false" tabindex="0"></div><div class="toggle-state" id="compclip-label">--</div></div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="ctrl-row"> |
|
|
|
|
|
<div class="ctrl-label-wrap"><span class="ctrl-label">Iterations</span><span class="ctrl-sub">clip-filter passes (1-5)</span></div> |
|
|
|
|
|
<div class="ctrl-input"><input type="range" min="1" max="5" step="1" id="compclip-iter-slider"><span class="val-display" id="compclip-iter-val">--</span><span class="tag tag-restart">restart</span></div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="ctrl-row"> |
|
|
|
|
|
<div class="ctrl-label-wrap"><span class="ctrl-label">Soft Knee</span><span class="ctrl-sub">0 = hard clip, 0.3 = gentle</span></div> |
|
|
|
|
|
<div class="ctrl-input"><input type="range" min="0" max="0.5" step="0.01" id="compclip-knee-slider"><span class="val-display" id="compclip-knee-val">--</span><span class="tag tag-restart">restart</span></div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="ctrl-row"> |
|
|
|
|
|
<div class="ctrl-label-wrap"><span class="ctrl-label">Look-ahead</span><span class="ctrl-sub">0 = off, 1.0 ms = typical</span></div> |
|
|
|
|
|
<div class="ctrl-input"><input type="range" min="0" max="3" step="0.1" id="compclip-la-slider"><span class="val-display" id="compclip-la-val">--</span><span class="unit-label">ms</span><span class="tag tag-restart">restart</span></div> |
|
|
|
|
|
</div> |
|
|
|
|
|
<div class="actions-row"><button class="apply-btn" id="compclip-apply" type="button">Save Clipper Settings</button><button class="apply-btn secondary" id="compclip-reset" type="button">Reset Draft</button></div> |
|
|
|
|
|
<div class="section-note reset-hint">Structural clipper changes (iterations, knee, look-ahead) are persisted to config and require TX restart.</div> |
|
|
|
|
|
</div> |
|
|
|
|
|
</div> |
|
|
<!-- Danger --> |
|
|
<!-- Danger --> |
|
|
<div class="card panel" data-panel-key="danger"> |
|
|
<div class="card panel" data-panel-key="danger"> |
|
|
<div class="panel-head" data-panel><h2>Danger Zone</h2><div class="meta">emergency</div><span class="chevron">▼</span></div> |
|
|
<div class="panel-head" data-panel><h2>Danger Zone</h2><div class="meta">emergency</div><span class="chevron">▼</span></div> |
|
|
@@ -725,8 +751,12 @@ const CFG={ |
|
|
toneLeftHz: {sec:'tones', live:true, path:'audio.toneLeftHz', min:0, max:20000,step:10}, |
|
|
toneLeftHz: {sec:'tones', live:true, path:'audio.toneLeftHz', min:0, max:20000,step:10}, |
|
|
toneRightHz: {sec:'tones', live:true, path:'audio.toneRightHz', min:0, max:20000,step:10}, |
|
|
toneRightHz: {sec:'tones', live:true, path:'audio.toneRightHz', min:0, max:20000,step:10}, |
|
|
toneAmplitude: {sec:'tones', live:true, path:'audio.toneAmplitude', min:0, max:1, step:.01}, |
|
|
toneAmplitude: {sec:'tones', live:true, path:'audio.toneAmplitude', min:0, max:1, step:.01}, |
|
|
|
|
|
compositeClipperEnabled: {sec:'compclip',live:true, path:'fm.compositeClipper.enabled'}, |
|
|
|
|
|
compositeClipperIterations: {sec:'compclip',live:false,path:'fm.compositeClipper.iterations',min:1,max:5,step:1}, |
|
|
|
|
|
compositeClipperSoftKnee: {sec:'compclip',live:false,path:'fm.compositeClipper.softKnee',min:0,max:.5,step:.01}, |
|
|
|
|
|
compositeClipperLookaheadMs:{sec:'compclip',live:false,path:'fm.compositeClipper.lookaheadMs',min:0,max:3,step:.1}, |
|
|
}; |
|
|
}; |
|
|
const CFG_PATCH_KEY={outputDrive:'outputDrive',limiterCeiling:'limiterCeiling',preEmphasisTauUS:'preEmphasisTauUS',audioGain:'audioGain',pilotLevel:'pilotLevel',rdsInjection:'rdsInjection',pi:'pi',pty:'pty',bs412Enabled:'bs412Enabled',bs412ThresholdDBr:'bs412ThresholdDBr',mpxGain:'mpxGain',toneLeftHz:'toneLeftHz',toneRightHz:'toneRightHz',toneAmplitude:'toneAmplitude'}; |
|
|
|
|
|
|
|
|
const CFG_PATCH_KEY={outputDrive:'outputDrive',limiterCeiling:'limiterCeiling',preEmphasisTauUS:'preEmphasisTauUS',audioGain:'audioGain',pilotLevel:'pilotLevel',rdsInjection:'rdsInjection',pi:'pi',pty:'pty',bs412Enabled:'bs412Enabled',bs412ThresholdDBr:'bs412ThresholdDBr',mpxGain:'mpxGain',toneLeftHz:'toneLeftHz',toneRightHz:'toneRightHz',toneAmplitude:'toneAmplitude',compositeClipperEnabled:'compositeClipperEnabled',compositeClipperIterations:'compositeClipperIterations',compositeClipperSoftKnee:'compositeClipperSoftKnee',compositeClipperLookaheadMs:'compositeClipperLookaheadMs'}; |
|
|
|
|
|
|
|
|
// ── Helpers ──────────────────────────────────────────────────────────────── |
|
|
// ── Helpers ──────────────────────────────────────────────────────────────── |
|
|
function nearEq(a,b,e=1e-9){if(a==null&&b==null)return true;if(a==null||b==null)return false;return Math.abs(Number(a)-Number(b))<=e;} |
|
|
function nearEq(a,b,e=1e-9){if(a==null&&b==null)return true;if(a==null||b==null)return false;return Math.abs(Number(a)-Number(b))<=e;} |
|
|
@@ -734,12 +764,12 @@ function clone(o){return JSON.parse(JSON.stringify(o??{}));} |
|
|
function gp(o,path){const ps=String(path||'').split('.');let c=o;for(const p of ps){if(!p)continue;if(c==null||typeof c!=='object')return undefined;c=c[p];}return c;} |
|
|
function gp(o,path){const ps=String(path||'').split('.');let c=o;for(const p of ps){if(!p)continue;if(c==null||typeof c!=='object')return undefined;c=c[p];}return c;} |
|
|
function sp(o,path,v){const ps=String(path||'').split('.');let c=o;for(let i=0;i<ps.length;i++){const p=ps[i];if(!p)continue;if(i===ps.length-1){c[p]=v;return;}if(!c[p]||typeof c[p]!=='object')c[p]={};c=c[p];}} |
|
|
function sp(o,path,v){const ps=String(path||'').split('.');let c=o;for(let i=0;i<ps.length;i++){const p=ps[i];if(!p)continue;if(i===ps.length-1){c[p]=v;return;}if(!c[p]||typeof c[p]!=='object')c[p]={};c=c[p];}} |
|
|
|
|
|
|
|
|
function srvVal(key){const cfg=S.server.config||{};switch(key){case 'frequencyMHz':return cfg.fm?.frequencyMHz;case 'ps':return cfg.rds?.ps??'';case 'radioText':return cfg.rds?.radioText??'';case 'stereoEnabled':return cfg.fm?.stereoEnabled;case 'rdsEnabled':return cfg.rds?.enabled;case 'limiterEnabled':return cfg.fm?.limiterEnabled;}return undefined;} |
|
|
|
|
|
|
|
|
function srvVal(key){const cfg=S.server.config||{};switch(key){case 'frequencyMHz':return cfg.fm?.frequencyMHz;case 'ps':return cfg.rds?.ps??'';case 'radioText':return cfg.rds?.radioText??'';case 'stereoEnabled':return cfg.fm?.stereoEnabled;case 'rdsEnabled':return cfg.rds?.enabled;case 'limiterEnabled':return cfg.fm?.limiterEnabled;case 'compositeClipperEnabled':return cfg.fm?.compositeClipper?.enabled;}return undefined;} |
|
|
function cfgSrvVal(key){const cfg=S.server.config||{};const f=CFG[key];return f?gp(cfg,f.path):undefined;} |
|
|
function cfgSrvVal(key){const cfg=S.server.config||{};const f=CFG[key];return f?gp(cfg,f.path):undefined;} |
|
|
function effVal(key){return S.dirty.has(key)?S.draft[key]:srvVal(key);} |
|
|
function effVal(key){return S.dirty.has(key)?S.draft[key]:srvVal(key);} |
|
|
function cfgEff(key){return S.cfgDraft[key]!==undefined?S.cfgDraft[key]:cfgSrvVal(key);} |
|
|
function cfgEff(key){return S.cfgDraft[key]!==undefined?S.cfgDraft[key]:cfgSrvVal(key);} |
|
|
|
|
|
|
|
|
function cfgEq(key,a,b){if(key==='pi')return String(a??'').trim().toUpperCase()===String(b??'').trim().toUpperCase();if(key==='pty'||key==='bs412Enabled')return a===b;return nearEq(a,b,1e-5);} |
|
|
|
|
|
|
|
|
function cfgEq(key,a,b){if(key==='pi')return String(a??'').trim().toUpperCase()===String(b??'').trim().toUpperCase();if(key==='pty'||key==='bs412Enabled'||key==='compositeClipperEnabled'||key==='compositeClipperIterations')return a===b;return nearEq(a,b,1e-5);} |
|
|
function cfgSetDirty(key,val){ |
|
|
function cfgSetDirty(key,val){ |
|
|
const f=CFG[key];if(!f)return; |
|
|
const f=CFG[key];if(!f)return; |
|
|
S.cfgDraft[key]=val; |
|
|
S.cfgDraft[key]=val; |
|
|
@@ -903,6 +933,7 @@ function _render(){ |
|
|
setText('info-pilot',fmtPilot(cfg.fm?.pilotLevel));setText('info-rdsinj',fmtPilot(cfg.fm?.rdsInjection)); |
|
|
setText('info-pilot',fmtPilot(cfg.fm?.pilotLevel));setText('info-rdsinj',fmtPilot(cfg.fm?.rdsInjection)); |
|
|
setText('info-mpxgain',cfg.fm?.mpxGain!=null?Number(cfg.fm.mpxGain).toFixed(2):'--'); |
|
|
setText('info-mpxgain',cfg.fm?.mpxGain!=null?Number(cfg.fm.mpxGain).toFixed(2):'--'); |
|
|
setText('info-bs412',cfg.fm?.bs412Enabled?`ON (${cfg.fm?.bs412ThresholdDBr??0} dBr)`:'OFF'); |
|
|
setText('info-bs412',cfg.fm?.bs412Enabled?`ON (${cfg.fm?.bs412ThresholdDBr??0} dBr)`:'OFF'); |
|
|
|
|
|
const cc=cfg.fm?.compositeClipper;setText('info-compclip',cc?.enabled?`ON (${cc.iterations??3}× ${cc.lookaheadMs?'LA ':''}${cc.softKnee>0?'soft':'hard'})`:'OFF'); |
|
|
|
|
|
|
|
|
// ── TX Control tab |
|
|
// ── TX Control tab |
|
|
// Freq |
|
|
// Freq |
|
|
@@ -931,6 +962,13 @@ function _render(){ |
|
|
syncSlider('mpxgain-slider','mpxgain-val','mpxGain',v=>v==null?'--':Number(v).toFixed(2)); |
|
|
syncSlider('mpxgain-slider','mpxgain-val','mpxGain',v=>v==null?'--':Number(v).toFixed(2)); |
|
|
setText('compliance-meta',S.cfgDirty['compliance']?'Draft pending':'Saved + Restart Required'); |
|
|
setText('compliance-meta',S.cfgDirty['compliance']?'Draft pending':'Saved + Restart Required'); |
|
|
$('compliance-apply').disabled=!S.cfgDirty['compliance'];$('compliance-reset').disabled=!S.cfgDirty['compliance']; |
|
|
$('compliance-apply').disabled=!S.cfgDirty['compliance'];$('compliance-reset').disabled=!S.cfgDirty['compliance']; |
|
|
|
|
|
// Composite Clipper |
|
|
|
|
|
syncToggle('tog-compclip','compclip-label','compositeClipperEnabled'); |
|
|
|
|
|
syncSlider('compclip-iter-slider','compclip-iter-val','compositeClipperIterations',v=>v==null?'--':String(Math.round(Number(v)))); |
|
|
|
|
|
syncSlider('compclip-knee-slider','compclip-knee-val','compositeClipperSoftKnee',v=>v==null?'--':Number(v).toFixed(2)); |
|
|
|
|
|
syncSlider('compclip-la-slider','compclip-la-val','compositeClipperLookaheadMs',v=>v==null?'--':Number(v).toFixed(1)); |
|
|
|
|
|
setText('compclip-meta',S.cfgDirty['compclip']?'Draft pending':'SM.1268'); |
|
|
|
|
|
$('compclip-apply').disabled=!S.cfgDirty['compclip'];$('compclip-reset').disabled=!S.cfgDirty['compclip']; |
|
|
// Danger |
|
|
// Danger |
|
|
$('danger-stop').disabled=S.txBusy;const rfl=$('danger-reset-fault');if(rfl){rfl.disabled=S.faultBusy||!S.server.runtimeOk;rfl.textContent=S.faultBusy?'Resetting...':'Reset Fault';} |
|
|
$('danger-stop').disabled=S.txBusy;const rfl=$('danger-reset-fault');if(rfl){rfl.disabled=S.faultBusy||!S.server.runtimeOk;rfl.textContent=S.faultBusy?'Resetting...':'Reset Fault';} |
|
|
const rh=$('reset-hint');if(rh){const sn=normState(eng.state);rh.textContent=sn==='faulted'?'Faulted: reset moves runtime back to DEGRADED.':sn==='muted'||sn==='degraded'?'Reset Fault holds at DEGRADED until queue recovers.':'Manual fault reset drops to DEGRADED while queue recovers.';} |
|
|
const rh=$('reset-hint');if(rh){const sn=normState(eng.state);rh.textContent=sn==='faulted'?'Faulted: reset moves runtime back to DEGRADED.':sn==='muted'||sn==='degraded'?'Reset Fault holds at DEGRADED until queue recovers.':'Manual fault reset drops to DEGRADED while queue recovers.';} |
|
|
@@ -1048,6 +1086,7 @@ function bindAll(){ |
|
|
// Compliance |
|
|
// Compliance |
|
|
bindCfgSlider('bs412-threshold-slider','bs412ThresholdDBr');bindCfgSlider('mpxgain-slider','mpxGain'); |
|
|
bindCfgSlider('bs412-threshold-slider','bs412ThresholdDBr');bindCfgSlider('mpxgain-slider','mpxGain'); |
|
|
$('compliance-apply').addEventListener('click',()=>applyCfgSection('compliance'));$('compliance-reset').addEventListener('click',()=>{cfgClear('compliance');toast('Draft reset','info');}); |
|
|
$('compliance-apply').addEventListener('click',()=>applyCfgSection('compliance'));$('compliance-reset').addEventListener('click',()=>{cfgClear('compliance');toast('Draft reset','info');}); |
|
|
|
|
|
$('compclip-apply').addEventListener('click',()=>applyCfgSection('compclip'));$('compclip-reset').addEventListener('click',()=>{cfgClear('compclip');toast('Draft reset','info');}); |
|
|
// TX |
|
|
// TX |
|
|
$('btn-start').addEventListener('click',()=>txAction('start'));$('btn-stop').addEventListener('click',()=>txAction('stop')); |
|
|
$('btn-start').addEventListener('click',()=>txAction('start'));$('btn-stop').addEventListener('click',()=>txAction('stop')); |
|
|
$('danger-stop').addEventListener('click',()=>txAction('stop'));$('btn-refresh').addEventListener('click',manualRefresh); |
|
|
$('danger-stop').addEventListener('click',()=>txAction('stop'));$('btn-refresh').addEventListener('click',manualRefresh); |
|
|
|