MeshDash Docs
R2.0
/
Home Getting Started Connection Manager

Connection Manager

Getting Started connection manager reconnect health check strike backoff serial tcp ble sendtext is_ready slot lifecycle
How the MeshtasticConnectionManager works — health checking, reconnect logic, strike system, send locking, and all timing constants.

connection.py contains the MeshtasticConnectionManager — the core class responsible for opening, monitoring, and recovering the radio connection throughout the life of the MeshDash process. Every radio slot gets its own independent manager instance.

Timing Constants

All timing values are class-level constants and can be understood as the "personality" of the connection loop:

HEALTH_CHECK_INTERVAL
12 s — how often the connection loop probes the radio to verify it is still alive
PROBE_TIMEOUT
8 s — how long to wait for the radio to respond to a health probe before counting it as a failed strike
MAX_STRIKES
2 — number of consecutive failed probes before the manager declares the connection dead and triggers a reconnect
RECONNECT_COOLDOWN
3 s — brief pause between closing the old interface and opening the new one
BASE_BACKOFF
2 s — starting exponential backoff delay when reconnect attempts fail
MAX_BACKOFF
15 s — maximum backoff cap (with jitter applied)
BACKOFF_JITTER
±1 s — random jitter added to each backoff to prevent thundering-herd if multiple slots reconnect simultaneously
INTERFACE_INIT_TIMEOUT
25 s — maximum time allowed for the Meshtastic interface to initialise on a fresh connection attempt
DISCONNECTED_POLL_INTERVAL
5 s — how often to retry when in a disconnected state waiting to reconnect
TCP_KEEPALIVE_IDLE
10 s — TCP keepalive idle time (TCP mode only)
TCP_KEEPALIVE_INTERVAL
5 s — interval between TCP keepalive probes
TCP_KEEPALIVE_COUNT
3 — keepalive probes before TCP declares the connection dead
The SCHEDULER_CONNECT_TIMEOUT and SCHEDULER_RW_TIMEOUT values in .mesh-dash_config are passed into the connection manager at runtime and override the class-level timeouts for initial connect and read/write operations respectively.

Connection Loop Lifecycle

The connect_loop() coroutine runs indefinitely as a background asyncio task. Its state machine:

IDLE / DISCONNECTED
       │
       ▼ attempt connect
  [Interface Init]  ← timeout: INTERFACE_INIT_TIMEOUT (25s)
       │ success          │ failure
       ▼                  ▼
   CONNECTED         BACKOFF → retry
       │
       ▼ every HEALTH_CHECK_INTERVAL (12s)
  [Health Probe]  ← timeout: PROBE_TIMEOUT (8s)
       │ OK        │ failed
       ▼           ▼
   (loop)      strike++  ← MAX_STRIKES = 2
               │
               │ strikes >= MAX_STRIKES
               ▼
         RECONNECT_COOLDOWN (3s)
               │
               ▼
         [Close interface]
               │
               ▼
         DISCONNECTED → back to top

The Strike System

Health probes check that the radio is still responding. A single missed probe adds a "strike". Only after MAX_STRIKES (2) consecutive failures does the manager declare the connection dead and begin the reconnect sequence. This prevents a single slow response from causing unnecessary disconnects.

Strikes reset to 0 on a successful probe. A successful probe also resets the _last_successful_probe timestamp used for staleness detection.

is_ready Event

connection_manager.is_ready is an asyncio.Event. It is:

  • Set when the interface initialises, callbacks are registered, and the background sync completes
  • Cleared immediately when a disconnect is detected

Always check is_ready.is_set() before calling sendText() or any other radio operation. The POST /api/messages endpoint returns 503 if is_ready is not set.

sendText

await connection_manager.sendText(text, destinationId, channelIndex, wantAck)

Thread-safe via an asyncio.Lock (_send_lock). Will return None and log an error if the interface is None or is_ready is not set. The mesh packet object is returned on success — its .id is the packet ID used for ACK tracking.

Config Hot-Reload

The connection loop watches the .mesh-dash_config file's modification time (_config_mtime). If the file changes while the loop is running, the new connection parameters are loaded without restarting the process. This enables changing the serial port or TCP host via the Settings UI without a full restart for some scenarios.

PubSub Topics

The manager subscribes to and publishes Meshtastic PubSub topics:

meshtastic.receive
Fired on every incoming packet. Filtered so only the interface owned by this manager instance fires its callback — prevents cross-slot packet leakage.
meshtastic.sent
Fired after each outbound packet. Used by the frontend to flash the TX LED indicator.
meshtastic.connection.established
Triggers on_connection() → sets local node info + starts background node sync.
meshtastic.connection.lost
Triggers on_connection() → broadcasts link-down SSE event.
meshtastic.node.updated
Fires when node database changes in the Meshtastic SDK. Merges updated node data into meshState.nodes.

Multi-Slot Isolation

When multiple radio slots are active, each has its own MeshtasticConnectionManager instance with its own interface reference stored in _interface_ref[0]. The receive callback is wrapped in a filter that checks interface is iface_ref[0] before firing — this prevents a packet from slot A accidentally being processed by slot B's handler. See Slot Management.

Graceful Shutdown and Restart

When MeshDash receives a shutdown signal (SIGTERM from systemd, or POST /api/system/restart), the lifespan shutdown handler:

  1. Waits up to 5 seconds for the packet queue to drain (all buffered packets written to DB)
  2. Calls await connection_manager.shutdown() with an 8-second timeout
  3. The shutdown method closes the Meshtastic interface cleanly (un-subscribes PubSub callbacks, closes serial/TCP/BLE connection)
  4. Cancels all remaining background tasks
  5. Exits the process

The disconnect_for_restart(settle_seconds=3.0) method is used specifically before updates — it disconnects the radio and waits 3 seconds for the hardware to settle before the process is replaced. This prevents the ESP32 from being caught mid-transmission when the process exits.

WebSerial Mode

When MESHTASTIC_CONNECTION_TYPE=WEBSERIAL, the connection manager does not run a connect_loop(). Instead, is_ready is set by the POST /api/webserial/status endpoint when the browser connects. The manager's sendText() method still works — outbound messages are queued for the browser to collect from the send endpoint.