Files
Jeff Luke c1b2195834 Expand Glitch operations hub, cabinet authoring, deployment, and RFID support
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
2026-10-17 14:53:54 -07:00

74 lines
3.9 KiB
Python

"""Explicit SSH service starts; no remote shell interpolation or automatic restarts."""
import concurrent.futures
import configparser
import json
import os
from pathlib import Path
import re
import shutil
import subprocess
import time
import urllib.request
def load_targets(config_path, mode):
if mode not in ('controller', 'all'): return []
config=configparser.ConfigParser(interpolation=None)
config.read(config_path, encoding='utf-8-sig')
if not config.getboolean('remote', 'enabled', fallback=False): return []
raw=json.loads(config.get('remote', 'cabinets', fallback='[]'))
if not isinstance(raw,list) or not raw: raise ValueError('remote.cabinets must be a nonempty JSON list')
ssh=shutil.which('ssh')
if not ssh: raise ValueError('OpenSSH client (ssh) is required for remote cabinet startup')
targets=[]
seen=set()
for item in raw:
if not isinstance(item,dict): raise ValueError('Each remote cabinet must be an object')
cabinet,host,user=(item.get(k,'') for k in ('id','host','user'))
for value,pattern,label in [(cabinet,r'[A-Za-z0-9_]+','cabinet ID'),(host,r'[A-Za-z0-9][A-Za-z0-9.-]*','host'),(user,r'[A-Za-z_][A-Za-z0-9_-]*','SSH user')]:
if not isinstance(value,str) or not re.fullmatch(pattern,value): raise ValueError('Invalid remote '+label)
if cabinet in seen: raise ValueError('Duplicate remote cabinet ID')
seen.add(cabinet)
port=item.get('port',22)
if type(port) is not int or not 1<=port<=65535: raise ValueError('Invalid SSH port')
command=[ssh,'-T','-o','BatchMode=yes','-o','StrictHostKeyChecking=yes','-o','ConnectTimeout=5','-o','ConnectionAttempts=1','-p',str(port)]
if item.get('identity_file'):
key=(Path(config_path).resolve().parent / Path(item['identity_file']).expanduser()).resolve()
if not key.is_file(): raise ValueError('SSH identity file not found: '+str(key))
command+=['-i',str(key),'-o','IdentitiesOnly=yes']
command += [user+'@'+host,'systemctl','--user','start','arcade-cabinet.service']
targets.append(dict(id=cabinet,host=host,command=command))
return targets
def start_one(target):
try:
result=subprocess.run(target['command'],stdin=subprocess.DEVNULL,stdout=subprocess.PIPE,stderr=subprocess.PIPE,text=True,timeout=15,creationflags=subprocess.CREATE_NO_WINDOW if os.name=='nt' else 0)
if result.returncode:
return target['id'],False,(result.stderr or result.stdout).strip()[-1500:]
return target['id'],True,'Service start accepted (already-running services are left running)'
except (OSError,subprocess.TimeoutExpired) as exc:
return target['id'],False,str(exc)
def start_and_check(targets,controller):
if not targets: return
print('Starting configured Pi services over SSH...',flush=True)
with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
for cabinet,ok,detail in executor.map(start_one,targets):
print(f"{cabinet}: {'SSH OK' if ok else 'SSH FAILED'} - {detail}",flush=True)
pending={t['id'] for t in targets}
deadline=time.monotonic()+15
while pending and time.monotonic()<deadline:
try:
request=urllib.request.Request(controller['ready'].rstrip('/')+'/operator/state',data=b'{}',headers={'Authorization':'Bearer '+controller['env']['ARCADE_OPERATOR_TOKEN']})
with urllib.request.urlopen(request,timeout=2) as response: state=json.load(response)
for cabinet in list(pending):
if state.get('cabinets',{}).get(cabinet,{}).get('online'):
print(cabinet+': ONLINE in room controller',flush=True)
pending.remove(cabinet)
except (OSError,ValueError): pass
if pending: time.sleep(.5)
for cabinet in sorted(pending):
print(cabinet+': OFFLINE - check SSH, Pi desktop login, service logs, and cabinet URL/token. Controller remains running.',flush=True)