merge: resolve icon conflicts — keep RGBA versions (fixes #200)

Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
ruv
2026-03-09 10:51:49 -04:00
51 changed files with 18179 additions and 48 deletions
+621
View File
@@ -0,0 +1,621 @@
# ADR-052 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend
This document maps out the domain model for the RuView Tauri desktop application
described in ADR-052. It defines bounded contexts, their aggregates, entities,
value objects, and the domain events flowing between them.
## Context Map
```
+-------------------+ +---------------------+ +--------------------+
| | | | | |
| Device Discovery |------>| Firmware Management |------>| Configuration / |
| | | | | Provisioning |
+-------------------+ +---------------------+ +--------------------+
| | |
| | |
v v v
+-------------------+ +---------------------+ +--------------------+
| | | | | |
| Sensing Pipeline |<------| Edge Module | | Visualization |
| | | (WASM) | | |
+-------------------+ +---------------------+ +--------------------+
Relationship types:
-----> Upstream/Downstream (upstream publishes events, downstream consumes)
<----- Conformist (downstream conforms to upstream's model)
```
---
## 1. Device Discovery Context
**Purpose**: Find, identify, and monitor ESP32 CSI nodes on the local network.
**Upstream of**: Firmware Management, Configuration, Sensing Pipeline, Visualization
### Aggregates
#### `NodeRegistry` (Aggregate Root)
Maintains the authoritative list of all known nodes. Merges discovery results
from multiple strategies (mDNS, UDP probe, HTTP sweep) and deduplicates by MAC
address.
| Field | Type | Description |
|-------|------|-------------|
| `nodes` | `Map<MacAddress, Node>` | All discovered nodes keyed by MAC |
| `scan_state` | `ScanState` | Idle, Scanning, Error |
| `last_scan` | `DateTime<Utc>` | Timestamp of last completed scan |
**Invariant**: No two nodes may share the same MAC address. If a node is
discovered via multiple strategies, the most recent data wins.
**Persistence**: The registry is persisted to `~/.ruview/nodes.db` (SQLite via
`rusqlite`). On startup, all previously known nodes are loaded as `Offline` and
reconciled against a fresh discovery scan. This means the app **remembers the
mesh** across restarts — critical for field deployments where nodes may be
temporarily powered off.
#### `Node` (Entity)
| Field | Type | Description |
|-------|------|-------------|
| `mac` | `MacAddress` (VO) | IEEE 802.11 MAC address (unique identity) |
| `ip` | `IpAddr` | Current IP address (may change on DHCP renewal) |
| `hostname` | `Option<String>` | mDNS hostname |
| `node_id` | `u8` | NVS-provisioned node ID |
| `firmware_version` | `Option<SemVer>` | Firmware version string |
| `health` | `HealthStatus` (VO) | Online / Offline / Degraded |
| `discovery_method` | `DiscoveryMethod` (VO) | How this node was found |
| `last_seen` | `DateTime<Utc>` | Last successful contact |
| `tdm_config` | `Option<TdmConfig>` (VO) | TDM slot assignment |
| `edge_tier` | `Option<u8>` | Edge processing tier (0/1/2) |
### Value Objects
- `MacAddress` — 6-byte hardware address, formatted as `AA:BB:CC:DD:EE:FF`
- `HealthStatus` — enum: `Online`, `Offline`, `Degraded(reason: String)`
- `DiscoveryMethod` — enum: `Mdns`, `UdpProbe`, `HttpSweep`, `Manual`
- `TdmConfig``{ slot_index: u8, total_nodes: u8 }`
- `SemVer` — semantic version `major.minor.patch`
### Domain Events
| Event | Payload | Consumers |
|-------|---------|-----------|
| `NodeDiscovered` | `{ node: Node }` | Firmware Mgmt (check for updates), Visualization (add to mesh graph) |
| `NodeWentOffline` | `{ mac: MacAddress, last_seen: DateTime }` | Visualization (gray out node), Sensing Pipeline (remove from active set) |
| `NodeCameOnline` | `{ node: Node }` | Visualization (restore node), Sensing Pipeline (re-add) |
| `NodeHealthChanged` | `{ mac: MacAddress, old: HealthStatus, new: HealthStatus }` | Visualization (update indicator) |
| `ScanCompleted` | `{ found: usize, new: usize, lost: usize }` | Dashboard (update summary) |
### Anti-Corruption Layer
When receiving data from the ESP32 OTA status endpoint (`GET /ota/status`), the
response format is owned by the firmware and may change across firmware versions.
The ACL translates the raw JSON response into `Node` entity fields:
```rust
/// ACL: Translate ESP32 OTA status response to Node fields.
fn translate_ota_status(raw: &serde_json::Value) -> Result<NodePatch, AclError> {
NodePatch {
firmware_version: raw["version"].as_str().map(SemVer::parse).transpose()?,
uptime_secs: raw["uptime_s"].as_u64(),
free_heap: raw["free_heap"].as_u64(),
// Firmware may add fields in future versions — unknown fields are ignored
}
}
```
---
## 2. Firmware Management Context
**Purpose**: Flash, update, and verify firmware on ESP32 nodes.
**Upstream of**: Configuration (a fresh flash triggers provisioning)
**Downstream of**: Device Discovery (needs node list and serial port info)
### Aggregates
#### `FlashSession` (Aggregate Root)
Represents a single firmware flashing operation from start to completion. Each
session has a lifecycle: Created -> Connecting -> Erasing -> Writing -> Verifying ->
Completed | Failed.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `Uuid` | Session identifier |
| `port` | `SerialPort` (VO) | Target serial port |
| `firmware` | `FirmwareBinary` (Entity) | The binary being flashed |
| `chip` | `ChipType` (VO) | Target chip (ESP32, ESP32-S3, ESP32-C3) |
| `phase` | `FlashPhase` (VO) | Current phase of the flash operation |
| `progress` | `Progress` (VO) | Bytes written / total, speed |
| `started_at` | `DateTime<Utc>` | When the session started |
| `error` | `Option<String>` | Error message if failed |
**Invariant**: Only one `FlashSession` may be active per serial port at a time.
#### `FirmwareBinary` (Entity)
| Field | Type | Description |
|-------|------|-------------|
| `path` | `PathBuf` | Filesystem path to the `.bin` file |
| `size_bytes` | `u64` | Binary size |
| `version` | `Option<SemVer>` | Extracted from ESP32 image header |
| `chip_type` | `Option<ChipType>` | Detected from image magic bytes |
| `checksum` | `Sha256Hash` (VO) | SHA-256 of the binary |
#### `OtaSession` (Aggregate Root)
Represents an over-the-air firmware update to a running node.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `Uuid` | Session identifier |
| `target_node` | `MacAddress` | Target node MAC |
| `target_ip` | `IpAddr` | Target node IP |
| `firmware` | `FirmwareBinary` | The binary being pushed |
| `psk` | `Option<SecureString>` | PSK for authentication (ADR-050) |
| `phase` | `OtaPhase` | Uploading / Rebooting / Verifying / Done / Failed |
| `progress` | `Progress` | Upload progress |
#### `BatchOtaSession` (Aggregate Root)
Coordinates rolling firmware updates across multiple mesh nodes. Prevents all
nodes from rebooting simultaneously, which would collapse the sensing network.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `Uuid` | Batch session identifier |
| `firmware` | `FirmwareBinary` | The binary being deployed |
| `strategy` | `OtaStrategy` | `Sequential`, `TdmSafe`, `Parallel` |
| `max_concurrent` | `usize` | Max nodes updating at once |
| `batch_delay_secs` | `u64` | Delay between batches |
| `fail_fast` | `bool` | Abort remaining on first failure |
| `node_states` | `Map<MacAddress, BatchNodeState>` | Per-node progress |
**Invariant**: In `TdmSafe` mode, adjacent TDM slots are never updated
concurrently. Even-slot nodes update first, then odd-slot nodes.
**Lifecycle**: `Planning → InProgress → Completed | PartialFailure | Aborted`
- `BatchNodeState` — enum: `Queued`, `Uploading(Progress)`, `Rebooting`, `Verifying`, `Done`, `Failed(String)`, `Skipped`
- `OtaStrategy` — enum:
- `Sequential` — one node at a time, wait for rejoin
- `TdmSafe` — update non-adjacent slots to maintain sensing coverage
- `Parallel` — all at once (development only)
### Value Objects
- `SerialPort``{ name: String, vid: u16, pid: u16, manufacturer: Option<String> }`
- `ChipType` — enum: `Esp32`, `Esp32s3`, `Esp32c3`
- `FlashPhase` — enum: `Connecting`, `Erasing`, `Writing`, `Verifying`, `Completed`, `Failed`
- `OtaPhase` — enum: `Uploading`, `Rebooting`, `Verifying`, `Completed`, `Failed`
- `Progress``{ bytes_done: u64, bytes_total: u64, speed_bps: u64 }`
- `Sha256Hash` — 32-byte hash
- `SecureString` — zeroized-on-drop string for PSK tokens
### Domain Events
| Event | Payload | Consumers |
|-------|---------|-----------|
| `FlashStarted` | `{ session_id, port, firmware_version }` | UI (show progress) |
| `FlashProgress` | `{ session_id, phase, progress }` | UI (update progress bar) |
| `FlashCompleted` | `{ session_id, duration_secs }` | Configuration (trigger provisioning prompt) |
| `FlashFailed` | `{ session_id, error }` | UI (show error) |
| `OtaStarted` | `{ session_id, target_mac, firmware_version }` | Discovery (mark node as updating) |
| `OtaCompleted` | `{ session_id, target_mac, new_version }` | Discovery (refresh node info) |
| `OtaFailed` | `{ session_id, target_mac, error }` | UI (show error) |
| `BatchOtaStarted` | `{ batch_id, strategy, node_count }` | UI (show batch progress) |
| `BatchNodeUpdated` | `{ batch_id, mac, state }` | UI (update per-node status), Discovery (refresh) |
| `BatchOtaCompleted` | `{ batch_id, succeeded, failed, skipped }` | UI (show summary), Discovery (full rescan) |
### Anti-Corruption Layer
The `espflash` crate has its own error types and progress reporting model. The
ACL translates these into domain events:
```rust
/// ACL: Translate espflash progress callbacks to domain FlashProgress events.
impl From<espflash::ProgressCallbackMessage> for FlashProgress {
fn from(msg: espflash::ProgressCallbackMessage) -> Self {
match msg {
espflash::ProgressCallbackMessage::Connecting => FlashProgress {
phase: FlashPhase::Connecting,
progress: Progress::indeterminate(),
},
espflash::ProgressCallbackMessage::Erasing { addr, total } => FlashProgress {
phase: FlashPhase::Erasing,
progress: Progress::new(addr as u64, total as u64),
},
// ... etc
}
}
}
```
---
## 3. Configuration / Provisioning Context
**Purpose**: Manage NVS configuration for ESP32 nodes — WiFi credentials, network
targets, TDM mesh settings, edge intelligence parameters, WASM security keys.
**Downstream of**: Device Discovery (needs serial port), Firmware Management (post-flash provisioning)
### Aggregates
#### `ProvisioningSession` (Aggregate Root)
Represents a single NVS write or read operation on a connected ESP32.
| Field | Type | Description |
|-------|------|-------------|
| `id` | `Uuid` | Session identifier |
| `port` | `SerialPort` (VO) | Target serial port |
| `config` | `NodeConfig` (Entity) | Configuration to write |
| `direction` | `Direction` | Read or Write |
| `phase` | `ProvisionPhase` | Generating / Flashing / Verifying / Done |
#### `NodeConfig` (Entity)
The full set of NVS key-value pairs for a single node. Maps directly to the
firmware's `nvs_config_t` struct (see `firmware/esp32-csi-node/main/nvs_config.h`).
| Field | Type | NVS Key | Description |
|-------|------|---------|-------------|
| `wifi_ssid` | `Option<String>` | `ssid` | WiFi SSID |
| `wifi_password` | `Option<SecureString>` | `password` | WiFi password |
| `target_ip` | `Option<IpAddr>` | `target_ip` | Aggregator IP |
| `target_port` | `Option<u16>` | `target_port` | Aggregator UDP port |
| `node_id` | `Option<u8>` | `node_id` | Node identifier |
| `tdm_slot` | `Option<u8>` | `tdm_slot` | TDM slot index |
| `tdm_total` | `Option<u8>` | `tdm_nodes` | Total TDM nodes |
| `edge_tier` | `Option<u8>` | `edge_tier` | Processing tier |
| `hop_count` | `Option<u8>` | `hop_count` | Channel hop count |
| `channel_list` | `Option<Vec<u8>>` | `chan_list` | Channel sequence |
| `dwell_ms` | `Option<u32>` | `dwell_ms` | Hop dwell time |
| `power_duty` | `Option<u8>` | `power_duty` | Power duty cycle |
| `presence_thresh` | `Option<u16>` | `pres_thresh` | Presence threshold |
| `fall_thresh` | `Option<u16>` | `fall_thresh` | Fall detection threshold |
| `vital_window` | `Option<u16>` | `vital_win` | Vital sign window |
| `vital_interval_ms` | `Option<u16>` | `vital_int` | Vital sign interval |
| `top_k_count` | `Option<u8>` | `subk_count` | Top-K subcarriers |
| `wasm_max_modules` | `Option<u8>` | `wasm_max` | Max WASM modules |
| `wasm_verify` | `Option<bool>` | `wasm_verify` | Require WASM signature |
| `wasm_pubkey` | `Option<[u8; 32]>` | `wasm_pubkey` | Ed25519 public key |
| `ota_psk` | `Option<SecureString>` | `ota_psk` | OTA pre-shared key |
**Invariant**: `tdm_slot < tdm_total` when both are set.
**Invariant**: `channel_list.len() == hop_count` when both are set.
**Invariant**: `10 <= power_duty <= 100`.
#### `MeshConfig` (Entity)
A mesh-level configuration that generates per-node `NodeConfig` instances.
Corresponds to ADR-044 Phase 2 (config file provisioning).
| Field | Type | Description |
|-------|------|-------------|
| `common` | `NodeConfig` | Shared settings (WiFi, target IP, edge tier) |
| `nodes` | `Vec<MeshNodeEntry>` | Per-node overrides (port, node_id, tdm_slot) |
```rust
pub struct MeshNodeEntry {
pub port: String,
pub node_id: u8,
pub tdm_slot: u8,
// All other fields inherited from common
}
```
**Invariant**: `tdm_total` is automatically computed as `nodes.len()`.
### Value Objects
- `ProvisionPhase` — enum: `Generating`, `Flashing`, `Verifying`, `Completed`, `Failed`
- `Direction` — enum: `Read`, `Write`
- `Preset` — enum: `Basic`, `Vitals`, `Mesh3`, `Mesh6Vitals` (ADR-044 Phase 3)
### Domain Events
| Event | Payload | Consumers |
|-------|---------|-----------|
| `NodeProvisioned` | `{ port, node_id, config_summary }` | Discovery (trigger re-scan), UI (show success) |
| `NvsReadCompleted` | `{ port, config: NodeConfig }` | UI (populate form) |
| `ProvisionFailed` | `{ port, error }` | UI (show error) |
| `MeshProvisionStarted` | `{ node_count }` | UI (show batch progress) |
| `MeshProvisionCompleted` | `{ success_count, fail_count }` | UI (show summary) |
---
## 4. Sensing Pipeline Context
**Purpose**: Control the sensing server process, receive real-time CSI data, and
manage the signal processing pipeline.
**Downstream of**: Device Discovery (needs node IPs for data attribution)
### Aggregates
#### `SensingServer` (Aggregate Root)
Represents the managed sensing server child process.
| Field | Type | Description |
|-------|------|-------------|
| `state` | `ServerState` (VO) | Stopped / Starting / Running / Stopping / Crashed |
| `config` | `ServerConfig` (VO) | Port configuration, log level, model paths |
| `pid` | `Option<u32>` | OS process ID when running |
| `started_at` | `Option<DateTime<Utc>>` | Start timestamp |
| `log_buffer` | `RingBuffer<LogEntry>` | Last N log lines |
| `ws_url` | `Option<Url>` | WebSocket URL for live data |
**Invariant**: Only one `SensingServer` process may be managed at a time.
#### `SensingSession` (Entity)
An active connection to the sensing server's WebSocket for receiving real-time data.
| Field | Type | Description |
|-------|------|-------------|
| `connection_state` | `WsState` | Connecting / Connected / Disconnected |
| `frames_received` | `u64` | Total CSI frames received this session |
| `last_frame_at` | `Option<DateTime<Utc>>` | Timestamp of last received frame |
| `subscriptions` | `HashSet<DataChannel>` | Which data streams are active |
### Value Objects
- `ServerState` — enum: `Stopped`, `Starting`, `Running`, `Stopping`, `Crashed(exit_code: i32)`
- `ServerConfig``{ http_port: u16, ws_port: u16, udp_port: u16, model_dir: PathBuf, log_level: Level }`
- `LogEntry``{ timestamp: DateTime, level: Level, target: String, message: String }`
- `DataChannel` — enum: `CsiFrames`, `PoseUpdates`, `VitalSigns`, `ActivityClassification`
- `WsState` — enum: `Connecting`, `Connected`, `Disconnected(reason: String)`
### Domain Events
| Event | Payload | Consumers |
|-------|---------|-----------|
| `ServerStarted` | `{ pid, ports: ServerConfig }` | UI (enable sensing view), Discovery (start health polling via WS) |
| `ServerStopped` | `{ exit_code, uptime_secs }` | UI (disable sensing view) |
| `ServerCrashed` | `{ exit_code, last_log_lines }` | UI (show crash report) |
| `CsiFrameReceived` | `{ node_id, timestamp, subcarrier_count }` | Visualization (update charts) |
| `PoseUpdated` | `{ persons: Vec<PersonPose> }` | Visualization (draw skeletons) |
| `VitalSignUpdate` | `{ node_id, bpm, breath_rate }` | Visualization (update vitals chart) |
| `ActivityDetected` | `{ label, confidence }` | Visualization (show activity) |
---
## 5. Edge Module (WASM) Context
**Purpose**: Upload, manage, and monitor WASM edge processing modules running
on ESP32 nodes.
**Downstream of**: Device Discovery (needs node IPs and WASM capability info)
**Upstream of**: Sensing Pipeline (WASM modules emit edge-processed events)
### Aggregates
#### `ModuleRegistry` (Aggregate Root)
Tracks all WASM modules across all nodes.
| Field | Type | Description |
|-------|------|-------------|
| `modules` | `Map<(MacAddress, ModuleId), WasmModule>` | Per-node module inventory |
#### `WasmModule` (Entity)
| Field | Type | Description |
|-------|------|-------------|
| `id` | `ModuleId` (VO) | Node-assigned module identifier |
| `name` | `String` | Filename of the uploaded `.wasm` |
| `size_bytes` | `u64` | Module size |
| `status` | `ModuleStatus` (VO) | Loaded / Running / Stopped / Error |
| `node_mac` | `MacAddress` | Which node this module runs on |
| `uploaded_at` | `DateTime<Utc>` | Upload timestamp |
| `signed` | `bool` | Whether the module has an Ed25519 signature |
### Value Objects
- `ModuleId` — string identifier assigned by the node firmware
- `ModuleStatus` — enum: `Loaded`, `Running`, `Stopped`, `Error(String)`
### Domain Events
| Event | Payload | Consumers |
|-------|---------|-----------|
| `ModuleUploaded` | `{ node_mac, module_id, name, size }` | UI (refresh list) |
| `ModuleStarted` | `{ node_mac, module_id }` | UI (update status) |
| `ModuleStopped` | `{ node_mac, module_id }` | UI (update status) |
| `ModuleUnloaded` | `{ node_mac, module_id }` | UI (remove from list) |
| `ModuleError` | `{ node_mac, module_id, error }` | UI (show error) |
### Anti-Corruption Layer
The ESP32 WASM management HTTP API (`/wasm/*` on port 8032) returns raw JSON
with firmware-specific field names. The ACL normalizes these:
```rust
/// ACL: Translate ESP32 WASM list response to domain WasmModule entities.
fn translate_wasm_list(raw: &[serde_json::Value]) -> Vec<WasmModule> {
raw.iter().filter_map(|entry| {
Some(WasmModule {
id: ModuleId(entry["id"].as_str()?.to_string()),
name: entry["name"].as_str().unwrap_or("unknown").to_string(),
size_bytes: entry["size"].as_u64().unwrap_or(0),
status: match entry["state"].as_str() {
Some("running") => ModuleStatus::Running,
Some("stopped") => ModuleStatus::Stopped,
Some("loaded") => ModuleStatus::Loaded,
other => ModuleStatus::Error(
format!("Unknown state: {:?}", other)
),
},
// ...
})
}).collect()
}
```
---
## 6. Visualization Context
**Purpose**: Render real-time and historical sensing data — CSI heatmaps, pose
skeletons, vital sign charts, mesh topology graphs.
**Downstream of**: Sensing Pipeline (receives data events), Device Discovery (needs
node metadata for labeling)
This context is **purely presentational** and contains no domain logic. It
transforms domain events from other contexts into visual representations.
### Aggregates
None — this context is a **Query Model** (CQRS read side). It subscribes to
domain events and projects them into view models.
### View Models
#### `DashboardView`
| Field | Source Context | Description |
|-------|---------------|-------------|
| `nodes` | Device Discovery | Node cards with health, version, signal quality |
| `server` | Sensing Pipeline | Server status, uptime, port info |
| `recent_activity` | All contexts | Timeline of recent events |
#### `SignalView`
| Field | Source Context | Description |
|-------|---------------|-------------|
| `csi_heatmap` | Sensing Pipeline | Subcarrier amplitude x time matrix |
| `signal_field` | Sensing Pipeline | 2D signal strength grid |
| `activity_label` | Sensing Pipeline | Current classification |
| `confidence` | Sensing Pipeline | Classification confidence |
#### `PoseView`
| Field | Source Context | Description |
|-------|---------------|-------------|
| `persons` | Sensing Pipeline | Array of detected person skeletons |
| `zones` | Sensing Pipeline | Active zones in the sensing area |
#### `VitalsView`
| Field | Source Context | Description |
|-------|---------------|-------------|
| `breathing_rate_bpm` | Sensing Pipeline | Per-node breathing rate time series |
| `heart_rate_bpm` | Sensing Pipeline | Per-node heart rate time series |
#### `MeshView`
| Field | Source Context | Description |
|-------|---------------|-------------|
| `nodes` | Device Discovery | Positioned nodes for graph layout |
| `edges` | Device Discovery | Inter-node visibility/connectivity |
| `tdm_timeline` | Device Discovery | TDM slot schedule visualization |
| `sync_status` | Sensing Pipeline | Per-node sync status with server |
---
## Cross-Context Event Flow
```
NodeDiscovered
Device Discovery ─────────────────────────────────> Firmware Management
│ │
│ NodeDiscovered │ FlashCompleted
│ NodeHealthChanged │
├──────────────────> Visualization v
│ Configuration
│ NodeDiscovered │
├──────────────────> Sensing Pipeline │ NodeProvisioned
│ │
│ v
│ Device Discovery
│ (re-scan triggered)
│ NodeDiscovered
└──────────────────> Edge Module (WASM)
│ ModuleUploaded, ModuleStarted
v
Sensing Pipeline
│ CsiFrameReceived, PoseUpdated, VitalSignUpdate
v
Visualization
```
## Implementation Notes
1. **Event Bus**: Domain events are dispatched via Tauri's event system
(`app_handle.emit("event-name", payload)`). The frontend subscribes using
`listen("event-name", callback)`. This provides natural cross-context
communication without coupling contexts directly.
2. **State Isolation**: Each bounded context maintains its own `State<'_, T>`
managed by Tauri. Contexts do not share mutable state directly — they
communicate exclusively through events.
3. **Module Organization**: Each bounded context maps to a Rust module under
`src/commands/` and `src/domain/`:
```
src/
commands/ # Tauri command handlers (application layer)
discovery.rs # Device Discovery context commands
flash.rs # Firmware Management context commands
ota.rs # Firmware Management context commands
provision.rs # Configuration context commands
server.rs # Sensing Pipeline context commands
wasm.rs # Edge Module context commands
domain/ # Domain models (pure Rust, no Tauri dependency)
discovery/
mod.rs
node.rs # Node entity, MacAddress VO
registry.rs # NodeRegistry aggregate
events.rs # Discovery domain events
firmware/
mod.rs
binary.rs # FirmwareBinary entity
flash.rs # FlashSession aggregate
ota.rs # OtaSession aggregate
events.rs
config/
mod.rs
nvs.rs # NodeConfig entity
mesh.rs # MeshConfig entity
provision.rs # ProvisioningSession aggregate
events.rs
sensing/
mod.rs
server.rs # SensingServer aggregate
session.rs # SensingSession entity
events.rs
wasm/
mod.rs
module.rs # WasmModule entity
registry.rs # ModuleRegistry aggregate
events.rs
acl/ # Anti-corruption layers
ota_status.rs # ESP32 OTA status response translator
wasm_api.rs # ESP32 WASM API response translator
espflash.rs # espflash crate adapter
```
4. **Testing Strategy**: Domain modules under `src/domain/` have no Tauri
dependency and can be tested with standard `cargo test`. Command handlers
under `src/commands/` require Tauri test utilities for integration testing.
5. **Shared Kernel**: The `MacAddress`, `SemVer`, and `SecureString` value objects
are shared across contexts. They live in a `src/domain/shared.rs` module.
This is acceptable because they are immutable value objects with no behavior
beyond validation and formatting.
+810
View File
@@ -0,0 +1,810 @@
# ADR-052: Tauri Desktop Frontend — RuView Hardware Management & Visualization
| Field | Value |
|-------|-------|
| Status | Proposed |
| Date | 2026-03-06 |
| Deciders | ruv |
| Depends on | ADR-012 (ESP32 CSI Mesh), ADR-039 (Edge Intelligence), ADR-040 (WASM Programmable Sensing), ADR-044 (Provisioning Enhancements), ADR-050 (Security Hardening), ADR-051 (Server Decomposition) |
| Issue | [#177](https://github.com/ruvnet/RuView/issues/177) |
## Context
RuView currently requires users to interact with multiple disconnected tools to manage a WiFi DensePose deployment:
| Task | Current Tool | Pain Point |
|------|-------------|------------|
| Flash firmware | `esptool.py` CLI | Requires Python, pip, correct chip/baud flags |
| Provision NVS | `provision.py` CLI | 13+ flags, no GUI, no read-back |
| OTA update | `curl POST :8032/ota` | Manual HTTP, PSK header construction |
| WASM modules | `curl` to `:8032/wasm/*` | No visibility into module state |
| Start sensing server | `cargo run` or binary | Manual port configuration, no log viewer |
| View sensing data | Browser at `localhost:8080` | Separate window, no hardware context |
| Mesh topology | Mental model | No visualization of TDM slots, sync, health |
| Node discovery | Manual IP tracking | No mDNS/UDP broadcast discovery |
There is no single tool that provides a unified view of the entire deployment — from ESP32 hardware through the sensing pipeline to pose visualization. Field operators deploying multi-node meshes must context-switch between terminals, browsers, and serial monitors.
### Why a Desktop App
A browser-based UI cannot access serial ports (for flashing), raw UDP sockets (for node discovery), or the local filesystem (for firmware binaries). A desktop application is required for hardware management. Tauri v2 is the natural choice because:
1. **Rust backend** — integrates directly with the existing Rust workspace (`wifi-densepose-rs`). Crates like `wifi-densepose-hardware` (serial port parsing), `wifi-densepose-config`, and `wifi-densepose-sensing-server` can be linked as library dependencies.
2. **Small binary** — Tauri bundles the system webview rather than shipping Chromium (~150 MB savings vs Electron).
3. **Cross-platform** — Windows, macOS, Linux from the same codebase.
4. **Security model** — Tauri's capability-based permissions system restricts frontend access to explicitly allowed Rust commands.
### Why Not Electron / Flutter / Native
| Option | Rejected Because |
|--------|-----------------|
| Electron | 150+ MB bundle, no Rust integration, duplicates webview |
| Flutter | No serial port plugins, Dart FFI to Rust is awkward |
| Native (GTK/Qt) | Platform-specific UI code, no web component reuse |
| Web-only (PWA) | Cannot access serial ports or raw UDP |
## Decision
Build a Tauri v2 desktop application as a new crate in the Rust workspace. The frontend uses TypeScript with React and Vite. The Rust backend exposes Tauri commands that bridge the frontend to serial ports, UDP sockets, HTTP management endpoints, and the sensing server process.
### 1. Workspace Integration
Add a new crate to the workspace:
```
rust-port/wifi-densepose-rs/
Cargo.toml # Add "crates/wifi-densepose-desktop" to members
crates/
wifi-densepose-desktop/ # NEW — Tauri app crate
Cargo.toml
tauri.conf.json
capabilities/
default.json # Tauri v2 capability permissions
icons/ # App icons (all platforms)
src/
main.rs # Tauri entry point
lib.rs # Command module re-exports
commands/
mod.rs
discovery.rs # Node discovery commands
flash.rs # Firmware flashing commands
ota.rs # OTA update commands
wasm.rs # WASM module management commands
server.rs # Sensing server lifecycle commands
provision.rs # NVS provisioning commands
serial.rs # Serial port enumeration
state.rs # Tauri managed state
discovery/
mod.rs
mdns.rs # mDNS service discovery
udp_broadcast.rs # UDP broadcast probe
flash/
mod.rs
espflash.rs # Rust-native ESP32 flashing (via espflash crate)
esptool.rs # Fallback: bundled esptool.py wrapper
frontend/
package.json
tsconfig.json
vite.config.ts
index.html
src/
main.tsx
App.tsx
routes.tsx
hooks/
useNodes.ts # Node discovery and status polling
useServer.ts # Sensing server state
useWebSocket.ts # WS connection to sensing server
stores/
nodeStore.ts # Zustand store for discovered nodes
serverStore.ts # Sensing server process state
settingsStore.ts # User preferences (dark mode, ports)
pages/
Dashboard.tsx # Hardware management overview
NodeDetail.tsx # Single node detail + config
FlashFirmware.tsx # Firmware flashing wizard
WasmModules.tsx # WASM module manager
SensingView.tsx # Live sensing data visualization
MeshTopology.tsx # Multi-node mesh topology view
Settings.tsx # App settings and preferences
components/
NodeCard.tsx # Node status card (health, version, signal)
NodeList.tsx # Discovered node list
FirmwareProgress.tsx # Flash/OTA progress indicator
LogViewer.tsx # Scrolling log output
SignalChart.tsx # Real-time CSI signal chart
PoseOverlay.tsx # Pose skeleton overlay
MeshGraph.tsx # D3/force-graph mesh topology
SerialPortSelect.tsx # Serial port dropdown
ProvisionForm.tsx # NVS provisioning form
lib/
tauri.ts # Typed Tauri invoke wrappers
types.ts # Shared TypeScript types
```
### 2. Rust Backend — Tauri Commands
#### 2.1 Node Discovery
```rust
// commands/discovery.rs
/// Discover ESP32 CSI nodes on the local network.
/// Strategy 1: mDNS — nodes announce _ruview._tcp service
/// Strategy 2: UDP broadcast probe on port 5005 (CSI aggregator port)
/// Strategy 3: HTTP health check sweep on port 8032 (OTA server)
#[tauri::command]
async fn discover_nodes(timeout_ms: u64) -> Result<Vec<DiscoveredNode>, String>;
/// Get detailed status from a specific node via HTTP.
/// Calls GET /ota/status on port 8032.
#[tauri::command]
async fn get_node_status(ip: String) -> Result<NodeStatus, String>;
/// Subscribe to node health updates (periodic polling).
#[tauri::command]
async fn watch_nodes(interval_ms: u64, state: State<'_, AppState>) -> Result<(), String>;
```
The `DiscoveredNode` struct:
```rust
#[derive(Serialize, Deserialize, Clone)]
pub struct DiscoveredNode {
pub ip: String,
pub mac: Option<String>,
pub hostname: Option<String>,
pub node_id: u8,
pub firmware_version: Option<String>,
pub tdm_slot: Option<u8>,
pub tdm_total: Option<u8>,
pub edge_tier: Option<u8>,
pub uptime_secs: Option<u64>,
pub discovery_method: DiscoveryMethod, // Mdns | UdpProbe | HttpSweep
pub last_seen: chrono::DateTime<chrono::Utc>,
}
```
#### 2.2 Firmware Flashing
```rust
// commands/flash.rs
/// List available serial ports with chip detection.
#[tauri::command]
async fn list_serial_ports() -> Result<Vec<SerialPortInfo>, String>;
/// Flash firmware binary to an ESP32 via serial port.
/// Uses the `espflash` crate for Rust-native flashing (no Python dependency).
/// Falls back to bundled esptool.py if espflash fails.
/// Emits progress events via Tauri event system.
#[tauri::command]
async fn flash_firmware(
port: String,
firmware_path: String,
chip: Chip, // Esp32, Esp32s3, Esp32c3
baud: Option<u32>,
app_handle: AppHandle,
) -> Result<FlashResult, String>;
/// Read firmware info from a connected ESP32 (chip type, flash size, MAC).
#[tauri::command]
async fn read_chip_info(port: String) -> Result<ChipInfo, String>;
```
Flash progress is emitted as Tauri events:
```rust
#[derive(Serialize, Clone)]
pub struct FlashProgress {
pub phase: FlashPhase, // Connecting | Erasing | Writing | Verifying
pub progress_pct: f32, // 0.0 - 100.0
pub bytes_written: u64,
pub bytes_total: u64,
pub speed_bps: u64,
}
```
#### 2.3 OTA Updates
```rust
// commands/ota.rs
/// Push firmware to a node via HTTP OTA (port 8032).
/// Includes PSK authentication per ADR-050.
#[tauri::command]
async fn ota_update(
node_ip: String,
firmware_path: String,
psk: Option<String>,
app_handle: AppHandle,
) -> Result<OtaResult, String>;
/// Get OTA status from a node (current version, partition info).
#[tauri::command]
async fn ota_status(node_ip: String, psk: Option<String>) -> Result<OtaStatus, String>;
/// Batch OTA update — push firmware to multiple nodes sequentially.
/// Skips nodes already running the target version.
#[tauri::command]
async fn ota_batch_update(
nodes: Vec<String>, // IPs
firmware_path: String,
psk: Option<String>,
app_handle: AppHandle,
) -> Result<Vec<OtaResult>, String>;
```
#### 2.4 WASM Module Management
```rust
// commands/wasm.rs
/// List WASM modules loaded on a node.
/// Calls GET /wasm/list on port 8032.
#[tauri::command]
async fn wasm_list(node_ip: String) -> Result<Vec<WasmModule>, String>;
/// Upload a WASM module to a node.
/// Calls POST /wasm/upload on port 8032 with binary payload.
#[tauri::command]
async fn wasm_upload(
node_ip: String,
wasm_path: String,
app_handle: AppHandle,
) -> Result<WasmUploadResult, String>;
/// Start/stop a WASM module on a node.
#[tauri::command]
async fn wasm_control(
node_ip: String,
module_id: String,
action: WasmAction, // Start | Stop | Unload
) -> Result<(), String>;
```
#### 2.5 Sensing Server Lifecycle
```rust
// commands/server.rs
/// Start the sensing server as a managed child process.
/// The server binary is either bundled with the Tauri app (sidecar)
/// or discovered on PATH.
#[tauri::command]
async fn start_server(
config: ServerConfig,
state: State<'_, AppState>,
app_handle: AppHandle,
) -> Result<(), String>;
/// Stop the managed sensing server process.
#[tauri::command]
async fn stop_server(state: State<'_, AppState>) -> Result<(), String>;
/// Get sensing server status (running/stopped, PID, ports, uptime).
#[tauri::command]
async fn server_status(state: State<'_, AppState>) -> Result<ServerStatus, String>;
#[derive(Serialize, Deserialize, Clone)]
pub struct ServerConfig {
pub http_port: u16, // Default: 8080
pub ws_port: u16, // Default: 8765
pub udp_port: u16, // Default: 5005
pub static_dir: Option<String>, // Path to UI static files
pub model_dir: Option<String>, // Path to ML models
pub log_level: String, // trace, debug, info, warn, error
}
```
The sensing server is bundled as a Tauri sidecar binary. Tauri v2 supports sidecar binaries via `externalBin` in `tauri.conf.json`:
```json
{
"bundle": {
"externalBin": ["sensing-server"]
}
}
```
#### 2.6 NVS Provisioning
```rust
// commands/provision.rs
/// Provision NVS configuration to an ESP32 via serial port.
/// Replaces the Python provision.py script with a Rust-native implementation.
/// Generates NVS partition binary and flashes it to the NVS partition offset.
#[tauri::command]
async fn provision_node(
port: String,
config: NvsConfig,
app_handle: AppHandle,
) -> Result<ProvisionResult, String>;
/// Read current NVS configuration from a connected ESP32.
/// Reads the NVS partition and parses key-value pairs.
#[tauri::command]
async fn read_nvs(port: String) -> Result<NvsConfig, String>;
#[derive(Serialize, Deserialize, Clone)]
pub struct NvsConfig {
pub wifi_ssid: Option<String>,
pub wifi_password: Option<String>,
pub target_ip: Option<String>,
pub target_port: Option<u16>,
pub node_id: Option<u8>,
pub tdm_slot: Option<u8>,
pub tdm_total: Option<u8>,
pub edge_tier: Option<u8>,
pub presence_thresh: Option<u16>,
pub fall_thresh: Option<u16>,
pub vital_window: Option<u16>,
pub vital_interval_ms: Option<u16>,
pub top_k_count: Option<u8>,
pub hop_count: Option<u8>,
pub channel_list: Option<Vec<u8>>,
pub dwell_ms: Option<u32>,
pub power_duty: Option<u8>,
pub wasm_max_modules: Option<u8>,
pub wasm_verify: Option<bool>,
pub wasm_pubkey: Option<Vec<u8>>,
pub ota_psk: Option<String>,
}
```
### 3. Frontend Architecture
#### 3.1 Tech Stack
| Layer | Choice | Rationale |
|-------|--------|-----------|
| Framework | React 19 | Component model, ecosystem, team familiarity |
| Build | Vite 6 | Fast HMR, Tauri plugin support |
| State | Zustand | Lightweight, no boilerplate, works with Tauri events |
| Routing | React Router v7 | File-based routes, type-safe |
| UI Components | shadcn/ui + Tailwind CSS | Accessible, customizable, no runtime CSS-in-JS |
| Charts | Recharts or visx | Real-time signal visualization |
| Topology Graph | D3 force-directed | Mesh network visualization |
| Serial UI | Custom | Tauri command integration |
| Icons | Lucide React | Consistent, tree-shakeable |
#### 3.2 Page Layout
```
+------------------------------------------+
| RuView [Settings] [?] |
+-------+----------------------------------+
| | |
| Nav | Dashboard / Active Page |
| | |
| [D] | +--------+ +--------+ +------+ |
| [F] | | Node 1 | | Node 2 | | +Add | |
| [W] | +--------+ +--------+ +------+ |
| [S] | |
| [M] | Server Status: Running |
| [T] | +--------------------------+ |
| | | Live Signal / Pose View | |
| | +--------------------------+ |
+-------+----------------------------------+
| Status Bar: 3 nodes | Server: :8080 |
+------------------------------------------+
Nav items:
[D] Dashboard — overview of all nodes and server
[F] Flash — firmware flashing wizard
[W] WASM — edge module management
[S] Sensing — live sensing data view
[M] Mesh — topology visualization
[T] Settings — ports, paths, preferences
```
#### 3.3 Dashboard Page
The dashboard is the primary landing page showing:
1. **Node Grid** — cards for each discovered ESP32 node showing:
- IP address and hostname
- Firmware version (with update indicator if newer available)
- Node ID and TDM slot assignment
- Edge processing tier (raw / stats / vitals)
- Signal quality indicator (last CSI frame age)
- Health status (online/offline/degraded)
- Quick actions: OTA update, configure, view logs
2. **Sensing Server Panel** — start/stop button, port configuration, log tail
3. **Discovery Controls** — scan button, auto-discovery toggle, network range filter
#### 3.4 Flash Firmware Page
A wizard-style flow:
1. **Select Port** — dropdown of detected serial ports with chip info
2. **Select Firmware** — file picker for `.bin` files, or select from bundled builds
3. **Configure** — chip type, baud rate, flash mode
4. **Flash** — progress bar with phase indicators (connecting, erasing, writing, verifying)
5. **Provision** — optional NVS provisioning form (WiFi, target IP, TDM, edge tier)
6. **Verify** — serial monitor showing boot log, success/fail indicator
#### 3.5 WASM Module Manager Page
| Column | Content |
|--------|---------|
| Module ID | Auto-assigned by node |
| Name | Filename of uploaded `.wasm` |
| Size | Module size in KB |
| Status | Running / Stopped / Error |
| Node | Which ESP32 node it runs on |
| Actions | Start / Stop / Unload / View Logs |
Upload panel: drag-and-drop `.wasm` file, select target node(s), upload button.
#### 3.6 Sensing View Page
Embeds the existing web UI (`ui/`) via an iframe pointing at the sensing server's static file route, or builds native React components that connect to the same WebSocket API. The native approach is preferred because it allows:
- Tighter integration with the node status sidebar
- Shared state between hardware management and visualization
- Offline access to recorded data
Key visualization components:
- **CSI Heatmap** — subcarrier amplitude over time
- **Signal Field** — 2D signal strength visualization
- **Pose Skeleton** — detected body keypoints and connections
- **Vital Signs** — real-time breathing rate and heart rate charts
- **Activity Classification** — current activity label with confidence
#### 3.7 Mesh Topology Page
A force-directed graph showing:
- Nodes as circles (color = health status, size = edge tier)
- Edges between nodes that can see each other
- TDM slot labels on each node
- Sync status indicators (in-sync / drifting / lost)
- Click a node to navigate to its detail page
### 4. Platform-Specific Considerations
#### 4.1 macOS
- **Serial driver signing**: CP210x and CH340 drivers require user approval in System Preferences > Security
- **App signing**: Tauri apps must be signed and notarized for distribution outside the App Store
- **USB permissions**: No special permissions needed beyond driver installation
- **CoreWLAN**: The sensing server can use CoreWLAN for WiFi scanning (ADR-025); the desktop app inherits this capability
#### 4.2 Windows
- **COM port access**: Windows assigns COM port numbers; the app lists them via the Windows Registry or `SetupDi` API
- **Driver installation**: USB-to-serial drivers (CP210x, CH340, FTDI) must be installed; the app can detect missing drivers and link to downloads
- **Firewall**: The sensing server's UDP listener may trigger Windows Firewall prompts; the app should pre-configure rules or guide the user
- **Code signing**: EV certificate required for SmartScreen trust; unsigned apps trigger warnings
#### 4.3 Linux
- **udev rules**: ESP32 serial ports (`/dev/ttyUSB*`, `/dev/ttyACM*`) require udev rules for non-root access. The app bundles a `99-ruview-esp32.rules` file and offers to install it:
```
SUBSYSTEM=="tty", ATTRS{idVendor}=="10c4", MODE="0666" # CP210x
SUBSYSTEM=="tty", ATTRS{idVendor}=="1a86", MODE="0666" # CH340
```
- **AppImage/deb/rpm**: Tauri supports all three packaging formats
- **Wayland vs X11**: Tauri uses webkit2gtk which works on both
### 5. Cargo.toml for the Desktop Crate
```toml
[package]
name = "wifi-densepose-desktop"
version.workspace = true
edition.workspace = true
description = "Tauri desktop frontend for RuView WiFi DensePose"
license.workspace = true
authors.workspace = true
[lib]
name = "wifi_densepose_desktop"
crate-type = ["staticlib", "cdylib", "rlib"]
[build-dependencies]
tauri-build = { version = "2", features = [] }
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-shell = "2" # Sidecar process management
tauri-plugin-dialog = "2" # File picker dialogs
tauri-plugin-fs = "2" # Filesystem access
tauri-plugin-process = "2" # Process management
tauri-plugin-notification = "2" # Desktop notifications
# Workspace crates
wifi-densepose-hardware = { workspace = true }
wifi-densepose-config = { workspace = true }
wifi-densepose-core = { workspace = true }
# Serial port access
serialport = { workspace = true }
# ESP32 flashing (Rust-native, replaces esptool.py)
espflash = "3"
# Network discovery
mdns-sd = "0.11" # mDNS/DNS-SD service discovery
# HTTP client for OTA and WASM management
reqwest = { version = "0.12", features = ["json", "multipart", "stream"] }
# Async runtime
tokio = { workspace = true }
# Serialization
serde = { workspace = true }
serde_json = { workspace = true }
# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
# Time
chrono = { version = "0.4", features = ["serde"] }
```
### 6. Tauri Configuration
```json
{
"$schema": "https://raw.githubusercontent.com/tauri-apps/tauri/dev/crates/tauri-config-schema/schema.json",
"productName": "RuView",
"version": "0.3.0",
"identifier": "net.ruv.ruview",
"build": {
"frontendDist": "../frontend/dist",
"devUrl": "http://localhost:5173",
"beforeDevCommand": "cd frontend && npm run dev",
"beforeBuildCommand": "cd frontend && npm run build"
},
"app": {
"windows": [
{
"title": "RuView - WiFi DensePose",
"width": 1280,
"height": 800,
"minWidth": 900,
"minHeight": 600
}
]
},
"bundle": {
"active": true,
"targets": "all",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
],
"externalBin": ["sensing-server"],
"linux": {
"deb": { "depends": ["libwebkit2gtk-4.1-0"] },
"appimage": { "bundleMediaFramework": true }
},
"windows": {
"wix": { "language": "en-US" }
}
}
}
```
### 7. Tauri v2 Capabilities (Permissions)
```json
{
"identifier": "default",
"description": "RuView default capability set",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-execute",
"shell:allow-open",
"dialog:allow-open",
"dialog:allow-save",
"fs:allow-read",
"fs:allow-write",
"process:allow-exit",
"notification:default"
]
}
```
### 8. Development Workflow
```bash
# Prerequisites
cargo install tauri-cli@^2
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop/frontend
npm install
# Development (hot-reload frontend + Rust rebuild)
cd rust-port/wifi-densepose-rs/crates/wifi-densepose-desktop
cargo tauri dev
# Production build
cargo tauri build
# Build sensing-server sidecar (must be done before tauri build)
cargo build --release -p wifi-densepose-sensing-server
# Copy to sidecar location:
# target/release/sensing-server -> crates/wifi-densepose-desktop/binaries/sensing-server-{arch}
```
### 9. Persistent Node Registry
Discovery alone is transient — nodes appear when they broadcast, disappear when they don't. A persistent local registry transforms discovery into **reconciliation**.
```
~/.ruview/nodes.db (SQLite via rusqlite)
```
**Schema:**
```sql
CREATE TABLE nodes (
mac TEXT PRIMARY KEY, -- e.g. "AA:BB:CC:DD:EE:FF"
last_ip TEXT, -- last known IP
last_seen INTEGER NOT NULL, -- Unix timestamp
firmware TEXT, -- e.g. "0.3.1"
chip TEXT DEFAULT 'esp32s3', -- esp32, esp32s3, esp32c3
mesh_role TEXT DEFAULT 'node', -- 'coordinator' | 'node' | 'aggregator'
tdm_slot INTEGER, -- assigned TDM slot index
capabilities TEXT, -- JSON: {"wasm": true, "ota": true, "csi": true}
friendly_name TEXT, -- user-assigned label
notes TEXT -- free-form notes
);
```
**Behavior:**
- On discovery broadcast, upsert into registry (update `last_ip`, `last_seen`, `firmware`)
- Dashboard shows **all registered nodes**, dimming those not seen recently
- User can manually add nodes by MAC/IP (for networks without mDNS)
- Export/import registry as JSON for fleet management across machines
- Node health history (uptime, last OTA, error count) tracked over time
This means the desktop app **remembers the mesh** across restarts, which is critical for field deployments where nodes may be offline temporarily.
### 10. OTA Safety Gate — Rolling Updates
Mesh deployments cannot tolerate all nodes rebooting simultaneously. The OTA subsystem includes a **rolling update mode** that preserves sensing continuity:
```rust
#[derive(Serialize, Deserialize)]
pub struct BatchOtaConfig {
/// Update strategy
pub strategy: OtaStrategy,
/// Max nodes updating concurrently
pub max_concurrent: usize,
/// Delay between batches (seconds)
pub batch_delay_secs: u64,
/// Abort if any node fails
pub fail_fast: bool,
}
#[derive(Serialize, Deserialize)]
pub enum OtaStrategy {
/// Update one node at a time, wait for it to rejoin mesh
Sequential,
/// Update non-adjacent TDM slots to maintain coverage
TdmSafe,
/// Update all nodes simultaneously (development only)
Parallel,
}
```
**`TdmSafe` strategy:**
1. Sort nodes by TDM slot index
2. Update even-slot nodes first (slots 0, 2, 4...)
3. Wait for each to reboot and rejoin mesh (verified via beacon)
4. Then update odd-slot nodes (slots 1, 3, 5...)
5. At no point are adjacent nodes offline simultaneously
**UI flow:**
- User selects target firmware + target nodes
- App shows pre-update diff (current vs new version per node)
- Progress bar per node with states: `queued → uploading → rebooting → verifying → done`
- Abort button halts remaining updates without rolling back completed ones
- Post-update health check confirms all nodes are sensing
### 11. Plugin Architecture (Future)
This desktop tool is quietly becoming the **control plane for RuView**. Once it manages discovery, firmware, OTA, WASM, sensing, and mesh topology, plugin extensibility becomes inevitable:
- **Firmware management** today → **swarm orchestration** tomorrow
- **WASM upload** today → **edge module marketplace** tomorrow
- **Sensing view** today → **activity classification dashboard** tomorrow
The Tauri command surface should be designed with this trajectory in mind:
- Commands are grouped by bounded context (already done)
- Each context can be extended by loading additional Tauri plugins
- The node registry becomes the source of truth for all plugins
- Event bus (Tauri's `emit`/`listen`) provides cross-plugin communication
This does NOT mean building a plugin system in Phase 1. It means keeping the architecture open to it: no hardcoded views, state flows through the registry, commands are typed and versioned.
### 12. Security Considerations
1. **PSK Storage**: OTA PSK tokens are stored in the OS keychain via `tauri-plugin-stronghold` or the platform's native credential store, never in plaintext config files.
2. **Serial Port Access**: Tauri's capability system restricts which commands the frontend can invoke. Serial port access is only available through the typed `flash_firmware` and `provision_node` commands, not raw serial I/O.
3. **Network Requests**: OTA and WASM management commands only communicate with nodes on the local network. The app does not make external network requests except for update checks (opt-in).
4. **Firmware Validation**: Before flashing, the app validates the firmware binary header (ESP32 image magic bytes, partition table offset) to prevent bricking.
5. **WASM Signature Verification**: The desktop app can sign WASM modules before upload using a locally stored Ed25519 key pair, complementing the node-side verification (ADR-040).
### 13. Implementation Phases
| Phase | Scope | Effort | Priority |
|-------|-------|--------|----------|
| **Phase 1: Skeleton** | Tauri project scaffolding, workspace integration, basic window with React | 1 week | P0 |
| **Phase 2: Discovery** | Serial port listing, UDP/mDNS node discovery, dashboard with node cards | 1 week | P0 |
| **Phase 3: Flash** | espflash integration, firmware flashing wizard with progress events | 1 week | P0 |
| **Phase 4: Server** | Sidecar sensing server start/stop, log viewer, status panel | 1 week | P1 |
| **Phase 5: OTA** | HTTP OTA with PSK auth, batch update, version comparison | 1 week | P1 |
| **Phase 6: Provisioning** | NVS read/write via serial, provisioning form, mesh config file | 1 week | P1 |
| **Phase 7: WASM** | Module upload/list/start/stop, drag-and-drop, per-module logs | 1 week | P2 |
| **Phase 8: Sensing** | WebSocket integration, live signal charts, pose overlay | 2 weeks | P2 |
| **Phase 9: Mesh View** | Force-directed topology graph, TDM slot visualization, sync status | 1 week | P2 |
| **Phase 10: Polish** | App signing, auto-update, udev rules installer, onboarding wizard | 1 week | P3 |
Total estimated effort: ~11 weeks for a single developer.
## Consequences
### Positive
- **Single pane of glass** — all hardware management, sensing, and visualization in one app
- **No Python dependency** — Rust-native `espflash` replaces `esptool.py` for firmware flashing
- **Replaces 6+ CLI tools** — flash, provision, OTA, WASM management, server control, visualization
- **Accessible to non-developers** — GUI replaces CLI flags and curl commands
- **Cross-platform** — one codebase for Windows, macOS, Linux
- **Workspace integration** — shares types, config, and hardware crates with sensing server
- **Small binary** — ~15-20 MB vs ~150 MB for Electron equivalent
### Negative
- **New frontend dependency** — introduces Node.js/npm build step into the Rust workspace
- **Tauri version churn** — Tauri v2 is recent; API stability is not yet proven at scale
- **webkit2gtk on Linux** — depends on system webview version; old distros may have stale webkit
- **espflash limitations** — the `espflash` crate may not support all chip variants or flash modes that `esptool.py` handles; fallback to bundled Python is needed
- **Maintenance surface** — adds ~5,000 lines of TypeScript and ~2,000 lines of Rust
### Risks
| Risk | Likelihood | Impact | Mitigation |
|------|-----------|--------|------------|
| espflash cannot flash all ESP32 variants | Medium | High | Bundle esptool.py as fallback sidecar |
| Tauri v2 breaking changes | Low | Medium | Pin to specific Tauri version; update in dedicated PRs |
| Serial port access fails on macOS Sequoia+ | Medium | Medium | Test on latest macOS; document driver requirements |
| webkit2gtk version mismatch on Linux | Medium | Low | Set minimum version in deb/rpm dependencies |
| Sidecar sensing server fails to start | Low | Medium | Detect failure and show manual start instructions |
## References
- Tauri v2 documentation: https://v2.tauri.app/
- espflash crate: https://crates.io/crates/espflash
- mdns-sd crate: https://crates.io/crates/mdns-sd
- ADR-012: ESP32 CSI Sensor Mesh
- ADR-039: ESP32 Edge Intelligence
- ADR-040: WASM Programmable Sensing
- ADR-044: Provisioning Tool Enhancements
- ADR-050: Quality Engineering — Security Hardening
- ADR-051: Sensing Server Decomposition
- `firmware/esp32-csi-node/` — ESP32 firmware source
- `firmware/esp32-csi-node/provision.py` — Current provisioning script
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-sensing-server/` — Sensing server
- `rust-port/wifi-densepose-rs/crates/wifi-densepose-hardware/` — Hardware crate
- `ui/` — Existing web UI
+274
View File
@@ -0,0 +1,274 @@
# ADR-053: UI Design System — Dark Professional + Unity-Inspired Interface
| Field | Value |
|-------|-------|
| Status | Accepted |
| Date | 2026-03-06 |
| Deciders | ruv |
| Depends on | ADR-052 (Tauri Desktop Frontend) |
## Context
RuView Desktop (ADR-052) needs a UI design system that communicates precision and control — befitting a hardware management control plane for embedded sensing infrastructure. The interface must handle dense data (CSI heatmaps, node registries, log streams, mesh topologies) without feeling overwhelming, while remaining usable by both engineers and field operators.
Two design inspirations:
1. **Data-first professional tools** — Dense information displays where data speaks for itself. Clean typography, structured layouts, and deliberate use of color for status. The interface shows what matters and hides what doesn't. Think: network monitoring dashboards, embedded systems IDEs, infrastructure control panels.
2. **Unity Editor** — Dockable panel system, inspector/hierarchy/scene separation, property grids, dark professional theme, and dense-but-organized data display. Unity's UI is purpose-built for managing complex real-time systems — exactly what RuView needs.
The combination yields a professional control panel for WiFi sensing infrastructure. Data is organized into scannable panels with clear hierarchy. Status is communicated through consistent color coding. The layout adapts from high-level overview down to individual node details through progressive disclosure.
## Decision
### Design Principles
1. **Data is the interface** — The system reveals patterns through visualization, not through explanation. Every pixel earns its place.
2. **Precision typography** — Typography is clean and authoritative. Technical values are displayed without ambiguity. Labels are concise.
3. **Panel-based layout** — Dockable regions inspired by Unity's panel system. The operator can see the entire mesh at a glance, then drill into any node.
4. **Status through color** — Deliberate color coding: green (online), amber (degraded), red (offline/failed), blue (scanning/new). No gratuitous color.
5. **Progressive disclosure** — Dashboard shows the overview. Clicking a node reveals its details. Summary first, detail on interaction.
6. **Dual typography** — Monospace for all technical values (MAC addresses, firmware versions, CSI amplitudes). Sans-serif for labels and descriptions. The contrast signals "data vs. context."
7. **Powered by rUv** — Subtle branding: footer tagline, about dialog, splash screen.
### Color System
```css
:root {
/* Background layers */
--bg-base: #0d1117; /* App background */
--bg-surface: #161b22; /* Panel backgrounds */
--bg-elevated: #1c2333; /* Cards, modals, dropdowns */
--bg-hover: #242d3d; /* Hover state */
--bg-active: #2d3748; /* Active/selected state */
/* Text hierarchy */
--text-primary: #e6edf3; /* Headings, primary content */
--text-secondary: #8b949e; /* Labels, descriptions */
--text-muted: #484f58; /* Disabled, hints, placeholders */
/* Status indicators */
--status-online: #3fb950; /* Node online, healthy */
--status-warning: #d29922; /* Degraded, needs attention */
--status-error: #f85149; /* Offline, failed, critical */
--status-info: #58a6ff; /* Scanning, discovering, info */
/* Accent */
--accent: #7c3aed; /* rUv purple — primary actions */
--accent-hover: #6d28d9;
/* Borders */
--border: #30363d;
--border-active: #58a6ff;
/* Data display */
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
}
```
### Typography Scale
```css
/* Typographic hierarchy */
.heading-xl { font: 600 28px/1.2 var(--font-sans); } /* Page titles */
.heading-lg { font: 600 20px/1.3 var(--font-sans); } /* Section titles */
.heading-md { font: 600 16px/1.4 var(--font-sans); } /* Card titles */
.heading-sm { font: 600 13px/1.4 var(--font-sans); } /* Panel labels */
.body { font: 400 14px/1.6 var(--font-sans); } /* Body text */
.body-sm { font: 400 12px/1.5 var(--font-sans); } /* Captions */
.data { font: 400 13px/1.4 var(--font-mono); } /* Technical values */
.data-lg { font: 500 18px/1.2 var(--font-mono); } /* Key metrics */
```
### Layout System
Three-region layout: navigation sidebar, node list, and detail inspector. Unity's docking system provides the mechanical framework.
```
+--[ Sidebar ]--+--[ Main ]-------------------------------------+
| | |
| [Nav Items] | +--[ Command Bar ]---------------------------+ |
| | | Breadcrumb | Actions | Search | |
| Dashboard | +-------+-----------------------------------+ |
| Nodes | | | | |
| Flash | | Node | Detail Inspector | |
| OTA | | List | (selected node properties) | |
| Edge Modules | | | | |
| Sensing | | | [Property Grid] | |
| Mesh View | | | [Status Indicators] | |
| Settings | | | [Action Buttons] | |
| | | | | |
+-[ Status Bar ]+--+-------+-----------------------------------+ |
| rUv | 3 nodes online | Server: running | Port: 8080 |
+---------------------------------------------------------------+
```
**Panel behaviors:**
- Sidebar collapses to icon-only on narrow windows
- Node List / Inspector split is resizable via drag handle
- Inspector scrolls independently — drill into any node without losing the list
- Status Bar shows global system state at a glance (node count, server status, port)
### Component Library
#### 1. NodeCard
```
+-- NodeCard -----------------------------------------------+
| [●] ESP32-S3 Node #2 firmware: 0.3.1 |
| MAC: AA:BB:CC:DD:EE:FF TDM Slot: 2/4 |
| IP: 192.168.1.42 Edge Tier: 1 |
| Last seen: 3s ago [Flash] [OTA] [···] |
+-----------------------------------------------------------+
```
Status dot uses `--status-online/warning/error`. Card background shifts on hover.
#### 2. FlashProgress
```
+-- Flash Progress -----------------------------------------+
| Flashing firmware to COM3 (ESP32-S3) |
| |
| Phase: Writing |
| [████████████████████░░░░░░░░░░] 67.3% |
| 412 KB / 612 KB • 38.2 KB/s • ~5s remaining |
+-----------------------------------------------------------+
```
Progress bar uses `--accent` fill with subtle pulse animation during active writes.
#### 3. Mesh Topology View (Three.js)
Interactive 3D visualization of the sensing network. Each node is a sphere. Edges are lines representing signal paths. The coordinator node is visually distinct (larger, outlined ring). Built with **Three.js**, consistent with the existing visualization stack in `ui/observatory/js/` and `ui/components/`.
```
+-- Mesh Topology ------------------------------------------+
| |
| [Node 0]----[Node 1] |
| | \ / | |
| | [Coordinator] | Coordinator = TDM master |
| | / \ | |
| [Node 2]----[Node 3] |
| |
| Drift: ±0.3ms | Cycle: 50ms | 4/4 nodes online |
+-----------------------------------------------------------+
```
**Three.js implementation details:**
- Force-directed layout computed on CPU, rendered as `THREE.Group` with `THREE.Mesh` (spheres) and `THREE.Line` (edges)
- Node spheres use `THREE.MeshPhongMaterial` with emissive color matching `--status-online/warning/error`
- Edge lines use `THREE.LineBasicMaterial` with opacity mapped to signal strength
- Coordinator node rendered with `THREE.RingGeometry` outline
- Camera: `OrbitControls` for pan/zoom/rotate, reset button returns to default view
- Follows existing patterns: `BufferGeometry` + `BufferAttribute` for dynamic updates (see `ui/observatory/js/subcarrier-manifold.js`)
- Raycasting for node click → opens detail in Inspector panel
- Real-time updates as nodes join, leave, or change status — geometry attributes updated per frame
#### 4. PropertyGrid (Unity Inspector-style)
```
+-- Node Inspector -----------------------------------------+
| General [▼] |
| MAC Address AA:BB:CC:DD:EE:FF |
| IP Address 192.168.1.42 |
| Firmware 0.3.1 |
| Chip ESP32-S3 |
| TDM Configuration [▼] |
| Slot Index 2 |
| Total Nodes 4 |
| Cycle Period 50 ms |
| Sync Drift +0.12 ms |
| WASM Modules [▼] |
| [0] activity_detect running 12.4 KB 83 us/f |
| [1] vital_monitor stopped 8.1 KB — us/f |
+-----------------------------------------------------------+
```
Collapsible sections with alternating row backgrounds for scanability.
#### 5. StatusBadge
```
[● Online] [◐ Degraded] [○ Offline] [↻ Updating]
```
Small inline badges with status dot, label, and optional tooltip.
#### 6. LogViewer
```
+-- Server Log (auto-scroll) -----------[ Clear ] [ ⏸ ]---+
| 19:42:01.234 INFO sensing-server HTTP on 127.0.0.1:8080|
| 19:42:01.235 INFO sensing-server WS on 127.0.0.1:8765 |
| 19:42:01.890 INFO udp_receiver CSI frame from .42 |
| 19:42:02.003 WARN vital_signs Low signal quality |
+-----------------------------------------------------------+
```
Monospace, color-coded by log level (INFO=text, WARN=amber, ERROR=red). Virtual scrolling for performance.
### Spacing and Grid
```css
/* 4px base grid */
--space-1: 4px; /* Tight spacing (within components) */
--space-2: 8px; /* Component internal padding */
--space-3: 12px; /* Between related elements */
--space-4: 16px; /* Card padding, section gaps */
--space-5: 24px; /* Between sections */
--space-6: 32px; /* Page-level spacing */
--space-8: 48px; /* Major section breaks */
/* Panel dimensions */
--sidebar-width: 220px;
--sidebar-collapsed: 52px;
--statusbar-height: 28px;
--toolbar-height: 44px;
```
### Animations
Minimal and purposeful:
- Panel collapse/expand: 200ms ease-out
- Node card health transition: 300ms (color fade, not flash)
- Progress bar fill: smooth 60fps CSS transition
- Mesh graph: Three.js render loop at 60fps, force simulation on requestAnimationFrame
- No loading spinners — use skeleton placeholders instead
### Branding
- **Splash screen**: rUv logo + "RuView Desktop" + version, 1.5s duration
- **Status bar**: "Powered by rUv" in `--text-muted`, left-aligned
- **About dialog**: rUv logo, version, license, links to GitHub and docs
- **App icon**: Stylized WiFi signal + human silhouette in rUv purple (#7c3aed)
## Consequences
### Positive
- Professional, data-dense UI suitable for hardware management
- Consistent design language across all 7 pages
- Dual typography (mono + sans-serif) ensures readability at all information densities
- Unity-inspired panels feel natural to engineers familiar with IDE/editor tools
- Dark theme reduces eye strain for extended monitoring sessions
### Negative
- Custom design system means no off-the-shelf component library (shadcn/ui partially usable)
- Dockable panels add complexity to the layout system
- Dark-only theme may not suit all users (could add light mode later)
### Neutral
- The design system is CSS-only with React components — no heavy UI framework dependency
- Component library can be extracted as a separate package if other rUv projects need it
## References
- ADR-052: Tauri Desktop Frontend
- Unity Editor UI Guidelines: https://docs.unity3d.com/Manual/UIE-USS.html
- Three.js (existing project dependency): `ui/observatory/js/`, `ui/components/`
- Inter font: https://rsms.me/inter/
- JetBrains Mono: https://www.jetbrains.com/lp/mono/