- Replace global _last_reply float with _last_reply_per_sender dict.
A reply to one node no longer blocks all other senders for 5 s.
LRU eviction keeps the dict bounded at 200 entries.
- _get_active_channels() now falls back to BotConfig defaults when
the stored channel set is empty (user never saved a selection).
Bot was silently deaf on first run despite the panel showing all
channels pre-checked.
Closes: bot only replies to first sender in multi-node #test session.
fix(packet_decoder): brute-force channel resolution when hash lookup fails
ChannelCrypto.calculate_channel_hash() and the MeshCore firmware compute
different channel identifiers for the same secret, causing _hash_to_idx to
return None for all hashtag-channel messages. Fallback: try each registered
key individually via a single-key keystore. First valid decryption wins.
Result cached in _hash_to_idx for O(1) resolution on subsequent packets.
fix(events): channel-agnostic sentinel prevents cross-channel duplicates
on_rx_log now marks '*' in DualDeduplicator after storing a message.
on_channel_msg checks this sentinel and suppresses storage regardless of
channel_idx or message_hash differences between the two library systems.
Fixes #mc-radar messages appearing in #weather and vice versa.
fix(events): secondary path-cache keyed by content for hash-mismatch
_path_cache keyed by meshcoredecoder hash; CHANNEL_MSG_RECV carries meshcore
hash (different value). Added _path_cache_by_content ("sender:text[:100]")
as fallback so path_hashes are recovered when the two hashes disagree.
fix(commands,cache): remove stale channel key after del_channel reindex
Cache entry for old_idx not removed after slot move, causing same channel
to appear twice. New DeviceCache.remove_channel_key(idx) called after each
move. asyncio.sleep(0.5) added before re-discovery to let device settle.
feat(channel_panel,commands,dashboard): channel edit — move/reindex support
First channel-edit capability in the GUI. New '↕️ Move / Reindex' mode in
Channel Manager dialog. Source channel selected from dropdown, target index
in number field. ↕ button inline with 🗑 in Messages and Archive submenus.
_cmd_move_channel reads secret from cache or device, writes new slot, clears
old slot, updates cache atomically, triggers re-discovery with settle delay.
### FIXED
- **Multibyte path hash support** (`ble/events.py`, `core/shared_data.py`):
corrected docstrings in both `_resolve_path_names` methods that incorrectly
described path hashes as "2-char hex strings". The actual contact lookup
uses `startswith` matching, which is hash-size agnostic and correctly
handles 1-byte (2 hex chars), 2-byte (4 hex chars) and 3-byte (6 hex chars)
path hashes as introduced in MeshCore firmware v1.14.0. No functional code
was changed — only the documentation was incorrect.
- **MariaDB schema** (`meshcore_schema.sql`): `meshcore_messages.path_hashes`
column widened from `VARCHAR(128)` to `VARCHAR(255)`. The old limit caused
silent truncation for paths longer than ~40 hops in 1-byte mode or ~25 hops
in 2-byte mode. Migration is backward-compatible; existing data is unchanged.
### CHANGED
- `config.py`: version bump `1.17.0 → 1.17.1`.
### RATIONALE
- MeshCore firmware v1.14.0 (2026-03-06) introduced configurable path hash
sizes (1-, 2- or 3-byte per repeater). Verification confirmed that
`meshcoredecoder 0.3.2` already returns correctly sized hex strings via
`_decode_path_len_byte`. The GUI path-resolution logic was already
forward-compatible; only the docstrings and the MariaDB column width required
correction.
### IMPACT
- No BLE handler, GUI panel, service or API endpoint modified.
- `meshcoredecoder` library unchanged; no pip update required.
- MariaDB migration: single `ALTER TABLE` statement, no downtime, no data loss.
WHAT: New + Add Channel button in the Messages submenu opens a dialog
supporting three modes — Hashtag (key derived from name), Private New
(random key + QR export), Private Existing (paste hex key).
WHY: Channels could only be added via firmware/external tools. The new
dialog covers all user scenarios without changing the BLE worker or
existing panels.
NOTES: Requires `qrcode[pil]` for QR rendering (graceful degradation if
absent). Channel re-discovery is triggered automatically on success.
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).
_abbrev_table used a list comprehension inline inside a generator
expression filter. In Python 3, list comprehensions have their own
scope, so the loop variable 'cu' was not visible to the outer 'if'
condition — causing a NameError on every !h / !help DM command.
Extract the comprehension to a local variable 'cats_upper' so both
the iteration and the filter operate on the same pre-built list.