All messages are JSON objects with a type field used for dispatch.
Sent immediately after WebSocket connection is established.
{
"type": "hello",
"watch_id": "watch_01",
"court_id": "court_01",
"token": "padel-secret-token-change-me",
"firmware_version": "1.0.0"
}Sent for every scoring action. event_id + seq together guarantee dedup.
{
"type": "watch_event",
"event_id": "watch_01-1700000000000-42",
"seq": 42,
"watch_id": "watch_01",
"court_id": "court_01",
"action": "point_a",
"timestamp_ms": 1700000000000,
"battery_pct": 85,
"rssi": -65
}Valid actions:
| action | meaning |
|---|---|
point_a |
Point for team/side A |
point_b |
Point for team/side B |
undo |
Undo last scoring action |
reset_game |
Reset current game to 0-0 |
reset_match |
Full match reset |
timer_start |
Start match timer |
timer_pause |
Pause match timer |
timer_toggle |
Toggle timer start/pause |
{
"type": "heartbeat",
"watch_id": "watch_01",
"timestamp_ms": 1700000000000,
"battery_pct": 85,
"rssi": -65
}{
"type": "hello_ack",
"status": "ok",
"court_id": "court_01",
"message": "Connected"
}Sent for every received watch_event.
{
"type": "ack",
"event_id": "watch_01-1700000000000-42",
"seq": 42,
"status": "applied",
"message": ""
}status values:
applied— event was processed and score updatedduplicate— event_id already seen; ignored safelyno_match— no active match on this courterror— processing error (details in message)
Pushed after every score change on the watch's court.
{
"type": "state_update",
"court_id": "court_01",
"score": {
"game_score_a": "30",
"game_score_b": "15",
"games_a": 2,
"games_b": 1,
"sets_a": 1,
"sets_b": 0,
"completed_sets": [[6,4]],
"status": "playing",
"timer_seconds": 1234,
"winner": null
}
}{
"type": "hello",
"display_id": "display_01",
"court_id": "court_01",
"token": "padel-secret-token-change-me"
}{
"type": "heartbeat",
"display_id": "display_01",
"timestamp_ms": 1700000000000
}{
"type": "hello_ack",
"status": "ok",
"court_id": "court_01"
}Full match state pushed whenever anything changes.
{
"type": "state_update",
"court_id": "court_01",
"court_name": "Court 1",
"match": {
"id": 1,
"team_a": "Alice / Bob",
"team_b": "Charlie / Dave",
"game_score_a": "30",
"game_score_b": "15",
"games_a": 2,
"games_b": 1,
"sets_a": 1,
"sets_b": 0,
"completed_sets": [[6,4]],
"status": "playing",
"timer_seconds": 1234,
"winner": null,
"advantage": null
}
}Sent when a court has no active match.
{
"type": "no_match",
"court_id": "court_01",
"court_name": "Court 1"
}Sent after connection and whenever media assignments change for this display.
{
"type": "media_sync",
"items": [
{
"id": 1,
"filename": "ad_sponsor.jpg",
"media_type": "image",
"hash": "sha256:abc123...",
"duration": 5.0,
"order": 0
},
{
"id": 2,
"filename": "promo_video.mp4",
"media_type": "video",
"hash": "sha256:def456...",
"duration": 15.0,
"order": 1
}
],
"download_base": "http://10.144.186.211:8000/api/media/files"
}The admin WebSocket is receive-only from the client side.
The server pushes admin_state updates every 2 seconds.
{
"type": "admin_state",
"courts": [
{
"court_id": "court_01",
"court_name": "Court 1",
"match_id": 1,
"team_a": "Alice / Bob",
"team_b": "Charlie / Dave",
"score_summary": "1-0 | 30-15",
"status": "playing",
"timer_seconds": 1234,
"watch_online": true,
"display_online": true,
"watch_battery": 85,
"last_event_ms": 1700000001234
}
]
}