Operator hub - Introduce Escape Gallery branding and Glitch: the Dark Machine room views - Add cabinet illustrations, Pac-Man marquee/gameplay artwork, and waiting states - Add device feedback panels with RFID readings, input/output states, and stale status - Add logged operator hardware interventions with request deduplication Room and cabinet authoring - Add a visual custom-PCB connection editor with per-cabinet device inventory - Support device IDs, bindings, wiring notes, input polarity, and validation - Add physical-room label/dimension toggles and editable doorway annotations - Preserve doorway changes through undo, save, and revert - Expand cabinet profiles, room layout, and Glitch scenario resources Scenarios and gameplay - Add per-assignment game configuration and runtime feature validation - Add timed test wins and Frogger individual-home objectives - Add the three-cabinet timed/score/Frogger test scenario - Add Cabinet 1 Pac-Man RFID credits, credit sound, and CREDIT 0/1 display - Require physical Start after an accepted scan and prevent duplicate credits Pi hardware and deployment - Add GPIO feedback sampling and a separate multiplexed RFID helper - Validate reader pin ownership and input/output conflicts - Add SSH cabinet startup and Pi desktop-session service support - Add master packaging, site setup, SSH enrollment, and staged cabinet releases - Verify deployment manifests and support explicit activation/status/log commands Documentation and verification - Consolidate the root README around the current architecture and setup workflow - Add the complete master/Pi/RFID export and deployment walkthrough - Document network isolation, credentials, card identification, and rollback - Add PCB, room-editor, feedback, credit, deployment, and controller tests - Isolate protocol tests from designer-edited cabinet configurations Known limitations - RFID helper compilation and physical Pi commissioning remain unverified - Fleet provisioning does not yet configure RFID-only installations completely - Broader Pac-Man tests retain a ghost-capture assertion failure - Staged exports must be rebuilt and checked before deploying runtime 2.3
115 lines
6.3 KiB
Python
115 lines
6.3 KiB
Python
"""Read-only feedback inventory; output writes remain in pi_gpio_bridge.Outputs."""
|
|
import json
|
|
import re
|
|
import subprocess
|
|
import threading
|
|
import time
|
|
import uuid
|
|
from collections import deque
|
|
from pathlib import Path
|
|
|
|
SIGNALS = dict(zip(['J10','J11','J12','J13','J14','J15','J17','J18','J19','J20','J21','J22'], [5,6,13,19,26,21,16,12,24,23,18,25]))
|
|
RESERVED = {17,27,22,20,8,9,10,11}
|
|
|
|
def load_devices(path, output_pins):
|
|
data = json.loads(Path(path).read_text(encoding='utf-8-sig'))
|
|
if not isinstance(data, list) or len(data)>32: raise ValueError('Device config must be a list of up to 32 devices')
|
|
ids, ports, inputs, readers = set(), set(), set(), False
|
|
for d in data:
|
|
if not isinstance(d,dict) or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}',d.get('id','')): raise ValueError('Invalid device ID')
|
|
if d['id'] in ids or d.get('port') in ports: raise ValueError('Duplicate device ID or port')
|
|
ids.add(d['id']); ports.add(d.get('port'))
|
|
if not isinstance(d.get('name',''),str) or len(d.get('name',''))>100: raise ValueError('Invalid name')
|
|
kind, port = d.get('kind'), d.get('port')
|
|
if kind=='rfid':
|
|
if port not in ['J'+str(i) for i in range(1,9)]: raise ValueError('RFID requires J1-J8')
|
|
readers=True
|
|
elif kind in ('input','output'):
|
|
if port not in SIGNALS: raise ValueError('Digital devices require a signal connector')
|
|
d['pin']=SIGNALS[port]
|
|
if kind=='input':
|
|
if type(d.get('pull_up')) is not bool: raise ValueError('Inputs require explicit pull_up true/false')
|
|
inputs.add(d['pin'])
|
|
elif d['pin'] not in output_pins: raise ValueError('Output connector not enabled in output pin allowlist')
|
|
else: raise ValueError('Supported kinds: input, output, rfid')
|
|
if inputs & set(output_pins): raise ValueError('A GPIO cannot be both an input and an output')
|
|
if readers and RESERVED & (set(output_pins)|inputs): raise ValueError('RFID selector/reset/SPI pins cannot be configured as general GPIO')
|
|
return data
|
|
|
|
class Feedback:
|
|
def __init__(self, definitions, input_factory, outputs, reader_binary=None, clock=time.monotonic):
|
|
self.definitions, self.outputs, self.clock = definitions, outputs, clock
|
|
self.lock=threading.RLock(); self.stop=threading.Event(); self.values={}; self.events=deque(maxlen=64)
|
|
self.boot=uuid.uuid4().hex; self.sequence=0; self.inputs=[]; self.worker=None; self.reader=None
|
|
try:
|
|
for d in definitions:
|
|
self.values[d['id']] = dict(id=d['id'],name=d.get('name',d['id']),port=d['port'],kind=d['kind'],value=None,error='Awaiting first sample',stamp=clock())
|
|
if d['kind']=='input': self.inputs.append((d,input_factory(d['pin'],pull_up=d['pull_up'],bounce_time=.05)))
|
|
readers=[d for d in definitions if d['kind']=='rfid']
|
|
if readers:
|
|
if not reader_binary: raise ValueError('RFID devices require --rfid-helper executable')
|
|
self.reader=subprocess.Popen([str(Path(reader_binary).resolve()),','.join(str(int(d['port'][1:])-1) for d in readers)],stdout=subprocess.PIPE,stderr=None,text=True,bufsize=1)
|
|
self.worker=threading.Thread(target=self._readers,args=(readers,),daemon=True); self.worker.start()
|
|
except Exception:
|
|
self.close(); raise
|
|
def update(self, ident, value, error=''):
|
|
with self.lock:
|
|
old=self.values[ident]
|
|
changed=old['value']!=value or old['error']!=error
|
|
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']))
|
|
def _readers(self, readers):
|
|
slots={int(d['port'][1:])-1:d['id'] for d in readers}
|
|
try:
|
|
for line in self.reader.stdout:
|
|
if self.stop.is_set(): break
|
|
if len(line)>2048: continue
|
|
value=json.loads(line)
|
|
ident=slots.get(value.get('slot'))
|
|
if ident:
|
|
self.update(ident,str(value.get('uid',''))[:32],str(value.get('error',''))[:120])
|
|
except Exception as exc:
|
|
for ident in slots.values(): self.update(ident,None,'Reader helper error: '+str(exc)[:80])
|
|
finally:
|
|
if not self.stop.is_set():
|
|
for ident in slots.values(): self.update(ident,None,'Reader helper stopped')
|
|
def tick(self):
|
|
for d,device in self.inputs:
|
|
try: self.update(d['id'],bool(device.value))
|
|
except Exception as exc: self.update(d['id'],None,str(exc)[:120])
|
|
with self.outputs.lock:
|
|
for d in self.definitions:
|
|
if d['kind']!='output': continue
|
|
entry=self.outputs.pins.get(d['pin'])
|
|
try: self.update(d['id'],bool(entry['device'].value) if entry else None,'' if entry else 'Output not initialized')
|
|
except Exception as exc: self.update(d['id'],None,str(exc)[:120])
|
|
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))
|
|
def close(self):
|
|
self.stop.set()
|
|
if self.reader:
|
|
self.reader.terminate()
|
|
try:self.reader.wait(timeout=2)
|
|
except subprocess.TimeoutExpired:self.reader.kill();self.reader.wait()
|
|
if self.worker:self.worker.join(timeout=2)
|
|
self.reader.stdout.close()
|
|
for _,device in self.inputs: device.close()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import argparse
|
|
parser=argparse.ArgumentParser(description='Extract saved PCB feedback inventory from the exported room catalog')
|
|
parser.add_argument('--catalog',required=True)
|
|
parser.add_argument('--cabinet',required=True)
|
|
parser.add_argument('--output',required=True)
|
|
args=parser.parse_args()
|
|
catalog=json.loads(Path(args.catalog).read_text(encoding='utf-8-sig'))
|
|
devices=catalog['cabinets'][args.cabinet].get('devices',[])
|
|
with Path(args.output).open('x',encoding='utf-8') as file:
|
|
json.dump(devices,file,indent=2)
|
|
print('Wrote',len(devices),'device definitions. Review input polarity and output allowlist before running the bridge.')
|