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,137 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Install a user service from a Pi desktop terminal; no root or SSH changes."""
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
HERE = Path(__file__).resolve().parent
|
||||
MARKER = '# Managed by Arcade Escape pi_service.py'
|
||||
|
||||
|
||||
def quoted(value):
|
||||
value=str(value)
|
||||
if any(c in value for c in ('\n','\r','\0')): raise ValueError('Paths must not contain control characters')
|
||||
return '"'+value.replace('\\','\\\\').replace('"','\\"').replace('%','%%')+'"'
|
||||
|
||||
|
||||
def working_directory(value):
|
||||
# Unlike ExecStart arguments, systemd reads this as a single unquoted path.
|
||||
value = str(value)
|
||||
if any(ord(c) < 32 or ord(c) == 127 for c in value):
|
||||
raise ValueError('Paths must not contain control characters')
|
||||
return value.replace('%', '%%')
|
||||
|
||||
|
||||
def reload_service():
|
||||
subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True)
|
||||
result = subprocess.run(
|
||||
['systemctl', '--user', 'show', 'arcade-cabinet.service',
|
||||
'--property=LoadState', '--value'],
|
||||
check=True, capture_output=True, text=True)
|
||||
if result.stdout.strip() != 'loaded':
|
||||
raise ValueError(
|
||||
'systemd rejected arcade-cabinet.service (LoadState: '
|
||||
+ result.stdout.strip() + '). Inspect it with: '
|
||||
'systemctl --user status arcade-cabinet.service --no-pager -l')
|
||||
|
||||
|
||||
def render_unit(python, launcher, config):
|
||||
return f"""{MARKER}
|
||||
[Unit]
|
||||
Description=Arcade Escape managed cabinet
|
||||
PartOf=graphical-session.target
|
||||
|
||||
[Service]
|
||||
Type=exec
|
||||
EnvironmentFile=%t/arcade-cabinet-display.env
|
||||
UnsetEnvironment=ARCADE_ROOM_URL ARCADE_CABINET_TOKEN ARCADE_OPERATOR_TOKEN ARCADE_CABINET_TOKENS
|
||||
WorkingDirectory={working_directory(Path(launcher).resolve().parents[2])}
|
||||
ExecStart=:{quoted(python)} {quoted(launcher)} cabinet --config {quoted(config)}
|
||||
Restart=no
|
||||
KillMode=mixed
|
||||
TimeoutStopSec=15
|
||||
"""
|
||||
|
||||
|
||||
def render_desktop(python, script):
|
||||
# Desktop Exec quoting also needs escaping of $ and backticks inside quotes.
|
||||
def desktop_quote(value):
|
||||
return quoted(value).replace('$','\\$').replace('`','\\`')
|
||||
return f"""{MARKER}
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Arcade cabinet display setup
|
||||
Exec={desktop_quote(python)} {desktop_quote(script)} refresh-session
|
||||
Terminal=false
|
||||
NoDisplay=true
|
||||
"""
|
||||
|
||||
|
||||
def refresh_session():
|
||||
if not os.environ.get('DISPLAY') and not os.environ.get('WAYLAND_DISPLAY'):
|
||||
raise ValueError('Run this from a terminal on the Pi desktop, not a plain SSH session')
|
||||
runtime=Path(os.environ.get('XDG_RUNTIME_DIR',f'/run/user/{os.getuid()}'))
|
||||
if not runtime.is_dir(): raise ValueError('The Pi desktop user runtime directory is unavailable')
|
||||
values={k:os.environ[k] for k in ('DISPLAY','WAYLAND_DISPLAY','XAUTHORITY','XDG_RUNTIME_DIR','XDG_SESSION_TYPE') if os.environ.get(k)}
|
||||
text=''
|
||||
for key,value in values.items():
|
||||
if any(c in value for c in ('\n','\r','\0')): raise ValueError('Invalid desktop environment value')
|
||||
text+=key+'="'+value.replace('\\','\\\\').replace('"','\\"')+'"\n'
|
||||
target=runtime/'arcade-cabinet-display.env'
|
||||
temporary=target.with_suffix('.tmp')
|
||||
temporary.write_text(text,encoding='utf-8')
|
||||
temporary.chmod(0o600)
|
||||
temporary.replace(target)
|
||||
print('Saved display environment for the current desktop login.')
|
||||
|
||||
|
||||
def write_managed(path, text):
|
||||
if path.exists() and not path.read_text(encoding='utf-8').startswith(MARKER):
|
||||
raise ValueError('Refusing to overwrite an unrelated file: '+str(path))
|
||||
path.parent.mkdir(parents=True,exist_ok=True)
|
||||
path.write_text(text,encoding='utf-8')
|
||||
|
||||
|
||||
def main():
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('action',choices=['install','refresh-session'])
|
||||
parser.add_argument('--config',default=str(HERE/'launcher.ini'))
|
||||
parser.add_argument('--remote', action='store_true', help='Register from SSH; wait for the desktop helper if no display is available')
|
||||
args=parser.parse_args()
|
||||
try:
|
||||
if not sys.platform.startswith('linux') or os.getuid()==0:
|
||||
raise ValueError('Run as the normal Pi desktop user on Linux, without sudo')
|
||||
if args.action=='refresh-session':
|
||||
refresh_session()
|
||||
return 0
|
||||
from launch import plan
|
||||
config=Path(args.config).resolve()
|
||||
# Validate against the user's actual config; do not start games or GPIO here.
|
||||
plan(config,'cabinet')
|
||||
if not args.remote or os.environ.get('DISPLAY') or os.environ.get('WAYLAND_DISPLAY'):
|
||||
refresh_session()
|
||||
else:
|
||||
print('Remote installation: display helper will capture settings at the next desktop login.')
|
||||
home=Path.home()/'.config'
|
||||
unit=home/'systemd/user/arcade-cabinet.service'
|
||||
desktop=home/'autostart/arcade-cabinet-display.desktop'
|
||||
unit_text=render_unit(sys.executable,HERE/'launch.py',config)
|
||||
desktop_text=render_desktop(sys.executable,Path(__file__).resolve())
|
||||
for target in (unit,desktop):
|
||||
if target.exists() and not target.read_text(encoding='utf-8').startswith(MARKER):
|
||||
raise ValueError('Refusing to overwrite an unrelated file: '+str(target))
|
||||
write_managed(unit,unit_text)
|
||||
write_managed(desktop,desktop_text)
|
||||
reload_service()
|
||||
print('Installed arcade-cabinet.service (not started and not enabled at boot).')
|
||||
print('Start locally: systemctl --user start arcade-cabinet.service')
|
||||
print('The display helper will refresh settings at future desktop logins.')
|
||||
return 0
|
||||
except (ValueError,OSError,subprocess.CalledProcessError) as exc:
|
||||
print('Pi service setup failed: '+str(exc),file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__=='__main__': sys.exit(main())
|
||||
Reference in new issue
Block a user