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 <devel@cybermind.fr>
This commit is contained in:
CyberMind-FR 2026-07-22 10:36:42 +02:00
parent 4cdae8d1eb
commit 66366537df
4 changed files with 41 additions and 2 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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 <devel@cybermind.fr> 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).

View File

@ -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