v1.22.1: JSONL stream output for RX log alongside existing JSON archive
Every received LoRa packet is now also written immediately as a single
JSON line to ~/.meshcore-gui/archive/<device>_rxlog.jsonl. This is an
append-only, unbuffered stream format that lets separate local services
(such as meshcore-watchlist) consume the RX feed in real time without
depending on the GUI's internal batched-JSON format.
- The existing <device>_rxlog.json is unchanged (60 s flush interval,
atomic rewrite). The GUI, the public REST API and the domca.nl
ingest continue to work without modification.
- Writes to the JSONL file are direct (no buffer), so end-to-end
latency from radio reception to JSONL line is sub-second.
- A failure on the JSONL path is logged via debug_print and does not
affect the buffered JSON archive — the two paths are independent.
- _cleanup_rxlog() now also rewrites the JSONL file to drop entries
older than RXLOG_RETENTION_DAYS. Corrupt lines (e.g. a partial
last line after a crash) are skipped during cleanup.
No BLE/worker changes, no public REST API changes; SharedData and the
BLE command pipeline are untouched. Disk usage increases modestly
(one additional file per device, same retention window).
PATCH bump 1.22.0 → 1.22.1: purely additive, fully backwards-compatible.
get_messages_payload() built each item dict with sender, text, timestamp, hops and path_hashes, but silently dropped the already-resolved sender_pubkey and path_names fields that BleEventHandler writes to every archived message (see _resolve_path_names() in ble/events.py and the archive schema in services/message_archive.py lines 135–137).
Downstream consumers were therefore forced to re-resolve path hashes themselves using only the 1-byte path-hash prefix — a lookup that collides heavily in networks with more than ~256 nodes and yields the wrong repeater name, type and coordinates on nearly every hop.
The sender_pubkey omission had a similar effect on the sender column: clients could only match on display-name, which is ambiguous when two nodes share a name stem (e.g. NL-OV-ZWO-LGH-PD5WB vs the mobile variant NL-OV-ZWO-LGH-PD5WB-MOB).
Fix: added "sender_pubkey" and "path_names" to the item dict. Both fields are read straight from the archive — no new resolution logic is introduced, so there is no additional cost on the hot path. The response schema change is additive: existing clients that ignore unknown keys continue to work unchanged.
- 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: Four read-only GET endpoints under /api/v1/ registered on the
NiceGUI/FastAPI instance via register_routes(_shared) in __main__.py.
Controlled by API_ENABLED in config.py (default: True).
WHY: Enables the domca.nl PHP collector to pull live mesh data over HTTP
without direct access to SQLite or SharedData internals.
NOTES: Private channel messages are unconditionally excluded in
public_api_service.py — filtering is server-side hard, no auth needed.
Channel rule: idx==0 (Public) or name.startswith('#') (Hashtag) = expose.
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.
Adds an offline BBS accessible via Direct Message to the node's own key.
Access is channel-based: anyone seen on a configured BBS channel is
automatically whitelisted for DM access. Channels stay clean.
- Multi-channel configuration: any combination of device channels can be
selected; senders on any of them are auto-whitelisted
- Short syntax: !p <cat> <text> and !r [cat] alongside full !bbs syntax
- Category abbreviations computed automatically (shortest unique prefix)
- handle_channel_msg: bootstrap reply on channel + auto-whitelist sender
- handle_dm: DM entry point, checks whitelist, routes to post/read/help
- DM reply routed back to sender via command_sink
- SQLite message store with WAL mode and configurable retention