mirror of
https://github.com/smittix/intercept.git
synced 2026-06-08 14:11:54 -07:00
docs: add Meshcore integration design spec
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
# Meshcore Support — Design Spec
|
||||
|
||||
**Date:** 2026-05-10
|
||||
**Status:** Approved
|
||||
|
||||
## Overview
|
||||
|
||||
Add a Meshcore mode to Intercept, providing full feature parity with the existing Meshtastic module. Meshcore is a LoRa mesh radio platform using a repeater-based routing model (dedicated infrastructure nodes relay; clients do not). It has an official Python library (`meshcore`, PyPI) and a published companion protocol.
|
||||
|
||||
## Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|---|---|---|
|
||||
| Connection methods | USB serial + TCP + BLE | Maximum hardware flexibility |
|
||||
| Feature scope | Full parity with Meshtastic | Messages, node map, telemetry, traceroute, repeater management |
|
||||
| Async integration | Background asyncio thread | meshcore library is asyncio-based; this isolates it cleanly from Flask/gevent |
|
||||
| UI layout | Messages-first (mirror Meshtastic) | Sidebar: contacts/nodes. Center: message feed. Tabs: map, telemetry, repeaters |
|
||||
| BLE in Docker | Document limitation + proxy workaround | BLE unavailable in containers; meshcore-proxy bridges BLE → TCP |
|
||||
|
||||
## Architecture
|
||||
|
||||
### New Files
|
||||
|
||||
```
|
||||
utils/meshcore.py # MeshcoreClient singleton + dataclasses
|
||||
utils/meshcore_client.py # Thin async wrapper around meshcore library (lives in asyncio thread)
|
||||
routes/meshcore.py # Flask blueprint (/meshcore)
|
||||
static/js/modes/meshcore.js # Frontend IIFE module
|
||||
static/css/modes/meshcore.css # Scoped styles
|
||||
templates/partials/modes/meshcore.html # Sidebar partial
|
||||
tests/test_meshcore_client.py
|
||||
tests/test_meshcore_routes.py
|
||||
tests/test_meshcore_integration.py
|
||||
```
|
||||
|
||||
### Modified Files
|
||||
|
||||
- `routes/__init__.py` — import + `register_blueprint(meshcore_bp)`
|
||||
- `templates/index.html` — ~12 insertion points (CSS, partial, JS, validModes, modeGroups, etc.)
|
||||
- `requirements.txt` — add `meshcore>=1.0.0` (optional dep, graceful fallback if absent)
|
||||
- `.gitignore` — already has `.superpowers/` ✓
|
||||
|
||||
### Async Bridge Pattern
|
||||
|
||||
```
|
||||
meshcore library (asyncio event loop in daemon OS thread)
|
||||
→ event callbacks (_on_message, _on_node_update, _on_telemetry)
|
||||
→ asyncio.run_coroutine_threadsafe() → queue.Queue (thread-safe, max 500)
|
||||
→ /meshcore/stream SSE generator drains queue (30s keepalive timeout)
|
||||
→ Frontend EventSource routes by event type
|
||||
```
|
||||
|
||||
This is the same conceptual pattern as all other decoder integrations in Intercept (ADS-B socket reader, AIS-catcher output thread, rtl_433 stdout thread), just with an explicit asyncio loop instead of a subprocess thread.
|
||||
|
||||
## Data Model
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class MeshcoreMessage:
|
||||
id: str
|
||||
sender_id: str
|
||||
recipient_id: str # node ID or broadcast address
|
||||
text: str
|
||||
timestamp: datetime
|
||||
hop_count: int
|
||||
snr: float | None
|
||||
is_direct: bool # DM vs broadcast
|
||||
pending: bool = False # optimistic send state
|
||||
|
||||
@dataclass
|
||||
class MeshcoreNode:
|
||||
node_id: str
|
||||
name: str
|
||||
is_repeater: bool # key Meshcore distinction — rendered differently on map
|
||||
lat: float | None
|
||||
lon: float | None
|
||||
battery_pct: int | None
|
||||
last_seen: datetime
|
||||
snr: float | None
|
||||
hops_away: int | None
|
||||
|
||||
@dataclass
|
||||
class MeshcoreContact:
|
||||
node_id: str
|
||||
name: str
|
||||
public_key: str # Meshcore uses key-based addressing
|
||||
last_msg: datetime | None
|
||||
|
||||
@dataclass
|
||||
class MeshcoreTelemetry:
|
||||
node_id: str
|
||||
timestamp: datetime
|
||||
battery_pct: int | None
|
||||
voltage: float | None
|
||||
temperature: float | None
|
||||
humidity: float | None
|
||||
uptime_secs: int | None
|
||||
|
||||
@dataclass
|
||||
class MeshcoreTraceroute:
|
||||
origin_id: str
|
||||
destination_id: str
|
||||
hops: list[str]
|
||||
snr_per_hop: list[float]
|
||||
timestamp: datetime
|
||||
|
||||
@dataclass
|
||||
class SerialConfig:
|
||||
port: str | None = None # None = auto-discover
|
||||
baud: int = 115200
|
||||
|
||||
@dataclass
|
||||
class TCPConfig:
|
||||
host: str = "localhost"
|
||||
port: int = 5000 # meshcore-proxy default
|
||||
|
||||
@dataclass
|
||||
class BLEConfig:
|
||||
device_address: str | None = None # None = scan for first Meshcore device
|
||||
|
||||
ConnectionConfig = SerialConfig | TCPConfig | BLEConfig
|
||||
```
|
||||
|
||||
Connection state enum: `DISCONNECTED | CONNECTING | CONNECTED | ERROR`
|
||||
|
||||
## Connection Handling
|
||||
|
||||
### Serial
|
||||
Auto-discover: scan `/dev/ttyUSB*`, `/dev/ttyACM*`, `/dev/cu.usbserial*` and return list to frontend via `GET /meshcore/ports`. User can also specify path directly.
|
||||
|
||||
### TCP
|
||||
Direct connection to `host:port`. Primary use case: meshcore-proxy running on the host, exposing a local USB or BLE device over TCP for Docker deployments.
|
||||
|
||||
### BLE
|
||||
- Linux/RPi: meshcore library uses BlueZ (requires `bluetoothctl` accessible)
|
||||
- macOS: meshcore library uses CoreBluetooth
|
||||
- Docker: detect via presence of `/.dockerenv` or `INTERCEPT_DOCKER=1` env var; connect attempt fails fast with clear error directing user to meshcore-proxy
|
||||
|
||||
`GET /meshcore/ble/scan` returns: `[{"address": "AA:BB:CC:DD:EE:FF", "name": "MeshCore-Node1", "rssi": -72}]`
|
||||
|
||||
### Reconnect
|
||||
Exponential backoff: 3 retries at 5s, 15s, 45s (cap 60s). On final failure, pushes `status` SSE event with `state: "error"`. User can manually retry via `POST /meshcore/connect`.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | /meshcore/status | Connection state + transport info |
|
||||
| POST | /meshcore/connect | Connect with SerialConfig, TCPConfig, or BLEConfig |
|
||||
| POST | /meshcore/disconnect | Disconnect and stop background thread |
|
||||
| GET | /meshcore/ports | List available serial ports |
|
||||
| GET | /meshcore/ble/scan | Scan for nearby Meshcore BLE devices |
|
||||
| GET | /meshcore/stream | SSE stream (messages, nodes, telemetry, status) |
|
||||
| GET | /meshcore/messages | Recent messages (last 500) |
|
||||
| POST | /meshcore/send | Send text message |
|
||||
| GET | /meshcore/nodes | All known nodes |
|
||||
| GET | /meshcore/contacts | Contact list |
|
||||
| POST | /meshcore/contacts | Add contact |
|
||||
| DELETE | /meshcore/contacts/`<id>` | Remove contact |
|
||||
| GET | /meshcore/telemetry/`<node_id>` | Telemetry history for node |
|
||||
| POST | /meshcore/traceroute | Request traceroute to node |
|
||||
| GET | /meshcore/repeaters | List repeater nodes |
|
||||
|
||||
## SSE Event Format
|
||||
|
||||
```json
|
||||
{"type": "message", "data": { ...MeshcoreMessage }}
|
||||
{"type": "node", "data": { ...MeshcoreNode }}
|
||||
{"type": "telemetry", "data": { ...MeshcoreTelemetry }}
|
||||
{"type": "traceroute", "data": { ...MeshcoreTraceroute }}
|
||||
{"type": "status", "data": {"state": "connected", "transport": "serial", "device": "/dev/ttyUSB0"}}
|
||||
```
|
||||
|
||||
Keepalive comment (`: keepalive`) sent every 30 seconds on idle.
|
||||
|
||||
## Frontend (meshcore.js)
|
||||
|
||||
IIFE pattern, same as all other Intercept JS modules. Key responsibilities:
|
||||
|
||||
- **SSE consumer** — `EventSource('/meshcore/stream')`, routes events by `type`
|
||||
- **Message feed** — append to scrolling list, optimistic pending state on send
|
||||
- **Sidebar** — contact list + node list; repeaters shown separately with triangle icon (vs circle for client nodes), matching Meshcore UI conventions
|
||||
- **Tabs** — Map (Leaflet, reuse existing map setup pattern), Telemetry (Chart.js, reuse existing chart helpers), Repeaters (dedicated table view)
|
||||
- **Connection panel** — transport selector (Serial / TCP / BLE), port/IP/address input, connect/disconnect button
|
||||
- **Traceroute modal** — hop diagram with SNR annotations, same visual style as Meshtastic traceroute
|
||||
|
||||
## Repeater Management
|
||||
|
||||
Meshcore repeaters are a first-class concept (unlike Meshtastic where all nodes relay). Design:
|
||||
|
||||
- Repeaters identified by `is_repeater: true` on `MeshcoreNode`
|
||||
- Rendered on map as orange triangles (client nodes = blue circles)
|
||||
- Dedicated "Repeaters" tab in the main panel showing: name, location, uptime, last seen, hop count
|
||||
- Repeater stats surfaced in telemetry if available (uptime_secs from `MeshcoreTelemetry`)
|
||||
|
||||
## Error Handling
|
||||
|
||||
- meshcore library not installed → mode loads but shows "meshcore package required: `pip install meshcore`"
|
||||
- BLE in Docker → clear error: "BLE unavailable in Docker. Run meshcore-proxy on the host and connect via TCP."
|
||||
- Serial port not found → return available ports list in error response
|
||||
- Connection lost mid-session → automatic reconnect with backoff; SSE `status` event updates UI indicator
|
||||
- Send failure → SSE event clears pending state, shows error in message feed
|
||||
|
||||
## Testing
|
||||
|
||||
**`tests/test_meshcore_client.py`**
|
||||
- Connection state machine transitions
|
||||
- Reconnect backoff timing (mock asyncio loop)
|
||||
- Message parsing and queue feeding
|
||||
- Node/contact TTL expiry
|
||||
- BLE unavailability error (Docker scenario)
|
||||
|
||||
**`tests/test_meshcore_routes.py`**
|
||||
- All REST endpoints: correct JSON shape, status codes
|
||||
- `/meshcore/connect` with each connection config type
|
||||
- `/meshcore/send` with missing/invalid params → 400
|
||||
- SSE stream yields keepalive on empty queue
|
||||
- Input validation via `utils/validation.py`
|
||||
|
||||
**`tests/test_meshcore_integration.py`**
|
||||
- Mock meshcore library at boundary (same approach as mocking meshtastic SDK)
|
||||
- Full round-trip: connect → receive message event → appears in SSE stream
|
||||
- Traceroute request → hop structure correctly parsed
|
||||
|
||||
## Dependencies
|
||||
|
||||
```
|
||||
meshcore>=1.0.0 # optional — graceful degradation if absent
|
||||
```
|
||||
|
||||
No new frontend dependencies — Leaflet and Chart.js already present.
|
||||
|
||||
## Reference
|
||||
|
||||
- Meshcore Python library: https://github.com/meshcore-dev/meshcore_py
|
||||
- Companion protocol: https://docs.meshcore.io/companion_protocol/
|
||||
- meshcore-proxy (BLE/serial → TCP bridge): https://github.com/rgregg/meshcore-proxy
|
||||
- Existing Meshtastic implementation (reference): `utils/meshtastic.py`, `routes/meshtastic.py`
|
||||
Reference in New Issue
Block a user