mirror of
https://github.com/pe1hvh/meshcore-gui.git
synced 2026-08-08 09:52:58 +02:00
Initial clean code
This commit is contained in:
@@ -0,0 +1,773 @@
|
||||
# CHANGELOG
|
||||
|
||||
<!-- CHANGED: Title changed from "CHANGELOG: Message & Metadata Persistence" to "CHANGELOG" —
|
||||
a root-level CHANGELOG.md should be project-wide, not feature-specific. -->
|
||||
|
||||
All notable changes to MeshCore GUI are documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/) and [Semantic Versioning](https://semver.org/).
|
||||
|
||||
---
|
||||
## [1.13.0] - 2026-03-09 — Leaflet Map Runtime Stabilization
|
||||
|
||||
### Added
|
||||
- ✅ `meshcore_gui/static/leaflet_map_panel.js` — Dedicated browser-side Leaflet runtime responsible for map lifecycle, marker registry, clustering and theme handling independent from NiceGUI redraw cycles
|
||||
- ✅ `meshcore_gui/static/leaflet_map_panel.css` — Styling for browser-side node markers, cluster icons and map container
|
||||
- ✅ `meshcore_gui/services/map_snapshot_service.py` — Snapshot service that normalizes device/contact map data into a compact payload for the browser runtime
|
||||
- ✅ Browser-side map state management for center, zoom and theme
|
||||
- ✅ Theme persistence across reconnect events via browser storage fallback
|
||||
- ✅ Browser-side contact clustering via `Leaflet.markercluster`
|
||||
- ✅ Separate non-clustered device marker layer so the own device remains individually visible
|
||||
|
||||
### Changed
|
||||
- 🔄 `meshcore_gui/gui/panels/map_panel.py` — Replaced NiceGUI Leaflet wrapper usage with a pure browser-managed Leaflet container while preserving the existing card layout, theme toggle and center-on-device control
|
||||
- 🔄 Leaflet bootstrap moved out of inline Python into a dedicated browser runtime loaded from `/static`
|
||||
- 🔄 Asset loading order is now explicit: Leaflet first, then `Leaflet.markercluster`, then the MeshCore panel runtime
|
||||
- 🔄 Map initialization now occurs only once per container; NiceGUI refresh cycles no longer recreate the map
|
||||
- 🔄 Dashboard update loop now sends compact map snapshots instead of triggering redraws
|
||||
- 🔄 Snapshot processing in the browser is coalesced so only the newest payload is applied
|
||||
- 🔄 Map markers are managed in separate device/contact layers and updated incrementally by stable node id
|
||||
- 🔄 Contact markers are rendered inside a persistent cluster layer while the device marker remains outside clustering
|
||||
- 🔄 Theme switching moved to a dedicated theme channel instead of being embedded in snapshot data
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Map disappearing during dashboard refresh cycles** — prevented repeated map reinitialization caused by the 500 ms NiceGUI update loop
|
||||
- 🛠 **Markers disappearing between refreshes** — marker updates are now incremental and keyed by node id
|
||||
- 🛠 **Blank map container on load** — browser bootstrap now waits for DOM host, Leaflet runtime and panel runtime before initialization
|
||||
- 🛠 **Leaflet clustering bootstrap failure (`L is not defined`)** — resolved by enforcing correct script dependency order before the panel runtime starts
|
||||
- 🛠 **MarkerClusterGroup failure (`Map has no maxZoom specified`)** — the map now defines `maxZoom` during initial creation before the cluster layer is attached
|
||||
- 🛠 **Half-initialized map retry cascade (`Map container is already initialized`)** — map state is now registered safely during initialization so a failed attempt cannot trigger a second `L.map(...)` on the same container
|
||||
- 🛠 **Race condition between queued snapshot and theme selection** — explicit theme changes can no longer be overwritten by stale snapshot payloads
|
||||
- 🛠 **Viewport jumping back to default center/zoom** — stored viewport is no longer reapplied on each snapshot update
|
||||
- 🛠 **Theme reverting to default during reconnect** — effective map theme is restored before snapshot processing resumes
|
||||
|
||||
### Impact
|
||||
- Leaflet map is now managed entirely in the browser and is no longer recreated on each dashboard refresh
|
||||
- Node markers remain stable and no longer flicker or disappear during the 500 ms update cycle
|
||||
- Dense contact sets can now be rendered with clustering without violating the browser-owned map lifecycle
|
||||
- Theme switching and viewport state persist reliably across reconnect events
|
||||
- No breaking changes outside the map subsystem
|
||||
---
|
||||
## [1.12.1] - 2026-03-08 — Minor change bot
|
||||
### Changed
|
||||
- 🔄 `meshcore_gui/services/bot.py`: remove path id's
|
||||
### Impact
|
||||
- No breaking changes — all existing functionality preserved serial.
|
||||
|
||||
---
|
||||
|
||||
## [1.12.0] - 2026-02-26 — MeshCore Observer Fase 1
|
||||
|
||||
### Added
|
||||
- ✅ **MeshCore Observer daemon** — New standalone read-only daemon (`meshcore_observer.py`) that reads archive JSON files produced by meshcore_gui and meshcore_bridge, aggregates them, and presents a unified NiceGUI monitoring dashboard on port 9093.
|
||||
- ✅ **ArchiveWatcher** — Core component that polls `~/.meshcore-gui/archive/` for `*_messages.json` and `*_rxlog.json` files, tracks mtime changes, and returns only new entries since previous poll. Thread-safe, zero writes, graceful on corrupt JSON.
|
||||
- ✅ **Observer dashboard panels** — Sources overview, aggregated messages feed (sorted by timestamp), aggregated RX log table, and statistics panel with uptime/counters/per-source breakdown. Full DOMCA theme (dark + light mode).
|
||||
- ✅ **Source filter** — Dropdown to filter messages and RX log by archive source.
|
||||
- ✅ **Channel filter** — Dropdown to filter messages by channel name.
|
||||
- ✅ **ObserverConfig** — YAML-based configuration with `from_yaml()` classmethod, defaults work without config file.
|
||||
- ✅ **observer_config.yaml** — Documented config template with all options.
|
||||
- ✅ **install_observer.sh** — systemd installer (`/opt/meshcore-observer/`, `/etc/meshcore/observer_config.yaml`), with `--uninstall` option.
|
||||
- ✅ **RxLogEntry raw packet fields** — 5 new fields on `RxLogEntry` dataclass: `raw_payload`, `packet_len`, `payload_len`, `route_type`, `packet_type_num` (all with defaults, backward compatible).
|
||||
- ✅ **EventHandler.on_rx_log() metadata** — Raw payload hex and packet metadata now passed through to RxLogEntry and archived (preparation for Fase 2 LetsMesh uplink).
|
||||
|
||||
### Changed
|
||||
- 🔄 `meshcore_gui/core/models.py`: RxLogEntry +5 fields with defaults (backward compatible).
|
||||
- 🔄 `meshcore_gui/ble/events.py`: on_rx_log() fills raw_payload and metadata (~10 lines added).
|
||||
- 🔄 `meshcore_gui/services/message_archive.py`: add_rx_log() serializes the 5 new RxLogEntry fields.
|
||||
- 🔄 `meshcore_gui/config.py`: Version bumped to `1.12.0`.
|
||||
|
||||
### Impact
|
||||
- **No breaking changes** — All new RxLogEntry fields have defaults; existing archives and code work identically.
|
||||
- **New daemon** — meshcore_observer is fully standalone; no imports from meshcore_gui (reads only JSON files).
|
||||
|
||||
---
|
||||
|
||||
### Added
|
||||
- ✅ **Serial CLI flags** — `--baud=BAUD` and `--serial-cx-dly=SECONDS` for serial configuration at startup.
|
||||
|
||||
### Changed
|
||||
- 🔄 **Connection layer** — Switched from BLE to serial (`MeshCore.create_serial`) with serial reconnect handling.
|
||||
- 🔄 `config.py`: Added `SERIAL_BAUDRATE`, `SERIAL_CX_DELAY`, `DEFAULT_TIMEOUT`, `MESHCORE_LIB_DEBUG`; removed BLE PIN settings; version bumped to `1.10.0`.
|
||||
- 🔄 `meshcore_gui.py` / `meshcore_gui/__main__.py`: Updated usage, banners and defaults for serial ports.
|
||||
- 🔄 Docs: Updated README and core docs for serial usage; BLE documents marked as legacy.
|
||||
|
||||
### Impact
|
||||
- No breaking changes — all existing functionality preserved serial.
|
||||
|
||||
---
|
||||
|
||||
## [1.9.11] - 2026-02-19 — Message Dedup Hotfix
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Duplicate messages after (re)connect** — `load_recent_from_archive()` appended archived messages on every connect attempt without clearing existing entries; after N failed connects, each message appeared N times. Method is now idempotent: clears the in-memory list before loading.
|
||||
- 🛠 **Persistent duplicate messages** — Live BLE events for messages already loaded from archive were not suppressed because the `DualDeduplicator` was never seeded with archived content. Added `_seed_dedup_from_messages()` in `BLEWorker` after cache/archive load and after reconnect.
|
||||
- 🛠 **Last-line-of-defence dedup in SharedData** — `add_message()` now maintains a fingerprint set (`message_hash` or `channel:sender:text`) and silently skips messages whose fingerprint is already tracked. This guards against duplicates regardless of their source.
|
||||
- 🛠 **Messages panel empty on first click** — `_show_panel()` made the container visible but relied on the next 500 ms timer tick to populate it. Added an immediate `_messages.update()` call so content is rendered the moment the panel becomes visible.
|
||||
|
||||
### Changed
|
||||
- 🔄 `core/shared_data.py`: Added `_message_fingerprints` set and `_message_fingerprint()` static method; `add_message()` checks fingerprint before insert and evicts fingerprints when messages are rotated out; `load_recent_from_archive()` clears messages and fingerprints before loading (idempotent)
|
||||
- 🔄 `ble/worker.py`: Added `_seed_dedup_from_messages()` helper; called after `_apply_cache()` and after reconnect `_load_data()` to seed `DualDeduplicator` with existing messages
|
||||
- 🔄 `gui/dashboard.py`: `_show_panel()` now forces an immediate `_messages.update()` when the messages panel is shown, eliminating the stale-content flash
|
||||
- 🔄 `config.py`: Version bumped to `1.9.11`
|
||||
|
||||
### Impact
|
||||
- Eliminates all duplicate message display scenarios: initial connect, failed retries, reconnect, and BLE event replay
|
||||
- No breaking changes — all existing functionality preserved
|
||||
- Fingerprint set is bounded to the same 100-message cap as the message list
|
||||
|
||||
---
|
||||
|
||||
## [1.9.10] - 2026-02-19 — Map Tooltips & Separate Own-Position Marker
|
||||
|
||||
### Added
|
||||
- ✅ **Map marker tooltips** — All markers on the Leaflet map now show a tooltip on hover with the node name and type icon (📱, 📡, 🏠) from `TYPE_ICONS`
|
||||
- ✅ **Separate own-position marker** — The device's own position is now tracked as a dedicated `_own_marker`, independent from contact markers. This prevents the own marker from being removed/recreated on every contact update cycle
|
||||
|
||||
### Changed
|
||||
- 🔄 `gui/panels/map_panel.py`: Renamed `_markers` to `_contacts_markers`; added `_own_marker` attribute; own position marker is only updated when `device_updated` flag is set (not every timer tick); contact markers are only rebuilt when `contacts_updated` is set; added `TYPE_ICONS` import for tooltip icons
|
||||
- 🔄 `gui/dashboard.py`: Added `self._map.update(data)` call in the `device_updated` block so the own-position marker updates when device info changes (e.g. GPS position update)
|
||||
- 🔄 `config.py`: Version bumped to `1.9.10`
|
||||
|
||||
### Impact
|
||||
- Map centering on own device now works correctly and updates only when position actually changes
|
||||
- Contact markers are no longer needlessly destroyed and recreated on every UI timer tick — only on actual contact data changes
|
||||
- Tooltips make it easy to identify nodes on the map without clicking
|
||||
- No breaking changes — all existing map functionality preserved
|
||||
|
||||
### Credits
|
||||
- Based on [PR #16](https://github.com/pe1hvh/meshcore-gui/pull/16) by [@rich257](https://github.com/rich257)
|
||||
|
||||
---
|
||||
|
||||
## [1.9.9] - 2026-02-18 — Variable Landing Page & Operator Callsign
|
||||
|
||||
### Added
|
||||
- ✅ **Configurable operator callsign** — New `OPERATOR_CALLSIGN` constant in `config.py` (default: `"PE1HVH"`). Used in the landing page SVG and the drawer footer copyright label. Change this single value to personalize the entire GUI for a different operator
|
||||
- ✅ **External landing page SVG** — The DOMCA splash screen is now loaded from a standalone file (`static/landing_default.svg`) instead of being hardcoded in `dashboard.py`. New `LANDING_SVG_PATH` constant in `config.py` points to the SVG file. The placeholder `{callsign}` in the SVG is replaced at runtime with `OPERATOR_CALLSIGN`
|
||||
- ✅ **Landing page customization** — To use a custom landing page: copy `landing_default.svg` (or create your own SVG), use `{callsign}` wherever the operator callsign should appear, and point `LANDING_SVG_PATH` to your file. The default SVG includes an instructive comment block explaining the placeholder mechanism
|
||||
|
||||
### Changed
|
||||
- 🔄 `config.py`: Added `OPERATOR_CALLSIGN` and `LANDING_SVG_PATH` constants in new **OPERATOR / LANDING PAGE** section; version bumped to `1.9.9`
|
||||
- 🔄 `gui/dashboard.py`: Removed hardcoded `_DOMCA_SVG` string (~70 lines); added `_load_landing_svg()` helper that reads SVG from disk and replaces `{callsign}` placeholder; CSS variable `--pe1hvh` renamed to `--callsign`; drawer footer copyright label now uses `config.OPERATOR_CALLSIGN`
|
||||
|
||||
### Added (files)
|
||||
- ✅ `static/landing_default.svg` — The original DOMCA splash SVG extracted as a standalone file, with `{callsign}` placeholder and `--callsign` CSS variable. Serves as both the default landing page and a reference template for custom SVGs
|
||||
|
||||
### Impact
|
||||
- Out-of-the-box behavior is identical to v1.9.8 (same DOMCA branding, same PE1HVH callsign)
|
||||
- Operators personalize by changing 1–2 lines in `config.py` — no code modifications needed
|
||||
- Fallback: if the SVG file is missing, a minimal placeholder text is shown instead of a crash
|
||||
- No breaking changes — all existing dashboard functionality (panels, menus, timer, theming) unchanged
|
||||
|
||||
---
|
||||
|
||||
## [1.9.8] - 2026-02-17 — Bugfix: Route Page Sender ID, Type & Location Not Populated
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Sender ID, Type and Location empty in Route Page** — After the v4.1 refactoring to `RouteBuilder`/`RouteNode`, the sender contact lookup relied solely on `SharedData.get_contact_by_prefix()` (live lock-based) and `get_contact_by_name()`. When both failed (e.g. empty `sender_pubkey` from RX_LOG decode, or name mismatch), `route['sender']` remained `None` and the route table fell through to a hardcoded fallback with `type: '-'`, `location: '-'`. The contact data was available in the snapshot `data['contacts']` but was never searched
|
||||
- 🛠 **Route table fallback row ignored available contact data** — When `route['sender']` was `None`, the `_render_route_table` method used a static fallback row without attempting to find the contact in the data snapshot. Even when the contact was present in `data['contacts']` with valid type and location, these fields showed as `'-'`
|
||||
|
||||
### Changed
|
||||
- 🔄 `services/route_builder.py`: Added two additional fallback strategies in `build()` after the existing SharedData lookups: (3) bidirectional pubkey prefix match against `data['contacts']` snapshot, (4) case-insensitive `adv_name` match against `data['contacts']` snapshot. Added helper methods `_find_contact_by_pubkey()` and `_find_contact_by_adv_name()` for snapshot-based lookups
|
||||
- 🔄 `gui/route_page.py`: Added defensive fallback in `_render_route_table()` sender section — when `route['sender']` is `None`, attempts to find the contact in the snapshot via `_find_sender_contact()` before falling back to the static `'-'` row. Added `_find_sender_contact()` helper method
|
||||
|
||||
### Impact
|
||||
- Sender ID (hash), Type and Location are now populated correctly in the route table when the contact is known
|
||||
- Four-layer lookup chain ensures maximum resolution: (1) SharedData pubkey lookup, (2) SharedData name lookup, (3) snapshot pubkey lookup, (4) snapshot name lookup
|
||||
- Defensive fallback in route_page guarantees data is shown even if RouteBuilder misses it
|
||||
- No breaking changes — all existing route page behavior, styling and data flows unchanged
|
||||
|
||||
---
|
||||
|
||||
## [1.9.7] - 2026-02-17 — Layout Fix: Archive Filter Toggle & Route Page Styling
|
||||
|
||||
### Changed
|
||||
- 🔄 `gui/archive_page.py`: Archive filter card now hidden by default; toggle visibility via a `filter_list` icon button placed right-aligned on the same row as the "📚 Archive" title. Header restructured from single label to `ui.row()` with `justify-between` layout
|
||||
- 🔄 `gui/route_page.py`: Route page now uses DOMCA theme (imported from `dashboard.py`) with dark mode as default, consistent with the main dashboard. Header restyled from `bg-blue-600` to Quasar-themed header with JetBrains Mono font. Content container changed from `w-full max-w-4xl mx-auto` to `domca-panel` class for consistent responsive sizing
|
||||
- 🔄 `gui/dashboard.py`: Added `domca-header-text` CSS class with `@media (max-width: 599px)` rule to hide header text on narrow viewports; applied to version label and status label
|
||||
- 🔄 `gui/route_page.py`: Header label also uses `domca-header-text` class for consistent responsive behaviour
|
||||
|
||||
### Added
|
||||
- ✅ **Archive filter toggle** — `filter_list` icon button in archive header row toggles the filter card visibility on click
|
||||
- ✅ **Route page close button** — `X` (close) icon button added right-aligned in the route page header; calls `window.close()` to close the browser tab
|
||||
- ✅ **Responsive header** — On viewports < 600px, header text labels are hidden; only icon buttons (menu, dark mode toggle, close) remain visible
|
||||
|
||||
### Impact
|
||||
- Archive page is cleaner by default — filters only shown when needed
|
||||
- Route page visually consistent with the main dashboard (DOMCA theme, dark mode, responsive panel width)
|
||||
- Headers degrade gracefully on mobile (< 600px): only icon buttons visible, no text overflow
|
||||
- No functional changes — all event handlers, callbacks, data bindings, logic and imports are identical to the input
|
||||
|
||||
---
|
||||
|
||||
## [1.9.6] - 2026-02-17 — Bugfix: Channel Discovery Reliability
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Channels not appearing (especially on mobile)** — Channel discovery aborted too early on slow BLE connections. The `_discover_channels()` probe used a single attempt per channel slot and stopped after just 2 consecutive empty responses. On mobile BLE stacks (WebBluetooth via NiceGUI) where GATT responses are slower, this caused discovery to abort before finding any channels, falling back to only `[0] Public`
|
||||
- 🛠 **Race condition: channel update flag lost between threads** — `get_snapshot()` and `clear_update_flags()` were two separate calls, each acquiring the lock independently. If the BLE worker set `channels_updated = True` between these two calls, the GUI consumed the flag via `get_snapshot()` but then `clear_update_flags()` reset it — causing the channel submenu and dropdown to never populate
|
||||
- 🛠 **Channels disappear on browser reconnect** — When a browser tab is closed and reopened, `render()` creates new (empty) NiceGUI containers for the drawer submenus, but did not reset `_last_channel_fingerprint`. The `_update_submenus()` method compared the new fingerprint against the stale one, found them equal, and skipped the rebuild — leaving the new containers permanently empty. Fixed by resetting both `_last_channel_fingerprint` and `_last_rooms_fingerprint` in `render()`
|
||||
|
||||
### Changed
|
||||
- 🔄 `core/shared_data.py`: New atomic method `get_snapshot_and_clear_flags()` that reads the snapshot and resets all update flags in a single lock acquisition. Internally refactored to `_build_snapshot_unlocked()` helper. Existing `get_snapshot()` and `clear_update_flags()` retained for backward compatibility
|
||||
- 🔄 `ble/worker.py`: `_discover_channels()` — `max_attempts` increased from 1 to 2 per channel slot; inter-attempt `delay` increased from 0.5s to 1.0s; consecutive error threshold raised from 2 to 3; inter-channel pause increased from 0.15s to 0.3s for mobile BLE stack breathing room
|
||||
- 🔄 `gui/dashboard.py`: `_update_ui()` now uses `get_snapshot_and_clear_flags()` instead of separate `get_snapshot()` + `clear_update_flags()`; `render()` now resets `_last_channel_fingerprint` and `_last_rooms_fingerprint` to `None` so that `_update_submenus()` rebuilds into the freshly created containers; channel-dependent updates (`update_filters`, `update_channel_options`, `_update_submenus`) now run unconditionally when channel data exists — safe because each method has internal idempotency checks
|
||||
- 🔄 `gui/panels/messages_panel.py`: `update_channel_options()` now includes an equality check on options dict to skip redundant `.update()` calls to the NiceGUI client on every 500ms timer tick
|
||||
|
||||
### Impact
|
||||
- Channel discovery now survives transient BLE timeouts that are common on mobile connections
|
||||
- Atomic snapshot eliminates the threading race condition that caused channels to silently never appear
|
||||
- Browser close+reopen no longer loses channels — the single-instance timer race on the shared `DashboardPage` is fully mitigated
|
||||
- No breaking changes — all existing API methods retained, all other functionality unchanged
|
||||
|
||||
---
|
||||
|
||||
## [1.9.5] - 2026-02-16 — Layout Fix: RX Log Table Responsive Sizing
|
||||
|
||||
### Fixed
|
||||
- 🛠 **RX Log table did not adapt to panel/card size** — The table used `max-h-48` (a maximum height cap) instead of a responsive fixed height, causing it to remain small regardless of available space. Changed to `h-40` which is overridden by the existing dashboard CSS to `calc(100vh - 20rem)` — the same responsive pattern used by the Messages panel
|
||||
- 🛠 **RX Log table did not fill card width** — Added `w-full` class to the table element so it stretches to the full width of the parent card
|
||||
- 🛠 **RX Log card did not fill panel height** — Added `flex-grow` class to the card container so it expands to fill the available panel space
|
||||
|
||||
### Changed
|
||||
- 🔄 `gui/panels/rxlog_panel.py`: Card classes `'w-full'` → `'w-full flex-grow'` (line 45); table classes `'text-xs max-h-48 overflow-y-auto'` → `'w-full text-xs h-40 overflow-y-auto'` (line 65)
|
||||
|
||||
### Impact
|
||||
- RX Log table now fills the panel consistently on both desktop and mobile viewports
|
||||
- Layout is consistent with other panels (Messages, Contacts) that use the same `h-40` responsive height pattern
|
||||
- No functional changes — all event handlers, callbacks, data bindings, logica and imports are identical to the input
|
||||
|
||||
---
|
||||
|
||||
## [1.9.4] - 2026-02-16 — BLE Address Log Prefix & Entry Point Cleanup
|
||||
|
||||
### Added
|
||||
- ✅ **BLE address prefix in log filename** — Log file is now named `<BLE_ADDRESS>_meshcore_gui.log` (e.g. `AA_BB_CC_DD_EE_FF_meshcore_gui.log`) instead of the generic `meshcore_gui.log`. Makes it easy to identify which device produced which log file when running multiple instances
|
||||
- New helper `_sanitize_ble_address()` strips `literal:` prefix and replaces colons with underscores
|
||||
- New function `configure_log_file(ble_address)` updates `LOG_FILE` at runtime before the logger is initialised
|
||||
- Rotated backups follow the same naming pattern automatically
|
||||
|
||||
### Removed
|
||||
- ❌ **`meshcore_gui/meshcore_gui.py`** — Redundant copy of `main()` that was never imported. All three entry points (`meshcore_gui.py` root, `__main__.py`, and `meshcore_gui/meshcore_gui.py`) contained near-identical copies of the same logic, causing changes to be missed (as demonstrated by this fix). `__main__.py` is now the single source of truth; root `meshcore_gui.py` is a thin wrapper that imports from it
|
||||
|
||||
### Changed
|
||||
- 🔄 `config.py`: Added `_sanitize_ble_address()` and `configure_log_file()`; version bumped to `1.9.4`
|
||||
- 🔄 `__main__.py`: Added `config.configure_log_file(ble_address)` call before any debug output
|
||||
- 🔄 `meshcore_gui.py` (root): Reduced to 4-line wrapper importing `main` from `__main__`
|
||||
|
||||
### Impact
|
||||
- Log files are now identifiable per BLE device
|
||||
- Single source of truth for `main()` eliminates future sync issues between entry points
|
||||
- Both startup methods (`python meshcore_gui.py` and `python -m meshcore_gui`) remain functional
|
||||
- No breaking changes — defaults and all existing behaviour unchanged
|
||||
---
|
||||
|
||||
## [1.9.3] - 2026-02-16 — Bugfix: Map Default Location & Payload Type Decoding
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Map centred on hardcoded Zwolle instead of device location** — All Leaflet maps used magic-number coordinates `(52.5, 6.0)` as initial centre and fallback. These are now replaced by a single configurable constant `DEFAULT_MAP_CENTER` in `config.py`. Once the device reports a valid `adv_lat`/`adv_lon`, maps re-centre on the actual device position (existing behaviour, unchanged)
|
||||
- 🛠 **Payload type shown as raw integer** — Payload type is now retrieved from the decoded payload and translated to human-readable text using MeshCoreDecoder functions, instead of displaying the raw numeric type value
|
||||
|
||||
### Changed
|
||||
- 🔄 `config.py`: Added `DEFAULT_MAP_CENTER` (default: `(52.5168, 6.0830)`) and `DEFAULT_MAP_ZOOM` (default: `9`) constants in new **MAP DEFAULTS** section. Version bumped to `1.9.2`
|
||||
- 🔄 `gui/panels/map_panel.py`: Imports `DEFAULT_MAP_CENTER` and `DEFAULT_MAP_ZOOM` from config; `ui.leaflet(center=...)` uses config constants instead of hardcoded values
|
||||
- 🔄 `gui/route_page.py`: Imports `DEFAULT_MAP_CENTER` and `DEFAULT_MAP_ZOOM` from config; fallback coordinates (`or 52.5` / `or 6.0`) replaced by `DEFAULT_MAP_CENTER[0]` / `[1]`; zoom uses `DEFAULT_MAP_ZOOM`
|
||||
|
||||
### Impact
|
||||
- Map default location is now a single-point-of-change in `config.py`
|
||||
- Payload type is displayed as readable text instead of a raw number
|
||||
- No breaking changes — all existing map behaviour (re-centre on device position, contact markers) unchanged
|
||||
|
||||
## [1.9.2] - 2026-02-15 — CLI Parameters & Cleanup
|
||||
|
||||
### Added
|
||||
- ✅ **`--port=PORT` CLI parameter** — Web server port is now configurable at startup (default: `8081`). Allows running multiple instances simultaneously on different ports
|
||||
- ✅ **`--ble-pin=PIN` CLI parameter** — BLE pairing PIN is now configurable at startup (default: `123456`). Eliminates the need to edit `config.py` for devices with a non-default PIN, and works in systemd service files
|
||||
- ✅ **Per-device log file** — Debug log file now includes the BLE address in its filename (e.g. `F0_9E_9E_75_A3_01_meshcore_gui.log`), so multiple instances log to separate files
|
||||
|
||||
### Fixed
|
||||
- 🛠 **BLE PIN not applied from CLI** — `ble/worker.py` imported `BLE_PIN` as a constant at module load time (`from config import BLE_PIN`), capturing the default value `"123456"` before CLI parsing could override `config.BLE_PIN`. Changed to runtime access via `config.BLE_PIN` so the `--ble-pin` parameter is correctly passed to the BLE agent
|
||||
|
||||
### Removed
|
||||
- ❌ **Redundant `meshcore_gui/meshcore_gui.py`** — This file was a near-identical copy of both `meshcore_gui.py` (top-level) and `meshcore_gui/__main__.py`, but was never imported or referenced. Removed to eliminate maintenance risk. The two remaining entry points cover all startup methods: `python meshcore_gui.py` and `python -m meshcore_gui`
|
||||
|
||||
### Impact
|
||||
- Multiple instances can run side-by-side with different ports, PINs and log files
|
||||
- Service deployments no longer require editing `config.py` — all runtime settings via CLI
|
||||
- No breaking changes — all defaults are unchanged
|
||||
|
||||
---
|
||||
|
||||
## [1.9.1] - 2026-02-14 — Bugfix: Dual Reconnect Conflict
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Library reconnect interfered with application reconnect** — The meshcore library's internal `auto_reconnect` (visible in logs as `"Attempting reconnection 1/3"`) ran a fast 3-attempt reconnect cycle without bond cleanup. This prevented the application's own `reconnect_loop` (which does `remove_bond()` + backoff) from succeeding, because BlueZ retained a stale bond → `"failed to discover service"`
|
||||
|
||||
### Changed
|
||||
- 🔄 `ble/worker.py`: Set `auto_reconnect=False` in both `MeshCore.create_ble()` call sites (`_connect()` and `_create_fresh_connection()`), so only the application's bond-aware `reconnect_loop` handles reconnection
|
||||
- 🔄 `ble/worker.py`: Added `"failed to discover"` and `"service discovery"` to disconnect detection keywords for defensive coverage
|
||||
|
||||
### Impact
|
||||
- Eliminates the ~9 second wasted library reconnect cycle after every BLE disconnect
|
||||
- Application's `reconnect_loop` (with bond cleanup) now runs immediately after disconnect detection
|
||||
- No breaking changes — the application reconnect logic was already fully functional
|
||||
|
||||
---
|
||||
|
||||
## [1.9.0] - 2026-02-14 — BLE Connection Stability
|
||||
|
||||
### Added
|
||||
- ✅ **Built-in BLE PIN agent** — New `ble/ble_agent.py` registers a D-Bus agent with BlueZ to handle PIN pairing requests automatically. Eliminates the need for external `bt-agent.service` and `bluez-tools` package
|
||||
- Uses `dbus_fast` (already a dependency of `bleak`, no new packages)
|
||||
- Supports `RequestPinCode`, `RequestPasskey`, `DisplayPasskey`, `RequestConfirmation`, `AuthorizeService` callbacks
|
||||
- Configurable PIN via `BLE_PIN` in `config.py` (default: `123456`)
|
||||
- ✅ **Automatic bond cleanup** — New `ble/ble_reconnect.py` provides `remove_bond()` function that removes stale BLE bonds via D-Bus, equivalent to `bluetoothctl remove <address>`. Called automatically on startup and before each reconnect attempt
|
||||
- ✅ **Automatic reconnect after disconnect** — BLEWorker main loop now detects BLE disconnects (via connection error exceptions) and automatically triggers a reconnect sequence: bond removal → linear backoff wait → fresh connection → re-wire handlers → reload device data
|
||||
- Configurable via `RECONNECT_MAX_RETRIES` (default: 5) and `RECONNECT_BASE_DELAY` (default: 5.0s)
|
||||
- After all retries exhausted: waits 60s then starts a new retry cycle (infinite recovery)
|
||||
- ✅ **Generic install script** — `install_ble_stable.sh` auto-detects user, project directory, venv path and entry point to generate systemd service and D-Bus policy. Supports `--uninstall` flag
|
||||
|
||||
### Changed
|
||||
- 🔄 **`ble/worker.py`** — `_async_main()` rewritten with three phases: (1) start PIN agent, (2) remove stale bond, (3) connect + main loop with disconnect detection. Reconnect logic re-wires all event handlers and reloads device data after successful reconnection
|
||||
- 🔄 **`config.py`** — Added `BLE_PIN`, `RECONNECT_MAX_RETRIES`, `RECONNECT_BASE_DELAY` constants
|
||||
|
||||
### Removed
|
||||
- ❌ **`bt-agent.service` dependency** — No longer needed; PIN pairing is handled by the built-in agent
|
||||
- ❌ **`bluez-tools` system package** — No longer needed
|
||||
- ❌ **`~/.meshcore-ble-pin` file** — No longer needed
|
||||
- ❌ **Manual `bluetoothctl remove` before startup** — Handled automatically
|
||||
- ❌ **`ExecStartPre` in systemd service** — Bond cleanup is internal
|
||||
|
||||
### Impact
|
||||
- Zero external dependencies for BLE pairing on Linux
|
||||
- Automatic recovery from the T1000e ~2 hour BLE disconnect issue
|
||||
- No manual intervention needed after BLE connection loss
|
||||
- Single systemd service (`meshcore-gui.service`) manages everything
|
||||
- No breaking changes to existing functionality
|
||||
|
||||
---
|
||||
|
||||
## [1.8.0] - 2026-02-14 — DRY Message Construction & Archive Layout Unification
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Case-sensitive prefix matching** — `get_contact_name_by_prefix()` and `get_contact_by_prefix()` in `shared_data.py` failed to match path hashes (uppercase, e.g. `'B8'`) against contact pubkeys (lowercase, e.g. `'b8a3f2...'`). Added `.lower()` to both sides of the comparison, consistent with `_resolve_path_names()` which already had it
|
||||
- 🛠 **Route page 404 from archive** — Archive page linked to `/route/{hash}` but route was registered as `/route/{msg_index:int}`, causing a JSON parse error for hex hash strings. Route parameter changed to `str` with 3-strategy lookup (index → memory hash → archive fallback)
|
||||
- 🛠 **Three entry points out of sync** — `meshcore_gui.py` (root), `meshcore_gui/meshcore_gui.py` (inner) and `meshcore_gui/__main__.py` had diverging route registrations. All three now use identical `/route/{msg_key}` with `str` parameter
|
||||
|
||||
### Changed
|
||||
- 🔄 **`core/models.py` — DRY factory methods and formatting**
|
||||
- `Message.now_timestamp()`: static method replacing 7× hardcoded `datetime.now().strftime('%H:%M:%S')` across `events.py` and `commands.py`
|
||||
- `Message.incoming()`: classmethod factory for received messages (`direction='in'`, auto-timestamp)
|
||||
- `Message.outgoing()`: classmethod factory for sent messages (`sender='Me'`, `direction='out'`, auto-timestamp)
|
||||
- `Message.format_line(channel_names)`: single-line display formatting (`"12:34:56 ← [Public] [2h✓] PE1ABC: Hello mesh!"`), replacing duplicate inline formatting in `messages_panel.py` and `archive_page.py`
|
||||
- 🔄 **`ble/events.py`** — 4× `Message(...)` constructors replaced by `Message.incoming()`; `datetime` import removed
|
||||
- 🔄 **`ble/commands.py`** — 3× `Message(...)` constructors replaced by `Message.outgoing()`; `datetime` import removed
|
||||
- 🔄 **`gui/panels/messages_panel.py`** — 15 lines inline formatting replaced by single `msg.format_line(channel_names)` call
|
||||
- 🔄 **`gui/archive_page.py` — Layout unified with main page**
|
||||
- Multi-row card layout replaced by single-line `msg.format_line()` in monospace container (same style as main page)
|
||||
- DM added to channel filter dropdown (post-filter on `channel is None`)
|
||||
- Message click opens `/route/{message_hash}` in new tab (was: no click handler on archive messages)
|
||||
- Removed `_render_message_card()` (98 lines) and `_render_archive_route()` (75 lines)
|
||||
- Removed `RouteBuilder` dependency and `TYPE_LABELS` import
|
||||
- File reduced from 445 to 267 lines
|
||||
- 🔄 **`gui/route_page.py`** — `render(msg_index: int)` → `render(msg_key: str)` with 3-strategy message lookup: (1) numeric index from in-memory list, (2) hash match in memory, (3) `archive.get_message_by_hash()` fallback
|
||||
- 🔄 **`services/message_archive.py`** — New method `get_message_by_hash(hash)` for single-message lookup by packet hash
|
||||
- 🔄 **`__main__.py` + `meshcore_gui.py` (both)** — Route changed from `/route/{msg_index}` (int) to `/route/{msg_key}` (str)
|
||||
|
||||
### Impact
|
||||
- DRY: timestamp formatting 7→1 definition, message construction 7→2 factories, line formatting 2→1 method
|
||||
- Archive page visually consistent with main messages panel (single-line, monospace)
|
||||
- Archive messages now clickable to open route visualization (was: only in-memory messages)
|
||||
- Case-insensitive prefix matching fixes path name resolution for contacts with uppercase path hashes
|
||||
- No breaking changes to BLE protocol handling, dedup, bot, or data storage
|
||||
|
||||
### Known Limitations
|
||||
- DM filter in archive uses post-filtering (query without channel filter + filter on `channel is None`); becomes exact when `query_messages()` gets native DM support
|
||||
|
||||
### Parked for later
|
||||
- Multi-path tracking (enrich RxLogEntry with multiple path observations)
|
||||
- Events correlation improvements (only if proven data loss after `.lower()` fix)
|
||||
|
||||
---
|
||||
|
||||
## [1.7.0] - 2026-02-13 — Archive Channel Name Persistence
|
||||
|
||||
### Added
|
||||
- ✅ **Channel name stored in archive** — Messages now persist `channel_name` alongside the numeric `channel` index in `<ADDRESS>_messages.json`, so archived messages retain their human-readable channel name even when the device is not connected
|
||||
- `Message` dataclass: new field `channel_name: str` (default `""`, backward compatible)
|
||||
- `SharedData.add_message()`: automatically resolves `channel_name` from the live channels list when not already set (new helper `_resolve_channel_name()`)
|
||||
- `MessageArchive.add_message()`: writes `channel_name` to the JSON dict
|
||||
- ✅ **Archive channel selector built from archived data** — Channel filter dropdown on `/archive` now populated via `SELECT DISTINCT channel_name` on the archive instead of the live BLE channels list
|
||||
- New method `MessageArchive.get_distinct_channel_names()` returns sorted unique channel names from stored messages
|
||||
- Selector shows only channels that actually have archived messages
|
||||
- ✅ **Archive filter on channel name** — `MessageArchive.query_messages()` parameter changed from `channel: Optional[int]` to `channel_name: Optional[str]` (exact match on name string)
|
||||
|
||||
### Changed
|
||||
- 🔄 `core/models.py`: Added `channel_name` field to `Message` dataclass and `from_dict()`
|
||||
- 🔄 `core/shared_data.py`: `add_message()` resolves channel name; added `_resolve_channel_name()` helper
|
||||
- 🔄 `services/message_archive.py`: `channel_name` persisted in JSON; `query_messages()` filters by name; new `get_distinct_channel_names()` method
|
||||
- 🔄 `gui/archive_page.py`: Channel selector built from `archive.get_distinct_channel_names()`; filter state changed from `_channel_filter` (int) to `_channel_name_filter` (str); message cards show `channel_name` directly from archive
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Main page empty after startup** — After a restart the messages panel showed no messages until new live BLE traffic arrived. `SharedData.load_recent_from_archive()` now loads up to 100 recent archived messages during the cache-first startup phase, so historical messages are immediately visible
|
||||
- New method `SharedData.load_recent_from_archive(limit)` — reads from `MessageArchive.query_messages()` and populates the in-memory list without re-archiving
|
||||
- `BLEWorker._apply_cache()` calls `load_recent_from_archive()` at the end of cache loading
|
||||
|
||||
### Impact
|
||||
- Archived messages now self-contained — channel name visible without live BLE connection
|
||||
- Main page immediately shows historical messages after startup (no waiting for live BLE traffic)
|
||||
- Backward compatible — old archive entries without `channel_name` fall back to `"Ch <idx>"`
|
||||
- No breaking changes to existing functionality
|
||||
|
||||
---
|
||||
|
||||
## [1.6.0] - 2026-02-13 — Dashboard Layout Consolidation
|
||||
|
||||
### Changed
|
||||
- 🔄 **Messages panel consolidated** — Filter checkboxes (DM + channels) and message input (text field, channel selector, Send button) are now integrated into the Messages panel, replacing the separate Filter and Input panels
|
||||
- DM + channel checkboxes displayed centered in the Messages header row, between the "💬 Messages" label and the "📚 Archive" button
|
||||
- Message input row (text field, channel selector, Send button) placed below the message list within the same card
|
||||
- `messages_panel.py`: Constructor now accepts `put_command` callable; added `update_filters(data)`, `update_channel_options(channels)` methods and `channel_filters`, `last_channels` properties (all logic 1:1 from FilterPanel/InputPanel); `update()` signature unchanged
|
||||
- 🔄 **Actions panel expanded** — BOT toggle checkbox moved from Filter panel to Actions panel, below the Refresh/Advert buttons
|
||||
- `actions_panel.py`: Constructor now accepts `set_bot_enabled` callable; added `update(data)` method for BOT state sync; `_on_bot_toggle()` logic 1:1 from FilterPanel
|
||||
- 🔄 **Dashboard layout simplified** — Centre column reduced from 4 panels (Map → Input → Filter → Messages) to 2 panels (Map → Messages)
|
||||
- `dashboard.py`: FilterPanel and InputPanel no longer rendered; all dependencies rerouted to MessagesPanel and ActionsPanel; `_update_ui()` call-sites updated accordingly
|
||||
|
||||
### Removed (from layout, files retained)
|
||||
- ❌ **Filter panel** no longer rendered as separate panel — `filter_panel.py` retained in codebase but not instantiated in dashboard
|
||||
- ❌ **Input panel** no longer rendered as separate panel — `input_panel.py` retained in codebase but not instantiated in dashboard
|
||||
|
||||
### Impact
|
||||
- Cleaner, more compact dashboard: 2 fewer panels in the centre column
|
||||
- All functionality preserved — message filtering, send, BOT toggle, archive all work identically
|
||||
- No breaking changes to BLE, services, core or other panels
|
||||
|
||||
---
|
||||
|
||||
<!-- ADDED: v1.5.0 feature + bugfix entry -->
|
||||
|
||||
## [1.5.0] - 2026-02-11 — Room Server Support, Dynamic Channel Discovery & Contact Management
|
||||
|
||||
### Added
|
||||
- ✅ **Room Server panel** — Dedicated per-room-server message panel in the centre column below Messages. Each Room Server (type=3 contact) gets its own `ui.card()` with login/logout controls and message display
|
||||
- Click a Room Server contact to open an add/login dialog with password field
|
||||
- After login: messages are displayed in the room card; send messages directly from the room panel
|
||||
- Password row + login button automatically replaced by Logout button after successful login
|
||||
- Room Server author attribution via `signature` field (txt_type=2) — real message author is resolved from the 4-byte pubkey prefix, not the room server pubkey
|
||||
- New panel: `gui/panels/room_server_panel.py` — per-room card management with login state tracking
|
||||
- ✅ **Room Server password store** — Passwords stored outside the repository in `~/.meshcore-gui/room_passwords/<ADDRESS>.json`
|
||||
- New service: `services/room_password_store.py` — JSON-backed persistent password storage per BLE device, analogous to `PinStore`
|
||||
- Room panels are restored from stored passwords on app restart
|
||||
- ✅ **Dynamic channel discovery** — Channels are now auto-discovered from the device at startup via `get_channel()` BLE probing, replacing the hardcoded `CHANNELS_CONFIG`
|
||||
- Single-attempt probe per channel slot with early stop after 2 consecutive empty slots
|
||||
- Channel name and encryption key extracted in a single pass (combined discovery + key loading)
|
||||
- Configurable channel caching via `CHANNEL_CACHE_ENABLED` (default: `False` — always fresh from device)
|
||||
- `MAX_CHANNELS` setting (default: 8) controls how many slots are probed
|
||||
- ✅ **Individual contact deletion** — 🗑️ delete button per unpinned contact in the contacts list, with confirmation dialog
|
||||
- New command: `remove_single_contact` in BLE command handler
|
||||
- Pinned contacts are protected (no delete button shown)
|
||||
- ✅ **"Also delete from history" option** — Checkbox in the Clean up confirmation dialog to also remove locally cached contact data
|
||||
|
||||
<!-- ADDED: Research document reference -->
|
||||
- ✅ **Room Server protocol research** — `RoomServer_Companion_App_Onderzoek.md` documents the full companion app message flow (login, push protocol, signature mechanism, auto_message_fetching)
|
||||
|
||||
### Changed
|
||||
- 🔄 `config.py`: Removed `CHANNELS_CONFIG` constant; added `MAX_CHANNELS` (default: 8) and `CHANNEL_CACHE_ENABLED` (default: `False`)
|
||||
- 🔄 `ble/worker.py`: Replaced hardcoded channel loading with `_discover_channels()` method; added `_try_get_channel_info()` helper; `_apply_cache()` respects `CHANNEL_CACHE_ENABLED` setting; removed `_load_channel_keys()` (integrated into discovery pass)
|
||||
- 🔄 `ble/commands.py`: Added `login_room`, `send_room_msg` and `remove_single_contact` command handlers
|
||||
- 🔄 `gui/panels/contacts_panel.py`: Contact click now dispatches by type — type=3 (Room Server) opens room dialog, others open DM dialog; added `on_add_room` callback parameter; added 🗑️ delete button per unpinned contact
|
||||
- 🔄 `gui/panels/messages_panel.py`: Room Server messages filtered from general message view via `_is_room_message()` with prefix matching; `update()` accepts `room_pubkeys` parameter
|
||||
- 🔄 `gui/dashboard.py`: Added `RoomServerPanel` in centre column; `_update_ui()` passes `room_pubkeys` to Messages panel; added `_on_add_room_server` callback
|
||||
- 🔄 `gui/panels/filter_panel.py`: Channel filter checkboxes now built dynamically from discovered channels (no hardcoded references)
|
||||
- 🔄 `services/bot.py`: Removed stale comment referencing hardcoded channels
|
||||
|
||||
### Fixed
|
||||
- 🛠 **Room Server messages appeared as DM** — Messages from Room Servers (txt_type=2) were displayed in the general Messages panel as direct messages. They are now filtered out and shown exclusively in the Room Server panel
|
||||
- 🛠 **Historical room messages not shown after login** — Post-login fetch loop was polling `get_msg()` before room server had time to push messages over LoRa RF (10–75s per message). Removed redundant fetch loop; the library's `auto_message_fetching` handles `MESSAGES_WAITING` events correctly and event-driven
|
||||
- 🛠 **Author attribution incorrect for room messages** — Room server messages showed the room server name as sender instead of the actual message author. Now correctly resolved from the `signature` field (4-byte pubkey prefix) via contact lookup
|
||||
|
||||
### Impact
|
||||
- Room Servers are now first-class citizens in the GUI with dedicated panels
|
||||
- Channel configuration no longer requires manual editing of `config.py`
|
||||
- Contact list management is more granular with per-contact deletion
|
||||
- No breaking changes to existing functionality (messages, DM, map, archive, bot, etc.)
|
||||
|
||||
---
|
||||
|
||||
## [1.4.0] - 2026-02-09 — SDK Event Race Condition Fix
|
||||
|
||||
### Fixed
|
||||
- 🛠 **BLE startup delay of ~2 minutes eliminated** — The meshcore Python SDK (`commands/base.py`) dispatched device response events before `wait_for_events()` registered its subscription. On busy networks with frequent `RX_LOG_DATA` events, this caused `send_device_query()` and `get_channel()` to fail repeatedly with `no_event_received`, wasting 110+ seconds in timeouts
|
||||
|
||||
### Changed
|
||||
- 📄 `meshcore` SDK `commands/base.py`: Rewritten `send()` method to subscribe to expected events **before** transmitting the BLE command (subscribe-before-send pattern), matching the approach used by the companion apps (meshcore.js, iOS, Android). Submitted upstream as [meshcore_py PR #52](https://github.com/meshcore-dev/meshcore_py/pull/52)
|
||||
|
||||
### Impact
|
||||
- Startup time reduced from ~2+ minutes to ~10 seconds on busy networks
|
||||
- All BLE commands (`send_device_query`, `get_channel`, `get_bat`, `send_appstart`, etc.) now succeed on first attempt instead of requiring multiple retries
|
||||
- No changes to meshcore_gui code required — the fix is entirely in the meshcore SDK
|
||||
|
||||
### Temporary Installation
|
||||
Until the fix is merged upstream, install the patched meshcore SDK:
|
||||
```bash
|
||||
pip install --force-reinstall git+https://github.com/PE1HVH/meshcore_py.git@fix/event-race-condition
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
<!-- ADDED: v1.3.2 bugfix entry -->
|
||||
|
||||
## [1.3.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: v1.3.1 bugfix entry -->
|
||||
|
||||
## [1.3.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: New v1.3.0 entry at top -->
|
||||
|
||||
## [1.3.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
|
||||
|
||||
---
|
||||
|
||||
## [1.2.0] - 2026-02-08 — Contact Maintenance Feature
|
||||
|
||||
### Added
|
||||
- ✅ **Pin/Unpin contacts** (Iteration A) — Toggle to pin individual contacts, protecting them from bulk deletion
|
||||
- Persistent pin state stored in `~/.meshcore-gui/cache/<ADDRESS>_pins.json`
|
||||
- Pinned contacts visually marked with yellow background
|
||||
- Pinned contacts sorted to top of contact list
|
||||
- Pin state survives app restart
|
||||
- New service: `services/pin_store.py` — JSON-backed persistent pin storage
|
||||
|
||||
- ✅ **Bulk delete unpinned contacts** (Iteration B) — Remove all unpinned contacts from device in one action
|
||||
- "🧹 Clean up" button in contacts panel with confirmation dialog
|
||||
- Shows count of contacts to be removed vs. pinned contacts kept
|
||||
- Progress status updates during removal
|
||||
- Automatic device resync after completion
|
||||
- New service: `services/contact_cleaner.py` — ContactCleanerService with purge statistics
|
||||
|
||||
- ✅ **Auto-add contacts toggle** (Iteration C) — Control whether device automatically adds new contacts from mesh adverts
|
||||
- "📥 Auto-add" checkbox in contacts panel (next to Clean up button)
|
||||
- Syncs with device via `set_manual_add_contacts()` SDK call
|
||||
- Inverted logic handled internally (UI "Auto-add ON" = `set_manual_add_contacts(false)`)
|
||||
- Optimistic update with automatic rollback on BLE failure
|
||||
- State synchronized from device on each GUI update cycle
|
||||
|
||||
### Changed
|
||||
- 🔄 `contacts_panel.py`: Added pin checkbox per contact, purge button, auto-add toggle, DM dialog (all existing functionality preserved)
|
||||
- 🔄 `commands.py`: Added `purge_unpinned` and `set_auto_add` command handlers
|
||||
- 🔄 `shared_data.py`: Added `auto_add_enabled` field with thread-safe getter/setter
|
||||
- 🔄 `protocols.py`: Added `set_auto_add_enabled` and `is_auto_add_enabled` to Writer and Reader protocols
|
||||
- 🔄 `dashboard.py`: Passes `PinStore` and `set_auto_add_enabled` callback to ContactsPanel
|
||||
- 🔄 **UI language**: All Dutch strings in `contacts_panel.py` and `commands.py` translated to English
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
|
||||
### Changed
|
||||
- 🔄 **CHANGELOG.md**: Corrected version numbering to semantic versioning, fixed inaccurate references (archive button location, filter state persistence)
|
||||
- 🔄 **README.md**: Added Message Archive feature, updated project structure, configuration table and architecture diagram
|
||||
- 🔄 **MeshCore_GUI_Design.docx**: Added ArchivePage, MessageArchive, Models components; updated project structure, protocols, configuration and version history
|
||||
|
||||
---
|
||||
|
||||
## [1.1.0] - 2026-02-07 — Archive Viewer Feature
|
||||
|
||||
|
||||
### Added
|
||||
- ✅ **Archive Viewer Page** (`/archive`) — Full-featured message archive browser
|
||||
- Pagination (50 messages per page, configurable)
|
||||
- Channel filter dropdown (All + configured channels)
|
||||
- Time range filter (24h, 7d, 30d, 90d, All time)
|
||||
- Text search (case-insensitive)
|
||||
- Filter state stored in instance variables (reset on page reload)
|
||||
- Message cards with same styling as main messages panel
|
||||
- Clickable messages for route visualization (where available)
|
||||
- **💬 Reply functionality** — Expandable reply panel per message
|
||||
- **🗺️ Inline route table** — Expandable route display per archive message with sender, repeaters and receiver (names, IDs, node types)
|
||||
- *(Note: Reply panels and inline route tables removed in v1.8.0, replaced by click-to-route navigation via message hash)*
|
||||
|
||||
<!-- CHANGED: "Filter state persistence (app.storage.user)" replaced with "Filter state stored in
|
||||
instance variables" — the code (archive_page.py:36-40) uses self._current_page etc.,
|
||||
not app.storage.user. The comment in the code is misleading. -->
|
||||
|
||||
<!-- ADDED: "Inline route table" entry — _render_archive_route() in archive_page.py:333-407
|
||||
was not documented. -->
|
||||
|
||||
- ✅ **MessageArchive.query_messages()** method
|
||||
- Filter by: time range, channel, text search, sender
|
||||
- Pagination support (limit, offset)
|
||||
- Returns tuple: (messages, total_count)
|
||||
- Sorting: Newest first
|
||||
|
||||
- ✅ **UI Integration**
|
||||
- "📚 Archive" button in Messages panel header (opens in new tab)
|
||||
- Back to Dashboard button in archive page
|
||||
|
||||
<!-- CHANGED: "📚 View Archive button in Actions panel" corrected — the button is in
|
||||
MessagesPanel (messages_panel.py:25), not in ActionsPanel (actions_panel.py).
|
||||
ActionsPanel only contains Refresh and Advert buttons. -->
|
||||
|
||||
- ✅ **Reply Panel**
|
||||
- Expandable reply per message (💬 Reply button)
|
||||
- Pre-filled with @sender mention
|
||||
- Channel selector
|
||||
- Send button with success notification
|
||||
- Auto-close expansion after send
|
||||
|
||||
### Changed
|
||||
- 🔄 `SharedData.get_snapshot()`: Now includes `'archive'` field
|
||||
- 🔄 `MessagesPanel`: Added archive button in header row
|
||||
- 🔄 Both entry points (`__main__.py` and `meshcore_gui.py`): Register `/archive` route
|
||||
|
||||
<!-- CHANGED: "ActionsPanel: Added archive button" corrected to "MessagesPanel" -->
|
||||
|
||||
### Performance
|
||||
- Query: ~10ms for 10k messages with filters
|
||||
- Memory: ~10KB per page (50 messages)
|
||||
- No impact on main UI (separate page)
|
||||
|
||||
### Known Limitations
|
||||
- ~~Route visualization only works for messages in recent buffer (last 100)~~ — Fixed in v1.8.0: archive messages now support click-to-route via `get_message_by_hash()` fallback
|
||||
- Text search is linear scan (no indexing yet)
|
||||
- Sender filter exists in API but not in UI yet
|
||||
|
||||
---
|
||||
|
||||
## [1.0.3] - 2026-02-07 — Critical Bugfix: Archive Overwrite Prevention
|
||||
|
||||
|
||||
### 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
|
||||
|
||||
### Changed
|
||||
- 🔄 `_flush_messages()`: Early return on read error instead of overwriting
|
||||
- 🔄 `_flush_rxlog()`: Early return on read error instead of overwriting
|
||||
- 🔄 Better error messages for version mismatch and JSON decode errors
|
||||
|
||||
### Details
|
||||
**Problem:** If the existing archive file had a JSON parse error or version mismatch,
|
||||
the flush operation would proceed with `existing_messages = []`, effectively
|
||||
overwriting all historical data with only the new buffered messages.
|
||||
|
||||
**Solution:** The flush methods now:
|
||||
1. Try to read existing archive first
|
||||
2. If read fails (JSON error, version mismatch, IO error), abort the flush
|
||||
3. Keep buffer intact for next retry
|
||||
4. Only clear buffer after successful write
|
||||
|
||||
**Impact:** No data loss on restart or when archive files have issues.
|
||||
|
||||
### Testing
|
||||
- ✅ Added `test_append_on_restart_not_overwrite()` integration test
|
||||
- ✅ Verifies data is appended across multiple sessions
|
||||
- ✅ All existing tests still pass
|
||||
|
||||
---
|
||||
|
||||
## [1.0.2] - 2026-02-07 — RxLog message_hash Enhancement
|
||||
|
||||
|
||||
### Added
|
||||
- ✅ `message_hash` field added to `RxLogEntry` model
|
||||
- ✅ RxLog entries now include message_hash for correlation with messages
|
||||
- ✅ Archive JSON includes message_hash in rxlog entries
|
||||
|
||||
### Changed
|
||||
- 🔄 `events.py`: Restructured `on_rx_log()` to extract message_hash before creating RxLogEntry
|
||||
- 🔄 `message_archive.py`: Updated rxlog archiving to include message_hash field
|
||||
- 🔄 Tests updated to verify message_hash persistence
|
||||
|
||||
### Benefits
|
||||
- **Correlation**: Link RX log entries to their corresponding messages
|
||||
- **Analysis**: Track which packets resulted in messages
|
||||
- **Debugging**: Better troubleshooting of packet processing
|
||||
|
||||
---
|
||||
|
||||
## [1.0.1] - 2026-02-07 — Entry Point Fix
|
||||
|
||||
|
||||
### Fixed
|
||||
- ✅ `meshcore_gui.py` (root entry point) now passes ble_address to SharedData
|
||||
- ✅ Archive works correctly regardless of how application is started
|
||||
|
||||
### Changed
|
||||
- 🔄 Both entry points (`meshcore_gui.py` and `meshcore_gui/__main__.py`) updated
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] - 2026-02-07 — Message & Metadata Persistence
|
||||
|
||||
|
||||
### Added
|
||||
- ✅ MessageArchive class for persistent storage
|
||||
- ✅ Configurable retention periods (MESSAGE_RETENTION_DAYS, RXLOG_RETENTION_DAYS, CONTACT_RETENTION_DAYS)
|
||||
- ✅ Automatic daily cleanup of old data
|
||||
- ✅ Batch writes for performance
|
||||
- ✅ Thread-safe with separate locks
|
||||
- ✅ Atomic file writes
|
||||
- ✅ Contact retention in DeviceCache
|
||||
- ✅ Archive statistics API
|
||||
- ✅ Comprehensive tests (20+ unit, 8+ integration)
|
||||
- ✅ Full documentation
|
||||
|
||||
### Storage Locations
|
||||
- `~/.meshcore-gui/archive/<ADDRESS>_messages.json`
|
||||
- `~/.meshcore-gui/archive/<ADDRESS>_rxlog.json`
|
||||
|
||||
### Requirements Completed
|
||||
- R1: All incoming messages persistent ✅
|
||||
- R2: All incoming RxLog entries persistent ✅
|
||||
- R3: Configurable retention ✅
|
||||
- R4: Automatic cleanup ✅
|
||||
- R5: Backward compatibility ✅
|
||||
- R6: Contact retention ✅
|
||||
- R7: Archive stats API ✅
|
||||
|
||||
- Fix3: Leaflet asset injection is now per page render instead of process-global, and browser bootstrap now retries until the host element, Leaflet runtime, and MeshCore panel runtime are all available. This fixes blank map containers caused by missing or late-loaded JS/CSS assets.
|
||||
|
||||
- Fix5: Removed per-snapshot map invalidate calls, stopped forcing a default dark theme during map bootstrap, and added client-side interaction/resize guards so zooming stays responsive and the theme no longer jumps back during status-loop updates.
|
||||
|
||||
|
||||
## 2026-03-09 map hotfix v2
|
||||
- regular map snapshots no longer carry theme state
|
||||
- explicit theme changes are now handled only via the dedicated theme channel
|
||||
- initial map render now sends an ensure_map command plus an immediate theme sync
|
||||
- added no-op ensure_map handling in the Leaflet runtime to avoid accidental fallback behaviour
|
||||
@@ -0,0 +1,32 @@
|
||||
### Map developer rules
|
||||
|
||||
To avoid regressions in the map subsystem, follow these rules:
|
||||
|
||||
**Do**
|
||||
|
||||
- Keep the Leaflet map lifecycle **inside the browser runtime**
|
||||
- Initialize Leaflet **exactly once per DOM container**
|
||||
- Send **compact snapshots only** from Python
|
||||
- Update markers **incrementally by node id**
|
||||
- Keep theme handling in a **dedicated theme channel**
|
||||
- Allow the browser runtime to maintain **viewport state**
|
||||
- Define map/tile `maxZoom` **before** attaching clustering layers
|
||||
|
||||
**Do NOT**
|
||||
|
||||
- Recreate the map inside the 500 ms dashboard update loop
|
||||
- Call `L.map(...)` from snapshot handlers, timers or retry loops
|
||||
- Use `ui.leaflet()` or any NiceGUI map wrapper
|
||||
- Embed theme state inside snapshot payloads
|
||||
- Force map center/zoom during normal refresh cycles
|
||||
- Call Leaflet APIs directly from Python
|
||||
- Place the device marker inside the contact cluster layer
|
||||
|
||||
Breaking these rules will reintroduce:
|
||||
|
||||
- disappearing maps
|
||||
- marker flicker
|
||||
- viewport resets
|
||||
- theme resets
|
||||
- cluster bootstrap failures
|
||||
- `Map container is already initialized` errors
|
||||
@@ -0,0 +1,332 @@
|
||||
# Message & Metadata Persistence
|
||||
|
||||
**Version:** 1.0
|
||||
**Author:** PE1HVH
|
||||
**Date:** 2026-02-07
|
||||
|
||||
## Overview
|
||||
|
||||
This feature implements persistent storage for all incoming messages, RX log entries, and contacts with configurable retention periods. The system uses a dual-layer architecture to balance real-time UI performance with comprehensive data retention.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ SharedData (in-memory buffer) │
|
||||
│ - Last 100 messages (UI) │
|
||||
│ - Last 50 rx_log (UI) │
|
||||
│ - Thread-safe via Lock │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ (on every add)
|
||||
▼
|
||||
┌─────────────────────────────────────┐
|
||||
│ MessageArchive (persistent) │
|
||||
│ - All messages (JSON) │
|
||||
│ - All rx_log (JSON) │
|
||||
│ - Retention filtering │
|
||||
│ - Automatic cleanup (daily) │
|
||||
│ - Separate Lock (no contention) │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Separation of Concerns**: SharedData handles real-time UI updates, MessageArchive handles persistence
|
||||
2. **Thread Safety**: Independent locks prevent contention between UI and archiving
|
||||
3. **Batch Writes**: Buffered writes reduce disk I/O (flushes every 10 items or 60 seconds)
|
||||
4. **Configurable Retention**: Automatic cleanup based on configurable periods
|
||||
5. **Backward Compatibility**: SharedData API unchanged, archive is optional
|
||||
|
||||
## Storage Format
|
||||
|
||||
### Messages Archive
|
||||
**Location:** `~/.meshcore-gui/archive/<ADDRESS>_messages.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"address": "literal:AA:BB:CC:DD:EE:FF",
|
||||
"last_updated": "2026-02-07T12:34:56.123456Z",
|
||||
"messages": [
|
||||
{
|
||||
"time": "12:34:56",
|
||||
"timestamp_utc": "2026-02-07T12:34:56.123456Z",
|
||||
"sender": "PE1HVH",
|
||||
"text": "Hello mesh!",
|
||||
"channel": 0,
|
||||
"direction": "in",
|
||||
"snr": 8.5,
|
||||
"path_len": 2,
|
||||
"sender_pubkey": "abc123...",
|
||||
"path_hashes": ["a1", "b2"],
|
||||
"message_hash": "def456..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### RX Log Archive
|
||||
**Location:** `~/.meshcore-gui/archive/<ADDRESS>_rxlog.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"address": "literal:AA:BB:CC:DD:EE:FF",
|
||||
"last_updated": "2026-02-07T12:34:56Z",
|
||||
"entries": [
|
||||
{
|
||||
"time": "12:34:56",
|
||||
"timestamp_utc": "2026-02-07T12:34:56Z",
|
||||
"snr": 8.5,
|
||||
"rssi": -95.0,
|
||||
"payload_type": "MSG",
|
||||
"hops": 2,
|
||||
"message_hash": "def456..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** The `message_hash` field enables correlation between RX log entries and messages. It will be empty for packets that are not messages (e.g., announcements, broadcasts).
|
||||
|
||||
## Configuration
|
||||
|
||||
Add to `meshcore_gui/config.py`:
|
||||
|
||||
```python
|
||||
# Retention period for archived messages (in days)
|
||||
MESSAGE_RETENTION_DAYS: int = 30
|
||||
|
||||
# Retention period for RX log entries (in days)
|
||||
RXLOG_RETENTION_DAYS: int = 7
|
||||
|
||||
# Retention period for contacts (in days)
|
||||
CONTACT_RETENTION_DAYS: int = 90
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
The archive is automatically initialized when SharedData is created with a device identifier (serial port):
|
||||
|
||||
```python
|
||||
from meshcore_gui.core.shared_data import SharedData
|
||||
|
||||
# With archive (normal use)
|
||||
shared = SharedData("literal:AA:BB:CC:DD:EE:FF")
|
||||
|
||||
# Without archive (backward compatible)
|
||||
shared = SharedData() # archive will be None
|
||||
```
|
||||
|
||||
### Adding Data
|
||||
|
||||
All data added to SharedData is automatically archived:
|
||||
|
||||
```python
|
||||
from meshcore_gui.core.models import Message, RxLogEntry
|
||||
|
||||
# Add message (goes to both SharedData and archive)
|
||||
msg = Message(
|
||||
time="12:34:56",
|
||||
sender="PE1HVH",
|
||||
text="Hello!",
|
||||
channel=0,
|
||||
direction="in",
|
||||
)
|
||||
shared.add_message(msg)
|
||||
|
||||
# Add RX log entry (goes to both SharedData and archive)
|
||||
entry = RxLogEntry(
|
||||
time="12:34:56",
|
||||
snr=8.5,
|
||||
rssi=-95.0,
|
||||
payload_type="MSG",
|
||||
hops=2,
|
||||
)
|
||||
shared.add_rx_log(entry)
|
||||
```
|
||||
|
||||
### Getting Statistics
|
||||
|
||||
```python
|
||||
# Get archive statistics
|
||||
stats = shared.get_archive_stats()
|
||||
if stats:
|
||||
print(f"Total messages: {stats['total_messages']}")
|
||||
print(f"Total RX log: {stats['total_rxlog']}")
|
||||
print(f"Pending writes: {stats['pending_messages']}")
|
||||
```
|
||||
|
||||
### Manual Flush
|
||||
|
||||
Archive writes are normally batched. To force immediate write:
|
||||
|
||||
```python
|
||||
if shared.archive:
|
||||
shared.archive.flush()
|
||||
```
|
||||
|
||||
### Manual Cleanup
|
||||
|
||||
Cleanup runs automatically daily, but can be triggered manually:
|
||||
|
||||
```python
|
||||
if shared.archive:
|
||||
shared.archive.cleanup_old_data()
|
||||
```
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Write Performance
|
||||
- Batch writes: 10 messages or 60 seconds (whichever comes first)
|
||||
- Write time: ~10ms for 1000 messages
|
||||
- Memory overhead: Minimal (only buffer in memory, ~10 messages)
|
||||
|
||||
### Startup Performance
|
||||
- Archive loading: <500ms for 10,000 messages
|
||||
- Archive is counted, not loaded into memory
|
||||
- No impact on UI responsiveness
|
||||
|
||||
### Storage Size
|
||||
With default retention (30 days messages, 7 days rxlog):
|
||||
- Typical message: ~200 bytes JSON
|
||||
- 100 messages/day → ~6KB/day → ~180KB/month
|
||||
- Expected archive size: <10MB
|
||||
|
||||
## Automatic Cleanup
|
||||
|
||||
The worker runs cleanup daily (every 86400 seconds):
|
||||
|
||||
1. **Message Cleanup**: Removes messages older than `MESSAGE_RETENTION_DAYS`
|
||||
2. **RxLog Cleanup**: Removes entries older than `RXLOG_RETENTION_DAYS`
|
||||
3. **Contact Cleanup**: Removes contacts not seen for `CONTACT_RETENTION_DAYS`
|
||||
|
||||
Cleanup is non-blocking and runs in the background worker thread.
|
||||
|
||||
## Thread Safety
|
||||
|
||||
### Lock Ordering
|
||||
1. SharedData acquires its lock
|
||||
2. SharedData calls MessageArchive methods
|
||||
3. MessageArchive acquires its own lock
|
||||
|
||||
This ordering prevents deadlocks.
|
||||
|
||||
### Concurrent Access
|
||||
- SharedData lock: Protects in-memory buffers
|
||||
- MessageArchive lock: Protects file writes and batch buffers
|
||||
- Independent locks prevent contention
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Disk Write Failures
|
||||
- Atomic writes using temp file + rename
|
||||
- If write fails: buffer retained for retry
|
||||
- Logged to debug output
|
||||
- Application continues normally
|
||||
|
||||
### Corrupt Archives
|
||||
- Version checking on load
|
||||
- Invalid JSON → skip and start fresh
|
||||
- Corrupted data → logged, not loaded
|
||||
|
||||
### Missing Directory
|
||||
- Archive directory created automatically
|
||||
- Parent directories created if needed
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit Tests
|
||||
```bash
|
||||
python -m unittest tests.test_message_archive
|
||||
```
|
||||
|
||||
Tests cover:
|
||||
- Message and RxLog archiving
|
||||
- Batch write behavior
|
||||
- Retention cleanup
|
||||
- Thread safety
|
||||
- JSON serialization
|
||||
|
||||
### Integration Tests
|
||||
```bash
|
||||
python -m unittest tests.test_integration_archive
|
||||
```
|
||||
|
||||
Tests cover:
|
||||
- SharedData + Archive flow
|
||||
- Buffer limits with archiving
|
||||
- Persistence across restarts
|
||||
- Backward compatibility
|
||||
|
||||
### Running All Tests
|
||||
```bash
|
||||
python -m unittest discover tests
|
||||
```
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From v5.1 to v5.2
|
||||
|
||||
No migration needed! The feature is fully backward compatible:
|
||||
|
||||
1. Existing SharedData code works unchanged
|
||||
2. Archive is optional (requires device identifier)
|
||||
3. First run creates archive files automatically
|
||||
4. No data loss from existing cache
|
||||
|
||||
### Upgrading Existing Installation
|
||||
|
||||
```bash
|
||||
# No special steps needed
|
||||
python meshcore_gui.py literal:AA:BB:CC:DD:EE:FF
|
||||
```
|
||||
|
||||
Archive files will be created automatically on first message/rxlog.
|
||||
|
||||
## Future Enhancements (Out of Scope for v1.0)
|
||||
|
||||
- Full-text search in archive
|
||||
- Export to CSV/JSON
|
||||
- Compression of old messages
|
||||
- Cloud sync / multi-device sync
|
||||
- Web interface for archive browsing
|
||||
- Advanced filtering and queries
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Archive Not Created
|
||||
**Problem:** No `~/.meshcore-gui/archive/` directory
|
||||
|
||||
**Solution:**
|
||||
- Check that SharedData was initialized with device identifier
|
||||
- Check disk permissions
|
||||
- Enable debug mode: `--debug-on`
|
||||
|
||||
### Cleanup Not Running
|
||||
**Problem:** Old messages not removed
|
||||
|
||||
**Solution:**
|
||||
- Cleanup runs every 24 hours
|
||||
- Manually trigger: `shared.archive.cleanup_old_data()`
|
||||
- Check retention config values
|
||||
|
||||
### High Disk Usage
|
||||
**Problem:** Archive files growing too large
|
||||
|
||||
**Solution:**
|
||||
- Reduce `MESSAGE_RETENTION_DAYS` in config
|
||||
- Run manual cleanup
|
||||
- Check for misconfigured retention values
|
||||
|
||||
## Support
|
||||
|
||||
For issues or questions:
|
||||
- GitHub: [PE1HVH/meshcore-gui](https://github.com/PE1HVH/meshcore-gui)
|
||||
- Email: pe1hvh@example.com
|
||||
|
||||
## License
|
||||
|
||||
MIT License - Copyright (c) 2026 PE1HVH
|
||||
@@ -0,0 +1,165 @@
|
||||
# MeshCore GUI — BLE Stabiliteit: Installatie-instructies (Legacy)
|
||||
|
||||
> **Let op:** Dit document is BLE-specifiek en wordt bewaard als referentie. De huidige GUI gebruikt USB-serieel; gebruik het handmatige systeemd-voorbeeld in de README.
|
||||
|
||||
## Wat is gewijzigd
|
||||
|
||||
### Nieuwe bestanden
|
||||
| Bestand | Doel |
|
||||
|---------|------|
|
||||
| `meshcore_gui/ble/ble_agent.py` | Ingebouwde BlueZ D-Bus PIN agent (vervangt `bt-agent.service`) |
|
||||
| `meshcore_gui/ble/ble_reconnect.py` | Bond-opruiming + automatische reconnect logica |
|
||||
| `install_ble_stable.sh` | Generiek installatiescript (detecteert paden/user automatisch) |
|
||||
|
||||
### Gewijzigde bestanden
|
||||
| Bestand | Wijziging |
|
||||
|---------|-----------|
|
||||
| `meshcore_gui/ble/worker.py` | Agent startup, disconnect detectie, auto-reconnect loop |
|
||||
| `meshcore_gui/config.py` | Nieuwe constanten: `BLE_PIN`, `RECONNECT_MAX_RETRIES`, `RECONNECT_BASE_DELAY` |
|
||||
|
||||
---
|
||||
|
||||
## Snelle installatie (aanbevolen)
|
||||
|
||||
```bash
|
||||
# 1. Verwijder eerst een eventuele kapotte service
|
||||
sudo systemctl stop meshcore-gui 2>/dev/null
|
||||
sudo systemctl disable meshcore-gui 2>/dev/null
|
||||
sudo rm -f /etc/systemd/system/meshcore-gui.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl reset-failed 2>/dev/null
|
||||
|
||||
# 2. Kopieer de nieuwe/gewijzigde bestanden naar je project
|
||||
cp ble_agent.py ~/meshcore-gui/meshcore_gui/ble/
|
||||
cp ble_reconnect.py ~/meshcore-gui/meshcore_gui/ble/
|
||||
cp worker.py ~/meshcore-gui/meshcore_gui/ble/
|
||||
cp config.py ~/meshcore-gui/meshcore_gui/
|
||||
|
||||
# 3. Ga naar je project directory en voer het installatiescript uit
|
||||
cd ~/meshcore-gui
|
||||
BLE_ADDRESS=FF:05:D6:71:83:8D bash install_ble_stable.sh
|
||||
```
|
||||
|
||||
Het script detecteert automatisch:
|
||||
- De juiste project directory (waar je het uitvoert)
|
||||
- De huidige user
|
||||
- Het pad naar de venv Python
|
||||
- Het correcte entry point
|
||||
|
||||
---
|
||||
|
||||
## Handmatige installatie
|
||||
|
||||
Als je het script niet wilt gebruiken:
|
||||
|
||||
### 1. Kopieer Python bestanden
|
||||
```bash
|
||||
# Pas het pad aan naar jouw project directory
|
||||
PROJECT=~/meshcore-gui
|
||||
|
||||
cp ble_agent.py $PROJECT/meshcore_gui/ble/
|
||||
cp ble_reconnect.py $PROJECT/meshcore_gui/ble/
|
||||
cp worker.py $PROJECT/meshcore_gui/ble/
|
||||
cp config.py $PROJECT/meshcore_gui/
|
||||
```
|
||||
|
||||
### 2. Upgrade meshcore library
|
||||
```bash
|
||||
cd $PROJECT
|
||||
source venv/bin/activate
|
||||
pip install --upgrade meshcore
|
||||
```
|
||||
|
||||
### 3. D-Bus policy installeren
|
||||
Maak `/etc/dbus-1/system.d/meshcore-ble.conf` met je eigen username:
|
||||
```bash
|
||||
sudo tee /etc/dbus-1/system.d/meshcore-ble.conf << 'EOF'
|
||||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<policy user="JOUW_USERNAME">
|
||||
<allow send_destination="org.bluez"/>
|
||||
<allow send_interface="org.bluez.Agent1"/>
|
||||
<allow send_interface="org.bluez.AgentManager1"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
EOF
|
||||
```
|
||||
|
||||
### 4. Systemd service installeren
|
||||
Maak `/etc/systemd/system/meshcore-gui.service` met je eigen paden:
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/meshcore-gui.service << EOF
|
||||
[Unit]
|
||||
Description=MeshCore GUI (BLE)
|
||||
After=bluetooth.target
|
||||
Wants=bluetooth.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$(whoami)
|
||||
WorkingDirectory=$PROJECT
|
||||
ExecStart=$PROJECT/venv/bin/python meshcore_gui.py JOUW_BLE_ADRES --debug-on
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
Environment=DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable meshcore-gui
|
||||
sudo systemctl start meshcore-gui
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verwijderen
|
||||
|
||||
### Via het script
|
||||
```bash
|
||||
cd ~/meshcore-gui
|
||||
bash install_ble_stable.sh --uninstall
|
||||
```
|
||||
|
||||
### Handmatig
|
||||
```bash
|
||||
sudo systemctl stop meshcore-gui
|
||||
sudo systemctl disable meshcore-gui
|
||||
sudo rm -f /etc/systemd/system/meshcore-gui.service
|
||||
sudo rm -f /etc/dbus-1/system.d/meshcore-ble.conf
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl reset-failed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verificatie
|
||||
|
||||
```bash
|
||||
# Service status
|
||||
sudo systemctl status meshcore-gui
|
||||
|
||||
# Live logs
|
||||
journalctl -u meshcore-gui -f
|
||||
|
||||
# Test PIN pairing (vanuit een andere terminal)
|
||||
bluetoothctl remove <BLE_ADRES>
|
||||
sudo systemctl restart meshcore-gui
|
||||
|
||||
# Test disconnect recovery
|
||||
# Zet device uit → wacht 30s → zet weer aan → check logs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuratie (config.py)
|
||||
|
||||
```python
|
||||
BLE_PIN = "123456" # T1000e pairing PIN
|
||||
RECONNECT_MAX_RETRIES = 5 # Max pogingen per disconnect
|
||||
RECONNECT_BASE_DELAY = 5.0 # Wachttijd × poging nummer (5s, 10s, 15s...)
|
||||
```
|
||||
|
||||
Pas deze waarden aan in `meshcore_gui/config.py` als je een ander device of andere timing nodig hebt.
|
||||
@@ -0,0 +1,108 @@
|
||||
# v5.5 Integration Guide — Subprocess BLE Connection
|
||||
|
||||
## Overzicht
|
||||
|
||||
Deze wijziging lost het BlueZ 5.82 probleem op door `meshcore-ble-connect`
|
||||
als **persistent subprocess** te draaien dat de D-Bus/BLE connectie openhoudt,
|
||||
terwijl `BleakClient` er alleen GATT service discovery overheen doet.
|
||||
|
||||
---
|
||||
|
||||
## Gewijzigde bestanden
|
||||
|
||||
### meshcore-ble-connect (5 bestanden)
|
||||
|
||||
| Bestand | Wijziging |
|
||||
|---|---|
|
||||
| `constants.py` | Versie → 1.1.0. Nieuwe constanten: `SERVICES_RESOLVED_TIMEOUT`, `DISCONNECT_POLL_INTERVAL`. Nieuwe exit codes: `CONNECT_FAILED` (5), `DISCONNECTED` (6). |
|
||||
| `exceptions.py` | Nieuwe exception: `ConnectHoldError`. |
|
||||
| `__main__.py` | Nieuw `--connect` flag. Mutual exclusion met `--check-only`. Doorgifte `connect_hold=` aan `BleConnectApp`. |
|
||||
| `app.py` | Nieuw `connect_hold` parameter. Na bond OK → `_enter_connect_hold()` → `device.connect_and_hold()`. Werkt bij zowel bestaande bond als verse pairing. |
|
||||
| `device.py` | Nieuwe methoden: `connect_and_hold()`, `is_connected()`, `is_services_resolved()`, `_wait_for_services_resolved()`, `_monitor_connection()`. Signal handling (SIGTERM/SIGINT) voor clean shutdown. |
|
||||
|
||||
### meshcore-gui (1 bestand)
|
||||
|
||||
| Bestand | Wijziging |
|
||||
|---|---|
|
||||
| `worker.py` | Nieuwe methoden: `_connect_via_subprocess()`, `_kill_connect_subprocess()`. Gewijzigd: `_connect()` gebruikt subprocess als primary path wanneer `_use_ble_connect=True`. Subprocess health check in main loop. Cleanup in finally block. |
|
||||
|
||||
---
|
||||
|
||||
## Architectuur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ meshcore-gui (worker.py) │
|
||||
│ │
|
||||
│ 1. ensure_bond() → bond OK │
|
||||
│ 2. start subprocess: │
|
||||
│ meshcore-ble-connect MAC --pin X │
|
||||
│ --connect │
|
||||
│ 3. wait for "READY" op stdout │
|
||||
│ 4. BleakScanner.find_device_by_address() │
|
||||
│ → populeert bleak's interne cache │
|
||||
│ 5. BleakClient(addr).connect() │
|
||||
│ → bleak ziet Connected=True in BlueZ │
|
||||
│ → slaat Device1.Connect() over │
|
||||
│ → doet alleen GATT service discovery │
|
||||
│ 6. MeshCore.create_ble(client=client) │
|
||||
│ 7. Main loop draait │
|
||||
│ 8. subprocess health check elke 100ms │
|
||||
└──────────────┬──────────────────────────────┘
|
||||
│ subprocess (stdout PIPE)
|
||||
│
|
||||
┌──────────────▼──────────────────────────────┐
|
||||
│ meshcore-ble-connect --connect │
|
||||
│ │
|
||||
│ 1. Bond flow (ensure/pair/trust) │
|
||||
│ 2. Device1.Connect() via D-Bus │
|
||||
│ 3. Poll ServicesResolved tot True │
|
||||
│ 4. print("READY") → stdout │
|
||||
│ 5. Monitor Connected property │
|
||||
│ → print("DISCONNECTED") bij verlies │
|
||||
│ 6. Wacht op SIGTERM of disconnect │
|
||||
│ 7. Device1.Disconnect() bij shutdown │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Installatie
|
||||
|
||||
```bash
|
||||
# 1. Update meshcore-ble-connect
|
||||
cd ~/meshcore-ble-connect
|
||||
# Vervang de 5 gewijzigde bestanden in meshcore_ble_connect/
|
||||
pip install -e . --break-system-packages
|
||||
|
||||
# 2. Test --connect mode standalone
|
||||
meshcore-ble-connect FF:05:D6:71:83:8D --pin 123456 --connect --verbose
|
||||
# Verwacht: READY op stdout, proces blijft draaien
|
||||
# Ctrl+C om te stoppen
|
||||
|
||||
# 3. Update worker.py
|
||||
cp worker.py ~/meshcore-gui/meshcore_gui/ble/worker.py
|
||||
|
||||
# 4. Start meshcore-gui
|
||||
cd ~/meshcore-gui && python -m meshcore_gui
|
||||
```
|
||||
|
||||
## Fallback gedrag
|
||||
|
||||
Als het subprocess faalt (bijv. op BlueZ < 5.78 waar het niet nodig is),
|
||||
valt `_connect()` automatisch terug op de directe `MeshCore.create_ble(address)`
|
||||
aanroep. Dit garandeert backwards compatibility.
|
||||
|
||||
## Risico-mitigatie
|
||||
|
||||
Het document noemde het risico dat bleak mogelijk opnieuw `Device1.Connect()`
|
||||
aanroept. Dit is opgelost door:
|
||||
|
||||
1. **`BleakScanner.find_device_by_address()`** vóór `BleakClient.connect()` —
|
||||
dit triggert bleak's `BlueZManager` singleton om bestaande BlueZ device
|
||||
objecten te ontdekken via `GetManagedObjects()`.
|
||||
2. De manager ziet `Connected=True` op het device (gezet door het subprocess)
|
||||
→ `BleakClient.connect()` slaat `Device1.Connect()` over.
|
||||
3. Bleak doet alleen GATT service resolution over de bestaande connectie.
|
||||
|
||||
Als de scanner het device niet vindt (het adverteert mogelijk niet terwijl
|
||||
het connected is), probeert bleak alsnog. De `GetManagedObjects` call bij
|
||||
manager initialisatie vangt dit op in de meeste gevallen.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Map Architecture — MeshCore GUI
|
||||
|
||||
## Overview
|
||||
|
||||
The MeshCore GUI map subsystem is implemented as a **browser-managed Leaflet runtime** embedded inside a NiceGUI container.
|
||||
|
||||
The key design decision is that the **map lifecycle is owned by the browser**, not by the Python UI update loop.
|
||||
|
||||
NiceGUI acts only as a container and data provider.
|
||||
|
||||
This architecture prevents map resets, marker flicker, and viewport jumps during the 500 ms dashboard refresh cycle.
|
||||
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
```
|
||||
NiceGUI Dashboard
|
||||
│
|
||||
│ snapshot (500 ms)
|
||||
▼
|
||||
MapPanel (Python)
|
||||
│
|
||||
│ JSON payload
|
||||
▼
|
||||
Leaflet Runtime (Browser)
|
||||
│
|
||||
├─ Map instance (persistent)
|
||||
├─ Marker registry
|
||||
├─ Theme state
|
||||
└─ Viewport state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Component Responsibilities
|
||||
|
||||
## MapPanel (Python)
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
meshcore_gui/gui/panels/map_panel.py
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
* provides the map container
|
||||
* injects the Leaflet runtime assets
|
||||
* sends compact map snapshots
|
||||
* handles UI actions:
|
||||
|
||||
* theme toggle
|
||||
* center on device
|
||||
|
||||
MapPanel **does NOT control the Leaflet map directly**.
|
||||
|
||||
It only sends data.
|
||||
|
||||
---
|
||||
|
||||
## MapSnapshotService
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
meshcore_gui/services/map_snapshot_service.py
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
* converts device/contact data into a compact JSON snapshot
|
||||
* ensures stable node identifiers
|
||||
* prepares payloads for the browser runtime
|
||||
|
||||
Example snapshot structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"device": {...},
|
||||
"contacts": [...],
|
||||
"force_center": false
|
||||
}
|
||||
```
|
||||
|
||||
Snapshots are emitted every **500 ms** by the dashboard update loop.
|
||||
|
||||
---
|
||||
|
||||
## Leaflet Runtime
|
||||
|
||||
Location:
|
||||
|
||||
```
|
||||
meshcore_gui/static/leaflet_map_panel.js
|
||||
```
|
||||
|
||||
Responsibilities:
|
||||
|
||||
* initialize the Leaflet map once
|
||||
* maintain persistent map instance
|
||||
* manage marker registry
|
||||
* apply snapshots incrementally
|
||||
* manage map theme and viewport state
|
||||
|
||||
Key design rules:
|
||||
|
||||
```
|
||||
map is created once
|
||||
markers updated incrementally
|
||||
snapshots never recreate the map
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Update Flow
|
||||
|
||||
```
|
||||
SharedData
|
||||
│
|
||||
▼
|
||||
Dashboard update loop (500 ms)
|
||||
│
|
||||
▼
|
||||
MapSnapshotService
|
||||
│
|
||||
▼
|
||||
MapPanel
|
||||
│
|
||||
▼
|
||||
Leaflet Runtime
|
||||
```
|
||||
|
||||
Snapshots are **coalesced** so the browser applies only the newest payload.
|
||||
|
||||
---
|
||||
|
||||
# Theme Handling
|
||||
|
||||
Theme changes are handled via a **dedicated theme channel**.
|
||||
|
||||
Snapshots do **not** carry theme information.
|
||||
|
||||
Reason:
|
||||
|
||||
Embedding theme state in snapshots caused race conditions where queued snapshots overwrote explicit user selections.
|
||||
|
||||
Theme state is managed in the browser runtime and restored on reconnect.
|
||||
|
||||
---
|
||||
|
||||
# Marker Model
|
||||
|
||||
Markers are keyed by **stable node id**.
|
||||
|
||||
```
|
||||
device marker
|
||||
contact markers
|
||||
```
|
||||
|
||||
Updates are applied incrementally:
|
||||
|
||||
```
|
||||
add marker
|
||||
update marker
|
||||
remove marker
|
||||
```
|
||||
|
||||
This prevents marker flicker during the refresh loop.
|
||||
|
||||
---
|
||||
|
||||
# Important Constraints
|
||||
|
||||
Developers must **not**:
|
||||
|
||||
* recreate the Leaflet map inside the dashboard refresh loop
|
||||
* embed theme state in snapshots
|
||||
* call Leaflet APIs directly from Python
|
||||
* force viewport resets during normal snapshot updates
|
||||
|
||||
Violating these rules will reintroduce:
|
||||
|
||||
* disappearing maps
|
||||
* marker flicker
|
||||
* viewport resets
|
||||
* theme resets
|
||||
|
||||
---
|
||||
|
||||
# Reconnect Behaviour
|
||||
|
||||
When the NiceGUI connection temporarily drops:
|
||||
|
||||
1. the Leaflet runtime persists in the browser
|
||||
2. the map instance remains intact
|
||||
3. theme and viewport state are restored
|
||||
4. snapshot updates resume once the connection returns
|
||||
|
||||
---
|
||||
|
||||
# Future Extensions
|
||||
|
||||
Possible improvements without breaking the architecture:
|
||||
|
||||
* marker clustering
|
||||
* heatmap layers
|
||||
* route overlays
|
||||
* tile provider switching
|
||||
|
||||
All extensions must remain **browser-managed**.
|
||||
|
||||
---
|
||||
|
||||
# Summary
|
||||
|
||||
The MeshCore map subsystem follows a strict separation:
|
||||
|
||||
```
|
||||
Python → data
|
||||
Browser → map lifecycle
|
||||
```
|
||||
|
||||
This prevents UI refresh cycles from interfering with map state and ensures smooth rendering even with frequent dashboard updates.
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# Running Multiple MeshCore GUI Instances
|
||||
|
||||
> ⚠️ **WARNING: This guide has not been tested yet.** The configuration below is based on the application's architecture and should work, but has not been validated in practice. Please report any issues.
|
||||
|
||||
## Overview
|
||||
|
||||
MeshCore GUI supports running multiple instances simultaneously — for example, to monitor two different MeshCore devices from the same machine. Each instance gets its own web port, serial connection, and all persistent data (cache, archive, logs, pins, room passwords) is automatically separated by device identifier (serial port).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- MeshCore GUI v1.9.2 or later (with `--port` and serial CLI parameters)
|
||||
|
||||
## Quick Test (foreground)
|
||||
|
||||
Before creating services, verify that both instances start correctly:
|
||||
|
||||
**Terminal 1:**
|
||||
```bash
|
||||
cd ~/meshcore-gui
|
||||
source venv/bin/activate
|
||||
python meshcore_gui.py /dev/ttyUSB0 --debug-on --port=8081 --baud=115200
|
||||
```
|
||||
|
||||
**Terminal 2:**
|
||||
```bash
|
||||
cd ~/meshcore-gui
|
||||
source venv/bin/activate
|
||||
python meshcore_gui.py /dev/ttyUSB1 --debug-on --port=8082 --baud=115200
|
||||
```
|
||||
|
||||
Verify both are accessible at `http://localhost:8081` and `http://localhost:8082`.
|
||||
|
||||
## systemd Service Setup
|
||||
|
||||
### Service 1
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/meshcore-gui-device1.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MeshCore GUI — Device 1 (/dev/ttyUSB0)
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your-username
|
||||
WorkingDirectory=/home/your-username/meshcore-gui
|
||||
ExecStart=/home/your-username/meshcore-gui/venv/bin/python meshcore_gui.py /dev/ttyUSB0 --debug-on --port=8081 --baud=115200
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
### Service 2
|
||||
|
||||
```bash
|
||||
sudo nano /etc/systemd/system/meshcore-gui-device2.service
|
||||
```
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=MeshCore GUI — Device 2 (/dev/ttyUSB1)
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your-username
|
||||
WorkingDirectory=/home/your-username/meshcore-gui
|
||||
ExecStart=/home/your-username/meshcore-gui/venv/bin/python meshcore_gui.py /dev/ttyUSB1 --debug-on --port=8082 --baud=115200
|
||||
Restart=on-failure
|
||||
RestartSec=30
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Replace `your-username` and serial ports with your actual values.
|
||||
|
||||
### Enable and Start
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable meshcore-gui-device1 meshcore-gui-device2
|
||||
sudo systemctl start meshcore-gui-device1
|
||||
sudo systemctl start meshcore-gui-device2
|
||||
```
|
||||
|
||||
## Data Separation
|
||||
|
||||
All persistent data is automatically separated by device identifier. No additional configuration is needed.
|
||||
|
||||
| Data | Path example (device `/dev/ttyUSB0`) |
|
||||
|------|------------------------------------------|
|
||||
| Web interface | `http://host:8081` (via `--port`) |
|
||||
| Cache | `~/.meshcore-gui/cache/_dev_ttyUSB0.json` |
|
||||
| Message archive | `~/.meshcore-gui/archive/_dev_ttyUSB0_messages.json` |
|
||||
| RX log archive | `~/.meshcore-gui/archive/_dev_ttyUSB0_rxlog.json` |
|
||||
| Debug log | `~/.meshcore-gui/logs/_dev_ttyUSB0_meshcore_gui.log` |
|
||||
| Pin state | `~/.meshcore-gui/pins/_dev_ttyUSB0_pins.json` |
|
||||
| Room passwords | `~/.meshcore-gui/room_passwords/_dev_ttyUSB0_rooms.json` |
|
||||
|
||||
## Useful Commands
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `sudo systemctl status meshcore-gui-device1` | Check status of device 1 |
|
||||
| `sudo systemctl status meshcore-gui-device2` | Check status of device 2 |
|
||||
| `sudo journalctl -u meshcore-gui-device1 -f` | Follow live log of device 1 |
|
||||
| `sudo journalctl -u meshcore-gui-device2 -f` | Follow live log of device 2 |
|
||||
| `sudo systemctl restart meshcore-gui-device1` | Restart device 1 (without affecting device 2) |
|
||||
| `sudo systemctl stop meshcore-gui-device1` | Stop device 1 only |
|
||||
| `sudo systemctl disable meshcore-gui-device1` | Prevent device 1 from starting on boot |
|
||||
|
||||
## Removing a Service
|
||||
|
||||
```bash
|
||||
sudo systemctl stop meshcore-gui-device2
|
||||
sudo systemctl disable meshcore-gui-device2
|
||||
sudo rm /etc/systemd/system/meshcore-gui-device2.service
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Optionally remove the device's persistent data:
|
||||
|
||||
```bash
|
||||
rm ~/.meshcore-gui/cache/_dev_ttyUSB1.json
|
||||
rm ~/.meshcore-gui/archive/_dev_ttyUSB1_*.json
|
||||
rm ~/.meshcore-gui/logs/_dev_ttyUSB1_meshcore_gui.log
|
||||
rm ~/.meshcore-gui/pins/_dev_ttyUSB1_pins.json
|
||||
rm ~/.meshcore-gui/room_passwords/_dev_ttyUSB1_rooms.json
|
||||
```
|
||||
Binary file not shown.
@@ -0,0 +1,219 @@
|
||||
# SOLID Analysis — MeshCore GUI
|
||||
|
||||
## 1. Reference: standard Python OOP project conventions
|
||||
|
||||
| Convention | Norm | This project |
|
||||
|-----------|------|-------------|
|
||||
| Package with subpackage when widgets emerge | ✅ | ✅ `widgets/` subpackage (6 classes) |
|
||||
| One class per module | ✅ | ✅ every module ≤1 class |
|
||||
| Entry point outside package | ✅ | ✅ `meshcore_gui.py` beside package |
|
||||
| `__init__.py` with version | ✅ | ✅ only `__version__` |
|
||||
| Constants in own module | ✅ | ✅ `config.py` |
|
||||
| No circular imports | ✅ | ✅ acyclic dependency tree |
|
||||
| Type hints on public API | ✅ | ✅ 84/84 methods typed |
|
||||
| Private methods with `_` prefix | ✅ | ✅ consistent |
|
||||
| Docstrings on modules and classes | ✅ | ✅ present everywhere |
|
||||
| PEP 8 import order | ✅ | ✅ stdlib → third-party → local |
|
||||
|
||||
### Dependency tree (acyclic)
|
||||
|
||||
```
|
||||
config protocols
|
||||
↑ ↑
|
||||
shared_data worker
|
||||
↑ main_page → widgets/*
|
||||
↑ route_builder ← route_page
|
||||
↑
|
||||
meshcore_gui.py (only place that knows the concrete SharedData)
|
||||
```
|
||||
|
||||
No circular dependencies. `config` and `protocols` are leaf nodes; everything points in one direction. Widgets depend only on `config` (for constants) and NiceGUI — they have zero knowledge of SharedData or protocols.
|
||||
|
||||
---
|
||||
|
||||
## 2. SOLID assessment per principle
|
||||
|
||||
### S — Single Responsibility Principle
|
||||
|
||||
> "A class should have only one reason to change."
|
||||
|
||||
| Module | Class | Responsibility | Verdict |
|
||||
|--------|-------|---------------|---------|
|
||||
| `config.py` | *(no class)* | Constants and debug helper | ✅ Single purpose |
|
||||
| `protocols.py` | *(Protocol classes)* | Interface contracts | ✅ Single purpose |
|
||||
| `shared_data.py` | SharedData | Thread-safe data store | ✅ See note |
|
||||
| `ble/worker.py` | SerialWorker | Serial communication thread | ✅ Single purpose |
|
||||
| `main_page.py` | DashboardPage | Dashboard layout orchestrator | ✅ See note |
|
||||
| `route_builder.py` | RouteBuilder | Route data construction (pure logic) | ✅ Single purpose |
|
||||
| `route_page.py` | RoutePage | Route page rendering | ✅ Single purpose |
|
||||
| `widgets/device_panel.py` | DevicePanel | Header, device info, actions | ✅ Single purpose |
|
||||
| `widgets/map_panel.py` | MapPanel | Leaflet map with markers | ✅ Single purpose |
|
||||
| `widgets/contacts_panel.py` | ContactsPanel | Contacts list + DM dialog | ✅ Single purpose |
|
||||
| `widgets/message_input.py` | MessageInput | Message input + channel select | ✅ Single purpose |
|
||||
| `widgets/message_list.py` | MessageList | Message feed + channel filter | ✅ Single purpose |
|
||||
| `widgets/rx_log_panel.py` | RxLogPanel | RX log table | ✅ Single purpose |
|
||||
|
||||
**SharedData:** 15 public methods in 5 categories (device updates, status, collections, snapshots, lookups). This is deliberate design: SharedData is the single source of truth between two threads. Splitting it would spread lock logic across multiple objects, making thread-safety harder. The responsibility is *"thread-safe data access"* — that is one reason to change.
|
||||
|
||||
**DashboardPage:** After the widget decomposition, DashboardPage is now 148 lines with only 4 methods. It is a thin orchestrator that composes six widgets into a layout and drives the update timer. All rendering and data-update logic has been extracted into the widget classes. The previous ⚠️ for DashboardPage is resolved.
|
||||
|
||||
**Conclusion SRP:** No violations. All classes have a single, well-defined responsibility.
|
||||
|
||||
---
|
||||
|
||||
### O — Open/Closed Principle
|
||||
|
||||
> "Open for extension, closed for modification."
|
||||
|
||||
| Scenario | How to extend | Existing code modified? |
|
||||
|----------|--------------|------------------------|
|
||||
| Add new page | New module + `@ui.page` in entry point | Only entry point (1 line) |
|
||||
| Add new command | `_handle_command()` case | Only `ble/worker.py` |
|
||||
| Add new contact type | `TYPE_ICONS/NAMES/LABELS` in config | Only `config.py` |
|
||||
| Add new dashboard widget | New widget class + compose in DashboardPage | Only `main_page.py` |
|
||||
| Add new route info | Extend RouteBuilder.build() | Only `route_builder.py` |
|
||||
|
||||
**Where not ideal:** `_handle_command()` in SerialWorker is an if/elif chain. In a larger project, a Command pattern or dict-dispatch would be more appropriate. For 4 commands this is pragmatically correct.
|
||||
|
||||
**Conclusion OCP:** Good. Extensions touch only one module.
|
||||
|
||||
---
|
||||
|
||||
### L — Liskov Substitution Principle
|
||||
|
||||
> "Subtypes must be substitutable for their base types."
|
||||
|
||||
There is **no inheritance** in this project. All classes are concrete and standalone. This is correct for the project scale — there is no reason for a class hierarchy.
|
||||
|
||||
**Where LSP does apply:** The Protocol interfaces (`SharedDataWriter`, `SharedDataReader`, `ContactLookup`, `SharedDataReadAndLookup`) define contracts that SharedData implements. Any object that satisfies these protocols can be substituted — for example a test stub. This is LSP via structural subtyping.
|
||||
|
||||
**Conclusion LSP:** Satisfied via Protocol interfaces. No violations.
|
||||
|
||||
---
|
||||
|
||||
### I — Interface Segregation Principle
|
||||
|
||||
> "Clients should not be forced to depend on interfaces they do not use."
|
||||
|
||||
| Client | Protocol | Methods visible | SharedData methods not visible |
|
||||
|--------|----------|----------------|-------------------------------|
|
||||
| SerialWorker | SharedDataWriter | 10 | 5 (snapshot, flags, GUI commands) |
|
||||
| DashboardPage | SharedDataReader | 4 | 11 (all write methods) |
|
||||
| RouteBuilder | ContactLookup | 1 | 14 (everything else) |
|
||||
| RoutePage | SharedDataReadAndLookup | 5 | 10 (all write methods) |
|
||||
| Widget classes | *(none — receive Dict/callback)* | 0 | 15 (all methods) |
|
||||
|
||||
Each consumer sees **only the methods it needs**. The protocols enforce this at the type level. Widget classes go even further: they have zero knowledge of SharedData and receive only plain dictionaries and callbacks.
|
||||
|
||||
**Conclusion ISP:** Satisfied. Each consumer depends on a narrow, purpose-built interface.
|
||||
|
||||
---
|
||||
|
||||
### D — Dependency Inversion Principle
|
||||
|
||||
> "Depend on abstractions, not on concretions."
|
||||
|
||||
| Dependency | Before (protocols) | After (protocols) |
|
||||
|-----------|---------------|---------------|
|
||||
| SerialWorker → SharedData | Concrete ⚠️ | Protocol (SharedDataWriter) ✅ |
|
||||
| DashboardPage → SharedData | Concrete ⚠️ | Protocol (SharedDataReader) ✅ |
|
||||
| RouteBuilder → SharedData | Concrete ⚠️ | Protocol (ContactLookup) ✅ |
|
||||
| RoutePage → SharedData | Concrete ⚠️ | Protocol (SharedDataReadAndLookup) ✅ |
|
||||
| Widget classes → SharedData | N/A | No dependency at all ✅ |
|
||||
| meshcore_gui.py → SharedData | Concrete | Concrete ✅ (composition root) |
|
||||
|
||||
The **composition root** (`meshcore_gui.py`) is the only place that knows the concrete `SharedData` class. All other modules depend on protocols or receive plain data. This is standard DIP practice: the wiring layer knows the concretions, the business logic knows only abstractions.
|
||||
|
||||
**Conclusion DIP:** Satisfied. Constructor injection was already present; now the abstractions are explicit.
|
||||
|
||||
---
|
||||
|
||||
## 3. Protocol interface design
|
||||
|
||||
### Why `typing.Protocol` and not `abc.ABC`?
|
||||
|
||||
Python offers two approaches for defining interfaces:
|
||||
|
||||
| Aspect | `abc.ABC` (nominal) | `typing.Protocol` (structural) |
|
||||
|--------|---------------------|-------------------------------|
|
||||
| Subclassing required | Yes (`class Foo(MyABC)`) | No |
|
||||
| Duck typing compatible | No | Yes |
|
||||
| Runtime checkable | Yes | Optional (`@runtime_checkable`) |
|
||||
| Python version | 3.0+ | 3.8+ |
|
||||
|
||||
Protocol was chosen because SharedData does not need to inherit from an abstract base class. Any object that has the right methods automatically satisfies the protocol — this is idiomatic Python (duck typing with type safety).
|
||||
|
||||
### Interface map
|
||||
|
||||
```
|
||||
SharedDataWriter (SerialWorker)
|
||||
├── update_from_appstart()
|
||||
├── update_from_device_query()
|
||||
├── set_status()
|
||||
├── set_connected()
|
||||
├── set_contacts()
|
||||
├── set_channels()
|
||||
├── add_message()
|
||||
├── add_rx_log()
|
||||
├── get_next_command()
|
||||
└── get_contact_name_by_prefix()
|
||||
|
||||
SharedDataReader (DashboardPage)
|
||||
├── get_snapshot()
|
||||
├── clear_update_flags()
|
||||
├── mark_gui_initialized()
|
||||
└── put_command()
|
||||
|
||||
ContactLookup (RouteBuilder)
|
||||
└── get_contact_by_prefix()
|
||||
|
||||
SharedDataReadAndLookup (RoutePage)
|
||||
├── get_snapshot()
|
||||
├── clear_update_flags()
|
||||
├── mark_gui_initialized()
|
||||
├── put_command()
|
||||
└── get_contact_by_prefix()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Summary
|
||||
|
||||
| Principle | Before protocols | With protocols | With widgets | Change |
|
||||
|----------|-----------------|----------------|--------------|--------|
|
||||
| **SRP** | ✅ Good | ✅ Good | ✅ Good | Widget extraction resolved DashboardPage size |
|
||||
| **OCP** | ✅ Good | ✅ Good | ✅ Good | Widgets are easy to add |
|
||||
| **LSP** | ✅ N/A | ✅ Satisfied via Protocol | ✅ Satisfied via Protocol | — |
|
||||
| **ISP** | ⚠️ Acceptable | ✅ Good | ✅ Good | Widgets have zero SharedData dependency |
|
||||
| **DIP** | ⚠️ Acceptable | ✅ Good | ✅ Good | — |
|
||||
|
||||
### Changes: Protocol interfaces
|
||||
|
||||
| # | Change | Files affected |
|
||||
|---|--------|---------------|
|
||||
| 1 | Added `protocols.py` with 4 Protocol interfaces | New file |
|
||||
| 2 | SerialWorker depends on `SharedDataWriter` | `ble/worker.py` |
|
||||
| 3 | DashboardPage depends on `SharedDataReader` | `main_page.py` |
|
||||
| 4 | RouteBuilder depends on `ContactLookup` | `route_builder.py` |
|
||||
| 5 | RoutePage depends on `SharedDataReadAndLookup` | `route_page.py` |
|
||||
| 6 | No consumer imports `shared_data.py` directly | All consumer modules |
|
||||
|
||||
### Changes: Widget decomposition
|
||||
|
||||
| # | Change | Files affected |
|
||||
|---|--------|---------------|
|
||||
| 1 | Added `widgets/` subpackage with 6 widget classes | New directory (7 files) |
|
||||
| 2 | MeshCoreGUI (740 lines) replaced by DashboardPage (148 lines) + 6 widgets | `main_page.py`, `widgets/*.py` |
|
||||
| 3 | DashboardPage is now a thin orchestrator | `main_page.py` |
|
||||
| 4 | Widget classes depend only on `config` and NiceGUI | `widgets/*.py` |
|
||||
| 5 | Maximum decoupling: widgets have zero SharedData knowledge | All widget modules |
|
||||
|
||||
### Metrics
|
||||
|
||||
| Metric | Monolith | With protocols | With widgets |
|
||||
|--------|----------|----------------|--------------|
|
||||
| Files | 1 | 8 | 16 |
|
||||
| Total lines | 1,395 | ~1,500 | ~1,955 |
|
||||
| Largest class (lines) | MeshCoreGUI (740) | MeshCoreGUI (740) | SharedData (263) |
|
||||
| Typed methods | 51 (partial) | 51 (partial) | 90/90 |
|
||||
| Protocol interfaces | 0 | 4 | 4 |
|
||||
@@ -0,0 +1,424 @@
|
||||
# MeshCore GUI - Legacy BLE Troubleshooting Guide
|
||||
|
||||
> **Note:** This guide applies to BLE connections only and is kept for historical reference. The current GUI uses USB serial; for serial issues, verify the correct port (e.g. `/dev/ttyUSB0`) and user permissions (e.g. `dialout` on Linux).
|
||||
|
||||
## Problem 1: EOFError during start_notify
|
||||
|
||||
BLE connection to MeshCore device fails with `EOFError` during `start_notify` on the UART TX characteristic. The error originates in `dbus_fast` (the D-Bus library used by `bleak`) and looks like this:
|
||||
|
||||
```
|
||||
File "src/dbus_fast/_private/unmarshaller.py", line 395, in dbus_fast._private.unmarshaller.Unmarshaller._read_sock_with_fds
|
||||
EOFError
|
||||
```
|
||||
|
||||
Basic BLE connect works fine, but subscribing to notifications (`start_notify`) crashes.
|
||||
|
||||
## Problem 2: PIN or Key Missing / Authentication Failure
|
||||
|
||||
BLE connection fails immediately after connecting with `failed to discover services, device disconnected` or `le-connection-abort-by-local`. In `btmon`, the trace shows:
|
||||
|
||||
```
|
||||
Encryption Change - Status: PIN or Key Missing (0x06)
|
||||
Disconnect - Reason: Authentication Failure (0x05)
|
||||
```
|
||||
|
||||
This happens when the MeshCore device requires BLE PIN pairing (e.g., PIN `123456`) but no BlueZ agent is running to handle the passkey exchange. Bleak cannot provide a PIN by itself — it relies on a BlueZ agent to handle pairing.
|
||||
|
||||
**Symptoms:**
|
||||
- `bluetoothctl connect` fails with `le-connection-abort-by-local`
|
||||
- `bluetoothctl pair` asks for a passkey and succeeds
|
||||
- meshcore-gui still fails because bleak creates its own connection without an agent
|
||||
- btmon shows repeated connect → encrypt → `PIN or Key Missing` → disconnect cycles
|
||||
|
||||
## Problem 3: Port already in use
|
||||
|
||||
meshcore-gui fails to start with:
|
||||
|
||||
```
|
||||
ERROR: [Errno 98] error while attempting to bind on address ('0.0.0.0', 8081): address already in use
|
||||
```
|
||||
|
||||
This means a previous meshcore-gui instance is still running (or the port hasn't been released yet).
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Steps
|
||||
|
||||
### 1. Check adapter status
|
||||
|
||||
```bash
|
||||
hciconfig -a
|
||||
```
|
||||
|
||||
Expected: `UP RUNNING`. If it shows `DOWN`, reset with:
|
||||
|
||||
```bash
|
||||
sudo hciconfig hci0 down
|
||||
sudo hciconfig hci0 up
|
||||
```
|
||||
|
||||
### 2. Check if adapter is detected
|
||||
|
||||
```bash
|
||||
lsusb | grep -i blue
|
||||
```
|
||||
|
||||
### 3. Check power supply (Raspberry Pi)
|
||||
|
||||
```bash
|
||||
vcgencmd get_throttled
|
||||
```
|
||||
|
||||
Expected: `throttled=0x0`. Any other value indicates power issues that can cause BLE instability.
|
||||
|
||||
### 4. Test basic BLE connection (without notify)
|
||||
|
||||
```bash
|
||||
python -c "
|
||||
import asyncio
|
||||
from bleak import BleakClient
|
||||
async def test():
|
||||
async with BleakClient('AA:BB:CC:DD:EE:FF') as c:
|
||||
print('Connected:', c.is_connected)
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
If this works but meshcli/meshcore_gui fails, the problem is specifically `start_notify`.
|
||||
|
||||
### 5. Test start_notify in isolation
|
||||
|
||||
```bash
|
||||
python -c "
|
||||
import asyncio
|
||||
from bleak import BleakClient
|
||||
UART_TX = '6e400003-b5a3-f393-e0a9-e50e24dcca9e'
|
||||
async def test():
|
||||
async with BleakClient('AA:BB:CC:DD:EE:FF') as c:
|
||||
def cb(s, d): print(f'RX: {d.hex()}')
|
||||
await c.start_notify(UART_TX, cb)
|
||||
print('Notify OK!')
|
||||
await asyncio.sleep(2)
|
||||
asyncio.run(test())
|
||||
"
|
||||
```
|
||||
|
||||
If this also fails with `EOFError`, the issue is confirmed at the BlueZ/D-Bus level.
|
||||
|
||||
### 6. Test notifications via bluetoothctl (outside Python)
|
||||
|
||||
```bash
|
||||
bluetoothctl
|
||||
scan on
|
||||
# Wait for device to appear
|
||||
connect AA:BB:CC:DD:EE:FF
|
||||
# Wait for "Connection successful"
|
||||
menu gatt
|
||||
select-attribute 6e400003-b5a3-f393-e0a9-e50e24dcca9e
|
||||
notify on
|
||||
```
|
||||
|
||||
If `connect` fails with `le-connection-abort-by-local`, the problem is at the BlueZ or device level. No Python fix will help.
|
||||
|
||||
### 7. Check if pairing is required (PIN or Key Missing)
|
||||
|
||||
If `bluetoothctl connect` fails with `le-connection-abort-by-local`, try pairing instead:
|
||||
|
||||
```bash
|
||||
bluetoothctl
|
||||
scan on
|
||||
pair AA:BB:CC:DD:EE:FF
|
||||
# If it asks for a passkey, the device requires PIN pairing
|
||||
```
|
||||
|
||||
If pairing succeeds but meshcore-gui still fails, the issue is a missing BlueZ agent (see Solution 2).
|
||||
|
||||
### 8. Use btmon for HCI-level debugging
|
||||
|
||||
```bash
|
||||
sudo btmon
|
||||
```
|
||||
|
||||
In another terminal, start meshcore-gui. Look for:
|
||||
- `Encryption Change - Status: PIN or Key Missing (0x06)` → pairing/agent issue (Solution 2)
|
||||
- Successful encryption but no service discovery → stale bond (Solution 1)
|
||||
|
||||
### 9. Check what is using port 8081
|
||||
|
||||
```bash
|
||||
lsof -i :8081
|
||||
```
|
||||
|
||||
If another process holds the port, see Solution 3.
|
||||
|
||||
---
|
||||
|
||||
## Solution 1: Stale BLE Pairing State (EOFError)
|
||||
|
||||
The root cause is a stale BLE pairing state between the Linux adapter and the MeshCore device. The fix requires a clean reconnect sequence:
|
||||
|
||||
### Step 1 - Remove the device from BlueZ
|
||||
|
||||
```bash
|
||||
bluetoothctl
|
||||
remove AA:BB:CC:DD:EE:FF
|
||||
exit
|
||||
```
|
||||
|
||||
### Step 2 - Hard power cycle the MeshCore device
|
||||
|
||||
Physically power off the T1000-e (not just a software reset). Wait 10 seconds, then power it back on.
|
||||
|
||||
### Step 3 - Scan and reconnect from scratch
|
||||
|
||||
```bash
|
||||
bluetoothctl
|
||||
scan on
|
||||
```
|
||||
|
||||
Wait until the device appears: `[NEW] Device AA:BB:CC:DD:EE:FF MeshCore-...`
|
||||
|
||||
Then immediately connect:
|
||||
|
||||
```
|
||||
connect AA:BB:CC:DD:EE:FF
|
||||
```
|
||||
|
||||
### Step 4 - Verify notifications work
|
||||
|
||||
```
|
||||
menu gatt
|
||||
select-attribute 6e400003-b5a3-f393-e0a9-e50e24dcca9e
|
||||
notify on
|
||||
```
|
||||
|
||||
If this succeeds, disconnect cleanly:
|
||||
|
||||
```
|
||||
notify off
|
||||
back
|
||||
disconnect AA:BB:CC:DD:EE:FF
|
||||
exit
|
||||
```
|
||||
|
||||
### Step 5 - Verify channels with meshcli
|
||||
|
||||
```bash
|
||||
meshcli -d AA:BB:CC:DD:EE:FF
|
||||
> get_channels
|
||||
```
|
||||
|
||||
Confirm output matches `CHANNELS_CONFIG` in `meshcore_gui.py`, then:
|
||||
|
||||
```
|
||||
> exit
|
||||
```
|
||||
|
||||
### Step 6 - Start the GUI
|
||||
|
||||
```bash
|
||||
cd ~/meshcore-gui
|
||||
source venv/bin/activate
|
||||
python meshcore_gui.py AA:BB:CC:DD:EE:FF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Solution 2: Missing BlueZ Agent for PIN Pairing
|
||||
|
||||
When the MeshCore device requires BLE PIN pairing, bleak cannot provide the PIN by itself. BlueZ needs a running agent that responds to pairing requests with the correct passkey.
|
||||
|
||||
**Why this happens:** `bluetoothctl` acts as its own agent (which is why manual pairing works), but when bleak connects independently, there is no agent to handle the passkey exchange. Even if the device was previously paired via `bluetoothctl`, the bond can become invalid when:
|
||||
- The MeshCore device is reset or firmware-updated
|
||||
- Another device (e.g., companion app) pairs with the MeshCore device and overwrites its bond slot
|
||||
- The bond keys get out of sync for any reason
|
||||
|
||||
### Step 1 - Install bluez-tools
|
||||
|
||||
```bash
|
||||
sudo apt install bluez-tools
|
||||
```
|
||||
|
||||
### Step 2 - Create a PIN file
|
||||
|
||||
```bash
|
||||
echo "* 123456" > ~/.meshcore-ble-pin
|
||||
chmod 600 ~/.meshcore-ble-pin
|
||||
```
|
||||
|
||||
The format is `<address-or-wildcard> <pin>`. Use `*` to match any device, or specify a specific address:
|
||||
|
||||
```
|
||||
FF:05:D6:71:83:8D 123456
|
||||
```
|
||||
|
||||
### Step 3 - Remove any existing (corrupt) bond
|
||||
|
||||
```bash
|
||||
bluetoothctl remove AA:BB:CC:DD:EE:FF
|
||||
```
|
||||
|
||||
### Step 4 - Start the agent and meshcore-gui
|
||||
|
||||
```bash
|
||||
bt-agent -c KeyboardOnly -p ~/.meshcore-ble-pin &
|
||||
python meshcore_gui.py AA:BB:CC:DD:EE:FF
|
||||
```
|
||||
|
||||
### Step 5 - Make the agent permanent (systemd service)
|
||||
|
||||
Create the service file:
|
||||
|
||||
```bash
|
||||
sudo tee /etc/systemd/system/bt-agent.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Bluetooth PIN Agent for MeshCore
|
||||
After=bluetooth.service
|
||||
Requires=bluetooth.service
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/bt-agent -c KeyboardOnly -p /home/hans/.meshcore-ble-pin
|
||||
Restart=always
|
||||
User=hans
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
```
|
||||
|
||||
Enable and start:
|
||||
|
||||
```bash
|
||||
sudo systemctl enable bt-agent
|
||||
sudo systemctl start bt-agent
|
||||
```
|
||||
|
||||
Verify it is running:
|
||||
|
||||
```bash
|
||||
sudo systemctl status bt-agent
|
||||
```
|
||||
|
||||
Now meshcore-gui can connect at any time without manual pairing. The agent survives reboots.
|
||||
|
||||
**Important:** Only run ONE bt-agent instance. Multiple agents conflict with each other. If you have both a manual `bt-agent &` process and the systemd service running, kill the manual one:
|
||||
|
||||
```bash
|
||||
pkill -f bt-agent
|
||||
sudo systemctl start bt-agent
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Solution 3: Port 8081 Already in Use
|
||||
|
||||
This happens when a previous meshcore-gui instance is still running or hasn't fully released the port.
|
||||
|
||||
### Quick fix - Kill previous instance and free the port
|
||||
|
||||
```bash
|
||||
pkill -9 -f meshcore_gui
|
||||
sleep 3
|
||||
```
|
||||
|
||||
Verify the port is free:
|
||||
|
||||
```bash
|
||||
lsof -i :8081
|
||||
```
|
||||
|
||||
If nothing shows up, the port is free. Start meshcore-gui:
|
||||
|
||||
```bash
|
||||
nohup python meshcore_gui.py AA:BB:CC:DD:EE:FF --debug-on > ~/meshcore.log 2>&1 &
|
||||
```
|
||||
|
||||
### If the port is still in use after killing
|
||||
|
||||
Sometimes TCP sockets linger in `TIME_WAIT` state. Wait 30 seconds or force it:
|
||||
|
||||
```bash
|
||||
sleep 30
|
||||
lsof -i :8081
|
||||
```
|
||||
|
||||
### Running in background with nohup
|
||||
|
||||
To run meshcore-gui in the background (survives terminal close):
|
||||
|
||||
```bash
|
||||
nohup python meshcore_gui.py AA:BB:CC:DD:EE:FF --debug-on > ~/meshcore.log 2>&1 &
|
||||
```
|
||||
|
||||
Check if it started successfully:
|
||||
|
||||
```bash
|
||||
sleep 5
|
||||
tail -30 ~/meshcore.log
|
||||
```
|
||||
|
||||
**Tip:** Always redirect output to a log file (not `/dev/null`) so you can diagnose problems:
|
||||
|
||||
```bash
|
||||
# Good - keeps logs for debugging
|
||||
nohup python meshcore_gui.py AA:BB:CC:DD:EE:FF --debug-on > ~/meshcore.log 2>&1 &
|
||||
|
||||
# Bad - hides all errors
|
||||
nohup python meshcore_gui.py AA:BB:CC:DD:EE:FF --debug-on > /dev/null 2>&1 &
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Things That Did NOT Help
|
||||
|
||||
| Action | Result |
|
||||
|---|---|
|
||||
| `sudo systemctl restart bluetooth` | No effect |
|
||||
| `sudo hciconfig hci0 down/up` | No effect |
|
||||
| `sudo rmmod btusb && sudo modprobe btusb` | No effect |
|
||||
| `sudo usbreset "8087:0026"` | No effect |
|
||||
| `sudo reboot` | No effect |
|
||||
| Clearing BlueZ cache (`/var/lib/bluetooth/*/cache`) | No effect |
|
||||
| Recreating Python venv | No effect |
|
||||
| Downgrading `dbus_fast` / `bleak` | No effect |
|
||||
| Downgrading `linux-firmware` | No effect |
|
||||
| Adding `pin="123456"` to `MeshCore.create_ble()` | Pairing fails — bleak's `pair()` cannot provide a passkey without a BlueZ agent |
|
||||
| Pre-connecting via `bluetoothctl connect` before meshcore-gui | Bleak creates its own connection and doesn't reuse the existing one |
|
||||
|
||||
---
|
||||
|
||||
## Key Takeaways
|
||||
|
||||
### EOFError / stale bond
|
||||
When `start_notify` fails with `EOFError` but basic BLE connect works, the issue is almost always a stale BLE state between the host adapter and the peripheral device. The fix is:
|
||||
|
||||
1. **Remove** the device from bluetoothctl
|
||||
2. **Hard power cycle** the peripheral device
|
||||
3. **Re-scan** and reconnect from scratch
|
||||
|
||||
### PIN or Key Missing / Authentication Failure
|
||||
When btmon shows `PIN or Key Missing (0x06)` and connections drop immediately after encryption negotiation, the fix is:
|
||||
|
||||
1. **Remove** the corrupt bond from bluetoothctl
|
||||
2. **Run `bt-agent`** with the correct PIN file so BlueZ can handle pairing requests
|
||||
3. **Install as systemd service** for persistence across reboots
|
||||
|
||||
### Port already in use
|
||||
When meshcore-gui fails with `[Errno 98] address already in use`:
|
||||
|
||||
1. **Kill** any existing meshcore-gui process: `pkill -9 -f meshcore_gui`
|
||||
2. **Wait** a few seconds for the port to be released
|
||||
3. **Verify** the port is free: `lsof -i :8081`
|
||||
|
||||
---
|
||||
|
||||
## Recommended Startup Sequence
|
||||
|
||||
For the most reliable BLE connection, always follow this order:
|
||||
|
||||
1. Ensure `bt-agent` is running (if device requires PIN pairing): `sudo systemctl status bt-agent`
|
||||
2. Ensure no other meshcore-gui instance is running: `pkill -f meshcore_gui` and `lsof -i :8081`
|
||||
3. Ensure no other application holds the BLE connection (BT manager, bluetoothctl, meshcli, companion app)
|
||||
4. Verify the device is visible: `bluetoothctl scan on`
|
||||
5. Check channels: `meshcli -d <BLE_ADDRESS>` → `get_channels` → `exit`
|
||||
6. Start the GUI: `python meshcore_gui.py <BLE_ADDRESS>`
|
||||
Binary file not shown.
@@ -0,0 +1,494 @@
|
||||
# MeshCore GUI — BLE Architecture
|
||||
|
||||
## Overzicht
|
||||
|
||||
Dit document beschrijft hoe MeshCore GUI communiceert met een MeshCore T1000-E device via Bluetooth Low Energy (BLE), welke libraries daarbij betrokken zijn, en hoe de volledige stack van hardware tot applicatielogica in elkaar zit.
|
||||
|
||||
---
|
||||
|
||||
## 1. De BLE Stack
|
||||
|
||||
De communicatie loopt door 7 lagen, van hardware tot GUI:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 7. meshcore_gui (applicatie) │
|
||||
│ BLEWorker, EventHandler, CommandHandler │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 6. meshcore (meshcore_py) (protocol) │
|
||||
│ MeshCore.connect(), commands.*, event callbacks │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 5. bleak (BLE abstractie) │
|
||||
│ BleakClient.connect(), start_notify(), write() │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 4. dbus_fast (D-Bus async client) │
|
||||
│ MessageBus, ServiceInterface, method calls │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 3. D-Bus system bus (IPC) │
|
||||
│ /org/bluez/hci0, org.bluez.Device1, Agent1 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 2. BlueZ (bluetoothd) (Bluetooth daemon) │
|
||||
│ GATT, pairing, bonding, device management │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 1. Linux Kernel + Hardware (HCI driver + radio) │
|
||||
│ hci0, Bluetooth 5.0 chip (RPi5 built-in / USB) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Libraries en hun rol
|
||||
|
||||
### 2.1 bleak (Bluetooth Low Energy platform Agnostic Klient)
|
||||
|
||||
**Doel:** Cross-platform Python BLE library. Abstracteert de platform-specifieke BLE backends achter één API.
|
||||
|
||||
| Platform | Backend | Communicatie |
|
||||
|----------|---------|-------------|
|
||||
| Linux | BlueZ via D-Bus | `dbus_fast` → `bluetoothd` |
|
||||
| macOS | CoreBluetooth | Objective-C bridge via `pyobjc` |
|
||||
| Windows | WinRT | Windows Runtime BLE API |
|
||||
|
||||
**Hoe bleak werkt op Linux:**
|
||||
|
||||
Bleak praat *niet* rechtstreeks met de Bluetooth hardware. In plaats daarvan stuurt bleak D-Bus berichten naar de BlueZ daemon (`bluetoothd`), die op zijn beurt de kernel HCI driver aanstuurt. Elk bleak-commando wordt vertaald naar een D-Bus method call:
|
||||
|
||||
| bleak API | D-Bus call naar BlueZ |
|
||||
|-----------|----------------------|
|
||||
| `BleakClient.connect()` | `org.bluez.Device1.Connect()` |
|
||||
| `BleakClient.disconnect()` | `org.bluez.Device1.Disconnect()` |
|
||||
| `BleakClient.start_notify(uuid, callback)` | `org.bluez.GattCharacteristic1.StartNotify()` |
|
||||
| `BleakClient.write_gatt_char(uuid, data)` | `org.bluez.GattCharacteristic1.WriteValue()` |
|
||||
| `BleakScanner.discover()` | `org.bluez.Adapter1.StartDiscovery()` |
|
||||
|
||||
Bleak installeert automatisch `dbus_fast` als dependency.
|
||||
|
||||
### 2.2 dbus_fast
|
||||
|
||||
**Doel:** Async Python D-Bus library. Biedt twee functies:
|
||||
|
||||
1. **Client** — Bleak gebruikt `dbus_fast.aio.MessageBus` om D-Bus method calls naar BlueZ te sturen (connect, read, write, notify). Dit is intern aan bleak; onze code raakt dit niet direct aan.
|
||||
|
||||
2. **Server** — Onze `ble_agent.py` gebruikt `dbus_fast.service.ServiceInterface` om een D-Bus service te *exporteren*: de PIN agent die BlueZ aanroept wanneer het device pairing nodig heeft.
|
||||
|
||||
Doordat `dbus_fast` al een dependency van `bleak` is, hoeven we geen extra packages te installeren.
|
||||
|
||||
### 2.3 meshcore (meshcore_py)
|
||||
|
||||
**Doel:** MeshCore protocol implementatie. Vertaalt hoge-niveau commando's naar BLE GATT read/write operaties.
|
||||
|
||||
**GATT Service:** MeshCore devices gebruiken de **Nordic UART Service (NUS)** voor communicatie:
|
||||
|
||||
| Characteristic | UUID | Richting | Functie |
|
||||
|---------------|------|----------|---------|
|
||||
| RX | `6e400002-b5a3-f393-e0a9-e50e24dcca9e` | Host → Device | Commando's schrijven |
|
||||
| TX | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Device → Host | Responses/events ontvangen (notify) |
|
||||
|
||||
**Protocol:** De meshcore library:
|
||||
- Serialiseert commando's (appstart, device_query, get_contacts, send_msg, etc.) naar binaire packets
|
||||
- Schrijft deze naar de NUS RX characteristic via `bleak.write_gatt_char()`
|
||||
- Luistert op de NUS TX characteristic via `bleak.start_notify()` voor responses en async events
|
||||
- Deserialiseert binaire responses terug naar Python dicts met event types
|
||||
|
||||
**Communicatiepatroon:** Request-response met async events:
|
||||
|
||||
```
|
||||
meshcore_gui → meshcore → bleak → D-Bus → BlueZ → HCI → Radio → T1000-E
|
||||
│
|
||||
meshcore_gui ← meshcore ← bleak ← D-Bus ← BlueZ ← HCI ← Radio ←──────┘
|
||||
```
|
||||
|
||||
Commando's zijn *subscribe-before-send*: meshcore registreert eerst een notify handler op de TX characteristic, stuurt dan het commando via de RX characteristic, en wacht op de response via de notify callback. Dit voorkomt race conditions waarbij de response arriveert voordat de listener klaar is (gefixt in meshcore_py PR #52).
|
||||
|
||||
### 2.4 meshcoredecoder
|
||||
|
||||
**Doel:** Decodering van ruwe LoRa packets die via de RX log binnenkomen. Decrypts packets met channel keys en extraheert route-informatie (path hashes, hop data). Gebruikt door `PacketDecoder` in de BLE events layer.
|
||||
|
||||
### 2.5 Onze eigen BLE modules
|
||||
|
||||
| Module | Library | Functie |
|
||||
|--------|---------|---------|
|
||||
| `ble_agent.py` | `dbus_fast` (server) | Exporteert `org.bluez.Agent1` interface op D-Bus; beantwoordt PIN requests |
|
||||
| `ble_reconnect.py` | `dbus_fast` (client) | `remove_bond()`: roept `org.bluez.Adapter1.RemoveDevice()` aan via D-Bus |
|
||||
| `worker.py` | `meshcore` + `bleak` (indirect) | `MeshCore.connect()`, command loop, disconnect detection |
|
||||
| `commands.py` | `meshcore` | `mc.commands.send_msg()`, `send_advert()`, etc. |
|
||||
| `events.py` | `meshcore` | Callbacks: `CHANNEL_MSG_RECV`, `RX_LOG_DATA`, etc. |
|
||||
|
||||
---
|
||||
|
||||
## 3. De drie D-Bus gesprekken
|
||||
|
||||
Onze applicatie voert drie soorten D-Bus communicatie uit, elk met een ander doel:
|
||||
|
||||
### 3.1 PIN Agent (dbus_fast — server mode)
|
||||
|
||||
**Probleem:** Wanneer BlueZ een BLE device wil pairen dat een PIN vereist, zoekt het op de D-Bus naar een geregistreerde Agent die de PIN kan leveren. Zonder agent faalt de pairing met "failed to discover services".
|
||||
|
||||
**Oplossing:** `ble_agent.py` exporteert een `org.bluez.Agent1` service op D-Bus path `/meshcore/ble_agent`. BlueZ roept methodes aan op onze agent:
|
||||
|
||||
```
|
||||
BlueZ (bluetoothd) Onze Agent (ble_agent.py)
|
||||
│ │
|
||||
│── RegisterAgent(/meshcore/ble_agent) ──→│ (bij startup)
|
||||
│← OK ──────────────────────────────────│
|
||||
│ │
|
||||
│── RequestDefaultAgent() ──────────────→│
|
||||
│← OK ──────────────────────────────────│
|
||||
│ │
|
||||
│ ... device wil pairen ... │
|
||||
│ │
|
||||
│── RequestPinCode(/org/bluez/.../dev) ─→│
|
||||
│← "123456" ───────────────────────────│
|
||||
│ │
|
||||
│ ... pairing succesvol ... │
|
||||
```
|
||||
|
||||
### 3.2 Bond Cleanup (dbus_fast — client mode)
|
||||
|
||||
**Probleem:** Na een disconnect slaat BlueZ de pairing keys op (een "bond"). Bij reconnectie gebruikt BlueZ deze oude keys, maar het device heeft ze verworpen → "PIN or Key Missing" error.
|
||||
|
||||
**Oplossing:** `ble_reconnect.py` stuurt een D-Bus method call naar BlueZ:
|
||||
|
||||
```python
|
||||
# Equivalent van: bluetoothctl remove FF:05:D6:71:83:8D
|
||||
bus.call(
|
||||
destination="org.bluez",
|
||||
path="/org/bluez/hci0", # Adapter
|
||||
interface="org.bluez.Adapter1",
|
||||
member="RemoveDevice",
|
||||
signature="o",
|
||||
body=["/org/bluez/hci0/dev_FF_05_D6_71_83_8D"] # Device object path
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 BLE Communicatie (bleak → dbus_fast — client mode)
|
||||
|
||||
Bleak stuurt intern D-Bus berichten voor alle BLE operaties. Dit is transparant voor onze code — wij roepen alleen de bleak API aan, bleak vertaalt naar D-Bus:
|
||||
|
||||
```python
|
||||
# Onze code (via meshcore):
|
||||
await mc.connect(ble_address)
|
||||
|
||||
# Wat bleak intern doet:
|
||||
await bus.call("org.bluez.Device1.Connect()")
|
||||
await bus.call("org.bluez.GattCharacteristic1.StartNotify()") # TX char
|
||||
await bus.call("org.bluez.GattCharacteristic1.WriteValue()") # RX char
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Sequence Diagram — Volledige BLE Lifecycle
|
||||
|
||||
Het onderstaande diagram toont de complete levenscyclus van een BLE sessie, van startup tot disconnect en reconnect.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant GUI as GUI Thread<br/>(NiceGUI)
|
||||
participant Worker as BLEWorker<br/>(asyncio thread)
|
||||
participant Agent as BleAgentManager<br/>(ble_agent.py)
|
||||
participant Reconnect as ble_reconnect.py
|
||||
participant MC as meshcore<br/>(MeshCore)
|
||||
participant Bleak as bleak<br/>(BleakClient)
|
||||
participant DBus as D-Bus<br/>(system bus)
|
||||
participant BZ as BlueZ<br/>(bluetoothd)
|
||||
participant Dev as T1000-E<br/>(BLE device)
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 1: PIN Agent Registratie ═══
|
||||
|
||||
Worker->>Agent: start(pin="123456")
|
||||
Agent->>DBus: connect to system bus
|
||||
Agent->>DBus: export /meshcore/ble_agent<br/>(org.bluez.Agent1)
|
||||
Agent->>DBus: RegisterAgent(/meshcore/ble_agent, "KeyboardOnly")
|
||||
DBus->>BZ: RegisterAgent
|
||||
BZ-->>DBus: OK
|
||||
Agent->>DBus: RequestDefaultAgent(/meshcore/ble_agent)
|
||||
DBus->>BZ: RequestDefaultAgent
|
||||
BZ-->>DBus: OK
|
||||
Agent-->>Worker: Agent ready
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 2: Bond Cleanup ═══
|
||||
|
||||
Worker->>Reconnect: remove_bond("FF:05:...")
|
||||
Reconnect->>DBus: Adapter1.RemoveDevice(/org/bluez/hci0/dev_FF_05_...)
|
||||
DBus->>BZ: RemoveDevice
|
||||
BZ-->>DBus: OK (of "Does Not Exist" → genegeerd)
|
||||
Reconnect-->>Worker: Bond removed
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 3: Verbinding + GATT Discovery ═══
|
||||
|
||||
Worker->>MC: MeshCore.connect("FF:05:...")
|
||||
MC->>Bleak: BleakClient.connect()
|
||||
Bleak->>DBus: Device1.Connect()
|
||||
DBus->>BZ: Connect
|
||||
BZ->>Dev: BLE Connection Request
|
||||
Dev-->>BZ: Connection Accepted
|
||||
|
||||
Note over BZ,Dev: Pairing vereist (PIN)
|
||||
|
||||
BZ->>DBus: Agent1.RequestPinCode(device_path)
|
||||
DBus->>Agent: RequestPinCode()
|
||||
Agent-->>DBus: "123456"
|
||||
DBus-->>BZ: PIN
|
||||
BZ->>Dev: Pairing met PIN
|
||||
Dev-->>BZ: Pairing OK + Encryption active
|
||||
|
||||
BZ->>BZ: GATT Service Discovery
|
||||
BZ-->>Bleak: Services resolved (NUS: 6e400001-...)
|
||||
Bleak-->>MC: Connected
|
||||
|
||||
MC->>Bleak: start_notify(TX: 6e400003-...)
|
||||
Bleak->>DBus: GattCharacteristic1.StartNotify()
|
||||
DBus->>BZ: StartNotify
|
||||
BZ-->>Bleak: Notifications enabled
|
||||
|
||||
MC->>Bleak: write(RX: 6e400002-..., appstart_cmd)
|
||||
Bleak->>DBus: GattCharacteristic1.WriteValue(data)
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: BLE Write (appstart)
|
||||
Dev-->>BZ: BLE Notify (response)
|
||||
BZ-->>Bleak: Notification callback
|
||||
Bleak-->>MC: Event: SELF_INFO
|
||||
MC-->>Worker: self_info = {name, pubkey, freq, ...}
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 4: Data Laden ═══
|
||||
|
||||
Worker->>MC: commands.send_device_query()
|
||||
MC->>Bleak: write(RX, device_query_cmd)
|
||||
Bleak->>DBus: WriteValue
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: device_query
|
||||
Dev-->>BZ: notify(response)
|
||||
BZ-->>Bleak: callback
|
||||
Bleak-->>MC: Event: DEVICE_QUERY
|
||||
MC-->>Worker: {firmware, tx_power, ...}
|
||||
|
||||
Worker->>MC: commands.get_channel(0..N)
|
||||
MC-->>Worker: {name, channel_secret}
|
||||
|
||||
Worker->>MC: commands.get_contacts()
|
||||
MC-->>Worker: [{pubkey, name, type, lat, lon}, ...]
|
||||
|
||||
Worker->>GUI: SharedData.set_channels(), set_contacts(), ...
|
||||
GUI->>GUI: Timer 500ms → update UI
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 5: Operationele Loop ═══
|
||||
|
||||
loop Elke 500ms
|
||||
GUI->>GUI: _update_ui() → lees SharedData snapshot
|
||||
end
|
||||
|
||||
loop Command Queue
|
||||
GUI->>Worker: put_command("send_msg", {text, channel})
|
||||
Worker->>MC: commands.send_msg(channel, text)
|
||||
MC->>Bleak: write(RX, send_msg_packet)
|
||||
Bleak->>DBus: WriteValue
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: BLE Write
|
||||
end
|
||||
|
||||
loop Async Events (continu)
|
||||
Dev-->>BZ: BLE Notify (incoming mesh message)
|
||||
BZ-->>Bleak: Notification callback
|
||||
Bleak-->>MC: raw data
|
||||
MC-->>Worker: Event: CHANNEL_MSG_RECV
|
||||
Worker->>Worker: EventHandler → dedup → SharedData.add_message()
|
||||
Worker->>GUI: message_updated = True
|
||||
end
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 6: Disconnect + Auto-Reconnect ═══
|
||||
|
||||
Dev--xBZ: BLE link lost (~2 uur timeout)
|
||||
BZ-->>Bleak: Disconnected callback
|
||||
Bleak-->>MC: Connection lost
|
||||
MC-->>Worker: Exception: "not connected" / "disconnected"
|
||||
|
||||
Worker->>Worker: Disconnect gedetecteerd
|
||||
|
||||
loop Reconnect (max 5 pogingen, lineaire backoff)
|
||||
Worker->>Reconnect: remove_bond("FF:05:...")
|
||||
Reconnect->>DBus: Adapter1.RemoveDevice
|
||||
DBus->>BZ: RemoveDevice
|
||||
BZ-->>Reconnect: OK
|
||||
|
||||
Worker->>Worker: wait(attempt × 5s)
|
||||
|
||||
Worker->>MC: MeshCore.connect("FF:05:...")
|
||||
MC->>Bleak: BleakClient.connect()
|
||||
Bleak->>DBus: Device1.Connect()
|
||||
DBus->>BZ: Connect
|
||||
BZ->>Dev: BLE Connection Request
|
||||
|
||||
alt Verbinding succesvol
|
||||
Dev-->>BZ: Connected + Paired (PIN via Agent)
|
||||
BZ-->>Bleak: Connected
|
||||
Worker->>Worker: Re-wire event handlers + reload data
|
||||
Worker->>GUI: set_status("✅ Reconnected")
|
||||
else Verbinding mislukt
|
||||
BZ-->>Bleak: Error
|
||||
Worker->>Worker: Volgende poging...
|
||||
end
|
||||
end
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 7: Cleanup ═══
|
||||
|
||||
Worker->>Agent: stop()
|
||||
Agent->>DBus: UnregisterAgent(/meshcore/ble_agent)
|
||||
Agent->>DBus: disconnect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. GATT Communicatie in Detail
|
||||
|
||||
### 5.1 Nordic UART Service (NUS)
|
||||
|
||||
Het MeshCore device adverteert één primaire BLE service: de **Nordic UART Service**. Dit is een de-facto standaard voor seriële communicatie over BLE, oorspronkelijk ontworpen door Nordic Semiconductor.
|
||||
|
||||
```
|
||||
Service: Nordic UART Service
|
||||
UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
|
||||
├── RX Characteristic (Write Without Response)
|
||||
│ UUID: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
|
||||
│ Richting: Host → Device
|
||||
│ Gebruik: Commando's sturen naar het T1000-E
|
||||
│ Max grootte: 20 bytes per write (MTU-afhankelijk)
|
||||
│
|
||||
└── TX Characteristic (Notify)
|
||||
UUID: 6e400003-b5a3-f393-e0a9-e50e24dcca9e
|
||||
Richting: Device → Host
|
||||
Gebruik: Responses en async events ontvangen
|
||||
Activatie: bleak.start_notify() → BlueZ StartNotify
|
||||
```
|
||||
|
||||
### 5.2 Dataflow per commando
|
||||
|
||||
Een typisch commando (bijv. "stuur een mesh bericht") doorloopt deze stappen:
|
||||
|
||||
```
|
||||
1. GUI: gebruiker typt bericht, klikt Send
|
||||
2. GUI → SharedData: put_command("send_msg", {channel: 0, text: "Hello"})
|
||||
3. BLEWorker: haalt command uit queue
|
||||
4. meshcore: serialiseert naar binary packet
|
||||
→ [header][cmd_type][channel_idx][payload_len][utf8_text]
|
||||
5. bleak: write_gatt_char(NUS_RX_UUID, packet)
|
||||
6. dbus_fast: GattCharacteristic1.WriteValue(packet_bytes, {})
|
||||
7. BlueZ: schrijft naar HCI controller
|
||||
8. HCI: stuurt BLE PDU via radio
|
||||
9. T1000-E: ontvangt, verwerkt, stuurt via LoRa mesh
|
||||
```
|
||||
|
||||
De response (of een inkomend mesh bericht) loopt de omgekeerde route:
|
||||
|
||||
```
|
||||
1. T1000-E: ontvangt mesh bericht via LoRa
|
||||
2. T1000-E → HCI: BLE notification met data
|
||||
3. BlueZ: ontvangt notification, stuurt via D-Bus
|
||||
4. dbus_fast: roept de notify callback in bleak aan
|
||||
5. bleak: roept de registered callback in meshcore aan
|
||||
6. meshcore: deserialiseert binary → Event(type, payload)
|
||||
7. BLEWorker: EventHandler verwerkt het event
|
||||
→ dedup check → naam resolutie → path hash extractie
|
||||
8. SharedData: add_message(Message.incoming(...))
|
||||
9. GUI: ziet message_updated flag bij volgende 500ms poll
|
||||
```
|
||||
|
||||
### 5.3 Waarom subscribe-before-send?
|
||||
|
||||
BLE notifications zijn asynchroon. Als meshcore eerst het commando schrijft en *daarna* `start_notify()` aanroept, kan de response al verloren zijn gegaan voordat de listener klaar is. Dit was een bug in de originele meshcore_py die leidde tot ~2 minuten startup delay:
|
||||
|
||||
```
|
||||
❌ Oud (race condition):
|
||||
write(RX, command) → device antwoordt direct
|
||||
start_notify(TX) → te laat, response is al weg
|
||||
|
||||
✅ Nieuw (PR #52):
|
||||
start_notify(TX) → listener actief
|
||||
write(RX, command) → device antwoordt
|
||||
callback fired → response ontvangen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Pairing en Bonding
|
||||
|
||||
### 6.1 Waarom PIN pairing?
|
||||
|
||||
Het T1000-E device is geconfigureerd met BLE PIN `123456` (instelbaar via firmware). Dit voorkomt dat willekeurige BLE clients verbinden. BlueZ ondersteunt PIN pairing via het **Agent** mechanisme.
|
||||
|
||||
### 6.2 Agent interface
|
||||
|
||||
BlueZ definieert de `org.bluez.Agent1` D-Bus interface. Onze `BluezAgent` class implementeert deze callbacks:
|
||||
|
||||
| Methode | D-Bus Signature | Wanneer aangeroepen | Ons antwoord |
|
||||
|---------|----------------|--------------------:|-------------|
|
||||
| `RequestPinCode` | `o → s` | Device vraagt PIN | `"123456"` |
|
||||
| `RequestPasskey` | `o → u` | Device vraagt numeriek passkey | `123456` (uint32) |
|
||||
| `DisplayPasskey` | `oqu → ` | Passkey tonen (info only) | (log only) |
|
||||
| `RequestConfirmation` | `ou → ` | Bevestig passkey match | (accept) |
|
||||
| `AuthorizeService` | `os → ` | Service autorisatie | (accept) |
|
||||
| `Cancel` | ` → ` | Pairing geannuleerd | (log only) |
|
||||
| `Release` | ` → ` | Agent niet meer nodig | (cleanup) |
|
||||
|
||||
### 6.3 Het bonding probleem
|
||||
|
||||
Na succesvolle pairing slaat BlueZ de encryption keys op in `/var/lib/bluetooth/<adapter>/<device>/info`. Dit heet een "bond". Bij de volgende connectie probeert BlueZ deze keys te hergebruiken.
|
||||
|
||||
**Het probleem:** Het T1000-E verwerpt na ~2 uur de BLE verbinding (firmware timeout). BlueZ heeft nog de oude bond keys, maar het device heeft ze verworpen. Resultaat:
|
||||
|
||||
```
|
||||
BlueZ: "Ik heb keys voor dit device, gebruik die"
|
||||
T1000-E: "Ik ken deze keys niet → Reject (PIN or Key Missing)"
|
||||
BlueZ: "Pairing failed"
|
||||
```
|
||||
|
||||
**De oplossing:** Vóór elke reconnectie verwijderen we de bond:
|
||||
|
||||
```
|
||||
remove_bond() → Adapter1.RemoveDevice() → BlueZ wist keys
|
||||
connect() → BlueZ: "Geen keys, start verse pairing"
|
||||
Agent → levert PIN → verse pairing succesvol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. D-Bus Policy
|
||||
|
||||
Normale gebruikers mogen standaard niet alle BlueZ D-Bus interfaces aanspreken. De D-Bus policy file (`/etc/dbus-1/system.d/meshcore-ble.conf`) geeft de gebruiker die de service draait toestemming:
|
||||
|
||||
```xml
|
||||
<busconfig>
|
||||
<policy user="hans">
|
||||
<allow send_destination="org.bluez"/>
|
||||
<allow send_interface="org.bluez.Agent1"/>
|
||||
<allow send_interface="org.bluez.AgentManager1"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
```
|
||||
|
||||
Zonder deze policy:
|
||||
- `bleak` kan nog steeds verbinden (bleak gebruikt een standaard D-Bus policy die al met BlueZ meekomt)
|
||||
- Onze **agent** kan zich niet registreren → PIN pairing faalt
|
||||
- Onze **bond cleanup** kan `RemoveDevice` niet aanroepen
|
||||
|
||||
---
|
||||
|
||||
## 8. Samenvatting Dependencies
|
||||
|
||||
```
|
||||
meshcore-gui
|
||||
├── nicegui → Web UI framework (onze GUI)
|
||||
├── meshcore → MeshCore protocol (commando's, events)
|
||||
│ └── bleak → BLE abstractie (connect, notify, write)
|
||||
│ └── dbus_fast → D-Bus communicatie (naar BlueZ)
|
||||
├── meshcoredecoder → LoRa packet decryptie + route extractie
|
||||
└── (geen extra) → ble_agent.py en ble_reconnect.py
|
||||
gebruiken dbus_fast die al via bleak
|
||||
geïnstalleerd is
|
||||
```
|
||||
|
||||
Alle BLE-gerelateerde functionaliteit draait op precies **vier Python packages**: `bleak`, `dbus_fast`, `meshcore`, en `meshcoredecoder`. Er zijn geen system-level dependencies meer nodig buiten `bluez` zelf (geen `bluez-tools`, geen `bt-agent`).
|
||||
# Legacy BLE Document
|
||||
|
||||
> **Note:** This document describes the BLE architecture and is retained for historical reference. The current GUI uses USB serial.
|
||||
@@ -0,0 +1,491 @@
|
||||
# MeshCore GUI — BLE Architecture
|
||||
|
||||
## Overzicht
|
||||
|
||||
Dit document beschrijft hoe MeshCore GUI communiceert met een MeshCore T1000-E device via Bluetooth Low Energy (BLE), welke libraries daarbij betrokken zijn, en hoe de volledige stack van hardware tot applicatielogica in elkaar zit.
|
||||
|
||||
---
|
||||
|
||||
## 1. De BLE Stack
|
||||
|
||||
De communicatie loopt door 7 lagen, van hardware tot GUI:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 7. meshcore_gui (applicatie) │
|
||||
│ BLEWorker, EventHandler, CommandHandler │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 6. meshcore (meshcore_py) (protocol) │
|
||||
│ MeshCore.connect(), commands.*, event callbacks │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 5. bleak (BLE abstractie) │
|
||||
│ BleakClient.connect(), start_notify(), write() │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 4. dbus_fast (D-Bus async client) │
|
||||
│ MessageBus, ServiceInterface, method calls │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 3. D-Bus system bus (IPC) │
|
||||
│ /org/bluez/hci0, org.bluez.Device1, Agent1 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 2. BlueZ (bluetoothd) (Bluetooth daemon) │
|
||||
│ GATT, pairing, bonding, device management │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 1. Linux Kernel + Hardware (HCI driver + radio) │
|
||||
│ hci0, Bluetooth 5.0 chip (RPi5 built-in / USB) │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Libraries en hun rol
|
||||
|
||||
### 2.1 bleak (Bluetooth Low Energy platform Agnostic Klient)
|
||||
|
||||
**Doel:** Cross-platform Python BLE library. Abstracteert de platform-specifieke BLE backends achter één API.
|
||||
|
||||
| Platform | Backend | Communicatie |
|
||||
|----------|---------|-------------|
|
||||
| Linux | BlueZ via D-Bus | `dbus_fast` → `bluetoothd` |
|
||||
| macOS | CoreBluetooth | Objective-C bridge via `pyobjc` |
|
||||
| Windows | WinRT | Windows Runtime BLE API |
|
||||
|
||||
**Hoe bleak werkt op Linux:**
|
||||
|
||||
Bleak praat *niet* rechtstreeks met de Bluetooth hardware. In plaats daarvan stuurt bleak D-Bus berichten naar de BlueZ daemon (`bluetoothd`), die op zijn beurt de kernel HCI driver aanstuurt. Elk bleak-commando wordt vertaald naar een D-Bus method call:
|
||||
|
||||
| bleak API | D-Bus call naar BlueZ |
|
||||
|-----------|----------------------|
|
||||
| `BleakClient.connect()` | `org.bluez.Device1.Connect()` |
|
||||
| `BleakClient.disconnect()` | `org.bluez.Device1.Disconnect()` |
|
||||
| `BleakClient.start_notify(uuid, callback)` | `org.bluez.GattCharacteristic1.StartNotify()` |
|
||||
| `BleakClient.write_gatt_char(uuid, data)` | `org.bluez.GattCharacteristic1.WriteValue()` |
|
||||
| `BleakScanner.discover()` | `org.bluez.Adapter1.StartDiscovery()` |
|
||||
|
||||
Bleak installeert automatisch `dbus_fast` als dependency.
|
||||
|
||||
### 2.2 dbus_fast
|
||||
|
||||
**Doel:** Async Python D-Bus library. Biedt twee functies:
|
||||
|
||||
1. **Client** — Bleak gebruikt `dbus_fast.aio.MessageBus` om D-Bus method calls naar BlueZ te sturen (connect, read, write, notify). Dit is intern aan bleak; onze code raakt dit niet direct aan.
|
||||
|
||||
2. **Server** — Onze `ble_agent.py` gebruikt `dbus_fast.service.ServiceInterface` om een D-Bus service te *exporteren*: de PIN agent die BlueZ aanroept wanneer het device pairing nodig heeft.
|
||||
|
||||
Doordat `dbus_fast` al een dependency van `bleak` is, hoeven we geen extra packages te installeren.
|
||||
|
||||
### 2.3 meshcore (meshcore_py)
|
||||
|
||||
**Doel:** MeshCore protocol implementatie. Vertaalt hoge-niveau commando's naar BLE GATT read/write operaties.
|
||||
|
||||
**GATT Service:** MeshCore devices gebruiken de **Nordic UART Service (NUS)** voor communicatie:
|
||||
|
||||
| Characteristic | UUID | Richting | Functie |
|
||||
|---------------|------|----------|---------|
|
||||
| RX | `6e400002-b5a3-f393-e0a9-e50e24dcca9e` | Host → Device | Commando's schrijven |
|
||||
| TX | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Device → Host | Responses/events ontvangen (notify) |
|
||||
|
||||
**Protocol:** De meshcore library:
|
||||
- Serialiseert commando's (appstart, device_query, get_contacts, send_msg, etc.) naar binaire packets
|
||||
- Schrijft deze naar de NUS RX characteristic via `bleak.write_gatt_char()`
|
||||
- Luistert op de NUS TX characteristic via `bleak.start_notify()` voor responses en async events
|
||||
- Deserialiseert binaire responses terug naar Python dicts met event types
|
||||
|
||||
**Communicatiepatroon:** Request-response met async events:
|
||||
|
||||
```
|
||||
meshcore_gui → meshcore → bleak → D-Bus → BlueZ → HCI → Radio → T1000-E
|
||||
│
|
||||
meshcore_gui ← meshcore ← bleak ← D-Bus ← BlueZ ← HCI ← Radio ←──────┘
|
||||
```
|
||||
|
||||
Commando's zijn *subscribe-before-send*: meshcore registreert eerst een notify handler op de TX characteristic, stuurt dan het commando via de RX characteristic, en wacht op de response via de notify callback. Dit voorkomt race conditions waarbij de response arriveert voordat de listener klaar is (gefixt in meshcore_py PR #52).
|
||||
|
||||
### 2.4 meshcoredecoder
|
||||
|
||||
**Doel:** Decodering van ruwe LoRa packets die via de RX log binnenkomen. Decrypts packets met channel keys en extraheert route-informatie (path hashes, hop data). Gebruikt door `PacketDecoder` in de BLE events layer.
|
||||
|
||||
### 2.5 Onze eigen BLE modules
|
||||
|
||||
| Module | Library | Functie |
|
||||
|--------|---------|---------|
|
||||
| `ble_agent.py` | `dbus_fast` (server) | Exporteert `org.bluez.Agent1` interface op D-Bus; beantwoordt PIN requests |
|
||||
| `ble_reconnect.py` | `dbus_fast` (client) | `remove_bond()`: roept `org.bluez.Adapter1.RemoveDevice()` aan via D-Bus |
|
||||
| `worker.py` | `meshcore` + `bleak` (indirect) | `MeshCore.connect()`, command loop, disconnect detection |
|
||||
| `commands.py` | `meshcore` | `mc.commands.send_msg()`, `send_advert()`, etc. |
|
||||
| `events.py` | `meshcore` | Callbacks: `CHANNEL_MSG_RECV`, `RX_LOG_DATA`, etc. |
|
||||
|
||||
---
|
||||
|
||||
## 3. De drie D-Bus gesprekken
|
||||
|
||||
Onze applicatie voert drie soorten D-Bus communicatie uit, elk met een ander doel:
|
||||
|
||||
### 3.1 PIN Agent (dbus_fast — server mode)
|
||||
|
||||
**Probleem:** Wanneer BlueZ een BLE device wil pairen dat een PIN vereist, zoekt het op de D-Bus naar een geregistreerde Agent die de PIN kan leveren. Zonder agent faalt de pairing met "failed to discover services".
|
||||
|
||||
**Oplossing:** `ble_agent.py` exporteert een `org.bluez.Agent1` service op D-Bus path `/meshcore/ble_agent`. BlueZ roept methodes aan op onze agent:
|
||||
|
||||
```
|
||||
BlueZ (bluetoothd) Onze Agent (ble_agent.py)
|
||||
│ │
|
||||
│── RegisterAgent(/meshcore/ble_agent) ──→│ (bij startup)
|
||||
│← OK ──────────────────────────────────│
|
||||
│ │
|
||||
│── RequestDefaultAgent() ──────────────→│
|
||||
│← OK ──────────────────────────────────│
|
||||
│ │
|
||||
│ ... device wil pairen ... │
|
||||
│ │
|
||||
│── RequestPinCode(/org/bluez/.../dev) ─→│
|
||||
│← "123456" ───────────────────────────│
|
||||
│ │
|
||||
│ ... pairing succesvol ... │
|
||||
```
|
||||
|
||||
### 3.2 Bond Cleanup (dbus_fast — client mode)
|
||||
|
||||
**Probleem:** Na een disconnect slaat BlueZ de pairing keys op (een "bond"). Bij reconnectie gebruikt BlueZ deze oude keys, maar het device heeft ze verworpen → "PIN or Key Missing" error.
|
||||
|
||||
**Oplossing:** `ble_reconnect.py` stuurt een D-Bus method call naar BlueZ:
|
||||
|
||||
```python
|
||||
# Equivalent van: bluetoothctl remove FF:05:D6:71:83:8D
|
||||
bus.call(
|
||||
destination="org.bluez",
|
||||
path="/org/bluez/hci0", # Adapter
|
||||
interface="org.bluez.Adapter1",
|
||||
member="RemoveDevice",
|
||||
signature="o",
|
||||
body=["/org/bluez/hci0/dev_FF_05_D6_71_83_8D"] # Device object path
|
||||
)
|
||||
```
|
||||
|
||||
### 3.3 BLE Communicatie (bleak → dbus_fast — client mode)
|
||||
|
||||
Bleak stuurt intern D-Bus berichten voor alle BLE operaties. Dit is transparant voor onze code — wij roepen alleen de bleak API aan, bleak vertaalt naar D-Bus:
|
||||
|
||||
```python
|
||||
# Onze code (via meshcore):
|
||||
await mc.connect(ble_address)
|
||||
|
||||
# Wat bleak intern doet:
|
||||
await bus.call("org.bluez.Device1.Connect()")
|
||||
await bus.call("org.bluez.GattCharacteristic1.StartNotify()") # TX char
|
||||
await bus.call("org.bluez.GattCharacteristic1.WriteValue()") # RX char
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Sequence Diagram — Volledige BLE Lifecycle
|
||||
|
||||
Het onderstaande diagram toont de complete levenscyclus van een BLE sessie, van startup tot disconnect en reconnect.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant GUI as GUI Thread<br/>(NiceGUI)
|
||||
participant Worker as BLEWorker<br/>(asyncio thread)
|
||||
participant Agent as BleAgentManager<br/>(ble_agent.py)
|
||||
participant Reconnect as ble_reconnect.py
|
||||
participant MC as meshcore<br/>(MeshCore)
|
||||
participant Bleak as bleak<br/>(BleakClient)
|
||||
participant DBus as D-Bus<br/>(system bus)
|
||||
participant BZ as BlueZ<br/>(bluetoothd)
|
||||
participant Dev as T1000-E<br/>(BLE device)
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 1: PIN Agent Registratie ═══
|
||||
|
||||
Worker->>Agent: start(pin="123456")
|
||||
Agent->>DBus: connect to system bus
|
||||
Agent->>DBus: export /meshcore/ble_agent<br/>(org.bluez.Agent1)
|
||||
Agent->>DBus: RegisterAgent(/meshcore/ble_agent, "KeyboardOnly")
|
||||
DBus->>BZ: RegisterAgent
|
||||
BZ-->>DBus: OK
|
||||
Agent->>DBus: RequestDefaultAgent(/meshcore/ble_agent)
|
||||
DBus->>BZ: RequestDefaultAgent
|
||||
BZ-->>DBus: OK
|
||||
Agent-->>Worker: Agent ready
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 2: Bond Cleanup ═══
|
||||
|
||||
Worker->>Reconnect: remove_bond("FF:05:...")
|
||||
Reconnect->>DBus: Adapter1.RemoveDevice(/org/bluez/hci0/dev_FF_05_...)
|
||||
DBus->>BZ: RemoveDevice
|
||||
BZ-->>DBus: OK (of "Does Not Exist" → genegeerd)
|
||||
Reconnect-->>Worker: Bond removed
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 3: Verbinding + GATT Discovery ═══
|
||||
|
||||
Worker->>MC: MeshCore.connect("FF:05:...")
|
||||
MC->>Bleak: BleakClient.connect()
|
||||
Bleak->>DBus: Device1.Connect()
|
||||
DBus->>BZ: Connect
|
||||
BZ->>Dev: BLE Connection Request
|
||||
Dev-->>BZ: Connection Accepted
|
||||
|
||||
Note over BZ,Dev: Pairing vereist (PIN)
|
||||
|
||||
BZ->>DBus: Agent1.RequestPinCode(device_path)
|
||||
DBus->>Agent: RequestPinCode()
|
||||
Agent-->>DBus: "123456"
|
||||
DBus-->>BZ: PIN
|
||||
BZ->>Dev: Pairing met PIN
|
||||
Dev-->>BZ: Pairing OK + Encryption active
|
||||
|
||||
BZ->>BZ: GATT Service Discovery
|
||||
BZ-->>Bleak: Services resolved (NUS: 6e400001-...)
|
||||
Bleak-->>MC: Connected
|
||||
|
||||
MC->>Bleak: start_notify(TX: 6e400003-...)
|
||||
Bleak->>DBus: GattCharacteristic1.StartNotify()
|
||||
DBus->>BZ: StartNotify
|
||||
BZ-->>Bleak: Notifications enabled
|
||||
|
||||
MC->>Bleak: write(RX: 6e400002-..., appstart_cmd)
|
||||
Bleak->>DBus: GattCharacteristic1.WriteValue(data)
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: BLE Write (appstart)
|
||||
Dev-->>BZ: BLE Notify (response)
|
||||
BZ-->>Bleak: Notification callback
|
||||
Bleak-->>MC: Event: SELF_INFO
|
||||
MC-->>Worker: self_info = {name, pubkey, freq, ...}
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 4: Data Laden ═══
|
||||
|
||||
Worker->>MC: commands.send_device_query()
|
||||
MC->>Bleak: write(RX, device_query_cmd)
|
||||
Bleak->>DBus: WriteValue
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: device_query
|
||||
Dev-->>BZ: notify(response)
|
||||
BZ-->>Bleak: callback
|
||||
Bleak-->>MC: Event: DEVICE_QUERY
|
||||
MC-->>Worker: {firmware, tx_power, ...}
|
||||
|
||||
Worker->>MC: commands.get_channel(0..N)
|
||||
MC-->>Worker: {name, channel_secret}
|
||||
|
||||
Worker->>MC: commands.get_contacts()
|
||||
MC-->>Worker: [{pubkey, name, type, lat, lon}, ...]
|
||||
|
||||
Worker->>GUI: SharedData.set_channels(), set_contacts(), ...
|
||||
GUI->>GUI: Timer 500ms → update UI
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 5: Operationele Loop ═══
|
||||
|
||||
loop Elke 500ms
|
||||
GUI->>GUI: _update_ui() → lees SharedData snapshot
|
||||
end
|
||||
|
||||
loop Command Queue
|
||||
GUI->>Worker: put_command("send_msg", {text, channel})
|
||||
Worker->>MC: commands.send_msg(channel, text)
|
||||
MC->>Bleak: write(RX, send_msg_packet)
|
||||
Bleak->>DBus: WriteValue
|
||||
DBus->>BZ: WriteValue
|
||||
BZ->>Dev: BLE Write
|
||||
end
|
||||
|
||||
loop Async Events (continu)
|
||||
Dev-->>BZ: BLE Notify (incoming mesh message)
|
||||
BZ-->>Bleak: Notification callback
|
||||
Bleak-->>MC: raw data
|
||||
MC-->>Worker: Event: CHANNEL_MSG_RECV
|
||||
Worker->>Worker: EventHandler → dedup → SharedData.add_message()
|
||||
Worker->>GUI: message_updated = True
|
||||
end
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 6: Disconnect + Auto-Reconnect ═══
|
||||
|
||||
Dev--xBZ: BLE link lost (~2 uur timeout)
|
||||
BZ-->>Bleak: Disconnected callback
|
||||
Bleak-->>MC: Connection lost
|
||||
MC-->>Worker: Exception: "not connected" / "disconnected"
|
||||
|
||||
Worker->>Worker: Disconnect gedetecteerd
|
||||
|
||||
loop Reconnect (max 5 pogingen, lineaire backoff)
|
||||
Worker->>Reconnect: remove_bond("FF:05:...")
|
||||
Reconnect->>DBus: Adapter1.RemoveDevice
|
||||
DBus->>BZ: RemoveDevice
|
||||
BZ-->>Reconnect: OK
|
||||
|
||||
Worker->>Worker: wait(attempt × 5s)
|
||||
|
||||
Worker->>MC: MeshCore.connect("FF:05:...")
|
||||
MC->>Bleak: BleakClient.connect()
|
||||
Bleak->>DBus: Device1.Connect()
|
||||
DBus->>BZ: Connect
|
||||
BZ->>Dev: BLE Connection Request
|
||||
|
||||
alt Verbinding succesvol
|
||||
Dev-->>BZ: Connected + Paired (PIN via Agent)
|
||||
BZ-->>Bleak: Connected
|
||||
Worker->>Worker: Re-wire event handlers + reload data
|
||||
Worker->>GUI: set_status("✅ Reconnected")
|
||||
else Verbinding mislukt
|
||||
BZ-->>Bleak: Error
|
||||
Worker->>Worker: Volgende poging...
|
||||
end
|
||||
end
|
||||
|
||||
Note over Worker,Dev: ═══ FASE 7: Cleanup ═══
|
||||
|
||||
Worker->>Agent: stop()
|
||||
Agent->>DBus: UnregisterAgent(/meshcore/ble_agent)
|
||||
Agent->>DBus: disconnect()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. GATT Communicatie in Detail
|
||||
|
||||
### 5.1 Nordic UART Service (NUS)
|
||||
|
||||
Het MeshCore device adverteert één primaire BLE service: de **Nordic UART Service**. Dit is een de-facto standaard voor seriële communicatie over BLE, oorspronkelijk ontworpen door Nordic Semiconductor.
|
||||
|
||||
```
|
||||
Service: Nordic UART Service
|
||||
UUID: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
|
||||
├── RX Characteristic (Write Without Response)
|
||||
│ UUID: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
|
||||
│ Richting: Host → Device
|
||||
│ Gebruik: Commando's sturen naar het T1000-E
|
||||
│ Max grootte: 20 bytes per write (MTU-afhankelijk)
|
||||
│
|
||||
└── TX Characteristic (Notify)
|
||||
UUID: 6e400003-b5a3-f393-e0a9-e50e24dcca9e
|
||||
Richting: Device → Host
|
||||
Gebruik: Responses en async events ontvangen
|
||||
Activatie: bleak.start_notify() → BlueZ StartNotify
|
||||
```
|
||||
|
||||
### 5.2 Dataflow per commando
|
||||
|
||||
Een typisch commando (bijv. "stuur een mesh bericht") doorloopt deze stappen:
|
||||
|
||||
```
|
||||
1. GUI: gebruiker typt bericht, klikt Send
|
||||
2. GUI → SharedData: put_command("send_msg", {channel: 0, text: "Hello"})
|
||||
3. BLEWorker: haalt command uit queue
|
||||
4. meshcore: serialiseert naar binary packet
|
||||
→ [header][cmd_type][channel_idx][payload_len][utf8_text]
|
||||
5. bleak: write_gatt_char(NUS_RX_UUID, packet)
|
||||
6. dbus_fast: GattCharacteristic1.WriteValue(packet_bytes, {})
|
||||
7. BlueZ: schrijft naar HCI controller
|
||||
8. HCI: stuurt BLE PDU via radio
|
||||
9. T1000-E: ontvangt, verwerkt, stuurt via LoRa mesh
|
||||
```
|
||||
|
||||
De response (of een inkomend mesh bericht) loopt de omgekeerde route:
|
||||
|
||||
```
|
||||
1. T1000-E: ontvangt mesh bericht via LoRa
|
||||
2. T1000-E → HCI: BLE notification met data
|
||||
3. BlueZ: ontvangt notification, stuurt via D-Bus
|
||||
4. dbus_fast: roept de notify callback in bleak aan
|
||||
5. bleak: roept de registered callback in meshcore aan
|
||||
6. meshcore: deserialiseert binary → Event(type, payload)
|
||||
7. BLEWorker: EventHandler verwerkt het event
|
||||
→ dedup check → naam resolutie → path hash extractie
|
||||
8. SharedData: add_message(Message.incoming(...))
|
||||
9. GUI: ziet message_updated flag bij volgende 500ms poll
|
||||
```
|
||||
|
||||
### 5.3 Waarom subscribe-before-send?
|
||||
|
||||
BLE notifications zijn asynchroon. Als meshcore eerst het commando schrijft en *daarna* `start_notify()` aanroept, kan de response al verloren zijn gegaan voordat de listener klaar is. Dit was een bug in de originele meshcore_py die leidde tot ~2 minuten startup delay:
|
||||
|
||||
```
|
||||
❌ Oud (race condition):
|
||||
write(RX, command) → device antwoordt direct
|
||||
start_notify(TX) → te laat, response is al weg
|
||||
|
||||
✅ Nieuw (PR #52):
|
||||
start_notify(TX) → listener actief
|
||||
write(RX, command) → device antwoordt
|
||||
callback fired → response ontvangen
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Pairing en Bonding
|
||||
|
||||
### 6.1 Waarom PIN pairing?
|
||||
|
||||
Het T1000-E device is geconfigureerd met BLE PIN `123456` (instelbaar via firmware). Dit voorkomt dat willekeurige BLE clients verbinden. BlueZ ondersteunt PIN pairing via het **Agent** mechanisme.
|
||||
|
||||
### 6.2 Agent interface
|
||||
|
||||
BlueZ definieert de `org.bluez.Agent1` D-Bus interface. Onze `BluezAgent` class implementeert deze callbacks:
|
||||
|
||||
| Methode | D-Bus Signature | Wanneer aangeroepen | Ons antwoord |
|
||||
|---------|----------------|--------------------:|-------------|
|
||||
| `RequestPinCode` | `o → s` | Device vraagt PIN | `"123456"` |
|
||||
| `RequestPasskey` | `o → u` | Device vraagt numeriek passkey | `123456` (uint32) |
|
||||
| `DisplayPasskey` | `oqu → ` | Passkey tonen (info only) | (log only) |
|
||||
| `RequestConfirmation` | `ou → ` | Bevestig passkey match | (accept) |
|
||||
| `AuthorizeService` | `os → ` | Service autorisatie | (accept) |
|
||||
| `Cancel` | ` → ` | Pairing geannuleerd | (log only) |
|
||||
| `Release` | ` → ` | Agent niet meer nodig | (cleanup) |
|
||||
|
||||
### 6.3 Het bonding probleem
|
||||
|
||||
Na succesvolle pairing slaat BlueZ de encryption keys op in `/var/lib/bluetooth/<adapter>/<device>/info`. Dit heet een "bond". Bij de volgende connectie probeert BlueZ deze keys te hergebruiken.
|
||||
|
||||
**Het probleem:** Het T1000-E verwerpt na ~2 uur de BLE verbinding (firmware timeout). BlueZ heeft nog de oude bond keys, maar het device heeft ze verworpen. Resultaat:
|
||||
|
||||
```
|
||||
BlueZ: "Ik heb keys voor dit device, gebruik die"
|
||||
T1000-E: "Ik ken deze keys niet → Reject (PIN or Key Missing)"
|
||||
BlueZ: "Pairing failed"
|
||||
```
|
||||
|
||||
**De oplossing:** Vóór elke reconnectie verwijderen we de bond:
|
||||
|
||||
```
|
||||
remove_bond() → Adapter1.RemoveDevice() → BlueZ wist keys
|
||||
connect() → BlueZ: "Geen keys, start verse pairing"
|
||||
Agent → levert PIN → verse pairing succesvol
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. D-Bus Policy
|
||||
|
||||
Normale gebruikers mogen standaard niet alle BlueZ D-Bus interfaces aanspreken. De D-Bus policy file (`/etc/dbus-1/system.d/meshcore-ble.conf`) geeft de gebruiker die de service draait toestemming:
|
||||
|
||||
```xml
|
||||
<busconfig>
|
||||
<policy user="hans">
|
||||
<allow send_destination="org.bluez"/>
|
||||
<allow send_interface="org.bluez.Agent1"/>
|
||||
<allow send_interface="org.bluez.AgentManager1"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
```
|
||||
|
||||
Zonder deze policy:
|
||||
- `bleak` kan nog steeds verbinden (bleak gebruikt een standaard D-Bus policy die al met BlueZ meekomt)
|
||||
- Onze **agent** kan zich niet registreren → PIN pairing faalt
|
||||
- Onze **bond cleanup** kan `RemoveDevice` niet aanroepen
|
||||
|
||||
---
|
||||
|
||||
## 8. Samenvatting Dependencies
|
||||
|
||||
```
|
||||
meshcore-gui
|
||||
├── nicegui → Web UI framework (onze GUI)
|
||||
├── meshcore → MeshCore protocol (commando's, events)
|
||||
│ └── bleak → BLE abstractie (connect, notify, write)
|
||||
│ └── dbus_fast → D-Bus communicatie (naar BlueZ)
|
||||
├── meshcoredecoder → LoRa packet decryptie + route extractie
|
||||
└── (geen extra) → ble_agent.py en ble_reconnect.py
|
||||
gebruiken dbus_fast die al via bleak
|
||||
geïnstalleerd is
|
||||
```
|
||||
|
||||
Alle BLE-gerelateerde functionaliteit draait op precies **vier Python packages**: `bleak`, `dbus_fast`, `meshcore`, en `meshcoredecoder`. Er zijn geen system-level dependencies meer nodig buiten `bluez` zelf (geen `bluez-tools`, geen `bt-agent`).
|
||||
@@ -0,0 +1,639 @@
|
||||
# BLE Capture Workflow T1000-e — Explanation & Background
|
||||
|
||||
> **Note:** This document is BLE-specific and kept for historical reference. The current GUI uses USB serial.
|
||||
|
||||
> **Source:** `ble_capture_workflow_t_1000_e.md`
|
||||
>
|
||||
> This document is a **companion guide** to the original technical working document. It provides:
|
||||
> - Didactic explanation of BLE concepts and terminology
|
||||
> - Background knowledge about GATT services and how they work
|
||||
> - Context for better understanding future BLE projects
|
||||
>
|
||||
> **Intended audience:** Myself, as a long-term reference.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is this document about?
|
||||
|
||||
This document explains the BLE concepts and terminology behind communicating with a **MeshCore T1000-e** radio from a Linux computer. It covers:
|
||||
|
||||
- How BLE connections work and how they differ from Classic Bluetooth
|
||||
- The GATT service model and the Nordic UART Service (NUS) used by MeshCore
|
||||
- Why BLE session ownership matters and how it can cause connection failures
|
||||
|
||||
**The key message in one sentence:**
|
||||
|
||||
> Only **one BLE client at a time** can be connected to the T1000-e. If something else is already connected, your connection will fail.
|
||||
|
||||
---
|
||||
|
||||
## 2. Terms and abbreviations explained
|
||||
|
||||
### 2.1 BLE — Bluetooth Low Energy
|
||||
|
||||
BLE is an **energy-efficient variant of Bluetooth**, designed for devices that need to run on a battery for months or years.
|
||||
|
||||
| Property | Classic Bluetooth | BLE |
|
||||
|----------|-------------------|-----|
|
||||
| Power consumption | High | Very low |
|
||||
| Data rate | High | Low |
|
||||
| Typical use | Audio, file transfer | Sensors, IoT, MeshCore |
|
||||
|
||||
**Analogy:** Classic Bluetooth is like a phone call (constantly connected, high energy). BLE is like sending text messages (brief contact when needed, low energy).
|
||||
|
||||
---
|
||||
|
||||
### 2.2 GATT — Generic Attribute Profile
|
||||
|
||||
GATT is the **structure** through which BLE devices expose their data. Think of it as a **digital bulletin board** with a fixed layout:
|
||||
|
||||
```
|
||||
Service (category)
|
||||
└── Characteristic (specific data point)
|
||||
└── Descriptor (additional configuration)
|
||||
```
|
||||
|
||||
**Example for MeshCore:**
|
||||
|
||||
```
|
||||
Nordic UART Service (NUS)
|
||||
├── RX Characteristic → messages from radio to computer
|
||||
└── TX Characteristic → messages from computer to radio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 NUS — Nordic UART Service
|
||||
|
||||
NUS is a **standard BLE service** developed by Nordic Semiconductor. It simulates an old-fashioned serial port (UART) over Bluetooth.
|
||||
|
||||
- **RX** (Receive): Data you **receive** from the device
|
||||
- **TX** (Transmit): Data you **send** to the device
|
||||
|
||||
Note: RX/TX are from the computer's perspective, not the radio's.
|
||||
|
||||
#### Is NUS a protocol?
|
||||
|
||||
**No.** NUS is a **service specification**, not a protocol. This is an important distinction:
|
||||
|
||||
| Level | What is it | Example |
|
||||
|-------|-----------|---------|
|
||||
| **Protocol** | Rules for communication | BLE, ATT, GATT |
|
||||
| **Service** | Collection of related characteristics | NUS, Heart Rate Service |
|
||||
| **Characteristic** | Specific data point within a service | RX, TX |
|
||||
|
||||
**Restaurant analogy:**
|
||||
|
||||
| Concept | Restaurant analogy |
|
||||
|---------|--------------------|
|
||||
| **Protocol (GATT)** | The rules: you order from the waiter, food comes from the kitchen |
|
||||
| **Service (NUS)** | A specific menu (e.g. "breakfast menu") |
|
||||
| **Characteristics** | The individual dishes on that menu |
|
||||
|
||||
People often say "we're using the NUS protocol", but strictly speaking **GATT** is the protocol and **NUS** is a service offered via GATT.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Other GATT services (official and custom)
|
||||
|
||||
NUS is just one of many BLE services. The **Bluetooth SIG** (the organisation behind Bluetooth) defines dozens of official services. In addition, manufacturers can create their own (custom) services.
|
||||
|
||||
#### Official services (Bluetooth SIG)
|
||||
|
||||
These services have a **16-bit UUID** and are standardised for interoperability:
|
||||
|
||||
| Service | UUID | Application |
|
||||
|---------|------|-------------|
|
||||
| **Heart Rate Service** | 0x180D | Heart rate monitors, fitness devices |
|
||||
| **Battery Service** | 0x180F | Reporting battery level |
|
||||
| **Device Information** | 0x180A | Manufacturer, model number, firmware version |
|
||||
| **Blood Pressure** | 0x1810 | Blood pressure monitors |
|
||||
| **Health Thermometer** | 0x1809 | Medical thermometers |
|
||||
| **Cycling Speed and Cadence** | 0x1816 | Bicycle sensors |
|
||||
| **Environmental Sensing** | 0x181A | Temperature, humidity, pressure |
|
||||
| **Glucose** | 0x1808 | Blood glucose meters |
|
||||
| **HID over GATT** | 0x1812 | Keyboards, mice, gamepads |
|
||||
| **Proximity** | 0x1802 | "Find My" functionality |
|
||||
| **Generic Access** | 0x1800 | **Mandatory** — device name and appearance |
|
||||
|
||||
#### Custom/vendor-specific services
|
||||
|
||||
Manufacturers can define their own services with a **128-bit UUID**. Examples:
|
||||
|
||||
| Service | Manufacturer | Application |
|
||||
|---------|--------------|-------------|
|
||||
| **Nordic UART Service (NUS)** | Nordic Semiconductor | Serial port over BLE |
|
||||
| **Apple Notification Center** | Apple | iPhone notifications to wearables |
|
||||
| **Xiaomi Mi Band Service** | Xiaomi | Fitness tracker communication |
|
||||
| **MeshCore Companion** | MeshCore | Radio communication (uses NUS) |
|
||||
|
||||
#### The difference: 16-bit vs. 128-bit UUID
|
||||
|
||||
| Type | Length | Example | Who can create it? |
|
||||
|------|--------|---------|--------------------|
|
||||
| **Official (SIG)** | 16-bit | `0x180D` | Bluetooth SIG only |
|
||||
| **Custom** | 128-bit | `6e400001-b5a3-f393-e0a9-e50e24dcca9e` | Anyone |
|
||||
|
||||
The NUS service uses this 128-bit UUID:
|
||||
```
|
||||
6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
```
|
||||
|
||||
#### Why this matters
|
||||
|
||||
In the MeshCore project we use **NUS** (a custom service) for communication. But when working with other BLE devices — such as a heart rate monitor or a smart thermostat — they typically use **official SIG services**.
|
||||
|
||||
The principle remains the same:
|
||||
1. Discover which services the device offers
|
||||
2. Find the right characteristic
|
||||
3. Read, write, or subscribe to notify
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Notify vs. Read
|
||||
|
||||
There are two ways to get data from a BLE device:
|
||||
|
||||
| Method | How it works | When to use |
|
||||
|--------|-------------|-------------|
|
||||
| **Read** | You actively request data | One-off values (e.g. battery status) |
|
||||
| **Notify** | Device sends automatically when new data is available | Continuous data stream (e.g. messages) |
|
||||
|
||||
**Analogy:**
|
||||
- **Read** = You call someone and ask "how are you?"
|
||||
- **Notify** = You automatically receive a WhatsApp message when there's news
|
||||
|
||||
For MeshCore captures you use **Notify** — after all, you want to know when a message arrives.
|
||||
|
||||
---
|
||||
|
||||
### 2.6 CCCD — Client Characteristic Configuration Descriptor
|
||||
|
||||
The CCCD is the **on/off switch for Notify**. Technically:
|
||||
|
||||
1. Your computer writes a `1` to the CCCD
|
||||
2. The device now knows: "this client wants notifications"
|
||||
3. When new data arrives, the device automatically sends a message
|
||||
|
||||
**The crucial point:** Only **one client at a time** can activate the CCCD. A second client will receive the error:
|
||||
|
||||
```
|
||||
Notify acquired
|
||||
```
|
||||
|
||||
This means: "someone else has already enabled notify."
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Pairing, Bonding and Trust
|
||||
|
||||
These are three separate steps in the BLE security process:
|
||||
|
||||
| Step | What happens | Analogy |
|
||||
|------|-------------|---------|
|
||||
| **Pairing** | Devices exchange cryptographic keys | You meet someone and exchange phone numbers |
|
||||
| **Bonding** | The keys are stored permanently | You save the number in your contacts |
|
||||
| **Trust** | The system trusts the device automatically | You add someone to your favourites |
|
||||
|
||||
After these three steps, you no longer need to enter the PIN code each time.
|
||||
|
||||
**Verification on Linux:**
|
||||
|
||||
```bash
|
||||
bluetoothctl info AA:BB:CC:DD:EE:FF | egrep -i "Paired|Bonded|Trusted"
|
||||
```
|
||||
|
||||
Expected output:
|
||||
|
||||
```
|
||||
Paired: yes
|
||||
Bonded: yes
|
||||
Trusted: yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.8 Ownership — The core problem
|
||||
|
||||
**Ownership** is an informal term indicating: "which client currently holds the active GATT session with notify?"
|
||||
|
||||
**Analogy:** Think of a walkie-talkie where only one person can listen at a time:
|
||||
|
||||
- If GNOME Bluetooth Manager is already connected → it is the "owner"
|
||||
- If your Python script then tries to connect → it won't get access
|
||||
|
||||
**Typical "owners" that cause problems:**
|
||||
|
||||
- GNOME Bluetooth GUI (often runs in the background)
|
||||
- `bluetoothctl connect` (makes bluetoothctl the owner)
|
||||
- Phone with Bluetooth enabled
|
||||
- Other BLE apps
|
||||
|
||||
---
|
||||
|
||||
### 2.9 BlueZ
|
||||
|
||||
**BlueZ** is the official Bluetooth stack for Linux. It is the software that handles all Bluetooth communication between your applications and the hardware.
|
||||
|
||||
---
|
||||
|
||||
### 2.10 Bleak
|
||||
|
||||
**Bleak** is a Python library for BLE communication. It builds on top of BlueZ (Linux), Core Bluetooth (macOS) or WinRT (Windows).
|
||||
|
||||
---
|
||||
|
||||
## 3. BLE versus Classic Bluetooth
|
||||
|
||||
A common question: are BLE and "regular" Bluetooth the same thing? The answer is **no** — they are different technologies that happen to share the same name and frequency band.
|
||||
|
||||
### 3.1 Two flavours of Bluetooth
|
||||
|
||||
Since Bluetooth 4.0 (2010) there are **two separate radio systems** within the Bluetooth standard:
|
||||
|
||||
| Name | Technical term | Characteristics |
|
||||
|------|---------------|-----------------|
|
||||
| **Classic Bluetooth** | BR/EDR (Basic Rate / Enhanced Data Rate) | High data rate, continuous connection, more power |
|
||||
| **Bluetooth Low Energy** | BLE (also: Bluetooth Smart) | Low data rate, short bursts, very efficient |
|
||||
|
||||
**Crucially:** These are **different radio protocols** that cannot communicate directly with each other.
|
||||
|
||||
### 3.2 Protocol and hardware
|
||||
|
||||
Bluetooth (both Classic and BLE) encompasses **multiple layers** — it is not just a protocol, but also hardware:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SOFTWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Application (your code) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Profiles / GATT Services │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Protocols (ATT, L2CAP, etc.) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Host Controller Interface (HCI) │ │ ← Software/firmware boundary
|
||||
│ └───────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ FIRMWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Link Layer / Controller │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ HARDWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Radio (2.4 GHz transceiver) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Antenna │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 Where is the difference?
|
||||
|
||||
The difference exists across **multiple layers**, not just the protocol:
|
||||
|
||||
| Layer | Classic (BR/EDR) | BLE | Hardware difference? |
|
||||
|-------|------------------|-----|---------------------|
|
||||
| **Radio** | GFSK, π/4-DQPSK, 8DPSK | GFSK | **Yes** — different modulation |
|
||||
| **Channels** | 79 channels, 1 MHz wide | 40 channels, 2 MHz wide | **Yes** — different layout |
|
||||
| **Link Layer** | LMP (Link Manager Protocol) | LL (Link Layer) | **Yes** — different state machine |
|
||||
| **Protocols** | L2CAP, RFCOMM, SDP | L2CAP, ATT, GATT | No — software |
|
||||
|
||||
### 3.4 Dual-mode devices
|
||||
|
||||
The overlap lies in devices that support **both**:
|
||||
|
||||
| Device type | Supports | Example |
|
||||
|-------------|----------|---------|
|
||||
| **Classic-only** | BR/EDR only | Old headsets, car audio |
|
||||
| **BLE-only** (Bluetooth Smart) | BLE only | Fitness trackers, sensors, T1000-e |
|
||||
| **Dual-Mode** (Bluetooth Smart Ready) | Both | Smartphones, laptops, ESP32 |
|
||||
|
||||
**Your smartphone** is dual-mode: it can talk to your classic Bluetooth headphones (BR/EDR) and to your MeshCore T1000-e (BLE).
|
||||
|
||||
### 3.5 Practical examples
|
||||
|
||||
| Scenario | What is used |
|
||||
|----------|-------------|
|
||||
| Music to your headphones | **Classic** (A2DP profile) |
|
||||
| Heart rate from your smartwatch | **BLE** (Heart Rate Service) |
|
||||
| Sending a file to a laptop | **Classic** (OBEX/FTP profile) |
|
||||
| Reading the MeshCore T1000-e | **BLE** (NUS service) |
|
||||
| Hands-free calling in the car | **Classic** (HFP profile) |
|
||||
| Controlling a smart light | **BLE** (custom GATT service) |
|
||||
|
||||
---
|
||||
|
||||
## 4. BLE channel layout and frequency hopping
|
||||
|
||||
### 4.1 The 40 BLE channels
|
||||
|
||||
The 2.4 GHz ISM band runs from **2400 MHz to 2483.5 MHz** (83.5 MHz wide).
|
||||
|
||||
BLE divides this into **40 channels of 2 MHz each**:
|
||||
|
||||
```
|
||||
2400 MHz 2480 MHz
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│00│01│02│03│04│05│06│07│08│09│10│11│12│13│14│15│16│17│18│19│...→ 39
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
└──────────────────────────────────────────────────────┘
|
||||
2 MHz per channel
|
||||
```
|
||||
|
||||
**Total:** 40 × 2 MHz = **80 MHz** used
|
||||
|
||||
### 4.2 Advertising vs. data channels
|
||||
|
||||
The 40 channels are not all equal:
|
||||
|
||||
| Type | Channels | Function |
|
||||
|------|----------|----------|
|
||||
| **Advertising** | 3 (nos. 37, 38, 39) | Device discovery, initiating connections |
|
||||
| **Data** | 37 (nos. 0-36) | Actual communication after connection |
|
||||
|
||||
The advertising channels are strategically chosen to **avoid Wi-Fi interference**:
|
||||
|
||||
```
|
||||
Wi-Fi channel 1 Wi-Fi channel 6 Wi-Fi channel 11
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
────████████─────────────█████████────────────████████────
|
||||
|
||||
BLE: ▲ ▲ ▲
|
||||
Ch.37 Ch.38 Ch.39
|
||||
|
||||
(advertising channels sit between the Wi-Fi channels)
|
||||
```
|
||||
|
||||
### 4.3 Comparison with Classic Bluetooth
|
||||
|
||||
| Aspect | Classic (BR/EDR) | BLE |
|
||||
|--------|------------------|-----|
|
||||
| **Number of channels** | 79 | 40 |
|
||||
| **Channel width** | 1 MHz | 2 MHz |
|
||||
| **Total bandwidth** | 79 MHz | 80 MHz |
|
||||
| **Frequency hopping** | Yes, all 79 | Yes, 37 data channels |
|
||||
|
||||
Classic has **more but narrower** channels, BLE has **fewer but wider** channels.
|
||||
|
||||
### 4.4 Frequency hopping: one channel at a time
|
||||
|
||||
**Key insight:** You only ever use **one channel at a time**. The 40 channels exist for **frequency hopping** — alternately switching channels to avoid interference:
|
||||
|
||||
```
|
||||
Time →
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 12 │ ▓ │ │ │ │ │ │ ▓ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 07 │ │ │ ▓ │ │ │ │ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 31 │ │ │ │ │ ▓ │ │ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
↑ ↑ ↑ ↑
|
||||
Packet 1 Packet 2 Packet 3 Packet 4
|
||||
```
|
||||
|
||||
This is **not parallel communication** — it is serial with alternating frequencies.
|
||||
|
||||
---
|
||||
|
||||
## 5. Two meanings of "serial"
|
||||
|
||||
When we say "NUS is serial", this can cause confusion. The word "serial" has **two different meanings** in this context.
|
||||
|
||||
### 5.1 Radio level: always serial
|
||||
|
||||
**All** wireless communication is serial at the physical level — you only have **one radio channel at a time** and bits go into the air **one after another**:
|
||||
|
||||
```
|
||||
Radio wave: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
|
||||
|
||||
Bits: 0 1 1 0 1 0 0 1 1 1 0 1 0 0 1 0 → one by one
|
||||
```
|
||||
|
||||
The 40 channels are for **frequency hopping**, not for parallel transmission. This applies to **all** BLE services — NUS, Heart Rate, Battery, all of them.
|
||||
|
||||
### 5.2 Data level: NUS simulates a serial port
|
||||
|
||||
When we say "NUS is a serial service", we mean something different:
|
||||
|
||||
**NUS simulates an old serial port (RS-232/UART):**
|
||||
|
||||
```
|
||||
Historical (1980s-2000s):
|
||||
|
||||
Computer Device
|
||||
┌──────┐ Serial cable ┌──────┐
|
||||
│ COM1 │←────────────────→│ UART │
|
||||
└──────┘ (RS-232) └──────┘
|
||||
|
||||
Bytes: 0x48 0x65 0x6C 0x6C 0x6F ("Hello")
|
||||
└─────────────────────┘
|
||||
No structure, just a stream of bytes
|
||||
```
|
||||
|
||||
**NUS mimics this over BLE:**
|
||||
|
||||
```
|
||||
Today:
|
||||
|
||||
Computer Device
|
||||
┌──────┐ BLE (NUS) ┌──────┐
|
||||
│ App │←~~~~~~~~~~~~~~~~~~~~→│ MCU │
|
||||
└──────┘ (wireless) └──────┘
|
||||
|
||||
Behaves as if there were a serial cable
|
||||
```
|
||||
|
||||
### 5.3 Comparison: serial vs. structured
|
||||
|
||||
| Aspect | NUS (serial) | Heart Rate (structured) |
|
||||
|--------|-------------|------------------------|
|
||||
| **Radio** | Serial, frequency hopping | Serial, frequency hopping |
|
||||
| **Data** | Unstructured byte stream | Fixed fields with meaning |
|
||||
| **Who determines the format?** | You (custom protocol) | Bluetooth SIG (specification) |
|
||||
|
||||
### 5.4 Analogy: motorway with lanes
|
||||
|
||||
Think of a **motorway with 40 lanes** (the channels):
|
||||
|
||||
- You may only use **one lane at a time**
|
||||
- You regularly switch lanes (frequency hopping, to avoid collisions)
|
||||
- The **cargo** you transport can differ:
|
||||
|
||||
| Service | Cargo analogy |
|
||||
|---------|--------------|
|
||||
| **NUS** | Loose items mixed together (flexible, but you need to figure out what's what) |
|
||||
| **Heart Rate** | Standardised pallets (everyone knows what goes where) |
|
||||
|
||||
The **motorway works the same** — the difference lies in how you organise the cargo.
|
||||
|
||||
---
|
||||
|
||||
## 6. Serial vs. structured services (deep dive)
|
||||
|
||||
An important distinction that is often overlooked: **not all BLE services work the same way**. There are fundamentally two approaches.
|
||||
|
||||
### 6.1 Serial services (stream-based)
|
||||
|
||||
**NUS (Nordic UART Service)** is designed to **simulate a serial port**:
|
||||
|
||||
- Continuous stream of raw bytes
|
||||
- No imposed structure
|
||||
- You determine the format and meaning yourself
|
||||
|
||||
**Analogy:** A serial service is like a **phone line** — you can say whatever you want, in any language, without fixed rules.
|
||||
|
||||
```
|
||||
Example NUS data (MeshCore):
|
||||
0x01 0x0A 0x48 0x65 0x6C 0x6C 0x6F ...
|
||||
└── Meaning determined by MeshCore protocol, not by BLE
|
||||
```
|
||||
|
||||
### 6.2 Structured services (field-based)
|
||||
|
||||
Most official SIG services work **differently** — they define **exactly** which bytes mean what:
|
||||
|
||||
**Analogy:** A structured service is like a **tax form** — each field has a fixed meaning and a prescribed format.
|
||||
|
||||
#### Example: Heart Rate Measurement
|
||||
|
||||
```
|
||||
Byte 0: Flags (bitfield)
|
||||
├── Bit 0: 0 = heart rate in 1 byte, 1 = heart rate in 2 bytes
|
||||
├── Bit 1-2: Sensor contact status
|
||||
├── Bit 3: Energy expended present?
|
||||
└── Bit 4: RR-interval present?
|
||||
|
||||
Byte 1(-2): Heart rate value
|
||||
Byte N...: Optional additional fields (depending on flags)
|
||||
```
|
||||
|
||||
**Concrete example:**
|
||||
|
||||
```
|
||||
Received bytes: 0x00 0x73
|
||||
|
||||
0x00 = Flags: 8-bit format, no additional fields
|
||||
0x73 = 115 decimal → heart rate is 115 bpm
|
||||
```
|
||||
|
||||
So you don't receive the text "115", but a binary packet that you need to **parse** according to the specification.
|
||||
|
||||
#### Example: Battery Level
|
||||
|
||||
Simpler — just **1 byte**:
|
||||
|
||||
```
|
||||
Received byte: 0x5A
|
||||
|
||||
0x5A = 90 decimal → battery is 90%
|
||||
```
|
||||
|
||||
#### Example: Environmental Sensing (temperature)
|
||||
|
||||
```
|
||||
Received bytes: 0x9C 0x08
|
||||
|
||||
Little-endian 16-bit signed integer: 0x089C = 2204
|
||||
Resolution: 0.01°C
|
||||
Temperature: 2204 × 0.01 = 22.04°C
|
||||
```
|
||||
|
||||
### 6.3 Comparison table
|
||||
|
||||
| Aspect | Serial (NUS) | Structured (SIG) |
|
||||
|--------|-------------|------------------|
|
||||
| **Data format** | Free, self-determined | Fixed, by specification |
|
||||
| **Who defines the format?** | You / the manufacturer | Bluetooth SIG |
|
||||
| **Where to find the spec?** | Own documentation / source code | bluetooth.com/specifications |
|
||||
| **Parsing** | Build your own parser | Standard parser possible |
|
||||
| **Interoperability** | Own software only | Any conformant app/device |
|
||||
| **Flexibility** | Maximum | Limited to spec |
|
||||
| **Complexity** | Easy to get started | Reading the spec required |
|
||||
|
||||
### 6.4 Examples of structured services
|
||||
|
||||
| Service | Characteristic | Data format |
|
||||
|---------|----------------|-------------|
|
||||
| **Battery Service** | Battery Level | 1 byte: 0-100 (percentage) |
|
||||
| **Heart Rate** | Heart Rate Measurement | Flags + 8/16-bit HR + optional fields |
|
||||
| **Health Thermometer** | Temperature Measurement | IEEE-11073 FLOAT (4 bytes) |
|
||||
| **Blood Pressure** | Blood Pressure Measurement | Compound: systolic, diastolic, MAP, pulse |
|
||||
| **Cycling Speed & Cadence** | CSC Measurement | 32-bit counters + 16-bit time |
|
||||
| **Environmental Sensing** | Temperature | 16-bit signed, resolution 0.01°C |
|
||||
| **Environmental Sensing** | Humidity | 16-bit unsigned, resolution 0.01% |
|
||||
| **Environmental Sensing** | Pressure | 32-bit unsigned, resolution 0.1 Pa |
|
||||
|
||||
### 6.5 When to use which approach?
|
||||
|
||||
| Situation | Recommended approach |
|
||||
|-----------|---------------------|
|
||||
| Custom protocol (MeshCore, custom IoT) | **Serial** (NUS or custom service) |
|
||||
| Standard use case (heart rate, battery) | **Structured** (SIG service) |
|
||||
| Interoperability with existing apps required | **Structured** (SIG service) |
|
||||
| Complex, variable data structures | **Serial** with custom protocol |
|
||||
| Quick prototype without studying specs | **Serial** (NUS) |
|
||||
|
||||
### 6.6 Why MeshCore uses NUS
|
||||
|
||||
MeshCore chose NUS (serial) because:
|
||||
|
||||
1. **Flexibility** — The Companion Protocol requires its own framing
|
||||
2. **No suitable SIG service** — There is no "Mesh Radio Service" standard
|
||||
3. **Bidirectional communication** — NUS offers both RX and TX characteristics
|
||||
4. **Simplicity** — No need to implement a complex SIG specification
|
||||
|
||||
The downside: you can't just use any arbitrary BLE app to talk to MeshCore — you need software that understands the MeshCore Companion Protocol.
|
||||
|
||||
---
|
||||
|
||||
## 7. The OSI model in context
|
||||
|
||||
The document places the problem in a **layer model**. This helps understand *where* the problem lies:
|
||||
|
||||
| Layer | Name | In this project | Problem here? |
|
||||
|-------|------|-----------------|---------------|
|
||||
| 7 | Application | MeshCore Companion Protocol | No |
|
||||
| 6 | Presentation | Frame encoding (hex) | No |
|
||||
| **5** | **Session** | **GATT client ↔ server session** | **★ YES** |
|
||||
| 4 | Transport | ATT / GATT | No |
|
||||
| 2 | Data Link | BLE Link Layer | No |
|
||||
| 1 | Physical | 2.4 GHz radio | No |
|
||||
|
||||
**Conclusion:** The ownership problem sits at **layer 5 (session)**. The firmware and protocol are not the problem — it's about who "owns" the session.
|
||||
|
||||
---
|
||||
|
||||
## 8. Conclusion
|
||||
|
||||
The key takeaways from this document:
|
||||
|
||||
- ✅ MeshCore BLE companion **works correctly** on Linux
|
||||
- ✅ The firmware **does not block notify**
|
||||
- ✅ The only requirement is: **exactly one active BLE client per radio**
|
||||
|
||||
Understanding the ownership model and BLE fundamentals described here is essential for working with any BLE-connected MeshCore device.
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
- MeshCore Companion Radio Protocol: [GitHub Wiki](https://github.com/meshcore-dev/MeshCore/wiki/Companion-Radio-Protocol)
|
||||
- Bluetooth SIG Assigned Numbers (official services): [bluetooth.com/specifications/assigned-numbers](https://www.bluetooth.com/specifications/assigned-numbers/)
|
||||
- Bluetooth SIG GATT Specifications: [bluetooth.com/specifications/specs](https://www.bluetooth.com/specifications/specs/)
|
||||
- Nordic Bluetooth Numbers Database: [GitHub](https://github.com/NordicSemiconductor/bluetooth-numbers-database)
|
||||
- GATT Explanation (Adafruit): [learn.adafruit.com](https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gatt)
|
||||
- Bleak documentation: [bleak.readthedocs.io](https://bleak.readthedocs.io/)
|
||||
- BlueZ: [bluez.org](http://www.bluez.org/)
|
||||
|
||||
---
|
||||
|
||||
> **Document:** `ble_capture_workflow_t_1000_e_explanation.md`
|
||||
> **Based on:** `ble_capture_workflow_t_1000_e.md`
|
||||
@@ -0,0 +1,639 @@
|
||||
# BLE Capture Workflow T1000-e — Uitleg & Achtergrond
|
||||
|
||||
> **Note:** Dit document is BLE-specifiek en wordt bewaard als referentie. De huidige GUI gebruikt USB-serieel.
|
||||
|
||||
> **Bron:** `ble_capture_workflow_t_1000_e.md`
|
||||
>
|
||||
> Dit document is een **verdiepingsdocument** bij het originele technische werkdocument. Het biedt:
|
||||
> - Didactische uitleg van BLE-concepten en terminologie
|
||||
> - Achtergrondkennis over GATT-services en hun werking
|
||||
> - Context om toekomstige BLE-projecten beter te begrijpen
|
||||
>
|
||||
> **Doelgroep:** Mezelf, als referentie voor de lange termijn.
|
||||
|
||||
---
|
||||
|
||||
## 1. Waar gaat dit document over?
|
||||
|
||||
Dit document legt de BLE-concepten en terminologie uit achter de communicatie met een **MeshCore T1000-e** radio vanaf een Linux-computer. Het behandelt:
|
||||
|
||||
- Hoe BLE-verbindingen werken en hoe ze verschillen van Classic Bluetooth
|
||||
- Het GATT-servicemodel en de Nordic UART Service (NUS) die MeshCore gebruikt
|
||||
- Waarom BLE-sessie-ownership belangrijk is en hoe het verbindingsproblemen kan veroorzaken
|
||||
|
||||
**De kernboodschap in één zin:**
|
||||
|
||||
> Er mag maar **één BLE-client tegelijk** verbonden zijn met de T1000-e. Als iets anders al verbonden is, faalt jouw verbinding.
|
||||
|
||||
---
|
||||
|
||||
## 2. Begrippen en afkortingen uitgelegd
|
||||
|
||||
### 2.1 BLE — Bluetooth Low Energy
|
||||
|
||||
BLE is een **zuinige variant van Bluetooth**, ontworpen voor apparaten die maanden of jaren op een batterij moeten werken.
|
||||
|
||||
| Eigenschap | Klassiek Bluetooth | BLE |
|
||||
|------------|-------------------|-----|
|
||||
| Stroomverbruik | Hoog | Zeer laag |
|
||||
| Datasnelheid | Hoog | Laag |
|
||||
| Typisch gebruik | Audio, bestanden | Sensoren, IoT, MeshCore |
|
||||
|
||||
**Analogie:** Klassiek Bluetooth is als een telefoongesprek (constant verbonden, veel energie). BLE is als SMS'jes sturen (kort contact wanneer nodig, weinig energie).
|
||||
|
||||
---
|
||||
|
||||
### 2.2 GATT — Generic Attribute Profile
|
||||
|
||||
GATT is de **structuur** waarmee BLE-apparaten hun data aanbieden. Zie het als een **digitaal prikbord** met een vaste indeling:
|
||||
|
||||
```
|
||||
Service (categorie)
|
||||
└── Characteristic (specifiek datapunt)
|
||||
└── Descriptor (extra configuratie)
|
||||
```
|
||||
|
||||
**Voorbeeld voor MeshCore:**
|
||||
|
||||
```
|
||||
Nordic UART Service (NUS)
|
||||
├── RX Characteristic → berichten van radio naar computer
|
||||
└── TX Characteristic → berichten van computer naar radio
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 NUS — Nordic UART Service
|
||||
|
||||
NUS is een **standaard BLE-service** ontwikkeld door Nordic Semiconductor. Het simuleert een ouderwetse seriële poort (UART) over Bluetooth.
|
||||
|
||||
- **RX** (Receive): Data die je **ontvangt** van het apparaat
|
||||
- **TX** (Transmit): Data die je **verstuurt** naar het apparaat
|
||||
|
||||
Let op: RX/TX zijn vanuit het perspectief van de computer, niet van de radio.
|
||||
|
||||
#### Is NUS een protocol?
|
||||
|
||||
**Nee.** NUS is een **servicespecificatie**, geen protocol. Dit is een belangrijk onderscheid:
|
||||
|
||||
| Niveau | Wat is het | Voorbeeld |
|
||||
|--------|-----------|-----------|
|
||||
| **Protocol** | Regels voor communicatie | BLE, ATT, GATT |
|
||||
| **Service** | Verzameling van gerelateerde characteristics | NUS, Heart Rate Service |
|
||||
| **Characteristic** | Specifiek datapunt binnen een service | RX, TX |
|
||||
|
||||
**Analogie met een restaurant:**
|
||||
|
||||
| Concept | Restaurant-analogie |
|
||||
|---------|---------------------|
|
||||
| **Protocol (GATT)** | De regels: je bestelt bij de ober, eten komt uit de keuken |
|
||||
| **Service (NUS)** | Een specifieke menukaart (bijv. "ontbijtmenu") |
|
||||
| **Characteristics** | De individuele gerechten op dat menu |
|
||||
|
||||
Mensen zeggen vaak "we gebruiken het NUS-protocol", maar strikt genomen is **GATT** het protocol en is **NUS** een service die via GATT wordt aangeboden.
|
||||
|
||||
---
|
||||
|
||||
### 2.4 Andere GATT-services (officieel en custom)
|
||||
|
||||
NUS is slechts één van vele BLE-services. De **Bluetooth SIG** (de organisatie achter Bluetooth) definieert tientallen officiële services. Daarnaast kunnen fabrikanten eigen (custom) services maken.
|
||||
|
||||
#### Officiële services (Bluetooth SIG)
|
||||
|
||||
Deze services hebben een **16-bit UUID** en zijn gestandaardiseerd voor interoperabiliteit:
|
||||
|
||||
| Service | UUID | Toepassing |
|
||||
|---------|------|------------|
|
||||
| **Heart Rate Service** | 0x180D | Hartslagmeters, fitnessapparaten |
|
||||
| **Battery Service** | 0x180F | Batterijniveau rapporteren |
|
||||
| **Device Information** | 0x180A | Fabrikant, modelnummer, firmwareversie |
|
||||
| **Blood Pressure** | 0x1810 | Bloeddrukmeters |
|
||||
| **Health Thermometer** | 0x1809 | Medische thermometers |
|
||||
| **Cycling Speed and Cadence** | 0x1816 | Fietssensoren |
|
||||
| **Environmental Sensing** | 0x181A | Temperatuur, luchtvochtigheid, druk |
|
||||
| **Glucose** | 0x1808 | Bloedglucosemeters |
|
||||
| **HID over GATT** | 0x1812 | Toetsenborden, muizen, gamepads |
|
||||
| **Proximity** | 0x1802 | "Find My"-functionaliteit |
|
||||
| **Generic Access** | 0x1800 | **Verplicht** — apparaatnaam en uiterlijk |
|
||||
|
||||
#### Custom/vendor-specific services
|
||||
|
||||
Fabrikanten kunnen eigen services definiëren met een **128-bit UUID**. Voorbeelden:
|
||||
|
||||
| Service | Fabrikant | Toepassing |
|
||||
|---------|-----------|------------|
|
||||
| **Nordic UART Service (NUS)** | Nordic Semiconductor | Seriële poort over BLE |
|
||||
| **Apple Notification Center** | Apple | iPhone notificaties naar wearables |
|
||||
| **Xiaomi Mi Band Service** | Xiaomi | Fitnesstracker communicatie |
|
||||
| **MeshCore Companion** | MeshCore | Radio-communicatie (gebruikt NUS) |
|
||||
|
||||
#### Het verschil: 16-bit vs. 128-bit UUID
|
||||
|
||||
| Type | Lengte | Voorbeeld | Wie mag het maken? |
|
||||
|------|--------|-----------|-------------------|
|
||||
| **Officieel (SIG)** | 16-bit | `0x180D` | Alleen Bluetooth SIG |
|
||||
| **Custom** | 128-bit | `6e400001-b5a3-f393-e0a9-e50e24dcca9e` | Iedereen |
|
||||
|
||||
De NUS-service gebruikt bijvoorbeeld deze 128-bit UUID:
|
||||
```
|
||||
6e400001-b5a3-f393-e0a9-e50e24dcca9e
|
||||
```
|
||||
|
||||
#### Waarom dit relevant is
|
||||
|
||||
In het MeshCore-project gebruiken we **NUS** (een custom service) voor de communicatie. Maar als je met andere BLE-apparaten werkt — zoals een hartslagmeter of een slimme thermostaat — dan gebruiken die vaak **officiële SIG-services**.
|
||||
|
||||
Het principe blijft hetzelfde:
|
||||
1. Ontdek welke services het apparaat aanbiedt
|
||||
2. Zoek de juiste characteristic
|
||||
3. Lees, schrijf, of abonneer op notify
|
||||
|
||||
---
|
||||
|
||||
### 2.5 Notify vs. Read
|
||||
|
||||
Er zijn twee manieren om data van een BLE-apparaat te krijgen:
|
||||
|
||||
| Methode | Werking | Wanneer gebruiken |
|
||||
|---------|---------|-------------------|
|
||||
| **Read** | Jij vraagt actief om data | Eenmalige waarden (bijv. batterijstatus) |
|
||||
| **Notify** | Apparaat stuurt automatisch bij nieuwe data | Continue datastroom (bijv. berichten) |
|
||||
|
||||
**Analogie:**
|
||||
- **Read** = Je belt iemand en vraagt "hoe gaat het?"
|
||||
- **Notify** = Je krijgt automatisch een WhatsApp-bericht als er nieuws is
|
||||
|
||||
Voor MeshCore-captures gebruik je **Notify** — je wilt immers weten wanneer er een bericht binnenkomt.
|
||||
|
||||
---
|
||||
|
||||
### 2.6 CCCD — Client Characteristic Configuration Descriptor
|
||||
|
||||
De CCCD is de **aan/uit-schakelaar voor Notify**. Technisch gezien:
|
||||
|
||||
1. Jouw computer schrijft een `1` naar de CCCD
|
||||
2. Het apparaat weet nu: "deze client wil notificaties"
|
||||
3. Bij nieuwe data stuurt het apparaat automatisch een bericht
|
||||
|
||||
**Het cruciale punt:** Slechts **één client tegelijk** kan de CCCD activeren. Een tweede client krijgt de foutmelding:
|
||||
|
||||
```
|
||||
Notify acquired
|
||||
```
|
||||
|
||||
Dit betekent: "iemand anders heeft notify al ingeschakeld."
|
||||
|
||||
---
|
||||
|
||||
### 2.7 Pairing, Bonding en Trust
|
||||
|
||||
Dit zijn drie afzonderlijke stappen in het BLE-beveiligingsproces:
|
||||
|
||||
| Stap | Wat gebeurt er | Analogie |
|
||||
|------|----------------|----------|
|
||||
| **Pairing** | Apparaten wisselen cryptografische sleutels uit | Je maakt kennis en wisselt telefoonnummers |
|
||||
| **Bonding** | De sleutels worden permanent opgeslagen | Je slaat het nummer op in je contacten |
|
||||
| **Trust** | Het systeem vertrouwt het apparaat automatisch | Je zet iemand in je favorieten |
|
||||
|
||||
Na deze drie stappen hoef je niet elke keer opnieuw de pincode in te voeren.
|
||||
|
||||
**Controle in Linux:**
|
||||
|
||||
```bash
|
||||
bluetoothctl info literal:AA:BB:CC:DD:EE:FF | egrep -i "Paired|Bonded|Trusted"
|
||||
```
|
||||
|
||||
Verwachte output:
|
||||
|
||||
```
|
||||
Paired: yes
|
||||
Bonded: yes
|
||||
Trusted: yes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.8 Ownership — Het kernprobleem
|
||||
|
||||
**Ownership** is een informele term die aangeeft: "welke client heeft op dit moment de actieve GATT-sessie met notify?"
|
||||
|
||||
**Analogie:** Denk aan een walkietalkie waarbij maar één persoon tegelijk kan luisteren:
|
||||
|
||||
- Als GNOME Bluetooth Manager al verbonden is → die is de "eigenaar"
|
||||
- Als jouw Python-script daarna probeert te verbinden → krijgt het geen toegang
|
||||
|
||||
**Typische "eigenaren" die problemen veroorzaken:**
|
||||
|
||||
- GNOME Bluetooth GUI (draait vaak op de achtergrond)
|
||||
- `bluetoothctl connect` (maakt bluetoothctl de eigenaar)
|
||||
- Telefoon met Bluetooth aan
|
||||
- Andere BLE-apps
|
||||
|
||||
---
|
||||
|
||||
### 2.9 BlueZ
|
||||
|
||||
**BlueZ** is de officiële Bluetooth-stack voor Linux. Het is de software die alle Bluetooth-communicatie afhandelt tussen je applicaties en de hardware.
|
||||
|
||||
---
|
||||
|
||||
### 2.10 Bleak
|
||||
|
||||
**Bleak** is een Python-bibliotheek voor BLE-communicatie. Het bouwt voort op BlueZ (Linux), Core Bluetooth (macOS) of WinRT (Windows).
|
||||
|
||||
---
|
||||
|
||||
## 3. BLE versus Classic Bluetooth
|
||||
|
||||
Een veelvoorkomende vraag: zijn BLE en "gewone" Bluetooth hetzelfde? Het antwoord is **nee** — het zijn verschillende technologieën die wel dezelfde naam en frequentieband delen.
|
||||
|
||||
### 3.1 Twee smaken van Bluetooth
|
||||
|
||||
Sinds Bluetooth 4.0 (2010) zijn er **twee afzonderlijke radiosystemen** binnen de Bluetooth-standaard:
|
||||
|
||||
| Naam | Technische term | Kenmerken |
|
||||
|------|-----------------|-----------|
|
||||
| **Classic Bluetooth** | BR/EDR (Basic Rate / Enhanced Data Rate) | Hoge datasnelheid, continue verbinding, meer stroom |
|
||||
| **Bluetooth Low Energy** | BLE (ook: Bluetooth Smart) | Lage datasnelheid, korte bursts, zeer zuinig |
|
||||
|
||||
**Cruciaal:** Dit zijn **verschillende radioprotocollen** die niet rechtstreeks met elkaar kunnen communiceren.
|
||||
|
||||
### 3.2 Protocol én hardware
|
||||
|
||||
Bluetooth (zowel Classic als BLE) omvat **meerdere lagen** — het is niet alleen een protocol, maar ook hardware:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ SOFTWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Applicatie (jouw code) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Profielen / GATT Services │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Protocollen (ATT, L2CAP, etc.) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Host Controller Interface (HCI) │ │ ← Grens software/firmware
|
||||
│ └───────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ FIRMWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Link Layer / Controller │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
├─────────────────────────────────────────┤
|
||||
│ HARDWARE │
|
||||
│ ┌───────────────────────────────────┐ │
|
||||
│ │ Radio (2.4 GHz transceiver) │ │
|
||||
│ ├───────────────────────────────────┤ │
|
||||
│ │ Antenne │ │
|
||||
│ └───────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.3 Waar zit het verschil?
|
||||
|
||||
Het verschil zit op **meerdere lagen**, niet alleen protocol:
|
||||
|
||||
| Laag | Classic (BR/EDR) | BLE | Verschil in hardware? |
|
||||
|------|------------------|-----|----------------------|
|
||||
| **Radio** | GFSK, π/4-DQPSK, 8DPSK | GFSK | **Ja** — andere modulatie |
|
||||
| **Kanalen** | 79 kanalen, 1 MHz breed | 40 kanalen, 2 MHz breed | **Ja** — andere indeling |
|
||||
| **Link Layer** | LMP (Link Manager Protocol) | LL (Link Layer) | **Ja** — andere state machine |
|
||||
| **Protocollen** | L2CAP, RFCOMM, SDP | L2CAP, ATT, GATT | Nee — software |
|
||||
|
||||
### 3.4 Dual-mode apparaten
|
||||
|
||||
De overlap zit in apparaten die **beide** ondersteunen:
|
||||
|
||||
| Apparaattype | Ondersteunt | Voorbeeld |
|
||||
|--------------|-------------|-----------|
|
||||
| **Classic-only** | Alleen BR/EDR | Oude headsets, auto-audio |
|
||||
| **BLE-only** (Bluetooth Smart) | Alleen BLE | Fitnesstrackers, sensoren, T1000-e |
|
||||
| **Dual-Mode** (Bluetooth Smart Ready) | Beide | Smartphones, laptops, ESP32 |
|
||||
|
||||
**Jouw smartphone** is dual-mode: hij kan praten met je klassieke Bluetooth-koptelefoon (BR/EDR) én met je MeshCore T1000-e (BLE).
|
||||
|
||||
### 3.5 Praktijkvoorbeelden
|
||||
|
||||
| Scenario | Wat wordt gebruikt |
|
||||
|----------|-------------------|
|
||||
| Muziek naar je koptelefoon | **Classic** (A2DP profiel) |
|
||||
| Hartslag van je smartwatch | **BLE** (Heart Rate Service) |
|
||||
| Bestand naar laptop sturen | **Classic** (OBEX/FTP profiel) |
|
||||
| MeshCore T1000-e uitlezen | **BLE** (NUS service) |
|
||||
| Handsfree bellen in auto | **Classic** (HFP profiel) |
|
||||
| Slimme lamp bedienen | **BLE** (eigen GATT service) |
|
||||
|
||||
---
|
||||
|
||||
## 4. BLE kanaalindeling en frequency hopping
|
||||
|
||||
### 4.1 De 40 BLE-kanalen
|
||||
|
||||
De 2.4 GHz ISM-band loopt van **2400 MHz tot 2483.5 MHz** (83.5 MHz breed).
|
||||
|
||||
BLE verdeelt dit in **40 kanalen van elk 2 MHz**:
|
||||
|
||||
```
|
||||
2400 MHz 2480 MHz
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
|
||||
│00│01│02│03│04│05│06│07│08│09│10│11│12│13│14│15│16│17│18│19│...→ 39
|
||||
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘
|
||||
└──────────────────────────────────────────────────────┘
|
||||
2 MHz per kanaal
|
||||
```
|
||||
|
||||
**Totaal:** 40 × 2 MHz = **80 MHz** gebruikt
|
||||
|
||||
### 4.2 Advertising vs. data kanalen
|
||||
|
||||
De 40 kanalen zijn niet allemaal gelijk:
|
||||
|
||||
| Type | Kanalen | Functie |
|
||||
|------|---------|---------|
|
||||
| **Advertising** | 3 (nrs. 37, 38, 39) | Apparaten vinden, verbinding starten |
|
||||
| **Data** | 37 (nrs. 0-36) | Daadwerkelijke communicatie na verbinding |
|
||||
|
||||
De advertising-kanalen zijn strategisch gekozen om **Wi-Fi-interferentie** te vermijden:
|
||||
|
||||
```
|
||||
Wi-Fi kanaal 1 Wi-Fi kanaal 6 Wi-Fi kanaal 11
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
────████████─────────────█████████────────────████████────
|
||||
|
||||
BLE: ▲ ▲ ▲
|
||||
Ch.37 Ch.38 Ch.39
|
||||
|
||||
(advertising kanalen zitten tússen de Wi-Fi kanalen)
|
||||
```
|
||||
|
||||
### 4.3 Vergelijking met Classic Bluetooth
|
||||
|
||||
| Aspect | Classic (BR/EDR) | BLE |
|
||||
|--------|------------------|-----|
|
||||
| **Aantal kanalen** | 79 | 40 |
|
||||
| **Kanaalbreedte** | 1 MHz | 2 MHz |
|
||||
| **Totale bandbreedte** | 79 MHz | 80 MHz |
|
||||
| **Frequency hopping** | Ja, alle 79 | Ja, 37 datakanalen |
|
||||
|
||||
Classic heeft **meer maar smallere** kanalen, BLE heeft **minder maar bredere** kanalen.
|
||||
|
||||
### 4.4 Frequency hopping: één kanaal tegelijk
|
||||
|
||||
**Belangrijk inzicht:** Je gebruikt altijd maar **één kanaal tegelijk**. De 40 kanalen zijn er voor **frequency hopping** — het afwisselend wisselen van kanaal om interferentie te vermijden:
|
||||
|
||||
```
|
||||
Tijd →
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 12 │ ▓ │ │ │ │ │ │ ▓ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 07 │ │ │ ▓ │ │ │ │ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||
Ch. 31 │ │ │ │ │ ▓ │ │ │
|
||||
└───┘ └───┘ └───┘ └───┘
|
||||
↑ ↑ ↑ ↑
|
||||
Pakket 1 Pakket 2 Pakket 3 Pakket 4
|
||||
```
|
||||
|
||||
Dit is **geen parallelle communicatie** — het is serieel met wisselende frequentie.
|
||||
|
||||
---
|
||||
|
||||
## 5. Twee betekenissen van "serieel"
|
||||
|
||||
Wanneer we zeggen "NUS is serieel", kan dit verwarring veroorzaken. Het woord "serieel" heeft namelijk **twee verschillende betekenissen** in deze context.
|
||||
|
||||
### 5.1 Radio-niveau: altijd serieel
|
||||
|
||||
**Alle** draadloze communicatie is serieel op fysiek niveau — je hebt maar **één radiokanaal tegelijk** en bits gaan **na elkaar** de lucht in:
|
||||
|
||||
```
|
||||
Radiogolf: ▁▂▃▄▅▆▇█▇▆▅▄▃▂▁▂▃▄▅▆▇█▇▆▅▄▃▂▁
|
||||
|
||||
Bits: 0 1 1 0 1 0 0 1 1 1 0 1 0 0 1 0 → één voor één
|
||||
```
|
||||
|
||||
De 40 kanalen zijn voor **frequency hopping**, niet voor parallel versturen. Dit geldt voor **alle** BLE-services — NUS, Heart Rate, Battery, allemaal.
|
||||
|
||||
### 5.2 Data-niveau: NUS simuleert een seriële poort
|
||||
|
||||
Wanneer we zeggen "NUS is een seriële service", bedoelen we iets anders:
|
||||
|
||||
**NUS simuleert een oude seriële poort (RS-232/UART):**
|
||||
|
||||
```
|
||||
Historisch (jaren '80-'00):
|
||||
|
||||
Computer Apparaat
|
||||
┌──────┐ Seriële kabel ┌──────┐
|
||||
│ COM1 │←────────────────→│ UART │
|
||||
└──────┘ (RS-232) └──────┘
|
||||
|
||||
Bytes: 0x48 0x65 0x6C 0x6C 0x6F ("Hello")
|
||||
└─────────────────────┘
|
||||
Geen structuur, gewoon een stroom bytes
|
||||
```
|
||||
|
||||
**NUS bootst dit na over BLE:**
|
||||
|
||||
```
|
||||
Vandaag:
|
||||
|
||||
Computer Apparaat
|
||||
┌──────┐ BLE (NUS) ┌──────┐
|
||||
│ App │←~~~~~~~~~~~~~~~~~~~~→│ MCU │
|
||||
└──────┘ (draadloos) └──────┘
|
||||
|
||||
Gedraagt zich alsof er een seriële kabel zit
|
||||
```
|
||||
|
||||
### 5.3 Vergelijking: serieel vs. gestructureerd
|
||||
|
||||
| Aspect | NUS (serieel) | Heart Rate (gestructureerd) |
|
||||
|--------|---------------|----------------------------|
|
||||
| **Radio** | Serieel, frequency hopping | Serieel, frequency hopping |
|
||||
| **Data** | Ongestructureerde bytestroom | Vaste velden met betekenis |
|
||||
| **Wie bepaalt formaat?** | Jij (eigen protocol) | Bluetooth SIG (specificatie) |
|
||||
|
||||
### 5.4 Analogie: snelweg met rijstroken
|
||||
|
||||
Denk aan een **snelweg met 40 rijstroken** (de kanalen):
|
||||
|
||||
- Je mag maar **één rijstrook tegelijk** gebruiken
|
||||
- Je wisselt regelmatig van rijstrook (frequency hopping, om botsingen te vermijden)
|
||||
- De **vracht** die je vervoert kan verschillen:
|
||||
|
||||
| Service | Vracht-analogie |
|
||||
|---------|-----------------|
|
||||
| **NUS** | Losse spullen door elkaar (flexibel, maar jij moet uitzoeken wat wat is) |
|
||||
| **Heart Rate** | Gestandaardiseerde pallets (iedereen weet wat waar zit) |
|
||||
|
||||
De **snelweg werkt hetzelfde** — het verschil zit in hoe je de vracht organiseert.
|
||||
|
||||
---
|
||||
|
||||
## 6. Seriële vs. gestructureerde services (verdieping)
|
||||
|
||||
Een belangrijk onderscheid dat vaak over het hoofd wordt gezien: **niet alle BLE-services werken hetzelfde**. Er zijn fundamenteel twee benaderingen.
|
||||
|
||||
### 6.1 Seriële services (stream-gebaseerd)
|
||||
|
||||
**NUS (Nordic UART Service)** is ontworpen om een **seriële poort te simuleren**:
|
||||
|
||||
- Continue datastroom van ruwe bytes
|
||||
- Geen opgelegde structuur
|
||||
- Jij bepaalt zelf het formaat en de betekenis
|
||||
|
||||
**Analogie:** Een seriële service is als een **telefoonlijn** — je kunt alles zeggen wat je wilt, in elke taal, zonder vaste regels.
|
||||
|
||||
```
|
||||
Voorbeeld NUS-data (MeshCore):
|
||||
0x01 0x0A 0x48 0x65 0x6C 0x6C 0x6F ...
|
||||
└── Betekenis bepaald door MeshCore protocol, niet door BLE
|
||||
```
|
||||
|
||||
### 6.2 Gestructureerde services (veld-gebaseerd)
|
||||
|
||||
De meeste officiële SIG-services werken **anders** — ze definiëren **exact** welke bytes wat betekenen:
|
||||
|
||||
**Analogie:** Een gestructureerde service is als een **belastingformulier** — elk vakje heeft een vaste betekenis en een voorgeschreven formaat.
|
||||
|
||||
#### Voorbeeld: Heart Rate Measurement
|
||||
|
||||
```
|
||||
Byte 0: Flags (bitfield)
|
||||
├── Bit 0: 0 = hartslag in 1 byte, 1 = hartslag in 2 bytes
|
||||
├── Bit 1-2: Sensor contact status
|
||||
├── Bit 3: Energy expended aanwezig?
|
||||
└── Bit 4: RR-interval aanwezig?
|
||||
|
||||
Byte 1(-2): Heart rate waarde
|
||||
Byte N...: Optionele extra velden (afhankelijk van flags)
|
||||
```
|
||||
|
||||
**Concreet voorbeeld:**
|
||||
|
||||
```
|
||||
Ontvangen bytes: 0x00 0x73
|
||||
|
||||
0x00 = Flags: 8-bit formaat, geen extra velden
|
||||
0x73 = 115 decimaal → hartslag is 115 bpm
|
||||
```
|
||||
|
||||
Je krijgt dus niet de tekst "115", maar een binair pakket dat je moet **parsen** volgens de specificatie.
|
||||
|
||||
#### Voorbeeld: Battery Level
|
||||
|
||||
Eenvoudiger — slechts **1 byte**:
|
||||
|
||||
```
|
||||
Ontvangen byte: 0x5A
|
||||
|
||||
0x5A = 90 decimaal → batterij is 90%
|
||||
```
|
||||
|
||||
#### Voorbeeld: Environmental Sensing (temperatuur)
|
||||
|
||||
```
|
||||
Ontvangen bytes: 0x9C 0x08
|
||||
|
||||
Little-endian 16-bit signed integer: 0x089C = 2204
|
||||
Resolutie: 0.01°C
|
||||
Temperatuur: 2204 × 0.01 = 22.04°C
|
||||
```
|
||||
|
||||
### 6.3 Vergelijkingstabel
|
||||
|
||||
| Aspect | Serieel (NUS) | Gestructureerd (SIG) |
|
||||
|--------|---------------|----------------------|
|
||||
| **Data-indeling** | Vrij, zelf bepalen | Vast, door specificatie |
|
||||
| **Wie definieert het formaat?** | Jij / de fabrikant | Bluetooth SIG |
|
||||
| **Waar vind je de specificatie?** | Eigen documentatie / broncode | bluetooth.com/specifications |
|
||||
| **Parsing** | Eigen parser bouwen | Standaard parser mogelijk |
|
||||
| **Interoperabiliteit** | Alleen eigen software | Elke conforme app/device |
|
||||
| **Flexibiliteit** | Maximaal | Beperkt tot spec |
|
||||
| **Complexiteit** | Eenvoudig te starten | Spec lezen vereist |
|
||||
|
||||
### 6.4 Voorbeelden van gestructureerde services
|
||||
|
||||
| Service | Characteristic | Data-formaat |
|
||||
|---------|----------------|--------------|
|
||||
| **Battery Service** | Battery Level | 1 byte: 0-100 (percentage) |
|
||||
| **Heart Rate** | Heart Rate Measurement | Flags + 8/16-bit HR + optionele velden |
|
||||
| **Health Thermometer** | Temperature Measurement | IEEE-11073 FLOAT (4 bytes) |
|
||||
| **Blood Pressure** | Blood Pressure Measurement | Compound: systolisch, diastolisch, MAP, pulse |
|
||||
| **Cycling Speed & Cadence** | CSC Measurement | 32-bit tellers + 16-bit tijd |
|
||||
| **Environmental Sensing** | Temperature | 16-bit signed, resolutie 0.01°C |
|
||||
| **Environmental Sensing** | Humidity | 16-bit unsigned, resolutie 0.01% |
|
||||
| **Environmental Sensing** | Pressure | 32-bit unsigned, resolutie 0.1 Pa |
|
||||
|
||||
### 6.5 Wanneer welke aanpak?
|
||||
|
||||
| Situatie | Aanbevolen aanpak |
|
||||
|----------|-------------------|
|
||||
| Eigen protocol (MeshCore, custom IoT) | **Serieel** (NUS of eigen service) |
|
||||
| Standaard use-case (hartslag, batterij) | **Gestructureerd** (SIG-service) |
|
||||
| Interoperabiliteit met bestaande apps vereist | **Gestructureerd** (SIG-service) |
|
||||
| Complexe, variabele datastructuren | **Serieel** met eigen protocol |
|
||||
| Snel prototype zonder spec-studie | **Serieel** (NUS) |
|
||||
|
||||
### 6.6 Waarom MeshCore NUS gebruikt
|
||||
|
||||
MeshCore koos voor NUS (serieel) omdat:
|
||||
|
||||
1. **Flexibiliteit** — Het Companion Protocol heeft eigen framing nodig
|
||||
2. **Geen passende SIG-service** — Er is geen "Mesh Radio Service" standaard
|
||||
3. **Bidirectionele communicatie** — NUS biedt RX én TX characteristics
|
||||
4. **Eenvoud** — Geen complexe SIG-specificatie implementeren
|
||||
|
||||
Het nadeel: je kunt niet zomaar een willekeurige BLE-app gebruiken om met MeshCore te praten — je hebt software nodig die het MeshCore Companion Protocol begrijpt.
|
||||
|
||||
---
|
||||
|
||||
## 7. Het OSI-model in context
|
||||
|
||||
Het document plaatst het probleem in een **lagenmodel**. Dit helpt begrijpen *waar* het probleem zit:
|
||||
|
||||
| Laag | Naam | In dit project | Probleem hier? |
|
||||
|------|------|----------------|----------------|
|
||||
| 7 | Applicatie | MeshCore Companion Protocol | Nee |
|
||||
| 6 | Presentatie | Frame-encoding (hex) | Nee |
|
||||
| **5** | **Sessie** | **GATT client ↔ server sessie** | **★ JA** |
|
||||
| 4 | Transport | ATT / GATT | Nee |
|
||||
| 2 | Data Link | BLE Link Layer | Nee |
|
||||
| 1 | Fysiek | 2.4 GHz radio | Nee |
|
||||
|
||||
**Conclusie:** Het ownership-probleem zit op **laag 5 (sessie)**. De firmware en het protocol zijn niet het probleem — het gaat om wie de sessie "bezit".
|
||||
|
||||
---
|
||||
|
||||
## 8. Conclusie
|
||||
|
||||
De belangrijkste inzichten uit dit document:
|
||||
|
||||
- ✅ MeshCore BLE companion **werkt correct** op Linux
|
||||
- ✅ De firmware **blokkeert notify niet**
|
||||
- ✅ Het enige vereiste is: **exact één actieve BLE-client per radio**
|
||||
|
||||
Het begrijpen van het ownership-model en de BLE-fundamenten uit dit document is essentieel voor het werken met elk BLE-verbonden MeshCore-apparaat.
|
||||
|
||||
---
|
||||
|
||||
## 9. Referenties
|
||||
|
||||
- MeshCore Companion Radio Protocol: [GitHub Wiki](https://github.com/meshcore-dev/MeshCore/wiki/Companion-Radio-Protocol)
|
||||
- Bluetooth SIG Assigned Numbers (officiële services): [bluetooth.com/specifications/assigned-numbers](https://www.bluetooth.com/specifications/assigned-numbers/)
|
||||
- Bluetooth SIG GATT Specifications: [bluetooth.com/specifications/specs](https://www.bluetooth.com/specifications/specs/)
|
||||
- Nordic Bluetooth Numbers Database: [GitHub](https://github.com/NordicSemiconductor/bluetooth-numbers-database)
|
||||
- GATT Uitleg (Adafruit): [learn.adafruit.com](https://learn.adafruit.com/introduction-to-bluetooth-low-energy/gatt)
|
||||
- Bleak documentatie: [bleak.readthedocs.io](https://bleak.readthedocs.io/)
|
||||
- BlueZ: [bluez.org](http://www.bluez.org/)
|
||||
|
||||
---
|
||||
|
||||
> **Document:** `ble_capture_workflow_t_1000_e_uitleg.md`
|
||||
> **Gebaseerd op:** `ble_capture_workflow_t_1000_e.md`
|
||||
Reference in New Issue
Block a user