MeshDash Docs
R2.0
/
Home API Reference SSE Events Reference

SSE Events Reference

API Reference sse events server-sent events real-time streaming connection_status packet nodes node_update stats activity traceroute plugin_update message_status EventSource
Complete reference for all 17 Server-Sent Event types — data shapes, source locations, frontend handlers, endpoints, and constants.

MeshDash uses Server-Sent Events (SSE) for real-time communication between the server and the browser. Events are pushed from the server to the client with no polling required. The frontend maintains a single EventSource connection to the active slot's SSE stream.

SSE Endpoints

EndpointDescriptionConnect-Time Events
/ssePrimary slot (node_0) stream. Legacy endpoint.connection_statusnodes (chunked at 500)
/sse/{slot_id}Per-slot stream (e.g. /sse/node_1).connection_statusstatslocal_node_infonodes (chunked)
/sse/allMultiplexed stream from all slots. Nodes stamped with heard_by_slot.connection_status → merged nodes from all slots (chunked)

Message Format

Every SSE message follows the structured event format — data is a JSON-encoded string:

{
  "event": "<event_name>",
  "data": "<json_string>"
}

Connection & Reconnection

SideBehaviour
ServerKeepalive ping every 30s of inactivity. sse-starlette also pings at 15s. Client limit: MAX_SSE_CLIENTS = 50 (HTTP 503 beyond). Dead clients removed from queue.
FrontendExponential backoff: 1s → 2s → 4s → ... → 16s max. Reconnects on visibility change, online event, or error. Closes EventSource on beforeunload.

Slot ID Stamping

When broadcast_data() is called with a slot_id other than "node_0", the data is tagged with "heard_by_slot": slot_id. Dicts containing node_id get the key injected directly; lists get it injected into each item. This lets the frontend know which radio produced each event in multi-slot mode.


Event Catalogue — All 17 Event Types

connection_status

Fires: On SSE connect (initial burst), on every connection state transition, WebSerial status changes, MeshCore/MQTT connection events.

{
  "state": "connected",       // idle|connecting|connected|reconnecting|degraded|disconnected|webserial|mqtt
  "detail": "Connected (TCP)", // Human-readable detail
  "transport": "TCP",          // Active transport
  "label": "Connected (TCP)"   // Backward-compatible field
}
Source files
core/data.py:611, core/routes/slot_routes.py (all 3 endpoints), core/webserial.py
SSE endpoints
All three
Frontend handler
_sseHandlers.connectionStatus — updates meshState.connectionStatus + meshState.connectionState

local_node_info

Fires: On slot connect (initial burst for /sse/{slot_id}), when local node info is set/refreshed/cleared, MeshCore/MQTT connection discovery.

{
  "node_id": "!a1b2c3d4",
  "node_num": 2712849364,
  "hardware_model": "T_ECHO",
  "firmware_version": "2.5.7",
  "long_name": "My Node",
  "short_name": "MYN",
  "macaddr": "aa:bb:cc:dd:ee:ff",
  "role": "CLIENT",
  "region": "EU_868",
  "max_channels": 8,
  "latitude": 50.7842,
  "longitude": -1.0735,
  "altitude": 15,
  "battery_level": 87,
  "voltage": 3.98,
  "lora_region": "EU_868",
  "lora_hop_limit": 3,
  "channels_json": "[...]",
  "channel_count": 5,
  "nodedb_count": 42,
  "last_updated": 1716393600.0
}
Source files
core/data.py:501 (primary), core/data.py:346 (clear), core/packet.py:257 (refresh), core/connections/meshcore.py:373, core/connections/mqtt.py:777
SSE endpoints
/sse/{slot_id}, /sse/all; not in /sse initial burst
Frontend handler
_sseHandlers.localNodeInfo — sets meshState.local_node_id

nodes

Fires: On every SSE connect (initial burst — full node list) and after successful background sync.

[
  {
    "node_id": "!a1b2c3d4",
    "node_num": 2712849364,
    "isLocal": true,
    "longName": "My Node",
    "shortName": "MYN",
    "lastHeard": 1716393600,
    "snr": 12.5,
    "rssi": -45,
    "position": {"latitude": 50.78, "longitude": -1.07},
    "deviceMetrics": {"batteryLevel": 87},
    "environmentMetrics": {...},
    "hw_model": "T_ECHO",
    "role": "CLIENT",
    "user": {...},
    "heard_by_slot": "node_1"
  },
  ...
]

Chunking: If >500 nodes, first 500 go in the nodes event, remainder in node_batch events.

Source files
core/routes/slot_routes.py (all 3 endpoints), core/sync.py:79 (after sync)
SSE endpoints
All three
Frontend handler
_sseHandlers.nodes — full replace (single-slot) or merge (multi-slot)

node_batch

Fires: On SSE connect when node list exceeds 500 entries — subsequent chunks after initial nodes. Same format as nodes — array of node objects, max 500 per chunk.

Source files
core/routes/slot_routes.py (all 3 SSE generators)
SSE endpoints
All three
Frontend handler
_sseHandlers.nodeBatch — incremental merge, preserves heard_by_slot

node_update

Fires: Every time MeshtasticData.update_node() is called with broadcast=True — happens on virtually every received packet. Debounced at 2000ms on frontend to coalesce rapid per-packet updates.

{
  "node_id": "!a1b2c3d4",
  "lastHeard": 1716393600,
  "snr": 12.5,
  "rssi": -45,
  "source": "RF_DIRECT",
  "source_confidence": 0.95,
  "position": {"latitude": 50.78, "longitude": -1.07},
  "deviceMetrics": {"batteryLevel": 87},
  "user": {"longName": "Some Node", "shortName": "SOM"},
  "heard_by_slot": "node_1"
}
Source files
core/data.py:335
SSE endpoints
All three
Frontend handler
_sseHandlers.nodeUpdate — merges into meshState.nodes[nodeId]

packet

Fires: After _packet_processing_worker_for_slot processes every packet. Every non-"Unknown"/"Raw:*" packet gets broadcast.

{
  "id": 123456789,
  "fromId": "!a1b2c3d4",
  "toId": "^all",
  "channel": 0,
  "rxTime": 1716393600,
  "rxSnr": 12.5,
  "rxRssi": -45,
  "hopLimit": 3,
  "hopStart": 7,
  "decoded": {
    "portnum": "TEXT_MESSAGE_APP",
    "text": "Hello mesh!",
    "payload": "Hello mesh!",
    "mesh_packet_id": 123456789
  },
  "app_packet_type": "Message",
  "source": "RF_DIRECT",
  "source_confidence": 0.95,
  "slot_id": "node_1",
  "heard_by_slot": "node_1",
  "timestamp": 1716393600.0,
  "original_channel_id": 0,
  "encrypted": false
}

app_packet_type values: Message, Position, Telemetry, Node Info, Ack, Routing Error, Neighbor Info, Traceroute, Paxcounter, Detection, Range Test, Serial, Store & Forward, Waypoint, Admin, Encrypted, or Raw: <port_name>.

Source files
core/packet.py:30 — after add_packet()
SSE endpoints
All three
Frontend handler
_sseHandlers.packet — anti-duplication, prepends to meshState.packets (max 500), flashes RX LED, feeds MeshShark, comms, terminal

stats

Fires: On SSE slot connect (/sse/{slot_id} initial burst) and after every processed packet.

{
  "packets_received_session": 1234,
  "text_messages_session": 456,
  "position_updates_session": 200,
  "telemetry_reports_session": 100,
  "user_info_updates_session": 30,
  "waypoint_updates_session": 5,
  "other_packets_session": 50,
  "start_time": 1716393600.0,
  "nodes_seen_session": 42,
  "channels_seen_session": 3,
  "elapsed_time_session": 3600.5
}
Source files
core/broadcast.py:89 (legacy node_0), core/broadcast.py:93 (per-slot), core/packet.py:32 (per-packet)
SSE endpoints
All three
Frontend handler
_sseHandlers.stats — accumulate (multi-slot) or replace (single-slot)

activity

Fires: On every packet receive (on_fast_rx callback — fast-path before queue) and every packet send (on_fast_tx callback).

"RX"

or

"TX"
Source files
core/packet.py:122 (RX), core/packet.py:130 (TX)
SSE endpoints
All three
Frontend handler
_sseHandlers.activity — flashes LED indicator

sync_status

Fires: During background node database sync — start, progress, and completion notifications.

{ "is_syncing": true, "current": "Syncing 42 nodes..." }
{ "is_syncing": false, "current": "Connected" }
Source files
core/sync.py:26,39,95,111,141
SSE endpoints
All three (slot-aware)
Frontend handler
_sseHandlers.syncStatus — shows progress indicator

error

Fires: When MeshtasticData.set_error() is called with a connection/system error.

"Connection refused"
Source files
core/data.py:683
SSE endpoints
All three
Frontend handler
_sseHandlers.serverError — logs to terminal

system_update

Fires: C2 system messages — link up/down notifications, status changes.

{ "message": "✅ <b>Link Up:</b> Node !a1b2c3d4 online. HW: T-ECHO" }
Source files
core/c2.py:867
SSE endpoints
All three (broadcasts on node_0)
Frontend handler
_sseHandlers.systemUpdate — delegates to terminal

system_message

Fires: System restart notifications, version check results, update download completion.

{ "message": "🔄 System restarting..." }
Source files
core/routes/system_routes.py:317,383,399,411,423,508
SSE endpoints
All three (broadcasts on node_0)
Frontend handler
No handler — event is received but silently dropped

message_status_update

Fires: When ACK or routing error is received for a previously-sent message.

{ "mesh_packet_id": 12345, "status": "DELIVERED" }

status values: "DELIVERED" (ACK received) or "FAILED" (routing error).

Source files
core/database.py:596 — ACK/routing error matching
SSE endpoints
All three (broadcasts on node_0)
Frontend handler
⚠ Not in _sseHandlers. Registered directly by DM view (dmes.js:127) and map (map.js:1637)

traceroute_result

Fires: After a traceroute request completes or times out with a result.

{
  "route": ["!a1b2c3d4", "!e5f6g7h8"],
  "routeBack": ["!e5f6g7h8", "!a1b2c3d4"],
  "snrTowards": [12.5, 8.0],
  "snrBack": [9.0, 11.0]
}
Source files
core/routes/node_routes.py:230 — after traceroute completes
SSE endpoints
Slot-specific (slot_id=req.slot_id)
Frontend handler
_sseHandlers.tracerouteResult — delegates to traceroute app

plugin_update

Fires: Plugin toggle (start/stop), auto-recovery attempts, background worker status changes.

{ "id": "auto_reply_plugin", "status": "running" }

status values: "running", "stopped", "pending_restart", "hung".

Source files
core/routes/plugin_routes.py:295,311,328, core/plugin_manager.py:510,525, core/background.py:180,195
SSE endpoints
All three (broadcasts on node_0)
Frontend handler
_sseHandlers.pluginUpdate — dispatches CustomEvent('plugin_update_sse')

ping

Fires: Every 30s of SSE inactivity (server-side keepalive). Also sent by sse-starlette at 15s intervals.

{}
Source files
core/routes/slot_routes.py (all 3 generators), sse-starlette
SSE endpoints
All three
Frontend handler
_sseHandlers.ping — no-op, updates lastEventAt for watchdog

Frontend Handler Map

EventHandler KeyIn _sseHandlers?Also Registered Elsewhere?
pingping
connection_statusconnectionStatus
statsstats
local_node_infolocalNodeInfo
nodesnodes
node_batchnodeBatch
node_updatenodeUpdate
system_updatesystemUpdate
packetpacket
activityactivity
errorserverError
sync_statussyncStatus
traceroute_resulttracerouteResult
plugin_updatepluginUpdate
message_status_updatedmes.js:127, map.js:1637
system_messageNowhere — event is silently dropped

Constants & Limits

ConstantValueLocation
MAX_SSE_CLIENTS50core/globals.py:20
Per-client queue (slot)200core/routes/slot_routes.py
Per-client queue (all)500core/routes/slot_routes.py
Node chunk size500core/routes/slot_routes.py
Server ping interval30score/routes/slot_routes.py
sse-starlette ping15sEventSourceResponse(gen(), ping=15)
Frontend base backoff1000msapp.js
Frontend max backoff16000msapp.js
Frontend max packets500app.js _sseHandlers.packet

See also: Plugin Development for broadcasting custom SSE events. Frontend Architecture for how the SSE stream drives the UI.