Merge pull request #6 from pe1hvh/BotName

BotName+BugFixes
This commit is contained in:
pe1hvh
2026-02-09 16:06:03 +01:00
committed by GitHub
14 changed files with 236 additions and 23 deletions

View File

@@ -8,7 +8,53 @@ Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Ver
---
<!-- ADDED: Nieuw v5.3.0 entry bovenaan -->
<!-- ADDED: v5.5.2 bugfix entry -->
## [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
---
<!-- ADDED: v5.5.1 bugfix entry -->
## [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`
---
<!-- ADDED: Nieuw v5.5.0 entry bovenaan -->
## [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
<!-- CHANGED: Versienummer gewijzigd van v1.0.3 naar v5.1.3 -->
### 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

View File

@@ -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) |
<!-- ADDED: Three retention settings above were missing from config table -->
| `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`) |
<!-- ADDED: BOT_DEVICE_NAME setting added in v5.5.0 -->
| `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 |
<!-- CHANGED: BOT_NAME removed in v5.5.0 — bot replies no longer include a name prefix -->
| `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.
<!-- CHANGED: Bot device name switching feature added in v5.5.0 -->
**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:**
<!-- CHANGED: Removed "Zwolle Bot:" prefix from example replies — bot replies no longer include a name prefix (v5.5.0) -->
| Keyword | Reply |
|---------|-------|
| `test` | `Zwolle Bot: <sender>, rcvd \| SNR <snr> \| path(<hops>); <repeaters>` |
| `ping` | `Zwolle Bot: Pong!` |
| `help` | `Zwolle Bot: test, ping, help` |
| `test` | `<sender>, rcvd \| SNR <snr> \| path(<hops>); <repeaters>` |
| `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)
<!-- CHANGED: Added set bot name and restore name commands (v5.5.0) -->
- **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
<!-- CHANGED: Added device name switching to MeshBot description (v5.5.0) -->
- **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
<!-- CHANGED: Added BOT_DEVICE_NAME to config.py description (v5.5.0) -->
│ ├── ble/ # BLE communication layer
│ │ ├── __init__.py
│ │ ├── worker.py # BLE thread, connection lifecycle, cache-first startup, background key retry

View File

@@ -0,0 +1 @@
,hans,hans-NLx0AU,09.02.2026 15:24,file:///home/hans/.config/libreoffice/4;

Binary file not shown.

4
install_venv.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env bash
python3 -m venv venv
source venv/bin/activate
pip install nicegui meshcore bleak meshcoredecoder

View File

@@ -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)
# ------------------------------------------------------------------

View File

@@ -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)
# ------------------------------------------------------------------

View File

@@ -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
# ==============================================================================

View File

@@ -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: ...
# ----------------------------------------------------------------------

View File

@@ -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
# ------------------------------------------------------------------

View File

@@ -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()

View File

@@ -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')

View File

@@ -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',
}

View File

@@ -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()