MeshDash Docs
R2.0
/
Home Getting Started Multi-Slot / Multi-Radio Setup

Multi-Slot / Multi-Radio Setup

Getting Started multi-slot multi-radio slots node_registry slot_id primary add slot remove slot isolated database per-slot sse slot_globals_proxy
Run up to 16 radios in a single MeshDash instance — each with its own database, SSE stream, and connection config. Full NodeSlot model, slot management API, and integration patterns.

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.

Why use multiple slots? Monitor several Meshtastic nodes from one dashboard. Bridge a local radio alongside an MQTT broker to see both RF-local and internet-relayed traffic. Mix protocols: Meshtastic, MQTT, and MeshCore side by side. Separate concerns with different filters per slot.

Maximum Slots

  • MAX_SLOTS = 16 — hard limit on additional slots (beyond the primary node_0). That's 17 radios maximum.
  • Defined in core/globals.py and enforced in slot_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_HOST value, or "Primary Radio"
  • Uses the global g.meshtastic_data, g.connection_manager, g.db_manager singletons for backward compatibility
  • SSE path: /sse (legacy, same as /sse/node_0)
  • Database: meshtastic_data.db (from DB_PATH config)

NodeSlot Data Model

Each slot is represented by a NodeSlot dataclass (core/routes/schemas.py):

FieldTypeDescription
slot_idstrUnique identifier: node_0 for primary, node_N or node_XXXXXX (hex) for additional
labelstrHuman-readable name (e.g. "Rooftop Node", "MQTT EU")
meshtastic_dataMeshtasticDataPer-slot data store: nodes, packets, stats, connection state
db_managerDatabaseManagerPer-slot SQLite database manager
connection_managerMeshtasticConnectionManager | MQTTConnectionManager | MeshCoreConnectionManagerConnection driver for this slot
packet_queueasyncio.QueueInbound packet queue (maxsize=2000)
tasksSet[asyncio.Task]Running async tasks for this slot (connect_loop, packet worker)
sse_queuesDict[int, asyncio.Queue]Slot-scoped SSE client queues
sse_lockasyncio.LockLock for slot's SSE queue mutations
db_uuidstrStable 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:

  1. Calls slot.connection_manager.shutdown() with a timeout
  2. Cancels all slot tasks
  3. Closes the DB connection
  4. Removes from NODE_REGISTRY
  5. 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:

EndpointDescription
/ssePrimary slot (node_0) stream
/sse/{slot_id}Per-slot stream (e.g. /sse/node_1)
/sse/allMultiplexed 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 removednode_0 is 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/all receives 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.