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 9ac3ac6..4b6b824 100644 Binary files a/docs/MeshCore_GUI_Design.docx and b/docs/MeshCore_GUI_Design.docx differ 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()