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.
+-------------------+ +---------------------+ +--------------------+
| | | | | |
| 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)
Purpose: Find, identify, and monitor ESP32 CSI nodes on the local network.
Upstream of: Firmware Management, Configuration, Sensing Pipeline, Visualization
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.
| 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) |
MacAddress— 6-byte hardware address, formatted asAA:BB:CC:DD:EE:FFHealthStatus— enum:Online,Offline,Degraded(reason: String)DiscoveryMethod— enum:Mdns,UdpProbe,HttpSweep,ManualTdmConfig—{ slot_index: u8, total_nodes: u8 }SemVer— semantic versionmajor.minor.patch
| 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) |
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:
/// 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
}
}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)
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.
| 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 |
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 |
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),SkippedOtaStrategy— enum:Sequential— one node at a time, wait for rejoinTdmSafe— update non-adjacent slots to maintain sensing coverageParallel— all at once (development only)
SerialPort—{ name: String, vid: u16, pid: u16, manufacturer: Option<String> }ChipType— enum:Esp32,Esp32s3,Esp32c3FlashPhase— enum:Connecting,Erasing,Writing,Verifying,Completed,FailedOtaPhase— enum:Uploading,Rebooting,Verifying,Completed,FailedProgress—{ bytes_done: u64, bytes_total: u64, speed_bps: u64 }Sha256Hash— 32-byte hashSecureString— zeroized-on-drop string for PSK tokens
| 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) |
The espflash crate has its own error types and progress reporting model. The
ACL translates these into domain events:
/// 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
}
}
}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)
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 |
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.
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) |
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().
ProvisionPhase— enum:Generating,Flashing,Verifying,Completed,FailedDirection— enum:Read,WritePreset— enum:Basic,Vitals,Mesh3,Mesh6Vitals(ADR-044 Phase 3)
| 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) |
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)
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.
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 |
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,ActivityClassificationWsState— enum:Connecting,Connected,Disconnected(reason: String)
| 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) |
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)
Tracks all WASM modules across all nodes.
| Field | Type | Description |
|---|---|---|
modules |
Map<(MacAddress, ModuleId), WasmModule> |
Per-node module inventory |
| 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 |
ModuleId— string identifier assigned by the node firmwareModuleStatus— enum:Loaded,Running,Stopped,Error(String)
| 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) |
The ESP32 WASM management HTTP API (/wasm/* on port 8032) returns raw JSON
with firmware-specific field names. The ACL normalizes these:
/// 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()
}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.
None — this context is a Query Model (CQRS read side). It subscribes to domain events and projects them into view models.
| 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 |
| 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 |
| Field | Source Context | Description |
|---|---|---|
persons |
Sensing Pipeline | Array of detected person skeletons |
zones |
Sensing Pipeline | Active zones in the sensing area |
| 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 |
| 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 |
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
-
Event Bus: Domain events are dispatched via Tauri's event system (
app_handle.emit("event-name", payload)). The frontend subscribes usinglisten("event-name", callback). This provides natural cross-context communication without coupling contexts directly. -
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. -
Module Organization: Each bounded context maps to a Rust module under
src/commands/andsrc/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 -
Testing Strategy: Domain modules under
src/domain/have no Tauri dependency and can be tested with standardcargo test. Command handlers undersrc/commands/require Tauri test utilities for integration testing. -
Shared Kernel: The
MacAddress,SemVer, andSecureStringvalue objects are shared across contexts. They live in asrc/domain/shared.rsmodule. This is acceptable because they are immutable value objects with no behavior beyond validation and formatting.