Rust backend for the connected pinball system. ESP32 (MQTT) <-> Central API (WebSocket) communication.
- 1 feature = add unit tests
- Follow DRY & SOLID principles
- Write clean code (meaningful variable names, use named constants, etc.)
- Add inline comments
//and doc comments///for important parts
- One PR = one concern
- Commit messages must be descriptive (please follow the convention)
crates/
├── api/ # HTTP + WebSocket server (Axum) — REST routes, JWT auth, Lucyd docs
├── game-logic/ # Game state machine and scoring engine
├── mqtt-bridge/ # Bidirectional relay MQTT <-> WebSocket
├── screen-hub/ # Bidirectional relay Frontend apps <-> Backend
└── shared/ # Types, events, DTOs shared across crates
docker compose up --build -d # Full stack
docker compose up -d mosquitto # Broker only (for MQTT Explorer)| Service | Port | Description |
|---|---|---|
| API | 8080 | REST + WS (/ws/bridge, /health) |
| Mosquitto | 1883 | MQTT broker (anonymous) |
| Nginx | 80 | Reverse proxy |
| Lucyd | 8080 | Interactive API docs (/docs) |
rustup default stable # Rust 1.89+
cargo build # Build workspace
cargo test # 16 tests (shared + api + bridge)Documentation of the codebase:
cargo doc --openInstallation
cargo install cargo-llvm-cov
Run the tests + report in the terminal
cargo llvm-cov
HTML report + Tab of the coverage
cargo llvm-cov --html
(Last testing 15/04/2026: ~61.5% codecoverage)
| Dep | Role |
|---|---|
| Rust edition 2024 / resolver 3 | Toolchain |
| Axum 0.8 | HTTP + WebSocket server |
| rumqttc 0.25 | Async MQTT client |
| tokio-tungstenite 0.26 | WebSocket client (bridge) |
| lucyd ≥ 0.1.9 | Interactive OpenAPI docs UI (/docs) |
| utoipa 5.4 | OpenAPI spec generation (Axum integration) |
| sqlx 0.8 | Async SQLite driver with migrations |
| jsonwebtoken 9 | JWT encoding / validation |
| tower-http 0.6 | CORS + request tracing middleware |
| thiserror 2 | Ergonomic error types |
| schemars 0.8 | JSON Schema generation |
| tracing + tracing-subscriber | Structured JSON logging |
| cargo-chef | Docker layer caching |
All frontend screens (front_screen, back_screen, dmd_screen) connect to the backend via WebSocket. Every message exchanged is a ScreenEnvelope serialized as JSON.
{
"from": "front_screen",
"to": { "kind": "screen", "id": "back_screen" },
"event_type": "game_state_update",
"payload": { "score": 42000, "combo": 3 }
}| Field | Type | Description |
|---|---|---|
from |
ScreenId |
Sender — front_screen, back_screen, or dmd_screen |
to |
ScreenTarget |
{"kind":"screen","id":"<id>"} or {"kind":"broadcast"} |
event_type |
string |
Free-form event name (e.g. game_state_update) |
payload |
any JSON |
Arbitrary data, no fixed schema (intentionally flexible) |
Screen WS client
│ JSON text frame (ScreenEnvelope)
▼
WS handler ──deserialise──► ScreenRouter
│
Interceptor chain
(validate / enrich / swallow)
│
ScreenRegistry
(mpsc per screen, cap 128)
│
┌──────────┴──────────┐
target screen all others
(unicast) (broadcast)
Key behaviours:
- A screen can only have one active connection — duplicate connections are rejected.
- Broadcast delivers to every connected screen except the sender.
- If a screen's channel is full (128 messages), the message is dropped and a warning is logged.
- When the WS connection closes, the
ScreenGuardauto-unregisters the screen from the registry. - Interceptors run before dispatch and can mutate or swallow any message.
Images are automatically built and pushed to ghcr.io/flipgame-hetic/backend on every push to main or dev, and on each GitHub release. The latest tag always tracks main; branches and releases get their own tags (branch name, semver, short SHA). Build cache is stored in the registry itself (buildcache tag) to speed up subsequent runs.
pinball/<device_id>/<subtopic>
Inbound: input/button, input/plunger, input/gyro, telemetry, events, status
Outbound: ball/hit, game/state, cmd