diff --git a/API_WEBSOCKET.md b/API_WEBSOCKET.md index c7613e9..c263071 100644 --- a/API_WEBSOCKET.md +++ b/API_WEBSOCKET.md @@ -1,15 +1,10 @@ # WebSocket API -`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes. +`go run . -port /dev/ttyUSB0 serve` exposes the WebSocket enpoint | URL | Port (default) | Role | |-----|----------------|------| -| `ws://localhost:8080/ws` | Dashboard (`-addr`) | Server → client only: full `DashboardState` JSON (~2 s poll + live-stream accel/tap) | -| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **accel** / **tap** push streams | - -Disable the external server with `-api-addr ""`. - -CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: [`API_REST.md`](API_REST.md). +| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **input** push stream | --- @@ -19,44 +14,49 @@ CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: 1. Connect → server sends **`hello`** (receive off; lists available commands). 2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command). -3. After `set_stream` / `set_tap_stream` with `enable: true`, the server may send **`accel`** and/or **`tap`** messages **without** a prior command (push stream). +3. After `set_stream` with `enable: true`, the server may send **`input`** messages **without** a prior command (push stream). Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error). -### Two layers (accel and tap) +### Two layers (firmware vs host) | Layer | Commands | Effect | |-------|----------|--------| -| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master | -| **This connection (host)** | `set_stream`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) | +| **Firmware (ESP-NOW)** | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel samples and/or tap events to the master | +| **This connection (host)** | `set_stream` | Whether **you** receive push JSON, at what rate (`interval_ms`, 1 ms … 10 s), and how early the UART read starts (`pre_fetch`) | -- **Accel UART polling** runs only if at least one connection has `receive_accel: true` **and** at least one slave streams accel (`set_accel_stream` or dashboard). -- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll. +- **UART polling** runs only if at least one connection has `receive_input: true` (`set_stream`) **and** at least one slave streams input (`set_input_stream`) or has tap notify enabled (`set_tap_notify`). +- **`set_tap_notify` alone** configures which tap kinds the slave reports; it does **not** enable host push by itself — you still need `set_stream`. Typical sequence: 1. `list_clients` → slave IDs -2. Per slave: `set_accel_stream` / `set_tap_notify` as needed -3. `set_stream` and/or `set_tap_stream` with `"enable": true` -4. Read push messages in a loop +2. Per slave: `set_input_stream` and/or `set_tap_notify` as needed +3. `set_stream` with `"enable": true` +4. Read **`input`** messages in a loop -There is **no per-slave filter** on push messages: each `accel` contains all cached slaves; each `tap` contains all visible events. Filter by `client_id` in your app. +There is **no per-slave filter** on push messages: each `input` contains all cached slaves. Filter by `client_id` in your app. --- ## Push stream messages -These are the samples you get after enabling receive. Interval is per WebSocket connection; the server UART poll uses the **minimum** `interval_ms` among all subscribers that want accel or tap. +These are the samples you get after enabling receive. Timing is per WebSocket connection: -### `accel` (type `"accel"`) +- **`interval_ms`** — minimum time between consecutive `input` pushes on this socket. +- **`pre_fetch`** — milliseconds **before** each scheduled push when the host sends the UART cache read, so the master has time to collect data from all slaves before the JSON goes out. -Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection. +The server UART poll uses the **minimum** `interval_ms` among all subscribers with `receive_input: true`. -**Success** — all slaves with a cache entry on the master (not only those with `valid: true`): +### `input` (type `"input"`) + +Sent when `set_stream` has `enable: true` and the poll tick fires for this connection (after the UART read started `pre_fetch` ms earlier). Each message combines the latest accel cache and visible tap state for every slave slot on the master. + +**Success** — all slaves with a cache entry (not only those with `valid: true`): ```json { - "type": "accel", + "type": "input", "t": 1716900123456789012, "success": true, "clients": [ @@ -66,11 +66,14 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p "x": 12, "y": -34, "z": 16384, - "age_ms": 8 + "accel_age_ms": 8, + "tap_kind": "single", + "tap_age_ms": 3 }, { "client_id": 42, - "valid": false + "valid": false, + "tap_kind": "none" } ] } @@ -82,15 +85,19 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p | `success` | `true` if `CACHE_STATUS` succeeded | | `clients[]` | One entry per slave slot in the master cache | | `client_id` | ESP-NOW client id (same as `list_clients`) | -| `valid` | `false` if no sample yet or stale; omit `x`/`y`/`z` when false | +| `valid` | `false` if no accel sample yet or stale; omit `x`/`y`/`z` when false | | `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) | -| `age_ms` | Milliseconds since the master received this sample | +| `accel_age_ms` | Milliseconds since the master received this accel sample | +| `tap_kind` | `"none"`, `"single"`, `"double"`, or `"triple"` | +| `tap_age_ms` | Milliseconds since the tap was seen in the master cache; omit when `tap_kind` is `"none"` | + +Tap events stay visible for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw them, even if the hardware age grows. **Failure** (e.g. UART busy): ```json { - "type": "accel", + "type": "input", "t": 1716900123456789012, "success": false, "error": "uart busy" @@ -99,53 +106,6 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p No `clients` array on failure. -### `tap` (type `"tap"`) - -Sent only when `set_tap_stream` has `enable: true` and there is at least one event to show. - -Events appear when the master cache reports a new tap. Each event stays in push payloads for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw it, even if the hardware age grows. - -**Success**: - -```json -{ - "type": "tap", - "t": 1716900123456789012, - "success": true, - "events": [ - { - "client_id": 16, - "valid": true, - "kind": "single", - "age_ms": 3, - "shown_at_ms": 1717000000123 - } - ] -} -``` - -| Field | Meaning | -|-------|---------| -| `t` | Unix timestamp in **nanoseconds** (poll time) | -| `events[]` | All taps currently “on screen” for the API | -| `client_id` | Slave that tapped | -| `kind` | `"single"`, `"double"`, or `"triple"` | -| `age_ms` | Age in the master cache when read | -| `shown_at_ms` | Unix **milliseconds** when this host first included the event | - -If no events are visible, **no** `tap` message is sent on that tick (unlike accel, which can send empty `clients` only on success with cache data). - -**Failure**: - -```json -{ - "type": "tap", - "t": 1716900123456789012, - "success": false, - "error": "uart busy" -} -``` - --- ## Commands (request → response) @@ -159,13 +119,13 @@ Send one JSON object per message. Field `type` selects the command. "type": "hello", "serial_port": "/dev/ttyUSB0", "interval_ms": 16, + "pre_fetch_ms": 2, "tap_display_min_ms": 2000, - "note": "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push", + "note": "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection", "commands": [ "list_clients", "set_stream", "get_stream", - "set_accel_stream", "get_accel_stream", - "set_tap_stream", "get_tap_stream", + "set_input_stream", "get_input_stream", "set_tap_notify", "get_tap_notify", "set_led_ring", "get_battery" ] @@ -191,7 +151,7 @@ Response `client_list`: "used": true, "last_ping": 1234, "last_success_ping": 1200, - "accel_stream": false, + "input_stream": false, "tap_notify_single": false, "tap_notify_double": false, "tap_notify_triple": false @@ -200,45 +160,38 @@ Response `client_list`: } ``` -### `set_stream` / `get_stream` (receive accel on this connection) +### `set_stream` / `get_stream` (receive input on this connection) ```json -{"type":"set_stream","enable":true,"interval_ms":32} +{"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2} {"type":"get_stream"} ``` +| Field | Meaning | +|-------|---------| +| `enable` | Turn push stream on/off for this connection | +| `interval_ms` | Minimum time between `input` pushes (1 … 10000) | +| `pre_fetch` | Milliseconds before each push when the host starts the UART cache read; optional, default in `hello` (`pre_fetch_ms`) | + Response `stream_status`: ```json -{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true} +{"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true} ``` -### `set_accel_stream` / `get_accel_stream` (firmware, per slave) +### `set_input_stream` / `get_input_stream` (firmware, per slave) -`client_id` required (> 0). +`client_id` required (> 0). Enables accel streaming from the slave to the master. ```json -{"type":"set_accel_stream","client_id":16,"enable":true} -{"type":"get_accel_stream","client_id":16} +{"type":"set_input_stream","client_id":16,"enable":true} +{"type":"get_input_stream","client_id":16} ``` -Response `accel_stream_status`: +Response `input_stream_status`: ```json -{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true} -``` - -### `set_tap_stream` / `get_tap_stream` (receive tap on this connection) - -```json -{"type":"set_tap_stream","enable":true,"interval_ms":16} -{"type":"get_tap_stream"} -``` - -Response `tap_stream_status`: - -```json -{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true} +{"type":"input_stream_status","client_id":16,"enabled":true,"success":true} ``` ### `set_tap_notify` / `get_tap_notify` (firmware, per slave) @@ -266,83 +219,55 @@ Response `tap_notify_status`: ### `set_led_ring` -Same JSON body as [`POST /api/led-ring`](API_REST.md#led-ring) with `"type":"set_led_ring"` added. Reply: `led_ring_status`. +Control the LED ring on the master or a slave. + +```json +{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128} +{"type":"set_led_ring","mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0} +{"type":"set_led_ring","mode":"find-me","all_clients":true,"slaves_only":true} +``` + +| `mode` | Notes | +|--------|--------| +| `clear` | Turn off | +| `color` | Full ring RGB + `intensity` | +| `progress` | `progress` 0–100 | +| `digit` | `digit` 0–10 | +| `blink` | `blink_ms`, `blink_count` | +| `find-me` | Locate pod | + +Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast. + +Response `led_ring_status`: + +```json +{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1} +``` ### `get_battery` -Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients. +Read cached battery samples from the master. Slaves push battery every **30 s**; this command reads the master cache. -Reply: `battery_status` with `samples[]` (see REST doc). - ---- - -## Examples - -### Accel stream - -```python -import asyncio, json, websockets - -async def main(): - async with websockets.connect("ws://127.0.0.1:8081/ws") as ws: - print(await ws.recv()) # hello - await ws.send(json.dumps({"type": "list_clients"})) - clients = json.loads(await ws.recv())["clients"] - for c in clients: - if not c.get("available"): - continue - await ws.send(json.dumps({ - "type": "set_accel_stream", "client_id": c["id"], "enable": True - })) - await ws.recv() # accel_stream_status - await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16})) - await ws.recv() # stream_status - while True: - msg = json.loads(await ws.recv()) - if msg.get("type") != "accel": - continue - if not msg.get("success"): - print("error:", msg.get("error")) - continue - for c in msg.get("clients", []): - if c.get("valid"): - print(c["client_id"], c["x"], c["y"], c["z"], "age", c.get("age_ms")) - -asyncio.run(main()) +```json +{"type":"get_battery","all_clients":true} +{"type":"get_battery","client_id":16} ``` -### Tap stream +Default if omitted: all clients. -```python -import asyncio, json, websockets +Response `battery_status`: -async def main(): - async with websockets.connect("ws://127.0.0.1:8081/ws") as ws: - print(await ws.recv()) # hello - await ws.send(json.dumps({ - "type": "set_tap_notify", "client_id": 16, - "single": True, "double_tap": False, "triple": False - })) - await ws.recv() # tap_notify_status - await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16})) - await ws.recv() # tap_stream_status - while True: - msg = json.loads(await ws.recv()) - if msg.get("type") == "tap" and msg.get("events"): - for e in msg["events"]: - print(e["client_id"], e["kind"], "age", e.get("age_ms")) - -asyncio.run(main()) +```json +{ + "type": "battery_status", + "success": true, + "samples": [ + { + "client_id": 16, + "lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71}, + "lipo2": {"valid": false}, + "age_ms": 1200 + } + ] +} ``` - ---- - -## Dashboard WebSocket (`:8080/ws`) - -Read-only from the browser’s perspective: the server pushes JSON whenever state changes. Clients do not send commands on this socket (messages are ignored). - -Payload shape: `DashboardState` — `updated_at`, `serial_port`, `uart_connected`, `live_stream`, `master`, `clients[]` (id, mac, accel, tap notify flags, battery, etc.). Accel/tap samples appear here when **Live stream** is enabled in the UI (`PUT /api/live-stream`). - -During OTA, additional messages with `"type":"ota_progress"` may appear on the same socket. - -Configure slaves via REST on `:8080` ([`API_REST.md`](API_REST.md)), not via this WebSocket. diff --git a/scenes/config.tscn b/scenes/config.tscn index 105a519..c0d0799 100644 --- a/scenes/config.tscn +++ b/scenes/config.tscn @@ -64,7 +64,7 @@ layout_mode = 2 theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1) theme_override_font_sizes/font_size = 16 horizontal_alignment = 1 -text = "WebSocket: ws://localhost:9090/ws" +text = "WebSocket: ws://localhost:8081/ws" [node name="ViewportLabel" type="Label" parent="UiLayer/Panel"] layout_mode = 2 diff --git a/scripts/calibration_overlay.gd b/scripts/calibration_overlay.gd index 1d4c467..fa03fe4 100644 --- a/scripts/calibration_overlay.gd +++ b/scripts/calibration_overlay.gd @@ -121,7 +121,7 @@ func _finish() -> void: func _show_waiting() -> void: - _instruction = "Warte auf Verbindung zu localhost:9090…" + _instruction = "Warte auf Verbindung zu localhost:8081…" _hint_label.text = _instruction + "\n(Oder „Kalibrierung starten“ ohne Verbindung)" diff --git a/scripts/color_game.gd b/scripts/color_game.gd index 499acd3..fffe2ef 100644 --- a/scripts/color_game.gd +++ b/scripts/color_game.gd @@ -62,7 +62,7 @@ func _send(payload: Dictionary) -> void: func _cleanup_streams() -> void: if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN: return - _send({"type": "set_tap_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS}) + _send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS}) for cid in _pod_ids: _send({ "type": "set_tap_notify", @@ -106,8 +106,8 @@ func _handle_message(text: String) -> void: _send({"type": "list_clients"}) "client_list": _on_client_list(data) - "tap": - _on_tap(data) + "input": + _on_input(data) func _on_client_list(data: Dictionary) -> void: @@ -155,7 +155,7 @@ func _start_round() -> void: for cid in _pod_ids: _send_led_digit(cid, int(_pod_digits[cid])) - _enable_tap_stream() + _enable_input_stream() _round_start_ms = Time.get_ticks_msec() _set_phase(Phase.PLAYING, "%d Pods bereit" % _pod_ids.size()) target_label.text = str(_target_digit) @@ -164,7 +164,7 @@ func _start_round() -> void: hint_label.text = "" -func _enable_tap_stream() -> void: +func _enable_input_stream() -> void: for cid in _pod_ids: _send({ "type": "set_tap_notify", @@ -173,17 +173,19 @@ func _enable_tap_stream() -> void: "double_tap": false, "triple": false, }) - _send({"type": "set_tap_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS}) + _send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS}) -func _on_tap(data: Dictionary) -> void: +func _on_input(data: Dictionary) -> void: if _phase != Phase.PLAYING or not data.get("success", false): return - for event in data.get("events", []): - if typeof(event) != TYPE_DICTIONARY: + for client in data.get("clients", []): + if typeof(client) != TYPE_DICTIONARY: continue - var cid := int(event.get("client_id", 0)) + if str(client.get("tap_kind", "none")) == "none": + continue + var cid := int(client.get("client_id", 0)) if not _pod_digits.has(cid): continue if cid == _target_cid: diff --git a/scripts/config.gd b/scripts/config.gd index 32d6c43..48b8b3e 100644 --- a/scripts/config.gd +++ b/scripts/config.gd @@ -1,7 +1,7 @@ extends Control const MENU_SCENE := "res://scenes/menu.tscn" -const WS_URL := "ws://localhost:9090/ws" +const WS_URL := "ws://localhost:8081/ws" @onready var back_button: Button = $UiLayer/BackButton @onready var ws_url_label: Label = $UiLayer/Panel/WSUrlLabel diff --git a/scripts/demo.gd b/scripts/demo.gd index 56b8b3b..ff0946f 100644 --- a/scripts/demo.gd +++ b/scripts/demo.gd @@ -79,7 +79,7 @@ func _start_stream() -> void: continue var accel_on: bool = ui["accel_cb"].button_pressed - _send({"type": "set_accel_stream", "client_id": cid, "enable": accel_on}) + _send({"type": "set_input_stream", "client_id": cid, "enable": accel_on}) if accel_on: want_accel = true @@ -97,24 +97,23 @@ func _start_stream() -> void: if tap_on: want_tap = true - _send({"type": "set_stream", "enable": want_accel, "interval_ms": _interval_ms()}) - _send({"type": "set_tap_stream", "enable": want_tap, "interval_ms": _interval_ms()}) + var want_input := want_accel or want_tap + _send({"type": "set_stream", "enable": want_input, "interval_ms": _interval_ms()}) _streaming = true start_stream_btn.disabled = true stop_stream_btn.disabled = false - _log("Stream gestartet (accel=%s, tap=%s)" % [want_accel, want_tap]) + _log("Stream gestartet (input=%s, accel=%s, tap=%s)" % [want_input, want_accel, want_tap]) func _stop_stream() -> void: _send({"type": "set_stream", "enable": false, "interval_ms": _interval_ms()}) - _send({"type": "set_tap_stream", "enable": false, "interval_ms": _interval_ms()}) for cid in _client_ui: var ui: Dictionary = _client_ui[cid] if not ui.get("available", false): continue - _send({"type": "set_accel_stream", "client_id": cid, "enable": false}) + _send({"type": "set_input_stream", "client_id": cid, "enable": false}) _send({ "type": "set_tap_notify", "client_id": cid, @@ -180,7 +179,7 @@ func _populate_clients(clients: Array) -> void: var accel_cb := CheckBox.new() accel_cb.text = "Accel Stream" - accel_cb.button_pressed = entry.get("accel_stream", false) + accel_cb.button_pressed = entry.get("input_stream", false) accel_cb.disabled = not available vbox.add_child(accel_cb) @@ -361,7 +360,6 @@ func _populate_clients(clients: Array) -> void: "led_digit_row": led_digit_row, "led_blink_row": led_blink_row, "values_label": values_label, - "last_tap": "", } _update_led_fields(cid) @@ -406,10 +404,8 @@ func _handle_message(text: String) -> void: _on_hello(data) "client_list": _on_client_list(data) - "accel": - _on_accel(data) - "tap": - _on_tap(data) + "input": + _on_input(data) "led_ring_status": _log("← LED #%s: %s" % [data.get("client_id", "?"), "OK" if data.get("success", false) else data.get("error", "?")]) _: @@ -431,9 +427,9 @@ func _on_client_list(data: Dictionary) -> void: _populate_clients(data.get("clients", [])) -func _on_accel(data: Dictionary) -> void: +func _on_input(data: Dictionary) -> void: if not data.get("success", false): - stream_output.text = "accel FEHLER: %s" % data.get("error", "?") + stream_output.text = "input FEHLER: %s" % data.get("error", "?") return var summary: PackedStringArray = [] @@ -445,54 +441,33 @@ func _on_accel(data: Dictionary) -> void: continue var ui: Dictionary = _client_ui[cid] - var tap_part: String = ui.get("last_tap", "") + var lines: PackedStringArray = [] if client.get("valid", false): - var accel_text := "x=%d y=%d z=%d (age %sms)" % [ - int(client.get("x", 0)), - int(client.get("y", 0)), - int(client.get("z", 0)), - str(client.get("age_ms", "?")), - ] - ui["values_label"].text = accel_text + ("\n" + tap_part if tap_part else "") - summary.append("#%d: %s" % [cid, accel_text]) + lines.append( + "x=%d y=%d z=%d (age %sms)" % [ + int(client.get("x", 0)), + int(client.get("y", 0)), + int(client.get("z", 0)), + str(client.get("accel_age_ms", "?")), + ] + ) + + var tap_kind := str(client.get("tap_kind", "none")) + if tap_kind != "none": + var tap_text := "tap: %s (age %sms)" % [tap_kind, str(client.get("tap_age_ms", "?"))] + lines.append(tap_text) + _flash_client_panel(cid) + + if lines.is_empty(): + ui["values_label"].text = "—" else: - ui["values_label"].text = "accel: —" + ("\n" + tap_part if tap_part else "") + ui["values_label"].text = "\n".join(lines) + summary.append("#%d: %s" % [cid, " · ".join(lines)]) if summary.is_empty(): - stream_output.text = "accel (keine gültigen Samples)" + stream_output.text = "input (keine Daten)" else: - stream_output.text = "accel\n" + "\n".join(summary) - - -func _on_tap(data: Dictionary) -> void: - if not data.get("success", false): - return - - var summary: PackedStringArray = [] - for event in data.get("events", []): - if typeof(event) != TYPE_DICTIONARY: - continue - var cid := int(event.get("client_id", 0)) - if not _client_ui.has(cid): - continue - - var tap_text := "tap: %s (age %sms)" % [str(event.get("kind", "?")), str(event.get("age_ms", "?"))] - var ui: Dictionary = _client_ui[cid] - ui["last_tap"] = tap_text - - var existing: String = ui["values_label"].text - if existing == "—" or existing.begins_with("accel: —"): - ui["values_label"].text = tap_text - elif "\n" in existing: - ui["values_label"].text = existing.split("\n", false, 1)[0] + "\n" + tap_text - else: - ui["values_label"].text = existing + "\n" + tap_text - - summary.append("#%d: %s" % [cid, tap_text]) - _flash_client_panel(cid) - - if not summary.is_empty(): - stream_output.text = "tap\n" + "\n".join(summary) + stream_output.text = "input\n" + "\n".join(summary) func _set_spinbox_enabled(spin: SpinBox, enabled: bool) -> void: diff --git a/scripts/pong.gd b/scripts/pong.gd index aa45191..217dc94 100644 --- a/scripts/pong.gd +++ b/scripts/pong.gd @@ -20,8 +20,11 @@ var _velocity_x := 0.0 var _accel := Vector3i.ZERO var _state := GameState.CALIBRATION var _calibration := AccelCalibration.new() +var _client_id := 0 +var _stream_ready := false -const WS_URL := "ws://localhost:9090/ws" +const WS_URL := "ws://localhost:8081/ws" +const STREAM_INTERVAL_MS := 16 const MENU_SCENE := "res://scenes/menu.tscn" const MAX_SPEED := 900.0 const FRICTION := 0.9 @@ -53,9 +56,14 @@ func _ready() -> void: func _return_to_menu() -> void: + _disable_input_stream() get_tree().change_scene_to_file(MENU_SCENE) +func _exit_tree() -> void: + _disable_input_stream() + + func _physics_process(delta: float) -> void: _socket.poll() _update_connection_status() @@ -63,7 +71,7 @@ func _physics_process(delta: float) -> void: if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN: while _socket.get_available_packet_count() > 0: var packet := _socket.get_packet().get_string_from_utf8() - _handle_accel_message(packet) + _handle_message(packet) if _state == GameState.CALIBRATION: if calibration_overlay.is_active(): @@ -173,38 +181,79 @@ func _update_connection_status() -> void: _socket.connect_to_url(WS_URL) -func _handle_accel_message(text: String) -> void: +func _send(payload: Dictionary) -> void: + if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN: + return + _socket.send_text(JSON.stringify(payload)) + + +func _disable_input_stream() -> void: + if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN: + return + _send({"type": "set_stream", "enable": false, "interval_ms": STREAM_INTERVAL_MS}) + if _client_id > 0: + _send({"type": "set_input_stream", "client_id": _client_id, "enable": false}) + _stream_ready = false + + +func _enable_input_stream() -> void: + if _client_id <= 0: + return + _send({"type": "set_input_stream", "client_id": _client_id, "enable": true}) + _send({"type": "set_stream", "enable": true, "interval_ms": STREAM_INTERVAL_MS}) + _stream_ready = true + + +func _handle_message(text: String) -> void: var data = JSON.parse_string(text) if typeof(data) != TYPE_DICTIONARY: return - if data.get("type") != "accel" or not data.get("success", false): + + match data.get("type", ""): + "hello": + _send({"type": "list_clients"}) + "client_list": + _on_client_list(data) + "input": + _on_input(data) + + +func _on_client_list(data: Dictionary) -> void: + if not data.get("success", false): return - var parsed: Variant = _parse_accel_vector(data) - if parsed == null: + for entry in data.get("clients", []): + if typeof(entry) != TYPE_DICTIONARY: + continue + if not entry.get("available", false): + continue + var cid := int(entry.get("id", 0)) + if cid > 0: + _client_id = cid + break + + if _client_id > 0 and not _stream_ready: + _enable_input_stream() + + +func _on_input(data: Dictionary) -> void: + if not data.get("success", false): return - _accel = parsed as Vector3i - -func _parse_accel_vector(data: Dictionary) -> Variant: - var source: Variant = _accel_fields_dict(data) - if source == null: - return null - return Vector3i( - _to_int(source.get("x")), - _to_int(source.get("y")), - _to_int(source.get("z")) - ) - - -func _accel_fields_dict(data: Dictionary) -> Variant: - if data.has("x") and data.has("y") and data.has("z"): - return data - for key in ["accel", "values", "data", "payload"]: - var nested: Variant = data.get(key) - if nested is Dictionary and nested.has("x") and nested.has("y") and nested.has("z"): - return nested - return null + for client in data.get("clients", []): + if typeof(client) != TYPE_DICTIONARY: + continue + var cid := int(client.get("client_id", 0)) + if _client_id > 0 and cid != _client_id: + continue + if not client.get("valid", false): + continue + _accel = Vector3i( + _to_int(client.get("x")), + _to_int(client.get("y")), + _to_int(client.get("z")) + ) + return func _to_int(value: Variant) -> int: