From 66366537df103f2fd95d5b15a5d5b2badb51d36f Mon Sep 17 00:00:00 2001 From: CyberMind-FR Date: Wed, 22 Jul 2026 10:36:42 +0200 Subject: [PATCH] fix(meshtastic): daemon survives an unreachable MQTT broker at startup (ref #897) Deploy found a crash-loop: the shipped default config has [shared_grid] set, but the opt-in mosquitto is not running, so Bridge.start()/the paho adapter's synchronous connect() raised ConnectionRefusedError and killed the daemon. Bridge.start() now skips an unreachable broker (log + continue); the paho adapter uses connect_async + loop_start (non-blocking, auto-reconnect). +regression test. Co-Authored-By: Gerald KERMA --- packages/secubox-meshtastic/api/bridge.py | 13 ++++++++++++- packages/secubox-meshtastic/api/daemon.py | 5 ++++- packages/secubox-meshtastic/debian/changelog | 11 +++++++++++ packages/secubox-meshtastic/tests/test_bridge.py | 14 ++++++++++++++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/secubox-meshtastic/api/bridge.py b/packages/secubox-meshtastic/api/bridge.py index b03e67e1..6bff9d7c 100644 --- a/packages/secubox-meshtastic/api/bridge.py +++ b/packages/secubox-meshtastic/api/bridge.py @@ -3,11 +3,14 @@ """SecuBox-Deb :: meshtastic — host-side serial↔MQTT bridge (grid-policy driven).""" from __future__ import annotations import json +import logging from typing import Callable from .config import Config from .gridpolicy import targets_for from .model import Packet +_log = logging.getLogger("secubox.meshtastic.bridge") + class Bridge: def __init__(self, cfg: Config, mqtt_factory: Callable[[str], object]) -> None: @@ -25,7 +28,15 @@ class Bridge: except ValueError: port_num = 1883 cli = self._factory(tgt) - cli.connect(host, port_num) + try: + cli.connect(host, port_num) + except Exception as exc: + # A configured broker that is DOWN at startup (e.g. the opt-in + # private mosquitto not yet running) must NOT crash the daemon — + # skip this target; it can be reconnected when the broker is up. + _log.warning("bridge: %s broker %s:%s unreachable at start (%s); skipping", + tgt, host, port_num, exc) + continue self._clients[tgt] = cli def publish(self, channel_name: str, p: Packet) -> None: diff --git a/packages/secubox-meshtastic/api/daemon.py b/packages/secubox-meshtastic/api/daemon.py index 4d2a60e3..229114f0 100644 --- a/packages/secubox-meshtastic/api/daemon.py +++ b/packages/secubox-meshtastic/api/daemon.py @@ -70,7 +70,10 @@ class _PahoAdapter: self._client = client def connect(self, host: str, port: int) -> None: - self._client.connect(host, port) + # connect_async (not connect): a broker that is DOWN at startup must not + # raise/crash the daemon — loop_start's background thread establishes and + # auto-reconnects the connection when the broker becomes reachable. + self._client.connect_async(host, port) self._client.loop_start() def publish(self, topic: str, payload: str) -> None: diff --git a/packages/secubox-meshtastic/debian/changelog b/packages/secubox-meshtastic/debian/changelog index 5c5b2372..c5048dac 100644 --- a/packages/secubox-meshtastic/debian/changelog +++ b/packages/secubox-meshtastic/debian/changelog @@ -1,3 +1,14 @@ +secubox-meshtastic (0.1.1-1~bookworm1) bookworm; urgency=medium + + * Fix daemon crash-loop when a configured MQTT broker is down at startup + (deploy #897): the opt-in private mosquitto is not running by default, and + Bridge.start() / the paho adapter's synchronous connect() raised + ConnectionRefusedError → the daemon died. Bridge.start() now skips an + unreachable broker (logs + continues), and the paho adapter uses + connect_async + loop_start (non-blocking, auto-reconnect). + + -- Gerald KERMA Tue, 22 Jul 2026 12:30:00 +0200 + secubox-meshtastic (0.1.0-1~bookworm1) bookworm; urgency=medium * Initial release: multi-grid Meshtastic LoRa node integration (ref #897). diff --git a/packages/secubox-meshtastic/tests/test_bridge.py b/packages/secubox-meshtastic/tests/test_bridge.py index 065e71d2..5f96a26c 100644 --- a/packages/secubox-meshtastic/tests/test_bridge.py +++ b/packages/secubox-meshtastic/tests/test_bridge.py @@ -36,3 +36,17 @@ def test_on_channel_publishes_only_when_enabled(): made={} b=Bridge(_cfg(["off","on"], on=True), lambda k: made.setdefault(k, FakeMqtt())); b.start(); b.publish("fam", _p()) assert "on" in made and made["on"].conn==("mqtt.x.org",8883) + + +class RefusingMqtt(FakeMqtt): + def connect(self, host, port): + raise ConnectionRefusedError("broker down") + + +def test_start_tolerates_unreachable_broker(): + # A configured broker that is DOWN at startup must not crash Bridge.start() + # (the daemon's #897 deploy bug: opt-in mosquitto not running). + from api.bridge import Bridge + b = Bridge(_cfg(["off", "shared"]), lambda k: RefusingMqtt()) + b.start() # must not raise + b.publish("fam", _p()) # no client registered → no-op, no crash