WHAT: New BotPanel replaces the BOT checkbox in ActionsPanel. Interactive channel checkboxes (from live device channel list) replace the hardcoded BOT_CHANNELS constant. Private mode restricts replies to pinned contacts only. BotConfigStore persists settings per device to ~/.meshcore-gui/bot/. WHY: Bot configuration was scattered (toggle in Actions, channels in code). A dedicated panel and config store aligns with the BBS panel/BbsConfigStore pattern and enables private mode without architectural changes. NOTES: ActionsPanel.__init__ signature simplified (set_bot_enabled removed). create_worker accepts pin_store kwarg (backwards compatible, defaults to None).
72 KiB
CHANGELOG
All notable changes to MeshCore GUI are documented in this file. Format follows Keep a Changelog and Semantic Versioning.
[1.15.0] - 2026-03-16
ADDED
- BOT panel (
gui/panels/bot_panel.py): new dedicated panel in the main menu (between RX LOG and BBS) with enable toggle, private mode toggle and interactive channel assignment via checkboxes built from the live device channel list. - BotConfigStore (
services/bot_config_store.py): persistent bot configuration per device stored at~/.meshcore-gui/bot/_<dev_id>_bot.json. Saves enabled flag, private mode state and selected channel set across restarts. - Private mode: when enabled the bot only replies to pinned contacts. Guard 1.5
added to
MeshBot.check_and_reply— reads live fromBotConfigStoreso changes take effect immediately without restart. - Private mode constraint: private mode can only be activated when at least one contact is pinned. The toggle is disabled (greyed out) with an explanation label when no pinned contacts exist; auto-disables if all pins are removed.
- Interactive channel assignment: BOT panel shows a checkbox per discovered
channel; selection persisted via
BotConfigStore.set_channels()on Save. BOT_DIRconfig constant (~/.meshcore-gui/bot/) centralising the storage root for bot configuration files.
CHANGED
- BOT toggle removed from ActionsPanel:
actions_panel.pyno longer contains the BOT checkbox orset_bot_enabledwiring; the panel is now solely for Refresh, Advertise and Set device name. MeshBotgains two optional constructor arguments:config_store(BotConfigStore) for live channel/private-mode reads, andpinned_check(Callable[[str], bool]) for pin lookups. Fully backwards-compatible — both default toNoneand existing behaviour is preserved when absent.MeshBot.check_and_replygains optionalsender_pubkeykwarg used by Guard 1.5._BaseWorkernow accepts optionalpin_storekwarg; wirespinned_checkandconfig_storeintoMeshBotat construction time.create_workerforwards optionalpin_storekwarg to subworkers.DashboardPagereceivesBotConfigStoreinstance;ActionsPanelcall no longer passesset_bot_enabled.
IMPACT
ble/events.py: bothcheck_and_replycall sites now passsender_pubkey=.ble/worker.py:_BaseWorker,SerialWorker,BLEWorker,create_workerupdated.gui/dashboard.py:BotPanelregistered as panel'bot'; menu item🤖 BOTadded.gui/panels/actions_panel.py: BOT toggle removed;ActionsPanel.__init__signature simplified to(put_command).config.py:VERSIONbumped to1.15.0;BOT_DIRconstant added.
RATIONALE
Bot functionality was embedded in the Actions panel and had no persistence. Extracting it to a dedicated panel and a config store aligns with the existing modularity of the codebase (cf. BBS panel / BbsConfigStore) and enables future extension. Private mode fulfils the requirement to restrict bot replies to trusted contacts only.
📈 Performance note — v1.13.1 through v1.13.4 Although versions 1.13.1–1.13.4 were released as targeted bugfix releases, the cumulative effect of the fixes delivered a significant performance improvement:
- v1.13.1 — Bot non-response fix eliminated a silent failure path that caused repeated dedup-marked command re-evaluation on every message tick.
- v1.13.2 — Map display fixes prevented Leaflet from being initialized on hidden zero-size containers, removing a source of repeated failed bootstrap retries and associated DOM churn.
- v1.13.3 — Active panel timer gating reduced the 500 ms dashboard update work to only the currently visible panel, cutting unnecessary UI updates and background redraw load substantially — especially noticeable over VPN or on slower hardware.
- v1.13.4 — Room Server event classification fix and sender name resolution removed redundant fallback processing paths and reduced per-tick contact lookup overhead.
Users upgrading from v1.12.x or earlier will notice noticeably faster panel switching, lower CPU usage during idle operation, and more stable map rendering.
[1.14.3] - 2026-03-16 — BBS !h / !help NameError fix
Fixed
- 🐛
services/bbs_service.py—!hen!helpDM-commando's gooiden eenNameError: name 'cu' is not definedin_abbrev_table().- Root cause:
cuwerd gedefinieerd in een inner list comprehension[cu.upper() for cu in categories], maar Python 3 list comprehensions hebben een eigen scope. Deif cu.upper() in invin de buitenste generator expression koncudaardoor niet bereiken. - Fix: list comprehension extracted naar een aparte variabele
cats_upper; de generator itereert nu over die lijst.
- Root cause:
[1.14.2] - 2026-03-16 — BBS whitelist fix: !bbs channel hook in on_rx_log
Fixed
- 🐛
ble/events.py—!bbsop een geconfigureerd BBS-channel deed nooit een whitelist-add, waardoor!hen andere DM-BBS-commando's daarna silently werden gedropped.- Root cause: de BBS channel hook stond uitsluitend in
on_channel_msg, maaron_channel_msgwordt in het normale pad onderdrukt door de content-dedup early-return (het bericht is dan al dooron_rx_logverwerkt en gemarkeerd). - Fix: BBS channel hook (
handle_channel_msg) ook aangeroepen inon_rx_log, direct ná de bot-aanroep, binnen deGroupText + channel_idx resolved-branch.sender_pubkeyis daar al opgelost viaget_contact_by_name. - De hook in
on_channel_msgblijft intact als fallback voor het deferred-path (channel_idx onopgelost inon_rx_log).
- Root cause: de BBS channel hook stond uitsluitend in
[1.14.1] - 2026-03-16 — BBS test corrections
Changed
- Testing package flattened to a single canonical
meshcore_gui/...tree so runtime and validation target one code path. !bbschannel bootstrap, DM-only!h/!help, and chunked BBS reply work were applied as in-progress fixes under version1.14.1while testing continues.- No release bump: version numbering is kept at
1.14.1for this test set.
[1.14.0] - 2026-03-14 — BBS (Bulletin Board System)
Added
- 🆕 BBS — Bulletin Board System — offline berichtenbord voor mesh-netwerken.
- Toegangsmodel: de beheerder selecteert één of meer channels in de settings. Iedereen die op een van die channels een bericht stuurt, wordt automatisch gewhitelist en kan daarna commando's sturen via Direct Message aan de node. Het channel blijft schoon; alleen de eerste interactie verloopt via het channel.
- Korte syntax:
!p <cat> <tekst>(post) en!r [cat](lezen). Categorie-afkortingen automatisch berekend als kortste unieke prefix (bijv.U=URGENT M=MEDICAL). - Volledige syntax behouden:
!bbs post,!bbs read,!bbs help. - Optioneel regio-filter en handmatige allowed-keys override in Advanced.
- Settings-pagina (
/bbs-settings): checkboxes per channel, categorieën, retentie, Advanced voor regio's en handmatige keys. - Berichten opgeslagen in SQLite (
~/.meshcore-gui/bbs/bbs_messages.db, WAL-mode).
Changed
- 🔄
ble/events.py—on_channel_msgroeptBbsCommandHandler.handle_channel_msg()aan op geconfigureerde BBS-channels: auto-whitelist + bootstrap reply.on_contact_msgstuurt!-DMs direct naarhandle_dm(). Beide paden volledig los vanMeshBot. - 🔄
services/bot.py—MeshBotis weer een pure keyword/channel responder; BBS-routing verwijderd. - 🔄
services/bbs_config_store.py—configure_board()(multi-channel),add_allowed_key()(auto-whitelist),clear_board(). - 🔄
gui/dashboard.py—BbsPanelgeregistreerd,📋 BBSdrawer-item toegevoegd.
Storage
~/.meshcore-gui/bbs/bbs_config.json — board configuratie
~/.meshcore-gui/bbs/bbs_messages.db — SQLite berichtenopslag
[1.13.5] - 2026-03-14 — Route back-button and map popup flicker fixes
Fixed
- 🛠 Route page back-button navigated to main menu regardless of origin — the two fixed navigation buttons (
/and/archive) are replaced by a singlearrow_backbutton that callswindow.history.back(), so the user is always returned to the screen that opened the route page. - 🛠 Map marker popup flickered on every 500 ms update tick — the periodic
applyContacts/applyDevicecalls inleaflet_map_panel.jsinvokedsetIcon()andsetPopupContent()on all existing markers unconditionally.setIcon()rebuilds the marker DOM element; when a popup was open this caused the popup anchor to detach and reattach, producing visible flickering. Both functions now checkmarker.isPopupOpen()and skip icon/content updates while the popup is visible. - 🛠 Map marker popup appeared with a flicker/flash on first click (main map and route map) — Leaflet's default
fadeAnimation: truecaused popups to fade in from opacity 0, which on the Raspberry Pi rendered as a visible flicker. BothL.map()initialisations (ensureMapandMeshCoreRouteMapBoot) now setfadeAnimation: falseandmarkerZoomAnimation: falseso popups appear immediately without animation artefacts.
Changed
- 🔄
meshcore_gui/gui/route_page.py— Replaced two fixed-destination header buttons with a singlearrow_backbutton usingwindow.history.back(). - 🔄
meshcore_gui/static/leaflet_map_panel.js—applyDeviceandapplyContactsguardsetIcon/setPopupContentbehindisPopupOpen(). BothL.map()calls addfadeAnimation: false, markerZoomAnimation: false. - 🔄
meshcore_gui/config.py— Version bumped to1.13.5.
Impact
- Back navigation from the route page now always returns to the correct origin screen.
- Open marker popups are stable during map update ticks; content refreshes on next tick after the popup is closed.
- Popup opening is instant on both maps; no animation artefacts on low-power hardware.
[1.13.4] - 2026-03-12 — Room Server message classification fix
Fixed
- 🛠 Incoming room messages from other participants could be misclassified as normal DMs —
CONTACT_MSG_RECVroom detection now keys ontxt_type == 2instead of requiringsignature. - 🛠 Incoming room traffic could be attached to the wrong key — room message handling now prefers
room_pubkey/ receiver-style payload keys before falling back topubkey_prefix. - 🛠 Room login UI could stay out of sync with the actual server-confirmed state —
LOGIN_SUCCESSnow updatesroom_login_statesand refreshes room history using the resolved room key. - 🛠 Room Server panel showed hex codes instead of sender names — when a contact was not yet known at the time a room message was archived,
msg.senderwas stored as a raw hex prefix. The panel now performs a live lookup against the current contacts snapshot on every render tick, so names are shown as soon as the contact is known.
Changed
- 🔄
meshcore_gui/ble/events.py— Broadened room payload parsing and added payload-key debug logging for incoming room traffic. - 🔄
meshcore_gui/ble/worker.py—LOGIN_SUCCESShandler now updates per-room login state and refreshes cached room history. - 🔄
meshcore_gui/config.py— Version kept at1.13.4.
Impact
- Keeps the existing Room Server panel logic intact.
- Fix is limited to room event classification and room login confirmation handling.
- No intended behavioural change for ordinary DMs or channel messages.
[1.13.3] - 2026-03-12 — Active Panel Timer Gating
Changed
- 🔄
meshcore_gui/gui/dashboard.py— The 500 ms dashboard timer now keeps only lightweight global state updates running continuously (status label, channel filters/options, drawer submenu consistency). Expensive panel refreshes are now gated to the currently active panel only - 🔄
meshcore_gui/gui/dashboard.py— Added immediate active-panel refresh on panel switch so newly opened panels populate at once instead of waiting for the next timer tick - 🔄
meshcore_gui/gui/panels/map_panel.py— Removed eager hiddenensure_mapbootstrap fromrender(); the browser map now starts only when real snapshot work exists or when a live map already exists - 🔄
meshcore_gui/static/leaflet_map_panel.js— Theme-only calls without snapshot work no longer start hidden host retry processing before a real map exists - 🔄
meshcore_gui/config.py— Version bumped to1.13.3
Fixed
- 🛠 Hidden panels still refreshed every 500 ms — Device, actions, contacts, messages, rooms and RX log are no longer needlessly updated while another panel is active
- 🛠 Map bootstrap activity while panel is not visible — Removed one source of
MeshCoreLeafletBoot timeout waiting for visible map hostcaused by eager hidden startup traffic - 🛠 Slow navigation over VPN — Reduced unnecessary dashboard-side UI churn by limiting timer-driven work to the active panel
Impact
- Faster panel switching because the selected panel gets one direct refresh immediately
- Lower background UI/update load on dashboard level, especially when the map panel is not active
- Smaller chance of Leaflet hidden-host retries and related console noise outside active map usage
- No intended functional regression for route maps or visible panel behaviour
[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 —
processPendingin the browser runtime calledL.map()on the host element while it was stilldisplay:none(Vue v-show, panel not yet visible). This produced a broken 0×0 map that never recovered becauseensureMapreturned the cached broken state on all subsequent calls. Fixed by adding aclientWidth/clientHeightguard inensureMap: initialization is deferred until the host has real dimensions. - 🛠 Route map container had no height —
route_page.pyused the Tailwind classh-96for the Leaflet host<div>. NiceGUI/Quasar does not include Tailwind CSS, soh-96had 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_mapreturned early before creating the Leaflet container whenpayload['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 inensureMap: returnsnullwhen host hasclientWidth === 0 && clientHeight === 0and no map state exists yet.processPendingretries 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. Addedh-96to the DOMCA CSS height overrides for consistency with the route page map container. - 🔄
meshcore_gui/gui/route_page.py— Replacedh-96Tailwind class on the route map host<div>with an explicit inlinestyle(height: 24rem). Removed earlyreturnguard so the Leaflet container is always created.
Impact
- MAP panel now renders reliably on first open regardless of contact/GPS availability
- Route map now always shows with correct height even when route nodes have no GPS
- No breaking changes outside the three files listed above
[1.13.0] - 2026-03-09 — Leaflet Map Runtime Stabilization
Added
- ✅
meshcore_gui/static/leaflet_map_panel.js— Dedicated browser-side Leaflet runtime responsible for map lifecycle, marker registry, clustering and theme handling independent from NiceGUI redraw cycles - ✅
meshcore_gui/static/leaflet_map_panel.css— Styling for browser-side node markers, cluster icons and map container - ✅
meshcore_gui/services/map_snapshot_service.py— Snapshot service that normalizes device/contact map data into a compact payload for the browser runtime - ✅ Browser-side map state management for center, zoom and theme
- ✅ Theme persistence across reconnect events via browser storage fallback
- ✅ Browser-side contact clustering via
Leaflet.markercluster - ✅ Separate non-clustered device marker layer so the own device remains individually visible
Changed
- 🔄
meshcore_gui/gui/panels/map_panel.py— Replaced NiceGUI Leaflet wrapper usage with a pure browser-managed Leaflet container while preserving the existing card layout, theme toggle and center-on-device control - 🔄 Leaflet bootstrap moved out of inline Python into a dedicated browser runtime loaded from
/static - 🔄 Asset loading order is now explicit: Leaflet first, then
Leaflet.markercluster, then the MeshCore panel runtime - 🔄 Map initialization now occurs only once per container; NiceGUI refresh cycles no longer recreate the map
- 🔄 Dashboard update loop now sends compact map snapshots instead of triggering redraws
- 🔄 Snapshot processing in the browser is coalesced so only the newest payload is applied
- 🔄 Map markers are managed in separate device/contact layers and updated incrementally by stable node id
- 🔄 Contact markers are rendered inside a persistent cluster layer while the device marker remains outside clustering
- 🔄 Theme switching moved to a dedicated theme channel instead of being embedded in snapshot data
Fixed
- 🛠 Map disappearing during dashboard refresh cycles — prevented repeated map reinitialization caused by the 500 ms NiceGUI update loop
- 🛠 Markers disappearing between refreshes — marker updates are now incremental and keyed by node id
- 🛠 Blank map container on load — browser bootstrap now waits for DOM host, Leaflet runtime and panel runtime before initialization
- 🛠 Leaflet clustering bootstrap failure (
L is not defined) — resolved by enforcing correct script dependency order before the panel runtime starts - 🛠 MarkerClusterGroup failure (
Map has no maxZoom specified) — the map now definesmaxZoomduring initial creation before the cluster layer is attached - 🛠 Half-initialized map retry cascade (
Map container is already initialized) — map state is now registered safely during initialization so a failed attempt cannot trigger a secondL.map(...)on the same container - 🛠 Race condition between queued snapshot and theme selection — explicit theme changes can no longer be overwritten by stale snapshot payloads
- 🛠 Viewport jumping back to default center/zoom — stored viewport is no longer reapplied on each snapshot update
- 🛠 Theme reverting to default during reconnect — effective map theme is restored before snapshot processing resumes
Impact
- Leaflet map is now managed entirely in the browser and is no longer recreated on each dashboard refresh
- Node markers remain stable and no longer flicker or disappear during the 500 ms update cycle
- Dense contact sets can now be rendered with clustering without violating the browser-owned map lifecycle
- Theme switching and viewport state persist reliably across reconnect events
- No breaking changes outside the map subsystem
[1.12.1] - 2026-03-08 — Minor change bot
Changed
- 🔄
meshcore_gui/services/bot.py: remove path id's
Impact
- No breaking changes — all existing functionality preserved serial.
[1.12.0] - 2026-02-26 — MeshCore Observer Fase 1
Added
- ✅ MeshCore Observer daemon — New standalone read-only daemon (
meshcore_observer.py) that reads archive JSON files produced by meshcore_gui and meshcore_bridge, aggregates them, and presents a unified NiceGUI monitoring dashboard on port 9093. - ✅ ArchiveWatcher — Core component that polls
~/.meshcore-gui/archive/for*_messages.jsonand*_rxlog.jsonfiles, tracks mtime changes, and returns only new entries since previous poll. Thread-safe, zero writes, graceful on corrupt JSON. - ✅ Observer dashboard panels — Sources overview, aggregated messages feed (sorted by timestamp), aggregated RX log table, and statistics panel with uptime/counters/per-source breakdown. Full DOMCA theme (dark + light mode).
- ✅ Source filter — Dropdown to filter messages and RX log by archive source.
- ✅ Channel filter — Dropdown to filter messages by channel name.
- ✅ ObserverConfig — YAML-based configuration with
from_yaml()classmethod, defaults work without config file. - ✅ observer_config.yaml — Documented config template with all options.
- ✅ install_observer.sh — systemd installer (
/opt/meshcore-observer/,/etc/meshcore/observer_config.yaml), with--uninstalloption. - ✅ RxLogEntry raw packet fields — 5 new fields on
RxLogEntrydataclass:raw_payload,packet_len,payload_len,route_type,packet_type_num(all with defaults, backward compatible). - ✅ EventHandler.on_rx_log() metadata — Raw payload hex and packet metadata now passed through to RxLogEntry and archived (preparation for Fase 2 LetsMesh uplink).
Changed
- 🔄
meshcore_gui/core/models.py: RxLogEntry +5 fields with defaults (backward compatible). - 🔄
meshcore_gui/ble/events.py: on_rx_log() fills raw_payload and metadata (~10 lines added). - 🔄
meshcore_gui/services/message_archive.py: add_rx_log() serializes the 5 new RxLogEntry fields. - 🔄
meshcore_gui/config.py: Version bumped to1.12.0.
Impact
- No breaking changes — All new RxLogEntry fields have defaults; existing archives and code work identically.
- New daemon — meshcore_observer is fully standalone; no imports from meshcore_gui (reads only JSON files).
Added
- ✅ Serial CLI flags —
--baud=BAUDand--serial-cx-dly=SECONDSfor serial configuration at startup.
Changed
- 🔄 Connection layer — Switched from BLE to serial (
MeshCore.create_serial) with serial reconnect handling. - 🔄
config.py: AddedSERIAL_BAUDRATE,SERIAL_CX_DELAY,DEFAULT_TIMEOUT,MESHCORE_LIB_DEBUG; removed BLE PIN settings; version bumped to1.10.0. - 🔄
meshcore_gui.py/meshcore_gui/__main__.py: Updated usage, banners and defaults for serial ports. - 🔄 Docs: Updated README and core docs for serial usage; BLE documents marked as legacy.
Impact
- No breaking changes — all existing functionality preserved serial.
[1.9.11] - 2026-02-19 — Message Dedup Hotfix
Fixed
- 🛠 Duplicate messages after (re)connect —
load_recent_from_archive()appended archived messages on every connect attempt without clearing existing entries; after N failed connects, each message appeared N times. Method is now idempotent: clears the in-memory list before loading. - 🛠 Persistent duplicate messages — Live BLE events for messages already loaded from archive were not suppressed because the
DualDeduplicatorwas never seeded with archived content. Added_seed_dedup_from_messages()inBLEWorkerafter cache/archive load and after reconnect. - 🛠 Last-line-of-defence dedup in SharedData —
add_message()now maintains a fingerprint set (message_hashorchannel:sender:text) and silently skips messages whose fingerprint is already tracked. This guards against duplicates regardless of their source. - 🛠 Messages panel empty on first click —
_show_panel()made the container visible but relied on the next 500 ms timer tick to populate it. Added an immediate_messages.update()call so content is rendered the moment the panel becomes visible.
Changed
- 🔄
core/shared_data.py: Added_message_fingerprintsset and_message_fingerprint()static method;add_message()checks fingerprint before insert and evicts fingerprints when messages are rotated out;load_recent_from_archive()clears messages and fingerprints before loading (idempotent) - 🔄
ble/worker.py: Added_seed_dedup_from_messages()helper; called after_apply_cache()and after reconnect_load_data()to seedDualDeduplicatorwith existing messages - 🔄
gui/dashboard.py:_show_panel()now forces an immediate_messages.update()when the messages panel is shown, eliminating the stale-content flash - 🔄
config.py: Version bumped to1.9.11
Impact
- Eliminates all duplicate message display scenarios: initial connect, failed retries, reconnect, and BLE event replay
- No breaking changes — all existing functionality preserved
- Fingerprint set is bounded to the same 100-message cap as the message list
[1.9.10] - 2026-02-19 — Map Tooltips & Separate Own-Position Marker
Added
- ✅ Map marker tooltips — All markers on the Leaflet map now show a tooltip on hover with the node name and type icon (📱, 📡, 🏠) from
TYPE_ICONS - ✅ Separate own-position marker — The device's own position is now tracked as a dedicated
_own_marker, independent from contact markers. This prevents the own marker from being removed/recreated on every contact update cycle
Changed
- 🔄
gui/panels/map_panel.py: Renamed_markersto_contacts_markers; added_own_markerattribute; own position marker is only updated whendevice_updatedflag is set (not every timer tick); contact markers are only rebuilt whencontacts_updatedis set; addedTYPE_ICONSimport for tooltip icons - 🔄
gui/dashboard.py: Addedself._map.update(data)call in thedevice_updatedblock so the own-position marker updates when device info changes (e.g. GPS position update) - 🔄
config.py: Version bumped to1.9.10
Impact
- Map centering on own device now works correctly and updates only when position actually changes
- Contact markers are no longer needlessly destroyed and recreated on every UI timer tick — only on actual contact data changes
- Tooltips make it easy to identify nodes on the map without clicking
- No breaking changes — all existing map functionality preserved
Credits
[1.9.9] - 2026-02-18 — Variable Landing Page & Operator Callsign
Added
- ✅ Configurable operator callsign — New
OPERATOR_CALLSIGNconstant inconfig.py(default:"PE1HVH"). Used in the landing page SVG and the drawer footer copyright label. Change this single value to personalize the entire GUI for a different operator - ✅ External landing page SVG — The DOMCA splash screen is now loaded from a standalone file (
static/landing_default.svg) instead of being hardcoded indashboard.py. NewLANDING_SVG_PATHconstant inconfig.pypoints to the SVG file. The placeholder{callsign}in the SVG is replaced at runtime withOPERATOR_CALLSIGN - ✅ Landing page customization — To use a custom landing page: copy
landing_default.svg(or create your own SVG), use{callsign}wherever the operator callsign should appear, and pointLANDING_SVG_PATHto your file. The default SVG includes an instructive comment block explaining the placeholder mechanism
Changed
- 🔄
config.py: AddedOPERATOR_CALLSIGNandLANDING_SVG_PATHconstants in new OPERATOR / LANDING PAGE section; version bumped to1.9.9 - 🔄
gui/dashboard.py: Removed hardcoded_DOMCA_SVGstring (~70 lines); added_load_landing_svg()helper that reads SVG from disk and replaces{callsign}placeholder; CSS variable--pe1hvhrenamed to--callsign; drawer footer copyright label now usesconfig.OPERATOR_CALLSIGN
Added (files)
- ✅
static/landing_default.svg— The original DOMCA splash SVG extracted as a standalone file, with{callsign}placeholder and--callsignCSS variable. Serves as both the default landing page and a reference template for custom SVGs
Impact
- Out-of-the-box behavior is identical to v1.9.8 (same DOMCA branding, same PE1HVH callsign)
- Operators personalize by changing 1–2 lines in
config.py— no code modifications needed - Fallback: if the SVG file is missing, a minimal placeholder text is shown instead of a crash
- No breaking changes — all existing dashboard functionality (panels, menus, timer, theming) unchanged
[1.9.8] - 2026-02-17 — Bugfix: Route Page Sender ID, Type & Location Not Populated
Fixed
- 🛠 Sender ID, Type and Location empty in Route Page — After the v4.1 refactoring to
RouteBuilder/RouteNode, the sender contact lookup relied solely onSharedData.get_contact_by_prefix()(live lock-based) andget_contact_by_name(). When both failed (e.g. emptysender_pubkeyfrom RX_LOG decode, or name mismatch),route['sender']remainedNoneand the route table fell through to a hardcoded fallback withtype: '-',location: '-'. The contact data was available in the snapshotdata['contacts']but was never searched - 🛠 Route table fallback row ignored available contact data — When
route['sender']wasNone, the_render_route_tablemethod used a static fallback row without attempting to find the contact in the data snapshot. Even when the contact was present indata['contacts']with valid type and location, these fields showed as'-'
Changed
- 🔄
services/route_builder.py: Added two additional fallback strategies inbuild()after the existing SharedData lookups: (3) bidirectional pubkey prefix match againstdata['contacts']snapshot, (4) case-insensitiveadv_namematch againstdata['contacts']snapshot. Added helper methods_find_contact_by_pubkey()and_find_contact_by_adv_name()for snapshot-based lookups - 🔄
gui/route_page.py: Added defensive fallback in_render_route_table()sender section — whenroute['sender']isNone, attempts to find the contact in the snapshot via_find_sender_contact()before falling back to the static'-'row. Added_find_sender_contact()helper method
Impact
- Sender ID (hash), Type and Location are now populated correctly in the route table when the contact is known
- Four-layer lookup chain ensures maximum resolution: (1) SharedData pubkey lookup, (2) SharedData name lookup, (3) snapshot pubkey lookup, (4) snapshot name lookup
- Defensive fallback in route_page guarantees data is shown even if RouteBuilder misses it
- No breaking changes — all existing route page behavior, styling and data flows unchanged
[1.9.7] - 2026-02-17 — Layout Fix: Archive Filter Toggle & Route Page Styling
Changed
- 🔄
gui/archive_page.py: Archive filter card now hidden by default; toggle visibility via afilter_listicon button placed right-aligned on the same row as the "📚 Archive" title. Header restructured from single label toui.row()withjustify-betweenlayout - 🔄
gui/route_page.py: Route page now uses DOMCA theme (imported fromdashboard.py) with dark mode as default, consistent with the main dashboard. Header restyled frombg-blue-600to Quasar-themed header with JetBrains Mono font. Content container changed fromw-full max-w-4xl mx-autotodomca-panelclass for consistent responsive sizing - 🔄
gui/dashboard.py: Addeddomca-header-textCSS class with@media (max-width: 599px)rule to hide header text on narrow viewports; applied to version label and status label - 🔄
gui/route_page.py: Header label also usesdomca-header-textclass for consistent responsive behaviour
Added
- ✅ Archive filter toggle —
filter_listicon button in archive header row toggles the filter card visibility on click - ✅ Route page close button —
X(close) icon button added right-aligned in the route page header; callswindow.close()to close the browser tab - ✅ Responsive header — On viewports < 600px, header text labels are hidden; only icon buttons (menu, dark mode toggle, close) remain visible
Impact
- Archive page is cleaner by default — filters only shown when needed
- Route page visually consistent with the main dashboard (DOMCA theme, dark mode, responsive panel width)
- Headers degrade gracefully on mobile (< 600px): only icon buttons visible, no text overflow
- No functional changes — all event handlers, callbacks, data bindings, logic and imports are identical to the input
[1.9.6] - 2026-02-17 — Bugfix: Channel Discovery Reliability
Fixed
- 🛠 Channels not appearing (especially on mobile) — Channel discovery aborted too early on slow BLE connections. The
_discover_channels()probe used a single attempt per channel slot and stopped after just 2 consecutive empty responses. On mobile BLE stacks (WebBluetooth via NiceGUI) where GATT responses are slower, this caused discovery to abort before finding any channels, falling back to only[0] Public - 🛠 Race condition: channel update flag lost between threads —
get_snapshot()andclear_update_flags()were two separate calls, each acquiring the lock independently. If the BLE worker setchannels_updated = Truebetween these two calls, the GUI consumed the flag viaget_snapshot()but thenclear_update_flags()reset it — causing the channel submenu and dropdown to never populate - 🛠 Channels disappear on browser reconnect — When a browser tab is closed and reopened,
render()creates new (empty) NiceGUI containers for the drawer submenus, but did not reset_last_channel_fingerprint. The_update_submenus()method compared the new fingerprint against the stale one, found them equal, and skipped the rebuild — leaving the new containers permanently empty. Fixed by resetting both_last_channel_fingerprintand_last_rooms_fingerprintinrender()
Changed
- 🔄
core/shared_data.py: New atomic methodget_snapshot_and_clear_flags()that reads the snapshot and resets all update flags in a single lock acquisition. Internally refactored to_build_snapshot_unlocked()helper. Existingget_snapshot()andclear_update_flags()retained for backward compatibility - 🔄
ble/worker.py:_discover_channels()—max_attemptsincreased from 1 to 2 per channel slot; inter-attemptdelayincreased from 0.5s to 1.0s; consecutive error threshold raised from 2 to 3; inter-channel pause increased from 0.15s to 0.3s for mobile BLE stack breathing room - 🔄
gui/dashboard.py:_update_ui()now usesget_snapshot_and_clear_flags()instead of separateget_snapshot()+clear_update_flags();render()now resets_last_channel_fingerprintand_last_rooms_fingerprinttoNoneso that_update_submenus()rebuilds into the freshly created containers; channel-dependent updates (update_filters,update_channel_options,_update_submenus) now run unconditionally when channel data exists — safe because each method has internal idempotency checks - 🔄
gui/panels/messages_panel.py:update_channel_options()now includes an equality check on options dict to skip redundant.update()calls to the NiceGUI client on every 500ms timer tick
Impact
- Channel discovery now survives transient BLE timeouts that are common on mobile connections
- Atomic snapshot eliminates the threading race condition that caused channels to silently never appear
- Browser close+reopen no longer loses channels — the single-instance timer race on the shared
DashboardPageis fully mitigated - No breaking changes — all existing API methods retained, all other functionality unchanged
[1.9.5] - 2026-02-16 — Layout Fix: RX Log Table Responsive Sizing
Fixed
- 🛠 RX Log table did not adapt to panel/card size — The table used
max-h-48(a maximum height cap) instead of a responsive fixed height, causing it to remain small regardless of available space. Changed toh-40which is overridden by the existing dashboard CSS tocalc(100vh - 20rem)— the same responsive pattern used by the Messages panel - 🛠 RX Log table did not fill card width — Added
w-fullclass to the table element so it stretches to the full width of the parent card - 🛠 RX Log card did not fill panel height — Added
flex-growclass to the card container so it expands to fill the available panel space
Changed
- 🔄
gui/panels/rxlog_panel.py: Card classes'w-full'→'w-full flex-grow'(line 45); table classes'text-xs max-h-48 overflow-y-auto'→'w-full text-xs h-40 overflow-y-auto'(line 65)
Impact
- RX Log table now fills the panel consistently on both desktop and mobile viewports
- Layout is consistent with other panels (Messages, Contacts) that use the same
h-40responsive height pattern - No functional changes — all event handlers, callbacks, data bindings, logica and imports are identical to the input
[1.9.4] - 2026-02-16 — BLE Address Log Prefix & Entry Point Cleanup
Added
- ✅ BLE address prefix in log filename — Log file is now named
<BLE_ADDRESS>_meshcore_gui.log(e.g.AA_BB_CC_DD_EE_FF_meshcore_gui.log) instead of the genericmeshcore_gui.log. Makes it easy to identify which device produced which log file when running multiple instances- New helper
_sanitize_ble_address()stripsliteral:prefix and replaces colons with underscores - New function
configure_log_file(ble_address)updatesLOG_FILEat runtime before the logger is initialised - Rotated backups follow the same naming pattern automatically
- New helper
Removed
- ❌
meshcore_gui/meshcore_gui.py— Redundant copy ofmain()that was never imported. All three entry points (meshcore_gui.pyroot,__main__.py, andmeshcore_gui/meshcore_gui.py) contained near-identical copies of the same logic, causing changes to be missed (as demonstrated by this fix).__main__.pyis now the single source of truth; rootmeshcore_gui.pyis a thin wrapper that imports from it
Changed
- 🔄
config.py: Added_sanitize_ble_address()andconfigure_log_file(); version bumped to1.9.4 - 🔄
__main__.py: Addedconfig.configure_log_file(ble_address)call before any debug output - 🔄
meshcore_gui.py(root): Reduced to 4-line wrapper importingmainfrom__main__
Impact
- Log files are now identifiable per BLE device
- Single source of truth for
main()eliminates future sync issues between entry points - Both startup methods (
python meshcore_gui.pyandpython -m meshcore_gui) remain functional - No breaking changes — defaults and all existing behaviour unchanged
[1.9.3] - 2026-02-16 — Bugfix: Map Default Location & Payload Type Decoding
Fixed
- 🛠 Map centred on hardcoded Zwolle instead of device location — All Leaflet maps used magic-number coordinates
(52.5, 6.0)as initial centre and fallback. These are now replaced by a single configurable constantDEFAULT_MAP_CENTERinconfig.py. Once the device reports a validadv_lat/adv_lon, maps re-centre on the actual device position (existing behaviour, unchanged) - 🛠 Payload type shown as raw integer — Payload type is now retrieved from the decoded payload and translated to human-readable text using MeshCoreDecoder functions, instead of displaying the raw numeric type value
Changed
- 🔄
config.py: AddedDEFAULT_MAP_CENTER(default:(52.5168, 6.0830)) andDEFAULT_MAP_ZOOM(default:9) constants in new MAP DEFAULTS section. Version bumped to1.9.2 - 🔄
gui/panels/map_panel.py: ImportsDEFAULT_MAP_CENTERandDEFAULT_MAP_ZOOMfrom config;ui.leaflet(center=...)uses config constants instead of hardcoded values - 🔄
gui/route_page.py: ImportsDEFAULT_MAP_CENTERandDEFAULT_MAP_ZOOMfrom config; fallback coordinates (or 52.5/or 6.0) replaced byDEFAULT_MAP_CENTER[0]/[1]; zoom usesDEFAULT_MAP_ZOOM
Impact
- Map default location is now a single-point-of-change in
config.py - Payload type is displayed as readable text instead of a raw number
- No breaking changes — all existing map behaviour (re-centre on device position, contact markers) unchanged
[1.9.2] - 2026-02-15 — CLI Parameters & Cleanup
Added
- ✅
--port=PORTCLI parameter — Web server port is now configurable at startup (default:8081). Allows running multiple instances simultaneously on different ports - ✅
--ble-pin=PINCLI parameter — BLE pairing PIN is now configurable at startup (default:123456). Eliminates the need to editconfig.pyfor devices with a non-default PIN, and works in systemd service files - ✅ Per-device log file — Debug log file now includes the BLE address in its filename (e.g.
F0_9E_9E_75_A3_01_meshcore_gui.log), so multiple instances log to separate files
Fixed
- 🛠 BLE PIN not applied from CLI —
ble/worker.pyimportedBLE_PINas a constant at module load time (from config import BLE_PIN), capturing the default value"123456"before CLI parsing could overrideconfig.BLE_PIN. Changed to runtime access viaconfig.BLE_PINso the--ble-pinparameter is correctly passed to the BLE agent
Removed
- ❌ Redundant
meshcore_gui/meshcore_gui.py— This file was a near-identical copy of bothmeshcore_gui.py(top-level) andmeshcore_gui/__main__.py, but was never imported or referenced. Removed to eliminate maintenance risk. The two remaining entry points cover all startup methods:python meshcore_gui.pyandpython -m meshcore_gui
Impact
- Multiple instances can run side-by-side with different ports, PINs and log files
- Service deployments no longer require editing
config.py— all runtime settings via CLI - No breaking changes — all defaults are unchanged
[1.9.1] - 2026-02-14 — Bugfix: Dual Reconnect Conflict
Fixed
- 🛠 Library reconnect interfered with application reconnect — The meshcore library's internal
auto_reconnect(visible in logs as"Attempting reconnection 1/3") ran a fast 3-attempt reconnect cycle without bond cleanup. This prevented the application's ownreconnect_loop(which doesremove_bond()+ backoff) from succeeding, because BlueZ retained a stale bond →"failed to discover service"
Changed
- 🔄
ble/worker.py: Setauto_reconnect=Falsein bothMeshCore.create_ble()call sites (_connect()and_create_fresh_connection()), so only the application's bond-awarereconnect_loophandles reconnection - 🔄
ble/worker.py: Added"failed to discover"and"service discovery"to disconnect detection keywords for defensive coverage
Impact
- Eliminates the ~9 second wasted library reconnect cycle after every BLE disconnect
- Application's
reconnect_loop(with bond cleanup) now runs immediately after disconnect detection - No breaking changes — the application reconnect logic was already fully functional
[1.9.0] - 2026-02-14 — BLE Connection Stability
Added
- ✅ Built-in BLE PIN agent — New
ble/ble_agent.pyregisters a D-Bus agent with BlueZ to handle PIN pairing requests automatically. Eliminates the need for externalbt-agent.serviceandbluez-toolspackage- Uses
dbus_fast(already a dependency ofbleak, no new packages) - Supports
RequestPinCode,RequestPasskey,DisplayPasskey,RequestConfirmation,AuthorizeServicecallbacks - Configurable PIN via
BLE_PINinconfig.py(default:123456)
- Uses
- ✅ Automatic bond cleanup — New
ble/ble_reconnect.pyprovidesremove_bond()function that removes stale BLE bonds via D-Bus, equivalent tobluetoothctl remove <address>. Called automatically on startup and before each reconnect attempt - ✅ Automatic reconnect after disconnect — BLEWorker main loop now detects BLE disconnects (via connection error exceptions) and automatically triggers a reconnect sequence: bond removal → linear backoff wait → fresh connection → re-wire handlers → reload device data
- Configurable via
RECONNECT_MAX_RETRIES(default: 5) andRECONNECT_BASE_DELAY(default: 5.0s) - After all retries exhausted: waits 60s then starts a new retry cycle (infinite recovery)
- Configurable via
- ✅ Generic install script —
install_ble_stable.shauto-detects user, project directory, venv path and entry point to generate systemd service and D-Bus policy. Supports--uninstallflag
Changed
- 🔄
ble/worker.py—_async_main()rewritten with three phases: (1) start PIN agent, (2) remove stale bond, (3) connect + main loop with disconnect detection. Reconnect logic re-wires all event handlers and reloads device data after successful reconnection - 🔄
config.py— AddedBLE_PIN,RECONNECT_MAX_RETRIES,RECONNECT_BASE_DELAYconstants
Removed
- ❌
bt-agent.servicedependency — No longer needed; PIN pairing is handled by the built-in agent - ❌
bluez-toolssystem package — No longer needed - ❌
~/.meshcore-ble-pinfile — No longer needed - ❌ Manual
bluetoothctl removebefore startup — Handled automatically - ❌
ExecStartPrein systemd service — Bond cleanup is internal
Impact
- Zero external dependencies for BLE pairing on Linux
- Automatic recovery from the T1000e ~2 hour BLE disconnect issue
- No manual intervention needed after BLE connection loss
- Single systemd service (
meshcore-gui.service) manages everything - No breaking changes to existing functionality
[1.8.0] - 2026-02-14 — DRY Message Construction & Archive Layout Unification
Fixed
- 🛠 Case-sensitive prefix matching —
get_contact_name_by_prefix()andget_contact_by_prefix()inshared_data.pyfailed to match path hashes (uppercase, e.g.'B8') against contact pubkeys (lowercase, e.g.'b8a3f2...'). Added.lower()to both sides of the comparison, consistent with_resolve_path_names()which already had it - 🛠 Route page 404 from archive — Archive page linked to
/route/{hash}but route was registered as/route/{msg_index:int}, causing a JSON parse error for hex hash strings. Route parameter changed tostrwith 3-strategy lookup (index → memory hash → archive fallback) - 🛠 Three entry points out of sync —
meshcore_gui.py(root),meshcore_gui/meshcore_gui.py(inner) andmeshcore_gui/__main__.pyhad diverging route registrations. All three now use identical/route/{msg_key}withstrparameter
Changed
- 🔄
core/models.py— DRY factory methods and formattingMessage.now_timestamp(): static method replacing 7× hardcodeddatetime.now().strftime('%H:%M:%S')acrossevents.pyandcommands.pyMessage.incoming(): classmethod factory for received messages (direction='in', auto-timestamp)Message.outgoing(): classmethod factory for sent messages (sender='Me',direction='out', auto-timestamp)Message.format_line(channel_names): single-line display formatting ("12:34:56 ← [Public] [2h✓] PE1ABC: Hello mesh!"), replacing duplicate inline formatting inmessages_panel.pyandarchive_page.py
- 🔄
ble/events.py— 4×Message(...)constructors replaced byMessage.incoming();datetimeimport removed - 🔄
ble/commands.py— 3×Message(...)constructors replaced byMessage.outgoing();datetimeimport removed - 🔄
gui/panels/messages_panel.py— 15 lines inline formatting replaced by singlemsg.format_line(channel_names)call - 🔄
gui/archive_page.py— Layout unified with main page- Multi-row card layout replaced by single-line
msg.format_line()in monospace container (same style as main page) - DM added to channel filter dropdown (post-filter on
channel is None) - Message click opens
/route/{message_hash}in new tab (was: no click handler on archive messages) - Removed
_render_message_card()(98 lines) and_render_archive_route()(75 lines) - Removed
RouteBuilderdependency andTYPE_LABELSimport - File reduced from 445 to 267 lines
- Multi-row card layout replaced by single-line
- 🔄
gui/route_page.py—render(msg_index: int)→render(msg_key: str)with 3-strategy message lookup: (1) numeric index from in-memory list, (2) hash match in memory, (3)archive.get_message_by_hash()fallback - 🔄
services/message_archive.py— New methodget_message_by_hash(hash)for single-message lookup by packet hash - 🔄
__main__.py+meshcore_gui.py(both) — Route changed from/route/{msg_index}(int) to/route/{msg_key}(str)
Impact
- DRY: timestamp formatting 7→1 definition, message construction 7→2 factories, line formatting 2→1 method
- Archive page visually consistent with main messages panel (single-line, monospace)
- Archive messages now clickable to open route visualization (was: only in-memory messages)
- Case-insensitive prefix matching fixes path name resolution for contacts with uppercase path hashes
- No breaking changes to BLE protocol handling, dedup, bot, or data storage
Known Limitations
- DM filter in archive uses post-filtering (query without channel filter + filter on
channel is None); becomes exact whenquery_messages()gets native DM support
Parked for later
- Multi-path tracking (enrich RxLogEntry with multiple path observations)
- Events correlation improvements (only if proven data loss after
.lower()fix)
[1.7.0] - 2026-02-13 — Archive Channel Name Persistence
Added
- ✅ Channel name stored in archive — Messages now persist
channel_namealongside the numericchannelindex in<ADDRESS>_messages.json, so archived messages retain their human-readable channel name even when the device is not connectedMessagedataclass: new fieldchannel_name: str(default"", backward compatible)SharedData.add_message(): automatically resolveschannel_namefrom the live channels list when not already set (new helper_resolve_channel_name())MessageArchive.add_message(): writeschannel_nameto the JSON dict
- ✅ Archive channel selector built from archived data — Channel filter dropdown on
/archivenow populated viaSELECT DISTINCT channel_nameon the archive instead of the live BLE channels list- New method
MessageArchive.get_distinct_channel_names()returns sorted unique channel names from stored messages - Selector shows only channels that actually have archived messages
- New method
- ✅ Archive filter on channel name —
MessageArchive.query_messages()parameter changed fromchannel: Optional[int]tochannel_name: Optional[str](exact match on name string)
Changed
- 🔄
core/models.py: Addedchannel_namefield toMessagedataclass andfrom_dict() - 🔄
core/shared_data.py:add_message()resolves channel name; added_resolve_channel_name()helper - 🔄
services/message_archive.py:channel_namepersisted in JSON;query_messages()filters by name; newget_distinct_channel_names()method - 🔄
gui/archive_page.py: Channel selector built fromarchive.get_distinct_channel_names(); filter state changed from_channel_filter(int) to_channel_name_filter(str); message cards showchannel_namedirectly from archive
Fixed
- 🛠 Main page empty after startup — After a restart the messages panel showed no messages until new live BLE traffic arrived.
SharedData.load_recent_from_archive()now loads up to 100 recent archived messages during the cache-first startup phase, so historical messages are immediately visible- New method
SharedData.load_recent_from_archive(limit)— reads fromMessageArchive.query_messages()and populates the in-memory list without re-archiving BLEWorker._apply_cache()callsload_recent_from_archive()at the end of cache loading
- New method
Impact
- Archived messages now self-contained — channel name visible without live BLE connection
- Main page immediately shows historical messages after startup (no waiting for live BLE traffic)
- Backward compatible — old archive entries without
channel_namefall back to"Ch <idx>" - No breaking changes to existing functionality
[1.6.0] - 2026-02-13 — Dashboard Layout Consolidation
Changed
- 🔄 Messages panel consolidated — Filter checkboxes (DM + channels) and message input (text field, channel selector, Send button) are now integrated into the Messages panel, replacing the separate Filter and Input panels
- DM + channel checkboxes displayed centered in the Messages header row, between the "💬 Messages" label and the "📚 Archive" button
- Message input row (text field, channel selector, Send button) placed below the message list within the same card
messages_panel.py: Constructor now acceptsput_commandcallable; addedupdate_filters(data),update_channel_options(channels)methods andchannel_filters,last_channelsproperties (all logic 1:1 from FilterPanel/InputPanel);update()signature unchanged
- 🔄 Actions panel expanded — BOT toggle checkbox moved from Filter panel to Actions panel, below the Refresh/Advert buttons
actions_panel.py: Constructor now acceptsset_bot_enabledcallable; addedupdate(data)method for BOT state sync;_on_bot_toggle()logic 1:1 from FilterPanel
- 🔄 Dashboard layout simplified — Centre column reduced from 4 panels (Map → Input → Filter → Messages) to 2 panels (Map → Messages)
dashboard.py: FilterPanel and InputPanel no longer rendered; all dependencies rerouted to MessagesPanel and ActionsPanel;_update_ui()call-sites updated accordingly
Removed (from layout, files retained)
- ❌ Filter panel no longer rendered as separate panel —
filter_panel.pyretained in codebase but not instantiated in dashboard - ❌ Input panel no longer rendered as separate panel —
input_panel.pyretained in codebase but not instantiated in dashboard
Impact
- Cleaner, more compact dashboard: 2 fewer panels in the centre column
- All functionality preserved — message filtering, send, BOT toggle, archive all work identically
- No breaking changes to BLE, services, core or other panels
[1.5.0] - 2026-02-11 — Room Server Support, Dynamic Channel Discovery & Contact Management
Added
- ✅ Room Server panel — Dedicated per-room-server message panel in the centre column below Messages. Each Room Server (type=3 contact) gets its own
ui.card()with login/logout controls and message display- Click a Room Server contact to open an add/login dialog with password field
- After login: messages are displayed in the room card; send messages directly from the room panel
- Password row + login button automatically replaced by Logout button after successful login
- Room Server author attribution via
signaturefield (txt_type=2) — real message author is resolved from the 4-byte pubkey prefix, not the room server pubkey - New panel:
gui/panels/room_server_panel.py— per-room card management with login state tracking
- ✅ Room Server password store — Passwords stored outside the repository in
~/.meshcore-gui/room_passwords/<ADDRESS>.json- New service:
services/room_password_store.py— JSON-backed persistent password storage per BLE device, analogous toPinStore - Room panels are restored from stored passwords on app restart
- New service:
- ✅ Dynamic channel discovery — Channels are now auto-discovered from the device at startup via
get_channel()BLE probing, replacing the hardcodedCHANNELS_CONFIG- Single-attempt probe per channel slot with early stop after 2 consecutive empty slots
- Channel name and encryption key extracted in a single pass (combined discovery + key loading)
- Configurable channel caching via
CHANNEL_CACHE_ENABLED(default:False— always fresh from device) MAX_CHANNELSsetting (default: 8) controls how many slots are probed
- ✅ Individual contact deletion — 🗑️ delete button per unpinned contact in the contacts list, with confirmation dialog
- New command:
remove_single_contactin BLE command handler - Pinned contacts are protected (no delete button shown)
- New command:
- ✅ "Also delete from history" option — Checkbox in the Clean up confirmation dialog to also remove locally cached contact data
- ✅ Room Server protocol research —
RoomServer_Companion_App_Onderzoek.mddocuments the full companion app message flow (login, push protocol, signature mechanism, auto_message_fetching)
Changed
- 🔄
config.py: RemovedCHANNELS_CONFIGconstant; addedMAX_CHANNELS(default: 8) andCHANNEL_CACHE_ENABLED(default:False) - 🔄
ble/worker.py: Replaced hardcoded channel loading with_discover_channels()method; added_try_get_channel_info()helper;_apply_cache()respectsCHANNEL_CACHE_ENABLEDsetting; removed_load_channel_keys()(integrated into discovery pass) - 🔄
ble/commands.py: Addedlogin_room,send_room_msgandremove_single_contactcommand handlers - 🔄
gui/panels/contacts_panel.py: Contact click now dispatches by type — type=3 (Room Server) opens room dialog, others open DM dialog; addedon_add_roomcallback parameter; added 🗑️ delete button per unpinned contact - 🔄
gui/panels/messages_panel.py: Room Server messages filtered from general message view via_is_room_message()with prefix matching;update()acceptsroom_pubkeysparameter - 🔄
gui/dashboard.py: AddedRoomServerPanelin centre column;_update_ui()passesroom_pubkeysto Messages panel; added_on_add_room_servercallback - 🔄
gui/panels/filter_panel.py: Channel filter checkboxes now built dynamically from discovered channels (no hardcoded references) - 🔄
services/bot.py: Removed stale comment referencing hardcoded channels
Fixed
- 🛠 Room Server messages appeared as DM — Messages from Room Servers (txt_type=2) were displayed in the general Messages panel as direct messages. They are now filtered out and shown exclusively in the Room Server panel
- 🛠 Historical room messages not shown after login — Post-login fetch loop was polling
get_msg()before room server had time to push messages over LoRa RF (10–75s per message). Removed redundant fetch loop; the library'sauto_message_fetchinghandlesMESSAGES_WAITINGevents correctly and event-driven - 🛠 Author attribution incorrect for room messages — Room server messages showed the room server name as sender instead of the actual message author. Now correctly resolved from the
signaturefield (4-byte pubkey prefix) via contact lookup
Impact
- Room Servers are now first-class citizens in the GUI with dedicated panels
- Channel configuration no longer requires manual editing of
config.py - Contact list management is more granular with per-contact deletion
- No breaking changes to existing functionality (messages, DM, map, archive, bot, etc.)
[1.4.0] - 2026-02-09 — SDK Event Race Condition Fix
Fixed
- 🛠 BLE startup delay of ~2 minutes eliminated — The meshcore Python SDK (
commands/base.py) dispatched device response events beforewait_for_events()registered its subscription. On busy networks with frequentRX_LOG_DATAevents, this causedsend_device_query()andget_channel()to fail repeatedly withno_event_received, wasting 110+ seconds in timeouts
Changed
- 📄
meshcoreSDKcommands/base.py: Rewrittensend()method to subscribe to expected events before transmitting the BLE command (subscribe-before-send pattern), matching the approach used by the companion apps (meshcore.js, iOS, Android). Submitted upstream as meshcore_py PR #52
Impact
- Startup time reduced from ~2+ minutes to ~10 seconds on busy networks
- All BLE commands (
send_device_query,get_channel,get_bat,send_appstart, etc.) now succeed on first attempt instead of requiring multiple retries - No changes to meshcore_gui code required — the fix is entirely in the meshcore SDK
Temporary Installation
Until the fix is merged upstream, install the patched meshcore SDK:
pip install --force-reinstall git+https://github.com/PE1HVH/meshcore_py.git@fix/event-race-condition
[1.3.2] - 2026-02-09 — Bugfix: Bot Device Name Restoration After Restart
Fixed
- 🛠 Bot device name not properly restored after restart/crash — After a restart or crash with bot mode previously active, the original device name was incorrectly stored as the bot name (e.g.
NL-OV-ZWL-STDSHGN-WKC Bot) instead of the real device name (e.g.PE1HVH T1000e). The original device name is now correctly preserved and restored when bot mode is disabled
Changed
- 🔄
commands.py:set_bot_namehandler now verifies that the stored original name is not already the bot name before saving - 🔄
shared_data.py:original_device_nameis only written when it differs fromBOT_DEVICE_NAMEto prevent overwriting with the bot name on restart
[1.3.1] - 2026-02-09 — Bugfix: Auto-add AttributeError
Fixed
- 🛠 Auto-add error on first toggle — Setting auto-add for the first time raised
AttributeError: 'telemetry_mode_base'. Theset_manual_add_contacts()SDK call now handles missingtelemetry_mode_baseattribute gracefully
Changed
- 🔄
commands.py:set_auto_addhandler wrapsset_manual_add_contacts()call with attribute check and error handling for missingtelemetry_mode_base
[1.3.0] - 2026-02-08 — Bot Device Name Management
Added
- ✅ Bot device name switching — When the BOT checkbox is enabled, the device name is automatically changed to a configurable bot name; when disabled, the original name is restored
- Original device name is saved before renaming so it can be restored on BOT disable
- Device name written to device via BLE
set_name()SDK call - Graceful handling of BLE failures during name change
- ✅
BOT_DEVICE_NAMEconstant inconfig.py— Configurable fixed device name used when bot mode is active (default:;NL-OV-ZWL-STDSHGN-WKC Bot)
Changed
- 🔄
config.py: AddedBOT_DEVICE_NAMEconstant for bot mode device name - 🔄
bot.py: Removed hardcodedBOT_NAMEprefix ("Zwolle Bot") from bot reply messages — bot replies no longer include a name prefix - 🔄
filter_panel.py: BOT checkbox toggle now triggers device name save/rename via command queue - 🔄
commands.py: Addedset_bot_nameandrestore_namecommand handlers for device name switching - 🔄
shared_data.py: Addedoriginal_device_namefield for storing the pre-bot device name
Removed
- ❌
BOT_NAMEconstant frombot.py— bot reply prefix removed; replies no longer prepend a bot display name
[1.2.0] - 2026-02-08 — Contact Maintenance Feature
Added
-
✅ Pin/Unpin contacts (Iteration A) — Toggle to pin individual contacts, protecting them from bulk deletion
- Persistent pin state stored in
~/.meshcore-gui/cache/<ADDRESS>_pins.json - Pinned contacts visually marked with yellow background
- Pinned contacts sorted to top of contact list
- Pin state survives app restart
- New service:
services/pin_store.py— JSON-backed persistent pin storage
- Persistent pin state stored in
-
✅ Bulk delete unpinned contacts (Iteration B) — Remove all unpinned contacts from device in one action
- "🧹 Clean up" button in contacts panel with confirmation dialog
- Shows count of contacts to be removed vs. pinned contacts kept
- Progress status updates during removal
- Automatic device resync after completion
- New service:
services/contact_cleaner.py— ContactCleanerService with purge statistics
-
✅ Auto-add contacts toggle (Iteration C) — Control whether device automatically adds new contacts from mesh adverts
- "📥 Auto-add" checkbox in contacts panel (next to Clean up button)
- Syncs with device via
set_manual_add_contacts()SDK call - Inverted logic handled internally (UI "Auto-add ON" =
set_manual_add_contacts(false)) - Optimistic update with automatic rollback on BLE failure
- State synchronized from device on each GUI update cycle
Changed
- 🔄
contacts_panel.py: Added pin checkbox per contact, purge button, auto-add toggle, DM dialog (all existing functionality preserved) - 🔄
commands.py: Addedpurge_unpinnedandset_auto_addcommand handlers - 🔄
shared_data.py: Addedauto_add_enabledfield with thread-safe getter/setter - 🔄
protocols.py: Addedset_auto_add_enabledandis_auto_add_enabledto Writer and Reader protocols - 🔄
dashboard.py: PassesPinStoreandset_auto_add_enabledcallback to ContactsPanel - 🔄 UI language: All Dutch strings in
contacts_panel.pyandcommands.pytranslated to English
Fixed
- 🛠 Route table names and IDs not displayed — Route tables in both current messages (RoutePage) and archive messages (ArchivePage) now correctly show node names and public key IDs for sender, repeaters and receiver
Changed
- 🔄 CHANGELOG.md: Corrected version numbering to semantic versioning, fixed inaccurate references (archive button location, filter state persistence)
- 🔄 README.md: Added Message Archive feature, updated project structure, configuration table and architecture diagram
- 🔄 MeshCore_GUI_Design.docx: Added ArchivePage, MessageArchive, Models components; updated project structure, protocols, configuration and version history
[1.1.0] - 2026-02-07 — Archive Viewer Feature
Added
- ✅ Archive Viewer Page (
/archive) — Full-featured message archive browser- Pagination (50 messages per page, configurable)
- Channel filter dropdown (All + configured channels)
- Time range filter (24h, 7d, 30d, 90d, All time)
- Text search (case-insensitive)
- Filter state stored in instance variables (reset on page reload)
- Message cards with same styling as main messages panel
- Clickable messages for route visualization (where available)
- 💬 Reply functionality — Expandable reply panel per message
- 🗺️ Inline route table — Expandable route display per archive message with sender, repeaters and receiver (names, IDs, node types)
- (Note: Reply panels and inline route tables removed in v1.8.0, replaced by click-to-route navigation via message hash)
-
✅ MessageArchive.query_messages() method
- Filter by: time range, channel, text search, sender
- Pagination support (limit, offset)
- Returns tuple: (messages, total_count)
- Sorting: Newest first
-
✅ UI Integration
- "📚 Archive" button in Messages panel header (opens in new tab)
- Back to Dashboard button in archive page
- ✅ Reply Panel
- Expandable reply per message (💬 Reply button)
- Pre-filled with @sender mention
- Channel selector
- Send button with success notification
- Auto-close expansion after send
Changed
- 🔄
SharedData.get_snapshot(): Now includes'archive'field - 🔄
MessagesPanel: Added archive button in header row - 🔄 Both entry points (
__main__.pyandmeshcore_gui.py): Register/archiveroute
Performance
- Query: ~10ms for 10k messages with filters
- Memory: ~10KB per page (50 messages)
- No impact on main UI (separate page)
Known Limitations
Route visualization only works for messages in recent buffer (last 100)— Fixed in v1.8.0: archive messages now support click-to-route viaget_message_by_hash()fallback- Text search is linear scan (no indexing yet)
- Sender filter exists in API but not in UI yet
[1.0.3] - 2026-02-07 — Critical Bugfix: Archive Overwrite Prevention
Fixed
- 🛠 CRITICAL: Fixed bug where archive was overwritten instead of appended on restart
- 🛠 Archive now preserves existing data when read errors occur
- 🛠 Buffer is retained for retry if existing archive cannot be read
Changed
- 🔄
_flush_messages(): Early return on read error instead of overwriting - 🔄
_flush_rxlog(): Early return on read error instead of overwriting - 🔄 Better error messages for version mismatch and JSON decode errors
Details
Problem: If the existing archive file had a JSON parse error or version mismatch,
the flush operation would proceed with existing_messages = [], effectively
overwriting all historical data with only the new buffered messages.
Solution: The flush methods now:
- Try to read existing archive first
- If read fails (JSON error, version mismatch, IO error), abort the flush
- Keep buffer intact for next retry
- Only clear buffer after successful write
Impact: No data loss on restart or when archive files have issues.
Testing
- ✅ Added
test_append_on_restart_not_overwrite()integration test - ✅ Verifies data is appended across multiple sessions
- ✅ All existing tests still pass
[1.0.2] - 2026-02-07 — RxLog message_hash Enhancement
Added
- ✅
message_hashfield added toRxLogEntrymodel - ✅ RxLog entries now include message_hash for correlation with messages
- ✅ Archive JSON includes message_hash in rxlog entries
Changed
- 🔄
events.py: Restructuredon_rx_log()to extract message_hash before creating RxLogEntry - 🔄
message_archive.py: Updated rxlog archiving to include message_hash field - 🔄 Tests updated to verify message_hash persistence
Benefits
- Correlation: Link RX log entries to their corresponding messages
- Analysis: Track which packets resulted in messages
- Debugging: Better troubleshooting of packet processing
[1.0.1] - 2026-02-07 — Entry Point Fix
Fixed
- ✅
meshcore_gui.py(root entry point) now passes ble_address to SharedData - ✅ Archive works correctly regardless of how application is started
Changed
- 🔄 Both entry points (
meshcore_gui.pyandmeshcore_gui/__main__.py) updated
[1.0.0] - 2026-02-07 — Message & Metadata Persistence
Added
- ✅ MessageArchive class for persistent storage
- ✅ Configurable retention periods (MESSAGE_RETENTION_DAYS, RXLOG_RETENTION_DAYS, CONTACT_RETENTION_DAYS)
- ✅ Automatic daily cleanup of old data
- ✅ Batch writes for performance
- ✅ Thread-safe with separate locks
- ✅ Atomic file writes
- ✅ Contact retention in DeviceCache
- ✅ Archive statistics API
- ✅ Comprehensive tests (20+ unit, 8+ integration)
- ✅ Full documentation
Storage Locations
~/.meshcore-gui/archive/<ADDRESS>_messages.json~/.meshcore-gui/archive/<ADDRESS>_rxlog.json
Requirements Completed
-
R1: All incoming messages persistent ✅
-
R2: All incoming RxLog entries persistent ✅
-
R3: Configurable retention ✅
-
R4: Automatic cleanup ✅
-
R5: Backward compatibility ✅
-
R6: Contact retention ✅
-
R7: Archive stats API ✅
-
Fix3: Leaflet asset injection is now per page render instead of process-global, and browser bootstrap now retries until the host element, Leaflet runtime, and MeshCore panel runtime are all available. This fixes blank map containers caused by missing or late-loaded JS/CSS assets.
-
Fix5: Removed per-snapshot map invalidate calls, stopped forcing a default dark theme during map bootstrap, and added client-side interaction/resize guards so zooming stays responsive and the theme no longer jumps back during status-loop updates.
2026-03-09 map hotfix v2
- regular map snapshots no longer carry theme state
- explicit theme changes are now handled only via the dedicated theme channel
- initial map render now sends an ensure_map command plus an immediate theme sync
- added no-op ensure_map handling in the Leaflet runtime to avoid accidental fallback behaviour