Adapt game clients to the unified WebSocket input stream.
Replace separate accel/tap push handling with input messages, set_input_stream, and a single set_stream on port 8081. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
440b49c889
commit
70e66f4d60
267
API_WEBSOCKET.md
267
API_WEBSOCKET.md
@ -1,15 +1,10 @@
|
|||||||
# WebSocket API
|
# 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 |
|
| 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 **input** push stream |
|
||||||
| `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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -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).
|
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).
|
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).
|
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 |
|
| Layer | Commands | Effect |
|
||||||
|-------|----------|--------|
|
|-------|----------|--------|
|
||||||
| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master |
|
| **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`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) |
|
| **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).
|
- **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`).
|
||||||
- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll.
|
- **`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:
|
Typical sequence:
|
||||||
|
|
||||||
1. `list_clients` → slave IDs
|
1. `list_clients` → slave IDs
|
||||||
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed
|
2. Per slave: `set_input_stream` and/or `set_tap_notify` as needed
|
||||||
3. `set_stream` and/or `set_tap_stream` with `"enable": true`
|
3. `set_stream` with `"enable": true`
|
||||||
4. Read push messages in a loop
|
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
|
## 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
|
```json
|
||||||
{
|
{
|
||||||
"type": "accel",
|
"type": "input",
|
||||||
"t": 1716900123456789012,
|
"t": 1716900123456789012,
|
||||||
"success": true,
|
"success": true,
|
||||||
"clients": [
|
"clients": [
|
||||||
@ -66,11 +66,14 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
|
|||||||
"x": 12,
|
"x": 12,
|
||||||
"y": -34,
|
"y": -34,
|
||||||
"z": 16384,
|
"z": 16384,
|
||||||
"age_ms": 8
|
"accel_age_ms": 8,
|
||||||
|
"tap_kind": "single",
|
||||||
|
"tap_age_ms": 3
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"client_id": 42,
|
"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 |
|
| `success` | `true` if `CACHE_STATUS` succeeded |
|
||||||
| `clients[]` | One entry per slave slot in the master cache |
|
| `clients[]` | One entry per slave slot in the master cache |
|
||||||
| `client_id` | ESP-NOW client id (same as `list_clients`) |
|
| `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) |
|
| `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):
|
**Failure** (e.g. UART busy):
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"type": "accel",
|
"type": "input",
|
||||||
"t": 1716900123456789012,
|
"t": 1716900123456789012,
|
||||||
"success": false,
|
"success": false,
|
||||||
"error": "uart busy"
|
"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.
|
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)
|
## Commands (request → response)
|
||||||
@ -159,13 +119,13 @@ Send one JSON object per message. Field `type` selects the command.
|
|||||||
"type": "hello",
|
"type": "hello",
|
||||||
"serial_port": "/dev/ttyUSB0",
|
"serial_port": "/dev/ttyUSB0",
|
||||||
"interval_ms": 16,
|
"interval_ms": 16,
|
||||||
|
"pre_fetch_ms": 2,
|
||||||
"tap_display_min_ms": 2000,
|
"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": [
|
"commands": [
|
||||||
"list_clients",
|
"list_clients",
|
||||||
"set_stream", "get_stream",
|
"set_stream", "get_stream",
|
||||||
"set_accel_stream", "get_accel_stream",
|
"set_input_stream", "get_input_stream",
|
||||||
"set_tap_stream", "get_tap_stream",
|
|
||||||
"set_tap_notify", "get_tap_notify",
|
"set_tap_notify", "get_tap_notify",
|
||||||
"set_led_ring", "get_battery"
|
"set_led_ring", "get_battery"
|
||||||
]
|
]
|
||||||
@ -191,7 +151,7 @@ Response `client_list`:
|
|||||||
"used": true,
|
"used": true,
|
||||||
"last_ping": 1234,
|
"last_ping": 1234,
|
||||||
"last_success_ping": 1200,
|
"last_success_ping": 1200,
|
||||||
"accel_stream": false,
|
"input_stream": false,
|
||||||
"tap_notify_single": false,
|
"tap_notify_single": false,
|
||||||
"tap_notify_double": false,
|
"tap_notify_double": false,
|
||||||
"tap_notify_triple": 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
|
```json
|
||||||
{"type":"set_stream","enable":true,"interval_ms":32}
|
{"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2}
|
||||||
{"type":"get_stream"}
|
{"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`:
|
Response `stream_status`:
|
||||||
|
|
||||||
```json
|
```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
|
```json
|
||||||
{"type":"set_accel_stream","client_id":16,"enable":true}
|
{"type":"set_input_stream","client_id":16,"enable":true}
|
||||||
{"type":"get_accel_stream","client_id":16}
|
{"type":"get_input_stream","client_id":16}
|
||||||
```
|
```
|
||||||
|
|
||||||
Response `accel_stream_status`:
|
Response `input_stream_status`:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
|
{"type":"input_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}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
|
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
|
||||||
@ -266,83 +219,55 @@ Response `tap_notify_status`:
|
|||||||
|
|
||||||
### `set_led_ring`
|
### `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`
|
### `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).
|
```json
|
||||||
|
{"type":"get_battery","all_clients":true}
|
||||||
---
|
{"type":"get_battery","client_id":16}
|
||||||
|
|
||||||
## 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())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Tap stream
|
Default if omitted: all clients.
|
||||||
|
|
||||||
```python
|
Response `battery_status`:
|
||||||
import asyncio, json, websockets
|
|
||||||
|
|
||||||
async def main():
|
```json
|
||||||
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
|
{
|
||||||
print(await ws.recv()) # hello
|
"type": "battery_status",
|
||||||
await ws.send(json.dumps({
|
"success": true,
|
||||||
"type": "set_tap_notify", "client_id": 16,
|
"samples": [
|
||||||
"single": True, "double_tap": False, "triple": False
|
{
|
||||||
}))
|
"client_id": 16,
|
||||||
await ws.recv() # tap_notify_status
|
"lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
|
||||||
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16}))
|
"lipo2": {"valid": false},
|
||||||
await ws.recv() # tap_stream_status
|
"age_ms": 1200
|
||||||
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())
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
@ -64,7 +64,7 @@ layout_mode = 2
|
|||||||
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
theme_override_colors/font_color = Color(0.75, 0.8, 0.9, 1)
|
||||||
theme_override_font_sizes/font_size = 16
|
theme_override_font_sizes/font_size = 16
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
text = "WebSocket: ws://localhost:9090/ws"
|
text = "WebSocket: ws://localhost:8081/ws"
|
||||||
|
|
||||||
[node name="ViewportLabel" type="Label" parent="UiLayer/Panel"]
|
[node name="ViewportLabel" type="Label" parent="UiLayer/Panel"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|||||||
@ -121,7 +121,7 @@ func _finish() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _show_waiting() -> 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)"
|
_hint_label.text = _instruction + "\n(Oder „Kalibrierung starten“ ohne Verbindung)"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ func _send(payload: Dictionary) -> void:
|
|||||||
func _cleanup_streams() -> void:
|
func _cleanup_streams() -> void:
|
||||||
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
if _socket.get_ready_state() != WebSocketPeer.STATE_OPEN:
|
||||||
return
|
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:
|
for cid in _pod_ids:
|
||||||
_send({
|
_send({
|
||||||
"type": "set_tap_notify",
|
"type": "set_tap_notify",
|
||||||
@ -106,8 +106,8 @@ func _handle_message(text: String) -> void:
|
|||||||
_send({"type": "list_clients"})
|
_send({"type": "list_clients"})
|
||||||
"client_list":
|
"client_list":
|
||||||
_on_client_list(data)
|
_on_client_list(data)
|
||||||
"tap":
|
"input":
|
||||||
_on_tap(data)
|
_on_input(data)
|
||||||
|
|
||||||
|
|
||||||
func _on_client_list(data: Dictionary) -> void:
|
func _on_client_list(data: Dictionary) -> void:
|
||||||
@ -155,7 +155,7 @@ func _start_round() -> void:
|
|||||||
for cid in _pod_ids:
|
for cid in _pod_ids:
|
||||||
_send_led_digit(cid, int(_pod_digits[cid]))
|
_send_led_digit(cid, int(_pod_digits[cid]))
|
||||||
|
|
||||||
_enable_tap_stream()
|
_enable_input_stream()
|
||||||
_round_start_ms = Time.get_ticks_msec()
|
_round_start_ms = Time.get_ticks_msec()
|
||||||
_set_phase(Phase.PLAYING, "%d Pods bereit" % _pod_ids.size())
|
_set_phase(Phase.PLAYING, "%d Pods bereit" % _pod_ids.size())
|
||||||
target_label.text = str(_target_digit)
|
target_label.text = str(_target_digit)
|
||||||
@ -164,7 +164,7 @@ func _start_round() -> void:
|
|||||||
hint_label.text = ""
|
hint_label.text = ""
|
||||||
|
|
||||||
|
|
||||||
func _enable_tap_stream() -> void:
|
func _enable_input_stream() -> void:
|
||||||
for cid in _pod_ids:
|
for cid in _pod_ids:
|
||||||
_send({
|
_send({
|
||||||
"type": "set_tap_notify",
|
"type": "set_tap_notify",
|
||||||
@ -173,17 +173,19 @@ func _enable_tap_stream() -> void:
|
|||||||
"double_tap": false,
|
"double_tap": false,
|
||||||
"triple": 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):
|
if _phase != Phase.PLAYING or not data.get("success", false):
|
||||||
return
|
return
|
||||||
|
|
||||||
for event in data.get("events", []):
|
for client in data.get("clients", []):
|
||||||
if typeof(event) != TYPE_DICTIONARY:
|
if typeof(client) != TYPE_DICTIONARY:
|
||||||
continue
|
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):
|
if not _pod_digits.has(cid):
|
||||||
continue
|
continue
|
||||||
if cid == _target_cid:
|
if cid == _target_cid:
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
extends Control
|
extends Control
|
||||||
|
|
||||||
const MENU_SCENE := "res://scenes/menu.tscn"
|
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 back_button: Button = $UiLayer/BackButton
|
||||||
@onready var ws_url_label: Label = $UiLayer/Panel/WSUrlLabel
|
@onready var ws_url_label: Label = $UiLayer/Panel/WSUrlLabel
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func _start_stream() -> void:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
var accel_on: bool = ui["accel_cb"].button_pressed
|
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:
|
if accel_on:
|
||||||
want_accel = true
|
want_accel = true
|
||||||
|
|
||||||
@ -97,24 +97,23 @@ func _start_stream() -> void:
|
|||||||
if tap_on:
|
if tap_on:
|
||||||
want_tap = true
|
want_tap = true
|
||||||
|
|
||||||
_send({"type": "set_stream", "enable": want_accel, "interval_ms": _interval_ms()})
|
var want_input := want_accel or want_tap
|
||||||
_send({"type": "set_tap_stream", "enable": want_tap, "interval_ms": _interval_ms()})
|
_send({"type": "set_stream", "enable": want_input, "interval_ms": _interval_ms()})
|
||||||
|
|
||||||
_streaming = true
|
_streaming = true
|
||||||
start_stream_btn.disabled = true
|
start_stream_btn.disabled = true
|
||||||
stop_stream_btn.disabled = false
|
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:
|
func _stop_stream() -> void:
|
||||||
_send({"type": "set_stream", "enable": false, "interval_ms": _interval_ms()})
|
_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:
|
for cid in _client_ui:
|
||||||
var ui: Dictionary = _client_ui[cid]
|
var ui: Dictionary = _client_ui[cid]
|
||||||
if not ui.get("available", false):
|
if not ui.get("available", false):
|
||||||
continue
|
continue
|
||||||
_send({"type": "set_accel_stream", "client_id": cid, "enable": false})
|
_send({"type": "set_input_stream", "client_id": cid, "enable": false})
|
||||||
_send({
|
_send({
|
||||||
"type": "set_tap_notify",
|
"type": "set_tap_notify",
|
||||||
"client_id": cid,
|
"client_id": cid,
|
||||||
@ -180,7 +179,7 @@ func _populate_clients(clients: Array) -> void:
|
|||||||
|
|
||||||
var accel_cb := CheckBox.new()
|
var accel_cb := CheckBox.new()
|
||||||
accel_cb.text = "Accel Stream"
|
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
|
accel_cb.disabled = not available
|
||||||
vbox.add_child(accel_cb)
|
vbox.add_child(accel_cb)
|
||||||
|
|
||||||
@ -361,7 +360,6 @@ func _populate_clients(clients: Array) -> void:
|
|||||||
"led_digit_row": led_digit_row,
|
"led_digit_row": led_digit_row,
|
||||||
"led_blink_row": led_blink_row,
|
"led_blink_row": led_blink_row,
|
||||||
"values_label": values_label,
|
"values_label": values_label,
|
||||||
"last_tap": "",
|
|
||||||
}
|
}
|
||||||
_update_led_fields(cid)
|
_update_led_fields(cid)
|
||||||
|
|
||||||
@ -406,10 +404,8 @@ func _handle_message(text: String) -> void:
|
|||||||
_on_hello(data)
|
_on_hello(data)
|
||||||
"client_list":
|
"client_list":
|
||||||
_on_client_list(data)
|
_on_client_list(data)
|
||||||
"accel":
|
"input":
|
||||||
_on_accel(data)
|
_on_input(data)
|
||||||
"tap":
|
|
||||||
_on_tap(data)
|
|
||||||
"led_ring_status":
|
"led_ring_status":
|
||||||
_log("← LED #%s: %s" % [data.get("client_id", "?"), "OK" if data.get("success", false) else data.get("error", "?")])
|
_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", []))
|
_populate_clients(data.get("clients", []))
|
||||||
|
|
||||||
|
|
||||||
func _on_accel(data: Dictionary) -> void:
|
func _on_input(data: Dictionary) -> void:
|
||||||
if not data.get("success", false):
|
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
|
return
|
||||||
|
|
||||||
var summary: PackedStringArray = []
|
var summary: PackedStringArray = []
|
||||||
@ -445,54 +441,33 @@ func _on_accel(data: Dictionary) -> void:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
var ui: Dictionary = _client_ui[cid]
|
var ui: Dictionary = _client_ui[cid]
|
||||||
var tap_part: String = ui.get("last_tap", "")
|
var lines: PackedStringArray = []
|
||||||
if client.get("valid", false):
|
if client.get("valid", false):
|
||||||
var accel_text := "x=%d y=%d z=%d (age %sms)" % [
|
lines.append(
|
||||||
|
"x=%d y=%d z=%d (age %sms)" % [
|
||||||
int(client.get("x", 0)),
|
int(client.get("x", 0)),
|
||||||
int(client.get("y", 0)),
|
int(client.get("y", 0)),
|
||||||
int(client.get("z", 0)),
|
int(client.get("z", 0)),
|
||||||
str(client.get("age_ms", "?")),
|
str(client.get("accel_age_ms", "?")),
|
||||||
]
|
]
|
||||||
ui["values_label"].text = accel_text + ("\n" + tap_part if tap_part else "")
|
)
|
||||||
summary.append("#%d: %s" % [cid, accel_text])
|
|
||||||
else:
|
|
||||||
ui["values_label"].text = "accel: —" + ("\n" + tap_part if tap_part else "")
|
|
||||||
|
|
||||||
if summary.is_empty():
|
var tap_kind := str(client.get("tap_kind", "none"))
|
||||||
stream_output.text = "accel (keine gültigen Samples)"
|
if tap_kind != "none":
|
||||||
else:
|
var tap_text := "tap: %s (age %sms)" % [tap_kind, str(client.get("tap_age_ms", "?"))]
|
||||||
stream_output.text = "accel\n" + "\n".join(summary)
|
lines.append(tap_text)
|
||||||
|
|
||||||
|
|
||||||
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)
|
_flash_client_panel(cid)
|
||||||
|
|
||||||
if not summary.is_empty():
|
if lines.is_empty():
|
||||||
stream_output.text = "tap\n" + "\n".join(summary)
|
ui["values_label"].text = "—"
|
||||||
|
else:
|
||||||
|
ui["values_label"].text = "\n".join(lines)
|
||||||
|
summary.append("#%d: %s" % [cid, " · ".join(lines)])
|
||||||
|
|
||||||
|
if summary.is_empty():
|
||||||
|
stream_output.text = "input (keine Daten)"
|
||||||
|
else:
|
||||||
|
stream_output.text = "input\n" + "\n".join(summary)
|
||||||
|
|
||||||
|
|
||||||
func _set_spinbox_enabled(spin: SpinBox, enabled: bool) -> void:
|
func _set_spinbox_enabled(spin: SpinBox, enabled: bool) -> void:
|
||||||
|
|||||||
101
scripts/pong.gd
101
scripts/pong.gd
@ -20,8 +20,11 @@ var _velocity_x := 0.0
|
|||||||
var _accel := Vector3i.ZERO
|
var _accel := Vector3i.ZERO
|
||||||
var _state := GameState.CALIBRATION
|
var _state := GameState.CALIBRATION
|
||||||
var _calibration := AccelCalibration.new()
|
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 MENU_SCENE := "res://scenes/menu.tscn"
|
||||||
const MAX_SPEED := 900.0
|
const MAX_SPEED := 900.0
|
||||||
const FRICTION := 0.9
|
const FRICTION := 0.9
|
||||||
@ -53,9 +56,14 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _return_to_menu() -> void:
|
func _return_to_menu() -> void:
|
||||||
|
_disable_input_stream()
|
||||||
get_tree().change_scene_to_file(MENU_SCENE)
|
get_tree().change_scene_to_file(MENU_SCENE)
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
_disable_input_stream()
|
||||||
|
|
||||||
|
|
||||||
func _physics_process(delta: float) -> void:
|
func _physics_process(delta: float) -> void:
|
||||||
_socket.poll()
|
_socket.poll()
|
||||||
_update_connection_status()
|
_update_connection_status()
|
||||||
@ -63,7 +71,7 @@ func _physics_process(delta: float) -> void:
|
|||||||
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
if _socket.get_ready_state() == WebSocketPeer.STATE_OPEN:
|
||||||
while _socket.get_available_packet_count() > 0:
|
while _socket.get_available_packet_count() > 0:
|
||||||
var packet := _socket.get_packet().get_string_from_utf8()
|
var packet := _socket.get_packet().get_string_from_utf8()
|
||||||
_handle_accel_message(packet)
|
_handle_message(packet)
|
||||||
|
|
||||||
if _state == GameState.CALIBRATION:
|
if _state == GameState.CALIBRATION:
|
||||||
if calibration_overlay.is_active():
|
if calibration_overlay.is_active():
|
||||||
@ -173,38 +181,79 @@ func _update_connection_status() -> void:
|
|||||||
_socket.connect_to_url(WS_URL)
|
_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)
|
var data = JSON.parse_string(text)
|
||||||
if typeof(data) != TYPE_DICTIONARY:
|
if typeof(data) != TYPE_DICTIONARY:
|
||||||
return
|
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
|
return
|
||||||
|
|
||||||
var parsed: Variant = _parse_accel_vector(data)
|
for entry in data.get("clients", []):
|
||||||
if parsed == null:
|
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
|
return
|
||||||
_accel = parsed as Vector3i
|
|
||||||
|
|
||||||
|
for client in data.get("clients", []):
|
||||||
func _parse_accel_vector(data: Dictionary) -> Variant:
|
if typeof(client) != TYPE_DICTIONARY:
|
||||||
var source: Variant = _accel_fields_dict(data)
|
continue
|
||||||
if source == null:
|
var cid := int(client.get("client_id", 0))
|
||||||
return null
|
if _client_id > 0 and cid != _client_id:
|
||||||
return Vector3i(
|
continue
|
||||||
_to_int(source.get("x")),
|
if not client.get("valid", false):
|
||||||
_to_int(source.get("y")),
|
continue
|
||||||
_to_int(source.get("z"))
|
_accel = Vector3i(
|
||||||
|
_to_int(client.get("x")),
|
||||||
|
_to_int(client.get("y")),
|
||||||
|
_to_int(client.get("z"))
|
||||||
)
|
)
|
||||||
|
return
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
func _to_int(value: Variant) -> int:
|
func _to_int(value: Variant) -> int:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user