From dbecf7ac2456cc4c0739d7b160fa366abf2c259a Mon Sep 17 00:00:00 2001 From: pe1hvh Date: Thu, 12 Mar 2026 16:23:56 +0100 Subject: [PATCH] HotFixRoomServer --- CHANGELOG.md | 57 +++++++++++++++++++++++---------- meshcore_gui/ble/commands.py | 61 +++++++++++++++++++++++++++++++++++- meshcore_gui/ble/events.py | 33 ++++++++++++------- meshcore_gui/ble/worker.py | 11 +------ 4 files changed, 122 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e79ede2..da9ed5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,26 +1,29 @@ # CHANGELOG + + All notable changes to MeshCore GUI are documented in this file. Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/). --- -## [1.13.4] - 2026-03-12 — Room Server Login & Receive Reliability +## [1.13.4] - 2026-03-12 — Room Server USB Login & Fetch Fix ### Changed -- 🔄 `meshcore_gui/ble/commands.py` — Room login success now refreshes archived room history immediately after `LOGIN_SUCCESS`, so the room panel is populated deterministically right after a successful login -- 🔄 `meshcore_gui/ble/events.py` — `CONTACT_MSG_RECV` with `txt_type == 2` is now always treated as a Room Server message, even when the `signature` field is absent; the author name falls back gracefully instead of routing the message through the normal DM path -- 🔄 `meshcore_gui/ble/worker.py` — The global `LOGIN_SUCCESS` subscriber now also synchronizes room login state into `SharedData` and refreshes room history, so UI state no longer depends solely on the command-side waiter winning the event timing race +- 🔄 `meshcore_gui/ble/commands.py` — After `LOGIN_SUCCESS`, the room login flow now starts a bounded background `get_msg()` sync loop so serial/USB sessions actively drain queued room messages instead of relying on a single defensive fetch +- 🔄 `meshcore_gui/ble/events.py` — Room messages are now classified on `txt_type == 2` even when the `signature` field is absent; sender/room pubkeys also use broader payload fallbacks for room traffic +- 🔄 `meshcore_gui/ble/worker.py` — Global `LOGIN_SUCCESS` handling now updates `room_login_states` and refreshes cached room history in `SharedData` - 🔄 `meshcore_gui/config.py` — Version bumped to `1.13.4` ### Fixed -- 🛠 **Initial room login could remain pending or feel unreliable** — UI state now also updates from the subscribed `LOGIN_SUCCESS` event, not only from the command coroutine waiting for the same event -- 🛠 **Room messages could be missed when `txt_type == 2` arrived without `signature`** — such packets are now still classified as room traffic and shown in the Room Server panel -- 🛠 **Room history refresh after login was timing-sensitive** — history is now reloaded both from the command success path and from the subscribed login-success callback +- 🛠 **USB/serial room login showed only app-sent messages** — After login, the app now keeps polling queued room messages for a short window so messages from other room participants are actually fetched +- 🛠 **Incoming room messages without `signature` were misclassified** — `CONTACT_MSG_RECV` packets with `txt_type == 2` no longer fall back to DM handling just because the room server omitted `signature` +- 🛠 **Room login UI state could depend on one code path** — Worker-side `LOGIN_SUCCESS` processing now reinforces the room state update even when the command-side wait path is not the only consumer ### Impact -- More reliable first login behaviour for Room Server panels -- Better chance that room history and newly arriving room messages show up immediately after login -- No intended breaking changes outside the Room Server receive/login flow +- Faster and more reliable room history retrieval on USB/serial setups +- Room traffic from other users has a better chance of appearing in the Room Server panel immediately after login +- No intended regression for DM or normal channel message handling --- ## [1.13.3] - 2026-03-12 — Active Panel Timer Gating @@ -47,15 +50,34 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver ## [1.13.2] - 2026-03-11 — Map Display Bugfix ### Fixed -- 🛠 **MAP panel blank when contacts list is empty at startup** — dashboard update loop had two separate conditional map-update blocks that both silently stopped firing after tick 1 when `data['contacts']` was empty. Map panel received no further snapshots and remained blank indefinitely. -- 🛠 **Leaflet map initialized on hidden (zero-size) container** — `processPending` in the browser runtime called `L.map()` on the host element while it was still `display:none` (Vue v-show, panel not yet visible). This produced a broken 0×0 map that never recovered because `ensureMap` returned the cached broken state on all subsequent calls. Fixed by adding a `clientWidth/clientHeight` guard in `ensureMap`: initialization is deferred until the host has real dimensions. -- 🛠 **Route map container had no height** — `route_page.py` used the Tailwind class `h-96` for the Leaflet host `
`. NiceGUI/Quasar does not include Tailwind CSS, so `h-96` had no effect and the container rendered at height 0. Leaflet initialized on a zero-height element and produced a blank map. -- 🛠 **Route map not rendered when no node has GPS coordinates** — `_render_map` returned early before creating the Leaflet container when `payload['nodes']` was empty. Fixed: container is always created; a notice label is shown instead. +- 🛠 **MAP panel blank when contacts list is empty at startup** — dashboard update loop + had two separate conditional map-update blocks that both silently stopped firing after + tick 1 when `data['contacts']` was empty. Map panel received no further snapshots and + remained blank indefinitely. +- 🛠 **Leaflet map initialized on hidden (zero-size) container** — `processPending` in + the browser runtime called `L.map()` on the host element while it was still + `display:none` (Vue v-show, panel not yet visible). This produced a broken 0×0 map + that never recovered because `ensureMap` returned the cached broken state on all + subsequent calls. Fixed by adding a `clientWidth/clientHeight` guard in `ensureMap`: + initialization is deferred until the host has real dimensions. +- 🛠 **Route map container had no height** — `route_page.py` used the Tailwind class + `h-96` for the Leaflet host `
`. NiceGUI/Quasar does not include Tailwind CSS, + so `h-96` had no effect and the container rendered at height 0. Leaflet initialized + on a zero-height element and produced a blank map. +- 🛠 **Route map not rendered when no node has GPS coordinates** — `_render_map` + returned early before creating the Leaflet container when `payload['nodes']` was + empty. Fixed: container is always created; a notice label is shown instead. ### Changed -- 🔄 `meshcore_gui/static/leaflet_map_panel.js` — Added size guard in `ensureMap`: returns `null` when host has `clientWidth === 0 && clientHeight === 0` and no map state exists yet. `processPending` retries on the next tick once the panel is visible. -- 🔄 `meshcore_gui/gui/dashboard.py` — Consolidated two conditional map-update blocks into a single unconditional update while the MAP panel is active. Added `h-96` to the DOMCA CSS height overrides for consistency with the route page map container. -- 🔄 `meshcore_gui/gui/route_page.py` — Replaced `h-96` Tailwind class on the route map host `
` with an explicit inline `style` (height: 24rem). Removed early `return` guard so the Leaflet container is always created. +- 🔄 `meshcore_gui/static/leaflet_map_panel.js` — Added size guard in `ensureMap`: + returns `null` when host has `clientWidth === 0 && clientHeight === 0` and no map + state exists yet. `processPending` retries on the next tick once the panel is visible. +- 🔄 `meshcore_gui/gui/dashboard.py` — Consolidated two conditional map-update blocks + into a single unconditional update while the MAP panel is active. Added `h-96` to the + DOMCA CSS height overrides for consistency with the route page map container. +- 🔄 `meshcore_gui/gui/route_page.py` — Replaced `h-96` Tailwind class on the route + map host `
` with an explicit inline `style` (height: 24rem). Removed early + `return` guard so the Leaflet container is always created. ### Impact - MAP panel now renders reliably on first open regardless of contact/GPS availability @@ -63,6 +85,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver - No breaking changes outside the three files listed above --- +>>>>>>> b76eacf1119026c49c25d2811a6d713da8f8e01b ## [1.13.0] - 2026-03-09 — Leaflet Map Runtime Stabilization ### Added diff --git a/meshcore_gui/ble/commands.py b/meshcore_gui/ble/commands.py index 6858f85..d1bae49 100644 --- a/meshcore_gui/ble/commands.py +++ b/meshcore_gui/ble/commands.py @@ -35,6 +35,7 @@ class CommandHandler: self._mc = mc self._shared = shared self._cache = cache + self._room_sync_tasks: Dict[str, asyncio.Task] = {} # Handler registry — add new commands here (OCP) self._handlers: Dict[str, object] = { @@ -406,7 +407,6 @@ class CommandHandler: pubkey, 'ok', f"admin={is_admin}", ) - self._shared.load_room_history(pubkey) self._shared.set_status( f"✅ Room login OK: {room_name} — " f"history arriving over RF…" @@ -426,6 +426,8 @@ class CommandHandler: except Exception as exc: debug_print(f"login_room: defensive get_msg() error: {exc}") + self._start_room_sync(pubkey, room_name) + else: self._shared.set_room_login_state( pubkey, 'fail', @@ -552,6 +554,63 @@ class CommandHandler: ) debug_print(f"send_room_msg exception: {exc}") + def _cancel_room_sync(self, pubkey: str) -> None: + """Cancel an active background room-history sync task.""" + task = self._room_sync_tasks.pop(pubkey, None) + if task and not task.done(): + task.cancel() + + def _start_room_sync(self, pubkey: str, room_name: str) -> None: + """Start a bounded background fetch loop for room history.""" + self._cancel_room_sync(pubkey) + self._room_sync_tasks[pubkey] = asyncio.create_task( + self._sync_room_history(pubkey, room_name) + ) + + async def _sync_room_history(self, pubkey: str, room_name: str) -> None: + """Fetch queued room messages for a short period after login. + + On some serial/USB setups the SDK's auto-message fetching is + not sufficient to drain the room backlog promptly after + ``LOGIN_SUCCESS``. This bounded loop polls ``get_msg()`` for a + short window so historical room messages from other users are + actually pulled into the app. + """ + idle_errors = 0 + try: + for attempt in range(24): + try: + result = await self._mc.commands.get_msg() + result_type = getattr(result, 'type', None) + if result_type == EventType.ERROR: + idle_errors += 1 + debug_print( + f"room_sync: get_msg ERROR for {room_name} " + f"(attempt {attempt + 1}/24, idle={idle_errors})" + ) + else: + idle_errors = 0 + debug_print( + f"room_sync: get_msg fetched data for {room_name} " + f"(attempt {attempt + 1}/24)" + ) + except Exception as exc: + idle_errors += 1 + debug_print( + f"room_sync: get_msg exception for {room_name}: {exc}" + ) + + if idle_errors >= 4: + break + + await asyncio.sleep(2.0) + except asyncio.CancelledError: + debug_print(f"room_sync: cancelled for {room_name}") + raise + finally: + self._shared.load_room_history(pubkey) + self._room_sync_tasks.pop(pubkey, None) + # ------------------------------------------------------------------ # Callback for refresh (set by SerialWorker after construction) # ------------------------------------------------------------------ diff --git a/meshcore_gui/ble/events.py b/meshcore_gui/ble/events.py index 65ca5c5..e5cd220 100644 --- a/meshcore_gui/ble/events.py +++ b/meshcore_gui/ble/events.py @@ -320,17 +320,26 @@ class EventHandler: # --- Room Server message (txt_type 2) --- if txt_type == 2: - # Prefer the embedded author signature when available. - # Some room-history / server-side messages arrive without a - # signature; those still belong to the room and must not fall - # through to the regular DM path. + room_pubkey = ( + payload.get('room_pubkey') + or payload.get('receiver_pubkey') + or payload.get('recipient_pubkey') + or payload.get('pubkey') + or pubkey + ) + author_prefix = ( + signature + or payload.get('sender_pubkey_prefix', '') + or payload.get('sender_pubkey', '') + or payload.get('sender_prefix', '') + ) author = '' - if signature: - author = self._shared.get_contact_name_by_prefix(signature) - if not author: - author = signature[:8] + if author_prefix: + author = self._shared.get_contact_name_by_prefix(author_prefix) if not author: - author = pubkey[:8] if pubkey else '?' + author = payload.get('sender_name', '') or payload.get('name', '') + if not author: + author = author_prefix[:8] if author_prefix else room_pubkey[:8] if room_pubkey else '?' self._shared.add_message(Message.incoming( author, @@ -338,14 +347,14 @@ class EventHandler: None, snr=self._extract_snr(payload), path_len=path_len, - sender_pubkey=pubkey, + sender_pubkey=room_pubkey, path_hashes=path_hashes, path_names=path_names, message_hash=msg_hash, )) debug_print( - f"Room msg from {author} (sig={signature or '-'}) " - f"via room {pubkey[:12]}: " + f"Room msg from {author} (sig={signature}) " + f"via room {room_pubkey[:12]}: " f"{payload.get('text', '')[:30]}" ) return diff --git a/meshcore_gui/ble/worker.py b/meshcore_gui/ble/worker.py index 7ed1751..af9965b 100644 --- a/meshcore_gui/ble/worker.py +++ b/meshcore_gui/ble/worker.py @@ -258,21 +258,12 @@ class _BaseWorker(abc.ABC): # ── LOGIN_SUCCESS handler (Room Server) ─────────────────────── def _on_login_success(self, event) -> None: - """Synchronise Room Server login success into SharedData. - - This callback is intentionally independent from the command-side - ``wait_for_event(LOGIN_SUCCESS)`` path. If the library delivers the - event to subscribers before or instead of the waiter, the UI must - still transition to the logged-in state and refresh room history. - """ payload = event.payload or {} pubkey = payload.get("pubkey_prefix", "") is_admin = payload.get("is_admin", False) - detail = f"admin={is_admin}" - debug_print(f"LOGIN_SUCCESS received: pubkey={pubkey}, admin={is_admin}") - self.shared.set_room_login_state(pubkey, 'ok', detail) if pubkey: + self.shared.set_room_login_state(pubkey, 'ok', f'admin={is_admin}') self.shared.load_room_history(pubkey) self.shared.set_status("✅ Room login OK — messages arriving over RF…")