Loading files
<!doctype html>
<html lang="en">
<head><meta charset="utf-8">
<script src="/pf-console.js"></script>
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Patternflow - Knobs</title>
<style>
/* Dark instrument tokens — see patterns_index.h for the note. */
:root{--cream:#0C0B09;--ink:#EDE7DB;--muted:#8A8272;--faint:#5A5546;
--rule:#242118;--rule-soft:#1B1914;--led:#FF5C2E;--ok:#57B87F;--panel:#131110;
--sans:'Inter',ui-sans-serif,system-ui,sans-serif;
--mono:'JetBrains Mono',ui-monospace,SFMono-Regular,Menlo,monospace}
*{box-sizing:border-box}
body{margin:0;background:var(--cream);color:var(--ink);font-family:var(--sans);
line-height:1.5;-webkit-font-smoothing:antialiased}
.wrap{max-width:680px;margin:0 auto;padding:32px 20px 64px}
header{display:flex;align-items:center;gap:8px;padding-bottom:12px;
border-bottom:1px solid var(--rule)}
.dot{width:7px;height:7px;background:var(--led);border-radius:1px}
h1{font-size:15px;font-weight:600;margin:0;flex:1}
.sub{font-family:var(--mono);font-size:11px;color:var(--faint)}
section{margin-top:26px}
h2{font-size:11px;font-weight:600;letter-spacing:.09em;text-transform:uppercase;
color:var(--muted);margin:0 0 8px}
table{width:100%;border-collapse:collapse;border-top:1px solid var(--rule-soft)}
th{font-size:11px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;
color:var(--faint);text-align:left;padding:6px 2px}
td{padding:9px 2px;border-bottom:1px solid var(--rule-soft);font-size:13px;vertical-align:middle}
td.k{font-family:var(--mono);font-size:12px;color:var(--muted);width:34px}
td.pos{font-family:var(--mono);font-size:15px;width:90px}
td.pos small{display:block;font-size:10px;color:var(--faint)}
select{font:inherit;font-family:var(--mono);font-size:12px;padding:5px 8px;
background:var(--panel);color:var(--ink);border:1px solid var(--rule);border-radius:2px;width:100%}
select:focus{outline:none;border-color:var(--led)}
select.dirty{border-color:var(--led)}
.actions{display:flex;gap:8px;align-items:center;margin-top:14px}
button{font:inherit;cursor:pointer}
button.primary{background:var(--ink);color:var(--cream);border:1px solid var(--ink);
border-radius:2px;padding:7px 16px;font-size:13px}
button.primary:hover{background:var(--led);border-color:var(--led)}
button.primary:disabled{opacity:.4;cursor:default}
button.secondary{font-size:13px;color:var(--ink);background:none;
border:1px solid var(--rule);border-radius:2px;padding:7px 16px}
button.secondary:hover{border-color:var(--led);color:var(--led)}
#msg{margin-top:12px;font-family:var(--mono);font-size:11px;min-height:16px}
#msg.pf-err{color:var(--led)}#msg.pf-ok{color:var(--ok)}
/* offline: the chrome's chip says so; the readout dims */
html.pf-offline td.pos,html.pf-offline #st{opacity:.4}
@media(pointer:coarse){.actions button{position:relative}
.actions button::after{content:"";position:absolute;inset:-6px 0}}
.note{font-size:12px;color:var(--faint);margin:8px 0 0;line-height:1.45}
.note b{color:var(--muted);font-weight:600}
footer{margin-top:32px;padding-top:12px;border-top:1px solid var(--rule);
font-family:var(--mono);font-size:11px;color:var(--faint)}
a{color:var(--muted)}
</style></head><body><div class="wrap">
<header><span class="dot"></span><h1>Knobs</h1><span class="sub" id="st">-</span></header>
<section>
<h2>Direction and click size</h2>
<table>
<thead><tr><th></th><th>Position</th><th>Direction</th><th>Edges per click</th></tr></thead>
<tbody id="rows"></tbody>
</table>
<div class="actions">
<button class="primary" type="button" id="save" disabled>Save</button>
<button class="secondary" type="button" id="revert" disabled>Revert</button>
</div>
<div id="msg"></div>
<p class="note"><b>Turn a knob clockwise.</b> Its position should go up by one per click.
Goes down: set that knob to <em>Reversed</em>. Moves two per click, or needs two clicks to
move one: change <em>Edges per click</em> until one click is one step (most encoders are 4;
the edge counter under the position shows how many one click really makes).
These are properties of the encoder soldered in, stored on the device — nothing to
rebuild, and a saved change does not move the knob.</p>
</section>
<footer><a href="/">Home</a> · <a href="/status">Status</a></footer>
</div>
<script>
function $(i){return document.getElementById(i)}
function say(t,k){PF.say(t,k||'info',$('msg'))}
var cur=null; // what the device has
var dirty=false; // an unsaved choice on the page
var saving=false; // a POST in flight: Save stays off
function selDir(i){return $('inv'+i)}
function selSub(i){return $('sub'+i)}
function build(){
var tb=$('rows');tb.innerHTML='';
for(var i=0;i<4;i++){
var tr=document.createElement('tr');
// off until the first reading gives a choice something to differ from
tr.innerHTML='<td class="k">K'+(i+1)+'</td>'+
'<td class="pos"><span id="pos'+i+'">-</span><small id="raw'+i+'"></small></td>'+
'<td><select id="inv'+i+'" disabled><option value="0">Normal</option><option value="1">Reversed</option></select></td>'+
'<td><select id="sub'+i+'" disabled><option value="4">4 (most encoders)</option><option value="2">2</option><option value="1">1</option></select></td>';
tb.appendChild(tr);
selDir(i).onchange=selSub(i).onchange=marks;
}
}
// PF.dirty (no version reload) while a choice differs from the device
function marks(){
var any=false;
for(var i=0;i<4;i++){
var a=(selDir(i).value==='1')!==cur.inv[i],b=parseInt(selSub(i).value,10)!==cur.sub[i];
selDir(i).className=a?'dirty':'';selSub(i).className=b?'dirty':'';
any=any||a||b;
}
setDirty(any);
}
function setDirty(on){dirty=PF.dirty=on;$('save').disabled=!on||saving;$('revert').disabled=!on}
function show(d){
cur=d;
for(var i=0;i<4;i++){
$('pos'+i).textContent=d.clicks[i];
$('raw'+i).textContent=d.raw[i]+' edges';
selDir(i).disabled=selSub(i).disabled=false;
if(!dirty){
selDir(i).value=d.inv[i]?'1':'0';
selSub(i).value=String(d.sub[i]);
}
}
// re-judged each reading: another tab may have saved
if(dirty)marks();else for(i=0;i<4;i++)selDir(i).className=selSub(i).className='';
$('st').textContent='live';
}
$('revert').onclick=function(){setDirty(false);if(cur)show(cur);say('')};
$('save').onclick=function(){
var body=[];
for(var i=0;i<4;i++){
body.push('inv'+i+'='+selDir(i).value);
body.push('sub'+i+'='+selSub(i).value);
}
saving=$('save').disabled=true;
fetch('/api/knobs',{method:'POST',
headers:{'Content-Type':'application/x-www-form-urlencoded'},body:body.join('&')})
.then(function(r){return r.json()}).then(function(d){
saving=false;
if(!d.ok){say(d.error||'failed','err');$('save').disabled=false;return}
setDirty(false);show(d);say('saved','ok');
}).catch(function(){saving=false;say('failed - the panel did not answer','err');$('save').disabled=false});
};
build();
// Chained on the chrome's lane: a slow link stretches it, a hidden tab
// pauses it, and the chip reports errors.
PF.poll('/api/knobs',400,function(d){if(d)show(d)});
</script></body></html>
07e931b46This file is pinned to the selected release. Download the original to open it in your design tool.