Multi-Slot / Multi-Radio Setup
MeshDash's multi-slot system allows a single dashboard instance to monitor and interact with up to 16 radios simultaneously. Each "slot" represents one independent radio connection — local Meshtastic node over TCP, remote Serial node, MQTT broker bridge, or MeshCore device. Slots are fully independent: each has its own database, SSE stream, packet queue, and connection manager.
Maximum Slots
MAX_SLOTS = 16— hard limit on additional slots (beyond the primarynode_0). That's 17 radios maximum.- Defined in
core/globals.pyand enforced inslot_routes.py(POST /api/slots). - Attempting to create a slot beyond this limit returns
HTTP 400: Maximum slot limit (16) reached.
The Primary Slot (node_0)
The primary slot is always node_0. It cannot be removed. It is configured at startup from .mesh-dash_config and uses the primary database (meshtastic_data.db):
- Label defaults to the configured
MESHTASTIC_HOSTvalue, or "Primary Radio" - Uses the global
g.meshtastic_data,g.connection_manager,g.db_managersingletons for backward compatibility - SSE path:
/sse(legacy, same as/sse/node_0) - Database:
meshtastic_data.db(fromDB_PATHconfig)
NodeSlot Data Model
Each slot is represented by a NodeSlot dataclass (core/routes/schemas.py):
| Field | Type | Description |
|---|---|---|
slot_id | str | Unique identifier: node_0 for primary, node_N or node_XXXXXX (hex) for additional |
label | str | Human-readable name (e.g. "Rooftop Node", "MQTT EU") |
meshtastic_data | MeshtasticData | Per-slot data store: nodes, packets, stats, connection state |
db_manager | DatabaseManager | Per-slot SQLite database manager |
connection_manager | MeshtasticConnectionManager | MQTTConnectionManager | MeshCoreConnectionManager | Connection driver for this slot |
packet_queue | asyncio.Queue | Inbound packet queue (maxsize=2000) |
tasks | Set[asyncio.Task] | Running async tasks for this slot (connect_loop, packet worker) |
sse_queues | Dict[int, asyncio.Queue] | Slot-scoped SSE client queues |
sse_lock | asyncio.Lock | Lock for slot's SSE queue mutations |
db_uuid | str | Stable UUID for DB filename — decoupled from slot_id so deleted+recreated slots never reuse old data |
The slot.g Proxy
Each NodeSlot has a .g property that returns a _SlotGlobalsProxy. This proxy ensures that accessing slot.g.meshtastic_data, slot.g.connection_manager, etc. returns the slot's own instances, not the shared core.globals module (which always holds node_0's state). Non-slot attributes fall through to core.globals.
Slot Creation Flow
POST /api/slots
Body: { connection_type, label, ... }
1. Validate connection_type from request
2. Create DatabaseManager (unique db_uuid → separate SQLite file)
3. Create MeshtasticData bound to the new DB
4. Create the appropriate connection manager based on connection_type:
- MQTT → MQTTConnectionManager (with optional preset)
- MESHCORE → MeshCoreConnectionManager
- SERIAL/TCP/BLE → MeshtasticConnectionManager
5. Wire the slot's packet_queue into the connection manager
6. Create NodeSlot and register in NODE_REGISTRY
7. Register callbacks (receive, connection, node_updated)
8. Start background tasks:
- connection_manager.connect_loop()
- _packet_processing_worker_for_slot()
Slot Removal
DELETE /api/slots/{slot_id}?purge=true|false
Removing a slot:
- Calls
slot.connection_manager.shutdown()with a timeout - Cancels all slot tasks
- Closes the DB connection
- Removes from
NODE_REGISTRY - If
purge=true, deletes the slot's database file permanently
Primary slot (node_0) cannot be removed — only disconnected.
Database Isolation
node_0 → meshtastic_data.db (from DB_PATH config)
slot_1 → meshtastic_data_a3f2c1.db (db_uuid-derived filename)
slot_2 → meshtastic_data_7b9e4d.db (db_uuid-derived filename)
...
Each slot's database is completely independent. Node data, messages, packets, and telemetry for one radio never mix with another. Delete a slot and purge its database — no side effects on other slots. The stable UUID ensures a new slot never accidentally inherits a deleted slot's old data.
Per-Slot SSE Streams
Each slot has its own SSE stream endpoints:
| Endpoint | Description |
|---|---|
/sse | Primary slot (node_0) stream |
/sse/{slot_id} | Per-slot stream (e.g. /sse/node_1) |
/sse/all | Multiplexed stream from all slots. Each node stamped with heard_by_slot. |
The frontend switches slots via the slot selector in the topbar. Switching updates the SSE path and reconnects. The dashboard UI gives a single pane of glass over all slots.
Multi-Slot in Plugin Code
Enumerating Slots
registry = context["node_registry"]
for slot_id, slot in registry.items():
logger.info("Slot %s: %d nodes, ready=%s",
slot_id, len(slot.meshtastic_data.nodes),
slot.connection_manager.is_ready.is_set())
Per-Slot DB Query
slot = registry.get("node_1")
if slot:
msgs = await asyncio.to_thread(slot.db_manager.get_messages, limit=20)
Sending via a Specific Slot
if slot and slot.connection_manager.is_ready.is_set():
asyncio.run_coroutine_threadsafe(
slot.connection_manager.sendText("Via slot 1", destinationId="^all", channelIndex=0),
context["event_loop"]
)
Aggregating All Slots
all_nodes = {}
for slot_id, slot in registry.items():
for nid, node in slot.meshtastic_data.nodes.items():
all_nodes[f"{slot_id}:{nid}"] = {**node, "heard_by_slot": slot_id}
Limitations & Notes
- Maximum 16 additional slots — plus the primary
node_0. That's 17 radios maximum. - Node Config is per-slot — in "All Radios" view, Node Config is disabled. Select a specific radio to configure it.
- Channel config reads from primary — channel configuration in "All Radios" mode comes from
node_0. - Primary slot can't be removed —
node_0is permanent. You can disconnect it, but you can't delete it. - Database files are per-slot — removing a slot and purging deletes that database permanently.
- SSE fan-out —
/sse/{slot_id}provides per-slot isolation;/sse/allreceives events from every active slot.
See also: Radio Connection for connection types and slot configuration. Plugin Development for the node_registry context in plugins. Configuration Reference for MQTT and MeshCore per-slot config keys.