diff --git a/data/mesh_ingestor/config.py b/data/mesh_ingestor/config.py index 3d6be04..e54254c 100644 --- a/data/mesh_ingestor/config.py +++ b/data/mesh_ingestor/config.py @@ -30,6 +30,9 @@ ENERGY_SAVING = os.environ.get("ENERGY_SAVING") == "1" _RECONNECT_INITIAL_DELAY_SECS = float(os.environ.get("MESH_RECONNECT_INITIAL", "5")) _RECONNECT_MAX_DELAY_SECS = float(os.environ.get("MESH_RECONNECT_MAX", "60")) _CLOSE_TIMEOUT_SECS = float(os.environ.get("MESH_CLOSE_TIMEOUT", "5")) +_INACTIVITY_RECONNECT_SECS = float( + os.environ.get("MESH_INACTIVITY_RECONNECT_SECS", str(60 * 60)) +) _ENERGY_ONLINE_DURATION_SECS = float( os.environ.get("ENERGY_ONLINE_DURATION_SECS", "300") ) @@ -61,6 +64,7 @@ __all__ = [ "_RECONNECT_INITIAL_DELAY_SECS", "_RECONNECT_MAX_DELAY_SECS", "_CLOSE_TIMEOUT_SECS", + "_INACTIVITY_RECONNECT_SECS", "_ENERGY_ONLINE_DURATION_SECS", "_ENERGY_SLEEP_SECS", "_debug_log", diff --git a/data/mesh_ingestor/daemon.py b/data/mesh_ingestor/daemon.py index 443b91d..592c559 100644 --- a/data/mesh_ingestor/daemon.py +++ b/data/mesh_ingestor/daemon.py @@ -173,6 +173,12 @@ def main() -> None: stop = threading.Event() initial_snapshot_sent = False energy_session_deadline = None + iface_connected_at: float | None = None + last_seen_packet_monotonic = handlers.last_packet_monotonic() + last_inactivity_reconnect: float | None = None + inactivity_reconnect_secs = max( + 0.0, getattr(config, "_INACTIVITY_RECONNECT_SECS", 0.0) + ) energy_saving_enabled = config.ENERGY_SAVING energy_online_secs = max(0.0, config._ENERGY_ONLINE_DURATION_SECS) @@ -226,6 +232,12 @@ def main() -> None: energy_session_deadline = time.monotonic() + energy_online_secs else: energy_session_deadline = None + iface_connected_at = time.monotonic() + # Seed the inactivity tracking from the connection time so a + # reconnect is given a full inactivity window even when the + # handler still reports the previous packet timestamp. + last_seen_packet_monotonic = iface_connected_at + last_inactivity_reconnect = None except interfaces.NoAvailableMeshInterface as exc: print(f"[error] {exc}") _close_interface(iface) @@ -316,6 +328,68 @@ def main() -> None: ) continue + if iface is not None and inactivity_reconnect_secs > 0: + now_monotonic = time.monotonic() + iface_activity = handlers.last_packet_monotonic() + if ( + iface_activity is not None + and iface_connected_at is not None + and iface_activity < iface_connected_at + ): + iface_activity = iface_connected_at + if iface_activity is not None and ( + last_seen_packet_monotonic is None + or iface_activity > last_seen_packet_monotonic + ): + last_seen_packet_monotonic = iface_activity + last_inactivity_reconnect = None + + latest_activity = iface_activity + if latest_activity is None and iface_connected_at is not None: + latest_activity = iface_connected_at + if latest_activity is None: + latest_activity = now_monotonic + + inactivity_elapsed = now_monotonic - latest_activity + + connected_attr = getattr(iface, "isConnected", None) + believed_disconnected = False + if callable(connected_attr): + try: + believed_disconnected = not bool(connected_attr()) + except Exception: + believed_disconnected = False + elif connected_attr is not None: + believed_disconnected = not bool(connected_attr) + + should_reconnect = believed_disconnected or ( + inactivity_elapsed >= inactivity_reconnect_secs + ) + + if should_reconnect: + if ( + last_inactivity_reconnect is None + or now_monotonic - last_inactivity_reconnect + >= inactivity_reconnect_secs + ): + reason = ( + "disconnected" + if believed_disconnected + else f"no data for {inactivity_elapsed:.0f}s" + ) + print( + "[warn] mesh interface inactivity detected " + f"({reason}); reconnecting" + ) + last_inactivity_reconnect = now_monotonic + _close_interface(iface) + iface = None + announced_target = False + initial_snapshot_sent = False + energy_session_deadline = None + iface_connected_at = None + continue + retry_delay = max(0.0, config._RECONNECT_INITIAL_DELAY_SECS) stop.wait(config.SNAPSHOT_SECS) except KeyboardInterrupt: # pragma: no cover - interactive only diff --git a/data/mesh_ingestor/handlers.py b/data/mesh_ingestor/handlers.py index e157d62..115e93c 100644 --- a/data/mesh_ingestor/handlers.py +++ b/data/mesh_ingestor/handlers.py @@ -816,6 +816,22 @@ def store_packet_dict(packet: Mapping) -> None: ) +_last_packet_monotonic: float | None = None + + +def last_packet_monotonic() -> float | None: + """Return the monotonic timestamp of the most recent packet.""" + + return _last_packet_monotonic + + +def _mark_packet_seen() -> None: + """Record that a packet has been processed.""" + + global _last_packet_monotonic + _last_packet_monotonic = time.monotonic() + + def on_receive(packet, interface) -> None: """Callback registered with Meshtastic to capture incoming packets. @@ -833,6 +849,8 @@ def on_receive(packet, interface) -> None: return packet["_potatomesh_seen"] = True + _mark_packet_seen() + packet_dict = None try: packet_dict = _pkt_to_dict(packet) @@ -846,6 +864,7 @@ def on_receive(packet, interface) -> None: __all__ = [ "_queue_post_json", + "last_packet_monotonic", "on_receive", "store_neighborinfo_packet", "store_nodeinfo_packet",