From c1b2195834f2bea1bab1847cde60bc90cce2c49d Mon Sep 17 00:00:00 2001 From: Jeff Luke Date: Sat, 17 Oct 2026 14:53:54 -0700 Subject: [PATCH] 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 --- README.md | 434 ++++++-------- addons/escape_room/escape_room_plugin.gd | 5 + addons/escape_room/pcb_inspector.gd | 13 + addons/escape_room/pcb_inspector.gd.uid | 1 + core/hardware/cabinet_pcb_config.gd | 92 +++ core/hardware/cabinet_pcb_config.gd.uid | 1 + core/hardware/cabinet_profile.gd | 4 + core/hardware/pcb_connection.gd | 12 + core/hardware/pcb_connection.gd.uid | 1 + core/network/managed_cabinet.gd | 23 +- core/rooms/room_wall_definition.gd | 3 + core/runtime/pi_gpio_client.gd | 18 + core/scenarios/scenario_assignment.gd | 2 + core/scenarios/scenario_edit_session.gd | 2 + core/simulation/room_edit_session.gd | 2 + data/cabinets/cabinet_01.tres | 55 +- data/cabinets/cabinet_01_hardware.tres | 10 +- data/cabinets/cabinet_02.tres | 53 +- data/cabinets/cabinet_02_hardware.tres | 16 +- data/cabinets/cabinet_03.tres | 13 +- data/cabinets/cabinet_03_hardware.tres | 15 +- data/cabinets/cabinet_04.tres | 20 + data/cabinets/cabinet_04_hardware.tres | 22 + data/project_database.tres | 14 +- data/rooms/arcade_room.tres | 18 +- data/scenarios/cabinet_chain_test.tres | 15 +- data/scenarios/eg_glitch.tres | 44 ++ data/scenarios/pacman_rfid_credit_test.tres | 18 + .../three_cabinet_objective_test.tres | 47 ++ docs/cabinet_pcb_setup.md | 40 ++ docs/master_pi_deployment_design.md | 94 +++ docs/pacman_rfid_first_setup.md | 542 ++++++++++++++++++ docs/remote_pi_start.md | 118 ++++ docs/three_cabinet_test.md | 92 +++ editor/hardware/pcb_editor_property.gd | 209 +++++++ editor/hardware/pcb_editor_property.gd.uid | 1 + editor/workspace/escape_room_workspace.gd | 81 ++- games/frogger/frogger.gd | 9 +- games/pacman/pacman.gd | 45 ++ games/pacman/pacman_view.gd | 7 +- setup/pc/ArcadeEscape.pck | Bin 5898380 -> 5925028 bytes setup/pi/ArcadeEscape.pck | Bin 5898380 -> 5925028 bytes ...cade_network.cfg => arcade_network_c1.cfg} | 0 setup/pi/arcade_network_c3.cfg | 4 + tests/gpio/test_feedback.py | 46 ++ tests/pcb/check_pcb.gd | 98 ++++ tests/pcb/check_pcb.gd.uid | 1 + tests/pcb/check_rfid_credit.gd | 42 ++ tests/room_editor/check_room_view.gd | 64 +++ tests/room_editor/check_room_view.gd.uid | 1 + tests/room_network/catalog_fixture.json | 440 ++++++++++++++ .../room_network/check_objective_defaults.gd | 52 ++ .../check_objective_defaults.gd.uid | 1 + .../check_objective_integration.py | 50 ++ tests/room_network/objective_agent.gd | 36 ++ tests/room_network/objective_agent.gd.uid | 1 + tests/room_network/test_deploy.py | 167 ++++++ tests/room_network/test_feedback.py | 24 + tests/room_network/test_fleet.py | 6 +- tests/room_network/test_hub.py | 54 ++ tests/room_network/test_objectives.py | 78 +++ tests/room_network/test_remote_start.py | 138 +++++ tools/deploy/.gitignore | 3 + tools/deploy/README.md | 134 +++++ tools/deploy/deploy.py | 313 ++++++++++ tools/deploy/receiver.py | 213 +++++++ tools/launch/README.md | 2 + tools/launch/controller.example.ini | 12 - tools/launch/launch.py | 36 +- tools/launch/launcher-pc.ini | 10 + tools/launch/launcher-pi.ini | 7 + tools/launch/pi_service.py | 137 +++++ tools/launch/remote.example.ini | 5 + tools/launch/remote_cabinets.py | 73 +++ tools/launch/windows-all.example.ini | 18 - tools/pi_gpio/README.md | 37 +- tools/pi_gpio/device_feedback.py | 114 ++++ tools/pi_gpio/pi_gpio_bridge.py | 19 +- tools/pi_gpio/rfid_helper/Cargo.toml | 10 + tools/pi_gpio/rfid_helper/src/main.rs | 43 ++ tools/room_controller/README.md | 37 ++ tools/room_controller/art/pacman_marquee.png | Bin 0 -> 248178 bytes .../art/pacman_marquee.png.import | 40 ++ tools/room_controller/art/pacman_screen.png | Bin 0 -> 6249 bytes .../art/pacman_screen.png.import | 40 ++ tools/room_controller/catalog.json | 267 +++++++-- tools/room_controller/export_catalog.gd | 3 +- tools/room_controller/operator.html | 70 ++- tools/room_controller/server.py | 178 +++++- ui/room_layout.gd | 90 ++- 90 files changed, 4969 insertions(+), 456 deletions(-) create mode 100644 addons/escape_room/pcb_inspector.gd create mode 100644 addons/escape_room/pcb_inspector.gd.uid create mode 100644 core/hardware/cabinet_pcb_config.gd create mode 100644 core/hardware/cabinet_pcb_config.gd.uid create mode 100644 core/hardware/pcb_connection.gd create mode 100644 core/hardware/pcb_connection.gd.uid create mode 100644 data/cabinets/cabinet_04.tres create mode 100644 data/cabinets/cabinet_04_hardware.tres create mode 100644 data/scenarios/eg_glitch.tres create mode 100644 data/scenarios/pacman_rfid_credit_test.tres create mode 100644 data/scenarios/three_cabinet_objective_test.tres create mode 100644 docs/cabinet_pcb_setup.md create mode 100644 docs/master_pi_deployment_design.md create mode 100644 docs/pacman_rfid_first_setup.md create mode 100644 docs/remote_pi_start.md create mode 100644 docs/three_cabinet_test.md create mode 100644 editor/hardware/pcb_editor_property.gd create mode 100644 editor/hardware/pcb_editor_property.gd.uid rename setup/pi/{arcade_network.cfg => arcade_network_c1.cfg} (100%) create mode 100644 setup/pi/arcade_network_c3.cfg create mode 100644 tests/gpio/test_feedback.py create mode 100644 tests/pcb/check_pcb.gd create mode 100644 tests/pcb/check_pcb.gd.uid create mode 100644 tests/pcb/check_rfid_credit.gd create mode 100644 tests/room_editor/check_room_view.gd create mode 100644 tests/room_editor/check_room_view.gd.uid create mode 100644 tests/room_network/catalog_fixture.json create mode 100644 tests/room_network/check_objective_defaults.gd create mode 100644 tests/room_network/check_objective_defaults.gd.uid create mode 100644 tests/room_network/check_objective_integration.py create mode 100644 tests/room_network/objective_agent.gd create mode 100644 tests/room_network/objective_agent.gd.uid create mode 100644 tests/room_network/test_deploy.py create mode 100644 tests/room_network/test_feedback.py create mode 100644 tests/room_network/test_hub.py create mode 100644 tests/room_network/test_objectives.py create mode 100644 tests/room_network/test_remote_start.py create mode 100644 tools/deploy/.gitignore create mode 100644 tools/deploy/README.md create mode 100644 tools/deploy/deploy.py create mode 100644 tools/deploy/receiver.py delete mode 100644 tools/launch/controller.example.ini create mode 100644 tools/launch/launcher-pc.ini create mode 100644 tools/launch/launcher-pi.ini create mode 100644 tools/launch/pi_service.py create mode 100644 tools/launch/remote.example.ini create mode 100644 tools/launch/remote_cabinets.py delete mode 100644 tools/launch/windows-all.example.ini create mode 100644 tools/pi_gpio/device_feedback.py create mode 100644 tools/pi_gpio/rfid_helper/Cargo.toml create mode 100644 tools/pi_gpio/rfid_helper/src/main.rs create mode 100644 tools/room_controller/art/pacman_marquee.png create mode 100644 tools/room_controller/art/pacman_marquee.png.import create mode 100644 tools/room_controller/art/pacman_screen.png create mode 100644 tools/room_controller/art/pacman_screen.png.import diff --git a/README.md b/README.md index 1b9ed53..0c69518 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,33 @@ -# Arcade Escape +# Escape Gallery — Glitch: the Dark Machine -**An arcade-powered escape room framework built with Godot.** +**A Godot arcade escape-room framework with a browser-based operations hub.** -Arcade Escape brings seven classic arcade recreations together with room-design tools, configurable cabinet controls, scenario-driven hardware outputs, and a browser operator dashboard. Designers define the room and its scenarios; Raspberry Pi cabinets run the games; staff prepare and monitor sessions from a computer or tablet. +Design rooms, configure cabinets and their connected devices, assign arcade games to scenarios, and manage play from a staff computer or tablet. Raspberry Pi cabinets run exported games; a Python controller coordinates their sessions and serves the **Escape Gallery** operator webpage. The first room is **Glitch: the Dark Machine**. -The Arcade Escape controller is separate from Odin and does not require its code or services. +The Arcade software is separate from Odin. Its RFID helper follows the tester PCB's selector arrangement and Odin's reader-library approach without requiring Odin's room services. + +## Start here + +**For a fresh installation, follow the [master walkthrough: Cabinet 1 Pac-Man RFID test](docs/pacman_rfid_first_setup.md).** It covers the entire process: PC exports, matching release packages, private cabinet LAN, Pi prerequisites, reader compilation, credentials, configuration, startup, and physical acceptance testing. + +| Task | Guide | +| --- | --- | +| Export and package a matching release | [Master walkthrough, section 3](docs/pacman_rfid_first_setup.md#3-export-package-and-copy-one-complete-release) | +| Run Cabinet 1 with RFID credits | [Full master walkthrough](docs/pacman_rfid_first_setup.md) | +| Configure cabinet controls | [Launcher and control setup](games/launcher/README.md) | +| Author the custom PCB wiring inventory | [Visual PCB setup](docs/cabinet_pcb_setup.md) | +| Configure GPIO outputs and device feedback | [Pi GPIO guide](tools/pi_gpio/README.md) | +| Run controller/cabinet launch scripts | [Launch tools](tools/launch/README.md) | +| Start Pi services remotely over SSH | [Remote cabinet startup](docs/remote_pi_start.md) | +| Stage and activate fleet releases | [Deployment tools and current limitations](tools/deploy/README.md) | +| Configure the operator/controller | [Room controller guide](tools/room_controller/README.md) | +| Test sequential cabinet objectives | [5 seconds → 500 points → 1 frog](docs/three_cabinet_test.md) | + +The fleet wizard is not yet a complete RFID installer: it does not generate the device/helper paths and rejects an empty output allowlist with real GPIO enabled. Use the master walkthrough's manual installation for the RFID-only test. Do not add an arbitrary output pin to bypass this limitation. ## Included games -| | Game | Cabinet controls | +| | Game | Controls | | --- | --- | --- | | ![Pac-Man](games/launcher/icons/ico_pacman.png) | **Pac-Man** | Four-way joystick + Start | | ![Galaga](games/launcher/icons/ico_galaga.png) | **Galaga** | Horizontal movement + Fire + Start | @@ -16,314 +35,191 @@ The Arcade Escape controller is separate from Odin and does not require its code | ![Missile Command](games/launcher/icons/ico_missilecommand.png) | **Missile Command** | Trackball + three battery buttons + Start | | ![Frogger](games/launcher/icons/ico_frogger.png) | **Frogger** | Four-way joystick + Start | | ![Space Invaders](games/launcher/icons/ico_spaceinvaders.png) | **Space Invaders** | Horizontal movement + Fire + Start | -| ![Robotron](games/launcher/icons/ico_robotron.png) | **Robotron 2084** | Two eight-way joysticks, or one joystick + four directional fire buttons, plus Start | +| ![Robotron](games/launcher/icons/ico_robotron.png) | **Robotron 2084** | Two eight-way joysticks, or movement joystick + four directional fire buttons, plus Start | -The games include attract/title screens, sound, and game-specific visual treatments. They are playable recreations under active development; arcade accuracy and individual mechanics continue to be refined. +These are playable recreations with title/attract screens, sound, and game-specific rendering. Arcade accuracy and mechanics continue to be refined. Pi 4 gameplay has been tested; verify controls, audio, performance, and hardware on each installation. -## Start here - -- **Start the services with one command:** [Windows and Pi launch scripts](tools/launch/README.md) -- **Play or configure controls:** [Launcher and cabinet controls](games/launcher/README.md) -- **Deploy the room and operator:** [Controller deployment guide](tools/room_controller/README.md) -- **Connect real Pi outputs:** [GPIO setup and LED test](tools/pi_gpio/README.md) - -## How it fits together +## System layout ```text -Godot development computer - Escape Room editor + game/cabinet/scenario resources - | | - | game export | exported catalog.json - v v -Raspberry Pi cabinets ------> Room controller on Mac mini / PC - games + saved controls Python + persistent SQLite state - | ^ - v | -Local GPIO bridge Browser operator on Mac / PC / iPad - | -Physical outputs +Development PC: Godot editor + saved resources + | ARM64 game/PCK + Python tools | matching catalog + operator assets + v v +Pi cabinets <--------------------> Master controller + | Python + SQLite + | / \ +Local GPIO bridge Private LAN Venue LAN + | | +PCB inputs / RFID / outputs Staff browsers ``` -| Component | Purpose | Runs on | +The master can run on a Pi, PC, Mac mini, or another suitable Python host. For the isolated Pi arrangement, its Ethernet connects to the private cabinet switch and its Wi-Fi or second Ethernet interface connects to the venue LAN. It hosts the operator on both networks without forwarding cabinet traffic to the venue. Network isolation requires the configuration in the walkthrough; the application does not configure routing/firewalls automatically. + +| Port | Service | Address | | --- | --- | --- | -| Godot project and Escape Room editor | Build games, place cabinets, and author scenarios | Development computer | -| Arcade Launcher | Select games and configure physical controls | Desktop or Pi cabinet | -| Managed cabinet client | Load assigned games and report session events | Each Pi cabinet | -| Room controller | Validate assignments, track cabinet state, and queue commands | Mac mini, PC, or suitable server | -| Browser operator | View the room, prepare scenarios, and start/reset cabinets | Staff computer or iPad | -| GPIO bridge | Drive configured local Pi outputs | Each Pi that controls hardware | +| **8766** | Controller API and operator webpage | Master private IP for cabinets; master venue IP for staff | +| **8765** | GPIO bridge | Cabinet-local `127.0.0.1` only | -## 1. Run the project locally +## Develop and play locally -1. Clone this repository and import **project.godot** into Godot. -2. Use the project's configured **Godot 4.7** version and **Compatibility** renderer. Allow the initial asset import to finish. -3. Run the project with **F5**, or open [ArcadeLauncher.tscn](games/launcher/ArcadeLauncher.tscn) and use **F6**. -4. Complete **Control Setup** if prompted, then select a game and press Start. +1. Import [project.godot](project.godot) into the project's Godot version, currently **4.7.2**, and let asset import complete. The project uses the Compatibility renderer. +2. Run the main project or [ArcadeLauncher.tscn](games/launcher/ArcadeLauncher.tscn). +3. Complete **Control Setup** and select a game. Review saved editor run arguments if the project starts in a test/managed mode instead. +4. Install matching Godot export templates before making Windows or Linux builds. -Godot's matching export templates are needed to create builds. Python 3 is needed for the room controller and GPIO bridge, but not for standalone desktop gameplay. +Standalone games do not require Python. The controller/launcher/bridge do. Pi hardware uses GPIO Zero/lgpio; the separate RFID helper is built with Rust on the Pi. -The launcher is the configured main scene. The project also has saved editor run arguments for a test scenario/cabinet; review **Project Settings → Editor → Run → Main Run Args** before testing another assignment. +- **Start:** select or start; **Select:** return to the standalone launcher. +- **F1:** reopen Control Setup; **Escape:** quit the application. +- Map each machine independently: Windows and Pi USB button indices can differ. +- Saved mappings live in `user://arcade_controls.cfg` and survive game-export replacement. -### Controls +## Author rooms, cabinets, and scenarios -- **Joystick:** navigate the launcher and move in games. -- **Start:** select an entry or begin play. -- **Select:** return to the launcher in standalone mode. -- **F1:** reopen Control Setup from the launcher. -- **Escape:** quit the application entirely. +Open the **Escape Room** main-screen editor tab. Its plugin is enabled in the project; check Project Settings > Plugins if the tab is missing. -Configure each machine independently. USB encoders can report different button numbers on Windows and Raspberry Pi OS. The setup wizard saves local mappings, including separate Robotron firing controls; a trackball should report as a mouse. - -Mappings live in `user://arcade_controls.cfg` and survive replacing the game export. See the [control guide](games/launcher/README.md) for keyboard defaults, multiple-controller behavior, and reconnect handling. - -## 2. Author a room and its scenarios - -The **Escape Room** editor plugin is enabled in this project. Open its main-screen tab in Godot; if it is missing, check **Project Settings → Plugins**. - -The existing resources provide a starting point: - -| Resource | What to configure | +| Resource | Purpose | | --- | --- | -| [Project database](data/project_database.tres) | Register cabinets, games, and scenarios | -| [Room layout](data/rooms/arcade_room.tres) | Boundary, walls, cabinet placements, and room devices | -| [Cabinet profiles](data/cabinets/) | Unique cabinet IDs, available controls, and hardware configurations | -| [Game definitions](data/games/) | Game scenes, control requirements, and exposed events | -| [Scenarios](data/scenarios/) | Cabinet/game assignments and event-to-hardware mappings | +| [Project database](data/project_database.tres) | Register games, cabinets, scenarios | +| [Physical room](data/rooms/arcade_room.tres) | Boundary, walls, doorways, placements, devices | +| [Cabinet profiles](data/cabinets/) | Unique IDs, control capabilities, hardware actions, PCB connections | +| [Game definitions](data/games/) | Scenes, required controls, game events | +| [Scenarios](data/scenarios/) | Assignments, dependency gates, per-game objectives, event/output mappings | -A typical authoring workflow: +Give each cabinet a unique ID, describe its actual controls, register it, and place it in the room. Assign compatible games to a scenario and map only intended events to configured hardware actions. Save all resources before exporting. **Capabilities** determine game compatibility; **input mappings** determine which physical controls activate game actions. -1. Create or duplicate a cabinet profile and give it a unique `cabinet_id`. -2. Describe its actual controls in `capabilities`, and configure its hardware actions with the correct controller ID and BCM pins. -3. Register the cabinet in the project database and place it in the room layout. -4. Create a scenario with one assignment per participating cabinet. Choose a compatible game and the `standard` variant. -5. Map game events to the cabinet's hardware action IDs. For example, `game_won` → `coin_release`. -6. Register and save the scenario. Use the editor's simulation tools before deploying real outputs. +Assignments support `game_config` overrides, including score/board objectives, Frogger home counts, and a test-only timed win. These settings survive scenario working-copy/save operations. The controller validates settings and cabinet feature support before preparation. Older example scenarios may require updating action IDs and capabilities to match the current designer-authored cabinet resources. -Designer-facing physical dimensions and positioning use feet; internal room geometry remains in meters. +### Physical room editor -**Capabilities and input mappings are separate.** Capabilities determine which games a cabinet can support; Control Setup determines which physical inputs activate their actions. The sample cabinet profiles describe limited controls, so update them to match your hardware before expecting every game to be available. +The **Physical** view provides Labels and Dimensions toggles to reduce clutter. Hidden labels do not prevent selecting objects; these toggles are editor-session view settings. -### Export the operator catalog +Use **DRAW DOORWAY** and drag from hinge to opposite jamb. Align it with a wall to display an opening, threshold, leaf, and swing arc. Select the doorway to edit its endpoints, convert wall/doorway type, or reverse its swing; use DELETE WALL to remove it. Edits use the room undo/save/revert workflow. Designer dimensions are in feet; stored geometry is in meters. -From the project root, with your Godot executable available as `godot`: +Doorways are layout annotations. They do not change game collisions or act as live door sensors. Browser room-map doorway rendering remains future work. + +### Visual PCB connections + +Open a cabinet profile's **PCB Connections** editor and select a connector to assign a device type, name, unique ID, binding, and wiring notes. Digital inputs expose their pull-up setting. The editor validates duplicate assignments and relevant pin/action conflicts, supports Undo/Redo, and saves the configuration with the cabinet resource. + +The board includes direct GPIO connectors, J16 I2C, and multiplexed J1–J8 reader connectors. See the [pin mapping guide](docs/cabinet_pcb_setup.md). Saving this inventory does not energize outputs or install drivers. Supported entries can be exported into the optional runtime feedback configuration; I2C and Other devices remain inventory-only. + +## Operator hub and managed play + +The browser hub provides an overview, **Glitch: the Dark Machine** room map, per-cabinet details, hardware controls, and activity history. It shows assigned games and reported cabinet phases. Pac-Man has a custom cabinet treatment with marquee and gameplay artwork; the screen says **Waiting for operator** before play. This artwork is an illustration, not live video. + +Prepare a scenario after its participating cabinets report online. Independent assignments can start through the operator or physical Start; dependency-gated assignments activate after their predecessor wins. RFID-credit assignments require a scan and physical Start. Reset before reassigning an active cabinet. + +Manual game assignments do not inherit scenario rewards. Operator hardware interventions require an online prepared cabinet, configured action, and a reason; requests are logged and deduplicated. An accepted output command is not proof of physical movement or delivery. + +Optional device panels show RFID identifiers, digital input activity, electrical output state, errors, and stale feedback. Activity records device changes. Generic sensor-driven puzzle rules and detailed live puzzle-step diagrams are not yet implemented; the Pac-Man RFID credit flow is the first specific input-driven scenario. + +### Pac-Man RFID credit scenario + +Select **Cabinet 1: Pac-Man RFID credit** (`pacman_rfid_credit_test`): + +1. Prepare Cabinet 1: Pac-Man displays **SCAN CARD / CREDIT 0**. +2. A new accepted scan on **J1 / cab1_rfid1** grants **CREDIT 1** and plays the credit sound. +3. The player presses mapped Start, consumes the credit, and begins play. +4. Clear one board to win; reset/re-prepare for another session. + +Credit is capped at one. Historical/stale samples, duplicate events, scans on J2, and scans during gameplay do not bank extra credits. The initial scenario accepts any compatible readable card; `rfid_allowed_uids` optionally restricts accepted identifiers. No card-writing step is required. No motor/relay reward is configured for this test. + +This needs **arcade-cabinet-2.3**, real GPIO feedback, the updated bridge, and the compiled RFID helper. The helper follows Odin's 32-bit identifier representation and is not full-length UID support for every card type. Compilation and physical reader testing on the tester Pi remain to be verified. + +## Configuration and credentials + +Prefer the launcher INIs shown in the master walkthrough. They start the required services and provide the managed game with its connection settings. + +| Role | Settings | +| --- | --- | +| Master controller | `[controller]` URL binding, catalog/database paths, operator token, cabinet-token map | +| Pi cabinet | `[cabinet]` own ID/token, master private URL, executable, real GPIO option | +| Local hardware bridge | `[gpio]` controller ID, output pin allowlist, device file, RFID helper path | +| Optional direct Godot client | `user://arcade_network.cfg`, `[network]`, role-specific token and controller URL | + +Browser staff use the **operator token**. Each cabinet uses its **own cabinet token**. These are application tokens, not SSH keys. Generate distinct values, for example: ```sh -godot --headless --path . --script tools/room_controller/export_catalog.gd -``` - -This generates [catalog.json](tools/room_controller/catalog.json) from the saved designer resources. The controller uses it for room positions, scenarios, and game compatibility. Regenerate it after authoring changes, restart the controller after copying it, and redeploy the game export when its packaged resources change. - -## 3. Export and run a Pi cabinet - -1. Install matching Godot export templates on the development computer. -2. Use the **Linux** export preset, configured for **ARM64**, for a 64-bit Raspberry Pi OS installation. The **Windows Desktop** preset is available for PC testing. -3. Copy the executable and any accompanying export files to the Pi. -4. Make the Linux executable runnable, then launch it normally to configure the controls: - -```sh -chmod +x ArcadeEscape.arm64 -./ArcadeEscape.arm64 -``` - -Use your actual exported filename if different. The Pi runs the exported game; it does not need the Godot editor. The project has been tested on a Pi 4; verify the target hardware, display resolution, and controls for each installation. - -For gameplay-only testing, no controller service or GPIO bridge is required. Real GPIO remains opt-in through `--gpio=real`. - -## 4. Start the room controller and browser operator - -Copy these three files together to the controller machine: - -- [server.py](tools/room_controller/server.py) -- [operator.html](tools/room_controller/operator.html) -- [catalog.json](tools/room_controller/catalog.json) - -The controller uses Python's standard library; it has no pip dependencies. Generate distinct random tokens of at least 16 characters for the operator and each cabinet. Replace the placeholders below with those values. - -**macOS / Linux**, from that controller folder: - -```sh -export ARCADE_OPERATOR_TOKEN='your-generated-operator-token' -export ARCADE_CABINET_TOKENS='{"cabinet_01":"your-generated-cabinet-one-token","cabinet_02":"your-generated-cabinet-two-token"}' -python3 server.py --bind 0.0.0.0 --database arcade_room.sqlite3 -``` - -**Windows PowerShell**, from the repository root: - -```powershell -$env:ARCADE_OPERATOR_TOKEN = 'your-generated-operator-token' -$env:ARCADE_CABINET_TOKENS = '{"cabinet_01":"your-generated-cabinet-one-token","cabinet_02":"your-generated-cabinet-two-token"}' -py -3 tools/room_controller/server.py --bind 0.0.0.0 --database arcade_room.sqlite3 -``` - -Keep the database at a stable location and back it up. Allow TCP **8766** through the controller machine's firewall for the room LAN. The assigned internal tokens are documented in the [cabinet setup guide](docs/cabinet_setup.md), as requested for this installation. Keep deployment databases out of source control. - -Open `http://CONTROLLER_IP:8766/` on the staff computer or iPad and enter the operator token. The iPad needs only a browser. Use a trusted isolated LAN; the default HTTP connection is not encrypted. See the [deployment guide](tools/room_controller/README.md) for network and credential details. - -## 5. Connect cabinets to the controller - -### PC versus Pi: which configuration file? - -`arcade_network.cfg` configures a **Godot client**, not the Python controller. Its contents depend on the role that Godot runs in, not just the operating system. - -| Machine / role | Where settings belong | Credentials | -| --- | --- | --- | -| PC running the Python controller + browser | `tools/launch/launcher.ini`, `[controller]` | `operator_token` and JSON `cabinet_tokens` for all cabinets | -| PC running the optional Godot `--operator` app | `user://arcade_network.cfg`, `[network]` | `url` and `operator_token` | -| Pi running a managed cabinet | `user://arcade_network.cfg`, `[network]` | `url`, its own `cabinet_id`, and its own `cabinet_token` | -| PC running a game as a test cabinet | Same cabinet fields as the Pi | That test cabinet's ID/token; do not duplicate an active Pi's ID | - -**For the normal browser-operator setup, the PC does not need an `arcade_network.cfg`.** Enter the operator token in the browser; the server takes its credentials from the launcher/environment. - -Example Pi Godot configuration: - -```ini -[network] -url="http://WINDOWS_PC_IP:8766" -cabinet_id="cabinet_01" -cabinet_token="CABINET_01_TOKEN" -``` - -Optional PC Godot operator configuration (only for `--operator`, with the controller on that same PC): - -```ini -[network] -url="http://127.0.0.1:8766" -operator_token="OPERATOR_TOKEN" -``` - -The Pi's URL must use the **PC's LAN IP**, never `127.0.0.1` unless the controller actually runs on that Pi. Both client roles use controller port **8766**. GPIO port **8765** is separate and stays local to the Pi. - -Godot CFG strings require double quotes. Launcher INI scalar values are unquoted; the `cabinet_tokens` JSON object still uses JSON quotes. - -When using the scripts, nonempty `[cabinet] url` and `token` values in `launcher.ini` override Godot's CFG. Existing `ARCADE_ROOM_URL` / `ARCADE_CABINET_TOKEN` environment values override those launcher values. Leave the INI URL/token blank to fall back to the CFG, and clear stale environment variables if needed. The scripts always pass their configured `[cabinet] id` (default `cabinet_01`) as `--cabinet`, overriding the CFG's ID: set the correct ID on every Pi even when using the CFG for its token. - -### Generate a 16-character token - -These are application authentication tokens, not SSH keys. Generate a separate token for each cabinet and for the operator: - -```powershell -# Windows -py -3 -c "import secrets; print(secrets.token_urlsafe(12))" -``` - -```sh -# Pi / macOS / Linux python3 -c "import secrets; print(secrets.token_urlsafe(12))" ``` -Each command produces exactly 16 URL-safe characters. Preserve capitalization and any `-` or `_`, including a trailing hyphen. Restart the affected controller/client after changing credentials. +This produces a 16-character URL-safe token; use `py -3` on Windows if appropriate. For existing installations, preserve their assigned credentials instead of silently generating replacements. The older installation-specific examples remain in [cabinet setup](docs/cabinet_setup.md); the clean-install walkthrough uses fresh placeholders. -The supplied internal token assignments, complete controller configuration, and individual Cabinet 1–7 examples are in the [cabinet setup guide](docs/cabinet_setup.md). Use those assigned tokens for the current installation; generate new ones when provisioning additional identities or deliberately replacing a token. +INI scalar values are unquoted; JSON values such as `cabinet_tokens` retain JSON quotes. Godot CFG strings use double quotes. The CFG belongs in Godot's user-data directory, not beside the executable. On Linux that is normally `~/.local/share/godot/app_userdata/ArcadeEscape/`; on Windows, `%APPDATA%/Godot/app_userdata/ArcadeEscape/`. -On each cabinet, create **arcade_network.cfg** in the application's Godot user-data directory: +Nonempty launcher URL/token values override the CFG; inherited `ARCADE_ROOM_URL` and `ARCADE_CABINET_TOKEN` override those launcher values. The configured cabinet ID is passed as a command-line argument. Clear stale environment overrides when troubleshooting. -```ini -[network] -url="http://CONTROLLER_IP:8766" -cabinet_id="cabinet_01" -cabinet_token="your-generated-cabinet-one-token" -``` +The cabinet controller URL is `http://MASTER_PRIVATE_IP:8766`, not the cabinet's own IP or GPIO port. The default operator connection is plain HTTP for the internal network; do not expose it through public router port forwarding. -Use that cabinet's own ID and token. CFG strings use **double quotes**. +## Exports, startup, and deployment -| Machine | Typical configuration directory | -| --- | --- | -| Raspberry Pi / Linux | `~/.local/share/godot/app_userdata/ArcadeEscape/` | -| Windows | `%APPDATA%/Godot/app_userdata/ArcadeEscape/` | +Use the [master walkthrough's release workflow](docs/pacman_rfid_first_setup.md#3-export-package-and-copy-one-complete-release) as the source of truth: -This file goes in the user-data directory, **not beside the executable**. `ARCADE_ROOM_URL` and `ARCADE_CABINET_TOKEN` environment variables override their file values; `--cabinet=...` overrides the cabinet ID. +1. Save resources and export `catalog.json` from the same project revision. +2. Build Linux ARM64 executable + PCK into a fresh release folder outside the repository; optionally build Windows separately. +3. Package the matching Python/controller assets with `master-package` and the cabinet files with `bundle`. +4. Configure site-specific files separately; compile the RFID helper on the cabinet. +5. Validate launcher settings, start the services, and run the physical acceptance test before enabling startup. -Start the managed client: +The existing `setup/pc` and `setup/pi` folders are manual staging areas, **not proof of a current build**. Their checked-in artifacts are not automatically rebuilt when source changes. Do not assume they contain the 2.3 RFID feature until explicitly exported and verified. Exclude staged exports from the game PCK. -```sh -./ArcadeEscape.arm64 -- --managed-cabinet --cabinet=cabinet_01 -``` +The deployment tools provide a terminal setup wizard, separate site credentials, SSH enrollment, per-cabinet preflight, versioned uploads with manifest hashes, explicit activation, and service/status/log commands. They do not install a private DHCP/firewall configuration or offer a browser provisioning wizard. RFID-only provisioning still needs the manual route described above. -This bypasses the game list and waits for the operator. It uses the controls previously saved through the launcher. Deploy the current export: the fleet controller requires protocol 2 (`arcade-cabinet-2`). +The Pi user-service helper captures the desktop environment and refreshes it on login. It does not automatically enable cabinet boot startup; follow the walkthrough for desktop autologin/startup, or use the master remote-start workflow. No Godot editor is needed on a deployed cabinet or browser-only master. -### Enable physical GPIO outputs - -Follow the [GPIO guide](tools/pi_gpio/README.md) to install Pi dependencies and verify the wiring with an LED first. GPIO action pins use **BCM numbering**, not physical header positions. - -For the Cabinet 01 GPIO17 example, run the helper and game in separate terminals: - -```sh -# Terminal 1: local GPIO helper -GPIOZERO_PIN_FACTORY=lgpio python3 pi_gpio_bridge.py --controller cabinet_01_gpio --pins 17 -``` - -```sh -# Terminal 2: managed game with real outputs enabled -./ArcadeEscape.arm64 -- --managed-cabinet --cabinet=cabinet_01 --gpio=real -``` - -Copy the bridge script from [tools/pi_gpio/](tools/pi_gpio/) to the Pi. Match its controller ID and allowed pins to that cabinet's hardware resources. - -| Port | Service | Address used | -| --- | --- | --- | -| **8766** | Room controller and browser operator | Controller machine's LAN address | -| **8765** | GPIO bridge | `127.0.0.1` on the cabinet Pi | - -Keep the room URL on **8766** even when the GPIO bridge reports **8765**. An accepted GPIO command confirms the software call; without sensor feedback it does not confirm physical coin delivery. - -## 6. Run a room session - -1. Confirm each required cabinet is **ONLINE** in the browser operator. -2. Select a scenario and choose **Prepare assigned cabinets**. -3. Wait for each cabinet to acknowledge **READY**, then start it remotely or with its physical Start button. -4. Monitor game state, event history, and hardware command acknowledgements. -5. Use **Reset cabinet…** to end its session before changing assignments. - -Scenario preparation validates the complete assignment list before dispatch, including connectivity and game compatibility. Cabinets acknowledge independently. Manual game assignments intentionally run without inherited scenario rewards. - -The sample [proof_of_concept](data/scenarios/proof_of_concept.tres) connects a Pac-Man win on Cabinet 01 to its `coin_release` action. The [two_cabinet_test](data/scenarios/two_cabinet_test.tres) demonstrates separate cabinet assignments and event mappings. - -The older `-- --operator` Godot mode remains available for Cabinet 01 quick controls. Use the browser interface for multiple cabinets and the room map. - -For a sequential two-Pi test, choose **Cabinet 1 -> Cabinet 2**: winning Pac-Man on the first cabinet loads and automatically starts Galaga on the second. See the [chain-test walkthrough](tools/room_controller/README.md#two-pi-chain-test-cabinet-1-wins---cabinet-2-starts). - -## Troubleshooting - -| Symptom | Check | -| --- | --- | -| Controls differ between Windows and Pi | Run Control Setup on the Pi; do not copy Windows mappings blindly. | -| Room connection token is not configured | Check the user-data CFG location, double quotes, role-specific token, and environment overrides. | -| Unauthorized | The service is reachable; check the token and cabinet ID against the controller configuration. | -| Cabinet stays offline | Check the controller IP, port 8766, firewall, running client, and export protocol version. | -| A game is missing from the compatible list | Update the cabinet's capabilities, regenerate the catalog, and redeploy the cabinet resources. | -| GPIO command fails or LED stays off | Check real mode, local helper, controller ID, pin allowlist, and the wiring test in the GPIO guide. | -| Delivery is unconfirmed | Software acknowledgement is available, but physical feedback is not; inspect the output. | - -## Repository layout - -```text -addons/escape_room/ Godot editor plugin -core/ Game contracts, resources, runtime, controls, networking -editor/ Room-authoring workspace and simulation tools -data/ Saved games, cabinets, rooms, scenarios, and mappings -games/ Seven games, art/audio, launcher, and control setup -operator/ Godot operator scenes -main/ Room runtime entry scene -tools/room_controller/ Python controller, browser dashboard, catalog exporter -tools/pi_gpio/ Local Raspberry Pi GPIO bridge -tests/ Godot and Python verification scripts -``` +Back up site configuration, controller state, input mappings, and the GPIO deduplication ledger. Keep previous release bundles for rollback. Do not mix old PCKs, new catalogs, and unrelated Python releases. ## Development checks -Run commands from the repository root, replacing `godot` with your executable as needed: +Run from the project root; replace `godot` with the project's console executable and `python3` with `py -3` on Windows as needed: ```sh python3 -m unittest discover -s tests/room_network -v python3 -m unittest discover -s tests/gpio -v -godot --headless --path . --script tests/room_network/check_capabilities.gd -godot --headless --path . --script tests/launcher/check_launcher.gd -godot --headless --path . --script tests/launcher/check_controls.gd -python3 tests/room_network/check_integration.py /path/to/godot +godot --headless --path . --script res://tests/pcb/check_pcb.gd +godot --headless --path . --script res://tests/pcb/check_rfid_credit.gd +godot --headless --path . --script res://tests/room_editor/check_room_view.gd +godot --headless --path . --script res://tests/launcher/check_controls.gd ``` -On Windows, `py -3` can replace `python3`. The network integration test launches two headless Godot clients and temporary services on ports 8765/8766, forces test-only wins, and checks a fake GPIO output. Stop any real services using those ports before running it. These tests do not replace physical Pi, wiring, or iPad checks. +Controller tests use a stable historical catalog fixture so designer changes do not alter protocol tests; the RFID scenario test also checks the actual exported catalog. Network/GPIO tests use temporary services and fake hardware, not physical reader/actuator validation. -## Current scope +Last recorded verification during this work: 66 controller tests and the RFID credit checks passed; the prior GPIO suite passed 10 tests. The broader `tests/test_pacman.gd` suite reported one ghost-capture assertion failure in unchanged model code. Godot test shutdown also reported resource-leak warnings. These are known validation limitations, not a claim that every project test passes. New Pi exports, RFID compilation, card compatibility, audio, and end-to-end physical operation still need installation testing. -The project supports standalone arcade play, room authoring and simulation, multiple managed cabinets, scenario assignments, a browser room map, and real GPIO outputs. External sensor inputs, detailed live puzzle-step diagrams, coordinated finale variants, and room timers/hints remain future work. Device locations on the map currently provide layout context rather than live physical sensor state. +## Troubleshooting + +| Symptom | First checks | +| --- | --- | +| Controls differ on Pi | Run Control Setup locally; do not assume Windows button indices match. | +| Unauthorized | Correct role/token and cabinet ID; check inherited environment overrides. | +| Cabinet offline | Master URL/8766, network, running client, matching export version. | +| Game unavailable | Cabinet capabilities and matching exported catalog/resources. | +| RFID scenario rejected | Cabinet 2.3, real GPIO, matching reader ID in the exported PCB inventory. | +| RFID pin conflict | Explicit empty `pins =` for RFID-only testing. BCM17/27/22/20 and SPI0 are reserved for readers. | +| UID visible but no credit | Prepare the RFID scenario, then remove/re-present at J1; check the accepted-card list. | +| Output delivery unconfirmed | GPIO acknowledgement/electrical state does not confirm physical movement; use an appropriate sensor. | +| Reader stopped or stale | Helper process, SPI/GPIO permissions, wiring, and competing Odin/bridge processes. | + +## Repository layout + +```text +addons/escape_room/ Editor plugin and PCB inspector registration +core/ Runtime, networking, controls, room/cabinet/scenario resources +editor/ Authoring workspace, physical room and PCB editors +data/ Saved games, cabinets, room layouts and scenarios +games/ Seven games, assets, launcher and control setup +operator/ Optional Godot operator scenes +main/ Room runtime entry scene +tools/room_controller/ Python controller, browser hub, catalog exporter +tools/pi_gpio/ Local GPIO bridge, feedback sampler, Rust RFID helper source +tools/launch/ Supervised launchers, Pi service helper, remote SSH startup +tools/deploy/ Packaging, site setup, staged deployment and activation +setup/pc/, setup/pi/ Existing manually staged exports/configuration examples +docs/ Setup, deployment and designer walkthroughs +tests/ Godot/Python checks and isolated fixtures +``` + +## Remaining work + +Physical RFID commissioning, complete RFID support in the fleet wizard, browser doorway rendering, additional cabinet artwork, general sensor-to-scenario rules, detailed puzzle-step views, coordinated finale variants, and room timers/hints remain future work. The hub can link to other services; it is not yet a full multi-room orchestration system. diff --git a/addons/escape_room/escape_room_plugin.gd b/addons/escape_room/escape_room_plugin.gd index 4a93631..a314aee 100644 --- a/addons/escape_room/escape_room_plugin.gd +++ b/addons/escape_room/escape_room_plugin.gd @@ -15,15 +15,20 @@ const ROOM_SETTING := ( ) +var pcb_inspector: EditorInspectorPlugin var workspace: EscapeRoomWorkspace func _enter_tree() -> void: + pcb_inspector = preload("res://addons/escape_room/pcb_inspector.gd").new() + add_inspector_plugin(pcb_inspector) _ensure_project_settings() _create_workspace() func _exit_tree() -> void: + remove_inspector_plugin(pcb_inspector) + pcb_inspector = null _destroy_workspace() diff --git a/addons/escape_room/pcb_inspector.gd b/addons/escape_room/pcb_inspector.gd new file mode 100644 index 0000000..40278bb --- /dev/null +++ b/addons/escape_room/pcb_inspector.gd @@ -0,0 +1,13 @@ +@tool +extends EditorInspectorPlugin + +const PROPERTY = preload("res://editor/hardware/pcb_editor_property.gd") + +func _can_handle(object: Object) -> bool: + return object is CabinetProfile + +func _parse_property(_object: Object, _type: Variant.Type, name: String, _hint: PropertyHint, _hint_string: String, _usage: int, _wide: bool) -> bool: + if name == "pcb_config": + add_property_editor(name, PROPERTY.new()) + return true + return false diff --git a/addons/escape_room/pcb_inspector.gd.uid b/addons/escape_room/pcb_inspector.gd.uid new file mode 100644 index 0000000..5cdf569 --- /dev/null +++ b/addons/escape_room/pcb_inspector.gd.uid @@ -0,0 +1 @@ +uid://dxenrkrq6ko7e diff --git a/core/hardware/cabinet_pcb_config.gd b/core/hardware/cabinet_pcb_config.gd new file mode 100644 index 0000000..5cc3efa --- /dev/null +++ b/core/hardware/cabinet_pcb_config.gd @@ -0,0 +1,92 @@ +@tool +class_name CabinetPCBConfig +extends Resource + +# This resource describes wiring. It never configures or writes GPIO. +@export var connections: Array[PCBConnection] = [] + +const TYPES := { + "rfid": "RFID reader", "button": "Button / switch", "sensor": "Digital sensor", + "led": "LED / lighting driver", "motor": "Motorized switch / motor driver", + "relay": "Relay / MOSFET module", "i2c": "I2C peripheral", "other": "Other (document wiring)" +} + +static func ports() -> Array[Dictionary]: + var result: Array[Dictionary] = [] + for index in range(8): + result.append({"id": "J%d" % (index + 1), "family": "reader", "label": "%d%d%d" % [(index >> 2) & 1, (index >> 1) & 1, index & 1], "pins": "Selector: BCM22=%d, BCM27=%d, BCM17=%d\nShared reset: BCM20. Reader connector contact pinout is not documented." % [(index >> 2) & 1, (index >> 1) & 1, index & 1], "types": ["rfid", "other"], "bcm": -1}) + result.append({"id": "J9", "family": "host", "label": "Pi ribbon", "pins": "Host GPIO ribbon connector. Confirm pin-1 orientation against the physical board.", "types": [], "bcm": -1}) + var signals := [5, 6, 13, 19, 26, 21] + for index in range(6): + result.append({"id": "J%d" % (10 + index), "family": "powered", "label": "BCM%d" % signals[index], "pins": "1 GND (black) | 2 3V3 (red) | 3 signal (yellow)\nBCM%d" % signals[index], "types": ["button", "sensor", "led", "motor", "relay", "other"], "bcm": signals[index]}) + result.append({"id": "J16", "family": "i2c", "label": "I2C1", "pins": "1 GND (black) | 2 3V3 (red) | 3 SCL (white) | 4 SDA (yellow)", "types": ["i2c", "other"], "bcm": -1}) + signals = [16, 12, 24, 23, 18, 25] + for index in range(6): + result.append({"id": "J%d" % (17 + index), "family": "signal", "label": "BCM%d" % signals[index], "pins": "1 GND (black) | 2 signal (red)\nBCM%d - red is signal, not a power supply." % signals[index], "types": ["button", "sensor", "led", "motor", "relay", "other"], "bcm": signals[index]}) + return result + +static func port(id: String) -> Dictionary: + for definition in ports(): + if definition.id == id: + return definition + return {} + +func connection(id: String) -> PCBConnection: + for item in connections: + if item != null and item.port_id == id: + return item + return null + +func set_connection(item: PCBConnection) -> void: + for index in range(connections.size() - 1, -1, -1): + if connections[index] != null and connections[index].port_id == item.port_id: + connections.remove_at(index) + if not item.device_type.is_empty(): + connections.append(item) + emit_changed() + +func issues(hardware: HardwareConfig = null) -> PackedStringArray: + var messages := PackedStringArray() + var used_ports: Dictionary = {} + var used_ids: Dictionary = {} + for item in connections: + if item == null: + messages.append("Empty connection resource.") + continue + var definition := port(item.port_id) + if definition.is_empty() or not definition.types.has(item.device_type): + messages.append("%s: unsupported connector/device combination." % item.port_id) + if used_ports.has(item.port_id): + messages.append("%s: connector is assigned more than once." % item.port_id) + used_ports[item.port_id] = true + if item.device_id.strip_edges().is_empty(): + messages.append("%s: enter a device ID." % item.port_id) + elif used_ids.has(item.device_id): + messages.append("Duplicate device ID: %s." % item.device_id) + used_ids[item.device_id] = true + if item.device_type in ["led", "motor", "relay"] and not item.binding_id.is_empty(): + var action: HardwareAction = hardware.get_action(StringName(item.binding_id)) if hardware else null + if action == null: + messages.append("%s: hardware action '%s' does not exist." % [item.port_id, item.binding_id]) + elif not definition.is_empty() and action.gpio_pin != definition.bcm: + messages.append("%s: action uses BCM%d, but connector uses BCM%d." % [item.port_id, action.gpio_pin, definition.bcm]) + if hardware: + for action in hardware.actions: + if action != null and action.gpio_pin in [17, 27, 22, 20]: + messages.append("Action '%s' uses reserved PCB selector/reset BCM%d." % [action.action_id, action.gpio_pin]) + return messages + +func feedback_devices() -> Array[Dictionary]: + var result: Array[Dictionary] = [] + for item in connections: + if item == null: + continue + var kind := "" + if item.device_type == "rfid": kind = "rfid" + elif item.device_type in ["button", "sensor"]: kind = "input" + elif item.device_type in ["led", "motor", "relay"]: kind = "output" + else: + push_warning("Feedback driver not implemented for %s (%s)" % [item.device_id, item.device_type]) + continue + result.append({"id": item.device_id, "name": item.display_name, "port": item.port_id, "kind": kind, "pull_up": item.input_pull_up}) + return result diff --git a/core/hardware/cabinet_pcb_config.gd.uid b/core/hardware/cabinet_pcb_config.gd.uid new file mode 100644 index 0000000..827f111 --- /dev/null +++ b/core/hardware/cabinet_pcb_config.gd.uid @@ -0,0 +1 @@ +uid://c0g0mq4b3d547 diff --git a/core/hardware/cabinet_profile.gd b/core/hardware/cabinet_profile.gd index 5842e74..f38792c 100644 --- a/core/hardware/cabinet_profile.gd +++ b/core/hardware/cabinet_profile.gd @@ -10,6 +10,10 @@ extends Resource @export var hardware_config: HardwareConfig +@export_group("PCB Connections") +@export var pcb_config: CabinetPCBConfig +@export_group("") + const FEET_TO_METERS: float = 0.3048 const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS diff --git a/core/hardware/pcb_connection.gd b/core/hardware/pcb_connection.gd new file mode 100644 index 0000000..46690cc --- /dev/null +++ b/core/hardware/pcb_connection.gd @@ -0,0 +1,12 @@ +@tool +class_name PCBConnection +extends Resource + +@export var port_id: String = "" +@export var device_type: String = "" +@export var device_id: String = "" +@export var display_name: String = "" +@export var binding_id: String = "" +@export_multiline var notes: String = "" + +@export var input_pull_up: bool = true diff --git a/core/hardware/pcb_connection.gd.uid b/core/hardware/pcb_connection.gd.uid new file mode 100644 index 0000000..098605f --- /dev/null +++ b/core/hardware/pcb_connection.gd.uid @@ -0,0 +1 @@ +uid://burui20kuayc2 diff --git a/core/network/managed_cabinet.gd b/core/network/managed_cabinet.gd index a6a9f68..91a68e5 100644 --- a/core/network/managed_cabinet.gd +++ b/core/network/managed_cabinet.gd @@ -1,5 +1,5 @@ extends Node -const VERSION := "arcade-cabinet-2" +const VERSION := "arcade-cabinet-2.3" var cabinet_id := "cabinet_01" var database: ProjectDatabase = preload("res://data/project_database.tres") var cabinet: CabinetProfile @@ -14,6 +14,14 @@ var busy := false var executing := false var event_number := 0 var notice: Label +var test_win_remaining := -1.0 + +func _process(delta: float) -> void: + if phase != "playing" or test_win_remaining < 0.0 or game == null: return + test_win_remaining -= delta + if test_win_remaining <= 0.0: + test_win_remaining = -1.0 + game.win_game() func _ready() -> void: var config := ConfigFile.new() @@ -47,9 +55,13 @@ func _ready() -> void: func poll() -> void: if busy or executing: return busy = true + if game != null and phase == "ready" and game.has_method("receive_device_feedback") and HardwareManager.real_gpio and HardwareManager.client != null: + game.receive_device_feedback(HardwareManager.client.get_feedback()) var sent := reports.duplicate(true) var result: Dictionary = await connection.post("/cabinet/poll", { "cabinet": cabinet_id, "protocol": 2, "boot": boot, "version": VERSION, + "features": ["assignment_config_v1", "device_feedback_v1", "rfid_credit_v1"], + "feedback": HardwareManager.client.get_feedback() if HardwareManager.real_gpio and HardwareManager.client != null else {}, "gpio": "real" if HardwareManager.real_gpio else "simulation", "reports": sent, }) busy = false @@ -70,6 +82,7 @@ func poll() -> void: notice.text = "Waiting for the room operator" if game == null else "" func unload() -> void: + test_win_remaining = -1.0 Input.mouse_mode = Input.MOUSE_MODE_VISIBLE if is_instance_valid(game): remove_child(game) @@ -77,6 +90,10 @@ func unload() -> void: game = null func event(name: String) -> void: + if name == "started" and game != null: + test_win_remaining = float(game.run_config.get("test_auto_win_seconds", -1.0)) + elif name in ["won", "lost"]: + test_win_remaining = -1.0 event_number += 1 reports.append({"kind": "event", "id": boot + ":" + str(event_number), "session": session, "event": name}) phase = {"started": "playing", "won": "won", "lost": "lost"}.get(name, phase) @@ -110,7 +127,9 @@ func execute(command: Dictionary) -> void: if str(definition.game_id) == "missile_command": game.set("relative_mouse_aim", true) Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if str(definition.game_id) in ["missile_command", "tempest"] else Input.MOUSE_MODE_VISIBLE add_child(game) - game.configure({}) + game.configure(command.get("game_config", {}).duplicate(true)) + if game.has_method("receive_device_feedback") and HardwareManager.client != null: + game.receive_device_feedback(HardwareManager.client.get_feedback()) game.game_started.connect(func(): event("started")) game.game_won.connect(func(): event("won")) game.game_lost.connect(func(): event("lost")) diff --git a/core/rooms/room_wall_definition.gd b/core/rooms/room_wall_definition.gd index 4e4d391..19a110f 100644 --- a/core/rooms/room_wall_definition.gd +++ b/core/rooms/room_wall_definition.gd @@ -7,6 +7,9 @@ const FEET_TO_METERS: float = 0.3048 const METERS_TO_FEET: float = 1.0 / FEET_TO_METERS +@export var is_doorway: bool = false +@export var swing_reversed: bool = false + @export var wall_id: StringName = &"" @export var start: Vector2 = Vector2.ZERO @export var end: Vector2 = Vector2( diff --git a/core/runtime/pi_gpio_client.gd b/core/runtime/pi_gpio_client.gd index 858c918..66f4292 100644 --- a/core/runtime/pi_gpio_client.gd +++ b/core/runtime/pi_gpio_client.gd @@ -5,6 +5,8 @@ const URL := "http://127.0.0.1:8765" var pending: Array[Dictionary] = [] var serial := 0 var busy := false +var device_feedback: Dictionary = {} +var feedback_received_ms: int = 0 var heartbeat: HTTPRequest func _ready() -> void: @@ -12,6 +14,7 @@ func _ready() -> void: heartbeat = HTTPRequest.new() heartbeat.timeout = 1.0 add_child(heartbeat) + heartbeat.request_completed.connect(_feedback_response) var timer := Timer.new() timer.wait_time = 1.0 timer.timeout.connect(_heartbeat) @@ -56,3 +59,18 @@ func _pump() -> void: status_changed.emit("GPIO %s accepted by Pi bridge" % item.payload.pin if ok else message) completed.emit(item.id, ok) _pump.call_deferred() + +func _feedback_response(result: int, code: int, _headers: PackedStringArray, body: PackedByteArray) -> void: + if result != HTTPRequest.RESULT_SUCCESS or code != 200: + return + var data = JSON.parse_string(body.get_string_from_utf8()) + if data is Dictionary and data.get("feedback") is Dictionary: + device_feedback = data.feedback + feedback_received_ms = Time.get_ticks_msec() + +func get_feedback() -> Dictionary: + var result := device_feedback.duplicate(true) + result["fresh"] = feedback_received_ms > 0 and Time.get_ticks_msec() - feedback_received_ms < 4000 + for device in result.get("devices", []): + device["age_ms"] = int(device.get("age_ms", 999999)) + maxi(0, Time.get_ticks_msec() - feedback_received_ms) + return result diff --git a/core/scenarios/scenario_assignment.gd b/core/scenarios/scenario_assignment.gd index 69142f1..8b6f6d7 100644 --- a/core/scenarios/scenario_assignment.gd +++ b/core/scenarios/scenario_assignment.gd @@ -9,6 +9,8 @@ extends Resource @export var variant: StringName = &"standard" ## Managed controller: load and auto-start only after this cabinet wins. @export var activate_after_win: StringName = &"" +## Per-assignment objectives; empty uses the game defaults. +@export var game_config: Dictionary = {} @export var event_mappings: Array[ScenarioEventMapping] = [] diff --git a/core/scenarios/scenario_edit_session.gd b/core/scenarios/scenario_edit_session.gd index 0d05c6d..e284ffd 100644 --- a/core/scenarios/scenario_edit_session.gd +++ b/core/scenarios/scenario_edit_session.gd @@ -154,6 +154,7 @@ func _create_working_copy() -> void: ) assignment.activate_after_win = source_assignment.activate_after_win + assignment.game_config = source_assignment.game_config.duplicate(true) assignment.event_mappings = ( _duplicate_mappings( @@ -260,6 +261,7 @@ func _capture_scenario_state( "game_id": StringName(), "variant": assignment.variant, "activate_after_win": assignment.activate_after_win, + "game_config": assignment.game_config.duplicate(true), "event_mappings": [], } diff --git a/core/simulation/room_edit_session.gd b/core/simulation/room_edit_session.gd index cd96f66..d7cd56d 100644 --- a/core/simulation/room_edit_session.gd +++ b/core/simulation/room_edit_session.gd @@ -401,6 +401,8 @@ func _capture_room_state( "start": wall.start, "end": wall.end, "thickness": wall.thickness, + "is_doorway": wall.is_doorway, + "swing_reversed": wall.swing_reversed, } ) diff --git a/data/cabinets/cabinet_01.tres b/data/cabinets/cabinet_01.tres index 9b0730a..7edef58 100644 --- a/data/cabinets/cabinet_01.tres +++ b/data/cabinets/cabinet_01.tres @@ -3,13 +3,64 @@ [ext_resource type="Resource" uid="uid://cxna0oimf8mop" path="res://data/cabinets/cabinet_01_hardware.tres" id="1_b4cnm"] [ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_hv02u"] [ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="1_v1x8f"] +[ext_resource type="Script" uid="uid://burui20kuayc2" path="res://core/hardware/pcb_connection.gd" id="3_0aat6"] +[ext_resource type="Script" uid="uid://c0g0mq4b3d547" path="res://core/hardware/cabinet_pcb_config.gd" id="4_w65ol"] + +[sub_resource type="Resource" id="Resource_ac1o8"] +script = ExtResource("3_0aat6") +port_id = "J17" +device_type = "button" +device_id = "cab1_btn_red" +display_name = "Red Button" + +[sub_resource type="Resource" id="Resource_gto24"] +script = ExtResource("3_0aat6") +port_id = "J18" +device_type = "button" +device_id = "cab1_btn_grn" +display_name = "Green Button" + +[sub_resource type="Resource" id="Resource_pqv68"] +script = ExtResource("3_0aat6") +port_id = "J19" +device_type = "button" +device_id = "cab1_btn_blue" +display_name = "Blue Button" + +[sub_resource type="Resource" id="Resource_33hyq"] +script = ExtResource("3_0aat6") +port_id = "J20" +device_type = "button" +device_id = "cab1_btn_ylw" +display_name = "Yellow Btn" + +[sub_resource type="Resource" id="Resource_qgibf"] +script = ExtResource("3_0aat6") +port_id = "J2" +device_type = "rfid" +device_id = "cab1_rfid2" +display_name = "RFID Reader 2" +binding_id = "credit_inserted_p2" + +[sub_resource type="Resource" id="Resource_e5qub"] +script = ExtResource("3_0aat6") +port_id = "J1" +device_type = "rfid" +device_id = "cab1_rfid1" +display_name = "RFID Reader 1" +binding_id = "credit_inserted_p1" + +[sub_resource type="Resource" id="Resource_npy45"] +script = ExtResource("4_w65ol") +connections = Array[ExtResource("3_0aat6")]([SubResource("Resource_ac1o8"), SubResource("Resource_gto24"), SubResource("Resource_pqv68"), SubResource("Resource_33hyq"), SubResource("Resource_qgibf"), SubResource("Resource_e5qub")]) [resource] script = ExtResource("1_v1x8f") cabinet_id = &"cabinet_01" -display_name = "Master Cabinet" -capabilities = Array[StringName]([&"joystick_4way", &"button_1", &"start_button", &"joystick_horizontal", &"button_2", &"button_3", &"button_4", &"button_5", &"button_6", &"select_button", &"trackball", &"joystick_8way", &"joystick_dual"]) +display_name = "Cabinet 01" +capabilities = Array[StringName]([&"joystick_4way", &"start_button", &"select_button"]) hardware_config = ExtResource("1_b4cnm") +pcb_config = SubResource("Resource_npy45") footprint_width_m = 0.9144000000000001 footprint_depth_m = 0.9144000000000001 height_m = 1.8288000000000002 diff --git a/data/cabinets/cabinet_01_hardware.tres b/data/cabinets/cabinet_01_hardware.tres index 0d3aefb..182c124 100644 --- a/data/cabinets/cabinet_01_hardware.tres +++ b/data/cabinets/cabinet_01_hardware.tres @@ -5,16 +5,18 @@ [sub_resource type="Resource" id="Resource_ogdj5"] script = ExtResource("1_w3p60") -action_id = &"coin_release" -display_name = "Coin Release" +action_id = &"coin_slot_1" +display_name = "Coin Slot" +action_type = 0 +gpio_pin = 0 pulse_duration_ms = 600 metadata/_custom_type_script = "uid://c7eu1n5yey1x4" [sub_resource type="Resource" id="Resource_gu74o"] script = ExtResource("1_w3p60") -action_id = &"start_light" +action_id = &"lock_released" display_name = "Start Light" -gpio_pin = 20 +gpio_pin = 0 metadata/_custom_type_script = "uid://c7eu1n5yey1x4" [resource] diff --git a/data/cabinets/cabinet_02.tres b/data/cabinets/cabinet_02.tres index 6031849..2c4d3c7 100644 --- a/data/cabinets/cabinet_02.tres +++ b/data/cabinets/cabinet_02.tres @@ -3,13 +3,64 @@ [ext_resource type="Resource" uid="uid://ddv64yg3h5vsi" path="res://data/cabinets/cabinet_02_hardware.tres" id="1_bfw68"] [ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_lrfm7"] [ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="2_lrfm7"] +[ext_resource type="Script" uid="uid://burui20kuayc2" path="res://core/hardware/pcb_connection.gd" id="3_xgtuu"] +[ext_resource type="Script" uid="uid://c0g0mq4b3d547" path="res://core/hardware/cabinet_pcb_config.gd" id="4_1abnh"] + +[sub_resource type="Resource" id="Resource_ac1o8"] +script = ExtResource("3_xgtuu") +port_id = "J17" +device_type = "button" +device_id = "cab1_btn_red" +display_name = "Red Button" + +[sub_resource type="Resource" id="Resource_gto24"] +script = ExtResource("3_xgtuu") +port_id = "J18" +device_type = "button" +device_id = "cab1_btn_grn" +display_name = "Green Button" + +[sub_resource type="Resource" id="Resource_pqv68"] +script = ExtResource("3_xgtuu") +port_id = "J19" +device_type = "button" +device_id = "cab1_btn_blue" +display_name = "Blue Button" + +[sub_resource type="Resource" id="Resource_33hyq"] +script = ExtResource("3_xgtuu") +port_id = "J20" +device_type = "button" +device_id = "cab1_btn_ylw" +display_name = "Yellow Btn" + +[sub_resource type="Resource" id="Resource_qgibf"] +script = ExtResource("3_xgtuu") +port_id = "J2" +device_type = "rfid" +device_id = "cab1_rfid2" +display_name = "RFID Reader 2" +binding_id = "credit_inserted_p2" + +[sub_resource type="Resource" id="Resource_e5qub"] +script = ExtResource("3_xgtuu") +port_id = "J1" +device_type = "rfid" +device_id = "cab1_rfid1" +display_name = "RFID Reader 1" +binding_id = "credit_inserted_p1" + +[sub_resource type="Resource" id="Resource_npy45"] +script = ExtResource("4_1abnh") +connections = Array[ExtResource("3_xgtuu")]([SubResource("Resource_ac1o8"), SubResource("Resource_gto24"), SubResource("Resource_pqv68"), SubResource("Resource_33hyq"), SubResource("Resource_qgibf"), SubResource("Resource_e5qub")]) [resource] script = ExtResource("2_lrfm7") cabinet_id = &"cabinet_02" display_name = "Cabinet 02" -capabilities = Array[StringName]([&"joystick_4way", &"button_1", &"start_button", &"joystick_horizontal"]) +capabilities = Array[StringName]([&"joystick_horizontal", &"start_button", &"select_button", &"button_1"]) hardware_config = ExtResource("1_bfw68") +pcb_config = SubResource("Resource_npy45") footprint_width_m = 0.9144000000000001 footprint_depth_m = 0.9144000000000001 height_m = 1.8288000000000002 diff --git a/data/cabinets/cabinet_02_hardware.tres b/data/cabinets/cabinet_02_hardware.tres index abde4a9..7bf86bc 100644 --- a/data/cabinets/cabinet_02_hardware.tres +++ b/data/cabinets/cabinet_02_hardware.tres @@ -11,23 +11,15 @@ gpio_pin = 27 pulse_duration_ms = 400 metadata/_custom_type_script = "uid://c7eu1n5yey1x4" -[sub_resource type="Resource" id="Resource_dasqc"] +[sub_resource type="Resource" id="Resource_gu74o"] script = ExtResource("1_dasqc") -action_id = &"start_light" +action_id = &"lock_released" display_name = "Start Light" -gpio_pin = 20 -metadata/_custom_type_script = "uid://c7eu1n5yey1x4" - -[sub_resource type="Resource" id="Resource_t81ly"] -script = ExtResource("1_dasqc") -action_id = &"glitch_code" -display_name = "Glitch Code" -gpio_pin = 21 -pulse_duration_ms = 1200 +gpio_pin = 0 metadata/_custom_type_script = "uid://c7eu1n5yey1x4" [resource] script = ExtResource("2_ye2fg") controller_id = &"cabinet_02_gpio" -actions = Array[ExtResource("1_dasqc")]([SubResource("Resource_ogdj5"), SubResource("Resource_dasqc"), SubResource("Resource_t81ly")]) +actions = Array[ExtResource("1_dasqc")]([SubResource("Resource_ogdj5"), SubResource("Resource_gu74o")]) metadata/_custom_type_script = "uid://60whm57itpjb" diff --git a/data/cabinets/cabinet_03.tres b/data/cabinets/cabinet_03.tres index a6c75e1..7f492d7 100644 --- a/data/cabinets/cabinet_03.tres +++ b/data/cabinets/cabinet_03.tres @@ -1,15 +1,20 @@ [gd_resource type="Resource" script_class="CabinetProfile" format=3 uid="uid://d4ns05ptr4ybw"] [ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="1_0s4iw"] +[ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_km331"] [ext_resource type="Resource" uid="uid://4t6n8gvu717o" path="res://data/cabinets/cabinet_03_hardware.tres" id="1_ojmvu"] [resource] script = ExtResource("1_0s4iw") cabinet_id = &"cabinet_03" display_name = "Cabinet 03" -capabilities = Array[StringName]([&"spinner", &"button_1", &"start_button"]) +capabilities = Array[StringName]([&"spinner", &"button_1", &"start_button", &"select_button"]) hardware_config = ExtResource("1_ojmvu") -footprint_width_m = 1.0 -footprint_depth_m = 1.0 -height_m = 2.0 +footprint_width_m = 0.9144000000000001 +footprint_depth_m = 0.9144000000000001 +height_m = 0.9144000000000001 +footprint_width_ft = 3.0 +footprint_depth_ft = 3.0 +height_ft = 3.0 +editor_texture = ExtResource("1_km331") metadata/_custom_type_script = "uid://c0jj5j5loqsma" diff --git a/data/cabinets/cabinet_03_hardware.tres b/data/cabinets/cabinet_03_hardware.tres index c414537..f788e13 100644 --- a/data/cabinets/cabinet_03_hardware.tres +++ b/data/cabinets/cabinet_03_hardware.tres @@ -5,14 +5,21 @@ [sub_resource type="Resource" id="Resource_ogdj5"] script = ExtResource("1_2vvyl") -action_id = &"coin_release" -display_name = "Coin Release" +action_id = &"coin_slot_1" +display_name = "Coin Slot 1" +action_type = 0 gpio_pin = 27 pulse_duration_ms = 400 metadata/_custom_type_script = "uid://c7eu1n5yey1x4" +[sub_resource type="Resource" id="Resource_ojmvu"] +script = ExtResource("1_2vvyl") +action_id = &"lock_released" +display_name = "Lock Released" +metadata/_custom_type_script = "uid://c7eu1n5yey1x4" + [resource] script = ExtResource("2_hx0bc") -controller_id = &"cabinet_02_gpio" -actions = Array[ExtResource("1_2vvyl")]([SubResource("Resource_ogdj5")]) +controller_id = &"cabinet_03_gpio" +actions = Array[ExtResource("1_2vvyl")]([SubResource("Resource_ogdj5"), SubResource("Resource_ojmvu")]) metadata/_custom_type_script = "uid://60whm57itpjb" diff --git a/data/cabinets/cabinet_04.tres b/data/cabinets/cabinet_04.tres new file mode 100644 index 0000000..bbf172a --- /dev/null +++ b/data/cabinets/cabinet_04.tres @@ -0,0 +1,20 @@ +[gd_resource type="Resource" script_class="CabinetProfile" format=3 uid="uid://cwa2ord52vdda"] + +[ext_resource type="Resource" uid="uid://dbcideufl5qhi" path="res://data/cabinets/cabinet_04_hardware.tres" id="1_2m3i6"] +[ext_resource type="Texture2D" uid="uid://cistuspr5bf8h" path="res://art/devices/tex_cabinet.png" id="1_8nqkk"] +[ext_resource type="Script" uid="uid://c0jj5j5loqsma" path="res://core/hardware/cabinet_profile.gd" id="1_a4ctu"] + +[resource] +script = ExtResource("1_a4ctu") +cabinet_id = &"cabinet_04" +display_name = "Cabinet 4" +capabilities = Array[StringName]([&"roller", &"button_1", &"button_2", &"button_3", &"select_button", &"start_button"]) +hardware_config = ExtResource("1_2m3i6") +footprint_width_m = 0.9144000000000001 +footprint_depth_m = 0.9144000000000001 +height_m = 0.9144000000000001 +footprint_width_ft = 3.0 +footprint_depth_ft = 3.0 +height_ft = 3.0 +editor_texture = ExtResource("1_8nqkk") +metadata/_custom_type_script = "uid://c0jj5j5loqsma" diff --git a/data/cabinets/cabinet_04_hardware.tres b/data/cabinets/cabinet_04_hardware.tres new file mode 100644 index 0000000..65be608 --- /dev/null +++ b/data/cabinets/cabinet_04_hardware.tres @@ -0,0 +1,22 @@ +[gd_resource type="Resource" script_class="HardwareConfig" format=3 uid="uid://dbcideufl5qhi"] + +[ext_resource type="Script" uid="uid://c7eu1n5yey1x4" path="res://core/hardware/hardware_action.gd" id="1_lpxoo"] +[ext_resource type="Script" uid="uid://60whm57itpjb" path="res://core/hardware/hardware_config.gd" id="2_tu3fw"] + +[sub_resource type="Resource" id="Resource_jt2rb"] +script = ExtResource("1_lpxoo") +action_id = &"coin_slot_1" +display_name = "Coin Slot 1" +metadata/_custom_type_script = "uid://c7eu1n5yey1x4" + +[sub_resource type="Resource" id="Resource_axg2k"] +script = ExtResource("1_lpxoo") +action_id = &"lock_released" +display_name = "Lock Released" +metadata/_custom_type_script = "uid://c7eu1n5yey1x4" + +[resource] +script = ExtResource("2_tu3fw") +controller_id = &"cabinet_04_gpio" +actions = Array[ExtResource("1_lpxoo")]([SubResource("Resource_jt2rb"), SubResource("Resource_axg2k")]) +metadata/_custom_type_script = "uid://60whm57itpjb" diff --git a/data/project_database.tres b/data/project_database.tres index 6e1f791..a0a10aa 100644 --- a/data/project_database.tres +++ b/data/project_database.tres @@ -9,20 +9,22 @@ [ext_resource type="Resource" uid="uid://c3v8o4nubjakg" path="res://data/games/pacman.tres" id="4_bfrqh"] [ext_resource type="Resource" uid="uid://d4ns05ptr4ybw" path="res://data/cabinets/cabinet_03.tres" id="4_gbm0a"] [ext_resource type="Resource" uid="uid://cnfhiky5834pw" path="res://data/games/galaga.tres" id="5_b427t"] -[ext_resource type="Resource" uid="uid://cqn58n5ahydil" path="res://data/scenarios/proof_of_concept.tres" id="7_gbm0a"] +[ext_resource type="Resource" uid="uid://cwa2ord52vdda" path="res://data/cabinets/cabinet_04.tres" id="5_wqg7d"] [ext_resource type="Resource" path="res://data/games/tempest.tres" id="8_0ke0w"] -[ext_resource type="Resource" uid="uid://tp7cvbnbhcrj" path="res://data/scenarios/galaga_proof_of_concept.tres" id="8_ib4xl"] [ext_resource type="Resource" path="res://data/games/missile_command.tres" id="9_0ke0w"] [ext_resource type="Resource" path="res://data/games/frogger.tres" id="10_8pm0t"] [ext_resource type="Resource" uid="uid://c07wbog147dml" path="res://data/scenarios/two_cabinet_test.tres" id="10_b427t"] [ext_resource type="Resource" uid="uid://yiejlr5h143m" path="res://data/games/space_invaders.tres" id="11_space_invaders"] -[ext_resource type="Resource" path="res://data/games/robotron.tres" id="12_robotron"] +[ext_resource type="Resource" uid="uid://bn8vgu5vrevgx" path="res://data/games/robotron.tres" id="12_robotron"] +[ext_resource type="Resource" uid="uid://crijr0r2ikxs1" path="res://data/scenarios/eg_glitch.tres" id="18_yfaxe"] +[ext_resource type="Resource" uid="uid://cv8oknw3dlwjk" path="res://data/scenarios/cabinet_chain_test.tres" id="chain_test"] +[ext_resource type="Resource" uid="uid://coylqrwbr26tb" path="res://data/scenarios/three_cabinet_objective_test.tres" id="three_test"] -[ext_resource type="Resource" path="res://data/scenarios/cabinet_chain_test.tres" id="chain_test"] +[ext_resource type="Resource" path="res://data/scenarios/pacman_rfid_credit_test.tres" id="rfid_credit_test"] [resource] script = ExtResource("4_algy0") -scenarios = Array[ExtResource("3_uw7gr")]([ExtResource("7_gbm0a"), ExtResource("8_ib4xl"), ExtResource("10_b427t"), ExtResource("chain_test")]) -cabinets = Array[ExtResource("1_uu70x")]([ExtResource("2_t6ujr"), ExtResource("3_bfrqh"), ExtResource("4_gbm0a")]) +scenarios = Array[ExtResource("3_uw7gr")]([ExtResource("10_b427t"), ExtResource("chain_test"), ExtResource("three_test"), ExtResource("18_yfaxe"), ExtResource("rfid_credit_test")]) +cabinets = Array[ExtResource("1_uu70x")]([ExtResource("2_t6ujr"), ExtResource("3_bfrqh"), ExtResource("4_gbm0a"), ExtResource("5_wqg7d")]) games = Array[ExtResource("2_an0il")]([ExtResource("4_bfrqh"), ExtResource("5_b427t"), ExtResource("8_0ke0w"), ExtResource("9_0ke0w"), ExtResource("10_8pm0t"), ExtResource("11_space_invaders"), ExtResource("12_robotron")]) metadata/_custom_type_script = "uid://diqnrxrhwx2ug" diff --git a/data/rooms/arcade_room.tres b/data/rooms/arcade_room.tres index 2b9743a..681b057 100644 --- a/data/rooms/arcade_room.tres +++ b/data/rooms/arcade_room.tres @@ -52,13 +52,25 @@ metadata/_custom_type_script = "uid://r2rpbasss511" [sub_resource type="Resource" id="Resource_prnev"] script = ExtResource("2_384hm") cabinet_id = &"cabinet_01" -position = Vector2(-5.9484997, 0.14750004) +position = Vector2(-6.5578, 1.9821999) metadata/_custom_type_script = "uid://cwblyetugbr4d" [sub_resource type="Resource" id="Resource_dicmx"] script = ExtResource("2_384hm") cabinet_id = &"cabinet_02" -position = Vector2(-3.5153918, 0.14274521) +position = Vector2(-2.8978, 1.9821999) +metadata/_custom_type_script = "uid://cwblyetugbr4d" + +[sub_resource type="Resource" id="Resource_df41d"] +script = ExtResource("2_384hm") +cabinet_id = &"cabinet_03" +position = Vector2(-2.8978, 3.2022) +metadata/_custom_type_script = "uid://cwblyetugbr4d" + +[sub_resource type="Resource" id="Resource_mmfj3"] +script = ExtResource("2_384hm") +cabinet_id = &"cabinet_04" +position = Vector2(-6.5578, 3.2022) metadata/_custom_type_script = "uid://cwblyetugbr4d" [sub_resource type="Resource" id="Resource_vv75m"] @@ -156,7 +168,7 @@ thickness_ft = 0.1 script = ExtResource("3_384hm") room_id = &"arcade_room" display_name = "Arcade Escape Room" -cabinet_placements = Array[ExtResource("2_384hm")]([SubResource("Resource_prnev"), SubResource("Resource_dicmx")]) +cabinet_placements = Array[ExtResource("2_384hm")]([SubResource("Resource_prnev"), SubResource("Resource_dicmx"), SubResource("Resource_df41d"), SubResource("Resource_mmfj3")]) devices = Array[ExtResource("2_hg7bw")]([SubResource("Resource_vv75m"), SubResource("Resource_12s2r"), SubResource("Resource_2f6ns"), SubResource("Resource_ks63c"), SubResource("Resource_kddy2")]) bindings = Array[ExtResource("1_icsgi")]([SubResource("Resource_icsgi"), SubResource("Resource_hg7bw"), SubResource("Resource_384hm"), SubResource("Resource_t81ly"), SubResource("Resource_clsof")]) boundary_points = PackedVector2Array(-7.3152, -0.6096, -2.1336, -0.6096, -2.1336, 7.62, -7.3152, 7.62) diff --git a/data/scenarios/cabinet_chain_test.tres b/data/scenarios/cabinet_chain_test.tres index 667f859..d582e0c 100644 --- a/data/scenarios/cabinet_chain_test.tres +++ b/data/scenarios/cabinet_chain_test.tres @@ -1,11 +1,12 @@ -[gd_resource type="Resource" script_class="ScenarioDefinition" format=3] +[gd_resource type="Resource" script_class="ScenarioDefinition" format=3 uid="uid://cv8oknw3dlwjk"] -[ext_resource type="Script" path="res://core/scenarios/scenario_definition.gd" id="1"] -[ext_resource type="Script" path="res://core/scenarios/scenario_assignment.gd" id="2"] -[ext_resource type="Resource" path="res://data/cabinets/cabinet_01.tres" id="3"] -[ext_resource type="Resource" path="res://data/cabinets/cabinet_02.tres" id="4"] -[ext_resource type="Resource" path="res://data/games/pacman.tres" id="5"] -[ext_resource type="Resource" path="res://data/games/galaga.tres" id="6"] +[ext_resource type="Script" uid="uid://d3pt6qa05kcnl" path="res://core/scenarios/scenario_definition.gd" id="1"] +[ext_resource type="Script" uid="uid://ibujr0ervhhq" path="res://core/scenarios/scenario_assignment.gd" id="2"] +[ext_resource type="Resource" uid="uid://cl4j1vtv388pt" path="res://data/cabinets/cabinet_01.tres" id="3"] +[ext_resource type="Script" uid="uid://lixrugm1v2d2" path="res://core/scenarios/scenario_event_mapping.gd" id="3_mhdgo"] +[ext_resource type="Resource" uid="uid://d1gyyfaaclas8" path="res://data/cabinets/cabinet_02.tres" id="4"] +[ext_resource type="Resource" uid="uid://c3v8o4nubjakg" path="res://data/games/pacman.tres" id="5"] +[ext_resource type="Resource" uid="uid://cnfhiky5834pw" path="res://data/games/galaga.tres" id="6"] [sub_resource type="Resource" id="First"] script = ExtResource("2") diff --git a/data/scenarios/eg_glitch.tres b/data/scenarios/eg_glitch.tres new file mode 100644 index 0000000..0577cd2 --- /dev/null +++ b/data/scenarios/eg_glitch.tres @@ -0,0 +1,44 @@ +[gd_resource type="Resource" script_class="ScenarioDefinition" format=3 uid="uid://crijr0r2ikxs1"] + +[ext_resource type="Script" uid="uid://ibujr0ervhhq" path="res://core/scenarios/scenario_assignment.gd" id="1_40f70"] +[ext_resource type="Resource" uid="uid://cl4j1vtv388pt" path="res://data/cabinets/cabinet_01.tres" id="2_nsmc3"] +[ext_resource type="Script" uid="uid://lixrugm1v2d2" path="res://core/scenarios/scenario_event_mapping.gd" id="3_4sjsw"] +[ext_resource type="Resource" uid="uid://c3v8o4nubjakg" path="res://data/games/pacman.tres" id="4_wb1pw"] +[ext_resource type="Resource" uid="uid://d1gyyfaaclas8" path="res://data/cabinets/cabinet_02.tres" id="5_cy2se"] +[ext_resource type="Resource" uid="uid://cnfhiky5834pw" path="res://data/games/galaga.tres" id="6_r5ac2"] +[ext_resource type="Resource" uid="uid://d4ns05ptr4ybw" path="res://data/cabinets/cabinet_03.tres" id="7_4fvja"] +[ext_resource type="Resource" path="res://data/games/tempest.tres" id="8_uer0y"] +[ext_resource type="Resource" uid="uid://cwa2ord52vdda" path="res://data/cabinets/cabinet_04.tres" id="9_oms42"] +[ext_resource type="Resource" path="res://data/games/missile_command.tres" id="10_sqsy7"] +[ext_resource type="Script" uid="uid://d3pt6qa05kcnl" path="res://core/scenarios/scenario_definition.gd" id="11_2gwsy"] + +[sub_resource type="Resource" id="Resource_p0bbs"] +script = ExtResource("1_40f70") +cabinet = ExtResource("2_nsmc3") +game = ExtResource("4_wb1pw") +metadata/_custom_type_script = "uid://ibujr0ervhhq" + +[sub_resource type="Resource" id="Resource_46jx7"] +script = ExtResource("1_40f70") +cabinet = ExtResource("5_cy2se") +game = ExtResource("6_r5ac2") +metadata/_custom_type_script = "uid://ibujr0ervhhq" + +[sub_resource type="Resource" id="Resource_vuaau"] +script = ExtResource("1_40f70") +cabinet = ExtResource("7_4fvja") +game = ExtResource("8_uer0y") +metadata/_custom_type_script = "uid://ibujr0ervhhq" + +[sub_resource type="Resource" id="Resource_l1iuy"] +script = ExtResource("1_40f70") +cabinet = ExtResource("9_oms42") +game = ExtResource("10_sqsy7") +metadata/_custom_type_script = "uid://ibujr0ervhhq" + +[resource] +script = ExtResource("11_2gwsy") +scenario_id = &"eg_glitch" +display_name = "Glitch: The Dark Machine" +assignments = Array[ExtResource("1_40f70")]([SubResource("Resource_p0bbs"), SubResource("Resource_46jx7"), SubResource("Resource_vuaau"), SubResource("Resource_l1iuy")]) +metadata/_custom_type_script = "uid://d3pt6qa05kcnl" diff --git a/data/scenarios/pacman_rfid_credit_test.tres b/data/scenarios/pacman_rfid_credit_test.tres new file mode 100644 index 0000000..7f74674 --- /dev/null +++ b/data/scenarios/pacman_rfid_credit_test.tres @@ -0,0 +1,18 @@ +[gd_resource type="Resource" script_class="ScenarioDefinition" format=3] + +[ext_resource type="Script" path="res://core/scenarios/scenario_definition.gd" id="1"] +[ext_resource type="Script" path="res://core/scenarios/scenario_assignment.gd" id="2"] +[ext_resource type="Resource" path="res://data/cabinets/cabinet_01.tres" id="3"] +[ext_resource type="Resource" path="res://data/games/pacman.tres" id="4"] + +[sub_resource type="Resource" id="Assignment"] +script = ExtResource("2") +cabinet = ExtResource("3") +game = ExtResource("4") +game_config = {"rfid_credit_device": "cab1_rfid1", "rfid_allowed_uids": [], "boards_to_win": 1, "score_to_win": 0} + +[resource] +script = ExtResource("1") +scenario_id = &"pacman_rfid_credit_test" +display_name = "Cabinet 1: Pac-Man RFID credit" +assignments = Array[ExtResource("2")]([SubResource("Assignment")]) diff --git a/data/scenarios/three_cabinet_objective_test.tres b/data/scenarios/three_cabinet_objective_test.tres new file mode 100644 index 0000000..f56fdb4 --- /dev/null +++ b/data/scenarios/three_cabinet_objective_test.tres @@ -0,0 +1,47 @@ +[gd_resource type="Resource" script_class="ScenarioDefinition" format=3 uid="uid://coylqrwbr26tb"] + +[ext_resource type="Script" uid="uid://d3pt6qa05kcnl" path="res://core/scenarios/scenario_definition.gd" id="1"] +[ext_resource type="Script" uid="uid://ibujr0ervhhq" path="res://core/scenarios/scenario_assignment.gd" id="2"] +[ext_resource type="Resource" uid="uid://cl4j1vtv388pt" path="res://data/cabinets/cabinet_01.tres" id="3"] +[ext_resource type="Script" uid="uid://lixrugm1v2d2" path="res://core/scenarios/scenario_event_mapping.gd" id="3_my636"] +[ext_resource type="Resource" uid="uid://d1gyyfaaclas8" path="res://data/cabinets/cabinet_02.tres" id="4"] +[ext_resource type="Resource" uid="uid://d4ns05ptr4ybw" path="res://data/cabinets/cabinet_03.tres" id="5"] +[ext_resource type="Resource" uid="uid://c3v8o4nubjakg" path="res://data/games/pacman.tres" id="6"] +[ext_resource type="Resource" uid="uid://cnfhiky5834pw" path="res://data/games/galaga.tres" id="7"] +[ext_resource type="Resource" path="res://data/games/frogger.tres" id="8"] + +[sub_resource type="Resource" id="First"] +script = ExtResource("2") +cabinet = ExtResource("3") +game = ExtResource("6") +game_config = { +"boards_to_win": 0, +"score_to_win": 0, +"test_auto_win_seconds": 5.0 +} + +[sub_resource type="Resource" id="Second"] +script = ExtResource("2") +cabinet = ExtResource("4") +game = ExtResource("7") +activate_after_win = &"cabinet_01" +game_config = { +"score_to_win": 500, +"stages_to_win": 0 +} + +[sub_resource type="Resource" id="Third"] +script = ExtResource("2") +cabinet = ExtResource("5") +game = ExtResource("8") +activate_after_win = &"cabinet_02" +game_config = { +"boards_to_win": 0, +"homes_to_win": 1 +} + +[resource] +script = ExtResource("1") +scenario_id = &"three_cabinet_objective_test" +display_name = "3-Cabinet Test: 5 seconds / 500 points / 1 frog" +assignments = Array[ExtResource("2")]([SubResource("First"), SubResource("Second"), SubResource("Third")]) diff --git a/docs/cabinet_pcb_setup.md b/docs/cabinet_pcb_setup.md new file mode 100644 index 0000000..ce3a585 --- /dev/null +++ b/docs/cabinet_pcb_setup.md @@ -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. diff --git a/docs/master_pi_deployment_design.md b/docs/master_pi_deployment_design.md new file mode 100644 index 0000000..52478fa --- /dev/null +++ b/docs/master_pi_deployment_design.md @@ -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). diff --git a/docs/pacman_rfid_first_setup.md b/docs/pacman_rfid_first_setup.md new file mode 100644 index 0000000..0fb2867 --- /dev/null +++ b/docs/pacman_rfid_first_setup.md @@ -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). diff --git a/docs/remote_pi_start.md b/docs/remote_pi_start.md new file mode 100644 index 0000000..69ef411 --- /dev/null +++ b/docs/remote_pi_start.md @@ -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). diff --git a/docs/three_cabinet_test.md b/docs/three_cabinet_test.md new file mode 100644 index 0000000..5e8e625 --- /dev/null +++ b/docs/three_cabinet_test.md @@ -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 +``` diff --git a/editor/hardware/pcb_editor_property.gd b/editor/hardware/pcb_editor_property.gd new file mode 100644 index 0000000..c0ab213 --- /dev/null +++ b/editor/hardware/pcb_editor_property.gd @@ -0,0 +1,209 @@ +@tool +extends EditorProperty + +var open_button: Button +var dialog: ConfirmationDialog +var draft: CabinetPCBConfig +var chosen := "J1" +var buttons: Dictionary = {} +var type_select: OptionButton +var input_pull: CheckBox +var device_name: LineEdit +var device_id: LineEdit +var binding: LineEdit +var notes: TextEdit +var pin_label: Label +var warning_label: Label +var summary: Label +var loading := false + +func _init() -> void: + open_button = Button.new() + open_button.text = "Open PCB connections..." + open_button.pressed.connect(_open) + add_child(open_button) + add_focusable(open_button) + +func _update_property() -> void: + var config: CabinetPCBConfig = get_edited_object().get(get_edited_property()) + open_button.text = "PCB connections (%d)..." % config.connections.size() if config else "Set up PCB connections..." + +func _open() -> void: + var source: CabinetPCBConfig = get_edited_object().get(get_edited_property()) + draft = source.duplicate(true) if source else CabinetPCBConfig.new() + # Rebuild static labels after editor script reloads as well as dynamic fields. + if is_instance_valid(dialog): + dialog.hide() + dialog.queue_free() + buttons.clear() + _build_dialog() + dialog.title = "%s - Custom GPIO routing PCB" % get_edited_object().display_name + _select(chosen) + dialog.popup_centered_clamped(Vector2i(1120, 760), 0.9) + +func _label(text: String, parent: Node) -> Label: + var label := Label.new() + label.text = text + label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + parent.add_child(label) + return label + +func _build_dialog() -> void: + dialog = ConfirmationDialog.new() + dialog.ok_button_text = "Apply connections" + dialog.min_size = Vector2i(700, 550) + dialog.confirmed.connect(_apply) + add_child(dialog) + var scroll := ScrollContainer.new() + dialog.add_child(scroll) + var content := VBoxContainer.new() + content.size_flags_horizontal = Control.SIZE_EXPAND_FILL + scroll.add_child(content) + _label("Select a socket, then describe the device. Apply supports Undo/Redo; save the cabinet resource to keep changes.", content) + var panel := PanelContainer.new() + var style := StyleBoxFlat.new() + style.bg_color = Color("164d3a") + style.content_margin_left = 16 + style.content_margin_right = 16 + style.content_margin_top = 16 + style.content_margin_bottom = 16 + panel.add_theme_stylebox_override("panel", style) + content.add_child(panel) + var board := VBoxContainer.new() + board.add_theme_constant_override("separation", 18) + panel.add_child(board) + var header := Button.new() + header.text = "J9 | Raspberry Pi GPIO ribbon | verify pin 1 orientation" + header.disabled = true + board.add_child(header) + var middle := HBoxContainer.new() + board.add_child(middle) + var group_index := 0 + for group in [["J15", "J14", "J13", "J12", "J11", "J10"], ["J22", "J21", "J20", "J19", "J18", "J17"], ["J16"]]: + var column := VBoxContainer.new() + column.size_flags_horizontal = Control.SIZE_EXPAND_FILL + middle.add_child(column) + _label(["SIGNAL + 3V3", "SIGNAL / GND", "I2C"][group_index], column) + group_index += 1 + var grid := GridContainer.new() + grid.columns = group.size() + grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + column.add_child(grid) + for id in group: + _port_button(id, grid) + _label("READER CONNECTORS | selector BCM22 / BCM27 / BCM17 | shared reset BCM20", board) + var readers := GridContainer.new() + readers.columns = 8 + board.add_child(readers) + for index in range(1, 9): + _port_button("J%d" % index, readers) + summary = _label("", content) + var details := HBoxContainer.new() + details.add_theme_constant_override("separation", 24) + content.add_child(details) + var left := VBoxContainer.new() + left.size_flags_horizontal = Control.SIZE_EXPAND_FILL + details.add_child(left) + pin_label = _label("", left) + _label("GPIO is a control signal. Motors, relays and lighting need an appropriate driver and supply. This editor does not write GPIO or install reader drivers.", left) + var form := VBoxContainer.new() + form.size_flags_horizontal = Control.SIZE_EXPAND_FILL + details.add_child(form) + _label("Connected device", form) + type_select = OptionButton.new() + type_select.item_selected.connect(_type_changed) + form.add_child(type_select) + input_pull = CheckBox.new() + input_pull.text = "Input pull-up (active when connected to GND)" + input_pull.toggled.connect(func(_value: bool): _store()) + form.add_child(input_pull) + device_name = _field("Device name", form) + device_id = _field("Unique device ID (within this cabinet)", form) + binding = _field("Hardware action ID (outputs) / event ID (inputs)", form) + _label("Wiring notes / driver / supply / I2C address", form) + notes = TextEdit.new() + notes.custom_minimum_size.y = 80 + notes.text_changed.connect(_store) + form.add_child(notes) + warning_label = _label("", content) + warning_label.modulate = Color("ffd07a") + +func _field(title: String, parent: Node) -> LineEdit: + _label(title, parent) + var field := LineEdit.new() + field.text_changed.connect(func(_text: String): _store()) + parent.add_child(field) + return field + +func _port_button(id: String, parent: Node) -> void: + var button := Button.new() + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.custom_minimum_size = Vector2(64, 82) + button.toggle_mode = true + button.pressed.connect(_select.bind(id)) + parent.add_child(button) + buttons[id] = button + +func _select(id: String) -> void: + chosen = id + loading = true + var definition := CabinetPCBConfig.port(id) + var item := draft.connection(id) + pin_label.text = "%s | %s\n\n%s" % [id, definition.label, definition.pins] + type_select.clear() + type_select.add_item("Empty / unplugged") + type_select.set_item_metadata(0, "") + for kind in definition.types: + type_select.add_item(CabinetPCBConfig.TYPES[kind]) + type_select.set_item_metadata(type_select.item_count - 1, kind) + type_select.select(0) + if item: + for index in range(type_select.item_count): + if type_select.get_item_metadata(index) == item.device_type: + type_select.select(index) + device_name.text = item.display_name if item else "" + device_id.text = item.device_id if item else "" + binding.text = item.binding_id if item else "" + notes.text = item.notes if item else "" + input_pull.set_pressed_no_signal(item.input_pull_up if item else true) + loading = false + _refresh_board() + +func _type_changed(_index: int) -> void: + _store() + +func _store() -> void: + if loading: + return + var item := PCBConnection.new() + item.port_id = chosen + item.device_type = str(type_select.get_selected_metadata()) + item.display_name = device_name.text.strip_edges() + item.device_id = device_id.text.strip_edges() + item.binding_id = binding.text.strip_edges() + item.notes = notes.text + item.input_pull_up = input_pull.button_pressed + draft.set_connection(item) + _refresh_board() + +func _refresh_board() -> void: + for id in buttons: + var definition := CabinetPCBConfig.port(id) + var item := draft.connection(id) + var button: Button = buttons[id] + button.text = "%s\n%s\n%s" % [id, definition.label, item.device_type.to_upper() if item else "Empty"] + button.set_pressed_no_signal(id == chosen) + button.tooltip_text = "%s\n%s" % [definition.pins, item.display_name if item else "Unassigned"] + var disabled := str(type_select.get_selected_metadata()).is_empty() + device_name.editable = not disabled + device_id.editable = not disabled + binding.editable = not disabled + notes.editable = not disabled + input_pull.visible = str(type_select.get_selected_metadata()) in ["button", "sensor"] + summary.text = "%d connections configured | %s selected" % [draft.connections.size(), chosen] + var issues := draft.issues(get_edited_object().hardware_config) + warning_label.text = "Review wiring:\n- " + "\n- ".join(issues) if not issues.is_empty() else "Inventory is consistent. Runtime wiring and drivers are configured separately." + +func _apply() -> void: + emit_changed(get_edited_property(), draft.duplicate(true)) + _update_property() diff --git a/editor/hardware/pcb_editor_property.gd.uid b/editor/hardware/pcb_editor_property.gd.uid new file mode 100644 index 0000000..3ed135f --- /dev/null +++ b/editor/hardware/pcb_editor_property.gd.uid @@ -0,0 +1 @@ +uid://pqhm21y6t7c6 diff --git a/editor/workspace/escape_room_workspace.gd b/editor/workspace/escape_room_workspace.gd index aee7ee0..221dd64 100644 --- a/editor/workspace/escape_room_workspace.gd +++ b/editor/workspace/escape_room_workspace.gd @@ -231,7 +231,12 @@ var simulation_driver := EditorSimulationDriver.new() var editor_selection := EscapeRoomEditorSelection.new() +var doorway_tool: Button +var doorway_check: CheckBox +var swing_check: CheckBox + func _ready() -> void: + _setup_view_controls() select_tool_button.button_pressed = true @@ -1503,6 +1508,9 @@ func _on_show_connections_check_box_toggled( ) func _clear_property_panel() -> void: + if doorway_check != null: + doorway_check.disabled = true + swing_check.disabled = true selected_label.text = ( "Nothing selected" @@ -1668,6 +1676,7 @@ func _on_wall_creation_requested( wall.start = start wall.end = end wall.thickness = 0.10 + wall.is_doorway = doorway_tool.button_pressed if undo_redo == null: _add_wall( @@ -1828,10 +1837,15 @@ func _refresh_wall_property_panel( return selected_label.text = ( - "Wall: %s" + ("Doorway: %s" if wall.is_doorway else "Wall: %s") % wall.wall_id ) + doorway_check.disabled = false + doorway_check.set_pressed_no_signal(wall.is_doorway) + swing_check.set_pressed_no_signal(wall.swing_reversed) + swing_check.disabled = not wall.is_doorway + wall_start_x_spin_box.set_value_no_signal( wall.start.x * METERS_TO_FEET ) @@ -3001,3 +3015,68 @@ func _on_room_layout_refresh_requested() -> void: status_label.text = ( "Physical room refreshed." ) + + +func _setup_view_controls() -> void: + var toolbar := draw_wall_button.get_parent() + doorway_tool = Button.new() + doorway_tool.text = "DRAW DOORWAY" + doorway_tool.toggle_mode = true + doorway_tool.button_group = draw_wall_button.button_group + doorway_tool.tooltip_text = "Drag from hinge to opposite jamb. Align with a wall to create an opening." + toolbar.add_child(doorway_tool) + toolbar.move_child(doorway_tool, draw_wall_button.get_index() + 1) + doorway_tool.pressed.connect(func(): room_layout.set_edit_mode(RoomLayout.EditMode.DRAW_WALL)) + var view_bar := HBoxContainer.new() + toolbar.get_parent().add_child(view_bar) + toolbar.get_parent().move_child(view_bar, toolbar.get_index() + 1) + for title in ["Labels", "Dimensions"]: + var toggle := CheckBox.new() + toggle.text = title + toggle.button_pressed = true + view_bar.add_child(toggle) + if title == "Labels": + toggle.toggled.connect(room_layout.set_labels_visible) + else: + toggle.toggled.connect(room_layout.set_dimensions_visible) + doorway_check = CheckBox.new() + doorway_check.text = "Doorway opening" + doorway_check.disabled = true + doorway_check.toggled.connect(func(value: bool): _commit_door_setting("is_doorway", value)) + swing_check = CheckBox.new() + swing_check.text = "Reverse door swing" + swing_check.disabled = true + swing_check.toggled.connect(func(value: bool): _commit_door_setting("swing_reversed", value)) + var properties := delete_wall_button.get_parent() + properties.add_child(doorway_check) + properties.add_child(swing_check) + properties.move_child(doorway_check, delete_wall_button.get_index()) + properties.move_child(swing_check, delete_wall_button.get_index()) + +func _commit_door_setting(property: String, value: bool) -> void: + if context == null: + return + var target := context.get_room() + var wall := target.get_wall(room_layout.selected_wall_id) + if wall == null: + doorway_check.set_pressed_no_signal(false) + swing_check.set_pressed_no_signal(false) + return + var old: bool = wall.get(property) + if old == value: + return + if undo_redo == null: + _apply_door_setting(target, wall, property, value) + return + undo_redo.create_action("Edit doorway", UndoRedo.MERGE_DISABLE, target) + undo_redo.add_do_method(self, "_apply_door_setting", target, wall, property, value) + undo_redo.add_undo_method(self, "_apply_door_setting", target, wall, property, old) + undo_redo.commit_action() + +func _apply_door_setting(target: RoomDefinition, wall: RoomWallDefinition, property: String, value: bool) -> void: + if not target.walls.has(wall): + return + wall.set(property, value) + context.room_edit_session.mark_dirty() + room_layout.queue_redraw() + _refresh_wall_property_panel(wall.wall_id) diff --git a/games/frogger/frogger.gd b/games/frogger/frogger.gd index 758afae..a2335c1 100644 --- a/games/frogger/frogger.gd +++ b/games/frogger/frogger.gd @@ -1,6 +1,8 @@ @tool extends ArcadeGame @export_range(0,99) var boards_to_win: int = 1 +## Zero disables the individual-home objective; normal play uses board completion. +@export_range(0,5) var homes_to_win: int = 0 @export_range(1,9) var starting_lives: int = 3 @export_range(15,120) var crossing_time: float = 60.0 @export_range(.5,2) var difficulty: float = 1.0 @@ -139,7 +141,10 @@ func _physics_process(delta: float) -> void: phase = "play" if music_enabled and not demo_active: $Audio.music() "home": - if not homes.has(false): + var target := int(run_config.get("homes_to_win", homes_to_win)) + if not demo_active and target > 0 and homes.count(true) >= target: + finish(true) + elif not homes.has(false): add_score(1000) phase = "clear" phase_time = 2.0 @@ -149,7 +154,7 @@ func _physics_process(delta: float) -> void: "clear": if demo_active: end_demo() - elif boards_to_win > 0 and board_number >= boards_to_win: + elif int(run_config.get("boards_to_win", boards_to_win)) > 0 and board_number >= int(run_config.get("boards_to_win", boards_to_win)): finish(true) else: board_number += 1 diff --git a/games/pacman/pacman.gd b/games/pacman/pacman.gd index 2d2306c..f0240c7 100644 --- a/games/pacman/pacman.gd +++ b/games/pacman/pacman.gd @@ -13,6 +13,40 @@ var model = Model.new() var view var audio var accumulator := 0.0 +var credits := 0 +var rfid_seen: Dictionary = {} +var rfid_initialized := false + +func receive_device_feedback(feedback: Dictionary) -> void: + var events: Array = feedback.get("events", []) + if not rfid_initialized: + if not bool(feedback.get("fresh", false)): return + for entry in events: rfid_seen[str(entry.get("id", ""))] = true + rfid_initialized = true + return + if not bool(feedback.get("fresh", false)): return + var device_id := str(run_config.get("rfid_credit_device", "")) + for entry in events: + var ident := str(entry.get("id", "")) + if ident.is_empty() or rfid_seen.has(ident): continue + rfid_seen[ident] = true + if device_id.is_empty() or is_running or credits > 0 or model.state != "idle": continue + if entry.get("device") != device_id or entry.get("kind") != "rfid" or not str(entry.get("error", "")).is_empty(): continue + var uid := str(entry.get("value", "")).to_upper() + if uid.is_empty(): continue + var allowed: Array = run_config.get("rfid_allowed_uids", []) + if not allowed.is_empty() and not allowed.has(uid): continue + for sample in feedback.get("devices", []): + if sample.get("id") == device_id and int(sample.get("age_ms", 999999)) < 2000 and sample.get("value") == entry.get("value") and str(sample.get("error", "")).is_empty(): + credits = 1 + view.credits = credits + audio.play_effect("credit") + break + # The bridge retains only its last 64 events. Keep deduplication bounded too. + if rfid_seen.size() > 256: + rfid_seen.clear() + for entry in events: rfid_seen[str(entry.get("id", ""))] = true + func _ready() -> void: view = View.new() @@ -26,9 +60,15 @@ func _ready() -> void: func configure(config: Dictionary = {}) -> void: super.configure(config) + credits = 0 + rfid_seen.clear() + rfid_initialized = false _apply_config() func _apply_config() -> void: + if view != null: + view.credit_required = not str(run_config.get("rfid_credit_device", "")).is_empty() + view.credits = credits model.boards_to_win = maxi(0, int(run_config.get("boards_to_win", boards_to_win))) model.score_to_win = maxi(0, int(run_config.get("score_to_win", score_to_win))) model.initial_lives = clampi(int(run_config.get("starting_lives", starting_lives)), 1, 9) @@ -36,6 +76,9 @@ func _apply_config() -> void: func start_game() -> bool: if is_running: return false + if not str(run_config.get("rfid_credit_device", "")).is_empty(): + if credits < 1: return false + credits -= 1 _apply_config() audio.stop_all() model.start() @@ -43,6 +86,8 @@ func start_game() -> bool: return super.start_game() func reset_game() -> void: + credits = 0 + if view != null: view.credits = 0 audio.stop_all() model.reset() accumulator = 0.0 diff --git a/games/pacman/pacman_view.gd b/games/pacman/pacman_view.gd index ebde2f5..9e4ec0e 100644 --- a/games/pacman/pacman_view.gd +++ b/games/pacman/pacman_view.gd @@ -10,6 +10,8 @@ const DEATH_ANIMATION_SECONDS := 1.55 const DEATH_POP_SECONDS := 0.24 var model var debug_grid := false +var credit_required := false +var credits := 0 func _ready() -> void: texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST @@ -50,12 +52,13 @@ func _draw() -> void: _text("HIGH SCORE", Vector2(80, 0)) _text(str(model.score).pad_zeros(2), Vector2(8, 8)) _text(str(model.high_score).pad_zeros(2), Vector2(88, 8)) + if credit_required: _text("CREDIT %d" % credits, Vector2(120, 280)) for i in range(mini(model.lives - 1, 5)): _sprite(Rect2(600, 16, 16, 16), Vector2(16 + i * 16, 280)) - _text("L%d" % model.level, Vector2(192, 276), Color("ffb852")) + if not credit_required: _text("L%d" % model.level, Vector2(192, 276), Color("ffb852")) match model.state: "idle": - _banner("PRESS START", Color.YELLOW) + _banner("SCAN CARD" if credit_required and credits == 0 else "PRESS START", Color.YELLOW) _text("ARROWS TO MOVE", Vector2(56, 256)) "ready": _banner("READY!", Color.YELLOW) diff --git a/setup/pc/ArcadeEscape.pck b/setup/pc/ArcadeEscape.pck index 6cdf308db29a4d21c5d03477c34c02bcdb5bf4a1..a69b707d202381aa729ba6921939c7e38bcbc0a0 100644 GIT binary patch delta 39913 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

}v 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

  • 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

    }v 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

  • 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 void: + if not condition: + failures += 1 + push_error(message) + +func _initialize() -> void: + run.call_deferred() + +func run() -> void: + if "--preview" in OS.get_cmdline_user_args(): + root.gui_embed_subwindows = true + root.size = Vector2i(1200, 850) + var board := CabinetPCBConfig.new() + check(board.ports().size() == 22, "All 22 connectors must be present") + check(board.port("J15").bcm == 21 and board.port("J10").bcm == 5, "Three pin order") + check(board.port("J22").bcm == 25 and board.port("J17").bcm == 16, "Two pin order") + check(board.port("J2").label == "001" and board.port("J8").label == "111", "Reader selector order") + var reader := PCBConnection.new() + reader.port_id = "J1" + reader.device_type = "rfid" + reader.device_id = "reader_left" + reader.display_name = "Left RFID reader" + board.set_connection(reader) + var reader2: PCBConnection = reader.duplicate(true) + reader2.port_id = "J2" + reader2.device_id = "reader_right" + board.set_connection(reader2) + check(board.issues().is_empty(), "Valid readers") + var cabinet := CabinetProfile.new() + cabinet.display_name = "PCB editor test" + cabinet.pcb_config = board + var copy: CabinetProfile = cabinet.duplicate(true) + copy.pcb_config.connection("J1").display_name = "Other cabinet" + check(board.connection("J1").display_name == "Left RFID reader", "Cabinet deep-copy isolation") + var path := "user://pcb_test_roundtrip.tres" + check(ResourceSaver.save(cabinet, path) == OK, "Save cabinet") + var loaded := ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE) as CabinetProfile + check(loaded.pcb_config.connection("J2").device_id == "reader_right", "Save/reload connector data") + DirAccess.remove_absolute(path) + reader2.device_id = reader.device_id + check(not board.issues().is_empty(), "Duplicate IDs reported") + reader2.device_id = "reader_right" + var hardware := HardwareConfig.new() + var action := HardwareAction.new() + action.action_id = &"lamp" + action.gpio_pin = 17 + hardware.actions.append(action) + check(not board.issues(hardware).is_empty(), "Reserved selector pin reported") + var output := PCBConnection.new() + output.port_id = "J22" + output.device_type = "led" + output.device_id = "lamp" + output.binding_id = "lamp" + board.set_connection(output) + action.gpio_pin = 18 + check(not board.issues(hardware).is_empty(), "Output BCM mismatch reported") + action.gpio_pin = 25 + check(board.issues(hardware).is_empty(), "Matching output action accepted") + cabinet.hardware_config = hardware + if not Engine.is_editor_hint(): + print("PCB resource tests: ", "PASS" if failures == 0 else "FAIL") + quit(failures) + return + var property := preload("res://editor/hardware/pcb_editor_property.gd").new() + root.add_child(property) + property.set_object_and_property(cabinet, "pcb_config") + property._update_property() + property._open() + property._select("J2") + check(property.device_id.text == "reader_right", "Editor selects correct reader") + property.device_name.text = "Pending edit" + property._store() + check(board.connection("J2").display_name != "Pending edit", "Draft cannot change source before Apply") + property.dialog.hide() + property._open() + check(property.device_name.text != "Pending edit", "Cancel/reopen discards draft") + property._select("J22") + property.device_name.text = "Reward lamp" + property._store() + property.property_changed.connect(func(_key: StringName, value: Variant, _field: StringName, _changing: bool): cabinet.pcb_config = value) + property._apply() + check(cabinet.pcb_config.connection("J22").display_name == "Reward lamp", "Apply emits updated configuration") + if "--preview" in OS.get_cmdline_user_args(): + root.size = Vector2i(1200, 850) + property.dialog.popup_centered(Vector2i(1120, 760)) + await process_frame + await process_frame + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png("user://pcb_editor_preview.png") + print("Preview: ", OS.get_user_data_dir(), "/pcb_editor_preview.png") + property.queue_free() + await process_frame + print("PCB tests: ", "PASS" if failures == 0 else "FAIL") + quit(failures) diff --git a/tests/pcb/check_pcb.gd.uid b/tests/pcb/check_pcb.gd.uid new file mode 100644 index 0000000..732e881 --- /dev/null +++ b/tests/pcb/check_pcb.gd.uid @@ -0,0 +1 @@ +uid://cbtyukgnhlhts diff --git a/tests/pcb/check_rfid_credit.gd b/tests/pcb/check_rfid_credit.gd new file mode 100644 index 0000000..8fa477a --- /dev/null +++ b/tests/pcb/check_rfid_credit.gd @@ -0,0 +1,42 @@ +extends SceneTree +var failures := 0 +func check(ok: bool, message: String) -> void: + if not ok: + failures += 1 + push_error(message) +func sample(id: String, uid := "1234ABCD", device := "cab1_rfid1", fresh := true, age := 0) -> Dictionary: + return {"fresh": fresh, "events": [{"id": id, "device": device, "kind": "rfid", "value": uid, "error": ""}], "devices": [{"id": device, "value": uid, "age_ms": age, "error": ""}]} +func _initialize() -> void: + call_deferred("run") +func run() -> void: + var game = load("res://games/pacman/pacman.gd").new() + root.add_child(game) + game.configure({"rfid_credit_device": "cab1_rfid1", "rfid_allowed_uids": ["1234ABCD"]}) + check(not game.start_game(), "Start must require a credit") + game.receive_device_feedback({}) + game.receive_device_feedback(sample("old")) + check(game.credits == 0, "First fresh snapshot must baseline historical scans") + game.receive_device_feedback(sample("wrong-reader", "1234ABCD", "other")) + game.receive_device_feedback(sample("wrong-card", "99999999")) + game.receive_device_feedback(sample("stale", "1234ABCD", "cab1_rfid1", true, 5000)) + game.receive_device_feedback(sample("offline", "1234ABCD", "cab1_rfid1", false)) + check(game.credits == 0, "Wrong reader/card, stale and offline samples must not credit") + game.receive_device_feedback(sample("scan")) + check(game.credits == 1 and game.view.credits == 1, "New accepted scan grants one visible credit") + game.receive_device_feedback(sample("scan")) + game.receive_device_feedback(sample("scan2")) + check(game.credits == 1, "Duplicate and additional scans must not stack credits") + check(game.start_game() and game.credits == 0, "Start consumes one credit") + game.receive_device_feedback(sample("during-play")) + check(game.credits == 0, "Playing scans must not bank credits") + game.reset_game() + game.receive_device_feedback(sample("scan")) + check(game.credits == 0, "Reset must not replay a prior event") + game.receive_device_feedback(sample("new-scan")) + check(game.credits == 1, "New scan works after reset") + game.configure({}) + check(game.start_game(), "Ordinary non-RFID Pac-Man remains free start") + game.queue_free() + await process_frame + print("RFID credit tests: ", "PASS" if failures == 0 else "FAIL") + quit(1 if failures else 0) diff --git a/tests/room_editor/check_room_view.gd b/tests/room_editor/check_room_view.gd new file mode 100644 index 0000000..21fcf01 --- /dev/null +++ b/tests/room_editor/check_room_view.gd @@ -0,0 +1,64 @@ +extends SceneTree +var failures := 0 +func check(value: bool, message: String) -> void: + if not value: + failures += 1 + push_error(message) +func _initialize() -> void: + run.call_deferred() +func run() -> void: + var room := RoomDefinition.new() + var wall := RoomWallDefinition.new() + wall.wall_id = &"wall" + wall.end = Vector2(4, 0) + var door := RoomWallDefinition.new() + door.wall_id = &"door" + door.start = Vector2(1, 0) + door.end = Vector2(2, 0) + door.is_doorway = true + room.walls.assign([wall, door]) + var path := "user://room_door_test.tres" + check(ResourceSaver.save(room, path) == OK, "Initial room save") + room.take_over_path(path) + var session := RoomEditSession.new() + session.begin(room) + session.working_room.get_wall(&"door").swing_reversed = true + session.mark_dirty() + check(session.is_dirty, "Door swing participates in dirty tracking") + check(not door.swing_reversed, "Source isolated from edits") + check(session.save(), "Save room") + var loaded := ResourceLoader.load(path, "", ResourceLoader.CACHE_MODE_IGNORE) as RoomDefinition + check(loaded.get_wall(&"door").is_doorway and loaded.get_wall(&"door").swing_reversed, "Door save/reload") + session.working_room.get_wall(&"door").is_doorway = false + session.mark_dirty() + check(session.is_dirty, "Door type participates in dirty tracking") + session.revert() + check(session.working_room.get_wall(&"door").is_doorway, "Revert restores doorway") + DirAccess.remove_absolute(path) + room.walls.assign([wall, door]) + var layout := RoomLayout.new() + layout.item_scene = load("res://ui/RoomLayoutItem.tscn") + root.add_child(layout) + var device := RoomDeviceDefinition.new() + device.device_id = &"lamp" + device.display_name = "Reward lamp" + room.devices.append(device) + layout.configure(room, null) + var ranges := layout._wall_solid_ranges(wall) + check(ranges == [Vector2(0, 0.25), Vector2(0.5, 1)], "Door cuts wall opening") + door.start = Vector2(2, 0) + door.end = Vector2(1, 0) + check(layout._wall_solid_ranges(wall) == ranges, "Reversed doorway endpoints") + check(layout._find_wall_at_screen_position(layout.world_to_screen(Vector2(1.5, 0))) == door, "Door selected ahead of underlying wall") + layout.set_labels_visible(false) + check(not layout.item_nodes[&"lamp"].title_label.visible, "Hide item label") + layout.configure(room, null) + check(not layout.item_nodes[&"lamp"].title_label.visible, "Label preference survives rebuild") + layout.set_labels_visible(true) + check(layout.item_nodes[&"lamp"].title_label.visible, "Restore item label") + layout.set_dimensions_visible(false) + check(not layout.show_dimensions, "Dimension visibility") + layout.queue_free() + await process_frame + print("Room editor tests: ", "PASS" if failures == 0 else "FAIL") + quit(failures) diff --git a/tests/room_editor/check_room_view.gd.uid b/tests/room_editor/check_room_view.gd.uid new file mode 100644 index 0000000..deaefc1 --- /dev/null +++ b/tests/room_editor/check_room_view.gd.uid @@ -0,0 +1 @@ +uid://0p3i2u8rm2x4 diff --git a/tests/room_network/catalog_fixture.json b/tests/room_network/catalog_fixture.json new file mode 100644 index 0000000..3a9e395 --- /dev/null +++ b/tests/room_network/catalog_fixture.json @@ -0,0 +1,440 @@ +{ + "cabinets": { + "cabinet_01": { + "actions": [ + "coin_release", + "start_light" + ], + "capabilities": [ + "joystick_4way", + "button_1", + "start_button", + "joystick_horizontal", + "button_2", + "button_3", + "button_4", + "button_5", + "button_6", + "select_button", + "trackball", + "joystick_8way", + "joystick_dual" + ], + "name": "Master Cabinet", + "position": [ + -5.94849967956543, + 0.147500038146973 + ], + "devices": [ + { + "id": "cab1_btn_red", + "kind": "input", + "name": "Red Button", + "port": "J17", + "pull_up": true + }, + { + "id": "cab1_btn_grn", + "kind": "input", + "name": "Green Button", + "port": "J18", + "pull_up": true + }, + { + "id": "cab1_btn_blue", + "kind": "input", + "name": "Blue Button", + "port": "J19", + "pull_up": true + }, + { + "id": "cab1_btn_ylw", + "kind": "input", + "name": "Yellow Btn", + "port": "J20", + "pull_up": true + }, + { + "id": "cab1_rfid2", + "kind": "rfid", + "name": "RFID Reader 2", + "port": "J2", + "pull_up": true + }, + { + "id": "cab1_rfid1", + "kind": "rfid", + "name": "RFID Reader 1", + "port": "J1", + "pull_up": true + } + ] + }, + "cabinet_02": { + "actions": [ + "coin_release", + "start_light", + "glitch_code" + ], + "capabilities": [ + "joystick_4way", + "button_1", + "start_button", + "joystick_horizontal" + ], + "name": "Cabinet 02", + "position": [ + -3.51539182662964, + 0.142745211720467 + ] + }, + "cabinet_03": { + "actions": [ + "coin_release" + ], + "capabilities": [ + "spinner", + "button_1", + "start_button", + "joystick_4way" + ], + "name": "Cabinet 03", + "position": null + } + }, + "games": { + "frogger": { + "alternatives": [], + "name": "Frogger", + "requirements": [ + "joystick_4way", + "start_button" + ] + }, + "galaga": { + "alternatives": [], + "name": "Galaga", + "requirements": [ + "joystick_horizontal", + "button_1", + "start_button" + ] + }, + "missile_command": { + "alternatives": [], + "name": "Missile Command", + "requirements": [ + "trackball", + "button_1", + "button_2", + "button_3", + "start_button" + ] + }, + "pacman": { + "alternatives": [], + "name": "Pac-Man", + "requirements": [ + "joystick_4way", + "start_button" + ] + }, + "robotron": { + "alternatives": [ + [ + "dual_joystick_8way" + ], + [ + "joystick_8way", + "button_1", + "button_2", + "button_3", + "button_4" + ] + ], + "name": "Robotron 2084", + "requirements": [ + "start_button" + ] + }, + "space_invaders": { + "alternatives": [], + "name": "Space Invaders", + "requirements": [ + "joystick_horizontal", + "button_1", + "start_button" + ] + }, + "tempest": { + "alternatives": [ + [ + "joystick_horizontal" + ], + [ + "spinner" + ], + [ + "trackball" + ] + ], + "name": "Tempest", + "requirements": [ + "button_1", + "button_2", + "start_button" + ] + } + }, + "layout": { + "boundary": [ + [ + -7.31519985198975, + -0.609600007534027 + ], + [ + -2.13359999656677, + -0.609600007534027 + ], + [ + -2.13359999656677, + 7.61999988555908 + ], + [ + -7.31519985198975, + 7.61999988555908 + ] + ], + "devices": [ + { + "id": "coin_release_a", + "name": "Coin Release A", + "position": [ + -6.0149998664856, + 0.555000007152557 + ] + }, + { + "id": "coin_release_b", + "name": "Coin Release B", + "position": [ + -3.57996416091919, + 0.568386673927307 + ] + }, + { + "id": "light_a", + "name": "Light A", + "position": [ + -6.46999979019165, + 0.560000002384186 + ] + }, + { + "id": "light_b", + "name": "Light B", + "position": [ + -4.0322380065918, + 0.576531648635864 + ] + }, + { + "id": "glitch_code", + "name": "Glitch Code", + "position": [ + -2.75500297546387, + 1.51234555244446 + ] + } + ], + "walls": [ + [ + [ + -7.01039981842041, + -0.304800003767014 + ], + [ + -2.43840003013611, + -0.304800003767014 + ] + ], + [ + [ + -2.43840003013611, + -0.304800003767014 + ], + [ + -2.43840003013611, + 7.31519985198975 + ] + ], + [ + [ + -2.43840003013611, + 7.31519985198975 + ], + [ + -7.01039981842041, + 7.31519985198975 + ] + ], + [ + [ + -7.01039981842041, + 7.31519985198975 + ], + [ + -7.01039981842041, + -0.304800003767014 + ] + ] + ] + }, + "scenarios": { + "cabinet_chain_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "cabinet_01", + "cabinet": "cabinet_02", + "game": "galaga", + "mappings": {}, + "variant": "standard" + } + ], + "name": "Cabinet 1 -> Cabinet 2" + }, + "galaga_proof_of_concept": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "galaga", + "mappings": { + "game_won": [ + "coin_release" + ] + }, + "variant": "standard" + } + ], + "name": "Galaga Proof of Concept" + }, + "proof_of_concept": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "mappings": { + "game_won": [ + "coin_release" + ] + }, + "variant": "standard" + } + ], + "name": "Proof of Concept" + }, + "two_cabinet_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "mappings": { + "game_started": [ + "start_light" + ], + "game_won": [ + "coin_release" + ] + }, + "variant": "standard" + }, + { + "activate_after_win": "", + "cabinet": "cabinet_02", + "game": "galaga", + "mappings": { + "game_activated": [ + "glitch_code" + ], + "game_started": [ + "start_light" + ], + "game_won": [ + "coin_release" + ] + }, + "variant": "standard" + } + ], + "name": "Two Cabinet Test" + }, + "three_cabinet_objective_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "game_config": { + "boards_to_win": 0, + "score_to_win": 0, + "test_auto_win_seconds": 5.0 + }, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "cabinet_01", + "cabinet": "cabinet_02", + "game": "galaga", + "game_config": { + "score_to_win": 500, + "stages_to_win": 0 + }, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "cabinet_02", + "cabinet": "cabinet_03", + "game": "frogger", + "game_config": { + "boards_to_win": 0, + "homes_to_win": 1 + }, + "mappings": {}, + "variant": "standard" + } + ], + "name": "3-Cabinet Test: 5 seconds / 500 points / 1 frog" + }, + "pacman_rfid_credit_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "game_config": { + "boards_to_win": 1, + "rfid_allowed_uids": [], + "rfid_credit_device": "cab1_rfid1", + "score_to_win": 0 + }, + "mappings": {}, + "variant": "standard" + } + ], + "name": "Cabinet 1: Pac-Man RFID credit" + } + }, + "version": 1 +} \ No newline at end of file diff --git a/tests/room_network/check_objective_defaults.gd b/tests/room_network/check_objective_defaults.gd new file mode 100644 index 0000000..bc7c050 --- /dev/null +++ b/tests/room_network/check_objective_defaults.gd @@ -0,0 +1,52 @@ +extends SceneTree +func _initialize() -> void: + call_deferred("run") +func run() -> void: + var scenario: ScenarioDefinition = load("res://data/scenarios/three_cabinet_objective_test.tres") + var edit := ScenarioEditSession.new() + assert(edit.begin(scenario)) + assert(edit.working_scenario.assignments[1].game_config.score_to_win == 500) + edit.working_scenario.assignments[1].game_config.score_to_win = 600 + edit.mark_dirty() + assert(edit.is_dirty) + assert(scenario.assignments[1].game_config.score_to_win == 500, "Editing must not mutate source") + var frogger = load("res://games/frogger/Frogger.tscn").instantiate() + root.add_child(frogger) + frogger.configure({}) + frogger.start_game() + frogger.homes[2] = true + frogger.phase = "home" + frogger.phase_time = 0.0 + frogger._physics_process(.01) + assert(frogger.is_running, "Default Frogger must not win on one home") + frogger.reset_game() + frogger.configure({"homes_to_win": 1, "boards_to_win": 0}) + frogger.start_game() + frogger.homes[2] = true + frogger.phase = "home" + frogger.phase_time = 0.0 + frogger._physics_process(.01) + assert(not frogger.is_running and frogger.phase == "won") + frogger.queue_free() + var agent = load("res://core/network/managed_cabinet.gd").new() + # Keep this isolated agent outside the tree: no network polling. + var game := ArcadeGame.new() + agent.add_child(game) + agent.game = game + game.configure({"test_auto_win_seconds": 5.0}) + game.start_game() + agent.event("started") + agent._process(4.0) + assert(game.is_running) + agent.unload() + var replacement := ArcadeGame.new() + agent.add_child(replacement) + agent.game = replacement + replacement.start_game() + agent.event("started") + agent._process(20.0) + assert(replacement.is_running, "Old timer must not affect the next game") + agent.free() + await process_frame + print("PASS: default Frogger, configured home objective, editor copy isolation, and timer cancellation") + quit() diff --git a/tests/room_network/check_objective_defaults.gd.uid b/tests/room_network/check_objective_defaults.gd.uid new file mode 100644 index 0000000..49b43e7 --- /dev/null +++ b/tests/room_network/check_objective_defaults.gd.uid @@ -0,0 +1 @@ +uid://di2ndju42rh4t diff --git a/tests/room_network/check_objective_integration.py b/tests/room_network/check_objective_integration.py new file mode 100644 index 0000000..09c1781 --- /dev/null +++ b/tests/room_network/check_objective_integration.py @@ -0,0 +1,50 @@ +"""Three real game clients; no GPIO, no live deployment database.""" +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import threading +import time +import urllib.request +from http.server import HTTPServer +from test_room import ROOT, server + +with tempfile.TemporaryDirectory() as temp: + fleet=server.Fleet(Path(temp)/'chain.db',json.loads((ROOT/'tools/room_controller/catalog.json').read_text())) + service=HTTPServer(('127.0.0.1',0),server.handler_for(fleet,'test-operator-token',{'cabinet_01':'test-token-one-123','cabinet_02':'test-token-two-123','cabinet_03':'test-token-three-123'})) + thread=threading.Thread(target=service.serve_forever,daemon=True) + thread.start() + url='http://127.0.0.1:'+str(service.server_port) + processes=[] + def post(path,data={}): + request=urllib.request.Request(url+path,data=json.dumps(data).encode(),headers={'Authorization':'Bearer test-operator-token'}) + with urllib.request.urlopen(request,timeout=3) as response: return json.load(response) + def wait(predicate): + deadline=time.monotonic()+30 + while time.monotonic() 499/500 score boundary -> one Frogger home -> all three wins') + finally: + for process in processes: + if process.poll() is None: + process.terminate(); process.wait(timeout=5) + service.shutdown(); service.server_close(); thread.join() + for room in fleet.rooms.values(): room.db.close() diff --git a/tests/room_network/objective_agent.gd b/tests/room_network/objective_agent.gd new file mode 100644 index 0000000..8f9db6b --- /dev/null +++ b/tests/room_network/objective_agent.gd @@ -0,0 +1,36 @@ +extends SceneTree +func _initialize() -> void: + call_deferred("run") +func run() -> void: + var agent = load("res://core/network/managed_cabinet.gd").new() + root.add_child(agent) + var injected := false + var started_at := -1 + for i in range(350): + await create_timer(.1).timeout + if agent.phase == "playing" and started_at < 0: started_at = Time.get_ticks_msec() + if agent.phase == "playing" and not injected: + if agent.cabinet_id == "cabinet_02": + injected = true + agent.game._add_score(499) + assert(agent.game.is_running and agent.game.state != "victory", "499 must not win") + await create_timer(.3).timeout + assert(agent.phase == "playing") + agent.game._add_score(1) + elif agent.cabinet_id == "cabinet_03" and agent.game.phase == "play": + injected = true + assert(agent.game.homes.count(true) == 0) + agent.game.frog = Vector2(112,40) + agent.game.hop_left = 0 + agent.game.step_world(0.0) + assert(agent.game.homes.count(true) == 1) + if agent.phase == "won": + if agent.cabinet_id == "cabinet_01": + assert(Time.get_ticks_msec()-started_at >= 4800, "Timer must run for five seconds") + elif agent.cabinet_id == "cabinet_02": assert(agent.game.score == 500) + elif agent.cabinet_id == "cabinet_03": assert(agent.game.homes.count(true) == 1) + await create_timer(1.0).timeout + quit() + return + push_error("Objective integration timed out on " + agent.cabinet_id) + quit(1) diff --git a/tests/room_network/objective_agent.gd.uid b/tests/room_network/objective_agent.gd.uid new file mode 100644 index 0000000..147b32d --- /dev/null +++ b/tests/room_network/objective_agent.gd.uid @@ -0,0 +1 @@ +uid://drmhfkifu2igv diff --git a/tests/room_network/test_deploy.py b/tests/room_network/test_deploy.py new file mode 100644 index 0000000..72a952c --- /dev/null +++ b/tests/room_network/test_deploy.py @@ -0,0 +1,167 @@ +import importlib.util +import io +import json +from pathlib import Path +import sys +import tarfile +import tempfile +import unittest +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT / 'tools/deploy')) +import deploy +import receiver + + +def site(): + return dict(schema=1, operator_token='operator_token_12345678', controller_url='http://192.168.77.1:8766', + cabinets=[dict(id='cabinet_01', host='192.168.77.11', user='arcade', port=22, + token='cabinet1_token_12345678', gpio=False, pins=[], gpio_controller='cabinet_01_gpio')]) + + +def archive(extra=None, missing=False, wrong_hash=False): + import hashlib + files = {name: b'example' for name in receiver.FILES} + manifest = dict(release='test-1', files={name: hashlib.sha256(data).hexdigest() for name, data in files.items()}) + if wrong_hash: + manifest['files']['ArcadeEscape.pck'] = '0' * 64 + stream = io.BytesIO() + with tarfile.open(fileobj=stream, mode='w:gz') as tar: + entries = [('manifest.json', json.dumps(manifest).encode())] + list(files.items()) + if missing: entries.pop() + for name, data in entries: + item = tarfile.TarInfo(name) + item.size = len(data) + tar.addfile(item, io.BytesIO(data)) + if extra: + tar.addfile(extra) + stream.seek(0) + return stream + + +class DeployTests(unittest.TestCase): + def test_config_separates_credentials_and_keeps_gpio_off(self): + with tempfile.TemporaryDirectory() as temp: + folder = Path(temp) + settings = site() + deploy.generate(folder, settings) + cabinet = (folder / 'cabinets/cabinet_01.ini').read_text() + self.assertNotIn(settings['operator_token'], cabinet) + self.assertIn('gpio = false', cabinet) + self.assertIn('~/.local/state/arcade_gpio/requests.sqlite3', cabinet) + self.assertIn(settings['operator_token'], (folder / 'controller.ini').read_text()) + + def test_duplicate_identity_host_and_tokens_rejected(self): + for change in ('id', 'host', 'token'): + settings = site() + other = dict(settings['cabinets'][0], id='cabinet_02', host='192.168.77.12', token='cabinet2_token_12345678') + other[change] = settings['cabinets'][0][change] + settings['cabinets'].append(other) + with self.subTest(change=change), self.assertRaises(ValueError): + deploy.validate(settings) + + def test_unknown_profile_and_command_injection_rejected(self): + for key, value in [('id', 'unknown'), ('host', 'pi; reboot'), ('user', '-root'), ('port', True)]: + settings = site() + settings['cabinets'][0][key] = value + with self.subTest(key=key), self.assertRaises(ValueError): + deploy.validate(settings) + + def test_bad_url_and_gpio_allowlists_rejected(self): + for url in ('http://127.0.0.1:8766', 'http://user:secret@192.168.77.1:8766', 'http://192.168.77.1:8766/path'): + settings = site() + settings['controller_url'] = url + with self.assertRaises(ValueError): deploy.validate(settings) + for pins in ([], [17, 17], [28], [True]): + with self.assertRaises(ValueError): + receiver.render_config(dict(site()['cabinets'][0], url=site()['controller_url'], gpio=True, pins=pins)) + + def test_archive_success_and_retry(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + receiver.stage(root, archive()) + receiver.stage(root, archive()) + self.assertEqual(receiver.verify_release(root / 'releases/test-1')['release'], 'test-1') + self.assertFalse((root / 'active.json').exists()) + + def test_archive_traversal_symlinks_duplicates_and_corruption_rejected(self): + cases = [] + cases.append(archive(extra=tarfile.TarInfo('../escape'))) + link = tarfile.TarInfo('unexpected') + link.type = tarfile.SYMTYPE + link.linkname = '/etc/passwd' + cases.append(archive(extra=link)) + cases.append(archive(extra=tarfile.TarInfo('ArcadeEscape.pck'))) + cases.append(archive(missing=True)) + cases.append(archive(wrong_hash=True)) + for stream in cases: + with tempfile.TemporaryDirectory() as temp, self.assertRaises(ValueError): + receiver.unpack(stream, Path(temp)) + + def test_ssh_is_strict_and_no_token_in_command(self): + with tempfile.TemporaryDirectory() as temp: + folder = Path(temp) + (folder / 'ssh').mkdir() + (folder / 'ssh/arcade_cabinets').touch() + command = deploy.ssh_args(folder, site()['cabinets'][0]) + self.assertIn('StrictHostKeyChecking=yes', command) + self.assertIn('BatchMode=yes', command) + self.assertNotIn(site()['cabinets'][0]['token'], str(command)) + + def test_activation_refuses_running_game_before_writing_config(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) + with patch.object(receiver, 'verify_release'), patch.object(receiver, 'render_config', return_value='config'), \ + patch.object(receiver.Path, 'read_text', return_value=json.dumps({'cabinets': {'cabinet_01': {}}})), \ + patch.object(receiver.subprocess, 'run') as run: + run.return_value.returncode = 0 + with self.assertRaisesRegex(ValueError, 'running'): + receiver.activate(root, 'test-1', site()['cabinets'][0]) + self.assertFalse((root / 'releases/test-1/tools/launch/launcher.ini').exists()) + + def test_setup_rerun_keeps_credentials(self): + with tempfile.TemporaryDirectory() as temp: + folder = Path(temp) + settings = site() + receiver.private_write(folder / 'inventory.json', json.dumps(settings)) + with patch('builtins.input', side_effect=['', '']): + deploy.setup(folder) + self.assertEqual(deploy.load(folder), settings) + + def test_activation_failure_restores_previous_service_and_config(self): + with tempfile.TemporaryDirectory() as temp: + root = Path(temp) / 'deploy' + home = Path(temp) / 'home' + folder = root / 'releases/test-1' + (folder / 'tools/room_controller').mkdir(parents=True) + (folder / 'tools/room_controller/catalog.json').write_text(json.dumps({'cabinets': {'cabinet_01': {}}})) + config = folder / 'tools/launch/launcher.ini' + receiver.private_write(config, 'old configuration') + unit = home / '.config/systemd/user/arcade-cabinet.service' + receiver.private_write(unit, receiver.MARKER + '\nold unit') + settings = dict(site()['cabinets'][0], url=site()['controller_url']) + def run(command, **kwargs): + import subprocess + if command[0] == sys.executable: + receiver.private_write(unit, receiver.MARKER + '\nchanged unit') + raise subprocess.CalledProcessError(1, command) + return subprocess.CompletedProcess(command, 3 if 'is-active' in command else 0) + import subprocess + with patch.object(receiver, 'verify_release'), patch.object(receiver.Path, 'home', return_value=home), \ + patch.object(receiver.subprocess, 'run', side_effect=run), self.assertRaises(subprocess.CalledProcessError): + receiver.activate(root, 'test-1', settings) + self.assertEqual(config.read_text(), 'old configuration') + self.assertEqual(unit.read_text(), receiver.MARKER + '\nold unit') + self.assertFalse((root / 'active.json').exists()) + + def test_master_package_excludes_live_site_data(self): + with tempfile.TemporaryDirectory() as temp: + target = Path(temp) / 'master' + deploy.master_package(target) + names = [str(p.relative_to(target)) for p in target.rglob('*') if p.is_file()] + self.assertTrue((target / 'tools/deploy/deploy.py').exists()) + self.assertFalse(any(n.endswith(('.ini', '.sqlite3', '.cfg')) for n in names)) + + +if __name__ == '__main__': unittest.main() diff --git a/tests/room_network/test_feedback.py b/tests/room_network/test_feedback.py new file mode 100644 index 0000000..13843a8 --- /dev/null +++ b/tests/room_network/test_feedback.py @@ -0,0 +1,24 @@ +import unittest +from test_room import server +class FeedbackTests(unittest.TestCase): + def test_authenticated_poll_feedback_dedup_and_stale(self): + now=[100] + room=server.Room(':memory:',lambda:now[0]) + packet=dict(cabinet='cabinet_01',protocol=2,boot='test',gpio='real',reports=[],feedback=dict(fresh=True,devices=[dict(id='reader',kind='rfid',port='J1',value='12345678',age_ms=0)],events=[dict(id='boot:1',device='reader',value='12345678')])) + room.poll(packet);room.poll(packet) + self.assertTrue(room.snapshot()['feedback']['fresh']) + self.assertEqual(sum('Device reader' in x['message'] for x in room.state['history']),1) + now[0]+=5 + self.assertFalse(room.snapshot()['feedback']['fresh']) + packet['feedback']['fresh']=False + room.poll(packet) + self.assertFalse(room.snapshot()['feedback']['fresh']) + room.db.close() + def test_invalid_feedback_is_bounded(self): + room=server.Room(':memory:',lambda:100) + room.receive_feedback(dict(devices=[{}]*33,events=[])) + self.assertEqual(room.feedback,{}) + room.receive_feedback(dict(devices=[dict(id='bad',kind='input',value=[],age_ms=0)],fresh=True)) + self.assertEqual(room.feedback['devices'],[]) + self.assertFalse(room.feedback['fresh']) + room.db.close() diff --git a/tests/room_network/test_fleet.py b/tests/room_network/test_fleet.py index dfd3cc2..4a4baa4 100644 --- a/tests/room_network/test_fleet.py +++ b/tests/room_network/test_fleet.py @@ -13,7 +13,8 @@ import unittest class FleetTests(unittest.TestCase): def setUp(self): self.now = 100 - self.catalog = json.loads((ROOT / 'tools/room_controller/catalog.json').read_text()) + # Fixed historical test room: designer edits must not alter protocol assertions. + self.catalog = json.loads(Path(__file__).with_name('catalog_fixture.json').read_text(encoding='utf-8')) self.fleet = server.Fleet(':memory:', self.catalog, lambda: self.now) for cabinet in self.fleet.rooms: self.poll(cabinet) @@ -54,6 +55,9 @@ class FleetTests(unittest.TestCase): self.assertFalse(self.poll('cabinet_01')['commands']) def test_compatibility_rejects_missing_controls_and_accepts_alternatives(self): + # Explicit fixture: actual cabinet hardware can evolve independently. + self.catalog['cabinets']['cabinet_01']['capabilities'] = ['joystick_4way', 'button_1', 'start_button'] + self.catalog['cabinets']['cabinet_03']['capabilities'] = ['spinner', 'button_1', 'start_button'] with self.assertRaisesRegex(ValueError, 'hardware'): self.fleet.operator('prepare', {'cabinet': 'cabinet_01', 'game': 'missile_command'}) cabinet = self.catalog['cabinets']['cabinet_03'] diff --git a/tests/room_network/test_hub.py b/tests/room_network/test_hub.py new file mode 100644 index 0000000..f1ccc08 --- /dev/null +++ b/tests/room_network/test_hub.py @@ -0,0 +1,54 @@ +import json +import tempfile +import unittest +from pathlib import Path +from test_room import ROOT, server + +class HubTests(unittest.TestCase): + def setUp(self): + self.temp = tempfile.TemporaryDirectory() + self.catalog = json.loads(Path(__file__).with_name('catalog_fixture.json').read_text(encoding='utf-8')) + self.path = str(Path(self.temp.name) / 'state.sqlite3') + self.fleet = server.Fleet(self.path, self.catalog, lambda: 100) + self.room = self.fleet.rooms['cabinet_01'] + self.room.last_seen = 100 + self.room.gpio = 'real' + self.room.state.update(session='test-session', phase='ready') + self.data = dict(cabinet='cabinet_01', expected_session='test-session', action_id='coin_release', request_id='test-request-0001', reason='Test release') + def tearDown(self): + for r in self.fleet.rooms.values(): r.db.close() + self.temp.cleanup() + def test_durable_duplicate_does_not_repeat_output(self): + self.fleet.operator('trigger_output', self.data) + self.fleet.operator('trigger_output', self.data) + self.assertEqual(len(self.room.state['commands']), 1) + for r in self.fleet.rooms.values(): r.db.close() + self.fleet = server.Fleet(self.path, self.catalog, lambda: 100) + self.fleet.operator('trigger_output', self.data) + self.assertEqual(len(self.fleet.rooms['cabinet_01'].state['commands']), 1) + self.assertTrue(any('Test release' in h['message'] for h in self.fleet.rooms['cabinet_01'].state['history'])) + def test_rejects_unsafe_or_stale_requests(self): + for key,value in [('expected_session','old'),('action_id','unknown'),('reason',''),('request_id','short')]: + with self.subTest(key=key), self.assertRaises(ValueError): + self.fleet.operator('trigger_output', dict(self.data, **{key:value})) + self.room.gpio = 'simulation' + with self.assertRaises(ValueError): self.fleet.operator('trigger_output', self.data) + self.room.gpio = 'real' + self.room.last_seen = 0 + with self.assertRaises(ValueError): self.fleet.operator('trigger_output', self.data) + self.assertFalse(self.room.state['commands']) + def test_pending_and_unprepared_rejected(self): + self.room.state['phase']='idle' + with self.assertRaises(ValueError): self.fleet.operator('trigger_output',self.data) + self.room.state['phase']='ready' + self.fleet.operator('trigger_output',self.data) + with self.assertRaises(ValueError): self.fleet.operator('trigger_output',dict(self.data,request_id='test-request-0002')) + def test_hub_config(self): + p=Path(self.temp.name)/'hub.json' + service=dict(id='trivia',name='Trivia',kind='trivia',url='http://trivia.local:8080') + p.write_text(json.dumps(dict(services=[service]))) + self.assertEqual(server.hub_config(p)['room_name'],'Glitch: the Dark Machine') + for url in ['javascript:alert(1)','http://user:password@host','http://host/?token=secret','http://host/#secret','http://host:invalid']: + p.write_text(json.dumps(dict(services=[dict(service,url=url)]))) + with self.subTest(url=url),self.assertRaises(ValueError): server.hub_config(p) + self.assertEqual(self.fleet.snapshot()['hub']['name'],'Escape Gallery') diff --git a/tests/room_network/test_objectives.py b/tests/room_network/test_objectives.py new file mode 100644 index 0000000..5a3f743 --- /dev/null +++ b/tests/room_network/test_objectives.py @@ -0,0 +1,78 @@ +import unittest +import test_fleet + +class ObjectiveTests(unittest.TestCase): + setUp = test_fleet.FleetTests.setUp + tearDown = test_fleet.FleetTests.tearDown + def poll(self, cabinet, reports=None, features=True): + return self.fleet.poll(dict(cabinet=cabinet,protocol=2,boot=cabinet,reports=reports or [],features=['assignment_config_v1'] if features else [])) + + def test_config_reaches_prepare_and_manual_clears_it(self): + self.fleet.operator('prepare_scenario',{'scenario':'three_cabinet_objective_test'}) + command=self.poll('cabinet_01')['commands'][0] + self.assertEqual(command['game_config']['test_auto_win_seconds'],5) + self.poll('cabinet_01',[dict(kind='ack',id=command['id'],session=command['session'],ok=True)]) + self.fleet.operator('reset',{'cabinet':'cabinet_01'}) + command=self.poll('cabinet_01')['commands'][0] + self.poll('cabinet_01',[dict(kind='ack',id=command['id'],session=command['session'],ok=True)]) + self.fleet.operator('prepare',{'cabinet':'cabinet_01','game':'pacman'}) + self.assertEqual(self.poll('cabinet_01')['commands'][0]['game_config'],{}) + + def test_old_export_rejected_before_dispatch(self): + self.poll('cabinet_03', features=False) + with self.assertRaisesRegex(ValueError,'Update the game export on cabinet_03'): + self.fleet.operator('prepare_scenario',{'scenario':'three_cabinet_objective_test'}) + self.assertFalse(self.poll('cabinet_01')['commands']) + + def test_invalid_objective_rejected(self): + assignment=self.catalog['scenarios']['three_cabinet_objective_test']['assignments'][2] + for config in [{'homes_to_win':6},{'homes_to_win':True},{'homes_to_win':.5},{'unknown':1},{'test_auto_win_seconds':float('nan')}]: + assignment['game_config']=config + with self.assertRaises(ValueError): + self.fleet.operator('prepare_scenario',{'scenario':'three_cabinet_objective_test'}) + self.assertFalse(self.poll('cabinet_01')['commands']) + + def test_never_connected_cabinet_reports_offline_not_old_export(self): + room=self.fleet.rooms['cabinet_03'] + room.last_seen=0 + room.features=[] + with self.assertRaisesRegex(ValueError,'cabinet_03 is offline'): + self.fleet.operator('prepare_scenario',{'scenario':'three_cabinet_objective_test'}) + self.assertFalse(self.poll('cabinet_01')['commands']) + + def test_rfid_credit_validation_and_start_gate(self): + room=self.fleet.rooms['cabinet_01'] + room.features=['assignment_config_v1','rfid_credit_v1'] + room.gpio='real' + self.catalog['cabinets']['cabinet_01']['devices']=[dict(id='reader',kind='rfid')] + item=dict(cabinet='cabinet_01',game='pacman',game_config=dict(rfid_credit_device='reader',rfid_allowed_uids=[])) + self.fleet.validate_assignment(item) + for changes in [dict(rfid_credit_device='missing'),dict(rfid_allowed_uids=['bad']),dict(rfid_allowed_uids=True)]: + invalid=dict(item,game_config=dict(item['game_config'],**changes)) + with self.assertRaises(ValueError): self.fleet.validate_assignment(invalid) + room.features=['assignment_config_v1'] + with self.assertRaisesRegex(ValueError,'2.3'): self.fleet.validate_assignment(item) + room.features.append('rfid_credit_v1') + room.gpio='simulation' + with self.assertRaisesRegex(ValueError,'real GPIO'): self.fleet.validate_assignment(item) + room.state.update(phase='ready',game_config=item['game_config'],commands=[]) + with self.assertRaisesRegex(ValueError,'press Start'): self.fleet.operator('start',dict(cabinet='cabinet_01')) + + def test_shipped_rfid_scenario_prepares_without_outputs(self): + # Exercise the actual exported scenario as well as the protocol fixture. + self.tearDown() + self.catalog=test_fleet.json.loads((test_fleet.ROOT/'tools/room_controller/catalog.json').read_text(encoding='utf-8')) + self.fleet=test_fleet.server.Fleet(':memory:',self.catalog,lambda:self.now) + self.poll('cabinet_01') + room=self.fleet.rooms['cabinet_01'] + room.features=['assignment_config_v1','rfid_credit_v1'] + room.gpio='real' + self.fleet.operator('prepare_scenario', {'scenario':'pacman_rfid_credit_test'}) + command=self.poll('cabinet_01')['commands'][0] + self.assertEqual(command['game_config']['rfid_credit_device'],'cab1_rfid1') + self.assertEqual(command['type'],'prepare') + self.poll('cabinet_01',[dict(kind='ack',id=command['id'],session=command['session'],ok=True)]) + self.assertEqual(room.state['phase'],'ready') + self.assertFalse(any(c['type'] in ('output','reward','start') for c in room.state['commands'])) + self.poll('cabinet_01',[dict(kind='event',id='physical-start',session=command['session'],event='started')]) + self.assertEqual(room.state['phase'],'playing') diff --git a/tests/room_network/test_remote_start.py b/tests/room_network/test_remote_start.py new file mode 100644 index 0000000..660549c --- /dev/null +++ b/tests/room_network/test_remote_start.py @@ -0,0 +1,138 @@ +import importlib.util +import json +import os +from pathlib import Path +import subprocess +import tempfile +import io +from contextlib import redirect_stdout +import unittest +from unittest.mock import patch +from test_room import ROOT + + +def module(name): + spec=importlib.util.spec_from_file_location(name,ROOT/'tools/launch'/f'{name}.py') + loaded=importlib.util.module_from_spec(spec) + spec.loader.exec_module(loaded) + return loaded + +remote=module('remote_cabinets') +service=module('pi_service') + + +class RemoteTests(unittest.TestCase): + def config(self,folder,items,enabled=True): + path=Path(folder)/'launcher.ini' + path.write_text('[remote]\nenabled = '+str(enabled)+'\ncabinets = '+json.dumps(items)) + return path + + def test_opt_in_and_cabinet_mode_do_not_start_remotes(self): + self.assertEqual(remote.load_targets('missing.ini','cabinet'),[]) + with tempfile.TemporaryDirectory() as folder: + path=self.config(folder,[],False) + self.assertEqual(remote.load_targets(path,'controller'),[]) + + def test_safe_idempotent_command(self): + with tempfile.TemporaryDirectory() as folder,patch.object(remote.shutil,'which',return_value='ssh'): + path=self.config(folder,[{'id':'cabinet_01','host':'10.0.0.12','user':'arcade'}]) + target=remote.load_targets(path,'controller')[0] + self.assertEqual(target['command'][-5:],['arcade@10.0.0.12','systemctl','--user','start','arcade-cabinet.service']) + self.assertIn('StrictHostKeyChecking=yes',target['command']) + self.assertIn('BatchMode=yes',target['command']) + self.assertNotIn('restart',target['command']) + + def test_invalid_hosts_and_duplicates_rejected(self): + with tempfile.TemporaryDirectory() as folder,patch.object(remote.shutil,'which',return_value='ssh'): + for host in ['-oProxyCommand=bad','host;reboot','user@host','host name']: + with self.assertRaises(ValueError): + remote.load_targets(self.config(folder,[{'id':'cabinet_01','host':host,'user':'arcade'}]),'controller') + item={'id':'cabinet_01','host':'pi-one','user':'arcade'} + with self.assertRaisesRegex(ValueError,'Duplicate'): + remote.load_targets(self.config(folder,[item,item]),'controller') + + def test_ssh_failure_is_reported_not_retried(self): + target={'id':'cabinet_01','command':['ssh','example']} + with patch.object(remote.subprocess,'run',return_value=subprocess.CompletedProcess([],255,'','Permission denied')) as run: + cabinet,ok,message=remote.start_one(target) + self.assertFalse(ok) + self.assertIn('Permission denied',message) + run.assert_called_once() + with patch.object(remote.subprocess,'run',side_effect=subprocess.TimeoutExpired('ssh',15)): + self.assertFalse(remote.start_one(target)[1]) + + def test_service_has_desktop_environment_and_no_restart_loop(self): + unit=service.render_unit('/usr/bin/python3','/home/arcade/room/tools/launch/launch.py','/home/arcade/room/tools/launch/pi.ini') + self.assertIn('EnvironmentFile=%t/arcade-cabinet-display.env',unit) + self.assertIn('Restart=no',unit) + self.assertIn('KillMode=mixed',unit) + self.assertIn('UnsetEnvironment=ARCADE_ROOM_URL',unit) + self.assertNotIn('[Install]',unit) + self.assertIn('%%',service.quoted('/folder/100%/file')) + self.assertIn('refresh-session',service.render_desktop('/usr/bin/python3','/home/arcade/pi_service.py')) + + def test_working_directory_is_not_shell_quoted(self): + launcher = Path('/home/arcade/room with spaces/100%/tools/launch/launch.py') + unit = service.render_unit('/usr/bin/python3', launcher, '/tmp/pi.ini') + line = next(line for line in unit.splitlines() if line.startswith('WorkingDirectory=')) + expected = str(launcher.resolve().parents[2]).replace('%', '%%') + self.assertEqual(line, 'WorkingDirectory=' + expected) + self.assertEqual(service.working_directory('/home/arcade/room with spaces'), + '/home/arcade/room with spaces') + with self.assertRaises(ValueError): + service.working_directory('/home/arcade/room\nRestart=always') + + def test_installer_checks_systemd_load_state(self): + for state in ('bad-setting', 'error', 'not-found'): + with self.subTest(state=state), patch.object(service.subprocess, 'run', + return_value=subprocess.CompletedProcess([], 0, state + '\n', '')): + with self.assertRaisesRegex(ValueError, 'systemd rejected'): + service.reload_service() + with patch.object(service.subprocess, 'run', + return_value=subprocess.CompletedProcess([], 0, 'loaded\n', '')) as run: + service.reload_service() + self.assertEqual(run.call_count, 2) + self.assertIn('--property=LoadState', run.call_args.args[0]) + + def test_remote_install_waits_for_desktop_without_guessing_display(self): + import sys + with tempfile.TemporaryDirectory() as folder: + from types import SimpleNamespace + with patch.object(service.sys, 'argv', ['pi_service.py', 'install', '--remote']), \ + patch.object(service.sys, 'platform', 'linux'), \ + patch.object(service.os, 'getuid', return_value=1000, create=True), \ + patch.dict(service.os.environ, {}, clear=True), \ + patch.dict(sys.modules, {'launch': SimpleNamespace(plan=lambda *args: None)}), \ + patch.object(service.Path, 'home', return_value=Path(folder)), \ + patch.object(service, 'reload_service'), patch.object(service, 'refresh_session') as refresh: + self.assertEqual(service.main(), 0) + refresh.assert_not_called() + self.assertTrue((Path(folder) / '.config/systemd/user/arcade-cabinet.service').exists()) + self.assertTrue((Path(folder) / '.config/autostart/arcade-cabinet-display.desktop').exists()) + + def test_installer_refuses_unrelated_files(self): + with tempfile.TemporaryDirectory() as folder: + target=Path(folder)/'service' + target.write_text('User-owned settings') + with self.assertRaisesRegex(ValueError,'unrelated'): + service.write_managed(target,'replacement') + self.assertEqual(target.read_text(),'User-owned settings') + + def test_online_confirmation_is_separate_from_ssh_success(self): + target={'id':'cabinet_01','host':'pi-one','command':['ssh']} + controller={'ready':'http://127.0.0.1:8766/','env':{'ARCADE_OPERATOR_TOKEN':'test-token'}} + response=io.BytesIO(json.dumps({'cabinets':{'cabinet_01':{'online':True}}}).encode()) + output=io.StringIO() + with patch.object(remote,'start_one',return_value=('cabinet_01',True,'accepted')),patch.object(remote.urllib.request,'urlopen',return_value=response),redirect_stdout(output): + remote.start_and_check([target],controller) + self.assertIn('SSH OK',output.getvalue()) + self.assertIn('ONLINE in room controller',output.getvalue()) + self.assertNotIn('test-token',output.getvalue()) + + def test_offline_target_does_not_raise_or_stop_controller(self): + target={'id':'cabinet_01','host':'pi-one','command':['ssh']} + controller={'ready':'http://127.0.0.1:8766/','env':{'ARCADE_OPERATOR_TOKEN':'test-token'}} + output=io.StringIO() + with patch.object(remote,'start_one',return_value=('cabinet_01',False,'unreachable')),patch.object(remote.time,'monotonic',side_effect=[0,16]),redirect_stdout(output): + remote.start_and_check([target],controller) + self.assertIn('Controller remains running',output.getvalue()) diff --git a/tools/deploy/.gitignore b/tools/deploy/.gitignore new file mode 100644 index 0000000..d84309b --- /dev/null +++ b/tools/deploy/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +site/ +*.tar.gz diff --git a/tools/deploy/README.md b/tools/deploy/README.md new file mode 100644 index 0000000..c6cdf64 --- /dev/null +++ b/tools/deploy/README.md @@ -0,0 +1,134 @@ +# Master Pi deployment: first box test + +This first implementation provides a **terminal setup wizard**, portable master software, versioned cabinet uploads, configuration generation and SSH service controls. The normal browser operator remains the game-master interface. There is not yet a browser provisioning wizard or automatic network reconfiguration. + +It does not connect to any Pi until you explicitly run an enrollment/deployment command. It does not pulse GPIO, trigger a puzzle, reset controller mappings, or copy the development site's credentials. Use one spare cabinet first. + +## 1. Prepare clean deployment packages on the development PC + +From the project root (replace the release label for each build): + +```powershell +py -3 tools/deploy/deploy.py master-package --output D:/ArcadeMaster-test1 +py -3 tools/deploy/deploy.py bundle --export setup/pi --output D:/arcade-test1.tar.gz --release test1 +``` + +The output directory and bundle must not already exist. The master package is an explicit allowlist of Python/scripts, operator assets and catalog. The cabinet bundle contains the current ARM64 executable/PCK plus scripts and a checksum manifest. No live INIs, tokens, SQLite files, SSH keys or Odin files are copied. Package an export built from the same game/cabinet catalog: the packager validates ARM64/PCK headers, but cannot prove a PCK was exported from the matching source revision. + +Copy `ArcadeMaster-test1` and `arcade-test1.tar.gz` to the master Pi, for example under its desktop user's home. No Godot editor is needed on the master. + +## 2. Network for the box + +First confirm the physical arrangement: master Ethernet to the box's private switch, cabinet Ethernet to that switch, and master Wi-Fi (or a second Ethernet adapter) to the venue LAN. Do not also connect the private switch to the venue router. Cabinet Wi-Fi must not bypass the private network. + +A plain switch does not supply DHCP. For the initial box test, manually configure **unique static wired addresses** through each Pi's desktop network settings. Choose a subnet that does not overlap the venue network; an example is master `192.168.77.1/24`, cabinet 1 `.11/24`, cabinet 2 `.12/24`, cabinet 3 `.13/24`. Leave the private connection's gateway and DNS blank, and keep the master's default Internet route on its venue connection. Record the real addresses in setup below. A later installer can provide private DHCP reservations. + +The deployment tool does not install a firewall or enforce network isolation. The master must not bridge the interfaces or enable IP forwarding/connection sharing. For an enforced deployment, apply a firewall policy that drops forwarding in both IPv4 and IPv6, permits cabinet controller traffic to the master on TCP 8766, and permits venue browsers to the operator port. This needs the actual interface/subnet information; do not blindly replace an existing firewall. Staff on an isolated guest Wi-Fi may not be allowed to reach the master. + +You may test the software on the existing LAN first; that is **not** a closed cabinet network. The browser connects to `http://MASTER_VENUE_IP:8766`; cabinets use `http://MASTER_PRIVATE_IP:8766`. Game-master commands travel through the master. No direct staff-browser-to-cabinet route is required. + +## 3. Pi prerequisites + +Use Raspberry Pi OS **64-bit with a desktop** on the cabinets. Enable SSH and desktop autologin for the same ordinary user who runs the game. The master needs Python 3 and the OpenSSH client; it can be headless after initial setup. + +On the master, if the commands are missing: + +```sh +sudo apt install python3 openssh-client +``` + +On cabinets using real GPIO, install dependencies before disconnecting Internet access: + +```sh +sudo apt install python3-gpiozero python3-lgpio +``` + +The cabinet user needs GPIO device permission (normally the Raspberry Pi OS `gpio` group). The first test may use GPIO disabled. Do not guess MOSFET/NFC pins: enable only the BCM outputs already defined and wired for that cabinet. NFC input integration is separate from deployment. + +## 4. Register the site on the master + +From the copied master folder: + +```sh +python3 tools/deploy/deploy.py setup +``` + +The wizard asks for the private controller URL, each cabinet's existing catalog ID, its address and SSH user, and GPIO settings. It generates distinct site credentials and configurations in `~/.local/share/arcade-master/`, with private permissions. It never asks for or stores the Pi login password. Re-running setup edits entries and preserves tokens. Press Enter at the cabinet-ID prompt when finished. + +For this release choose `cabinet_01`, `cabinet_02`, or `cabinet_03`, matching the existing exported hardware profiles. Arbitrary new IDs require a matching export/catalog; dynamic runtime profiles are not implemented yet. The GPIO controller ID must match the project's hardware action definitions, not merely a newly invented name. + +To inspect/edit the inventory later, use `inventory.json` in that site directory and run `python3 tools/deploy/deploy.py generate`. Never rotate or hand out another cabinet's token accidentally. Changing an active site's master credentials/config requires a controller restart and matching cabinet reactivation. Do maintenance between sessions. + +## 5. Enroll, check and upload ONE cabinet + +```sh +python3 tools/deploy/deploy.py enroll cabinet_01 +python3 tools/deploy/deploy.py preflight cabinet_01 +python3 tools/deploy/deploy.py stage cabinet_01 --bundle ~/arcade-test1.tar.gz +``` + +Enrollment creates a dedicated master SSH key if none exists, asks you to verify the target host fingerprint, and uses `ssh-copy-id` with the cabinet's existing login. It preserves other authorized keys. The key has no passphrase so startup can be unattended; protect the master account and its site backup. Subsequent commands require the verified host key and key-based authentication without prompts. + +Preflight reports architecture, Python version, GPIO-package availability, free space and whether the display helper has captured a desktop session. Uploads use SSH and verify an exact file allowlist and every SHA-256 checksum. Staging does not stop or start a game. An interrupted upload leaves the previous release untouched; rerun stage. Reusing a release ID with different contents is rejected. + +## 6. Activate and initialize the desktop helper + +Stop any manually launched game/bridge first. If the cabinet service is running, explicitly stop it between tests: + +```sh +python3 tools/deploy/deploy.py stop cabinet_01 +python3 tools/deploy/deploy.py activate cabinet_01 --release test1 +``` + +Skip the stop command on a fresh cabinet with no service. Activation refuses to replace a running service and refuses to overwrite unrelated service files. It writes that cabinet's INI and installs the normal user service through SSH. The master/operator token is never sent to the cabinet. If registration fails, prior service files/config are restored. + +On a **fresh cabinet**, log out and back into its desktop once after activation, or run this on its local desktop terminal: + +```sh +python3 ~/.local/share/arcade-deploy/releases/test1/tools/launch/pi_service.py refresh-session +``` + +The desktop autostart helper refreshes this display environment at future logins. SSH cannot safely guess the active display. The first remote install therefore reports waiting for desktop readiness instead of pretending the game has started. No root process renders the game. + +## 7. Start the master and verify + +On the master, from its software folder: + +```sh +python3 tools/deploy/deploy.py install-master +sudo loginctl enable-linger "$USER" +systemctl --user start arcade-master.service +python3 tools/deploy/deploy.py health +``` + +The installer enables the master user service, but does not start it until the start command. Lingering starts the master service at boot even without a desktop login. The master launcher tries to start the registered cabinet services and reports authenticated ONLINE status separately from SSH success. A cabinet must already have its display environment ready. If it logs in later, issue `start` below; startup has bounded waits, not an endless retry loop. + +Open `http://MASTER_VENUE_IP:8766` in a staff browser. Read the operator token from the master site's `controller.ini`; it is separate from SSH credentials and cabinet tokens. Existing scenario controls, overrides and hardware actions remain available in the operator page. Deployment does not automatically prepare a scenario or test a hardware output. + +```sh +python3 tools/deploy/deploy.py start cabinet_01 +python3 tools/deploy/deploy.py status cabinet_01 +python3 tools/deploy/deploy.py logs cabinet_01 +python3 tools/deploy/deploy.py health +``` + +To diagnose the master: `journalctl --user -u arcade-master.service -n 60 --no-pager`. + +## Updates, rollback and state + +Use a new release label and stage it while the old game is running. Between sessions, stop the cabinet, activate the new release and start it. To roll back, stop it and activate the previous release label again. Previous releases are retained; no automatic cleanup deletes them. Reusing the same release for another cabinet identity on the same device overwrites only that release's generated INI after the service is stopped. + +Game user data/control mappings stay in Godot's existing user-data directory. GPIO deduplication stays at `~/.local/state/arcade_gpio/requests.sqlite3`. Cabinet logs are under `~/.local/state/arcade_cabinet/logs`. The new master site has its own controller database under the site's `state/`; it does not silently import or overwrite the development controller database. + +Back up the master site folder (inventory, tokens, SSH key and controller database) securely. After moving to a non-overlapping venue network, retain the private addresses and update the master venue connection/operator bookmark. This version does not reconfigure network interfaces automatically. + +## Box acceptance checklist + +- One cabinet enrolls, stages and activates; a failed upload does not stop the existing game. +- A fresh desktop login produces display readiness and the game appears on the cabinet monitor. +- Operator sees the expected cabinet ONLINE and can prepare/start/reset the existing scenario. +- Existing manual progression/hardware controls still work through the master; only test a known wired output deliberately. +- Restart the master; it comes back without a login and cabinets reconnect without replaying a reward. +- Restart a cabinet; after desktop login, explicitly start it if the master's initial startup attempt has already passed. +- Verify staff can reach the operator and, once isolation is configured, cannot directly reach cabinet addresses. +- Repeat with remaining cabinets, then run the three-cabinet test. diff --git a/tools/deploy/deploy.py b/tools/deploy/deploy.py new file mode 100644 index 0000000..062dc45 --- /dev/null +++ b/tools/deploy/deploy.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python3 +"""First-run master setup and explicit SSH deployment for Arcade Escape.""" +import argparse +import configparser +import hashlib +import io +import ipaddress +import json +import os +from pathlib import Path +import re +import secrets +import shlex +import shutil +import subprocess +import sys +import tarfile +import time +import urllib.request +from receiver import FILES, private_write, release_id, render_config + +ROOT = Path(__file__).resolve().parents[2] +DEFAULT_SITE = Path.home() / '.local/share/arcade-master' + + +def catalog(): + return json.loads((ROOT / 'tools/room_controller/catalog.json').read_text(encoding='utf-8')) + + +def validate(site): + if site.get('schema') != 1 or not site.get('cabinets'): + raise ValueError('Expected schema 1 and at least one cabinet') + tokens = [site['operator_token']] + seen, targets = set(), set() + for item in site['cabinets']: + for key, pattern in [('id', r'[A-Za-z0-9_]+'), ('host', r'[A-Za-z0-9][A-Za-z0-9.-]*'), + ('user', r'[A-Za-z_][A-Za-z0-9_-]*')]: + if not isinstance(item.get(key), str) or not re.fullmatch(pattern, item[key]): + raise ValueError('Invalid cabinet ' + key) + if item['id'] not in catalog()['cabinets']: + raise ValueError('Cabinet ID is not in the exported catalog: ' + item['id']) + target = (item['host'].lower(), item.get('port', 22)) + if item['id'] in seen or target in targets: + raise ValueError('Duplicate cabinet ID or SSH target') + seen.add(item['id']) + targets.add(target) + if type(target[1]) is not int or not 1 <= target[1] <= 65535: + raise ValueError('Invalid SSH port') + tokens.append(item['token']) + render_config(dict(item, url=site['controller_url'])) + if any(not isinstance(t, str) or not re.fullmatch(r'[A-Za-z0-9_-]{16,128}', t) for t in tokens) or len(set(tokens)) != len(tokens): + raise ValueError('Operator and cabinet tokens must be distinct, 16+ character URL-safe strings') + from urllib.parse import urlsplit + parsed = urlsplit(site['controller_url']) + # This launcher hosts HTTP locally; a TLS proxy would require additional configuration. + if parsed.scheme != 'http' or parsed.port != 8766: + raise ValueError('Use http://MASTER_PRIVATE_IP:8766 for the cabinet controller URL') + address = ipaddress.ip_address(parsed.hostname) + if address.version != 4 or address.is_loopback or address.is_unspecified or address.is_multicast: + raise ValueError('Use the master private-interface IPv4 address, not localhost') + if any(item['host'] == str(address) for item in site['cabinets']): + raise ValueError('A cabinet target cannot be the master controller IP') + + +def load(folder): + site = json.loads((folder / 'inventory.json').read_text(encoding='utf-8')) + validate(site) + return site + + +def generate(folder, site): + validate(site) + key = folder / 'ssh/arcade_cabinets' + ini = configparser.ConfigParser(interpolation=None) + ini['controller'] = dict(bind='0.0.0.0', port='8766', operator_token=site['operator_token'], + cabinet_tokens=json.dumps({c['id']: c['token'] for c in site['cabinets']}), + catalog=str(ROOT / 'tools/room_controller/catalog.json'), database=str(folder / 'state/arcade_room.sqlite3')) + ini['launcher'] = dict(logs=str(folder / 'logs')) + ini['remote'] = dict(enabled='true', cabinets=json.dumps([ + {k: c[k] for k in ('id', 'host', 'user', 'port')} | {'identity_file': str(key)} + for c in site['cabinets']])) + output = io.StringIO() + ini.write(output) + private_write(folder / 'controller.ini', output.getvalue()) + for c in site['cabinets']: + private_write(folder / 'cabinets' / (c['id'] + '.ini'), render_config(dict(c, url=site['controller_url']))) + + +def prompt(label, default=''): + value = input(label + (f' [{default}]' if default else '') + ': ').strip() + return value or default + + +def setup(folder): + path = folder / 'inventory.json' + if path.exists(): + site = load(folder) + print('Existing site loaded; credentials will be preserved.') + else: + site = dict(schema=1, operator_token=secrets.token_urlsafe(24), controller_url='', cabinets=[]) + site['controller_url'] = prompt('Master PRIVATE URL', site['controller_url'] or 'http://192.168.77.1:8766') + print('Available cabinet profiles: ' + ', '.join(catalog()['cabinets'])) + existing = {c['id']: c for c in site['cabinets']} + while True: + cabinet = prompt('Cabinet ID to add/edit (Enter when done)') + if not cabinet: + break + if cabinet not in catalog()['cabinets']: + print('Choose an ID in the list. New IDs require an updated game export and catalog.') + continue + c = dict(existing.get(cabinet, dict(id=cabinet, token=secrets.token_urlsafe(24), port=22))) + c['host'] = prompt('Cabinet IP or hostname', c.get('host', '')) + c['user'] = prompt('Pi desktop / SSH username', c.get('user', '')) + c['port'] = int(prompt('SSH port', str(c.get('port', 22)))) + c['gpio'] = prompt('Enable real GPIO? yes/no', 'yes' if c.get('gpio') else 'no').lower() == 'yes' + pins = prompt('Allowed BCM pins, comma-separated', ','.join(map(str, c.get('pins', [])))) + c['pins'] = [int(p.strip()) for p in pins.split(',') if p.strip()] + c['gpio_controller'] = prompt('GPIO controller ID from cabinet hardware profile', c.get('gpio_controller', cabinet + '_gpio')) + existing[cabinet] = c + site['cabinets'] = list(existing.values()) + validate(site) + private_write(path, json.dumps(site, indent=2)) + generate(folder, site) + print('Saved site inventory and generated configs at ' + str(folder)) + print('Operator credential is in controller.ini. No cabinet contacted; no network settings changed.') + + +def ssh_args(folder, c, batch=True): + key = folder / 'ssh/arcade_cabinets' + if not key.is_file(): + raise ValueError('Run enroll first to generate and authorize the master key') + return ['ssh', '-T', '-p', str(c.get('port', 22)), '-i', str(key), + '-o', 'IdentitiesOnly=yes', '-o', 'StrictHostKeyChecking=yes', + '-o', 'BatchMode=' + ('yes' if batch else 'no'), + '-o', 'ConnectTimeout=10', '-o', 'ServerAliveInterval=10', + '-o', 'ServerAliveCountMax=3', c['user'] + '@' + c['host']] + + +def enroll(folder, c): + if not sys.platform.startswith('linux'): + raise ValueError('Run enrollment on the master Pi (requires ssh-copy-id)') + if not shutil.which('ssh-copy-id'): + raise ValueError('Install openssh-client on the master') + key = folder / 'ssh/arcade_cabinets' + key.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + if not key.exists(): + subprocess.run(['ssh-keygen', '-t', 'ed25519', '-N', '', '-f', str(key), '-C', 'arcade-master'], check=True) + if not key.with_suffix('.pub').is_file(): + raise ValueError('Private key exists but .pub is missing; refusing to replace the key') + print('Verify the Pi host fingerprint when prompted. Enter its existing SSH password if requested.') + subprocess.run(['ssh-copy-id', '-i', str(key) + '.pub', '-p', str(c.get('port', 22)), + '-o', 'StrictHostKeyChecking=ask', c['user'] + '@' + c['host']], check=True) + subprocess.run(ssh_args(folder, c) + ['true'], check=True, timeout=30) + print(c['id'] + ': key authentication verified') + + +def remote(folder, c, action, source=None, release=None): + code = Path(__file__).with_name('receiver.py').read_text(encoding='utf-8') + command = 'python3 -c ' + shlex.quote(code) + ' ' + shlex.quote(action) + if release is not None: + command += ' --release ' + shlex.quote(release_id(release)) + subprocess.run(ssh_args(folder, c) + [command], stdin=source or subprocess.DEVNULL, check=True) + + +def bundle(export, output, release): + release_id(release) + paths = {name: (export / name if name.startswith('ArcadeEscape.') else ROOT / name) for name in FILES} + with paths['ArcadeEscape.arm64'].open('rb') as binary: + header = binary.read(20) + if header[:6] != b'\x7fELF\x02\x01' or header[18:20] != b'\xb7\x00': + raise ValueError('Expected a little-endian ARM64 Linux game export') + with paths['ArcadeEscape.pck'].open('rb') as pack: + if pack.read(4) != b'GDPC': + raise ValueError('Invalid Godot PCK') + manifest = dict(release=release, files={name: hashlib.sha256(path.read_bytes()).hexdigest() for name, path in paths.items()}) + encoded = json.dumps(manifest).encode() + # Exclusive creation prevents silently replacing a released bundle. + with output.open('xb') as raw, tarfile.open(fileobj=raw, mode='w:gz') as archive: + info = tarfile.TarInfo('manifest.json') + info.size = len(encoded) + info.mode = 0o600 + archive.addfile(info, io.BytesIO(encoded)) + for name, path in paths.items(): + info = archive.gettarinfo(str(path), arcname=name) + info.uid = info.gid = 0 + info.uname = info.gname = '' + info.mode = 0o700 if name.endswith('.arm64') else 0o600 + with path.open('rb') as content: + archive.addfile(info, content) + print('Created release bundle: ' + str(output)) + + +def master_package(output): + # Explicit allowlist: never ship a developer's INI, credentials, database or keys. + output.mkdir(parents=True, exist_ok=False) + names = [name for name in FILES if not name.startswith('ArcadeEscape.')] + names += ['tools/deploy/deploy.py', 'tools/deploy/receiver.py', 'tools/deploy/README.md'] + for name in names: + target = output / name + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(ROOT / name, target) + print('Portable master software: ' + str(output)) + + +def install_master(folder): + if not sys.platform.startswith('linux') or os.getuid() == 0: + raise ValueError('Install the master service as the normal master Pi user, without sudo') + sys.path.insert(0, str(ROOT / 'tools/launch')) + from pi_service import quoted, working_directory + path = Path.home() / '.config/systemd/user/arcade-master.service' + marker = '# Managed by Arcade Escape deploy.py' + if path.exists() and not path.read_text(encoding='utf-8').startswith(marker): + raise ValueError('Refusing to overwrite an unrelated master service') + unit = f'''{marker} +[Unit] +Description=Arcade room master controller +[Service] +Type=exec +WorkingDirectory={working_directory(ROOT)} +ExecStart=:{quoted(sys.executable)} {quoted(ROOT / 'tools/launch/launch.py')} controller --config {quoted(folder / 'controller.ini')} +UnsetEnvironment=ARCADE_ROOM_URL ARCADE_CABINET_TOKEN ARCADE_OPERATOR_TOKEN ARCADE_CABINET_TOKENS +Restart=on-failure +RestartSec=10 +KillMode=mixed +TimeoutStopSec=15 +[Install] +WantedBy=default.target +''' + private_write(path, unit) + subprocess.run(['systemctl', '--user', 'daemon-reload'], check=True) + result = subprocess.check_output(['systemctl', '--user', 'show', 'arcade-master.service', '--property=LoadState', '--value'], text=True) + if result.strip() != 'loaded': + raise ValueError('systemd rejected the master unit; inspect systemctl --user status arcade-master.service') + subprocess.run(['systemctl', '--user', 'enable', 'arcade-master.service'], check=True) + print('Master service installed/enabled, not started. For startup before login run:') + print('sudo loginctl enable-linger ' + shlex.quote(os.environ.get('USER', 'YOUR_MASTER_USERNAME'))) + print('Then: systemctl --user start arcade-master.service') + + +def health(site): + request = urllib.request.Request('http://127.0.0.1:8766/operator/state', data=b'{}', + headers={'Authorization': 'Bearer ' + site['operator_token']}) + with urllib.request.urlopen(request, timeout=5) as response: + state = json.load(response) + for c in site['cabinets']: + result = state.get('cabinets', {}).get(c['id'], {}) + print(c['id'] + ': ' + ('ONLINE' if result.get('online') else 'OFFLINE')) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--site', type=Path, default=DEFAULT_SITE) + sub = parser.add_subparsers(dest='action', required=True) + sub.add_parser('setup', help='Interactive inventory/configuration wizard; no remote changes') + sub.add_parser('generate', help='Validate edited inventory and regenerate configs') + sub.add_parser('install-master') + sub.add_parser('health') + master = sub.add_parser('master-package') + master.add_argument('--output', type=Path, required=True) + pack = sub.add_parser('bundle') + pack.add_argument('--export', type=Path, required=True) + pack.add_argument('--output', type=Path, required=True) + pack.add_argument('--release', required=True) + for action in ('enroll', 'preflight', 'stage', 'activate', 'start', 'stop', 'status', 'logs'): + command = sub.add_parser(action) + command.add_argument('cabinet', help='One registered cabinet ID; no implicit fleet-wide mutations') + if action == 'stage': command.add_argument('--bundle', type=Path, required=True) + if action == 'activate': command.add_argument('--release', required=True) + args = parser.parse_args() + folder = args.site.expanduser().resolve() + try: + if args.action == 'master-package': + master_package(args.output.resolve()) + return 0 + if args.action == 'bundle': + bundle(args.export.resolve(), args.output.resolve(), args.release) + return 0 + if args.action == 'setup': + setup(folder) + return 0 + site = load(folder) + if args.action == 'generate': generate(folder, site) + elif args.action == 'install-master': install_master(folder) + elif args.action == 'health': health(site) + else: + c = next((c for c in site['cabinets'] if c['id'] == args.cabinet), None) + if c is None: raise ValueError('Cabinet not registered in this site') + if args.action == 'enroll': enroll(folder, c) + elif args.action == 'preflight': remote(folder, c, 'preflight') + elif args.action == 'stage': + with args.bundle.open('rb') as source: remote(folder, c, 'stage', source) + elif args.action == 'activate': + import tempfile + with tempfile.TemporaryFile() as source: + source.write(json.dumps(dict(c, url=site['controller_url'])).encode()) + source.seek(0) + remote(folder, c, 'activate', source, args.release) + else: + command = ['journalctl', '--user', '-u', 'arcade-cabinet.service', '-n', '60', '--no-pager'] if args.action == 'logs' else ['systemctl', '--user', args.action, 'arcade-cabinet.service', '--no-pager'] + subprocess.run(ssh_args(folder, c) + command, check=True, timeout=45) + return 0 + except (OSError, ValueError, KeyError, TypeError, subprocess.SubprocessError) as exc: + # CalledProcessError's command can contain configuration input only on stdin, + # but suppress the huge receiver source from diagnostics as well. + if isinstance(exc, subprocess.CalledProcessError): + print('Command failed (exit ' + str(exc.returncode) + '); see diagnostics above.', file=sys.stderr) + else: + print('Setup failed: ' + str(exc), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/deploy/receiver.py b/tools/deploy/receiver.py new file mode 100644 index 0000000..e00de70 --- /dev/null +++ b/tools/deploy/receiver.py @@ -0,0 +1,213 @@ +"""Uploaded over authenticated SSH; no daemon and no root required.""" +import argparse +import configparser +import hashlib +import importlib.util +import io +import json +import os +from pathlib import Path +import platform +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile + +FILES = ( + 'ArcadeEscape.arm64', 'ArcadeEscape.pck', + 'tools/launch/launch.py', 'tools/launch/pi_service.py', + 'tools/launch/remote_cabinets.py', 'tools/pi_gpio/pi_gpio_bridge.py', + 'tools/room_controller/server.py', 'tools/room_controller/operator.html', + 'tools/room_controller/catalog.json', + 'tools/pi_gpio/device_feedback.py', + 'tools/pi_gpio/rfid_helper/Cargo.toml', 'tools/pi_gpio/rfid_helper/src/main.rs', +) +MAX_BYTES = 1024 * 1024 * 1024 +SERVICE = 'arcade-cabinet.service' +MARKER = '# Managed by Arcade Escape pi_service.py' + + +def release_id(value): + if not isinstance(value, str) or not re.fullmatch(r'[A-Za-z0-9][A-Za-z0-9._-]{0,63}', value): + raise ValueError('Invalid release ID') + return value + + +def private_write(path, text): + path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) + fd = os.open(str(path), os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) + with os.fdopen(fd, 'w', encoding='utf-8', newline='\n') as output: + output.write(text) + path.chmod(0o600) + + +def unpack(stream, destination): + """Read only the exact regular files named by our bounded manifest.""" + with tarfile.open(fileobj=stream, mode='r|gz') as archive: + first = next(iter(archive), None) + if first is None or first.name != 'manifest.json' or not first.isfile() or first.size > 65536: + raise ValueError('Release must begin with a small regular manifest.json') + manifest = json.load(archive.extractfile(first)) + release_id(manifest['release']) + expected = manifest['files'] + if set(expected) != set(FILES): + raise ValueError('Release contains an unexpected file set') + if any(not isinstance(v, str) or not re.fullmatch('[0-9a-f]{64}', v) for v in expected.values()): + raise ValueError('Invalid release checksums') + seen, total = set(), 0 + # TarFile iteration begins at its cached first member; skip the manifest once. + for member in archive: + if member is first: + continue + if member.name not in expected or member.name in seen or not member.isfile(): + raise ValueError('Unexpected, duplicate, or nonregular archive entry') + total += member.size + if total > MAX_BYTES: + raise ValueError('Release exceeds size limit') + target = destination / member.name + target.parent.mkdir(parents=True, exist_ok=True) + digest = hashlib.sha256() + with archive.extractfile(member) as source, target.open('xb') as output: + while chunk := source.read(1024 * 1024): + digest.update(chunk) + output.write(chunk) + if digest.hexdigest() != expected[member.name]: + raise ValueError('Checksum mismatch: ' + member.name) + target.chmod(0o700 if member.name == 'ArcadeEscape.arm64' else 0o600) + seen.add(member.name) + if seen != set(expected): + raise ValueError('Incomplete release') + private_write(destination / 'manifest.json', json.dumps(manifest, indent=2)) + return manifest + + +def verify_release(folder): + manifest = json.loads((folder / 'manifest.json').read_text(encoding='utf-8')) + if set(manifest['files']) != set(FILES): + raise ValueError('Unexpected release contents') + for name, expected in manifest['files'].items(): + target = folder / name + if target.is_symlink() or hashlib.sha256(target.read_bytes()).hexdigest() != expected: + raise ValueError('Release verification failed: ' + name) + return manifest + + +def preflight(): + runtime = Path(os.environ.get('XDG_RUNTIME_DIR', '/run/user/' + str(os.getuid()))) + return dict(architecture=platform.machine(), python=list(sys.version_info[:3]), + gpio_packages=all(importlib.util.find_spec(m) is not None for m in ('gpiozero', 'lgpio')), + display_ready=(runtime / 'arcade-cabinet-display.env').is_file(), + free_bytes=shutil.disk_usage(Path.home()).free) + + +def render_config(settings): + cabinet, token, url = (settings[k] for k in ('id', 'token', 'url')) + if not re.fullmatch(r'[A-Za-z0-9_]+', cabinet): + raise ValueError('Invalid cabinet ID') + if not re.fullmatch(r'[A-Za-z0-9_-]{16,128}', token): + raise ValueError('Invalid cabinet token') + from urllib.parse import urlsplit + parsed = urlsplit(url) + if parsed.scheme not in ('http', 'https') or not parsed.hostname or parsed.username or parsed.password or parsed.path not in ('', '/') or parsed.query or parsed.fragment or any(c.isspace() for c in url): + raise ValueError('Invalid controller URL') + if type(settings.get('gpio')) is not bool: + raise ValueError('GPIO must be true or false') + pins = settings.get('pins', []) + if not isinstance(pins, list) or any(type(p) is not int or not 0 <= p <= 27 for p in pins) or len(set(pins)) != len(pins) or (settings['gpio'] and not pins): + raise ValueError('Invalid BCM pin allowlist') + controller = settings.get('gpio_controller', cabinet + '_gpio') + if not re.fullmatch(r'[A-Za-z0-9_]+', controller): + raise ValueError('Invalid GPIO controller ID') + config = configparser.ConfigParser(interpolation=None) + config['cabinet'] = dict(executable='../../ArcadeEscape.arm64', id=cabinet, + url=url.rstrip('/'), token=token, gpio=str(settings['gpio']).lower()) + config['gpio'] = dict(controller=controller, pins=','.join(map(str, pins)), + ledger='~/.local/state/arcade_gpio/requests.sqlite3') + config['launcher'] = dict(logs='~/.local/state/arcade_cabinet/logs') + output = io.StringIO() + config.write(output) + return output.getvalue() + + +def stage(root, stream): + releases = root / 'releases' + releases.mkdir(parents=True, exist_ok=True, mode=0o700) + with tempfile.TemporaryDirectory(prefix='.incoming-', dir=releases) as temp: + folder = Path(temp) + manifest = unpack(stream, folder) + target = releases / manifest['release'] + if target.exists(): + if verify_release(target) != manifest: + raise ValueError('Release ID already exists with different contents') + print('Release already staged and verified: ' + manifest['release']) + return + folder.rename(target) + print('Staged and verified: ' + manifest['release'] + '; not activated') + + +def activate(root, release, settings): + folder = root / 'releases' / release_id(release) + verify_release(folder) + text = render_config(settings) + catalog = json.loads((folder / 'tools/room_controller/catalog.json').read_text(encoding='utf-8')) + if settings['id'] not in catalog['cabinets']: + raise ValueError('Cabinet ID is not included in this release catalog') + if subprocess.run(['systemctl', '--user', 'is-active', '--quiet', SERVICE]).returncode == 0: + raise ValueError('Cabinet is running. Stop it explicitly before activating a release.') + home = Path.home() / '.config' + managed = [home / 'systemd/user' / SERVICE, home / 'autostart/arcade-cabinet-display.desktop'] + previous = {} + for path in managed: + previous[path] = path.read_bytes() if path.exists() else None + if previous[path] is not None and not previous[path].decode('utf-8').startswith(MARKER): + raise ValueError('Refusing to replace an unrelated service/helper') + config = folder / 'tools/launch/launcher.ini' + old_config = config.read_bytes() if config.exists() else None + private_write(config, text) + try: + subprocess.run([sys.executable, str(folder / 'tools/launch/pi_service.py'), 'install', + '--remote', '--config', str(config)], check=True) + # Informational receipt; service refers directly to the immutable release path. + private_write(root / 'active.json', json.dumps(dict(release=release, cabinet=settings['id']))) + except Exception: + for path, content in previous.items(): + if content is None: + path.unlink(missing_ok=True) + else: + private_write(path, content.decode('utf-8')) + if old_config is None: + config.unlink(missing_ok=True) + else: + private_write(config, old_config.decode('utf-8')) + subprocess.run(['systemctl', '--user', 'daemon-reload'], check=False) + raise + print('Activated ' + release + '. Service is stopped; start after desktop readiness is confirmed.') + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('action', choices=['preflight', 'stage', 'activate']) + parser.add_argument('--release') + args = parser.parse_args() + try: + if not sys.platform.startswith('linux') or os.getuid() == 0: + raise ValueError('Use the normal Pi desktop account, not root') + if platform.machine() not in ('aarch64', 'arm64'): + raise ValueError('A 64-bit ARM Raspberry Pi OS is required') + root = Path.home() / '.local/share/arcade-deploy' + if args.action == 'preflight': + print(json.dumps(preflight())) + elif args.action == 'stage': + stage(root, sys.stdin.buffer) + else: + activate(root, args.release, json.loads(sys.stdin.read(65536))) + return 0 + except (OSError, ValueError, KeyError, TypeError, tarfile.TarError, subprocess.CalledProcessError) as exc: + print('Deployment failed: ' + str(exc), file=sys.stderr) + return 1 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/launch/README.md b/tools/launch/README.md index 7cb0135..7d989b7 100644 --- a/tools/launch/README.md +++ b/tools/launch/README.md @@ -1,5 +1,7 @@ # Start the Arcade stack +To start the Pi applications automatically from the PC controller command, follow the [remote Pi startup guide](../../docs/remote_pi_start.md). + For the assigned internal operator/Cabinet 1–7 tokens and per-machine Godot CFG examples, see the [cabinet setup guide](../../docs/cabinet_setup.md). One launcher starts the needed processes, waits for services to respond, saves logs, and stops its children when you press Ctrl+C. It runs on Windows and Linux/Pi (and supports controller-only use on macOS). diff --git a/tools/launch/controller.example.ini b/tools/launch/controller.example.ini deleted file mode 100644 index 561246d..0000000 --- a/tools/launch/controller.example.ini +++ /dev/null @@ -1,12 +0,0 @@ -[launcher] -logs = logs - -[controller] -bind = 0.0.0.0 -port = 8766 -; Paths are relative to THIS file. Reuse your existing database path when upgrading. -database = state/arcade_room.sqlite3 -catalog = ../room_controller/catalog.json -; Use your existing tokens. Leave blank if supplied via environment variables. -operator_token = -cabinet_tokens = {} diff --git a/tools/launch/launch.py b/tools/launch/launch.py index 14df4e6..e6e635b 100644 --- a/tools/launch/launch.py +++ b/tools/launch/launch.py @@ -70,6 +70,10 @@ def plan(config_path, mode): url_host = '127.0.0.1' if bind == '0.0.0.0' else bind url = f'http://{url_host}:{port}' jobs.append(dict(name='controller', command=[sys.executable, '-u', str(script), '--bind', bind, '--port', str(port), '--database', str(database), '--catalog', str(catalog)], env=server_env, port=(bind, port), ready=url+'/', directory=database.parent)) + if value('controller', 'hub_config'): + hub_path = path('controller', 'hub_config', '') + require_file(hub_path) + jobs[-1]['command'] += ['--hub-config', str(hub_path)] if mode in ('cabinet', 'all'): executable = path('cabinet', 'executable', '../../ArcadeEscape.exe' if os.name == 'nt' else '../../ArcadeEscape.arm64') require_file(executable) @@ -92,13 +96,21 @@ def plan(config_path, mode): if not sys.platform.startswith('linux'): raise ValueError('Real GPIO requires the Linux Pi; set gpio = false on Windows') if importlib.util.find_spec('gpiozero') is None or importlib.util.find_spec('lgpio') is None: raise ValueError('Install Pi dependencies first: sudo apt install python3-gpiozero python3-lgpio') - pins = [int(p.strip()) for p in value('gpio', 'pins', '17').split(',')] - if not pins or len(set(pins)) != len(pins) or any(p < 0 or p > 27 for p in pins): raise ValueError('GPIO pins must be unique BCM numbers 0..27') + pins = [int(p.strip()) for p in value('gpio', 'pins', '17').split(',') if p.strip()] + if len(set(pins)) != len(pins) or any(p < 0 or p > 27 for p in pins): raise ValueError('GPIO pins must be unique BCM numbers 0..27') script = ROOT / 'tools/pi_gpio/pi_gpio_bridge.py' require_file(script) ledger = path('gpio', 'ledger', '~/.local/state/arcade_gpio/requests.sqlite3') bridge_env = dict(env, GPIOZERO_PIN_FACTORY='lgpio') jobs.append(dict(name='gpio', command=[sys.executable, '-u', str(script), '--controller', value('gpio', 'controller', cabinet_id+'_gpio'), '--pins', ','.join(map(str,pins)), '--ledger', str(ledger)], env=bridge_env, port=('127.0.0.1', 8765), ready='http://127.0.0.1:8765/heartbeat', directory=ledger.parent)) + if value('gpio', 'devices'): + devices_path = path('gpio', 'devices', '') + require_file(devices_path) + jobs[-1]['command'] += ['--devices', str(devices_path)] + if value('gpio', 'rfid_helper'): + helper_path = path('gpio', 'rfid_helper', '') + require_file(helper_path) + jobs[-1]['command'] += ['--rfid-helper', str(helper_path)] args = [str(executable), '--', '--managed-cabinet', '--cabinet='+cabinet_id] if real_gpio: args.append('--gpio=real') jobs.append(dict(name='cabinet', command=args, env=cabinet_env, port=None, ready=None, directory=executable.parent)) @@ -127,7 +139,7 @@ def wait_ready(job, process): raise RuntimeError(f"{job['name']} did not become ready; check its log") -def run(jobs, logs, open_browser=False): +def run(jobs, logs, open_browser=False, remote_targets=None): check_ports(jobs) logs.mkdir(parents=True, exist_ok=True) children, files = [], [] @@ -144,6 +156,9 @@ def run(jobs, logs, open_browser=False): if job['name'] == 'controller': print('Operator: '+job['ready'], flush=True) if open_browser: webbrowser.open(job['ready']) + if remote_targets: + from remote_cabinets import start_and_check + start_and_check(remote_targets, job) print('Running. Ctrl+C stops this stack. Exiting the game also stops services started here.', flush=True) while True: for job, process in children: @@ -178,12 +193,25 @@ def main(): try: print('Launcher configuration: '+str(Path(args.config).resolve()), flush=True) jobs, logs = plan(args.config, args.mode) + from remote_cabinets import load_targets + remote_targets = load_targets(args.config, args.mode) + if remote_targets: + controller = next(j for j in jobs if j['name'] == 'controller') + catalog_path = controller['command'][controller['command'].index('--catalog')+1] + known = json.loads(Path(catalog_path).read_text(encoding='utf-8'))['cabinets'] + tokens = json.loads(controller['env']['ARCADE_CABINET_TOKENS']) + for target in remote_targets: + if target['id'] not in known or target['id'] not in tokens: + raise ValueError('Remote cabinet must be registered and have a controller token: '+target['id']) + if any(j['name']=='cabinet' and '--cabinet='+target['id'] in j['command'] for j in jobs): + raise ValueError('Cannot launch the same cabinet identity locally and remotely') if args.check: check_ports(jobs) print('Configuration and ports OK. Planned services: '+', '.join(j['name'] for j in jobs)) print('Logs: '+str(logs)) + print('Remote start targets (not contacted by --check): '+', '.join(t['id'] for t in remote_targets)) return 0 - return run(jobs, logs, args.browser) + return run(jobs, logs, args.browser, remote_targets) except (ValueError, OSError, RuntimeError, configparser.Error) as exc: print('Startup failed: '+str(exc), file=sys.stderr) return 1 diff --git a/tools/launch/launcher-pc.ini b/tools/launch/launcher-pc.ini new file mode 100644 index 0000000..bcf5ada --- /dev/null +++ b/tools/launch/launcher-pc.ini @@ -0,0 +1,10 @@ +[launcher] +logs = logs + +[controller] +bind = 0.0.0.0 +port = 8766 +database = state/arcade_room.sqlite3 +catalog = ../room_controller/catalog.json +operator_token = Yu1qH7nz_aMUTlkk +cabinet_tokens = {"cabinet_01":"K32qg0lYioXmzWAK","cabinet_02":"LY9ATvM3wkTxJg0x","cabinet_03":"qZSD9eM2dmeRtmVA","cabinet_04":"2xXcj0sUbOzQCn2j","cabinet_05":"weISFvFBOt6pxfk-","cabinet_06":"h-lP-SfQMXT_dLtc","cabinet_07":"pUT4GdBYtW3slhOq"} diff --git a/tools/launch/launcher-pi.ini b/tools/launch/launcher-pi.ini new file mode 100644 index 0000000..377db83 --- /dev/null +++ b/tools/launch/launcher-pi.ini @@ -0,0 +1,7 @@ +[launcher] +logs = logs + +[cabinet] +id = cabinet_03 +url = 10.0.0.57 +token = qZSD9eM2dmeRtmVA \ No newline at end of file diff --git a/tools/launch/pi_service.py b/tools/launch/pi_service.py new file mode 100644 index 0000000..ff4a281 --- /dev/null +++ b/tools/launch/pi_service.py @@ -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()) diff --git a/tools/launch/remote.example.ini b/tools/launch/remote.example.ini new file mode 100644 index 0000000..efe4256 --- /dev/null +++ b/tools/launch/remote.example.ini @@ -0,0 +1,5 @@ +; Merge this section into the PC controller's existing launcher INI. +; Replace the hosts and usernames before setting enabled = true. +[remote] +enabled = false +cabinets = [{"id":"cabinet_01","host":"PI_1_HOST","user":"PI_USER"},{"id":"cabinet_02","host":"PI_2_HOST","user":"PI_USER"},{"id":"cabinet_03","host":"PI_3_HOST","user":"PI_USER"}] diff --git a/tools/launch/remote_cabinets.py b/tools/launch/remote_cabinets.py new file mode 100644 index 0000000..3ea4bdc --- /dev/null +++ b/tools/launch/remote_cabinets.py @@ -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()32: raise ValueError('Device config must be a list of up to 32 devices') + ids, ports, inputs, readers = set(), set(), set(), False + for d in data: + if not isinstance(d,dict) or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}',d.get('id','')): raise ValueError('Invalid device ID') + if d['id'] in ids or d.get('port') in ports: raise ValueError('Duplicate device ID or port') + ids.add(d['id']); ports.add(d.get('port')) + if not isinstance(d.get('name',''),str) or len(d.get('name',''))>100: raise ValueError('Invalid name') + kind, port = d.get('kind'), d.get('port') + if kind=='rfid': + if port not in ['J'+str(i) for i in range(1,9)]: raise ValueError('RFID requires J1-J8') + readers=True + elif kind in ('input','output'): + if port not in SIGNALS: raise ValueError('Digital devices require a signal connector') + d['pin']=SIGNALS[port] + if kind=='input': + if type(d.get('pull_up')) is not bool: raise ValueError('Inputs require explicit pull_up true/false') + inputs.add(d['pin']) + elif d['pin'] not in output_pins: raise ValueError('Output connector not enabled in output pin allowlist') + else: raise ValueError('Supported kinds: input, output, rfid') + if inputs & set(output_pins): raise ValueError('A GPIO cannot be both an input and an output') + if readers and RESERVED & (set(output_pins)|inputs): raise ValueError('RFID selector/reset/SPI pins cannot be configured as general GPIO') + return data + +class Feedback: + def __init__(self, definitions, input_factory, outputs, reader_binary=None, clock=time.monotonic): + self.definitions, self.outputs, self.clock = definitions, outputs, clock + self.lock=threading.RLock(); self.stop=threading.Event(); self.values={}; self.events=deque(maxlen=64) + self.boot=uuid.uuid4().hex; self.sequence=0; self.inputs=[]; self.worker=None; self.reader=None + try: + for d in definitions: + self.values[d['id']] = dict(id=d['id'],name=d.get('name',d['id']),port=d['port'],kind=d['kind'],value=None,error='Awaiting first sample',stamp=clock()) + if d['kind']=='input': self.inputs.append((d,input_factory(d['pin'],pull_up=d['pull_up'],bounce_time=.05))) + readers=[d for d in definitions if d['kind']=='rfid'] + if readers: + if not reader_binary: raise ValueError('RFID devices require --rfid-helper executable') + self.reader=subprocess.Popen([str(Path(reader_binary).resolve()),','.join(str(int(d['port'][1:])-1) for d in readers)],stdout=subprocess.PIPE,stderr=None,text=True,bufsize=1) + self.worker=threading.Thread(target=self._readers,args=(readers,),daemon=True); self.worker.start() + except Exception: + self.close(); raise + def update(self, ident, value, error=''): + with self.lock: + old=self.values[ident] + changed=old['value']!=value or old['error']!=error + old.update(value=value,error=error,stamp=self.clock()) + if changed: + self.sequence+=1 + self.events.append(dict(id=self.boot+':'+str(self.sequence),device=ident,value=value,error=error,kind=old['kind'])) + def _readers(self, readers): + slots={int(d['port'][1:])-1:d['id'] for d in readers} + try: + for line in self.reader.stdout: + if self.stop.is_set(): break + if len(line)>2048: continue + value=json.loads(line) + ident=slots.get(value.get('slot')) + if ident: + self.update(ident,str(value.get('uid',''))[:32],str(value.get('error',''))[:120]) + except Exception as exc: + for ident in slots.values(): self.update(ident,None,'Reader helper error: '+str(exc)[:80]) + finally: + if not self.stop.is_set(): + for ident in slots.values(): self.update(ident,None,'Reader helper stopped') + def tick(self): + for d,device in self.inputs: + try: self.update(d['id'],bool(device.value)) + except Exception as exc: self.update(d['id'],None,str(exc)[:120]) + with self.outputs.lock: + for d in self.definitions: + if d['kind']!='output': continue + entry=self.outputs.pins.get(d['pin']) + try: self.update(d['id'],bool(entry['device'].value) if entry else None,'' if entry else 'Output not initialized') + except Exception as exc: self.update(d['id'],None,str(exc)[:120]) + def snapshot(self): + with self.lock: + now=self.clock() + return dict(devices=[{k:v for k,v in d.items() if k!='stamp'}|{'age_ms':int(max(0,now-d['stamp'])*1000)} for d in self.values.values()],events=list(self.events)) + def close(self): + self.stop.set() + if self.reader: + self.reader.terminate() + try:self.reader.wait(timeout=2) + except subprocess.TimeoutExpired:self.reader.kill();self.reader.wait() + if self.worker:self.worker.join(timeout=2) + self.reader.stdout.close() + for _,device in self.inputs: device.close() + + +if __name__ == '__main__': + import argparse + parser=argparse.ArgumentParser(description='Extract saved PCB feedback inventory from the exported room catalog') + parser.add_argument('--catalog',required=True) + parser.add_argument('--cabinet',required=True) + parser.add_argument('--output',required=True) + args=parser.parse_args() + catalog=json.loads(Path(args.catalog).read_text(encoding='utf-8-sig')) + devices=catalog['cabinets'][args.cabinet].get('devices',[]) + with Path(args.output).open('x',encoding='utf-8') as file: + json.dump(devices,file,indent=2) + print('Wrote',len(devices),'device definitions. Review input polarity and output allowlist before running the bridge.') diff --git a/tools/pi_gpio/pi_gpio_bridge.py b/tools/pi_gpio/pi_gpio_bridge.py index c2a66c5..b5941b9 100644 --- a/tools/pi_gpio/pi_gpio_bridge.py +++ b/tools/pi_gpio/pi_gpio_bridge.py @@ -103,7 +103,7 @@ class Outputs: self.ledger.close() -def handler_for(outputs): +def handler_for(outputs, feedback=None): class Handler(BaseHTTPRequestHandler): def do_POST(self): try: @@ -115,7 +115,7 @@ def handler_for(outputs): data = json.loads(self.rfile.read(length) or b"{}") if self.path == "/heartbeat": outputs.heartbeat() - result = {"ok": True} + result = {"ok": True, "feedback": feedback.snapshot() if feedback else {"devices": [], "events": []}} elif self.path == "/output": result = outputs.command(data) print("GPIO:", data, flush=True) @@ -142,14 +142,19 @@ def main(): parser.add_argument("--controller", default="cabinet_01_gpio") parser.add_argument("--pins", default="17", help="BCM pin allowlist, comma separated") parser.add_argument("--ledger", default=str(Path.home() / ".local/state/arcade_gpio/requests.sqlite3")) + parser.add_argument("--devices", help="Explicit PCB feedback JSON inventory") + parser.add_argument("--rfid-helper", help="Built arcade-rfid helper executable") args = parser.parse_args() - pins = [int(n) for n in args.pins.split(",")] - if not pins or any(pin < 0 or pin > 27 for pin in pins): + pins = [int(n) for n in args.pins.split(",") if n.strip()] + if len(set(pins)) != len(pins) or any(pin < 0 or pin > 27 for pin in pins): parser.error("Use BCM header GPIO numbers 0..27") - from gpiozero import OutputDevice + from gpiozero import OutputDevice, Button + from device_feedback import Feedback, load_devices + definitions = load_devices(args.devices, pins) if args.devices else [] Path(args.ledger).parent.mkdir(parents=True, exist_ok=True) outputs = Outputs(OutputDevice, args.controller, pins, ledger=args.ledger) - server = HTTPServer(("127.0.0.1", 8765), handler_for(outputs)) + feedback = Feedback(definitions, Button, outputs, args.rfid_helper) + server = HTTPServer(("127.0.0.1", 8765), handler_for(outputs, feedback)) server.timeout = .05 stop = threading.Event() signal.signal(signal.SIGTERM, lambda *_: stop.set()) @@ -158,6 +163,7 @@ def main(): def watchdog(): while not stop.wait(.02): outputs.tick() + feedback.tick() worker = threading.Thread(target=watchdog, daemon=True) worker.start() @@ -168,6 +174,7 @@ def main(): finally: stop.set() worker.join() + feedback.close() outputs.close() server.server_close() diff --git a/tools/pi_gpio/rfid_helper/Cargo.toml b/tools/pi_gpio/rfid_helper/Cargo.toml new file mode 100644 index 0000000..c1ae7a5 --- /dev/null +++ b/tools/pi_gpio/rfid_helper/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "arcade-rfid" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +rppal = "0.22.1" +rppal_mfrc522 = "0.4.1" +serde_json = "1" diff --git a/tools/pi_gpio/rfid_helper/src/main.rs b/tools/pi_gpio/rfid_helper/src/main.rs new file mode 100644 index 0000000..57eeee3 --- /dev/null +++ b/tools/pi_gpio/rfid_helper/src/main.rs @@ -0,0 +1,43 @@ +// Uses the same MFRC522 library and selector sequence as Odin; no Odin room logic. +use rppal::{gpio::Gpio, spi::{Bus, Mode, SlaveSelect, Spi}}; +use rppal_mfrc522::Mfrc522; +use std::{thread, time::Duration, io::{self, Write}}; + +fn main() -> anyhow::Result<()> { + let arg = std::env::args().nth(1).unwrap_or_else(|| "0,1".into()); + let slots: Vec = arg.split(',').map(str::parse).collect::>()?; + anyhow::ensure!(!slots.is_empty() && slots.len() <= 8 && slots.iter().all(|x| *x < 8), "Slots must be 0..7"); + let gpio = Gpio::new()?; + let mut a = gpio.get(17)?.into_output_low(); + let mut b = gpio.get(27)?.into_output_low(); + let mut c = gpio.get(22)?.into_output_low(); + let mut reset = gpio.get(20)?.into_output_low(); + thread::sleep(Duration::from_millis(100)); + let mut spi = Spi::new(Bus::Spi0, SlaveSelect::Ss0, 1_000_000, Mode::Mode0)?; + let mut reader = Mfrc522::new(&mut spi); + loop { + for slot in &slots { + reset.set_low(); + if slot & 1 != 0 {a.set_high()} else {a.set_low()} + if slot & 2 != 0 {b.set_high()} else {b.set_low()} + if slot & 4 != 0 {c.set_high()} else {c.set_low()} + reset.set_high(); + thread::sleep(Duration::from_millis(50)); + let sample = (|| -> anyhow::Result { + reader.reset()?; + thread::sleep(Duration::from_millis(100)); + let version = reader.version()?; + anyhow::ensure!(version != 0 && version != 255, "Reader did not respond"); + Ok(match reader.uid(Duration::from_millis(250)) { + Ok(uid) => serde_json::json!({"slot":slot,"uid":format!("{:08X}",uid.to_u32()),"error":""}), + Err(_) => serde_json::json!({"slot":slot,"uid":"","error":"No tag read (absent or unreadable)"}), + }) + })(); + reset.set_low(); + thread::sleep(Duration::from_millis(50)); + let value = sample.unwrap_or_else(|e| serde_json::json!({"slot":slot,"uid":"","error":e.to_string()})); + println!("{}",value); + io::stdout().flush()?; + } + } +} diff --git a/tools/room_controller/README.md b/tools/room_controller/README.md index ce1df38..646b442 100644 --- a/tools/room_controller/README.md +++ b/tools/room_controller/README.md @@ -165,3 +165,40 @@ Automated two-client check (no GPIO; wins forced only by the test harness): ```sh python3 tests/room_network/check_chain_integration.py /path/to/godot ``` + + +## Three-cabinet objective test + +See the [three-cabinet walkthrough](../../docs/three_cabinet_test.md) for the separate Pac-Man five-second auto-win → Galaga 500-point objective → Frogger one-home scenario. This scenario requires updated `arcade-cabinet-2.1` game exports on all three cabinets. + +## Escape Gallery browser hub + +The browser now opens an **Escape Gallery** overview, with **Glitch: the Dark Machine** as the current room. Navigation provides Room control, Cabinets, Hardware, and Activity. Update the controller Python files and `operator.html`, restart the controller, and reload the browser. These controls use the existing cabinet protocol; this UI update alone does not require a new Godot export. + +Room control uses the boundary, walls, device references and cabinet positions exported in `catalog.json`. Cabinets without a position still appear in the cabinet list. Selecting a cabinet opens an original generic cabinet illustration: its marquee follows the assigned game and its screen shows reported phase. It is not gameplay video. Custom cabinet artwork and game-specific screen/marquee assets can replace this illustration later. + +Cabinet state refreshes approximately every 1.5 seconds. Device markers are reference locations until sensor integration is implemented. Output rings show pending or failed/unconfirmed commands, not physical device activation. Per-cabinet RFID, digital input, and electrical output feedback is available through the optional [PCB feedback bridge](../pi_gpio/README.md#pcb-live-feedback). Cabinet and Hardware views show readings, errors, and stale status; Activity records changes. The Pac-Man cabinet uses assigned-game artwork, not a live video feed. + +Hardware offers only each cabinet's configured actions. An operator reviews the action and supplies a reason. The controller requires real GPIO mode, an online prepared cabinet, the current session, and no pending commands. Requests are deduplicated and reasons appear in Activity. An accepted acknowledgement still does not establish physical delivery. A timeout is not retried automatically. Tokens remain in browser memory and Lock clears access on that device. + +Optional hub branding and links to other rooms/services can be set in a separate JSON file: + +```json +{ + "name": "Escape Gallery", + "room_name": "Glitch: the Dark Machine", + "services": [ + { + "id": "trivia", + "name": "Trivia", + "kind": "trivia", + "url": "http://TRIVIA_HOST:8080", + "description": "Open the trivia operator application" + } + ] +} +``` + +Replace the example host before using it, or keep `services` empty. Entries are links, not aggregated live status or shared authentication. Allowed kinds are `room`, `trivia`, and `service`. URLs cannot contain credentials or token query strings. + +Pass `--hub-config /path/to/hub.json` to `server.py`, or add `hub_config = /path/to/hub.json` under `[controller]` in the launcher INI. Restart after changes. Keep this optional setting when regenerating deployment configurations. Current authentication remains the shared operator token; individual staff identities are not implemented. diff --git a/tools/room_controller/art/pacman_marquee.png b/tools/room_controller/art/pacman_marquee.png new file mode 100644 index 0000000000000000000000000000000000000000..fb6b18ee1d6e3fd9b08eb7c27327cd06a5bac657 GIT binary patch literal 248178 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

    +!p9zwZt#jB#5XR=4#Z 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 literal 0 HcmV?d00001 diff --git a/tools/room_controller/art/pacman_screen.png.import b/tools/room_controller/art/pacman_screen.png.import new file mode 100644 index 0000000..14b76e7 --- /dev/null +++ b/tools/room_controller/art/pacman_screen.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5u7udnl0fhnw" +path="res://.godot/imported/pacman_screen.png-7852643f5de365f46bdcadee22b5b971.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://tools/room_controller/art/pacman_screen.png" +dest_files=["res://.godot/imported/pacman_screen.png-7852643f5de365f46bdcadee22b5b971.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/tools/room_controller/catalog.json b/tools/room_controller/catalog.json index b6781c9..ac606c5 100644 --- a/tools/room_controller/catalog.json +++ b/tools/room_controller/catalog.json @@ -2,59 +2,162 @@ "cabinets": { "cabinet_01": { "actions": [ - "coin_release", - "start_light" + "coin_slot_1", + "lock_released" ], "capabilities": [ "joystick_4way", - "button_1", "start_button", - "joystick_horizontal", - "button_2", - "button_3", - "button_4", - "button_5", - "button_6", - "select_button", - "trackball", - "joystick_8way", - "joystick_dual" + "select_button" ], - "name": "Master Cabinet", + "devices": [ + { + "id": "cab1_btn_red", + "kind": "input", + "name": "Red Button", + "port": "J17", + "pull_up": true + }, + { + "id": "cab1_btn_grn", + "kind": "input", + "name": "Green Button", + "port": "J18", + "pull_up": true + }, + { + "id": "cab1_btn_blue", + "kind": "input", + "name": "Blue Button", + "port": "J19", + "pull_up": true + }, + { + "id": "cab1_btn_ylw", + "kind": "input", + "name": "Yellow Btn", + "port": "J20", + "pull_up": true + }, + { + "id": "cab1_rfid2", + "kind": "rfid", + "name": "RFID Reader 2", + "port": "J2", + "pull_up": true + }, + { + "id": "cab1_rfid1", + "kind": "rfid", + "name": "RFID Reader 1", + "port": "J1", + "pull_up": true + } + ], + "name": "Cabinet 01", "position": [ - -5.94849967956543, - 0.147500038146973 + -6.55779981613159, + 1.98219990730286 ] }, "cabinet_02": { "actions": [ "coin_release", - "start_light", - "glitch_code" + "lock_released" ], "capabilities": [ - "joystick_4way", - "button_1", + "joystick_horizontal", "start_button", - "joystick_horizontal" + "select_button", + "button_1" + ], + "devices": [ + { + "id": "cab1_btn_red", + "kind": "input", + "name": "Red Button", + "port": "J17", + "pull_up": true + }, + { + "id": "cab1_btn_grn", + "kind": "input", + "name": "Green Button", + "port": "J18", + "pull_up": true + }, + { + "id": "cab1_btn_blue", + "kind": "input", + "name": "Blue Button", + "port": "J19", + "pull_up": true + }, + { + "id": "cab1_btn_ylw", + "kind": "input", + "name": "Yellow Btn", + "port": "J20", + "pull_up": true + }, + { + "id": "cab1_rfid2", + "kind": "rfid", + "name": "RFID Reader 2", + "port": "J2", + "pull_up": true + }, + { + "id": "cab1_rfid1", + "kind": "rfid", + "name": "RFID Reader 1", + "port": "J1", + "pull_up": true + } ], "name": "Cabinet 02", "position": [ - -3.51539182662964, - 0.142745211720467 + -2.89779996871948, + 1.98219990730286 ] }, "cabinet_03": { "actions": [ - "coin_release" + "coin_slot_1", + "lock_released" ], "capabilities": [ "spinner", "button_1", + "start_button", + "select_button" + ], + "devices": [], + "name": "Cabinet 03", + "position": [ + -2.89779996871948, + 3.20219993591309 + ] + }, + "cabinet_04": { + "actions": [ + "coin_slot_1", + "lock_released" + ], + "capabilities": [ + "roller", + "button_1", + "button_2", + "button_3", + "select_button", "start_button" ], - "name": "Cabinet 03", - "position": null + "devices": [], + "name": "Cabinet 4", + "position": [ + -6.55779981613159, + 3.20219993591309 + ] } }, "games": { @@ -252,6 +355,7 @@ "activate_after_win": "", "cabinet": "cabinet_01", "game": "pacman", + "game_config": {}, "mappings": {}, "variant": "standard" }, @@ -259,43 +363,106 @@ "activate_after_win": "cabinet_01", "cabinet": "cabinet_02", "game": "galaga", + "game_config": {}, "mappings": {}, "variant": "standard" } ], "name": "Cabinet 1 -> Cabinet 2" }, - "galaga_proof_of_concept": { - "assignments": [ - { - "activate_after_win": "", - "cabinet": "cabinet_01", - "game": "galaga", - "mappings": { - "game_won": [ - "coin_release" - ] - }, - "variant": "standard" - } - ], - "name": "Galaga Proof of Concept" - }, - "proof_of_concept": { + "eg_glitch": { "assignments": [ { "activate_after_win": "", "cabinet": "cabinet_01", "game": "pacman", - "mappings": { - "game_won": [ - "coin_release" - ] - }, + "game_config": {}, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "", + "cabinet": "cabinet_02", + "game": "galaga", + "game_config": {}, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "", + "cabinet": "cabinet_03", + "game": "tempest", + "game_config": {}, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "", + "cabinet": "cabinet_04", + "game": "missile_command", + "game_config": {}, + "mappings": {}, "variant": "standard" } ], - "name": "Proof of Concept" + "name": "Glitch: The Dark Machine" + }, + "pacman_rfid_credit_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "game_config": { + "boards_to_win": 1, + "rfid_allowed_uids": [], + "rfid_credit_device": "cab1_rfid1", + "score_to_win": 0 + }, + "mappings": {}, + "variant": "standard" + } + ], + "name": "Cabinet 1: Pac-Man RFID credit" + }, + "three_cabinet_objective_test": { + "assignments": [ + { + "activate_after_win": "", + "cabinet": "cabinet_01", + "game": "pacman", + "game_config": { + "boards_to_win": 0, + "score_to_win": 0, + "test_auto_win_seconds": 5.0 + }, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "cabinet_01", + "cabinet": "cabinet_02", + "game": "galaga", + "game_config": { + "score_to_win": 500, + "stages_to_win": 0 + }, + "mappings": {}, + "variant": "standard" + }, + { + "activate_after_win": "cabinet_02", + "cabinet": "cabinet_03", + "game": "frogger", + "game_config": { + "boards_to_win": 0, + "homes_to_win": 1 + }, + "mappings": {}, + "variant": "standard" + } + ], + "name": "3-Cabinet Test: 5 seconds / 500 points / 1 frog" }, "two_cabinet_test": { "assignments": [ @@ -303,6 +470,7 @@ "activate_after_win": "", "cabinet": "cabinet_01", "game": "pacman", + "game_config": {}, "mappings": { "game_started": [ "start_light" @@ -317,6 +485,7 @@ "activate_after_win": "", "cabinet": "cabinet_02", "game": "galaga", + "game_config": {}, "mappings": { "game_activated": [ "glitch_code" diff --git a/tools/room_controller/export_catalog.gd b/tools/room_controller/export_catalog.gd index e48c2d9..58457e3 100644 --- a/tools/room_controller/export_catalog.gd +++ b/tools/room_controller/export_catalog.gd @@ -14,6 +14,7 @@ func run() -> void: for action in cabinet.hardware_config.actions: actions.append(str(action.action_id)) var placement := room.get_cabinet_placement(cabinet.cabinet_id) catalog.cabinets[str(cabinet.cabinet_id)] = {"name": cabinet.display_name, "capabilities": Array(cabinet.capabilities), "actions": actions, "position": [placement.position.x, placement.position.y] if placement else null} + catalog.cabinets[str(cabinet.cabinet_id)]["devices"] = cabinet.pcb_config.feedback_devices() if cabinet.pcb_config else [] for scenario in database.scenarios: var assignments: Array = [] for assignment in scenario.assignments: @@ -22,7 +23,7 @@ func run() -> void: for mapping in assignment.event_mappings: if not mappings.has(str(mapping.game_event)): mappings[str(mapping.game_event)] = [] mappings[str(mapping.game_event)].append_array(Array(mapping.hardware_actions)) - assignments.append({"cabinet": str(assignment.cabinet.cabinet_id), "game": str(assignment.game.game_id), "variant": str(assignment.variant), "activate_after_win": str(assignment.activate_after_win), "mappings": mappings}) + assignments.append({"cabinet": str(assignment.cabinet.cabinet_id), "game": str(assignment.game.game_id), "variant": str(assignment.variant), "activate_after_win": str(assignment.activate_after_win), "game_config": assignment.game_config, "mappings": mappings}) catalog.scenarios[str(scenario.scenario_id)] = {"name": scenario.display_name, "assignments": assignments} for point in room.boundary_points: catalog.layout.boundary.append([point.x, point.y]) for wall in room.walls: catalog.layout.walls.append([[wall.start.x, wall.start.y], [wall.end.x, wall.end.y]]) diff --git a/tools/room_controller/operator.html b/tools/room_controller/operator.html index 5b6aaf7..8c5a02c 100644 --- a/tools/room_controller/operator.html +++ b/tools/room_controller/operator.html @@ -1,28 +1,46 @@ - - -Arcade · Room operator +Escape Gallery · Operations -
    ARCADE ESCAPE / OPERATIONS

    Room control

    Not connected
    -
    -

    Connect to this room

    Use the operator token configured on this controller. It stays in memory until this page closes or locks.

    -
    diff --git a/tools/room_controller/server.py b/tools/room_controller/server.py index 1e0735a..0c07974 100644 --- a/tools/room_controller/server.py +++ b/tools/room_controller/server.py @@ -3,7 +3,9 @@ import argparse import hmac import json +import math import os +import re from pathlib import Path import sqlite3 import time @@ -12,6 +14,42 @@ from urllib.parse import urlparse from http.server import BaseHTTPRequestHandler, HTTPServer +def hub_config(path=None): + default = dict(name="Escape Gallery", room_name="Glitch: the Dark Machine", services=[]) + if path is None: + return default + data = json.loads(Path(path).read_text(encoding="utf-8")) + if not isinstance(data, dict): + raise ValueError("Hub configuration must be an object") + for key in ("name", "room_name"): + value = data.get(key, default[key]) + if not isinstance(value, str) or not 1 <= len(value.strip()) <= 80: + raise ValueError("Invalid hub " + key) + default[key] = value.strip() + entries = data.get("services", []) + if not isinstance(entries, list) or len(entries) > 40: + raise ValueError("Hub services must be a list of up to 40 entries") + seen = set() + for entry in entries: + if not isinstance(entry, dict): raise ValueError("Invalid hub service") + ident, name, kind, url = (entry.get(k, "") for k in ("id", "name", "kind", "url")) + if not isinstance(ident, str) or not re.fullmatch(r"[a-z0-9_-]{1,64}", ident) or ident in seen: + raise ValueError("Invalid or duplicate hub service ID") + seen.add(ident) + if not isinstance(name, str) or not 1 <= len(name) <= 80 or kind not in ("room", "trivia", "service"): + raise ValueError("Invalid hub service name or kind") + if not isinstance(url, str): raise ValueError("Invalid hub service URL") + parsed = urlparse(url) + if parsed.scheme not in ("http", "https") or not parsed.hostname or parsed.username or parsed.password or parsed.fragment or parsed.query or any(c.isspace() or ord(c) < 32 for c in url) or "\\" in url: + raise ValueError("Use an HTTP(S) service URL without credentials, query or fragment") + try: parsed.port + except ValueError: raise ValueError("Invalid hub service port") + description = entry.get("description", "") + if not isinstance(description, str) or len(description) > 240: raise ValueError("Invalid service description") + default["services"].append(dict(id=ident, name=name, kind=kind, url=url, description=description)) + return default + + class Room: def __init__(self, database, clock=time.time, cabinet_id="cabinet_01"): self.clock = clock @@ -25,9 +63,13 @@ class Room: self.state = json.loads(row[0]) if row else dict( session="", phase="idle", reward="not requested", commands=[], seen=[], history=[]) # Never carry live connectivity across a controller restart. + self.feedback = {} + self.feedback_at = 0 + self.feedback_seen = [] self.last_seen = 0 self.boot = self.state.get("boot", "") self.version = "unknown" + self.features = [] self.gpio = "unknown" self.state.setdefault("game", "pacman") self.state.setdefault("mappings", {"game_won": ["coin_release"]}) @@ -47,8 +89,42 @@ class Room: def snapshot(self): self.expire() return dict(self.state, online=self.online(), cabinet=self.cabinet_id, - version=self.version, gpio=self.gpio, - last_seen=self.last_seen, protocol=2) + version=self.version, gpio=self.gpio, features=self.features, + last_seen=self.last_seen, protocol=2, feedback=self.feedback_snapshot()) + + def feedback_snapshot(self): + result = dict(self.feedback) + age = max(0, self.clock() - self.feedback_at) + result['fresh'] = bool(result.get('fresh') and self.online() and age < 4) + result['devices'] = [dict(d, age_ms=d['age_ms'] + int(age*1000)) for d in result.get('devices', [])] + return result + + def receive_feedback(self, payload): + if not isinstance(payload, dict): return + devices, events = payload.get('devices', []), payload.get('events', []) + if not isinstance(devices, list) or len(devices) > 32 or not isinstance(events, list) or len(events) > 64: return + clean = [] + for d in devices: + if not isinstance(d, dict) or d.get('kind') not in ('input', 'output', 'rfid'): continue + ident = d.get('id') + if not isinstance(ident, str) or not re.fullmatch(r'[A-Za-z0-9_-]{1,64}', ident): continue + value = d.get('value') + if value is not None and type(value) is not bool and not isinstance(value, str): continue + age = d.get('age_ms', 999999) + if type(age) not in (int, float) or not math.isfinite(age) or age < 0: continue + clean.append(dict(id=ident, name=str(d.get('name',ident))[:100], port=str(d.get('port',''))[:8], kind=d['kind'], value=value[:64] if isinstance(value,str) else value, error=str(d.get('error',''))[:120], age_ms=min(int(age),999999))) + fresh = payload.get('fresh') is True and self.gpio == 'real' + self.feedback = dict(devices=clean, fresh=fresh) + self.feedback_at = self.clock() + if not fresh: return + known = {d['id'] for d in clean} + for event in events: + if not isinstance(event, dict): continue + ident = event.get('id') + if not isinstance(ident,str) or len(ident)>100 or ident in self.feedback_seen or event.get('device') not in known: continue + self.feedback_seen.append(ident) + self.feedback_seen = self.feedback_seen[-256:] + self.log('Device '+event['device']+': '+str(event.get('error') or event.get('value'))[:120]) def expire(self): for command in self.state["commands"]: @@ -65,7 +141,7 @@ class Room: command = dict(id=str(uuid.uuid4()), session=self.state["session"], type=kind, status="queued", expires=self.clock() + 15) if kind == "prepare": - command.update(game=self.state["game"], variant=self.state.get("variant", "standard")) + command.update(game=self.state["game"], variant=self.state.get("variant", "standard"), game_config=self.state.get("game_config", {})) self.state["commands"].append(command) self.log("Queued " + kind) return command @@ -82,6 +158,8 @@ class Room: reward="not requested", commands=[], seen=[]) self.command(action) elif action == "start": + if self.state.get("game_config", {}).get("rfid_credit_device"): + raise ValueError("Scan a card and press Start on the cabinet for this RFID scenario") if self.state["phase"] != "ready": raise ValueError("Prepare the cabinet first") self.state["phase"] = "starting" self.command("start") @@ -103,7 +181,9 @@ class Room: self.state["boot"] = boot self.last_seen = self.clock() self.version = str(data.get("version", "unknown"))[:80] + self.features = data.get("features", []) if isinstance(data.get("features", []), list) else [] self.gpio = str(data.get("gpio", "unknown"))[:100] + self.receive_feedback(data.get("feedback", {})) self.expire() reports = data.get("reports", []) if not isinstance(reports, list) or len(reports) > 50: raise ValueError("Invalid reports") @@ -170,7 +250,8 @@ class Room: class Fleet: """Editor catalog plus independently acknowledged cabinet sessions.""" - def __init__(self, database, catalog, clock=time.time): + def __init__(self, database, catalog, clock=time.time, hub=None): + self.hub = hub if hub is not None else hub_config() self.catalog = catalog self.rooms = {key: Room(database, clock, key) for key in catalog["cabinets"]} @@ -187,7 +268,7 @@ class Fleet: compatible_games=[g for g in self.catalog["games"] if self.compatible(key, g)]) # Retain the single-cabinet desktop UI response while the browser offers the fleet view. first = cabinets.get("cabinet_01", next(iter(cabinets.values()))) - return dict(first, cabinets=cabinets, catalog=self.catalog) + return dict(first, cabinets=cabinets, catalog=self.catalog, hub=self.hub) def validate_assignment(self, item): cabinet, game = item.get("cabinet"), item.get("game") @@ -195,6 +276,39 @@ class Fleet: raise ValueError("Unknown cabinet or game") if not self.compatible(cabinet, game): raise ValueError(f"{game} cannot run on {cabinet}: hardware requirements not met") if item.get("variant", "standard") != "standard": raise ValueError("Only standard variants are supported by this runtime") + settings = item.get("game_config", {}) + if not isinstance(settings, dict): raise ValueError("game_config must be an object") + limits = {"test_auto_win_seconds": (0.1, 300)} + limits.update({"galaga": {"score_to_win": (0, 1000000), "stages_to_win": (0, 100)}, + "frogger": {"homes_to_win": (0, 5), "boards_to_win": (0, 99)}, + "pacman": {"boards_to_win": (0, 100), "score_to_win": (0, 1000000)}}.get(game, {})) + credit_device = settings.get("rfid_credit_device") + if credit_device is not None: + if game != "pacman" or not isinstance(credit_device, str) or not re.fullmatch(r"[A-Za-z0-9_-]{1,64}", credit_device): + raise ValueError("Invalid RFID credit device") + if not any(d.get("id") == credit_device and d.get("kind") == "rfid" for d in self.catalog["cabinets"][cabinet].get("devices", [])): + raise ValueError("RFID credit device is missing from the cabinet PCB inventory") + if "rfid_credit_v1" not in self.rooms[cabinet].features: + raise ValueError("Update the game export: RFID credits require arcade-cabinet-2.3") + if self.rooms[cabinet].gpio != "real": + raise ValueError("RFID credits require real GPIO feedback") + if item.get("activate_after_win"): + raise ValueError("RFID credit scenarios require player Start, not automatic start") + if "rfid_allowed_uids" in settings: + uids = settings["rfid_allowed_uids"] + if credit_device is None or not isinstance(uids, list) or len(uids) > 100 or any(not isinstance(uid, str) or not re.fullmatch(r"[0-9A-F]{8}", uid) for uid in uids): + raise ValueError("RFID allowed UIDs must be a list of uppercase 8-digit hexadecimal IDs") + for key, value in settings.items(): + if key in ("rfid_credit_device", "rfid_allowed_uids"): continue + if key not in limits or type(value) not in (int, float) or not math.isfinite(value): + raise ValueError("Unsupported game objective: " + str(key)) + low, high = limits[key] + if not low <= value <= high or (key != "test_auto_win_seconds" and value != int(value)): + raise ValueError("Invalid game objective: " + key) + if settings and not self.rooms[cabinet].online(): + raise ValueError(f"{cabinet} is offline; check its controller URL, cabinet ID and token before preparing objectives") + if settings and "assignment_config_v1" not in self.rooms[cabinet].features: + raise ValueError(f"Update the game export on {cabinet}: connected agent {self.rooms[cabinet].version} does not advertise assignment objectives (requires arcade-cabinet-2.1)") allowed = set(self.catalog["cabinets"][cabinet]["actions"]) for event, actions in item.get("mappings", {}).items(): if event not in ("game_activated", "game_started", "game_won", "game_lost"): @@ -210,7 +324,13 @@ class Fleet: def operator(self, action, data=None): data = data or {} - if action == "prepare_scenario": + if "expected_session" in data: + cabinet = data.get("cabinet") + if cabinet not in self.rooms or self.rooms[cabinet].state["session"] != data["expected_session"]: + raise ValueError("Cabinet session changed; review its current state before sending a command") + if action == "trigger_output": + self.trigger_output(data) + elif action == "prepare_scenario": scenario_id = data.get("scenario") scenario = self.catalog["scenarios"].get(scenario_id) if not scenario: raise ValueError("Unknown scenario") @@ -256,10 +376,47 @@ class Fleet: self.advance() return self.snapshot() + def trigger_output(self, data): + cabinet, action_id = data.get("cabinet"), data.get("action_id") + if cabinet not in self.rooms: raise ValueError("Unknown cabinet") + room = self.rooms[cabinet] + request_id = data.get("request_id", "") + reason = data.get("reason", "") + if not isinstance(request_id, str) or not re.fullmatch(r"[A-Za-z0-9_-]{16,100}", request_id): + raise ValueError("Manual hardware command requires a unique request ID") + if not isinstance(reason, str) or not 3 <= len(reason.strip()) <= 200: + raise ValueError("Enter a reason (3–200 characters) for the hardware action") + if data.get("expected_session") != room.state["session"]: + raise ValueError("Cabinet session changed; review before triggering hardware") + if action_id not in self.catalog["cabinets"][cabinet]["actions"]: + raise ValueError("Hardware action is not configured for this cabinet") + receipts = room.state.setdefault("manual_requests", {}) + if request_id in receipts: + if receipts[request_id] != [action_id, room.state["session"]]: + raise ValueError("Request ID was already used for another action/session") + return # A lost HTTP reply must not cause a second physical action. + if len(receipts) >= 10000: + raise ValueError("Manual action history limit reached; contact the administrator") + room.expire() + if not room.online(): raise ValueError("Cabinet is offline; no command sent") + if room.gpio != "real": raise ValueError("Cabinet must report real GPIO mode") + if not room.state["session"] or room.state["phase"] not in ("ready", "playing", "won", "lost"): + raise ValueError("Prepare the cabinet before triggering its hardware") + if any(c["status"] == "queued" for c in room.state["commands"]): + raise ValueError("Wait for the pending command or its timeout") + command = room.command("output") + command["action_id"] = action_id + command["source"] = "operator" + room.state["last_output"] = "requested" + receipts[request_id] = [action_id, room.state["session"]] + # Refuse new requests rather than evict deduplication receipts during a session. + room.log("Operator triggered " + str(action_id) + ": " + reason.strip()) + room.save() + def prepare(self, item, scenario): room = self.rooms[item["cabinet"]] room.state.update(game=item["game"], mappings=item.get("mappings", {}), - variant=item.get("variant", "standard"), scenario=scenario) + variant=item.get("variant", "standard"), game_config=item.get("game_config", {}), scenario=scenario) room.state.pop("gate", None) room.state.pop("auto_start", None) room.operator("reset" if item.get("activate_after_win") else "prepare") @@ -363,6 +520,7 @@ def main(): parser.add_argument("--port", type=int, default=8766) parser.add_argument("--database", default="arcade_room.sqlite3") parser.add_argument("--catalog", default=str(Path(__file__).with_name("catalog.json"))) + parser.add_argument("--hub-config", help="Optional operations hub names and service links") args = parser.parse_args() operator = os.environ.get("ARCADE_OPERATOR_TOKEN", "") cabinet = os.environ.get("ARCADE_CABINET_TOKEN", "") @@ -375,7 +533,11 @@ def main(): if not isinstance(tokens, dict) or not tokens or len(operator) < 16 or any(not isinstance(t, str) or len(t) < 16 or t == operator for t in tokens.values()) or len(set(tokens.values())) != len(tokens): parser.error("Set an operator token and unique cabinet tokens (16+ characters); use ARCADE_CABINET_TOKEN for cabinet_01 or ARCADE_CABINET_TOKENS JSON for multiple cabinets") catalog = json.loads(Path(args.catalog).read_text(encoding="utf-8")) - room = Fleet(Path(args.database), catalog) + try: + hub = hub_config(args.hub_config) + except (OSError, ValueError) as exc: + parser.error(str(exc)) + room = Fleet(Path(args.database), catalog, hub=hub) server = HTTPServer((args.bind, args.port), handler_for(room, operator, tokens)) print(f"Arcade controller ready at http://{args.bind}:{args.port}/ — {len(room.rooms)} cabinets", flush=True) try: server.serve_forever() diff --git a/ui/room_layout.gd b/ui/room_layout.gd index 92dfca0..d373dd5 100644 --- a/ui/room_layout.gd +++ b/ui/room_layout.gd @@ -82,6 +82,8 @@ var highlighted_bindings: Dictionary = {} var binding_labels: Array[Label] = [] var show_connections: bool = true +var show_labels: bool = true +var show_dimensions: bool = true var selected_binding_key: String = "" @@ -178,6 +180,7 @@ func _rebuild() -> void: _build_cabinets() _build_devices() _build_binding_labels() + set_labels_visible(show_labels) queue_redraw() @@ -976,7 +979,7 @@ func set_connections_visible( show_connections = visible for label in binding_labels: - label.visible = visible + label.visible = visible and show_labels queue_redraw() @@ -1703,7 +1706,7 @@ func _draw_dimension_label( text: String ) -> void: - if text.is_empty(): + if not show_dimensions or text.is_empty(): return var font := ( @@ -2075,7 +2078,7 @@ func _draw_walls() -> void: if room == null: return - for wall in room.walls: + for wall in _ordered_walls(): if wall == null: continue @@ -2111,13 +2114,7 @@ func _draw_walls() -> void: width += 3.0 - draw_line( - start, - end, - wall_color, - width, - true - ) + _draw_wall_or_door(wall, start, end, wall_color, width) # # Wall dimension. @@ -2867,7 +2864,9 @@ func _find_wall_at_screen_position( var hit_radius_pixels: float = 10.0 - for wall in room.walls: + var ordered := _ordered_walls() + ordered.reverse() + for wall in ordered: if wall == null: continue @@ -3987,3 +3986,72 @@ func _get_cabinet_profile( return assignment.cabinet return null + + +func set_labels_visible(value: bool) -> void: + show_labels = value + for item in item_nodes.values(): + item.title_label.visible = value + item.subtitle_label.visible = value + item.tooltip_text = item.base_title + "\n" + item.base_subtitle + for label in binding_labels: + label.visible = value and show_connections + queue_redraw() + +func set_dimensions_visible(value: bool) -> void: + show_dimensions = value + queue_redraw() + +func _ordered_walls() -> Array[RoomWallDefinition]: + var ordered: Array[RoomWallDefinition] = [] + for wall in room.walls: + if wall != null and not wall.is_doorway: + ordered.append(wall) + for wall in room.walls: + if wall != null and wall.is_doorway: + ordered.append(wall) + return ordered + +# Openings are projected onto collinear wall segments without destroying geometry. +func _wall_solid_ranges(wall: RoomWallDefinition) -> Array[Vector2]: + var ranges: Array[Vector2] = [Vector2(0, 1)] + var delta := wall.end - wall.start + if delta.length_squared() < 0.000001: + return ranges + for door in room.walls: + if door == null or not door.is_doorway: + continue + var tolerance := maxf(wall.thickness * 0.5, 0.02) + if absf(delta.cross(door.start - wall.start)) / delta.length() > tolerance or absf(delta.cross(door.end - wall.start)) / delta.length() > tolerance: + continue + var a := delta.dot(door.start - wall.start) / delta.length_squared() + var b := delta.dot(door.end - wall.start) / delta.length_squared() + var low := minf(a, b) + var high := maxf(a, b) + var next: Array[Vector2] = [] + for segment in ranges: + if high <= segment.x or low >= segment.y: + next.append(segment) + else: + if low > segment.x: + next.append(Vector2(segment.x, low)) + if high < segment.y: + next.append(Vector2(high, segment.y)) + ranges = next + return ranges + +func _draw_wall_or_door(wall: RoomWallDefinition, start: Vector2, end: Vector2, color: Color, width: float) -> void: + if not wall.is_doorway: + for segment in _wall_solid_ranges(wall): + draw_line(start.lerp(end, segment.x), start.lerp(end, segment.y), color, width, true) + return + var direction := end - start + if direction.length() < 1.0: + return + var angle := direction.angle() + var swing := -PI / 2.0 if wall.swing_reversed else PI / 2.0 + var door_color := Color("67d5c5") if wall.wall_id != selected_wall_id else color + draw_dashed_line(start, end, door_color, 1.5, 5.0) + draw_line(start, start + direction.rotated(swing), door_color, 2.0, true) + draw_arc(start, direction.length(), minf(angle, angle + swing), maxf(angle, angle + swing), 32, door_color, 1.5, true) + draw_circle(start, 4.0, door_color)