feat(tv-input): gamepad->keyboard bridge daemon + controller-navigable modals
- archipelago-gamepad-keys: evdev->uinput daemon (pure python3 stdlib) that mirrors any attached controller as a virtual keyboard — D-pad/stick to arrows, A/B to Enter/Escape, X/Y to Space/f, shoulders to Tab/Shift+Tab. The browser sees real keys, so controllers work inside EVERY app iframe (IndeeHub, Jellyfin…) with zero per-app code. Ships via the ISO splice and bootstrap self-heal (kiosk nodes only), hotplug via 5s rescans. Implements docs/tv-input-iframe-apps.md. - useControllerNav: an open [role=dialog][aria-modal] owns navigation — focus is pulled inside, arrows move spatially between its controls, Enter activates, Escape stays with the modal's own close handling. One standard mapping for every modal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
402183c0ae
commit
54ddd043bd
@@ -52,6 +52,15 @@ const AUDIO_SERVICE: &str =
|
||||
const AUDIO_ROUTER_PATH: &str = "/usr/local/bin/archipelago-audio-router";
|
||||
const AUDIO_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-audio-router.service";
|
||||
|
||||
// Gamepad→keyboard bridge (TV input inside every app iframe) — same
|
||||
// splice-from-configs + self-heal pattern as the audio router.
|
||||
const GAMEPAD_KEYS: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-gamepad-keys.py");
|
||||
const GAMEPAD_SERVICE: &str =
|
||||
include_str!("../../../image-recipe/configs/archipelago-gamepad-keys.service");
|
||||
const GAMEPAD_KEYS_PATH: &str = "/usr/local/bin/archipelago-gamepad-keys";
|
||||
const GAMEPAD_SERVICE_PATH: &str = "/etc/systemd/system/archipelago-gamepad-keys.service";
|
||||
|
||||
// Journald log-volume policy (size cap + per-service rate limit). Fresh ISOs
|
||||
// write the identical file at build time (image-recipe/_archived/
|
||||
// build-auto-installer-iso.sh); this heals already-deployed nodes via OTA.
|
||||
@@ -879,6 +888,37 @@ pub async fn ensure_audio_stack() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Gamepad→keyboard bridge self-heal for kiosk nodes: keeps the evdev→uinput
|
||||
/// daemon (controller works inside every app iframe on the TV) installed and
|
||||
/// current. Same gating as audio: no kiosk → no display input to bridge.
|
||||
pub async fn ensure_gamepad_keys() {
|
||||
if fs::metadata(KIOSK_SERVICE_PATH).await.is_err() {
|
||||
return;
|
||||
}
|
||||
let unit_was_missing = fs::metadata(GAMEPAD_SERVICE_PATH).await.is_err();
|
||||
let script_changed = write_root_if_needed(GAMEPAD_KEYS_PATH, GAMEPAD_KEYS)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if script_changed {
|
||||
let _ = host_sudo(&["chmod", "+x", GAMEPAD_KEYS_PATH]).await;
|
||||
}
|
||||
let unit_changed = write_root_if_needed(GAMEPAD_SERVICE_PATH, GAMEPAD_SERVICE)
|
||||
.await
|
||||
.unwrap_or(false);
|
||||
if script_changed || unit_changed {
|
||||
if let Err(e) = host_sudo(&["systemctl", "daemon-reload"]).await {
|
||||
warn!("gamepad bridge: daemon-reload failed: {:#}", e);
|
||||
}
|
||||
}
|
||||
if unit_was_missing {
|
||||
let _ = host_sudo(&["systemctl", "enable", "--now", "archipelago-gamepad-keys.service"]).await;
|
||||
info!("gamepad: bridge installed and enabled (TV controller input)");
|
||||
} else if script_changed || unit_changed {
|
||||
let _ = host_sudo(&["systemctl", "try-restart", "archipelago-gamepad-keys.service"]).await;
|
||||
info!("gamepad: bridge updated");
|
||||
}
|
||||
}
|
||||
|
||||
/// Patch the nginx site config to add missing backend proxy blocks. Older ISO
|
||||
/// configs shipped individual per-endpoint `location` blocks, so missing
|
||||
/// endpoints silently fell through to the SPA `index.html` and the frontend
|
||||
|
||||
@@ -391,6 +391,10 @@ async fn main() -> Result<()> {
|
||||
// boot-time ELD race that leaves HDMI silently unavailable).
|
||||
tokio::spawn(bootstrap::ensure_audio_stack());
|
||||
|
||||
// TV input: gamepad→keyboard bridge so controllers work inside every app
|
||||
// iframe on kiosk nodes (docs/tv-input-iframe-apps.md).
|
||||
tokio::spawn(bootstrap::ensure_gamepad_keys());
|
||||
|
||||
// Pine voice: re-point IP-pinned Wyoming satellite entries (speakers) when
|
||||
// DHCP renumbering strands them — HA never re-resolves on its own.
|
||||
tokio::spawn(api::rpc::wyoming_satellite_keeper());
|
||||
|
||||
Reference in New Issue
Block a user