From 54943d0f47bdefdc19fc512fe8218462c6fea85b Mon Sep 17 00:00:00 2001 From: pe1hvh Date: Mon, 9 Feb 2026 15:33:10 +0100 Subject: [PATCH] BotName+BugFixes --- CHANGELOG.md | 56 ++++++++++++++-- README.md | 26 +++++--- docs/.~lock.MeshCore_GUI_Design.docx# | 1 + docs/MeshCore_GUI_Design.docx | Bin 16304 -> 16427 bytes install_venv.sh | 4 ++ meshcore_gui/ble/commands.py | 83 +++++++++++++++++++++++- meshcore_gui/ble/worker.py | 8 ++- meshcore_gui/config.py | 13 ++++ meshcore_gui/core/protocols.py | 3 + meshcore_gui/core/shared_data.py | 23 +++++++ meshcore_gui/gui/dashboard.py | 2 +- meshcore_gui/gui/panels/filter_panel.py | 18 ++++- meshcore_gui/services/bot.py | 6 +- meshcore_gui/services/cache.py | 16 +++++ 14 files changed, 236 insertions(+), 23 deletions(-) create mode 100644 docs/.~lock.MeshCore_GUI_Design.docx# create mode 100755 install_venv.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 95bea4e..fd12201 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,53 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver --- - + + +## [5.5.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_name` handler now verifies that the stored original name is not already the bot name before saving +- 🔄 `shared_data.py`: `original_device_name` is only written when it differs from `BOT_DEVICE_NAME` to prevent overwriting with the bot name on restart + +--- + + + +## [5.5.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'`. The `set_manual_add_contacts()` SDK call now handles missing `telemetry_mode_base` attribute gracefully + +### Changed +- 🔄 `commands.py`: `set_auto_add` handler wraps `set_manual_add_contacts()` call with attribute check and error handling for missing `telemetry_mode_base` + +--- + + + +## [5.5.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_NAME` constant** in `config.py` — Configurable fixed device name used when bot mode is active (default: `;NL-OV-ZWL-STDSHGN-WKC Bot`) + +### Changed +- 🔄 `config.py`: Added `BOT_DEVICE_NAME` constant for bot mode device name +- 🔄 `bot.py`: Removed hardcoded `BOT_NAME` prefix ("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`: Added `set_bot_name` and `restore_name` command handlers for device name switching +- 🔄 `shared_data.py`: Added `original_device_name` field for storing the pre-bot device name + +### Removed +- ❌ `BOT_NAME` constant from `bot.py` — bot reply prefix removed; replies no longer prepend a bot display name + +--- ## [5.4.0] - 2026-02-08 — Contact Maintenance Feature @@ -45,7 +91,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver --- ### 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 +- 🛠 **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 (v1.0.x → v5.x), fixed inaccurate references (archive button location, filter state persistence) @@ -124,9 +170,9 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver ### 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 +- 🛠 **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 diff --git a/README.md b/README.md index d2b990f..e60e134 100644 --- a/README.md +++ b/README.md @@ -198,8 +198,10 @@ The GUI opens automatically in your browser at `http://localhost:8080` | `CONTACT_RETENTION_DAYS` | `meshcore_gui/config.py` | Retention period for cached contacts (default: 90 days) | | `KEY_RETRY_INTERVAL` | `meshcore_gui/ble/worker.py` | Interval between background retry attempts for missing channel keys (default: 30s) | +| `BOT_DEVICE_NAME` | `meshcore_gui/config.py` | Device name set when bot mode is active (default: `;NL-OV-ZWL-STDSHGN-WKC Bot`) | + | `BOT_CHANNELS` | `meshcore_gui/services/bot.py` | Channel indices the bot listens on | -| `BOT_NAME` | `meshcore_gui/services/bot.py` | Display name prepended to bot replies | + | `BOT_COOLDOWN_SECONDS` | `meshcore_gui/services/bot.py` | Minimum seconds between bot replies | | `BOT_KEYWORDS` | `meshcore_gui/services/bot.py` | Keyword → reply template mapping | | BLE Address | Command line argument | | @@ -290,20 +292,25 @@ If BLE connection fails, the GUI remains usable with cached data and shows an of The built-in bot automatically replies to messages containing recognised keywords. Enable or disable it via the 🤖 BOT checkbox in the filter bar. + +**Device name switching:** When the BOT checkbox is enabled, the device name is automatically changed to the configured `BOT_DEVICE_NAME` (default: `;NL-OV-ZWL-STDSHGN-WKC Bot`). The original device name is saved and restored when bot mode is disabled. This allows the mesh network to identify the node as a bot by its name. + **Default keywords:** + + | Keyword | Reply | |---------|-------| -| `test` | `Zwolle Bot: , rcvd \| SNR \| path(); ` | -| `ping` | `Zwolle Bot: Pong!` | -| `help` | `Zwolle Bot: test, ping, help` | +| `test` | `, rcvd \| SNR \| path(); ` | +| `ping` | `Pong!` | +| `help` | `test, ping, help` | **Safety guards:** - Only replies on configured channels (`BOT_CHANNELS`) - Ignores own messages and messages from other bots (names ending in "Bot") - Cooldown period between replies (default: 5 seconds) -**Customisation:** Edit `BOT_KEYWORDS` in `meshcore_gui/services/bot.py`. Templates support `{bot}`, `{sender}`, `{snr}` and `{path}` variables. +**Customisation:** Edit `BOT_KEYWORDS` in `meshcore_gui/services/bot.py`. Templates support `{sender}`, `{snr}` and `{path}` variables. ### RX Log - Received packets with SNR and type @@ -353,10 +360,12 @@ The built-in bot automatically replies to messages containing recognised keyword ``` - **BLEWorker**: Runs in separate thread with its own asyncio loop, with background retry for missing channel keys -- **CommandHandler**: Executes commands (send message, advert, refresh, purge unpinned, set auto-add) +- **CommandHandler**: Executes commands (send message, advert, refresh, purge unpinned, set auto-add, set bot name, restore name) + - **EventHandler**: Processes incoming BLE events (messages, RX log) - **PacketDecoder**: Decodes raw LoRa packets and extracts route data -- **MeshBot**: Keyword-triggered auto-reply on configured channels +- **MeshBot**: Keyword-triggered auto-reply on configured channels with automatic device name switching + - **DualDeduplicator**: Prevents duplicate messages (hash-based + content-based) - **DeviceCache**: Local JSON cache per device for instant startup and offline resilience - **MessageArchive**: Persistent storage for messages and RX log with configurable retention and automatic cleanup @@ -462,7 +471,8 @@ meshcore-gui/ ├── meshcore_gui/ # Application package │ ├── __init__.py │ ├── __main__.py # Alternative entry: python -m meshcore_gui -│ ├── config.py # DEBUG flag, channel configuration, refresh interval, retention settings +│ ├── config.py # DEBUG flag, channel configuration, refresh interval, retention settings, BOT_DEVICE_NAME + │ ├── ble/ # BLE communication layer │ │ ├── __init__.py │ │ ├── worker.py # BLE thread, connection lifecycle, cache-first startup, background key retry diff --git a/docs/.~lock.MeshCore_GUI_Design.docx# b/docs/.~lock.MeshCore_GUI_Design.docx# new file mode 100644 index 0000000..1daeb6c --- /dev/null +++ b/docs/.~lock.MeshCore_GUI_Design.docx# @@ -0,0 +1 @@ +,hans,hans-NLx0AU,09.02.2026 15:24,file:///home/hans/.config/libreoffice/4; \ No newline at end of file diff --git a/docs/MeshCore_GUI_Design.docx b/docs/MeshCore_GUI_Design.docx index 9ac3ac6e1d26f9507b79c7c9b69f517203ae7fae..4b6b824107288ae4fda33f31dafa36ff503aa1bf 100644 GIT binary patch literal 16427 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 X@=}mc|NIU1^X>QP-=BQ{{QCa@Us-rP literal 16304 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 diff --git a/install_venv.sh b/install_venv.sh new file mode 100755 index 0000000..689f490 --- /dev/null +++ b/install_venv.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +python3 -m venv venv +source venv/bin/activate +pip install nicegui meshcore bleak meshcoredecoder diff --git a/meshcore_gui/ble/commands.py b/meshcore_gui/ble/commands.py index 4090ad2..782a421 100644 --- a/meshcore_gui/ble/commands.py +++ b/meshcore_gui/ble/commands.py @@ -12,9 +12,10 @@ from typing import Dict, List, Optional from meshcore import MeshCore, EventType -from meshcore_gui.config import debug_print +from meshcore_gui.config import BOT_DEVICE_NAME, DEVICE_NAME, debug_print from meshcore_gui.core.models import Message from meshcore_gui.core.protocols import SharedDataWriter +from meshcore_gui.services.cache import DeviceCache class CommandHandler: @@ -23,11 +24,18 @@ class CommandHandler: Args: mc: Connected MeshCore instance. shared: SharedDataWriter for storing results. + cache: DeviceCache for persistent storage. """ - def __init__(self, mc: MeshCore, shared: SharedDataWriter) -> None: + def __init__( + self, + mc: MeshCore, + shared: SharedDataWriter, + cache: Optional[DeviceCache] = None, + ) -> None: self._mc = mc self._shared = shared + self._cache = cache # Handler registry — add new commands here (OCP) self._handlers: Dict[str, object] = { @@ -37,6 +45,7 @@ class CommandHandler: 'refresh': self._cmd_refresh, 'purge_unpinned': self._cmd_purge_unpinned, 'set_auto_add': self._cmd_set_auto_add, + 'set_device_name': self._cmd_set_device_name, } async def process_all(self) -> None: @@ -191,6 +200,12 @@ class CommandHandler: On failure the SharedData flag is rolled back so the GUI checkbox reverts on the next update cycle. + Note: some firmware/SDK versions raise ``KeyError`` (e.g. + ``'telemetry_mode_base'``) when parsing the device response. + The BLE command itself was already sent successfully in that + case, so we treat ``KeyError`` as *probable success* and keep + the requested state instead of rolling back. + Expected command dict:: { @@ -201,6 +216,7 @@ class CommandHandler: enabled: bool = cmd.get('enabled', False) # Invert: UI "auto-add ON" → manual_add = False manual_add = not enabled + state = "ON" if enabled else "OFF" try: r = await self._mc.commands.set_manual_add_contacts(manual_add) @@ -216,9 +232,18 @@ class CommandHandler: ) else: self._shared.set_auto_add_enabled(enabled) - state = "ON" if enabled else "OFF" self._shared.set_status(f"✅ Auto-add contacts: {state}") debug_print(f"set_auto_add: success → {state}") + except KeyError as exc: + # SDK response-parsing error (e.g. missing 'telemetry_mode_base'). + # The BLE command was already transmitted; the device has likely + # accepted the new setting. Keep the requested state. + self._shared.set_auto_add_enabled(enabled) + self._shared.set_status(f"✅ Auto-add contacts: {state}") + debug_print( + f"set_auto_add: KeyError '{exc}' during response parse — " + f"command sent, treating as success → {state}" + ) except Exception as exc: # Rollback self._shared.set_auto_add_enabled(not enabled) @@ -227,6 +252,58 @@ class CommandHandler: ) debug_print(f"set_auto_add exception: {exc}") + async def _cmd_set_device_name(self, cmd: Dict) -> None: + """Set or restore the device name when BOT is toggled. + + Uses the fixed names from config.py: + - BOT enabled → ``BOT_DEVICE_NAME`` (e.g. "NL-OV-ZWL-STDSHGN-WKC Bot") + - BOT disabled → ``DEVICE_NAME`` (e.g. "PE1HVH T1000e") + + This avoids the previous bug where the dynamically read device + name could already be the bot name (e.g. after a restart while + BOT was active), causing the original name to be overwritten + with the bot name. + + On failure the bot_enabled flag is rolled back so the GUI + checkbox reverts on the next update cycle. + + Expected command dict:: + + { + 'action': 'set_device_name', + 'bot_enabled': True/False, + } + """ + bot_enabled: bool = cmd.get('bot_enabled', False) + target_name = BOT_DEVICE_NAME if bot_enabled else DEVICE_NAME + + try: + r = await self._mc.commands.set_name(target_name) + if r.type == EventType.ERROR: + # Rollback: revert bot flag to previous state + self._shared.set_bot_enabled(not bot_enabled) + self._shared.set_status( + f"⚠️ Failed to set device name to '{target_name}'" + ) + debug_print( + f"set_device_name: ERROR response for '{target_name}', " + f"rolled back bot_enabled to {not bot_enabled}" + ) + return + + self._shared.set_status(f"✅ Device name → {target_name}") + debug_print(f"set_device_name: success → '{target_name}'") + + # Send advert so the network sees the new name + await self._mc.commands.send_advert(flood=True) + debug_print("set_device_name: advert sent") + + except Exception as exc: + # Rollback on exception + self._shared.set_bot_enabled(not bot_enabled) + self._shared.set_status(f"⚠️ Device name error: {exc}") + debug_print(f"set_device_name exception: {exc}") + # ------------------------------------------------------------------ # Callback for refresh (set by BLEWorker after construction) # ------------------------------------------------------------------ diff --git a/meshcore_gui/ble/worker.py b/meshcore_gui/ble/worker.py index 6eaf250..9634cfe 100644 --- a/meshcore_gui/ble/worker.py +++ b/meshcore_gui/ble/worker.py @@ -154,7 +154,7 @@ class BLEWorker: dedup=self._dedup, bot=self._bot, ) - self._cmd_handler = CommandHandler(mc=self.mc, shared=self.shared) + self._cmd_handler = CommandHandler(mc=self.mc, shared=self.shared, cache=self._cache) self._cmd_handler.set_load_data_callback(self._load_data) # Subscribe to events @@ -228,6 +228,12 @@ class BLEWorker: except (ValueError, TypeError) as exc: debug_print(f"Cache → bad channel key [{idx_str}]: {exc}") + # Restore original device name (if BOT was active when app closed) + cached_orig_name = self._cache.get_original_device_name() + if cached_orig_name: + self.shared.set_original_device_name(cached_orig_name) + debug_print(f"Cache → original device name: {cached_orig_name}") + # ------------------------------------------------------------------ # Initial data loading (refreshes cache) # ------------------------------------------------------------------ diff --git a/meshcore_gui/config.py b/meshcore_gui/config.py index 7bee9cc..43842d6 100644 --- a/meshcore_gui/config.py +++ b/meshcore_gui/config.py @@ -44,6 +44,19 @@ CHANNELS_CONFIG: List[Dict] = [ ] +# ============================================================================== +# BOT DEVICE NAME +# ============================================================================== + +# Fixed device name applied when the BOT checkbox is enabled. +# The original device name is saved and restored when BOT is disabled. +BOT_DEVICE_NAME: str = "NL-OV-ZWL-STDSHGN-WKC Bot" + +# Default device name used as fallback when restoring from BOT mode +# and no original name was saved (e.g. after a restart). +DEVICE_NAME: str = "PE1HVH T1000e" + + # ============================================================================== # CACHE / REFRESH # ============================================================================== diff --git a/meshcore_gui/core/protocols.py b/meshcore_gui/core/protocols.py index da40fe7..4325a76 100644 --- a/meshcore_gui/core/protocols.py +++ b/meshcore_gui/core/protocols.py @@ -61,6 +61,9 @@ class SharedDataWriter(Protocol): def put_command(self, cmd: Dict) -> None: ... def set_auto_add_enabled(self, enabled: bool) -> None: ... def is_auto_add_enabled(self) -> bool: ... + def set_original_device_name(self, name: Optional[str]) -> None: ... + def get_original_device_name(self) -> Optional[str]: ... + def get_device_name(self) -> str: ... # ---------------------------------------------------------------------- diff --git a/meshcore_gui/core/shared_data.py b/meshcore_gui/core/shared_data.py index de99ea6..e0d59b9 100644 --- a/meshcore_gui/core/shared_data.py +++ b/meshcore_gui/core/shared_data.py @@ -65,6 +65,9 @@ class SharedData: # Auto-add contacts flag (synced with device) self.auto_add_enabled: bool = False + # Original device name (saved when BOT is enabled, restored when disabled) + self.original_device_name: Optional[str] = None + # Message archive (persistent storage) self.archive: Optional[MessageArchive] = None if ble_address: @@ -139,6 +142,26 @@ class SharedData: with self.lock: return self.auto_add_enabled + # ------------------------------------------------------------------ + # Original device name (BOT feature) + # ------------------------------------------------------------------ + + def set_original_device_name(self, name: Optional[str]) -> None: + """Store the original device name before BOT rename (thread-safe).""" + with self.lock: + self.original_device_name = name + debug_print(f"Original device name stored: {name}") + + def get_original_device_name(self) -> Optional[str]: + """Get the stored original device name (thread-safe).""" + with self.lock: + return self.original_device_name + + def get_device_name(self) -> str: + """Get the current device name (thread-safe).""" + with self.lock: + return self.device.name + # ------------------------------------------------------------------ # Command queue # ------------------------------------------------------------------ diff --git a/meshcore_gui/gui/dashboard.py b/meshcore_gui/gui/dashboard.py index 7898b2a..b69a983 100644 --- a/meshcore_gui/gui/dashboard.py +++ b/meshcore_gui/gui/dashboard.py @@ -74,7 +74,7 @@ class DashboardPage: self._contacts = ContactsPanel(put_cmd, self._pin_store, self._shared.set_auto_add_enabled) self._map = MapPanel() self._input = InputPanel(put_cmd) - self._filter = FilterPanel(self._shared.set_bot_enabled) + self._filter = FilterPanel(self._shared.set_bot_enabled, put_cmd) self._messages = MessagesPanel() self._actions = ActionsPanel(put_cmd) self._rxlog = RxLogPanel() diff --git a/meshcore_gui/gui/panels/filter_panel.py b/meshcore_gui/gui/panels/filter_panel.py index 5901f6c..3cd4bc8 100644 --- a/meshcore_gui/gui/panels/filter_panel.py +++ b/meshcore_gui/gui/panels/filter_panel.py @@ -10,10 +10,16 @@ class FilterPanel: Args: set_bot_enabled: Callable to toggle the bot in SharedData. + put_command: Callable to enqueue a BLE command. """ - def __init__(self, set_bot_enabled: Callable[[bool], None]) -> None: + def __init__( + self, + set_bot_enabled: Callable[[bool], None], + put_command: Callable[[dict], None], + ) -> None: self._set_bot_enabled = set_bot_enabled + self._put_command = put_command self._container = None self._bot_checkbox = None self._channel_filters: Dict = {} @@ -35,6 +41,14 @@ class FilterPanel: ui.label('📻 Filter:').classes('text-sm text-gray-600') self._container = ui.row().classes('gap-4') + def _on_bot_toggle(self, value: bool) -> None: + """Handle BOT checkbox toggle: update flag and queue name change.""" + self._set_bot_enabled(value) + self._put_command({ + 'action': 'set_device_name', + 'bot_enabled': value, + }) + def update(self, data: Dict) -> None: """Rebuild checkboxes when channel data changes.""" if not self._container or not data['channels']: @@ -47,7 +61,7 @@ class FilterPanel: self._bot_checkbox = ui.checkbox( '🤖 BOT', value=data.get('bot_enabled', False), - on_change=lambda e: self._set_bot_enabled(e.value), + on_change=lambda e: self._on_bot_toggle(e.value), ) ui.label('│').classes('text-gray-300') diff --git a/meshcore_gui/services/bot.py b/meshcore_gui/services/bot.py index 96bb11e..60ccdfa 100644 --- a/meshcore_gui/services/bot.py +++ b/meshcore_gui/services/bot.py @@ -37,9 +37,9 @@ BOT_COOLDOWN_SECONDS: float = 5.0 # The bot checks whether the incoming message text *contains* the keyword # (case-insensitive). First match wins. BOT_KEYWORDS: Dict[str, str] = { - 'test': '{bot}: {sender}, rcvd | SNR {snr} | {path}', - 'ping': '{bot}: Pong!', - 'help': '{bot}: test, ping, help', + 'test': '{sender}, rcvd | SNR {snr} | {path}', + 'ping': 'Pong!', + 'help': 'test, ping, help', } diff --git a/meshcore_gui/services/cache.py b/meshcore_gui/services/cache.py index 7fca813..a5a2a3c 100644 --- a/meshcore_gui/services/cache.py +++ b/meshcore_gui/services/cache.py @@ -242,3 +242,19 @@ class DeviceCache: def get_last_updated(self) -> Optional[str]: """Return ISO timestamp of last cache update, or None.""" return self._data.get("last_updated") + + # ------------------------------------------------------------------ + # Original device name (BOT feature) + # ------------------------------------------------------------------ + + def get_original_device_name(self) -> Optional[str]: + """Return cached original device name, or None.""" + return self._data.get("original_device_name") + + def set_original_device_name(self, name: Optional[str]) -> None: + """Store or clear the original device name and persist to disk.""" + if name is None: + self._data.pop("original_device_name", None) + else: + self._data["original_device_name"] = name + self.save()