Add Pac-Man power-on flow and document Odin integration
- Add a four-second arcade-style boot sequence followed by the Character / Nickname introduction. - Separate cabinet power-on from RFID credit and player Start. - Add operator Power on control and cabinet power-state reporting. - Prevent repeated power commands from restarting boot or gameplay. - Ignore RFID scans received before the cabinet is ready. - Add power-state, RFID, controller, and GPIO feedback regression tests. - Expand the master walkthrough with export, packaging, file transfer, fresh-Pi setup, RFID compilation, and end-to-end testing instructions. - Correct tester hardware documentation: BCM23/J20 drives an LED; physical buttons connect through a separate I2C1 input board. - Document operator-triggered power-on as the current test workflow and retain the temporary 2.4 configuration compatibility notes. - Record successful physical testing of power-on, RFID credit, and player Start. - Add an Odin integration README describing the proposed Rust/NATS bridge, shared messages, Flutter GM interface, autopilot sequencing, hardware ownership, and Starfish SSH-management role. - Define migration milestones and information needed from the framework owner; link the integration guide from the root README. Validation: 69 controller tests, 10 GPIO tests, Pac-Man power/RFID checks, managed-cabinet lifecycle checks, and visual screen inspection.
This commit is contained in:
1 parent
c1b2195834
commit
41ef12f954
27 files changed
+577
-48
No files matched your search
@@ -59,7 +59,7 @@ class Feedback:
|
||||
old.update(value=value,error=error,stamp=self.clock())
|
||||
if changed:
|
||||
self.sequence+=1
|
||||
self.events.append(dict(id=self.boot+':'+str(self.sequence),device=ident,value=value,error=error,kind=old['kind']))
|
||||
self.events.append(dict(id=self.boot+':'+str(self.sequence),device=ident,value=value,error=error,kind=old['kind'],stamp=self.clock()))
|
||||
def _readers(self, readers):
|
||||
slots={int(d['port'][1:])-1:d['id'] for d in readers}
|
||||
try:
|
||||
@@ -88,7 +88,7 @@ class Feedback:
|
||||
def snapshot(self):
|
||||
with self.lock:
|
||||
now=self.clock()
|
||||
return dict(devices=[{k:v for k,v in d.items() if k!='stamp'}|{'age_ms':int(max(0,now-d['stamp'])*1000)} for d in self.values.values()],events=list(self.events))
|
||||
return dict(devices=[{k:v for k,v in d.items() if k!='stamp'}|{'age_ms':int(max(0,now-d['stamp'])*1000)} for d in self.values.values()],events=[{k:v for k,v in e.items() if k!='stamp'}|{'age_ms':int(max(0,now-e['stamp'])*1000)} for e in self.events])
|
||||
def close(self):
|
||||
self.stop.set()
|
||||
if self.reader:
|
||||
|
||||
@@ -415,6 +415,7 @@
|
||||
"game": "pacman",
|
||||
"game_config": {
|
||||
"boards_to_win": 1,
|
||||
"power_on_device": "cab1_btn_ylw",
|
||||
"rfid_allowed_uids": [],
|
||||
"rfid_credit_device": "cab1_rfid1",
|
||||
"score_to_win": 0
|
||||
|
||||
@@ -8,12 +8,12 @@
|
||||
<div id="shell" hidden><aside class="sidebar"><div class="brand"><div class="mark">EG</div><div><strong id="siteName">Escape Gallery</strong><small>OPERATIONS</small></div></div><nav aria-label="Main navigation"><div class="nav-label">WORKSPACE</div><button class="nav" data-view="home"><span>◈</span>Hub overview</button><div class="nav-label">CURRENT ROOM</div><button class="nav" data-view="room"><span>▤</span>Room control</button><button class="nav" data-view="cabinets"><span>▣</span>Cabinets</button><button class="nav" data-view="hardware"><span>⌘</span>Hardware</button><button class="nav" data-view="activity"><span>≡</span>Activity</button></nav><footer><p id="sidebarRoom">Glitch: the Dark Machine</p><p>Building network · Operator access</p><button class="lock">Lock this device</button></footer></aside><div class="content"><header class="topbar"><div><small id="breadcrumb">Operations / Overview</small><strong id="roomName">Glitch: the Dark Machine</strong></div><div class="row"><span id="status" class="badge" role="status">Connecting</span><button id="refresh">Refresh</button><button class="lock" id="mobileLock">Lock</button></div></header><main><div id="error" class="notice" role="alert"></div><div id="toast" class="notice" role="status"></div><div id="stale" class="notice" hidden>Connection lost. Showing last received state; commands are disabled.</div>
|
||||
<section data-page="home"><div class="heading"><div class="eyebrow">Your operations, together</div><h1>Keep the experience moving.</h1><p>Open a room, check what needs attention, and get back to your players.</p></div><div class="row between"><h2>Experiences</h2><span class="badge">Local network</span></div><div id="experiences" class="grid"></div><div class="stats"><div class="stat"><strong id="onlineCount">—</strong><span>Cabinets online</span></div><div class="stat"><strong id="playingCount">—</strong><span>Games in progress</span></div><div class="stat"><strong id="readyCount">—</strong><span>Ready to start</span></div><div class="stat"><strong id="attentionCount">—</strong><span>Need attention</span></div></div><div class="columns"><section class="panel"><div class="row between"><h2>Needs attention</h2><button class="link" data-go="cabinets">View cabinets →</button></div><div id="attention"></div></section><section class="panel"><div class="row between"><h2>Recent activity</h2><button class="link" data-go="activity">View all →</button></div><ol id="recent" class="activity"></ol></section></div></section>
|
||||
<section data-page="room" hidden><div class="heading"><div class="eyebrow">Game-master workspace</div><h1 id="roomHeading">Glitch: the Dark Machine</h1><p>Follow the room layout and cabinet sequence. Select a station for its live reported state and controls.</p></div><div class="columns"><section class="panel"><h2>Scenario preparation</h2><label for="scenario">Scenario</label><select id="scenario" style="width:100%"></select><ol id="scenarioPlan" class="scenario-plan"></ol><button id="prepareScenario" class="primary" style="width:100%">Prepare assigned cabinets…</button><p class="small muted" style="margin-top:14px">Preparation can run configured activation outputs. Linked cabinets wait for the preceding win; independent cabinets are started individually.</p><div id="scenarioBlockers" class="notice"></div></section><section class="panel"><h2>Room layout</h2><svg id="map" role="group" aria-label="Room layout; select a cabinet for controls"></svg><div class="legend"><span><i style="background:#287c58"></i>Ready / won</span><span><i style="background:#426ca4"></i>Playing</span><span><i style="background:#9a762d"></i>Waiting / idle</span><span><i style="background:#89918d"></i>Offline</span></div><p class="small muted">Device markers are reference locations, not live sensor readings. Cabinet rings indicate pending or unconfirmed output commands.</p></section></div><div class="row between" style="margin:24px 0 14px"><h2 style="margin:0">Cabinet status</h2><button class="link" data-go="cabinets">Open controls →</button></div><div class="grid" id="roomCards"></div></section>
|
||||
<section data-page="cabinets" hidden><div class="heading"><div class="eyebrow">Connected stations</div><h1>Cabinets</h1><p>The marquee follows the assigned game. The screen represents reported state; it is not a live video feed.</p></div><div class="grid" id="cards"></div><section class="panel detail" id="detail"><div class="row between" style="margin-bottom:20px"><div><h2 id="cabinetTitle">Select a cabinet</h2><small id="connection"></small></div><span class="badge" id="phaseBadge"></span></div><div class="columns"><div><dl id="assignment" class="kv"></dl><div id="steps" class="steps"></div><div class="row"><button id="start" class="primary">Start game</button><button id="reset" class="danger">Reset cabinet…</button><button id="openHardware">Hardware →</button></div><p id="controlHint" class="small muted" style="margin-top:14px"></p><hr><h3>Manual game assignment</h3><p class="small muted">Runs a compatible game without scenario hardware rewards.</p><label for="game">Compatible game</label><div class="row"><select id="game"></select><button id="prepare">Prepare game…</button></div><h3 style="margin-top:24px">Win output</h3><p id="reward" class="small muted"></p><h3>Device feedback</h3><div id="cabinetFeedback" class="feedback-list"></div></div><div class="visual-panel"><div class="cabinet-visual" aria-label="Cabinet state illustration"><div class="marquee" id="marquee">GAME</div><div class="screen"><div class="screen-game" id="screenGame"></div><div class="screen-state" id="screenState"></div><div class="screen-detail" id="screenDetail"></div></div><div class="deck" aria-hidden="true"><div class="joystick"></div><div class="buttons"><i></i><i></i><i></i></div><div class="joystick"></div></div><div class="coin-door" aria-hidden="true"><div class="coin-slot"></div></div></div><p class="cabinet-caption">Assigned-game artwork · reported cabinet state</p></div></div></section></section>
|
||||
<section data-page="cabinets" hidden><div class="heading"><div class="eyebrow">Connected stations</div><h1>Cabinets</h1><p>The marquee follows the assigned game. The screen represents reported state; it is not a live video feed.</p></div><div class="grid" id="cards"></div><section class="panel detail" id="detail"><div class="row between" style="margin-bottom:20px"><div><h2 id="cabinetTitle">Select a cabinet</h2><small id="connection"></small></div><span class="badge" id="phaseBadge"></span></div><div class="columns"><div><dl id="assignment" class="kv"></dl><div id="steps" class="steps"></div><div class="row"><button id="powerOn">Power on</button><button id="start" class="primary">Start game</button><button id="reset" class="danger">Reset cabinet…</button><button id="openHardware">Hardware →</button></div><p id="controlHint" class="small muted" style="margin-top:14px"></p><hr><h3>Manual game assignment</h3><p class="small muted">Runs a compatible game without scenario hardware rewards.</p><label for="game">Compatible game</label><div class="row"><select id="game"></select><button id="prepare">Prepare game…</button></div><h3 style="margin-top:24px">Win output</h3><p id="reward" class="small muted"></p><h3>Device feedback</h3><div id="cabinetFeedback" class="feedback-list"></div></div><div class="visual-panel"><div class="cabinet-visual" aria-label="Cabinet state illustration"><div class="marquee" id="marquee">GAME</div><div class="screen"><div class="screen-game" id="screenGame"></div><div class="screen-state" id="screenState"></div><div class="screen-detail" id="screenDetail"></div></div><div class="deck" aria-hidden="true"><div class="joystick"></div><div class="buttons"><i></i><i></i><i></i></div><div class="joystick"></div></div><div class="coin-door" aria-hidden="true"><div class="coin-slot"></div></div></div><p class="cabinet-caption">Assigned-game artwork · reported cabinet state</p></div></div></section></section>
|
||||
<section data-page="hardware" hidden><div class="heading"><div class="eyebrow">Physical devices</div><h1>Hardware & outputs</h1><p>Trigger configured actions and review acknowledgements. Accepted does not prove that a lock moved or a reward was delivered.</p></div><section class="panel"><label for="hardwareCabinet">Cabinet</label><select id="hardwareCabinet"></select><p id="hardwareHint" class="small muted" style="margin-top:14px"></p><div class="grid" id="actions"></div><h2 style="margin-top:26px">Command history</h2><div class="table-wrap" id="output"></div><h2 style="margin-top:24px">Live device feedback</h2><div id="feedback" class="feedback-list"></div></section><section class="panel" style="margin-top:20px"><div class="row between"><h2>Room devices</h2><span class="badge">Integration pending</span></div><p class="small muted">Device feedback is listed above when a configured Pi bridge reports it. These room markers remain reference locations until mapped to sensor IDs.</p><div id="roomDevices" class="grid"></div></section></section>
|
||||
<section data-page="activity" hidden><div class="heading"><div class="eyebrow">Room history</div><h1>Activity</h1><p>Cabinet events, operator interventions, and command acknowledgements in one place.</p></div><section class="panel"><div class="row filters"><label class="visually-hidden" for="activityCabinet">Filter cabinet</label><select id="activityCabinet"></select><label class="visually-hidden" for="activitySearch">Search activity</label><input type="search" id="activitySearch" placeholder="Search events or actions…"><span id="eventCount" class="small muted"></span></div><ol class="activity" id="events"></ol><p class="small muted" style="margin-top:20px">Latest retained history: up to 100 events per cabinet. Operator entries record actions and reasons; individual staff accounts are not configured.</p></section></section>
|
||||
</main></div></div><dialog id="confirmDialog" aria-labelledby="confirmTitle"><form id="confirmForm"><div class="eyebrow">Review action</div><h2 id="confirmTitle"></h2><p id="confirmText" class="muted"></p><div id="confirmReason" hidden><label for="reason">Reason for this intervention</label><textarea id="reason" rows="3" maxlength="200"></textarea></div><div class="row"><button type="button" id="cancelConfirm">Cancel</button><button type="submit" class="primary" id="acceptConfirm">Confirm</button></div></form></dialog>
|
||||
<script>
|
||||
const $=id=>document.getElementById(id), phases={idle:'Idle',waiting:'Waiting',preparing:'Preparing',ready:'Ready',starting:'Starting',playing:'Playing',won:'Won',lost:'Lost',resetting:'Resetting',needs_prepare:'Needs preparation'};
|
||||
const $=id=>document.getElementById(id), phases={powered_off:'Waiting for power',booting:'Booting',idle:'Idle',waiting:'Waiting',preparing:'Preparing',ready:'Ready',starting:'Starting',playing:'Playing',won:'Won',lost:'Lost',resetting:'Resetting',needs_prepare:'Needs preparation'};
|
||||
let token='',state=null,selected='',view='home',connected=false,pending=false,refreshing=false,epoch=0,confirmation=null;
|
||||
const node=(tag,text='',cls='')=>{const n=document.createElement(tag);n.textContent=text;n.className=cls;return n};
|
||||
const name=id=>state?.cabinets[id]?.definition.name||id, gameName=id=>state?.catalog.games[id]?.name||id||'No game assigned';
|
||||
@@ -24,7 +24,7 @@ function cached(id,key,fn){const e=$(id),s=JSON.stringify(key);if(e.dataset.key!
|
||||
function navigate(next){view=['home','room','cabinets','hardware','activity'].includes(next)?next:'home';document.querySelectorAll('[data-page]').forEach(e=>e.hidden=e.dataset.page!==view);document.querySelectorAll('[data-view]').forEach(e=>e.setAttribute('aria-current',e.dataset.view===view?'page':'false'));$('breadcrumb').textContent='Operations / '+({home:'Overview',room:'Room control',cabinets:'Cabinets',hardware:'Hardware',activity:'Activity'}[view]);if(state)render()}
|
||||
function select(id,next='cabinets'){selected=id;navigate(next)}
|
||||
async function api(path,data={}){const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),6000);try{const r=await fetch(path,{method:'POST',headers:{'Content-Type':'application/json',Authorization:'Bearer '+token},body:JSON.stringify(data),signal:controller.signal});const b=await r.json();if(!r.ok)throw Error(b.error||'Request failed');return b}finally{clearTimeout(timer)}}
|
||||
function enable(){const c=state?.cabinets[selected],ok=connected&&!pending;['start','reset','prepare','prepareScenario'].forEach(id=>$(id).disabled=true);if(c){$('start').disabled=!(ok&&c.online&&c.phase==='ready'&&!c.gate&&!c.game_config?.rfid_credit_device&&!busy(c));$('reset').disabled=!(ok&&c.online&&!busy(c));$('prepare').disabled=!(ok&&prep(c)&&$('game').value)}const plan=state?.catalog.scenarios[$('scenario').value];$('prepareScenario').disabled=!(ok&&plan?.assignments.length&&plan.assignments.every(a=>prep(state.cabinets[a.cabinet])));document.querySelectorAll('[data-output]').forEach(b=>b.disabled=!hardware(c));$('acceptConfirm').disabled=!ok;$('refresh').disabled=pending||refreshing}
|
||||
function enable(){const c=state?.cabinets[selected],ok=connected&&!pending;['powerOn','start','reset','prepare','prepareScenario'].forEach(id=>$(id).disabled=true);if(c){$('powerOn').disabled=!(ok&&c.online&&c.phase==='powered_off'&&!busy(c));$('start').disabled=!(ok&&c.online&&c.phase==='ready'&&!c.gate&&!c.game_config?.rfid_credit_device&&!busy(c));$('reset').disabled=!(ok&&c.online&&!busy(c));$('prepare').disabled=!(ok&&prep(c)&&$('game').value)}const plan=state?.catalog.scenarios[$('scenario').value];$('prepareScenario').disabled=!(ok&&plan?.assignments.length&&plan.assignments.every(a=>prep(state.cabinets[a.cabinet])));document.querySelectorAll('[data-output]').forEach(b=>b.disabled=!hardware(c));$('acceptConfirm').disabled=!ok;$('refresh').disabled=pending||refreshing}
|
||||
async function refresh(){if(!token||pending||refreshing)return;refreshing=true;const generation=epoch;try{const s=await api('/operator/state');if(generation!==epoch)return;state=s;connected=true;$('login').hidden=true;$('shell').hidden=false;$('loginError').textContent='';render()}catch(e){if(generation!==epoch)return;connected=false;$('loginError').textContent=e.message;enable()}finally{refreshing=false;$('status').textContent=connected?'Connected':'Disconnected';$('status').className='badge '+(connected?'good':'bad');$('stale').hidden=connected;enable()}}
|
||||
async function command(action,data){if(!connected||pending)return;pending=true;const generation=++epoch;enable();$('error').textContent='';$('toast').textContent='';try{const result=await api('/operator/command',{action,...data});if(generation!==epoch)return;state=result;render();$('toast').textContent='Command submitted. Follow its acknowledgement in Activity.'}catch(e){if(generation!==epoch)return;$('error').textContent=e.name==='AbortError'?'Reply timed out. Check Activity before repeating the action.':e.message}finally{pending=false;enable();refresh()}}
|
||||
function review(title,text,fn,reason=false){confirmation=fn;$('confirmTitle').textContent=title;$('confirmText').textContent=text;$('confirmReason').hidden=!reason;$('reason').value='';$('reason').required=reason;$('reason').minLength=reason?3:0;$('confirmDialog').showModal()}
|
||||
@@ -34,13 +34,13 @@ function cards(id){const root=$(id);for(const [key,c] of Object.entries(state.ca
|
||||
function home(){const cs=Object.values(state.cabinets);$('onlineCount').textContent=cs.filter(c=>c.online).length+' / '+cs.length;$('playingCount').textContent=cs.filter(c=>c.online&&c.phase==='playing').length;$('readyCount').textContent=cs.filter(c=>c.online&&c.phase==='ready').length;const attention=Object.entries(state.cabinets).filter(([,c])=>!c.online||c.phase==='needs_prepare'||c.commands.some(x=>['failed','unknown'].includes(x.status)));$('attentionCount').textContent=attention.length;cached('experiences',state.hub,e=>{const b=node('button','','experience');b.append(node('span','ARCADE ROOM','eyebrow'),node('h2',state.hub.room_name),node('p','Room layout, cabinet controls and hardware activity'),node('span','Open room →','link'));b.onclick=()=>navigate('room');e.replaceChildren(b);for(const s of state.hub.services){const a=node('a','','experience');a.href=s.url;a.target='_blank';a.rel='noopener noreferrer';a.append(node('span',s.kind.toUpperCase(),'eyebrow'),node('h2',s.name),node('p',s.description),node('small','External service · status not monitored'));e.append(a)}});cached('attention',attention.map(([id,c])=>[id,c.online,c.phase]),e=>{e.replaceChildren(...attention.map(([id,c])=>{const b=node('button',name(id)+' — '+(!c.online?'Offline':c.phase==='needs_prepare'?'Needs preparation':'Review command history'),'attention');b.onclick=()=>select(id);return b}));if(!attention.length)e.append(node('p','All cabinets are reporting normally.','muted'))});events('recent',allEvents().slice(0,6))}
|
||||
function scenario(){const s=state.catalog.scenarios[$('scenario').value];$('scenarioPlan').replaceChildren(...(s?.assignments||[]).map(a=>{const li=node('li');li.append(node('strong',name(a.cabinet)+' · '+gameName(a.game)),node('p',a.activate_after_win?'After '+name(a.activate_after_win)+' wins':'Ready for operator start'),node('small',Object.entries(a.game_config||{}).map(([k,v])=>k.replaceAll('_',' ')+': '+v).join(' · ')));return li}));$('scenarioBlockers').textContent=(s?.assignments||[]).filter(a=>!prep(state.cabinets[a.cabinet])).map(a=>name(a.cabinet)+': must be online, reset, and free of pending commands').join('. ')}
|
||||
function map(){const layout=state.catalog.layout||{},cs=Object.entries(state.cabinets);cached('map',[layout,cs.map(([id,c])=>[id,c.online,c.phase,c.commands])],svg=>{svg.replaceChildren();const pts=[...(layout.boundary||[]),...(layout.walls||[]).flat(),...(layout.devices||[]).map(d=>d.position),...cs.map(([,c])=>c.definition.position)].filter(Boolean);if(!pts.length)return;const xs=pts.map(p=>p[0]),ys=pts.map(p=>p[1]),minX=Math.min(...xs)-.7,minY=Math.min(...ys)-.7,w=Math.max(...xs)-minX+.7,h=Math.max(...ys)-minY+.7;svg.setAttribute('viewBox',[minX,minY,w,h].join(' '));const draw=(tag,attrs,parent=svg)=>{const e=document.createElementNS('http://www.w3.org/2000/svg',tag);for(const[k,v]of Object.entries(attrs))e.setAttribute(k,v);parent.append(e);return e};if(layout.boundary?.length)draw('polygon',{points:layout.boundary.map(p=>p.join(',')).join(' '),fill:'#edf0e9',stroke:'#b6beb5','stroke-width':.04});for(const wall of layout.walls||[])draw('polyline',{points:wall.map(p=>p.join(',')).join(' '),fill:'none',stroke:'#728176','stroke-width':.07});for(const d of layout.devices||[]){if(!d.position)continue;const e=draw('rect',{x:d.position[0]-.05,y:d.position[1]-.05,width:.1,height:.1,fill:'#555'});draw('title',{},e).textContent=d.name+' — reference location; feedback unavailable'}for(const[id,c]of cs){const p=c.definition.position;if(!p)continue;const g=draw('g',{role:'button',tabindex:0,'aria-label':name(id)+': '+(c.online?c.phase:'offline')});g.style.cursor='pointer';g.onclick=()=>select(id);g.onkeydown=e=>{if(['Enter',' '].includes(e.key)){e.preventDefault();select(id)}};draw('circle',{cx:p[0],cy:p[1],r:.17,fill:!c.online?'#89918d':c.phase==='playing'?'#426ca4':['ready','won'].includes(c.phase)?'#287c58':'#9a762d'},g);const output=c.commands.filter(x=>['output','reward'].includes(x.type)).at(-1);if(output&&['queued','unknown','failed'].includes(output.status)){const ring=draw('circle',{cx:p[0],cy:p[1],r:.23,fill:'none',stroke:output.status==='queued'?'#b28934':'#bd4a3e','stroke-width':.035},g);draw('title',{},ring).textContent='Output command '+output.status+'; physical state unverified'}draw('text',{x:p[0],y:p[1]-.3,'text-anchor':'middle','font-size':.16,fill:'#263f32'},g).textContent=name(id)}})}
|
||||
function detail(){renderFeedback('cabinetFeedback');const c=state.cabinets[selected];$('detail').hidden=!c;if(!c)return;$('cabinetTitle').textContent=name(selected);$('connection').textContent=selected+' · '+c.version+' · GPIO: '+c.gpio;$('phaseBadge').textContent=c.online?(phases[c.phase]||c.phase):'Offline';$('assignment').replaceChildren(...Object.entries({Game:gameName(c.game),Assignment:c.scenario||'None',Activation:c.gate?'Waiting for '+name(c.gate.cabinet)+' to win':'Independent',Objectives:Object.entries(c.game_config||{}).map(([k,v])=>k.replaceAll('_',' ')+': '+v).join(', ')||'Game defaults'}).flatMap(([k,v])=>[node('dt',k),node('dd',v)]));$('steps').replaceChildren(...['preparing','ready','playing','won'].map(p=>node('span',phases[p],c.phase===p?'current':'')));options('game',c.compatible_games.map(id=>[id,gameName(id)]));$('reward').textContent=c.reward||'No win output reported.';$('controlHint').textContent=!c.online?'Reconnect this cabinet to send commands.':busy(c)?'Waiting for command acknowledgement.':c.gate?'This cabinet starts automatically after its dependency wins.':c.game_config?.rfid_credit_device?'Scan a card, then press Start on the cabinet.':'Start a ready game, or reset before changing its assignment.';renderCabinetArt(c);$('screenState').textContent=c.online?(phases[c.phase]||c.phase):'Offline';$('screenDetail').textContent=c.gate?'Waiting for '+name(c.gate.cabinet):busy(c)?'Command pending':'Reported state · '+selected}
|
||||
function detail(){renderFeedback('cabinetFeedback');const c=state.cabinets[selected];$('detail').hidden=!c;if(!c)return;$('cabinetTitle').textContent=name(selected);$('connection').textContent=selected+' · '+c.version+' · GPIO: '+c.gpio;$('phaseBadge').textContent=c.online?(phases[c.phase]||c.phase):'Offline';$('assignment').replaceChildren(...Object.entries({Game:gameName(c.game),Assignment:c.scenario||'None',Activation:c.gate?'Waiting for '+name(c.gate.cabinet)+' to win':'Independent',Objectives:Object.entries(c.game_config||{}).map(([k,v])=>k.replaceAll('_',' ')+': '+v).join(', ')||'Game defaults'}).flatMap(([k,v])=>[node('dt',k),node('dd',v)]));$('steps').replaceChildren(...['preparing','powered_off','booting','ready','playing','won'].filter(p=>c.game==='pacman'||!['powered_off','booting'].includes(p)).map(p=>node('span',phases[p],c.phase===p?'current':'')));options('game',c.compatible_games.map(id=>[id,gameName(id)]));$('reward').textContent=c.reward||'No win output reported.';$('controlHint').textContent=!c.online?'Reconnect this cabinet to send commands.':busy(c)?'Waiting for command acknowledgement.':c.gate?'This cabinet starts automatically after its dependency wins.':c.phase==='powered_off'?'Press the configured PCB power button, or use Power on.':c.phase==='booting'?'Pac-Man is running its four-second startup check.':c.game_config?.rfid_credit_device?'Scan a card, then press Start on the cabinet.':'Start a ready game, or reset before changing its assignment.';renderCabinetArt(c);$('screenState').textContent=c.online?(phases[c.phase]||c.phase):'Offline';$('screenDetail').textContent=c.gate?'Waiting for '+name(c.gate.cabinet):busy(c)?'Command pending':'Reported state · '+selected}
|
||||
function hardwareView(){renderFeedback();const c=state.cabinets[selected];$('hardwareCabinet').value=selected;$('hardwareHint').textContent=c?name(selected)+' · GPIO: '+c.gpio+' · '+(hardware(c)?'Configured outputs available.':'Requires an online, prepared cabinet with real GPIO and no pending commands.'):'No cabinet selected';cached('actions',[selected,c?.definition.actions],e=>{e.replaceChildren(...(c?.definition.actions||[]).map(action=>{const box=node('div','','panel'),b=node('button','Trigger…');b.dataset.output=action;b.onclick=()=>{const id=selected,session=state.cabinets[id].session;review('Trigger '+action+'?',name(id)+' will run this configured physical action. Check the connected device before proceeding.',reason=>{const bytes=crypto.getRandomValues(new Uint8Array(16));command('trigger_output',{cabinet:id,expected_session:session,action_id:action,reason,request_id:Array.from(bytes,x=>x.toString(16).padStart(2,'0')).join('')})},true)};box.append(node('h3',action),node('p','Configured cabinet action','small muted'),b);return box}));if(!e.children.length)e.append(node('p','No actions configured.','empty'))});cached('output',[selected,c?.commands],e=>{const list=(c?.commands||[]).filter(x=>['reward','output'].includes(x.type)).slice().reverse();e.replaceChildren(...list.map(x=>node('p',(x.action_id||x.type)+' · '+(x.source==='operator'?'Operator':'Scenario / win output')+' · '+x.status+(x.status==='accepted'?' — physical delivery unconfirmed':''))));if(!list.length)e.append(node('p','No hardware commands yet.','muted'))});cached('roomDevices',state.catalog.layout?.devices,e=>e.replaceChildren(...(state.catalog.layout?.devices||[]).map(d=>{const n=node('div','','panel');n.append(node('strong',d.name),node('p',d.id+' · feedback unavailable','small muted'));return n})))}
|
||||
function activity(){const id=$('activityCabinet').value,q=$('activitySearch').value.toLowerCase();const list=allEvents().filter(e=>(!id||e.id===id)&&(!q||(e.message+' '+name(e.id)).toLowerCase().includes(q)));$('eventCount').textContent=list.length+' events';events('events',list)}
|
||||
function render(){if(!state?.cabinets)return;if(!state.cabinets[selected])selected=Object.keys(state.cabinets)[0];const hub=state.hub||{name:'Escape Gallery',room_name:'Glitch: the Dark Machine',services:[]};state.hub=hub;$('siteName').textContent=hub.name;for(const id of ['sidebarRoom','roomName','roomHeading'])$(id).textContent=hub.room_name;document.title=hub.name+' · Operations';options('scenario',Object.entries(state.catalog.scenarios).map(([id,s])=>[id,s.name||id]));options('hardwareCabinet',Object.keys(state.cabinets).map(id=>[id,name(id)]));options('activityCabinet',[['','All cabinets'],...Object.keys(state.cabinets).map(id=>[id,name(id)])]);if(view==='home')home();if(view==='room'){scenario();map();cards('roomCards')}if(view==='cabinets'){cards('cards');detail()}if(view==='hardware')hardwareView();if(view==='activity')activity();enable()}
|
||||
$('login').onsubmit=e=>e.preventDefault();$('connect').onclick=()=>{token=$('token').value.trim();if(token)refresh()};$('token').onkeydown=e=>{if(e.key==='Enter')$('connect').click()};$('refresh').onclick=refresh;document.querySelectorAll('.lock').forEach(b=>b.onclick=()=>{++epoch;token='';state=null;connected=false;$('token').value='';$('shell').hidden=true;$('login').hidden=false;$('confirmDialog').close();$('token').focus()});document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>navigate(b.dataset.view));document.querySelectorAll('[data-go]').forEach(b=>b.onclick=()=>navigate(b.dataset.go));$('scenario').onchange=()=>{scenario();enable()};$('game').onchange=enable;$('hardwareCabinet').onchange=()=>select($('hardwareCabinet').value,'hardware');$('activityCabinet').onchange=activity;$('activitySearch').oninput=activity;$('openHardware').onclick=()=>navigate('hardware');$('start').onclick=()=>command('start',{cabinet:selected,expected_session:state.cabinets[selected].session});$('reset').onclick=()=>{const id=selected,session=state.cabinets[id].session;review('Reset '+name(id)+'?','This ends its current game and clears its activation dependency.',()=>command('reset',{cabinet:id,expected_session:session}))};$('prepare').onclick=()=>{const id=selected,session=state.cabinets[id].session,game=$('game').value;review('Prepare '+gameName(game)+'?',name(id)+' will receive a manual assignment without scenario hardware rewards.',()=>command('prepare',{cabinet:id,expected_session:session,game}))};$('prepareScenario').onclick=()=>{const scenario=$('scenario').value;review('Prepare this scenario?','Assignments will be sent to its cabinets. Configured activation outputs may run. Linked cabinets wait for the preceding win.',()=>command('prepare_scenario',{scenario}))};$('cancelConfirm').onclick=()=>{$('confirmDialog').close();confirmation=null};$('confirmForm').onsubmit=e=>{e.preventDefault();if(!connected||pending)return;const reason=$('reason').value.trim();if(!$('confirmReason').hidden&&reason.length<3){$('reason').setCustomValidity('Enter at least three characters.');$('reason').reportValidity();return}const fn=confirmation;confirmation=null;$('confirmDialog').close();fn?.(reason)};$('reason').oninput=()=>$('reason').setCustomValidity('');navigate('home');setInterval(refresh,1500);
|
||||
$('login').onsubmit=e=>e.preventDefault();$('connect').onclick=()=>{token=$('token').value.trim();if(token)refresh()};$('token').onkeydown=e=>{if(e.key==='Enter')$('connect').click()};$('refresh').onclick=refresh;document.querySelectorAll('.lock').forEach(b=>b.onclick=()=>{++epoch;token='';state=null;connected=false;$('token').value='';$('shell').hidden=true;$('login').hidden=false;$('confirmDialog').close();$('token').focus()});document.querySelectorAll('[data-view]').forEach(b=>b.onclick=()=>navigate(b.dataset.view));document.querySelectorAll('[data-go]').forEach(b=>b.onclick=()=>navigate(b.dataset.go));$('scenario').onchange=()=>{scenario();enable()};$('game').onchange=enable;$('hardwareCabinet').onchange=()=>select($('hardwareCabinet').value,'hardware');$('activityCabinet').onchange=activity;$('activitySearch').oninput=activity;$('openHardware').onclick=()=>navigate('hardware');$('powerOn').onclick=()=>command('power_on',{cabinet:selected,expected_session:state.cabinets[selected].session});$('start').onclick=()=>command('start',{cabinet:selected,expected_session:state.cabinets[selected].session});$('reset').onclick=()=>{const id=selected,session=state.cabinets[id].session;review('Reset '+name(id)+'?','This ends its current game and clears its activation dependency.',()=>command('reset',{cabinet:id,expected_session:session}))};$('prepare').onclick=()=>{const id=selected,session=state.cabinets[id].session,game=$('game').value;review('Prepare '+gameName(game)+'?',name(id)+' will receive a manual assignment without scenario hardware rewards.',()=>command('prepare',{cabinet:id,expected_session:session,game}))};$('prepareScenario').onclick=()=>{const scenario=$('scenario').value;review('Prepare this scenario?','Assignments will be sent to its cabinets. Configured activation outputs may run. Linked cabinets wait for the preceding win.',()=>command('prepare_scenario',{scenario}))};$('cancelConfirm').onclick=()=>{$('confirmDialog').close();confirmation=null};$('confirmForm').onsubmit=e=>{e.preventDefault();if(!connected||pending)return;const reason=$('reason').value.trim();if(!$('confirmReason').hidden&&reason.length<3){$('reason').setCustomValidity('Enter at least three characters.');$('reason').reportValidity();return}const fn=confirmation;confirmation=null;$('confirmDialog').close();fn?.(reason)};$('reason').oninput=()=>$('reason').setCustomValidity('');navigate('home');setInterval(refresh,1500);
|
||||
|
||||
const PACMAN_ART={"screen": "data:image/png;base64,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 truncated
|
||||
function renderCabinetArt(c){document.querySelector('.cabinet-visual').dataset.game=c.game;const active=c.online&&c.phase==='playing';cached('marquee',[c.game],e=>{e.replaceChildren();if(c.game==='pacman'){const img=node('img');img.src=PACMAN_ART.marquee;img.alt='Pac-Man marquee';e.append(img)}else e.textContent=gameName(c.game)});cached('screenGame',[c.game,c.phase,c.online],e=>{e.replaceChildren();if(active&&c.game==='pacman'){const img=node('img');img.src=PACMAN_ART.screen;img.alt='Pac-Man gameplay illustration (not live video)';e.append(img)}else e.textContent=!c.online?'Cabinet offline':active?gameName(c.game):['won','lost'].includes(c.phase)?phases[c.phase]:'Waiting for operator'});}
|
||||
function renderCabinetArt(c){document.querySelector('.cabinet-visual').dataset.game=c.game;const active=c.online&&c.phase==='playing';cached('marquee',[c.game],e=>{e.replaceChildren();if(c.game==='pacman'){const img=node('img');img.src=PACMAN_ART.marquee;img.alt='Pac-Man marquee';e.append(img)}else e.textContent=gameName(c.game)});cached('screenGame',[c.game,c.phase,c.online],e=>{e.replaceChildren();if(active&&c.game==='pacman'){const img=node('img');img.src=PACMAN_ART.screen;img.alt='Pac-Man gameplay illustration (not live video)';e.append(img)}else e.textContent=!c.online?'Cabinet offline':active?gameName(c.game):['won','lost','powered_off','booting'].includes(c.phase)?phases[c.phase]:c.phase==='ready'&&c.game_config?.rfid_credit_device?'Scan card, then Start':'Waiting for operator'});}
|
||||
function renderFeedback(target='feedback'){const c=state.cabinets[selected];const f=c?.feedback;cached(target,[selected,f,c?.online],e=>{e.replaceChildren();if(!f?.devices?.length){e.append(node('p',f?.error||'No configured device feedback received.','muted'));return}for(const d of f.devices){const box=node('div','','feedback-item'+(d.value===true?' active':'')+(!c.online||!f.fresh||d.age_ms>6000?' stale':''));box.append(node('strong',d.name||d.id),node('p',(d.port||'')+' · '+d.kind+' · '+(!c.online||!f.fresh||d.age_ms>6000?'STALE / unavailable':d.error||(typeof d.value==='boolean'?(d.value?'Active':'Inactive'):String(d.value))),'small'));if(d.kind==='output')box.append(node('small','Electrical output state; physical movement is unconfirmed.'));e.append(box)}})}
|
||||
</script></body></html>
|
||||
@@ -157,6 +157,11 @@ class Room:
|
||||
self.state.update(session=str(uuid.uuid4()), phase="preparing" if action == "prepare" else "resetting",
|
||||
reward="not requested", commands=[], seen=[])
|
||||
self.command(action)
|
||||
elif action == "power_on":
|
||||
if self.state["game"] != "pacman" or "pacman_power_v1" not in self.features or self.state["phase"] != "powered_off":
|
||||
raise ValueError("Power on requires a prepared, powered-off Pac-Man cabinet (2.4+)")
|
||||
self.state["phase"] = "booting"
|
||||
self.command("power_on")
|
||||
elif action == "start":
|
||||
if self.state.get("game_config", {}).get("rfid_credit_device"):
|
||||
raise ValueError("Scan a card and press Start on the cabinet for this RFID scenario")
|
||||
@@ -214,9 +219,13 @@ class Room:
|
||||
self.state["reward"] = "GPIO accepted (physical delivery unconfirmed)"
|
||||
else:
|
||||
self.state["reward"] = "requested"
|
||||
elif command["type"] == "power_on":
|
||||
if not success: self.state["phase"] = "needs_prepare"
|
||||
elif success:
|
||||
self.state["phase"] = {"prepare": "ready", "reset": "waiting" if self.state.get("gate") else "idle", "start": "playing"}[command["type"]]
|
||||
if command["type"] == "prepare" and self.state.pop("auto_start", False):
|
||||
if command["type"] == "prepare" and self.state["game"] == "pacman" and "pacman_power_v1" in self.features:
|
||||
self.state["phase"] = "powered_off" if self.state.get("game_config", {}).get("power_on_device") else "booting"
|
||||
if command["type"] == "prepare" and self.state["phase"] == "ready" and self.state.pop("auto_start", False):
|
||||
self.state["phase"] = "starting"
|
||||
self.command("start")
|
||||
if command["type"] == "prepare":
|
||||
@@ -232,7 +241,14 @@ class Room:
|
||||
self.state["seen"].append(event_id)
|
||||
self.state["seen"] = self.state["seen"][-500:]
|
||||
name = report.get("event")
|
||||
if name == "started" and self.state["phase"] in ("ready", "starting"):
|
||||
if name == "power_booting" and self.state["game"] == "pacman" and self.state["phase"] == "powered_off":
|
||||
self.state["phase"] = "booting"
|
||||
elif name == "power_ready" and self.state["game"] == "pacman" and self.state["phase"] == "booting":
|
||||
self.state["phase"] = "ready"
|
||||
if self.state.pop("auto_start", False):
|
||||
self.state["phase"] = "starting"
|
||||
self.command("start")
|
||||
elif name == "started" and self.state["phase"] in ("ready", "starting"):
|
||||
self.state["phase"] = "playing"
|
||||
elif name == "won" and self.state["phase"] == "playing":
|
||||
self.state["phase"] = "won"
|
||||
@@ -282,6 +298,18 @@ class Fleet:
|
||||
limits.update({"galaga": {"score_to_win": (0, 1000000), "stages_to_win": (0, 100)},
|
||||
"frogger": {"homes_to_win": (0, 5), "boards_to_win": (0, 99)},
|
||||
"pacman": {"boards_to_win": (0, 100), "score_to_win": (0, 1000000)}}.get(game, {}))
|
||||
power_device = settings.get("power_on_device")
|
||||
if power_device is not None:
|
||||
if game != "pacman" or not isinstance(power_device, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", power_device):
|
||||
raise ValueError("Invalid power-on input device")
|
||||
if not any(d.get("id") == power_device and d.get("kind") == "input" for d in self.catalog["cabinets"][cabinet].get("devices", [])):
|
||||
raise ValueError("Power-on device is missing from the cabinet PCB input inventory")
|
||||
if "pacman_power_v1" not in self.rooms[cabinet].features:
|
||||
raise ValueError("Update the game export: power-on inputs require arcade-cabinet-2.4")
|
||||
if self.rooms[cabinet].gpio != "real":
|
||||
raise ValueError("Power-on inputs require real GPIO feedback")
|
||||
if item.get("activate_after_win"):
|
||||
raise ValueError("Choose button power-on or automatic dependency activation")
|
||||
credit_device = settings.get("rfid_credit_device")
|
||||
if credit_device is not None:
|
||||
if game != "pacman" or not isinstance(credit_device, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", credit_device):
|
||||
@@ -299,7 +327,7 @@ class Fleet:
|
||||
if credit_device is None or not isinstance(uids, list) or len(uids) > 100 or any(not isinstance(uid, str) or not re.fullmatch(r"[0-9A-F]{8}", uid) for uid in uids):
|
||||
raise ValueError("RFID allowed UIDs must be a list of uppercase 8-digit hexadecimal IDs")
|
||||
for key, value in settings.items():
|
||||
if key in ("rfid_credit_device", "rfid_allowed_uids"): continue
|
||||
if key in ("rfid_credit_device", "rfid_allowed_uids", "power_on_device"): continue
|
||||
if key not in limits or type(value) not in (int, float) or not math.isfinite(value):
|
||||
raise ValueError("Unsupported game objective: " + str(key))
|
||||
low, high = limits[key]
|
||||
|
||||
Reference in new issue
Block a user