Files
room_glitch/tools/launch/pi_service.py
T
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

138 lines
5.6 KiB
Python

#!/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())