mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-10-31 01:42:04 +01:00
fix(worker): poll device message queue when notification is missed
WHAT Add MSG_POLL_INTERVAL (config.py, default 30s) and SerialWorker._poll_pending_messages(), which drains the device message queue via get_msg() until NO_MORE_MSGS, independently of the messages_waiting event. WHY meshcore 2.3.9.1 fetches messages only in response to messages_waiting. When that event is missed the radio ACKs the packet but no message event ever reaches the application, so the message is neither archived nor displayed. Node resets do not help: the trigger goes missing, not the queue. NOTES start_auto_message_fetching() stays active; the poll only picks up what the event path leaves behind. Fetched messages dispatch as ordinary message events, so dedup and archiving are unchanged. Room Server history timestamps remain a separate open issue.
This commit is contained in:
1 parent
4b6d3a2d78
commit
39e9d81161
3 files changed
+135
-1
No files matched your search
@@ -11,6 +11,67 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver
|
||||
---
|
||||
|
||||
|
||||
## [1.22.6] - 2026-08-31 — Queued messages no longer stall when the device notification is missed
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Incoming messages received by the radio never reached the
|
||||
application** (`ble/worker.py`): the companion radio received the packet
|
||||
and ACKed it, but no `CONTACT_MSG_RECV` event followed and the message
|
||||
was never archived or displayed. `MeshCore.start_auto_message_fetching()`
|
||||
(meshcore 2.3.9.1) is purely event-driven: `_fetch_messages_loop()` runs
|
||||
only when the device emits `messages_waiting`. There is no periodic
|
||||
fallback, so a missed notification leaves the message queued on the
|
||||
device indefinitely. Diagnosis confirmed the gap: `RX_LOG_DATA` showed a
|
||||
DIRECT `TEXT_MSG` arriving at -55 dBm followed by an outgoing ACK, while
|
||||
no `get_msg()` command was sent over the serial link for minutes and no
|
||||
traceback was logged — the fetch loop had never been started rather than
|
||||
having crashed. Restarting the node does not help, because the trigger,
|
||||
not the queue, is what goes missing.
|
||||
|
||||
### Added
|
||||
- `MSG_POLL_INTERVAL` in `config.py` (default 30 s, `0` disables) —
|
||||
interval for the safety-net poll of the device message queue.
|
||||
- `SerialWorker._poll_pending_messages()` — drains the queue by calling
|
||||
`get_msg()` until the device reports `NO_MORE_MSGS`, independently of
|
||||
the `messages_waiting` event.
|
||||
|
||||
### Changed
|
||||
- `_main_loop()` runs the message poll alongside the existing periodic
|
||||
tasks (contact refresh, key retry, cleanup), following the same
|
||||
interval-guarded pattern.
|
||||
|
||||
### Impact
|
||||
- Messages that the notification path misses are delayed by at most one
|
||||
poll interval instead of being lost. `start_auto_message_fetching()`
|
||||
remains active, so the event path still delivers immediately when it
|
||||
works; the poll only picks up what it leaves behind.
|
||||
- Fetched messages are dispatched by the library as ordinary message
|
||||
events, so existing handlers, deduplication and archiving apply
|
||||
unchanged — messages already delivered via the event path are not
|
||||
stored twice.
|
||||
- No change to the archive JSON schema and no change to any public
|
||||
method signature.
|
||||
|
||||
### Rationale
|
||||
- A fixed-interval poll is the smallest change that closes the gap
|
||||
without reimplementing message retrieval or forking the library. The
|
||||
interval is a config constant so it can be tuned per deployment, or
|
||||
switched off entirely once the underlying notification proves reliable.
|
||||
|
||||
### Known limitation
|
||||
- Room Server history messages still carry their **receive** time rather
|
||||
than their original send time: `EventHandler.on_contact_msg()` calls
|
||||
`Message.incoming()` without `time=` / `date=`, so a message replayed
|
||||
from server history is dated at the moment it arrives. Confirmed
|
||||
against the archive, where a message sent on 2026-08-27 is stored as
|
||||
`2026-08-31 07:10:57`. Not fixed here: whether the
|
||||
`CONTACT_MSG_RECV` payload carries a usable original timestamp cannot
|
||||
be determined until room traffic flows again, and guessing at the field
|
||||
would be a fix without a confirmed root cause.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [1.22.5] - 2026-08-31 — Failed channel discovery no longer erases the channel list
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -44,6 +44,7 @@ from meshcore_gui.config import (
|
||||
CHANNEL_DISCOVERY_ABORT_THRESHOLD,
|
||||
CONTACT_REFRESH_SECONDS,
|
||||
MAX_CHANNELS,
|
||||
MSG_POLL_INTERVAL,
|
||||
RECONNECT_BASE_DELAY,
|
||||
RECONNECT_MAX_RETRIES,
|
||||
debug_data,
|
||||
@@ -206,6 +207,7 @@ class _BaseWorker(abc.ABC):
|
||||
last_contact_refresh = time.time()
|
||||
last_key_retry = time.time()
|
||||
last_cleanup = time.time()
|
||||
last_msg_poll = time.time()
|
||||
|
||||
while self.running and not self._disconnected:
|
||||
try:
|
||||
@@ -232,6 +234,10 @@ class _BaseWorker(abc.ABC):
|
||||
await self._cleanup_old_data()
|
||||
last_cleanup = now
|
||||
|
||||
if MSG_POLL_INTERVAL > 0 and now - last_msg_poll > MSG_POLL_INTERVAL:
|
||||
await self._poll_pending_messages()
|
||||
last_msg_poll = now
|
||||
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
async def _handle_reconnect(self) -> bool:
|
||||
@@ -803,6 +809,60 @@ class _BaseWorker(abc.ABC):
|
||||
except Exception as exc:
|
||||
debug_print(f"Periodic contact refresh failed: {exc}")
|
||||
|
||||
async def _poll_pending_messages(self) -> None:
|
||||
"""Drain the device message queue as a safety net.
|
||||
|
||||
``MeshCore.start_auto_message_fetching()`` only fetches messages
|
||||
when the radio emits a ``messages_waiting`` event. If that event
|
||||
is missed the queue is never drained: the radio receives and ACKs
|
||||
the packet, but no message event reaches the application.
|
||||
|
||||
This poll calls ``get_msg()`` directly on a fixed interval and
|
||||
keeps reading until the device reports ``NO_MORE_MSGS``. Each
|
||||
fetched message is dispatched by the library as a normal
|
||||
``CONTACT_MSG_RECV`` / ``CHANNEL_MSG_RECV`` event, so the existing
|
||||
handlers and deduplication apply unchanged and messages already
|
||||
delivered via the event path are not stored twice.
|
||||
|
||||
Errors are logged and end the poll; the next interval retries.
|
||||
Disconnects are left to the main loop, which inspects command
|
||||
errors for transport keywords.
|
||||
"""
|
||||
if not self.mc:
|
||||
return
|
||||
|
||||
fetched = 0
|
||||
try:
|
||||
while self.running and not self._disconnected:
|
||||
result = await self.mc.commands.get_msg()
|
||||
|
||||
if result is None:
|
||||
debug_print("Message poll: get_msg returned None, stopping")
|
||||
break
|
||||
|
||||
if result.type == EventType.NO_MORE_MSGS:
|
||||
break
|
||||
|
||||
if result.type == EventType.ERROR:
|
||||
debug_print("Message poll: device returned ERROR, stopping")
|
||||
break
|
||||
|
||||
fetched += 1
|
||||
|
||||
# Yield between reads so the device is not overwhelmed and
|
||||
# the event loop stays responsive.
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except Exception as exc:
|
||||
debug_print(f"Message poll failed: {exc}")
|
||||
return
|
||||
|
||||
if fetched:
|
||||
debug_print(
|
||||
f"Message poll: drained {fetched} queued message(s) "
|
||||
f"that the messages_waiting event did not deliver"
|
||||
)
|
||||
|
||||
async def _cleanup_old_data(self) -> None:
|
||||
try:
|
||||
if self.shared.archive:
|
||||
|
||||
+14
-1
@@ -25,7 +25,7 @@ from typing import Any, Dict, List
|
||||
# ==============================================================================
|
||||
|
||||
|
||||
VERSION: str = "1.22.5"
|
||||
VERSION: str = "1.22.6"
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
@@ -389,6 +389,19 @@ RECONNECT_BASE_DELAY: float = 5.0
|
||||
# only present in cache are kept so offline nodes are preserved).
|
||||
CONTACT_REFRESH_SECONDS: float = 300.0 # 5 minutes
|
||||
|
||||
# Interval in seconds between safety-net polls of the device message queue.
|
||||
#
|
||||
# The meshcore library fetches messages only in response to a
|
||||
# ``messages_waiting`` event from the companion radio. When that event is
|
||||
# missed, incoming messages stay queued on the device indefinitely: the
|
||||
# radio receives and ACKs the packet, but no ``CONTACT_MSG_RECV`` /
|
||||
# ``CHANNEL_MSG_RECV`` event ever reaches the application.
|
||||
#
|
||||
# This poll drains the queue on a fixed interval regardless of the event,
|
||||
# so a missed notification delays a message instead of losing it. Set to
|
||||
# ``0`` to disable the poll and rely on the event alone.
|
||||
MSG_POLL_INTERVAL: float = 30.0
|
||||
|
||||
# ==============================================================================
|
||||
# EXTERNAL LINKS (drawer menu)
|
||||
# ==============================================================================
|
||||
|
||||
Reference in new issue
Block a user