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:
Jeff Luke committed 2026-10-17 14:53:54 -07:00
1 parent 705ec31e0b
commit c1b2195834
90 files changed
+4969 -456

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
# Cabinet PCB connections
Open a cabinet resource in Godot, for example `data/cabinets/cabinet_01.tres`. With the Escape Room editor plugin enabled, expand **PCB Connections** and click **Set up PCB connections…**.
1. Click a connector on the green board. The arrangement matches the supplied custom PCB diagram.
2. Choose the device type: RFID reader, button/switch, digital sensor, LED/lighting driver, motorized switch/motor driver, relay/MOSFET, I2C peripheral, or Other. Choices depend on the connector family.
3. Enter a friendly name and a device ID unique within this cabinet. Use an output's existing HardwareAction ID, or an input's intended event ID, in the optional binding field. Add driver, power, I2C address and wiring details in Notes.
4. Click **Apply connections**, then save the cabinet resource using Godot's resource save command. Apply participates in the Inspector's Undo/Redo. Cancel discards the dialog draft.
5. Reopen to review the saved connections. Choose **Empty / unplugged** to clear a slot.
Every cabinet starts unassigned. The tester's two RFID readers would be entered separately in **J1** and **J2**; these are not automatically added to every cabinet. A cabinet's new `pcb_config` is embedded in its resource by the visual editor. Editing through this dialog duplicates the draft, so another cabinet sharing the original resource is not changed.
## Fixed board mapping
| Connectors | Signal assignment | Contacts |
| --- | --- | --- |
| J15, J14, J13, J12, J11, J10 | BCM21, 26, 19, 13, 6, 5 | 1 GND black; 2 3V3 red; 3 signal yellow |
| J22, J21, J20, J19, J18, J17 | BCM25, 18, 23, 24, 12, 16 | 1 GND black; 2 signal red |
| J16 | I2C1 | 1 GND black; 2 3V3 red; 3 SCL white; 4 SDA yellow |
| J1–J8 | Selector 000–111 | Bits BCM22 / BCM27 / BCM17; reset BCM20 |
| J9 | Pi GPIO ribbon | Check physical pin-1 orientation |
J1–J8 reader contact pinouts have not been supplied; the diagram shows the verified selection bits, not an invented contact assignment. Device types describe the attached interface/driver. Motors and lighting loads are not powered directly from GPIO.
## Validation and runtime scope
The editor reports duplicate device IDs/connectors, unsupported connector/type combinations, missing output actions, GPIO/action mismatches, and existing actions using reserved selector/reset BCM17/27/22/20. Warnings remain visible but do not prevent saving an incomplete wiring plan. Existing cabinet actions using BCM17 may therefore need a separate wiring review before this PCB is installed.
The saved inventory can now be exported for optional live RFID, digital input, and electrical output feedback. Input connections include an explicit pull-up setting. Saving the diagram alone does not enable GPIO or remap HardwareActions. Follow the [live feedback setup](../tools/pi_gpio/README.md#pcb-live-feedback) to configure the bridge and reader helper. I2C and Other entries remain inventory-only; sensor events do not automatically complete puzzle steps.
## Verification
From the project root:
```text
Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://tests/pcb/check_pcb.gd
Godot_v4.7.2-stable_win64_console.exe --headless --editor --path . --import --quit
```
The resource tests cover connector mapping, validation, save/reload and deep-copy isolation. Adding `--editor` to the test command also exercises the inspector's draft, cancel/reopen and Apply behavior; Godot may report editor shutdown/scan warnings for that isolated editor harness.
+94
View File
@@ -0,0 +1,94 @@
# Master Pi installation and cabinet provisioning
Status: target architecture. A first terminal-wizard implementation now exists; see [the box-test guide](../tools/deploy/README.md). SSH staging/activation and configuration generation are implemented. Browser provisioning, automatic network isolation and arbitrary runtime cabinet profiles remain future work.
## Intended installer experience
1. Install the Arcade master package on a Raspberry Pi running 64-bit Raspberry Pi OS. The master does not need the Godot editor or a display to serve the operator website.
2. Run the local first-time setup wizard. Select the venue-facing interface and the private cabinet interface; review the network plan before applying it.
3. Add each cabinet: display name, stable cabinet ID, private IP or DHCP reservation, SSH username/port, SSH authentication, hardware profile, and GPIO settings. Prefer reserving addresses by Ethernet MAC rather than relying on hostname discovery.
4. Test access and verify the SSH host fingerprint. Use the existing login once to authorize a dedicated master deployment key; do not retain login passwords. Reuse an existing deployment key on reruns.
5. Select an ARM64 release bundle and review the proposed target/configuration changes. Deploy to selected cabinets, with per-cabinet progress and errors.
6. Verify the installed release, desktop session, local GPIO bridge availability if enabled, authenticated controller heartbeat, and expected protocol/features. Then open the normal operator page. Do not automatically start a puzzle or trigger hardware outputs during installation.
7. Later, use the same deployment inventory to update, replace, or add cabinets without repeating the room configuration.
The initial setup should run locally on the master, with privilege elevation confined to OS installation/network tasks. A browser setup interface should be loopback-only initially and must not expose an unauthenticated remote deployment endpoint. The normal operator page remains available on the venue LAN using its operator credential.
## Recommended network
```text
Venue LAN / staff Wi-Fi
| computers and iPads
| HTTP(S) operator page
Master Pi venue interface (Wi-Fi, or a second Ethernet adapter)
Master Pi private Ethernet interface
|
Private Ethernet switch -- cabinet_01
-- cabinet_02
-- cabinet_03
```
A separate Ethernet switch is the simplest cabinet network. Do not connect that switch to the venue router. Disable cabinet Wi-Fi connections to the venue network after commissioning. The master needs two interfaces: built-in Ethernet plus venue Wi-Fi, or two Ethernet interfaces when wired access is preferred.
The private network uses a subnet that does not overlap the venue LAN, VPNs, or existing routes. Example only: master 192.168.77.1/24, cabinets 192.168.77.11 onward. The wizard must check overlap instead of hard-coding this subnet for every site.
Provide DHCP reservations on the private interface, scoped only to that interface. Do not advertise a default router to cabinets. The master terminates connections on both networks; it does not bridge or forward traffic between them. Disable forwarding and enforce an IPv4/IPv6 forward-drop firewall policy for this boundary. Do not use NetworkManager shared mode as the isolation mechanism: shared mode enables forwarding. Cabinets receive bundles from the master over SSH, not through an Internet route.
This isolates cabinets from the venue LAN; cabinets on the same switch can still communicate with each other. Port-isolated switching or VLANs would be needed if cabinet-to-cabinet isolation is also required.
Allow venue staff to reach only the master's operator web port (and explicitly enabled management access). Keep the cabinet GPIO bridge bound to 127.0.0.1:8765. Cabinet polling reaches the master's private address on controller port 8766. Use a stable venue address/DHCP reservation or site DNS name for the operator bookmark. No public Internet port forwarding is needed. Venue guest-network isolation can still prevent staff devices from reaching the master and must be addressed by the venue network administrator.
Changing venue Wi-Fi/address should affect only the master connection and the staff bookmark; cabinet addresses, identities, tokens, and controller URL remain stable on the private side. If the new venue subnet overlaps, a coordinated private-network migration is required.
## Cabinet prerequisites
- Raspberry Pi OS 64-bit with a desktop, SSH enabled, and the intended login already created.
- An active desktop session with desktop autologin for unattended game startup. The SSH account and game desktop account should be the same.
- Python 3 and OpenSSH, normally available on the prepared OS image.
- GPIO Zero/lgpio packages and GPIO group access for cabinets that use real outputs.
No additional persistent management client is necessary. SSH can upload an installer and the release, register the existing user service and desktop helper, and start the application. Current pi_service.py cannot be called unchanged from a plain SSH session: it requires DISPLAY or WAYLAND_DISPLAY. Add a remote-install mode that registers the helper without pretending it has captured a desktop environment. Report waiting-for-desktop-login until the helper records the actual session. Never hard-code DISPLAY=:0 or guess Wayland socket names. A first login/reboot may be required after initial registration; do not reboot or log out a live cabinet without an explicit maintenance action.
For the least repeated setup, prepare one documented cabinet OS baseline with SSH, desktop autologin, and GPIO packages before moving onto the closed network. An offline deployment bundle must include matching OS/architecture package dependencies if these are not preinstalled; a disconnected cabinet cannot simply apt install missing packages. The game can be installed remotely without a Godot editor.
## Current code that can be reused
- tools/room_controller/server.py: Python standard-library controller, SQLite state, browser operator, per-cabinet authentication and protocol checks.
- tools/launch/launch.py: configuration generation target, process supervision, GPIO dependency checks and managed-game startup.
- tools/launch/remote_cabinets.py: strict-host-key SSH starts and separate ONLINE confirmation. An already-running service is not restarted.
- tools/launch/pi_service.py: non-root cabinet user service, display helper and systemd load-state verification.
- tools/pi_gpio/pi_gpio_bridge.py: local-only hardware bridge and durable request ledger.
Current limitation: core/network/managed_cabinet.gd looks up the cabinet ID in the exported ProjectDatabase; the server independently looks it up in catalog.json. Registering a new ID only in INI files is insufficient. The initial installer can safely bind machines to IDs/profiles already included in a release. To support arbitrary new IDs and hardware selections without rebuilding, add a validated external deployment profile consumed by both the Godot runtime and controller. It must include compatible input capabilities and the intended hardware action/controller mappings. The wizard must not silently advertise capabilities that the game export lacks.
## Configuration ownership
Maintain one private master inventory, separate from the authored game/scenario catalog. It records site network settings, cabinet addresses, SSH users/key references and verified fingerprints, cabinet profile selections, installed versions, and deployment status. Stable cabinet IDs survive IP changes and hardware replacement.
Generate the controller INI, per-cabinet INIs, and any required Godot CFG files from that inventory. Generate distinct operator/cabinet tokens for a new site; reuse them on rerun unless explicitly rotating them. Cabinets receive only their own token. Never package the existing development site's live INIs, tokens, database, SSH keys, or Odin folder into a distributable release.
Launcher INI values override the network environment supplied to the game. Keep any generated CFG consistent for direct-launch fallback, and account for existing user:// configuration precedence. Do not overwrite control mappings, player/operator state, or GPIO request ledgers as part of an application update.
Install the master controller as its own service so it survives browser closure and master reboot. Staff browsers connect to the server; they do not run the game export. The existing service binds one address, so exposure to both networks must be planned through binding/firewall rules or a reverse proxy. HTTPS can be added at the venue-facing proxy using a certificate trusted by staff devices; do not claim the current HTTP server already provides TLS.
## Release and deployment requirements
- Build one explicit ARM64 release bundle containing the executable/PCK, required Python scripts, operator assets, catalog, release metadata and SHA-256 checksums. Checksums verify transfer integrity; authenticated SSH establishes the source/channel.
- Keep credentials and persistent state outside versioned release folders.
- Validate IDs, IPs, duplicate targets, catalog compatibility, free space, CPU/OS, dependencies and desktop readiness before activation.
- Copy into staging, verify checksums, and reject unsafe archive paths/symlinks before extracting. Use a versioned release directory and an atomic current-release switch.
- Do not stop a running game to upload a bundle. Activation is a separate maintenance action. Preserve the prior release/config for rollback and avoid database downgrades that are not supported by its schema.
- Resume/report per cabinet after network failure; never mark SSH upload success as successful game startup.
- Confirm process/service health and the authenticated cabinet heartbeat independently. Preserve all GPIO deduplication state; neither retries nor rollback should replay a reward.
- Back up the master inventory, controller database and deployment key securely so a replacement master can recover the site. Losing these should not require reflashing every cabinet.
## Implementation order and acceptance checks
1. Release packaging, inventory schema, validation and deterministic INI/CFG generation; unit tests for identity/token separation and exclusion of live site data.
2. Remote preflight, SSH enrollment, staged upload, remote-safe display-helper/service installation and health reporting; test on one spare Pi before fleet rollout.
3. Local first-run wizard and master controller service; test repeat setup, interrupted upload, unavailable cabinet and reboot recovery.
4. Private-network configuration with backup/recovery instructions; verify from a venue device that the operator works but direct cabinet access does not, and from a cabinet that the venue LAN/Internet is not routed.
5. Runtime cabinet-profile support for arbitrary IDs, then integration tests for game compatibility, physical controller mappings and GPIO allowlists.
6. Move the master to a different non-overlapping venue network and verify cabinets keep the same private configuration and still complete the three-cabinet scenario.
References: [Raspberry Pi SSH setup](https://www.raspberrypi.com/documentation/computers/remote-access.html), [NetworkManager connection settings and shared-mode forwarding](https://networkmanager.pages.freedesktop.org/NetworkManager/NetworkManager/nm-settings-nmcli.html).
+542
View File
@@ -0,0 +1,542 @@
# Cabinet 1: Pac-Man RFID credit — clean setup and physical test
This guide sets up **Glitch: the Dark Machine** with a master Pi hosting the operator webpage and Cabinet 1 running Pac-Man with the custom GPIO PCB. Follow the steps in order. Commands marked **PC**, **master**, or **cabinet** run on that machine. Replace `YOUR_USER` with the actual Pi login.
The supplied scenario is **Cabinet 1: Pac-Man RFID credit** (`pacman_rfid_credit_test`). It prepares Pac-Man with CREDIT 0. A new accepted scan on **J1 / cab1_rfid1** plays `games/pacman/audio/credit.wav` and changes the display to CREDIT 1. The player presses their mapped Start button, consuming the credit. It wins after one cleared board. It has no automatic win timer and no relay/motor rewards.
The first test accepts any readable card. Credit is capped at one; scans during play do not bank credits. After a win/loss, reset and prepare the scenario again. The ordinary free-start Pac-Man assignment remains available separately.
## 1. Machines, cards, and wiring
Use Raspberry Pi OS **64-bit Desktop** on Cabinet 1. The master may use Desktop or Lite. In Raspberry Pi Imager, give them different hostnames (for example `glitch-master` and `glitch-cabinet1`), set their login details, and enable SSH. Enable desktop autologin on the cabinet so its game has a graphical session. Godot itself is not installed on either Pi.
For this baseline use the master as both the network boundary and operator server:
```text
Venue LAN / staff computers and tablets
|
master Wi-Fi (venue IP)
Master Pi: operator server on TCP 8766
master Ethernet: 192.168.77.1
|
Private Ethernet switch
|
Cabinet 1: 192.168.77.11
GPIO PCB + RFID readers on J1 / J2
```
The private switch has no direct connection to the venue router. Cabinet Wi-Fi is disabled after downloading dependencies. The master does not route or bridge traffic between the networks. Browsers communicate with the master, and cabinets communicate with that same master on its private address. This setup uses static addresses, so no DHCP server is required on the private network.
If you keep a separate existing network/router Pi, do not apply these network instructions on top of its unknown configuration. It needs its own subnet/firewall plan. The clean two-Pi arrangement above replaces that role for this test and leaves the original Odin installation untouched.
Power off before moving the PCB ribbon or reader connections. Use your documented connector orientation. J1 is selector slot 0, J2 slot 1. RFID owns BCM17/27/22 (selection), BCM20 (reset), and SPI0 BCM8/9/10/11. **Remove the old BCM17 LED test from this setup; do not run its output configuration.** This test enables no general output pins.
Cards do not need a game written onto them: this implementation reads their identifier. The MFRC522 supports ISO/IEC 14443-A / MIFARE / NTAG cards; an unspecified RFID card is not automatically compatible (for example 125 kHz cards will not work). Start by reading one. The helper currently follows Odin's **32-bit identifier representation**; it is not full-length UID support for every card, a balance stored on the card, or an anti-cloning/payment system.
## 2. Install prerequisites while Internet access is available
**Master:**
```bash
sudo apt update
sudo apt install -y python3 openssh-client nftables
```
**Cabinet:**
```bash
sudo apt update
sudo apt install -y python3 python3-gpiozero python3-lgpio build-essential curl ca-certificates
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
cargo --version
sudo usermod -aG spi,gpio "$USER"
sudo raspi-config
```
Choose the default Rust installation. In raspi-config enable **Interface Options > SPI**, and desktop autologin if not already enabled. Reboot the cabinet and log into its desktop. Check:
```bash
ls -l /dev/spidev0.0
groups
```
The SPI device must exist and your user should have `spi` and `gpio` membership. Download/build the reader helper in step 5 before removing cabinet Internet access.
## 3. Export, package, and copy one complete release
This is the release-building part of the master walkthrough. Build everything from the same saved project before copying anything to a Pi. For the first RFID installation, use the manual configuration in steps 5–9 below; the fleet deployment wizard does not yet preserve all RFID settings (see 3.7).
### 3.1 Save and check the source project — Windows PC
Open `D:\Projects\escape-gallery\room_glitch` in the project's Godot version (currently **4.7.2**). Save scenes and resources before running command-line exports. Check:
- Cabinet 1 has ID `cabinet_01`, and its PCB identifies J1 as `cab1_rfid1` and J2 as `cab1_rfid2`.
- `data/project_database.tres` includes **Cabinet 1: Pac-Man RFID credit**. Its assignment uses Pac-Man and `rfid_credit_device = cab1_rfid1`.
- The assignment has no output mappings for this initial reader test. `rfid_allowed_uids = []` accepts any readable card.
- The Linux preset is **ARM64**, with **Embed PCK disabled**. The existing Windows Desktop preset is x86_64.
- Export templates matching the editor version are installed (Editor > Manage Export Templates). An editor executable alone is not sufficient to export.
Use a release folder **outside the Godot project**. Do not export back into the project or mix old `setup/pi` exports with current source. In each preset's Resources tab, exclude the existing `setup/*` folder from the game export: it contains old exported artifacts and copied tools that do not belong inside a game PCK. Keep the actual `core`, `data`, `games`, `ui`, and runtime resources included. Do not add INIs, private keys, or SQLite files to the export's non-resource include filter.
The runtime for this scenario must advertise **arcade-cabinet-2.3** and `rfid_credit_v1`. Python files or a fresh catalog cannot add that feature to an old game PCK.
### 3.2 Create a fresh release directory — Windows PowerShell
The example release name is `glitch-rfid-001`. Pick a new name for every build you intend to deploy. These commands are examples to run, not artifacts already built by this walkthrough.
```powershell
Set-Location 'D:\Projects\escape-gallery\room_glitch'
$releaseName = 'glitch-rfid-001'
$releaseDir = "D:\ArcadeReleases\$releaseName"
if (Test-Path -LiteralPath $releaseDir) { throw 'Choose a new release name; this directory already exists.' }
New-Item -ItemType Directory -Path "$releaseDir\pi", "$releaseDir\windows" | Out-Null
```
Use this same PowerShell session for the following commands so the variables remain available. If you return later, set them again to the intended existing release; do not rerun the directory-creation check.
### 3.3 Export the catalog and game — Windows PowerShell
First export the controller catalog from the saved resources:
```powershell
.\Godot_v4.7.2-stable_win64_console.exe --headless --path . --script res://tools/room_controller/export_catalog.gd
if ($LASTEXITCODE -ne 0) { throw 'Catalog export failed.' }
```
Then export the actual Pi game, using the preset name exactly as shown:
```powershell
.\Godot_v4.7.2-stable_win64_console.exe --headless --path . --export-release 'Linux' "$releaseDir\pi\ArcadeEscape.arm64"
if ($LASTEXITCODE -ne 0) { throw 'Pi export failed.' }
```
Check the export log for errors, not just whether an old file exists. The folder must contain both `ArcadeEscape.arm64` and `ArcadeEscape.pck`. Godot may also create `ArcadeEscape.sh`; the Python launcher runs the ARM64 executable directly, so that shell wrapper is not required by our deployment package.
Optionally export Windows for PC testing:
```powershell
.\Godot_v4.7.2-stable_win64_console.exe --headless --path . --export-release 'Windows Desktop' "$releaseDir\windows\ArcadeEscape.exe"
if ($LASTEXITCODE -ne 0) { throw 'Windows export failed.' }
```
Keep each platform's executable and accompanying PCK together. Do not swap the Windows and Pi PCKs between release folders. The Windows export can test games and controls with GPIO disabled; it cannot validate the physical SPI reader. A browser-only master does **not** need either Godot game export or the Godot editor.
If you prefer the Godot UI, use Project > Export > the corresponding preset > Export Project, disable Export With Debug, and use the same filenames and separate release folders above. Still run the catalog export before packaging.
### 3.4 Package the master software and cabinet bundle — Windows PowerShell
From the project root, run:
```powershell
py -3 tools/deploy/deploy.py master-package --output "$releaseDir\master"
if ($LASTEXITCODE -ne 0) { throw 'Master packaging failed.' }
py -3 tools/deploy/deploy.py bundle --export "$releaseDir\pi" --output "$releaseDir\$releaseName.tar.gz" --release $releaseName
if ($LASTEXITCODE -ne 0) { throw 'Cabinet packaging failed.' }
Get-FileHash -Algorithm SHA256 -LiteralPath "$releaseDir\$releaseName.tar.gz" | Format-List
```
The master directory and archive must not already exist. Do not edit project code, game resources, or the catalog between exporting and packaging. If something changes, build a new release from the beginning.
The packager uses a file allowlist. It checks the ARM64 executable header and PCK header and writes SHA-256 hashes into the cabinet manifest. It does **not** prove that the PCK was built from the current source; keeping the build steps together is essential.
Your deliverables are now:
| Item | Destination / purpose |
| --- | --- |
| `master/` | Master Pi: controller, browser operator, matching catalog, launch/deployment scripts. |
| `glitch-rfid-001.tar.gz` | Cabinet software: ARM64 game + PCK, bridge, helper source, launch scripts, catalog and manifest. |
| `windows/` | Optional Windows game test. Not needed on the master Pi. |
| Release notes and archive SHA-256 | Keep with your release records for verification and rollback. |
The packages intentionally omit live tokens/INIs, SSH keys, databases, Godot saved controls, and Odin. They also omit the **compiled RFID executable** and each cabinet's **devices.json**. Build/create those on the cabinet in step 5. Custom cabinet artwork is embedded in the packaged operator HTML; you do not need to copy the source PNG directory separately for this page.
Record the release name, editor version, source commit (if committed), any uncommitted source changes included, export date, and tests performed in a small release-notes file. Keep the original archive unchanged. Retain `Cargo.lock` produced by the successful Pi helper build with your records as well.
### 3.5 Copy to clean installations
Use a USB drive or SFTP/SCP. The target folder is `~/arcade` under the actual Pi user, not `/root/arcade`. This subsection is for **fresh installs**; do not extract over a running or previously configured installation. Updates are covered in 3.8.
**Master:** create `~/arcade` and copy the **contents** of the release's `master` folder into it. The result must be:
```text
~/arcade/tools/room_controller/server.py
~/arcade/tools/room_controller/operator.html
~/arcade/tools/room_controller/catalog.json
~/arcade/tools/launch/launch.py
~/arcade/tools/deploy/deploy.py
```
Other allowlisted files will also be present. Do not accidentally end up with `~/arcade/master/tools/...`; the remaining walkthrough assumes `~/arcade/tools/...`.
**Cabinet:** copy the archive into the cabinet user's home. Before extraction, compare its hash with the Windows result (letter case does not matter):
```bash
sha256sum ~/glitch-rfid-001.tar.gz
mkdir -p ~/arcade
tar -tzf ~/glitch-rfid-001.tar.gz
```
Confirm `~/arcade` is empty on this fresh installation, and that this is the archive you just built. Extract:
```bash
tar -xzf ~/glitch-rfid-001.tar.gz -C ~/arcade
chmod +x ~/arcade/ArcadeEscape.arm64
```
The resulting cabinet layout includes:
```text
~/arcade/manifest.json
~/arcade/ArcadeEscape.arm64
~/arcade/ArcadeEscape.pck
~/arcade/tools/launch/launch.py
~/arcade/tools/launch/pi_service.py
~/arcade/tools/pi_gpio/pi_gpio_bridge.py
~/arcade/tools/pi_gpio/device_feedback.py
~/arcade/tools/pi_gpio/rfid_helper/Cargo.toml
~/arcade/tools/pi_gpio/rfid_helper/src/main.rs
```
The extra controller files inside the cabinet bundle are harmless; do not start a second controller there. Continue with network setup in step 4, RFID build/devices.json in step 5, and the separate master/cabinet INIs in step 6. The `.sh` file, source repository, and Godot editor are not required on the cabinet.
### 3.6 Preflight before calling the release ready
After completing steps 4–6, validate each machine without starting it:
**Master:**
```bash
cd ~/arcade
python3 tools/launch/launch.py controller --config controller.ini --check
```
**Cabinet:**
```bash
cd ~/arcade
python3 tools/launch/launch.py cabinet --config cabinet.ini --check
```
These checks validate configuration/dependencies/port availability. They do not prove card communication, display rendering, audio, or successful gameplay. Stop any existing stack first if the check reports its ports are occupied.
Use step 7's physical test as the acceptance gate: correct version, fresh J1 readings, CREDIT 0 -> scan/sound -> CREDIT 1 -> physical Start -> CREDIT 0, and no unintended outputs. Verify again after the reboot/startup test in step 9. Only then label the release ready for the room.
### 3.7 Optional fleet deployment: current RFID limitation
For later multi-cabinet rollout, the generated master package includes `tools/deploy/deploy.py`; the [fleet deployment guide](../tools/deploy/README.md) explains setup, enrollment, preflight, stage, activate, and rollback. Staging verifies each manifest hash before accepting a release. It does not start a game by itself.
**The current fleet configuration generator is not yet a complete RFID installer.** It rejects real-GPIO cabinets with an empty output pin list and does not write `devices` or `rfid_helper`. Do not add a fake output pin to bypass that check, and do not run fleet activation over the working RFID installation expecting those settings to be preserved. For this RFID-only first test, follow the manual `~/arcade` installation and service steps in this walkthrough. The bundle remains useful for consistent, checked packaging even when copied manually.
The deployment wizard also uses its own site credentials/configuration directory. Do not mix its generated `controller.ini` with the manual one below or install two competing master services. Choose one configuration owner. Until RFID provisioning is supported by the generator, use this walkthrough's manual path for Cabinet 1.
### 3.8 Updating and rolling back the manual installation
Do updates between room sessions. Preserve the previous complete release and configuration, not just the executable.
1. Build a new versioned master package and cabinet archive on the PC using 3.1–3.4. Do not overwrite/reuse a release archive.
2. Back up the master's `controller.ini`, controller database/state directory, and any SSH/site credentials. Back up the cabinet's `cabinet.ini`, `devices.json`, working RFID binary/lockfile, and Godot control mappings. Keep the GPIO request ledger at `~/.local/state/arcade_gpio/requests.sqlite3`; do not erase it to retry an uncertain physical action.
3. Extract/copy the new software into a **new sibling directory**, such as `~/arcade-next`, on each machine. Build the helper there if its source/dependencies changed. Copy your existing site-specific INIs/devices.json into that new installation and inspect their paths. The guide's relative paths remain valid inside the new root; any custom absolute paths need adjustment.
4. Stop the old services with `systemctl --user stop arcade-master.service` on the master and `systemctl --user stop arcade-cabinet.service` on the cabinet, or Ctrl+C their manual launchers. Copy the stopped master's database into the new installation's matching state location before switching. Avoid opening the live database with two controllers.
5. Keep the old directory as `~/arcade-previous-RELEASE_NAME`, and move the new directory into `~/arcade`. Use previously unused backup names; do not merge two release directories. Because the service paths still point at `~/arcade`, the manual services in step 9 continue to use the intended location. Recheck config paths, helper permissions, and the same operator/cabinet credentials.
6. Start the master and cabinet and repeat the preflight/physical acceptance test. Re-prepare the scenario for the new session. Do not resume an uncertain old room state automatically.
7. If the release fails, stop both services, keep the failed installation separately for diagnosis, restore the previous full software/configuration/database set at `~/arcade`, and start again. Keep the GPIO deduplication ledger intact. Future database format changes may require release-specific migration/rollback steps; never point an older controller at an unreviewed newer database.
Do not overwrite saved input mappings or re-image the SD card just to update game code. The manual path does not provide atomic activation; the fleet tool's staged activation can replace this workflow once its RFID configuration support is completed.
## 4. Configure the private network
Do this from each Pi's **local terminal**, not an SSH connection that you are about to disconnect. These commands assume a clean Raspberry Pi OS installation with NetworkManager and `eth0` as the wired interface. Check first:
```bash
nmcli device status
nmcli connection show
```
Use the actual Ethernet connection name in place of `Wired connection 1` if different. Verify that `192.168.77.0/24` is not the venue subnet; if it overlaps, choose another private subnet and change every corresponding address in this guide.
**Master:**
```bash
sudo nmcli connection modify "Wired connection 1" ipv4.method manual ipv4.addresses 192.168.77.1/24 ipv4.gateway "" ipv4.dns "" ipv4.never-default yes ipv6.method disabled
sudo nmcli connection up "Wired connection 1"
```
Keep the master's Wi-Fi on the venue LAN, with its normal Internet route. Reserve that Wi-Fi address in the venue router so the operator bookmark stays stable. Do not choose NetworkManager's `shared` mode.
**Cabinet (after step 5 has downloaded the Rust dependencies):**
```bash
sudo nmcli connection modify "Wired connection 1" ipv4.method manual ipv4.addresses 192.168.77.11/24 ipv4.gateway "" ipv4.dns "" ipv4.never-default yes ipv6.method disabled
sudo nmcli connection up "Wired connection 1"
sudo nmcli radio wifi off
```
Cabinet 2 can later use `.12`, Cabinet 3 `.13`, etc. Leave gateway/DNS blank on all private wired connections. Verify from the master:
```bash
ping -c 3 192.168.77.11
ssh YOUR_CABINET_USER@192.168.77.11
```
### Enforce no forwarding on the clean master
These steps are for the fresh master described above, not an existing managed router/firewall. They do not restrict connections *to* the master, including SSH and the operator webpage; they block forwarding *through* it.
Create `/etc/sysctl.d/90-arcade-private.conf` with `sudo nano`:
```ini
net.ipv4.ip_forward=0
net.ipv6.conf.all.forwarding=0
```
Apply it:
```bash
sudo sysctl --system
```
On the fresh master, inspect `/etc/nftables.conf`. In its default `table inet filter`, change the `forward` chain's policy from `accept` to `drop`. Leave the input/output policies alone for this first test. If the file contains an existing custom policy, stop here and have it reviewed rather than replacing it. Validate and enable the policy:
```bash
sudo nft -c -f /etc/nftables.conf
sudo systemctl enable --now nftables
sudo systemctl restart nftables
sudo nft list ruleset
sysctl net.ipv4.ip_forward net.ipv6.conf.all.forwarding
```
Expected: forward policy `drop` and both forwarding values `0`. Do not bridge the two interfaces or set up Internet sharing. Do not add router port forwarding. Staff browsers will use `http://MASTER_VENUE_IP:8766`; cabinets use `http://192.168.77.1:8766`. The GPIO bridge stays local to the cabinet at `127.0.0.1:8765`.
## 5. Compile and test the RFID readers on the cabinet
Do this before disabling its Internet connection:
```bash
cd ~/arcade
chmod +x ArcadeEscape.arm64
cargo build --release --manifest-path tools/pi_gpio/rfid_helper/Cargo.toml
```
A successful build creates `tools/pi_gpio/rfid_helper/target/release/arcade-rfid`. The new helper has not yet been built or physically verified on our tester Pi; if compilation fails, retain the exact output for troubleshooting. Do not assume a successful Windows test proves hardware communication.
Stop the game service, GPIO bridge, and any Odin reader process before testing. On a fresh Pi there should be none. On an existing Arcade installation:
```bash
systemctl --user stop arcade-cabinet.service
```
Then:
```bash
cd ~/arcade
./tools/pi_gpio/rfid_helper/target/release/arcade-rfid 0,1
```
Present a card separately to J1 and J2. Success resembles:
```json
{"slot":0,"uid":"12AB34CD","error":""}
```
The example UID is not a real enrolled card. Write down the exact value reported for each of your cards. No card-writing/programming step is required. A blank `uid` with `No tag read (absent or unreadable)` is not an accepted scan. `Reader did not respond` indicates reader communication trouble, not simply an unprogrammed card. Press Ctrl+C before starting the bridge.
Create `~/arcade/devices.json` with this minimal RFID-only inventory:
```json
[
{"id":"cab1_rfid1","name":"Credit reader","port":"J1","kind":"rfid"},
{"id":"cab1_rfid2","name":"Second reader","port":"J2","kind":"rfid"}
]
```
J2 is visible for diagnostics but grants no credit in this scenario. This minimal inventory intentionally excludes existing buttons/outputs so the first test owns only the readers. Later you can extract the saved PCB catalog using `device_feedback.py` as documented in the GPIO guide.
## 6. Generate credentials and create the two INIs
**Master:** generate two different 16-character tokens (first operator, second cabinet):
```bash
python3 -c 'import secrets; print("Operator:", secrets.token_urlsafe(12)); print("Cabinet 1:", secrets.token_urlsafe(12))'
```
Create `~/arcade/controller.ini` on the master:
```ini
[controller]
bind = 0.0.0.0
port = 8766
catalog = tools/room_controller/catalog.json
database = state/arcade_room.sqlite3
operator_token = REPLACE_WITH_OPERATOR_TOKEN
cabinet_tokens = {"cabinet_01":"REPLACE_WITH_CABINET_TOKEN"}
```
Create `~/arcade/cabinet.ini` on Cabinet 1:
```ini
[cabinet]
id = cabinet_01
url = http://192.168.77.1:8766
token = REPLACE_WITH_CABINET_TOKEN
executable = ArcadeEscape.arm64
gpio = true
[gpio]
controller = cabinet_01_gpio
pins =
devices = devices.json
rfid_helper = tools/pi_gpio/rfid_helper/target/release/arcade-rfid
```
Use matching cabinet tokens on both sides; the operator token belongs only on the master and in the browser login. INI token values have **no surrounding quotes**; the cabinet_tokens JSON string values **do** have JSON double quotes. All relative file paths above are relative to the containing INI at `~/arcade`.
Leave `pins =` explicitly empty. Omitting it defaults to the old BCM17 output, which conflicts with RFID and will be rejected. The scenario has no output actions, so there is no reason to enable output pins for this test.
With this launcher approach, no hand-written `arcade_network.cfg` is needed: the launcher supplies the URL and token. Old environment variables can override INIs. In a reused terminal, clear them before launching:
```bash
unset ARCADE_ROOM_URL ARCADE_CABINET_TOKEN ARCADE_OPERATOR_TOKEN ARCADE_CABINET_TOKENS
```
Protect the INIs on their respective machines:
```bash
chmod 600 ~/arcade/controller.ini
```
On the cabinet use `chmod 600 ~/arcade/cabinet.ini` instead.
## 7. Start and run the test
**Master:**
```bash
cd ~/arcade
python3 tools/launch/launch.py controller --config controller.ini
```
**Cabinet, in a terminal on its desktop:**
```bash
cd ~/arcade
python3 tools/launch/launch.py cabinet --config cabinet.ini
```
The cabinet launcher starts both the local GPIO bridge/reader helper and the game. Do not also start the standalone reader or another bridge. In a staff browser open `http://MASTER_VENUE_IP:8766` and log in with the **operator** token.
1. Confirm Cabinet 1 is online, reports `arcade-cabinet-2.3`, and GPIO is real.
2. In Room control select **Cabinet 1: Pac-Man RFID credit** and prepare it. Reset an already active cabinet first if requested.
3. Wait for the game to show **SCAN CARD / CREDIT 0** and the operator Hardware view to receive fresh readings. Historical scans at preparation are deliberately ignored. Keep the card away until ready.
4. Press the cabinet Start button without a card: nothing should start.
5. Scan J1: hear the credit sound and see **CREDIT 1 / PRESS START**. Hold/re-scan the card: it must stay at one credit and not repeatedly sound for the same accepted credit.
6. Press the mapped Start button: Pac-Man starts and shows **CREDIT 0**. The operator changes to Playing. Operator Start is disabled for this scenario because the player must start it.
7. Scan during gameplay: no credit is banked. Clear one board to win. No physical reward is configured.
8. Reset/re-prepare to test again. Scanning J2 must never grant credit. Disconnecting the reader/bridge must not create credits; feedback becomes stale/error.
The game uses the `arcade_start` input mapping. If the physical button is wrong, use the launcher Controls setup to map it and save, then return to managed mode. Keyboard/controller mapping is independent of the RFID reader.
The operator's Pac-Man image is an illustration, not a streamed screen; CREDIT 0/1 is rendered on the actual cabinet display. The Hardware view shows reader feedback, not a claim that a credit was granted. The physical test above verifies both.
## 8. Optionally restrict accepted cards
After recording the helper's reported identifier, edit the scenario assignment's `game_config` in Godot:
```text
rfid_credit_device: cab1_rfid1
rfid_allowed_uids: ["12AB34CD", "5678ABCD"]
boards_to_win: 1
score_to_win: 0
```
Replace the examples with your actual uppercase eight-character values. An empty array accepts any readable card. Changing this list does not write to the card. Keep a separate list of card labels and identifiers for your staff.
Save, re-export the controller catalog, and copy the updated catalog to the master. Restart the controller between sessions and re-prepare the scenario. For changes only to this assignment's allowlist/objective, the compatible 2.3 cabinet runtime accepts the new configuration without another game export. Game code, cabinet profiles, and new assets still require exporting the game.
## 9. Enable startup after the manual test passes
**Cabinet, on its local desktop:**
```bash
cd ~/arcade
python3 tools/launch/pi_service.py install --config "$HOME/arcade/cabinet.ini"
systemctl --user start arcade-cabinet.service
```
Stop the manually launched cabinet stack with Ctrl+C before installing. The installer captures the current desktop environment. The service intentionally has no boot enable target because the display must be ready first. For automatic start after desktop login, create `~/arcade/start-cabinet.sh`:
```bash
#!/bin/sh
/usr/bin/python3 "$HOME/arcade/tools/launch/pi_service.py" refresh-session || exit 1
exec systemctl --user start arcade-cabinet.service
```
Run `chmod +x ~/arcade/start-cabinet.sh`. Create `~/.config/autostart/glitch-cabinet-start.desktop`:
```ini
[Desktop Entry]
Type=Application
Name=Start Glitch Cabinet
Exec=/home/YOUR_USER/arcade/start-cabinet.sh
Terminal=false
```
Replace `YOUR_USER` in the Exec path. This explicitly refreshes the display environment before starting the game. Desktop autologin must remain enabled. Do not use `sudo` for the game service.
**Master:** stop the manually launched controller, then create `~/.config/systemd/user/arcade-master.service` (create the directory first):
```ini
[Unit]
Description=Glitch room operator controller
[Service]
Type=simple
WorkingDirectory=%h/arcade
ExecStart=/usr/bin/python3 %h/arcade/tools/launch/launch.py controller --config %h/arcade/controller.ini
UnsetEnvironment=ARCADE_ROOM_URL ARCADE_CABINET_TOKEN ARCADE_OPERATOR_TOKEN ARCADE_CABINET_TOKENS
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
```
Then:
```bash
systemctl --user daemon-reload
systemctl --user enable --now arcade-master.service
sudo loginctl enable-linger "$USER"
```
After a reboot verify both services and the web page. Preparing a scenario remains an operator action; a cabinet reboot invalidates the old session and requires preparation again.
Logs:
```bash
journalctl --user -u arcade-master.service -n 80 --no-pager
journalctl --user -u arcade-cabinet.service -n 80 --no-pager
```
Run each command on the corresponding Pi. If startup is already managed by the deployment tools, use their service/config instead of installing a second master service.
## Troubleshooting and acceptance
| Symptom | Check |
| --- | --- |
| Build fails | Keep the first Rust compiler error; confirm Cargo installed and Internet available. |
| SPI device missing | Enable SPI, reboot, check `/dev/spidev0.0`. |
| GPIO/SPI permission denied | `spi`/`gpio` groups; log out/back in after adding membership. |
| Reader unresponsive | Ribbon orientation, reader wiring/power, correct J1/J2 slots, no competing Odin process. |
| Blank/unprogrammed cards not reading | Check card technology; no card data-writing is required by this test. |
| Pin conflict on startup | Explicit blank output `pins`; RFID reserves BCM17/27/22/20 and SPI0. |
| Scenario rejected | Updated master catalog, cabinet 2.3 export, real GPIO, matching `cab1_rfid1` inventory. |
| Live UID but no credit | Prepare this scenario, remove/re-present card after initial feedback, use J1, verify allowed UID. |
| Credit but no start | Verify `arcade_start` mapping and physical button. |
| Unauthorized | Operator token in browser; matching Cabinet 1 token on master/cabinet; check inherited environment. |
| Cabinet offline | Cabinet URL uses private master IP and `http://...:8766`; 8765 is only local GPIO. |
| Staff page inaccessible | Master venue address, venue client isolation/firewall, server bound to 0.0.0.0. |
Software regression tests cover credit gating, fresh versus historical/stale feedback, UID/reader filtering, duplicates, credit consumption, and non-RFID behavior. Physical reader/audio/controller/network behavior must still be checked on this installation. No Pi, GPIO, network, or live deployment was changed while preparing this guide.
References: [Raspberry Pi networking/configuration](https://www.raspberrypi.com/documentation/computers/configuration.html), [Rust installation](https://doc.rust-lang.org/book/ch01-01-installation.html), [NXP MFRC522 supported card families](https://www.nxp.com/docs/en/data-sheet/MFRC522.pdf), [project GPIO guide](../tools/pi_gpio/README.md), [fleet deployment guide](../tools/deploy/README.md).
+118
View File
@@ -0,0 +1,118 @@
# Start the Pi cabinets from the controller PC
After one-time setup, your usual command starts the Windows controller, opens the browser, and asks each configured Pi to start its cabinet service:
```powershell
.\Start-Arcade.ps1 -Mode controller -Browser
```
The Pi service runs the existing cabinet launcher, which starts its GPIO bridge when enabled and then the managed game. SSH disconnects after the start request; the service stays running on the Pi. Already-running services are not restarted. Starting the apps does not automatically prepare or start a scenario; continue doing that in the operator.
The Pis must be powered on, reachable, and logged into their graphical desktop. This is remote application startup, not remote power-on. Desktop autologin is useful for cabinets; configure it on each Pi if desired.
## 1. Install the cabinet service on each Pi
Copy the updated `tools/launch/` scripts to the Pi's deployment folder. Stop the manually running cabinet launcher with Ctrl+C first so it releases the GPIO bridge port.
From a terminal **on the Pi desktop**, as the normal account that runs the game (without sudo):
```sh
python3 tools/launch/pi_service.py install --config tools/launch/launcher.ini
```
Use your actual INI filename, such as `launcher-pi.ini`, if different. It must contain that Pi's executable path, cabinet ID, controller URL/token (or blank URL/token to use its existing Godot CFG), and GPIO settings. The installer records the absolute config path; rerun it if the deployment/config moves.
This creates:
- `~/.config/systemd/user/arcade-cabinet.service`: runs your existing managed launcher as the desktop user.
- `~/.config/autostart/arcade-cabinet-display.desktop`: refreshes display settings at each desktop login, without launching the game.
- A temporary display environment file under the user's runtime directory, refreshed immediately during installation.
The installer does not start the game, enable boot startup, configure SSH, or change OS autologin. It captures only display-related variables, not application tokens. The service clears inherited `ARCADE_*` credential/URL overrides, so keep its intended settings in the configured INI or Godot CFG rather than relying on a terminal's environment.
Test locally:
```sh
systemctl --user start arcade-cabinet.service
systemctl --user status arcade-cabinet.service
```
Confirm the game appears on the cabinet monitor and becomes ONLINE. To stop it:
```sh
systemctl --user stop arcade-cabinet.service
```
If display settings change, run `python3 tools/launch/pi_service.py refresh-session` from the current Pi desktop terminal before starting it again.
## 2. Set up SSH access from Windows
SSH must be enabled on each Pi. Use the same SSH username as the desktop account where you installed the service.
These are **SSH keys**, separate from the 16-character application tokens in the cabinet setup guide. The tokens cannot authenticate SSH.
Generate a dedicated key on the PC (do not overwrite an existing one):
```powershell
ssh-keygen -t ed25519 -f "$env:USERPROFILE\.ssh\arcade_cabinets" -C "arcade-controller"
```
If you use a passphrase, load the key into your SSH agent before launching the room. Automated startup uses batch mode and cannot ask for passwords or passphrases.
For each Pi, first connect interactively and verify its host fingerprint before trusting it:
```powershell
ssh PI_USER@PI_HOST
```
Log out with `exit`. To authorize the public key on that Pi, run from PowerShell (substitute its real user and host):
```powershell
Get-Content "$env:USERPROFILE\.ssh\arcade_cabinets.pub" | ssh PI_USER@PI_HOST 'umask 077; mkdir -p ~/.ssh; cat >> ~/.ssh/authorized_keys'
```
Only copy the `.pub` key; the private key stays on the PC. Test unattended access:
```powershell
ssh -i "$env:USERPROFILE\.ssh\arcade_cabinets" -o BatchMode=yes -o StrictHostKeyChecking=yes PI_USER@PI_HOST "systemctl --user status arcade-cabinet.service"
```
An inactive service may return a nonzero status; confirm SSH succeeded and systemd found the installed unit. The launcher keeps host-key checking enabled and never automatically accepts a new or changed host key.
## 3. Add the Pi list to the PC launcher INI
Merge this into the controller INI you actually launch with. Replace each host and username:
```ini
[remote]
enabled = true
cabinets = [{"id":"cabinet_01","host":"PI_1_HOST","user":"PI_USER","identity_file":"~/.ssh/arcade_cabinets"},{"id":"cabinet_02","host":"PI_2_HOST","user":"PI_USER","identity_file":"~/.ssh/arcade_cabinets"},{"id":"cabinet_03","host":"PI_3_HOST","user":"PI_USER","identity_file":"~/.ssh/arcade_cabinets"}]
```
`identity_file` is optional if your default SSH identity or agent is already configured. `port` is optional and defaults to 22. Each ID must exist in the controller catalog and its cabinet-token map. The Pi's own launcher must use the same cabinet ID; this list does not remotely rewrite its identity or credentials.
Validate, then launch:
```powershell
.\Start-Arcade.ps1 -Mode controller -Check
.\Start-Arcade.ps1 -Mode controller -Browser
```
Use `-Config tools/launch/launcher-pc.ini` if that is your actual config filename. `-Check` validates local settings only; it does not contact or start a Pi.
The controller starts first. The launcher sends the SSH requests concurrently, then checks controller heartbeats. It prints separate **SSH OK/FAILED** and **ONLINE/OFFLINE** results, so a successful service command is not mistaken for a working game connection. A failed/offline Pi leaves the controller running for troubleshooting.
## Operating behavior
- Starting the controller again leaves active Pi services running; it does not interrupt their games with `restart`.
- Closing the browser or stopping the PC controller leaves the Pi services running. They reconnect when the controller returns.
- Closing the Pi game with Escape stops that Pi's launcher/bridge. It will not restart automatically; the next controller launch can start it again.
- A crash is not automatically restarted, avoiding unexpected game resets during a room session.
- Use `ssh PI_USER@PI_HOST "systemctl --user stop arcade-cabinet.service"` for an intentional remote stop.
- Application logs remain in the Pi launcher's configured log folder. Service startup errors are available through `journalctl --user -u arcade-cabinet.service -n 60 --no-pager`.
- If the PC launches before the Pi desktop is ready, startup can fail. Wait for desktop login and launch the controller again, or issue the same `systemctl --user start` command over SSH. There is no automatic power-on or infinite retry loop.
- After a desktop restart/logout, confirm the display helper has run before starting games. If your desktop does not honor XDG autostart entries, run `pi_service.py refresh-session` locally and check the desktop's startup configuration.
The service can be removed by stopping it, removing the two managed files listed above, and running `systemctl --user daemon-reload`. Game resources, saved control mappings, tokens, and GPIO ledger are unaffected.
Implementation references: [OpenSSH client configuration](https://man.openbsd.org/ssh_config), [systemd execution environment](https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html), and [systemd desktop integration](https://github.com/systemd/systemd/blob/main/docs/DESKTOP_ENVIRONMENTS.md).
+92
View File
@@ -0,0 +1,92 @@
# Three-cabinet objective test
Choose **3-Cabinet Test: 5 seconds / 500 points / 1 frog** in the browser operator.
| Cabinet | Game | Starts when | Win objective |
| --- | --- | --- | --- |
| `cabinet_01` | Pac-Man | Operator or physical Start after preparation | Automatic win five seconds after Start |
| `cabinet_02` | Galaga | Cabinet 1 reports a win | Score reaches **500 or more** |
| `cabinet_03` | Frogger | Cabinet 2 reports a win | **One frog** reaches a valid home |
Cabinets 2 and 3 remain unloaded while waiting. They load and automatically start after their predecessor wins. The five-second clock begins with Cabinet 1's game-start event, including its intro; it does not count time spent waiting in READY. Galaga keeps its normal brief victory transition before reporting completion. Frogger reports completion after the home-arrival pause.
## What was configured
The resource is [three_cabinet_objective_test.tres](../data/scenarios/three_cabinet_objective_test.tres), registered in [project_database.tres](../data/project_database.tres).
Each `ScenarioAssignment` now has a `game_config` Dictionary, in addition to its cabinet, game, variant, dependency, and event mappings. These values are exported to the controller catalog, included in the prepare command, and passed to the game's `configure()` method. The editor's working copy preserves these dictionaries independently.
The assignments are:
```text
Cabinet 1 / Pac-Man
activate_after_win: empty
game_config:
test_auto_win_seconds: 5.0
boards_to_win: 0
score_to_win: 0
Cabinet 2 / Galaga
activate_after_win: cabinet_01
game_config:
score_to_win: 500
stages_to_win: 0
Cabinet 3 / Frogger
activate_after_win: cabinet_02
game_config:
homes_to_win: 1
boards_to_win: 0
```
Setting board/stage targets to zero disables those alternate win conditions for this scenario. Galaga already supported score targets. Frogger now supports the individual-home target; its default remains a full board. The managed agent's test timer is enabled only by `test_auto_win_seconds`; ordinary scenarios and manual assignments do not inherit it. Reset/unload cancels the timer.
Cabinet 3's profile now includes `joystick_4way`, alongside its existing controls, so Frogger passes compatibility checks. Map its actual joystick using Control Setup. This test has no hardware event mappings, so no GPIO reward or additional wiring is required.
## How to change it in Godot
1. Open `data/scenarios/three_cabinet_objective_test.tres` in the Inspector.
2. Expand **Assignments**, then the assignment to edit.
3. Choose its Cabinet and Game references. Keep Variant as `standard`.
4. Set **Activate After Win** to the preceding cabinet's ID (blank for Cabinet 1).
5. Expand **Game Config** and set the objective keys and numeric values shown above.
6. Save the resource. The sample is already registered in the project database; register any newly created scenario there too.
7. Regenerate the controller catalog from the project root:
```sh
godot --headless --path . --script tools/room_controller/export_catalog.gd
```
Supported network objective keys currently include `test_auto_win_seconds`; Pac-Man board/score targets; Galaga stage/score targets; and Frogger board/home targets. Invalid values and unsupported keys are rejected before scenario dispatch. Chain execution belongs to the managed controller; the editor's local simulation does not run this network handoff.
## Build and deployment checklist
**This update needs fresh game exports on all three Pis.** Agents now advertise `arcade-cabinet-2.1` and the `assignment_config_v1` feature. The controller rejects this objective scenario on older agents so a cabinet cannot silently ignore the five-second or one-frog objective.
1. In Godot, build **Windows Desktop** for PC testing and **Linux** (ARM64) for the Pis. Use the configured engine version and matching export templates. The current presets write one directory above the project; check the output paths before exporting.
2. Copy the new ARM64 executable and any accompanying export files to all three Pis. The same build serves every cabinet; identity and tokens remain machine-specific.
3. Update the Windows controller's `server.py`, `catalog.json`, and `operator.html` together from `tools/room_controller/`.
4. Preserve each machine's local `launcher.ini`, Godot CFG, input mappings, controller database, and GPIO ledger. Confirm its unique ID/token using the [cabinet setup guide](cabinet_setup.md).
5. Restart Windows in **controller-only** mode and restart each Pi cabinet. Do not run an additional PC game with an ID already used by a Pi.
No builds or live controller restarts are performed by editing these resources. The exported binaries must be rebuilt and copied before the hardware test.
## Run the test
1. Confirm Cabinets 1, 2, and 3 are ONLINE with agent version `arcade-cabinet-2.1`.
2. Reset any active sessions. Select **3-Cabinet Test: 5 seconds / 500 points / 1 frog** and prepare it.
3. Confirm Cabinet 1 is READY and Cabinets 2/3 are WAITING.
4. Start Cabinet 1. After roughly five seconds plus the next network poll, Cabinet 2 loads and starts Galaga.
5. Play Galaga until the score reaches at least 500. Cabinet 3 then loads and starts Frogger.
6. Get one frog into an open home. Cabinet 3 becomes WON, completing the test.
7. To repeat, reset all three and prepare the scenario again. Resetting or restarting an upstream cabinet invalidates its dependent sessions. A lost game does not unlock the next cabinet.
## Verification
The automated integration test runs three actual headless Godot clients against a temporary controller, with no real GPIO. Cabinet 1 uses the real five-second timer. Test-only code adds Galaga points at 499 and 500, then places Frogger at a valid home; it exercises normal score/home completion paths rather than directly forcing their wins.
```sh
python3 -m unittest discover -s tests/room_network -v
python3 tests/room_network/check_objective_integration.py /path/to/godot
godot --headless --path . --script tests/room_network/check_objective_defaults.gd
```