backup commit

This commit is contained in:
Dorian
2026-03-17 00:03:08 +00:00
parent f23be63bba
commit 253c305cc8
43 changed files with 9514 additions and 308 deletions

View File

@@ -0,0 +1,15 @@
---
name: Local Frontend Dev Workflow
description: How to start the local frontend dev environment — use start-dev.sh from neode-ui/, NOT npm start from root
type: feedback
---
Run local frontend dev from `neode-ui/` directory: `./start-dev.sh` (NOT `npm start` from project root — there's no root package.json).
**Why:** The project root has no package.json. Running `npm start` there fails with ENOENT. The frontend dev script lives in `neode-ui/start-dev.sh`.
**How to apply:**
- `cd neode-ui && ./start-dev.sh` — clears ports, starts Docker apps, runs `npm run dev:mock` (mock backend on :5959, Vite on :8100)
- Stop with `./stop-dev.sh` or Ctrl+C
- Login password in dev mode: `password123`
- When telling the user how to test locally, always reference `cd neode-ui && ./start-dev.sh`

View File

@@ -0,0 +1,247 @@
# Phase 2: Mesh as Federation Transport
## Context
Phase 1 built three independent communication silos: Tor-HTTP for federation, Meshcore LoRa for offline mesh, Nostr relays for discovery. They share no common abstraction — federation hardcodes Tor SOCKS5, mesh has its own serial listener, peer messaging uses a separate HTTP POST. Adding a new transport (LAN/mDNS) or changing routing logic means touching every caller.
Phase 2 unifies these under a `NodeTransport` trait with automatic transport selection per peer (Mesh > LAN > Tor), chunked LoRa messaging with Reed-Solomon FEC for payloads >160 bytes, CBOR delta compression for state sync, mDNS LAN discovery, and a "mesh only" off-grid mode.
The v2.0 roadmap (`docs/roadmap-v2.0.md`) also calls for a visual topology map and CRDT-based state sync — those are deferred to Phase 5 (Mesh Intelligence), but the transport abstraction here enables them.
## Dependencies (Verified)
```toml
ciborium = "0.2.2" # CBOR serde (134M downloads, active)
reed-solomon-erasure = "6.0" # FEC (4M downloads, MIT)
mdns-sd = "0.18.2" # LAN peer discovery (1.86M, active)
```
## Architecture
```
core/archipelago/src/transport/
├── mod.rs — NodeTransport trait, TransportKind, TransportRouter, PeerRegistry
├── tor.rs — TorTransport (wraps node_message.rs SOCKS5 logic)
├── mesh_transport.rs — MeshTransport (bridges MeshService cmd channel)
├── lan.rs — LanTransport (mdns-sd + direct HTTP)
├── chunking.rs — Split/reassemble with Reed-Solomon FEC
└── delta.rs — CBOR delta encoding for NodeStateSnapshot
```
### Unified Peer Identity
DID is the canonical identifier. Transport-specific addresses are attributes:
```rust
pub struct PeerRecord {
pub did: String,
pub pubkey_hex: String,
pub name: Option<String>,
pub trust_level: TrustLevel,
// Transport capabilities
pub mesh_contact_id: Option<u32>,
pub lan_address: Option<SocketAddr>,
pub onion_address: Option<String>,
// Freshness
pub last_mesh: Option<String>,
pub last_lan: Option<String>,
pub last_tor: Option<String>,
}
```
### Transport Routing
```
TransportRouter::send_to_peer(did, payload)
1. Look up PeerRecord by DID
2. Filter to transports where peer has address AND transport is available AND not stale
3. Sort by priority: Mesh(1) > LAN(2) > Tor(3)
4. If mesh_only_mode: only allow Mesh
5. Try each in order, fall back on failure
```
### Chunking Protocol (for LoRa >160B)
Per-chunk structure (8 byte header + 124 byte payload = 132B, fits in 160B after encryption):
```
[0x01] [msg_id: u32 LE] [chunk_idx: u8] [total: u8] [is_parity: u8] [payload: 124B]
```
Reed-Solomon: 25% parity shards. 4 data chunks = 1 parity chunk. Any N-of-(N+parity) reconstructs.
Max practical: ~10 chunks (~1.2KB) before LoRa airtime becomes unreasonable.
### CBOR Delta Sync
Full JSON state: ~500-2000 bytes. CBOR delta when only CPU/mem changed: ~30-50 bytes. Fits single LoRa chunk.
```rust
pub struct StateDelta {
pub ts: String,
pub v: u8,
pub apps: Option<Vec<AppStatus>>, // Only changed apps
pub apps_rm: Option<Vec<String>>, // Removed app IDs
pub cpu: Option<f64>,
pub mem_u: Option<u64>,
// ... only changed fields, short names for wire size
}
```
## Implementation Steps
### Step 1: Add deps to Cargo.toml
**File**: `core/archipelago/Cargo.toml`
Add ciborium, reed-solomon-erasure, mdns-sd.
### Step 2: Create `transport/mod.rs` — Core trait + PeerRegistry + TransportRouter
**New file**: `core/archipelago/src/transport/mod.rs`
- `NodeTransport` trait: `kind()`, `is_available()`, `send(address, payload)`
- `TransportKind` enum: `Mesh = 1, Lan = 2, Tor = 3`
- `TransportMessage`: `{from_did, payload: Vec<u8>, message_type}`
- `PeerRegistry`: unified peer store (DID -> PeerRecord), JSON persistence to `transport-peers.json`
- `TransportRouter`: holds Vec<Box<dyn NodeTransport>>, routes by priority with fallback
- Register `mod transport;` in `core/archipelago/src/lib.rs` or main.rs
### Step 3: Create `transport/chunking.rs` — Reed-Solomon FEC
**New file**: `core/archipelago/src/transport/chunking.rs`
- `encode_chunked(data: &[u8]) -> Vec<Vec<u8>>` — split + FEC parity shards
- `ChunkReassembler` — state machine tracking pending messages, attempts reconstruction when enough chunks arrive
- `decode_chunked(chunks: &[Option<Vec<u8>>]) -> Result<Vec<u8>>` — reconstruct from data+parity
- Unit tests: roundtrip, missing chunks, corrupted chunks
### Step 4: Create `transport/delta.rs` — CBOR delta sync
**New file**: `core/archipelago/src/transport/delta.rs`
- `compute_delta(prev, curr: &NodeStateSnapshot) -> StateDelta`
- `apply_delta(base: &NodeStateSnapshot, delta: &StateDelta) -> NodeStateSnapshot`
- `encode_cbor(delta) -> Vec<u8>` via ciborium
- `decode_cbor(bytes) -> Result<StateDelta>`
- Unit tests: delta correctness, CBOR roundtrip, size comparison vs JSON
### Step 5: Create `transport/tor.rs` — TorTransport
**New file**: `core/archipelago/src/transport/tor.rs`
- Wraps existing `node_message::send_to_peer()` pattern (reqwest + SOCKS5)
- Reuses: `node_message.rs:66` send logic, `node_message.rs:115` health check
- `is_available()` checks SOCKS5 proxy reachable at 127.0.0.1:9050
- Receives handled by existing HTTP handler in `api/handler.rs`
### Step 6: Create `transport/mesh_transport.rs` — MeshTransport
**New file**: `core/archipelago/src/transport/mesh_transport.rs`
- Holds reference to `Arc<RwLock<Option<MeshService>>>` (same as RpcHandler)
- `send()` routes through `MeshService::send_message()` for small payloads
- For payloads >124B: chunk with `chunking::encode_chunked()`, send each chunk via `MeshCommand::SendText`
- 200ms inter-chunk delay for LoRa airtime fairness
- `is_available()` checks mesh device connected via `MeshService::status()`
### Step 7: Create `transport/lan.rs` — LanTransport
**New file**: `core/archipelago/src/transport/lan.rs`
- Uses `mdns-sd::ServiceDaemon` for discovery
- Advertises: `_archipelago._tcp.local.` with TXT records: `did=..., pubkey=..., version=...`
- Browses for peers, updates PeerRegistry when found
- `send()`: direct HTTP POST to `http://{ip}:5678/archipelago/node-message` (same endpoint as Tor, no proxy)
- Non-blocking init — if Avahi not available, fail gracefully
### Step 8: Add `mesh_only_mode` to MeshConfig
**File**: `core/archipelago/src/mesh/mod.rs`
- Add `pub mesh_only_mode: Option<bool>` to MeshConfig (with `#[serde(default)]`)
- TransportRouter reads this flag to restrict routing
### Step 9: Wire TransportRouter into server.rs
**File**: `core/archipelago/src/server.rs` (~line 136, after mesh service init)
- Create PeerRegistry, load from disk
- Create TorTransport, MeshTransport, LanTransport
- Start LAN advertisement + discovery
- Create TransportRouter with all three + mesh_only flag
- Spawn background task bridging `MeshEvent::IdentityReceived` -> PeerRegistry
- Store router on RpcHandler
### Step 10: Add transport RPC endpoints
**New file**: `core/archipelago/src/api/rpc/transport.rs`
**Modify**: `core/archipelago/src/api/rpc/mod.rs` (add dispatch routes)
Endpoints:
- `transport.status` — available transports, mesh_only flag, peer count
- `transport.peers` — unified peer list with per-peer transport capabilities + preferred transport
- `transport.send` — send message via best transport (by DID)
- `transport.set-mode` — toggle mesh_only mode
### Step 11: Add opt-in federation sync via transport
**File**: `core/archipelago/src/federation.rs`
- Add `sync_with_peer_via_transport()` alongside existing `sync_with_peer()`
- Uses CBOR delta encoding when transport is mesh (saves bandwidth)
- Falls back to full JSON over Tor for backward compat
- Background sync task checks for TransportRouter, uses it if available
### Step 12: Frontend — transport store + UI indicators
**New file**: `neode-ui/src/stores/transport.ts` — Pinia store for transport status
**Modify**: `neode-ui/src/views/Mesh.vue` — "OFF-GRID" indicator when mesh_only mode
**Modify**: `neode-ui/mock-backend.js` — add transport.* mock RPC responses
### Step 13: Mock backend transport data
**File**: `neode-ui/mock-backend.js`
Add `transport.status`, `transport.peers`, `transport.send`, `transport.set-mode` mock responses with realistic data showing mixed transport capabilities.
## Files Summary
### New (8)
1. `core/archipelago/src/transport/mod.rs`
2. `core/archipelago/src/transport/tor.rs`
3. `core/archipelago/src/transport/mesh_transport.rs`
4. `core/archipelago/src/transport/lan.rs`
5. `core/archipelago/src/transport/chunking.rs`
6. `core/archipelago/src/transport/delta.rs`
7. `core/archipelago/src/api/rpc/transport.rs`
8. `neode-ui/src/stores/transport.ts`
### Modified (8)
1. `core/archipelago/Cargo.toml` — add 3 deps
2. `core/archipelago/src/lib.rs` or main — `mod transport;`
3. `core/archipelago/src/server.rs` — init router, bridge events
4. `core/archipelago/src/api/rpc/mod.rs` — dispatch + store router
5. `core/archipelago/src/mesh/mod.rs``mesh_only_mode` config field
6. `core/archipelago/src/federation.rs``sync_with_peer_via_transport()`
7. `neode-ui/src/views/Mesh.vue` — off-grid indicator
8. `neode-ui/mock-backend.js` — transport.* mock data
### Reused Existing Code
- `node_message.rs:66-112` — Tor SOCKS5 send logic -> wrapped by TorTransport
- `node_message.rs:115-135` — Tor health check -> TorTransport.is_available()
- `mesh/mod.rs:209-282` — MeshService::send_message() -> called by MeshTransport
- `mesh/listener.rs:35-37` — MeshCommand channel -> used for chunked sends
- `mesh/crypto.rs` — X25519 ECDH + ChaCha20-Poly1305 -> reused as-is
- `federation.rs:36-49` — FederatedNode struct -> PeerRecord wraps this
- `federation.rs:179-191` — update_node_state() -> adapted for CBOR deltas
## Verification
1. **Unit tests** (run on dev server):
```bash
cargo test --all-features -- transport
cargo test --all-features -- chunking
cargo test --all-features -- delta
```
2. **Frontend type check**:
```bash
cd neode-ui && npm run type-check
```
3. **Dev mode test**:
```bash
cd neode-ui && npm start
# Navigate to /dashboard/mesh — verify OFF-GRID toggle
```
4. **Deploy + integration test**:
```bash
./scripts/deploy-to-target.sh --live
# On .228: curl transport.status RPC
# Verify mDNS advertises _archipelago._tcp.local.
# If mesh device connected: send chunked message >160B to .198
# Verify federation sync uses CBOR delta when available
```
## Implementation Order
**Week 1**: Steps 1-4 (deps, trait, chunking, delta — pure library code with unit tests)
**Week 2**: Steps 5-7 (three transport implementations)
**Week 3**: Steps 8-11 (integration: server wiring, RPC, federation migration)
**Week 4**: Steps 12-13 (frontend UI, mock data, deploy + test)

View File

@@ -0,0 +1,108 @@
# Meshcore Mesh Networking — Phase 1 Implementation Plan
## Context
Adding mesh networking to Archipelago using Heltec V3 devices running Meshcore firmware (Companion USB). Two nodes (.228 and .198) will exchange encrypted identity and text messages over LoRa radio with no internet required. The existing `mesh.rs` wraps the Meshtastic CLI — this replaces it with a native Meshcore serial protocol driver.
## Architecture
Convert `mesh.rs` into `mesh/` module directory:
```
core/archipelago/src/mesh/
├── mod.rs — Public API, MeshService, config (migrated from mesh.rs)
├── types.rs — MeshPeer, MeshMessage, MeshStatus, DeviceType
├── protocol.rs — Meshcore binary frame protocol (encode/decode/commands)
├── serial.rs — MeshcoreDevice: async serial driver (serial2-tokio)
├── crypto.rs — X25519 ECDH + ChaCha20-Poly1305 per-message encryption
└── listener.rs — Background tokio task: serial reader + message dispatcher
```
Frontend:
```
neode-ui/src/stores/mesh.ts — Pinia store
neode-ui/src/views/Mesh.vue — Mesh status, peers, messaging UI
```
## Dependency
Add to `core/archipelago/Cargo.toml`:
```toml
serial2-tokio = "0.1"
```
All crypto deps already present (chacha20poly1305, ed25519-dalek, curve25519-dalek).
## Meshcore Protocol Summary
- **Frame format**: `>` + 2-byte LE length + data (outbound), `<` + 2-byte LE length + data (inbound)
- **Baud**: 115200, 8N1
- **Max message**: 160 bytes
- **Init sequence**: CMD_DEVICE_QUERY (0x16) -> CMD_APP_START (0x01) -> CMD_SET_DEVICE_TIME (0x06)
- **Key commands**: SEND_TXT_MSG (0x02), SEND_CHANNEL_TXT_MSG (0x03), GET_CONTACTS (0x04), SYNC_NEXT_MESSAGE (0x0A), SEND_SELF_ADVERT (0x07)
- **Push events** (async, >=0x80): NEW_CONTACT (0x8A), ACK (0x82), MESSAGES_WAITING (0x83)
## Encryption Design
Reuses existing identity.rs X25519 key agreement:
1. Nodes broadcast identity on mesh channel: `ARCHY:1:{did}:{ed25519_pubkey}:{x25519_pubkey}`
2. Receiving node derives shared secret: X25519(our_secret, their_x25519_pub)
3. All DMs encrypted: ChaCha20-Poly1305 with random 12-byte nonce
4. Wire format: [nonce 12B] + [ciphertext] + [tag 16B] — fits in 160B limit for ~130B plaintext
## RPC Endpoints
| Method | Action |
|--------|--------|
| `mesh.status` | Device + mesh status (updated) |
| `mesh.peers` | **NEW** — list discovered mesh peers |
| `mesh.messages` | **NEW** — get message history (last 100) |
| `mesh.send` | **NEW** — send encrypted message to peer |
| `mesh.broadcast` | Broadcast identity (updated for Meshcore) |
| `mesh.configure` | Update config (updated) |
## Implementation Steps
1. **Create mesh/ module, migrate existing code** — types.rs + mod.rs from mesh.rs
2. **protocol.rs** — Binary frame encode/decode, command builders, response parsers + unit tests
3. **crypto.rs** — X25519 ECDH + ChaCha20-Poly1305 encrypt/decrypt + unit tests
4. **serial.rs** — MeshcoreDevice with open/init/send/recv + device auto-detection
5. **listener.rs** — Background task: serial reader, peer cache, message store, reconnect
6. **mod.rs MeshService** — Wraps listener + config, start/stop lifecycle
7. **Update RPC handlers** — New endpoints, wire MeshService into RpcHandler
8. **Update RPC dispatch** — Add routes in mod.rs ~line 622
9. **Frontend store + view** — mesh.ts Pinia store, Mesh.vue with glass-card UI, router + nav
10. **Deploy + test** — Deploy to .228 and .198, plug in Heltec V3s, test end-to-end
## Key Files to Modify
- `core/archipelago/src/mesh.rs` -> delete, replace with `mesh/` directory
- `core/archipelago/src/api/rpc/mesh.rs` — update handlers
- `core/archipelago/src/api/rpc/mod.rs` — add routes (~line 622)
- `core/archipelago/Cargo.toml` — add serial2-tokio
- `neode-ui/src/router/index.ts` — add /dashboard/mesh route
- `neode-ui/src/views/Dashboard.vue` — add Mesh nav item
## Reusable Existing Code
- `identity.rs` lines 140-152: Ed25519 -> X25519 conversion (CompressedEdwardsY -> Montgomery)
- `identity.rs` `pubkey_bytes_from_did_key()`: extract raw pubkey from DID string
- `node_message.rs` pattern: IncomingMessage store with max 100 circular buffer
- `mesh.rs` `MeshConfig` + `load_config`/`save_config`: migrate directly into mod.rs
- `mesh.rs` `detect_meshtastic_devices()`: keep as fallback, add Meshcore probe-based detection
## Prerequisites
- Flash both Heltec V3 with Meshcore **Companion USB** role
- Add `archipelago` user to `dialout` group: `usermod -aG dialout archipelago`
- Connect Heltec V3 to USB on .228 and .198
## Verification
1. `cargo clippy --all-targets` passes with zero warnings
2. Unit tests pass: protocol encode/decode, crypto encrypt/decrypt roundtrip
3. Device detected on /dev/ttyUSB0 or /dev/ttyACM0
4. Init handshake completes (visible in tracing logs)
5. Identity broadcast from .228, received on .198
6. Encrypted DM sent .228 -> .198, decrypted and visible in UI
7. Mesh.vue shows device status, peer list, message history

View File

@@ -0,0 +1,155 @@
---
name: mesh
description: Mesh networking development for Archipelago — protocol, crypto, serial driver, transport abstraction, and LoRa chat. Use when working on mesh radio, Meshcore protocol, LoRa messaging, transport layers, peer discovery, or off-grid communication features.
---
# Mesh Networking Skill
## Architecture
The mesh subsystem enables offline peer discovery and end-to-end encrypted messaging between Archipelago nodes via Meshcore LoRa radio devices (Heltec V3, T-Beam, RAK WisBlock).
```
USB Meshcore Device (115200 baud)
↕ serial2-tokio
core/archipelago/src/mesh/
├── mod.rs — MeshService: lifecycle, config, public API
├── types.rs — MeshPeer, MeshMessage, MeshStatus, MeshEvent
├── protocol.rs — Meshcore binary frame protocol (encode/decode)
├── serial.rs — MeshcoreDevice: async serial driver
├── crypto.rs — X25519 ECDH + ChaCha20-Poly1305 encryption
└── listener.rs — Background tokio task: serial reader + dispatcher
↕ RPC
core/archipelago/src/api/rpc/mesh.rs — 6 endpoints
↕ HTTP
neode-ui/src/stores/mesh.ts — Pinia store
neode-ui/src/views/Mesh.vue — Two-column chat UI
```
## Key Files
### Backend (Rust)
- `core/archipelago/src/mesh/mod.rs` — MeshService (start/stop/status/peers/messages/send/configure)
- `core/archipelago/src/mesh/types.rs` — All shared types
- `core/archipelago/src/mesh/protocol.rs` — Binary frame format, command builders, response parsers (12 unit tests)
- `core/archipelago/src/mesh/serial.rs` — USB serial driver, handshake, device detection
- `core/archipelago/src/mesh/crypto.rs` — X25519 key agreement + ChaCha20-Poly1305 (7 unit tests)
- `core/archipelago/src/mesh/listener.rs` — Background event loop, auto-reconnect, peer cache
- `core/archipelago/src/api/rpc/mesh.rs` — RPC handlers (mesh.status/peers/messages/send/broadcast/configure)
- `core/archipelago/src/server.rs` — MeshService initialization (non-blocking)
- `core/archipelago/src/identity.rs` — Ed25519 keypair, DID, X25519 derivation
### Frontend (Vue 3 + TypeScript)
- `neode-ui/src/stores/mesh.ts` — Pinia store with unread tracking
- `neode-ui/src/views/Mesh.vue` — Full chat UI (~1000 lines)
- `neode-ui/src/router/index.ts` — Route: `/dashboard/mesh`
### Mock Backend
- `neode-ui/mock-backend.js` — Dev mode mesh RPC responses (mesh.status/peers/messages/send/broadcast/configure)
## Protocol Reference
### Meshcore Frame Format
- Outbound: `<` (0x3C) + 2-byte LE length + data
- Inbound: `>` (0x3E) + 2-byte LE length + data
- Max LoRa payload: 160 bytes
- Baud: 115200, 8N1
### Key Commands
| Byte | Command | Description |
|------|---------|-------------|
| 0x01 | APP_START | Init session with version negotiation |
| 0x02 | SEND_TXT_MSG | Direct message (6-byte pubkey prefix) |
| 0x03 | SEND_CHANNEL_TXT_MSG | Broadcast on channel |
| 0x04 | GET_CONTACTS | Fetch contact list |
| 0x06 | SET_DEVICE_TIME | Sync device clock |
| 0x07 | SEND_SELF_ADVERT | Broadcast identity |
| 0x0A | SYNC_NEXT_MESSAGE | Retrieve queued messages |
### Identity Wire Format
`ARCHY:2:{ed25519_hex_64}:{x25519_hex_64}` (137 bytes, fits 160)
### Encryption
- X25519 Diffie-Hellman from Ed25519 keys (RFC 7748 clamping)
- ChaCha20-Poly1305 AEAD with random 12-byte nonce
- Wire: `[nonce 12B] + [ciphertext + tag 16B]` — max 132B plaintext
## RPC Endpoints
| Method | Params | Returns |
|--------|--------|---------|
| `mesh.status` | — | MeshStatus |
| `mesh.peers` | — | `{peers, count}` |
| `mesh.messages` | `{limit?}` | `{messages, count}` |
| `mesh.send` | `{contact_id, message}` | `{sent, message_id, encrypted}` |
| `mesh.broadcast` | — | `{broadcast}` |
| `mesh.configure` | `{enabled?, device_path?, channel_name?, broadcast_identity?, advert_name?}` | `{configured}` |
## Development Workflow
### Building & Testing (on dev server, NOT macOS)
```bash
# Deploy mesh changes
./scripts/deploy-to-target.sh --live
# Run mesh unit tests on server
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
'cd ~/archy/core && cargo test --all-features -- mesh'
# Check device is detected
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
'ls -la /dev/ttyUSB* /dev/ttyACM* 2>/dev/null'
# Watch mesh logs
ssh -i ~/.ssh/archipelago-deploy archipelago@192.168.1.228 \
'sudo journalctl -u archipelago -f | grep -i mesh'
```
### Frontend Dev (local, mock backend)
```bash
cd neode-ui && npm start
# Mesh mock data at http://localhost:8100/dashboard/mesh
```
## Roadmap Phases
### Phase 1: Core Implementation (COMPLETE)
- Meshcore binary protocol, serial driver, crypto, listener, RPC, Vue UI
### Phase 2: Mesh as Federation Transport
- NodeTransport trait abstraction (mesh/tor/lan backends)
- Transport priority: Mesh (1) > LAN/mDNS (2) > Tor (3)
- Chunked message protocol for >160B payloads (Reed-Solomon FEC)
- CBOR delta sync instead of full JSON state
- Transport indicator per peer in federation UI
- "Mesh only" off-grid mode
- Dependencies: `ciborium` (CBOR), `reed-solomon-erasure` (FEC), `mdns-sd` (LAN discovery)
### Phase 3: Encrypted Mesh Messaging
- Double Ratchet (Signal protocol) over LoRa
- X3DH key agreement using existing Ed25519/X25519
- Store-and-forward relay for offline peers (24h TTL)
- Message types: TEXT, ALERT, INVOICE (bolt11), PSBT_HASH, COORDINATE
- Per-peer chat threads, delivery status, offline indicators
### Phase 4: Off-Grid Bitcoin Operations
- Compact block headers over mesh (SPV verification)
- Transaction relay via internet-connected mesh peer
- Lightning payment coordination over mesh
- Emergency alert system (signed alerts, GPS, dead man's switch)
### Phase 5: Mesh Network Intelligence
- Adaptive routing, signal strength mapping, spreading factor adjustment
- Multi-path routing for reliability
- Steganographic modes
- Additional hardware: T-Beam, RAK WisBlock, WiFi mesh (802.11s), BLE, Blockstream Satellite
## Conventions
- All crypto uses existing identity infrastructure (Ed25519 signing key → X25519 derivation)
- Mesh init is non-blocking — errors logged but don't crash server
- Config persists to `{data_dir}/mesh-config.json`
- Message buffer: circular, max 100 messages
- Never build Rust on macOS — always deploy to server
- USB device paths: `/dev/ttyUSB*` and `/dev/ttyACM*`
- `archipelago` user must be in `dialout` group for serial access