extends Node signal completed(id: int, ok: bool) signal status_changed(message: String) 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: process_mode = Node.PROCESS_MODE_ALWAYS 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) add_child(timer) timer.start() func _heartbeat() -> void: if heartbeat.get_http_client_status() == HTTPClient.STATUS_DISCONNECTED: heartbeat.request(URL + "/heartbeat", [], HTTPClient.METHOD_POST, "{}") func send(payload: Dictionary) -> bool: if pending.size() >= 32: status_changed.emit("GPIO output queue is full") return false serial += 1 var id := serial pending.append({"id": id, "payload": payload}) _pump.call_deferred() while true: var result: Array = await completed if result[0] == id: return result[1] return false func _pump() -> void: if busy or pending.is_empty(): return busy = true var item: Dictionary = pending[0] var request := HTTPRequest.new() request.timeout = 2.0 add_child(request) var ok := false var message := "GPIO bridge unavailable — start pi_gpio_bridge.py on this Pi" var error := request.request(URL + "/output", ["Content-Type: application/json"], HTTPClient.METHOD_POST, JSON.stringify(item.payload)) if error == OK: var response: Array = await request.request_completed var body = JSON.parse_string(response[3].get_string_from_utf8()) ok = response[0] == HTTPRequest.RESULT_SUCCESS and response[1] == 200 and body is Dictionary and body.get("ok", false) == true if body is Dictionary and body.has("error"): message = str(body.error) request.queue_free() pending.pop_front() busy = false 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) for entry in result.get("events", []): entry["age_ms"] = int(entry.get("age_ms", 999999)) + maxi(0, Time.get_ticks_msec() - feedback_received_ms) return result