WHAT
- README: add 8.3 Repeater Configuration Read; document poll retries,
traffic priority and cancellation in 8.2; add repeater, channel-sort
and API keys to the configuration table; list rxlog.jsonl and
repeater_config.jsonl; update 9.8, 9.16, 10, 14.2 and roadmap
- MULTI_INSTANCE: complete per-device file table and cleanup commands
- FEATURE_MESSAGE_PERSISTENCE: document the RX log JSONL stream,
align example records with the archive fields
- MeshCore_GUI_Design.docx: v5.11.0 -> v6.0.0 for application 1.24.5
- CLAUDE.md: replace references to non-existent INSTALLATIE.md and
TROUBLESHOOTING.md with README sections
- Fix stale defaults (MAX_CHANNELS, RXLOG_RETENTION_DAYS,
BOT_DEVICE_NAME) and file paths (rooms, pins)
- Bump version 1.24.4 -> 1.24.5
WHY
Documentation lagged behind the code: most documents dated from 1.22.2,
the design document from 1.9.0, while the changelog had reached 1.24.4.
NOTES
Documentation only; no code, schema or API change. CLAUDE.md/ADR-003
describe meshcore-ble-connect as the primary BLE path, the code does
not use it; left unchanged pending decision.
Add a RepeaterConfigPoller that reads flood limits, timing parameters,
interference threshold, RX gain, AGC interval and region once a day,
inside a window that opens at 03:00 local time.
These values are configuration, not telemetry. No binary request returns
them, so each one costs its own CLI round trip. Putting them on the
15-minute statistics schedule would add ten round trips per poll for
values that did not change, and would lengthen exactly the window that
1.24.3 closed: a longer poll is likelier to be cancelled by outgoing
traffic, and a cancelled poll writes no record.
Readings go to their own append-only JSONL archive, so the statistics
record schema stays untouched. Both pollers share the one cancellable
task slot, so they can never occupy the radio at the same time.
CLI replies arrive as CONTACT_MSG_RECV with text type CLI_DATA and are
now dropped in on_contact_msg instead of appearing in the message list
as a direct message nobody sent.
fix(worker): run repeater poll as cancellable background task
WHAT
Run _poll_repeaters() as an asyncio.Task instead of awaiting it in
_main_loop(). The task is cancelled as soon as has_pending_commands()
reports queued work, and a new poll is only started when the queue is
empty. RepeaterPoller.poll_due() catches the CancelledError, resets the
repeater's _next_due to now and re-raises, so a cancelled poll writes no
archive record and is retried on the next tick.
Adds SharedData.has_pending_commands() with its SharedDataWriter protocol
entry, and REPEATER_POLL_CANCEL_TIMEOUT (default 5.0) to bound the logout
cleanup of a cancelled poll. Corrects the comment on
REPEATER_POLL_MAX_ATTEMPTS, which still claimed a poll blocks the loop.
WHY
_main_loop() awaited the poll in the same iteration as process_all(), so
a repeater poll blocked command dispatch for its full duration. With the
1.24.2 defaults an unreachable repeater occupies 5 x 30 s login timeout
plus 4 x 5 s retry delay, roughly 190 s. A bot reply queued in that
window was only transmitted once the poll gave up, and a restart within
the window discarded it because the command queue lives in memory.
Confirmed in journal meshcore-gui-ttyUSB1.service of 2026-09-06: replies
queued at 09:33:46 and 09:35:23 were both sent at 09:36:25, the second
the poll ran out of attempts.
Statistics collection has no deadline; a bot reply does. Shortening the
retry budget would only shorten the window, not remove it.
NOTES
No breaking changes. has_pending_commands() is additive. Archive schema
unchanged, but no record is written for a cancelled poll, so the number
of polls per day can be lower than the interval suggests. A repeater on a
busy channel may go longer without a measurement — the intended trade-off.
Bumps version to 1.24.3.
WHAT
Repeater statistics polls now retry within the same poll instead of
recording a failure after a single session. REPEATER_POLL_MAX_ATTEMPTS
(default 5) and REPEATER_POLL_RETRY_DELAY (default 5.0) control the
budget. The login/status/logout sequence moved into a private
_attempt_session(); _poll_one() drives the loop and writes exactly one
archive record carrying the new "attempts" field and the last error.
A missing password is not retried. Version bumped to 1.24.2; the stale
__version__ = "5.0" in __init__.py was aligned with config.VERSION.
WHY
Polls fail at irregular intervals with no established root cause. This
is an explicit workaround: retrying recovers readings now, while the
attempt count makes the underlying failure rate measurable so the real
cause can be identified from data instead of guessed at.
NOTES
No API or schema break — add_measurement() gained a keyword argument
with a default, and "attempts" is an added field that older records
simply lack. A poll blocks the worker loop for its full duration, so
worst-case command latency rises from roughly 60 seconds to roughly
three minutes for a repeater that never answers. Airtime rises
proportionally: up to five login/logout round trips per failing poll.
Bring README.md in line with the codebase after five releases and the
extraction of the bridge and observer daemons.
WHAT
- Document repeater statistics polling (1.23.0): config constants,
archive path, panel behaviour and a new section 9.16.
- Correct section 8.2, which claimed values are shown exactly as
reported. Since 1.23.2 uptime and airtime are rendered as
d/h/m/s. Storage is unchanged; only presentation differs.
- Document the per-repeater Poll now button (1.24.0) and the
reset_path() recovery after a login without confirmation (1.23.2).
- Add MSG_POLL_INTERVAL (1.22.6) and
CHANNEL_DISCOVERY_ABORT_THRESHOLD (1.22.5) to the config table, and
record the Room Server history timestamp issue as a known limitation.
- Replace section 11 with a pointer to meshcore-bridge and
meshcore-observer, now separate repositories.
- Correct the project structure: the docs/ tree listed a file that does
not exist and placed two others in the wrong directory; adr/, ble/ and
examples/ were missing entirely. Also adds ble_connector.py,
channel_discovery.py, repeater_stats_panel.py and the three
repeater services.
WHY
The claim about value conversion in 8.2 was actively wrong, not merely
incomplete, and section 11 documented a package no longer in this
repository. Both mislead a reader who trusts the README over the code.
NOTES
- Documentation only. No code, no version bump.
- Section 11 is shortened rather than removed so sections 12-19 keep
their numbering and existing anchors stay valid.
- Screenshot placeholders added in sections 3 and 9.16.
- Two pre-existing broken anchors (#1412-ble-quick-fixes,
#13-known-limitations) are left untouched; out of scope here.
The "Polling…" message raised by the Poll now button in the REPEATERS
panel stayed on screen indefinitely.
Root cause
The notification used type='ongoing', which Quasar renders with a timeout
of zero. Such a notification is meant to be dismissed programmatically by
the code that raised it, but the poll runs in the worker thread and the
panel never holds a handle to dismiss it. The message therefore remained
visible regardless of whether the poll succeeded or failed. Every other
panel in the project uses a typed notification with an explicit timeout;
this one was the exception.
Fix
The notification is now type='info' with a 3 second timeout, matching the
convention used elsewhere in the GUI.
Presentation only. The poll itself was unaffected — the message was
stale, not the polling.
fix(gui): pass repeater stores to the dashboard page handler
The REPEATERS menu item and panel were absent from the dashboard while
polling ran normally: statistics were collected and archived, but nothing
was visible in the GUI.
Root cause
DashboardPage is constructed in two places. The instance built in main()
received repeater_config_store and repeater_stats_archive, but the one
the @ui.page('/') handler creates per browser session did not. With both
arguments defaulting to None, self._repeater_stats stayed None, and both
the menu entry and the panel container are guarded on that attribute — so
they were skipped without an error anywhere.
The worker receives its stores through main() and was unaffected, which
is why polling and archiving kept working and the failure was invisible
in the logs.
Diagnosis
On the affected tree, _repeater_config_store appeared four times in
__main__.py against seven in a correct one. The three missing
occurrences were exactly the ones the page handler depends on: the
module-level global, the global declaration in main(), and the keyword
argument in _page_dashboard().
Fix
- Declare _repeater_config_store and _repeater_stats_archive at module
level and add them to the global statement in main().
- Forward both stores from _page_dashboard() to DashboardPage.
Impact
- The REPEATERS panel appears whether or not a repeater is configured; an
empty panel names the configuration file to create.
- No change to polling, archiving or stored data. Archives written under
1.23.0 remain valid.
Anything a page handler needs must be passed at that call site, not only
where the object is first built in main(). A store that reaches one of
the two construction sites fails silently.
feat(repeater): poll repeater statistics on a configurable interval
Configured repeaters are logged into, queried for their status and
logged out again on a configurable interval. Raw values are archived
with the moment of each poll, so battery voltage can be tracked over
multiple days.
New modules
- services/repeater_config_store.py — per-device configuration in
~/.meshcore-gui/repeaters/<device>_repeaters.json (directory 0700,
file 0600). Holds the full 32-byte public key, display name, login
password, poll interval and enabled flag. A key that is not 64 hex
characters is rejected: the meshcore library refuses a prefix for
login, status and logout requests. An absent password means "not
configured", an empty string is a valid credential.
- services/repeater_stats_archive.py — append-only JSONL at
~/.meshcore-gui/archive/<device>_repeater_stats.jsonl, one record per
poll, retention through the existing daily cleanup task.
- services/repeater_poller.py — send_login_sync, req_status_sync,
send_logout. The sync variants are used because plain send_login and
send_statusreq return as soon as the frame is on the wire and say
nothing about the outcome.
- gui/panels/repeater_stats_panel.py — read-only REPEATERS panel showing
the latest values, the age of the last successful poll and the last
error per repeater.
Changed
- config.py: VERSION 1.23.0, REPEATERS_DIR, REPEATER_POLL_ENABLED,
REPEATER_POLL_INTERVAL (900 s), REPEATER_POLL_CHECK_INTERVAL,
REPEATER_LOGIN_TIMEOUT, REPEATER_STATUS_TIMEOUT,
REPEATER_STATS_RETENTION_DAYS.
- ble/worker.py: _main_loop runs the repeater poll alongside the existing
periodic tasks, following the same interval-guarded pattern;
_cleanup_old_data applies retention to the new archive.
- __main__.py, gui/dashboard.py, gui/panels/__init__.py: wiring.
Behaviour
- At most one repeater is polled per check, each with its own start
offset within the interval, so two repeaters are never queried back to
back. Two repeaters on the default interval land ~7.5 minutes apart.
- A failed login, an absent status response or an exception is recorded
as a poll attempt with ok=false and a reason, and blocks neither the
other repeater nor the application. Logout is always sent once a login
has been attempted, including after a timeout — a login that returned
no confirmation may still have succeeded on the far side.
- Values are stored exactly as reported: no scaling, rounding or
averaging. Battery voltage keeps the unit the firmware returns.
- The password is reachable through a single method that only the poller
calls. Objects handed to the GUI have no password field, and no
password reaches the archive, the log or any error message.
- Polling stays idle until a repeater is configured, so an installation
without a configuration file behaves exactly as before.
No change to the archive JSON schema, the public REST API or any
existing public method signature. Worker and dashboard constructors
gained optional keyword arguments only.
WHAT
Add MSG_POLL_INTERVAL (config.py, default 30s) and
SerialWorker._poll_pending_messages(), which drains the device
message queue via get_msg() until NO_MORE_MSGS, independently of
the messages_waiting event.
WHY
meshcore 2.3.9.1 fetches messages only in response to
messages_waiting. When that event is missed the radio ACKs the
packet but no message event ever reaches the application, so the
message is neither archived nor displayed. Node resets do not
help: the trigger goes missing, not the queue.
NOTES
start_auto_message_fetching() stays active; the poll only picks up
what the event path leaves behind. Fetched messages dispatch as
ordinary message events, so dedup and archiving are unchanged.
Room Server history timestamps remain a separate open issue.
WHAT
_discover_channels() recorded no distinction between a slot the device
reported as empty and a slot it never answered for; both ended as an
absent entry, and the result was written over the cached channel names
wholesale. A device that returned ERROR on every slot therefore reduced
the cache to {0: "Public"}, permanently — those names are the only
channel source at startup while CHANNEL_CACHE_ENABLED is off.
The scan now records answered slots in a ChannelDiscoveryResult and
merges into the cached names: answered slots win, unanswered slots keep
their cached entry. Stale-key cleanup follows the same rule, replacing
the len(discovered) >= 2 heuristic.
WHY
The existing safety net guarded only the key cleanup, not the name write
above it. Recording answered slots addresses the defect itself rather
than adding a second heuristic, and lets one rule serve names and keys.
NOTES
No breaking changes. Cache JSON schema unchanged; _discover_channels()
keeps its signature. New config key CHANNEL_DISCOVERY_ABORT_THRESHOLD
(default 3) replaces the hardcoded consecutive-error limit.
WHAT
- MessagesPanel.set_active_channel() now points the send selector at the
active channel through the new _sync_send_channel() helper
- New _send_channel_pending flag, raised only on an actual channel change
- VERSION 1.22.3 -> 1.22.4
WHY
The drawer submenu filtered the list and rewrote the header but left the
selector alone, and update_channel_options()'s corrective branch only
fires for a value no longer in the options - Public always is. Viewing
#noodnet-ov and pressing Send therefore published on Public.
NOTES
The pending flag is required: update_filters() re-invokes
set_active_channel() every 500 ms, so an unconditional sync would
overwrite a manual choice twice per second. It also lets the sync retry
once options are loaded, which matters on page load with ?channel=N.
No public signature changed.
WHAT
- route_page: _default_reply_channel() derives the reply select default
from msg.channel instead of the first entry in the channel list
- models: new Message.date field, now_date() and display_timestamp();
format_line() and incoming()/outgoing() use them
- message_archive: persist "date" alongside "time" and "timestamp_utc"
- room_server_panel + route_page info header use display_timestamp()
- VERSION 1.22.2 -> 1.22.3
WHY
The route page exists to show one specific message, so that message —
not the ordering of the channel list — is the correct source for the
reply default. The date could not be fixed at the render site because
Message had no date at all: now_timestamp() yields HH:MM:SS and the
date lived only as timestamp_utc inside the archive layer, unread by
from_dict(). A first-class field plus a single formatting helper fixes
the cause and keeps the format in one place.
NOTES
No breaking changes. The archive schema gains one additive field;
pre-1.22.3 entries have their date derived from timestamp_utc
(converted to local time) and fall back to time-only when absent.
Message.incoming() gained an optional keyword-only date parameter.
Message display lines are ~11 characters wider.
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.
The README had fallen significantly behind the current state of the
codebase. This commit brings it up to date across the board.
Content changes:
- Rewrite title, subtitle and opening line to reflect the all-in-one
platform nature of the project instead of focusing on the bridge alone
- Rewrite section 1 (Why This Project Exists) to cover the full
operator workflow: connect, monitor, manage, archive, automate, BBS,
bridge, publish, headless
- Merge the two overlapping platform/testing notes into one
- Update features list: add BBS as a first-class feature, merge
Dynamic Channel Discovery and Add/Delete Channel into Channel
Management, rename Contact Maintenance to Contact Management, remove
Threaded Architecture (implementation detail), add Headless /
Multi-instance as explicit feature
- Move BBS section from top-level §12 into §9 Functionality as §9.14,
adjusting heading levels accordingly
- Add §9.2 note clarifying that contacts and nodes are the same concept
- Add new §8.1 Data Directory with a complete overview of all files and
subdirectories under ~/.meshcore-gui/, including maintenance commands
- Update §10 architecture diagram and component list: add BBSPanel,
BotPanel, REST API block, ChannelService, BbsService,
PublicApiService and MapSnapshotService
- Update §15.2 project structure: add meshcore_gui/api/, all missing
panels and services, and reflect install_scripts/ as a subdirectory
- Fix all install script paths from root level to install_scripts/
- Document install_scripts/install_venv.sh as the first setup step
- Document install_scripts/install_observer.sh in project structure
- Update Observer mode in roadmap: installer present, daemon in
development
- Renumber sections 13–20 to 12–19 after BBS was absorbed into §9
- Update all cross-references and table of contents accordingly
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
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
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
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
Adds an offline Bulletin Board System accessible via Direct Message to
the node's own key. All BBS commands (!p, !r, !bbs) are handled directly
in EventHandler.on_contact_msg, independent of MeshBot.
- One node = one board; settings reduced to a single channel selector
- Short syntax: !p <cat> <text> and !r [cat] alongside full !bbs syntax
- Category abbreviations computed automatically (shortest unique prefix)
- !r and !bbs help always include the abbreviation table in the reply
- DM reply routed back to sender via command_sink
- SQLite message store with WAL mode and configurable retention
Implements a fully offline Bulletin Board System for emergency mesh
communication (NoodNet Zwolle, NoodNet OV, Dalfsen and similar
organisations).
New files:
- services/bbs_config_store.py: Manages ~/.meshcore-gui/bbs/bbs_config.json.
Thread-safe, atomic writes. Created on first run. Channels are
enabled and configured at runtime via the GUI — no code changes
required.
- services/bbs_service.py: SQLite persistence at
~/.meshcore-gui/bbs/bbs_messages.db. WAL-mode enabled so multiple
simultaneous instances (e.g. 800 MHz + 433 MHz) share the same
bulletin board safely. BbsCommandHandler parses !bbs post/read/help
mesh commands with live config from BbsConfigStore. Whitelist
enforcement via sender public key (silent drop on unknown key).
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Settings section lists all active device channels; per channel:
enable toggle, categories, regions, retention and key whitelist.
Changes take effect immediately without restart.
Modified files:
- services/bot.py: MeshBot accepts optional bbs_handler; !bbs commands
are routed to BbsCommandHandler before keyword matching.
- config.py: BBS_CHANNELS removed (config now lives in bbs_config.json).
Version bumped to 1.14.0.
- gui/dashboard.py: BbsConfigStore and BbsService instantiated and
shared across handler and panel. BBS drawer menu item added.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage layout:
~/.meshcore-gui/bbs/bbs_config.json — channel configuration
~/.meshcore-gui/bbs/bbs_messages.db — SQLite message store
No new external dependencies (SQLite is stdlib).
Implements a fully offline Bulletin Board System for use on MeshCore
mesh networks, designed for emergency communication organisations
(NoodNet Zwolle, NoodNet OV, Dalfsen).
New files:
- services/bbs_service.py: SQLite-backed persistence layer with
BbsMessage dataclass, BbsService (post/read/purge) and
BbsCommandHandler (!bbs post/read/help mesh command parser).
Whitelist enforcement via sender public key (silent drop on
unknown sender). Per-channel configurable regions, categories
and retention period.
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Region filter is conditionally visible based on channel config.
Modified files:
- config.py: BBS_CHANNELS configuration block added (ch 2/3/4).
Version bumped to 1.14.0.
- services/bot.py: MeshBot accepts optional bbs_handler parameter.
Incoming !bbs commands are routed to BbsCommandHandler before
keyword matching; no changes to existing bot behaviour.
- gui/dashboard.py: BbsPanel registered as standalone panel with
📋 BBS drawer menu item.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage: ~/.meshcore-gui/bbs/bbs_messages.db (SQLite, stdlib only).
No new external dependencies.
Implements a fully offline Bulletin Board System for use on MeshCore
mesh networks, designed for emergency communication organisations
(NoodNet Zwolle, NoodNet OV, Dalfsen).
New files:
- services/bbs_service.py: SQLite-backed persistence layer with
BbsMessage dataclass, BbsService (post/read/purge) and
BbsCommandHandler (!bbs post/read/help mesh command parser).
Whitelist enforcement via sender public key (silent drop on
unknown sender). Per-channel configurable regions, categories
and retention period.
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Region filter is conditionally visible based on channel config.
Modified files:
- config.py: BBS_CHANNELS configuration block added (ch 2/3/4).
Version bumped to 1.14.0.
- services/bot.py: MeshBot accepts optional bbs_handler parameter.
Incoming !bbs commands are routed to BbsCommandHandler before
keyword matching; no changes to existing bot behaviour.
- gui/dashboard.py: BbsPanel registered as standalone panel with
📋 BBS drawer menu item.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage: ~/.meshcore-gui/bbs/bbs_messages.db (SQLite, stdlib only).
No new external dependencies.
Implements a fully offline Bulletin Board System for use on MeshCore
mesh networks, designed for emergency communication organisations
(NoodNet Zwolle, NoodNet OV, Dalfsen).
New files:
- services/bbs_service.py: SQLite-backed persistence layer with
BbsMessage dataclass, BbsService (post/read/purge) and
BbsCommandHandler (!bbs post/read/help mesh command parser).
Whitelist enforcement via sender public key (silent drop on
unknown sender). Per-channel configurable regions, categories
and retention period.
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Region filter is conditionally visible based on channel config.
Modified files:
- config.py: BBS_CHANNELS configuration block added (ch 2/3/4).
Version bumped to 1.14.0.
- services/bot.py: MeshBot accepts optional bbs_handler parameter.
Incoming !bbs commands are routed to BbsCommandHandler before
keyword matching; no changes to existing bot behaviour.
- gui/dashboard.py: BbsPanel registered as standalone panel with
📋 BBS drawer menu item.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage: ~/.meshcore-gui/bbs/bbs_messages.db (SQLite, stdlib only).
No new external dependencies.
Implements a fully offline Bulletin Board System for emergency mesh
communication (NoodNet Zwolle, NoodNet OV, Dalfsen and similar
organisations).
New files:
- services/bbs_config_store.py: Manages ~/.meshcore-gui/bbs/bbs_config.json.
Thread-safe, atomic writes. Created on first run. Channels are
enabled and configured at runtime via the GUI — no code changes
required.
- services/bbs_service.py: SQLite persistence at
~/.meshcore-gui/bbs/bbs_messages.db. WAL-mode enabled so multiple
simultaneous instances (e.g. 800 MHz + 433 MHz) share the same
bulletin board safely. BbsCommandHandler parses !bbs post/read/help
mesh commands with live config from BbsConfigStore. Whitelist
enforcement via sender public key (silent drop on unknown key).
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Settings section lists all active device channels; per channel:
enable toggle, categories, regions, retention and key whitelist.
Changes take effect immediately without restart.
Modified files:
- services/bot.py: MeshBot accepts optional bbs_handler; !bbs commands
are routed to BbsCommandHandler before keyword matching.
- config.py: BBS_CHANNELS removed (config now lives in bbs_config.json).
Version bumped to 1.14.0.
- gui/dashboard.py: BbsConfigStore and BbsService instantiated and
shared across handler and panel. BBS drawer menu item added.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage layout:
~/.meshcore-gui/bbs/bbs_config.json — channel configuration
~/.meshcore-gui/bbs/bbs_messages.db — SQLite message store
No new external dependencies (SQLite is stdlib).
Implements a fully offline Bulletin Board System for emergency mesh
communication (NoodNet Zwolle, NoodNet OV, Dalfsen and similar
organisations).
New files:
- services/bbs_config_store.py: Manages ~/.meshcore-gui/bbs/bbs_config.json.
Thread-safe, atomic writes. Created on first run. Channels are
enabled and configured at runtime via the GUI — no code changes
required.
- services/bbs_service.py: SQLite persistence at
~/.meshcore-gui/bbs/bbs_messages.db. WAL-mode enabled so multiple
simultaneous instances (e.g. 800 MHz + 433 MHz) share the same
bulletin board safely. BbsCommandHandler parses !bbs post/read/help
mesh commands with live config from BbsConfigStore. Whitelist
enforcement via sender public key (silent drop on unknown key).
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Settings section lists all active device channels; per channel:
enable toggle, categories, regions, retention and key whitelist.
Changes take effect immediately without restart.
Modified files:
- services/bot.py: MeshBot accepts optional bbs_handler; !bbs commands
are routed to BbsCommandHandler before keyword matching.
- config.py: BBS_CHANNELS removed (config now lives in bbs_config.json).
Version bumped to 1.14.0.
- gui/dashboard.py: BbsConfigStore and BbsService instantiated and
shared across handler and panel. BBS drawer menu item added.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage layout:
~/.meshcore-gui/bbs/bbs_config.json — channel configuration
~/.meshcore-gui/bbs/bbs_messages.db — SQLite message store
No new external dependencies (SQLite is stdlib).
Implements a fully offline Bulletin Board System for emergency mesh
communication (NoodNet Zwolle, NoodNet OV, Dalfsen and similar
organisations).
New files:
- services/bbs_config_store.py: Manages ~/.meshcore-gui/bbs/bbs_config.json.
Thread-safe, atomic writes. Created on first run. Channels are
enabled and configured at runtime via the GUI — no code changes
required.
- services/bbs_service.py: SQLite persistence at
~/.meshcore-gui/bbs/bbs_messages.db. WAL-mode enabled so multiple
simultaneous instances (e.g. 800 MHz + 433 MHz) share the same
bulletin board safely. BbsCommandHandler parses !bbs post/read/help
mesh commands with live config from BbsConfigStore. Whitelist
enforcement via sender public key (silent drop on unknown key).
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Settings section lists all active device channels; per channel:
enable toggle, categories, regions, retention and key whitelist.
Changes take effect immediately without restart.
Modified files:
- services/bot.py: MeshBot accepts optional bbs_handler; !bbs commands
are routed to BbsCommandHandler before keyword matching.
- config.py: BBS_CHANNELS removed (config now lives in bbs_config.json).
Version bumped to 1.14.0.
- gui/dashboard.py: BbsConfigStore and BbsService instantiated and
shared across handler and panel. BBS drawer menu item added.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage layout:
~/.meshcore-gui/bbs/bbs_config.json — channel configuration
~/.meshcore-gui/bbs/bbs_messages.db — SQLite message store
No new external dependencies (SQLite is stdlib).
Implements a fully offline Bulletin Board System for use on MeshCore
mesh networks, designed for emergency communication organisations
(NoodNet Zwolle, NoodNet OV, Dalfsen).
New files:
- services/bbs_service.py: SQLite-backed persistence layer with
BbsMessage dataclass, BbsService (post/read/purge) and
BbsCommandHandler (!bbs post/read/help mesh command parser).
Whitelist enforcement via sender public key (silent drop on
unknown sender). Per-channel configurable regions, categories
and retention period.
- gui/panels/bbs_panel.py: Dashboard panel with channel selector,
region/category filters, scrollable message list and post form.
Region filter is conditionally visible based on channel config.
Modified files:
- config.py: BBS_CHANNELS configuration block added (ch 2/3/4).
Version bumped to 1.14.0.
- services/bot.py: MeshBot accepts optional bbs_handler parameter.
Incoming !bbs commands are routed to BbsCommandHandler before
keyword matching; no changes to existing bot behaviour.
- gui/dashboard.py: BbsPanel registered as standalone panel with
📋 BBS drawer menu item.
- gui/panels/__init__.py: BbsPanel re-exported.
Storage: ~/.meshcore-gui/bbs/bbs_messages.db (SQLite, stdlib only).
No new external dependencies.
- Replace two fixed-destination back-buttons on the route page with a
single arrow_back button using window.history.back(), so navigation
always returns to the calling screen (Messages or Archive).
- Guard setIcon() and setPopupContent() in applyDevice/applyContacts
behind isPopupOpen() to prevent popup flickering on the 500 ms
update tick.
- Set fadeAnimation: false and markerZoomAnimation: false on both
Leaflet map instances (main map and route map) to eliminate popup
flash on first click, particularly noticeable on Raspberry Pi.