extends Node signal action_activated(cabinet_id: StringName, action_id: StringName, context: Dictionary) signal action_deactivated(cabinet_id: StringName, action_id: StringName, context: Dictionary) signal status_changed(message: String) var active_cabinet: CabinetProfile var real_gpio := false var status := "GPIO simulation (use --gpio=real on the Pi)" var client: Node var generations: Dictionary = {} func _ready() -> void: real_gpio = "--gpio=real" in OS.get_cmdline_user_args() if real_gpio: client = preload("res://core/runtime/pi_gpio_client.gd").new() client.status_changed.connect(_status) add_child(client) _status("GPIO real mode — local Pi bridge required") if "--gpio-test" in OS.get_cmdline_user_args(): test_led.call_deferred() func _status(message: String) -> void: status = message print("[GPIO] ", message) status_changed.emit(message) func configure(cabinet: CabinetProfile) -> void: active_cabinet = cabinet func trigger(action_id: StringName, context: Dictionary = {}) -> void: if active_cabinet == null: _status("No active cabinet for GPIO action") return trigger_for_cabinet(active_cabinet, action_id, context) func trigger_for_cabinet(cabinet: CabinetProfile, action_id: StringName, context: Dictionary = {}) -> void: if cabinet == null or cabinet.hardware_config == null: _status("Missing cabinet hardware configuration") return var action := cabinet.hardware_config.get_action(action_id) if action == null: _status("Unknown hardware action: %s" % action_id) return _send_action(cabinet, action, context) func _send_action(cabinet: CabinetProfile, action: HardwareAction, context: Dictionary) -> void: var key := "%s:%s" % [cabinet.hardware_config.controller_id, action.gpio_pin] var generation := int(generations.get(key, 0)) + 1 generations[key] = generation var pulse := action.action_type == HardwareAction.ActionType.PULSE_OUTPUT var active := true if pulse else bool(context.get("active", true)) if real_gpio: var ok: bool = await client.send({ "controller": str(cabinet.hardware_config.controller_id), "pin": action.gpio_pin, "active_high": action.active_high, "mode": "pulse" if pulse else "set", "duration_ms": action.pulse_duration_ms, "active": active, }) if not ok: return else: print("[MOCK GPIO] %s cabinet=%s action=%s BCM=%d" % ["PULSE" if pulse else "SET", cabinet.cabinet_id, action.action_id, action.gpio_pin]) if active: action_activated.emit(cabinet.cabinet_id, action.action_id, context) else: action_deactivated.emit(cabinet.cabinet_id, action.action_id, context) if pulse: await get_tree().create_timer(action.pulse_duration_ms / 1000.0).timeout if int(generations.get(key, 0)) == generation: # Physical timing belongs to the bridge, even if the game exits mid-pulse. action_deactivated.emit(cabinet.cabinet_id, action.action_id, context) func test_led() -> void: var cabinet = load("res://data/cabinets/cabinet_01.tres").duplicate(true) var action: HardwareAction = cabinet.hardware_config.get_action(&"coin_release") action.pulse_duration_ms = 2000 _send_action(cabinet, action, {"source": "gpio_led_test"})