-
Notifications
You must be signed in to change notification settings - Fork 0
Games! #23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Games! #23
Changes from 3 commits
bbe7fee
0f3b56b
e12dfb2
dbdb089
c9bdfd0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,135 @@ | ||
| # dramma | ||
|
|
||
| Donation kiosk + arcade machine for Hacker Embassy. | ||
| Accepts cash (bills + coins), processes donations, and now lets people insert coins to play retro games on RetroArch. | ||
|
|
||
| --- | ||
|
|
||
| ## Running | ||
|
|
||
| ```bash | ||
| nix-shell | ||
| cargo run | ||
| ``` | ||
|
|
||
| The app starts fullscreen. Tap the logo **5 times** to open the diagnostics panel. | ||
|
|
||
| --- | ||
|
|
||
| ## Configuration | ||
|
|
||
| Create `.config/dramma.toml` next to the binary (or in the working directory you run from): | ||
|
|
||
| ```toml | ||
| token = "your-bearer-token" # For Bot donates | ||
|
|
||
| # Optional overrides (these are the defaults): | ||
| home_assistant_url = "https://ha.hackem.cc/web-dramma/0?BrowserID=dramma" | ||
| cashcode_serial_port = "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0" | ||
| cctalk_serial_port = "/dev/ttyUSB0" | ||
| stats_db_path = "data/Stats.db" | ||
| ``` | ||
|
|
||
| --- | ||
|
|
||
| ## 🕹️ Setting Up Games (Arcade Mode) | ||
|
|
||
| Pressing **PLAY** on the main screen takes the user to the coin-insertion screen where they can select a game and insert money: | ||
|
|
||
| > **100 AMD = 5 minutes of playtime** | ||
| > **50 AMD = 2 minutes 30 seconds** | ||
|
|
||
| When they hit **Launch**, dramma starts RetroArch fullscreen + kiosk and auto-closes it when the time runs out. The speaker will announce "2 minutes left" and "1 minute left". | ||
|
|
||
| > ROMs are not included for obvious copyright reasons. You know where to find them. | ||
|
|
||
| ### Configure games in dramma.toml | ||
|
|
||
| Add a `[[games]]` block for each game. `name` is what shows up in the UI, `core` is the path to the `.so`, `rom` is the path to your ROM file. | ||
|
|
||
| ```toml | ||
| retroarch_command = "retroarch" | ||
|
|
||
| [[games]] | ||
| name = "🧱 Tetris" | ||
| core = "/etc/retroarch/cores/nestopia_libretro.so" | ||
| rom = "/home/dramma/roms/tetris.nes" | ||
|
|
||
| [[games]] | ||
| name = "🟡 Pac-Man" | ||
| core = "/etc/retroarch/cores/fbneo_libretro.so" | ||
| rom = "/home/dramma/roms/pacman.zip" | ||
|
|
||
| [[games]] | ||
| name = "🦔 Sonic the Hedgehog" | ||
| core = "/etc/retroarch/cores/picodrive_libretro.so" | ||
| rom = "/home/dramma/roms/sonic.md" | ||
|
|
||
| [[games]] | ||
| name = "🔫 DOOM" | ||
| core = "/etc/retroarch/cores/dosbox_pure_libretro.so" | ||
| rom = "/home/dramma/roms/doom.wad" | ||
|
|
||
| [[games]] | ||
| name = "👊 Street Fighter II" | ||
| core = "/etc/retroarch/cores/fbneo_libretro.so" | ||
| rom = "/home/dramma/roms/sf2.zip" | ||
| ``` | ||
|
|
||
| If `[[games]]` is **not configured**, the UI shows a built-in placeholder list (same names, no actual cores/ROMs). RetroArch will still launch but will open its own menu — not useful in production. | ||
|
|
||
| ### Test it manually first | ||
|
|
||
| Before trusting the machine to do it, test the exact command dramma will run: | ||
|
|
||
| ```bash | ||
| retroarch --fullscreen --kiosk -L /path/to/core_libretro.so /path/to/rom | ||
| ``` | ||
|
|
||
| If the game boots correctly, the config is right. If RetroArch opens its menu instead of loading the game, the core or ROM path is wrong. | ||
|
|
||
|
|
||
| --- | ||
|
|
||
| ## Money → Time conversion | ||
|
|
||
| | Inserted | Play time | | ||
| |---|---| | ||
| | 50 ֏ | 2 min 30 sec | | ||
| | 100 ֏ | 5 min | | ||
| | 200 ֏ | 10 min | | ||
| | 500 ֏ | 25 min | | ||
|
|
||
| --- | ||
|
|
||
| ## Architecture | ||
|
|
||
| ``` | ||
| main.rs | ||
| ├── bill_acceptor — CashCode bill acceptor driver (serial) | ||
| ├── coin_acceptor — ccTalk coin acceptor driver (serial) | ||
| ├── donation_handler — Donation flow + inactivity timeout | ||
| ├── game_handler — Arcade mode: RetroArch lifecycle + session timer | ||
| ├── home_assistant_handler — Chromium kiosk for HASS page | ||
| └── diagnostics_handler — Debug log viewer | ||
|
|
||
| src/ | ||
| ├── cashcode.rs — CashCode serial protocol | ||
| ├── cctalk.rs — ccTalk serial protocol | ||
| ├── config.rs — dramma.toml loader | ||
| ├── retroarch.rs — RetroArch process manager | ||
| ├── sound.rs — Audio (yippee + time warnings) | ||
| └── ... | ||
|
|
||
| ui/ | ||
| ├── pages/ | ||
| │ ├── main.slint — Main screen (Donate / Play / HASS) | ||
| │ ├── insert_coins.slint — Game selector + coin insertion | ||
| │ ├── insert_money.slint — Donation coin insertion | ||
| │ ├── donate.slint — Donation form | ||
| │ └── ... | ||
| └── assets/ | ||
| ├── yippee.wav | ||
| ├── two_minutes_left.wav | ||
| └── one_minute_left.wav | ||
| ``` |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -15,36 +15,42 @@ pub enum ConfigError { | |||||
| ParseError(#[from] toml::de::Error), | ||||||
| } | ||||||
|
|
||||||
| /// A single playable game entry, configured via `dramma.toml`. | ||||||
| #[derive(Debug, Clone, Deserialize)] | ||||||
| pub struct GameEntry { | ||||||
| pub name: String, | ||||||
| pub core: String, | ||||||
| pub rom: String, | ||||||
| } | ||||||
|
|
||||||
| #[derive(Debug, Clone, Deserialize)] | ||||||
| #[serde(default)] | ||||||
| pub struct Config { | ||||||
| pub token: Option<String>, | ||||||
| pub home_assistant_url: String, | ||||||
| pub hass_api_port: u16, | ||||||
| pub cashcode_serial_port: String, | ||||||
| /// Serial port for the ccTalk coin acceptor. Set to `"auto"` (the | ||||||
| /// default) to probe all available USB serial ports at startup and on | ||||||
| /// every reconnect, which handles `/dev/ttyUSBx` number changes. | ||||||
| pub cctalk_serial_port: String, | ||||||
| /// Override the value for specific coin positions (channels). | ||||||
| /// Use this when the device has misconfigured coin IDs. | ||||||
| /// Format in dramma.toml: | ||||||
| /// cctalk_coin_overrides = [[1, 50], [3, 500]] | ||||||
| /// means position 1 → 50 AMD, position 3 → 500 AMD. | ||||||
| pub cctalk_coin_overrides: Vec<[i32; 2]>, | ||||||
| pub stats_db_path: String, | ||||||
| pub retroarch_command: String, | ||||||
| pub games: Vec<GameEntry>, | ||||||
| } | ||||||
|
|
||||||
| impl Default for Config { | ||||||
| fn default() -> Self { | ||||||
| Self { | ||||||
| token: None, | ||||||
| home_assistant_url: "https://ha.hackem.cc/web-dramma/0?BrowserID=dramma".to_string(), | ||||||
| hass_api_port: 8321, | ||||||
| cashcode_serial_port: | ||||||
| "/dev/serial/by-id/usb-Prolific_Technology_Inc._USB-Serial_Controller_D-if00-port0" | ||||||
| .to_string(), | ||||||
| cctalk_serial_port: "auto".to_string(), | ||||||
| cctalk_serial_port: "/dev/ttyUSB0".to_string(), | ||||||
|
||||||
| cctalk_serial_port: "/dev/ttyUSB0".to_string(), | |
| cctalk_serial_port: "auto".to_string(), |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,8 @@ | ||
| use log::{error, info}; | ||
| use std::io::{Read, Write}; | ||
| use std::net::TcpListener; | ||
| use std::process::{Child, Command}; | ||
| use std::sync::mpsc::Sender; | ||
| use std::sync::{Arc, Mutex}; | ||
|
|
||
| /// Manages a Chromium subprocess for displaying Home Assistant | ||
|
|
@@ -30,7 +33,7 @@ impl ChromiumManager { | |
| // Try chromium first, then chromium-browser as fallback (different Debian versions) | ||
| let command_result = Command::new("chromium") | ||
| .arg("--app=".to_string() + url) | ||
| .arg("--window-size=1280,940") | ||
| .arg("--start-fullscreen") | ||
| .arg("--window-position=0,0") | ||
| .arg("--disable-infobars") | ||
| .arg("--noerrdialogs") | ||
|
|
@@ -50,7 +53,7 @@ impl ChromiumManager { | |
| // Fallback to chromium-browser | ||
| Command::new("chromium-browser") | ||
| .arg("--app=".to_string() + url) | ||
| .arg("--window-size=1280,940") | ||
| .arg("--start-fullscreen") | ||
| .arg("--window-position=0,0") | ||
| .arg("--disable-infobars") | ||
| .arg("--noerrdialogs") | ||
|
|
@@ -108,3 +111,46 @@ impl Drop for ChromiumManager { | |
| self.close(); | ||
| } | ||
| } | ||
|
|
||
| /// Starts a simple HTTP listener for remote control from Home Assistant. | ||
| /// When a `POST /close-hass` request is received, sends a signal through `tx`. | ||
| #[allow(dead_code)] | ||
| pub fn start_close_listener(port: u16, tx: Sender<()>) { | ||
| let addr = format!("0.0.0.0:{}", port); | ||
| let listener = match TcpListener::bind(&addr) { | ||
| Ok(l) => l, | ||
| Err(e) => { | ||
| error!("Failed to bind HASS close listener on {}: {}", addr, e); | ||
| return; | ||
| } | ||
| }; | ||
| info!("🏠 Home Assistant close listener on port {}", port); | ||
|
|
||
| for stream in listener.incoming() { | ||
| let Ok(mut stream) = stream else { | ||
| continue; | ||
| }; | ||
| let mut buf = [0u8; 512]; | ||
| let Ok(n) = stream.read(&mut buf) else { | ||
| continue; | ||
| }; | ||
| let request = String::from_utf8_lossy(&buf[..n]); | ||
| let first_line = request.lines().next().unwrap_or(""); | ||
|
|
||
| if first_line.starts_with("POST /close-hass") { | ||
| info!("🏠 Received remote close-hass request"); | ||
| let _ = tx.send(()); | ||
| let _ = stream.write_all( | ||
| b"HTTP/1.1 200 OK\r\nAccess-Control-Allow-Origin: *\r\nContent-Length: 2\r\n\r\nOK", | ||
| ); | ||
|
Comment on lines
+118
to
+145
|
||
| } else if first_line.starts_with("OPTIONS") { | ||
| // CORS preflight | ||
| let _ = stream.write_all( | ||
| b"HTTP/1.1 204 No Content\r\nAccess-Control-Allow-Origin: *\r\nAccess-Control-Allow-Methods: POST, OPTIONS\r\nAccess-Control-Allow-Headers: Content-Type\r\n\r\n", | ||
| ); | ||
| } else { | ||
| let _ = | ||
| stream.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 9\r\n\r\nNot Found"); | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hass_api_portis added toConfig, but there are currently no usages in the codebase, so setting it indramma.tomlhas no effect. Either wire this into the Home Assistant control path (e.g., the listener) or remove it until it’s actually used to avoid misleading configuration.