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
This commit is contained in:
1 parent
705ec31e0b
commit
c1b2195834
90 files changed
+4969
-456
No files matched your search
@@ -0,0 +1,73 @@
|
||||
"""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)
|
||||
Reference in new issue
Block a user