* fix(signal): circular phase variance for ghost-tap guard (ADR-154 §7.4 #1) `phase_variance` computed a LINEAR sample variance over phase angles that wrap at ±π, so a tightly-clustered set straddling the branch cut reported spuriously HIGH dispersion — false-tripping the `> TAU` ghost-tap guard on real, tightly-clustered CIR taps. Replace with Mardia's circular variance V = 1 − R̄, bounded [0,1] and invariant to where the cluster sits on the circle. Re-derive the guard against the bounded metric via a named const `GHOST_TAP_CIRCULAR_VARIANCE_MAX` (the old TAU-scaled threshold is meaningless on [0,1]). Grade: metric fix MEASURED; threshold value DATA-GATED — a clean single-path ramp also sweeps the circle, so V alone cannot separate clean from unsanitized without labelled frames. Conservative default (0.99) errs toward never false-rejecting, strictly more permissive at the wrap boundary than the buggy linear guard. Fails-on-old test: `phase_variance_circular_not_fooled_by_branch_cut` — inlines the old linear variance to show it exceeds TAU on wrap-straddling phases while circular V≈0 and the guard no longer trips. Plus `phase_variance_circular_is_bounded_and_extremal` (V∈[0,1], V≈0 identical, V≈1 uniform). cargo test -p wifi-densepose-signal --no-default-features --features cir --lib → 432 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * fix(signal): pin Welford n=0/n=1 finiteness guard (ADR-154 §7.4 #10) The shared `WelfordStats` (field_model.rs, used by longitudinal.rs and others) relies on `count < 2` guards in `variance`/`sample_variance`/`std_dev`/ `z_score` to stay finite at the boundaries. The guards existed but the n=0 boundary was UNTESTED — exactly the §4 divide-by-(n−1) family the ADR groups this with. Add `welford_finite_at_n0_and_n1` asserting every statistic is finite and returns the documented sentinel (0.0) at n=0 and n=1, plus load-bearing doc comments on the two guards. Fails-on-old proof: with the `sample_variance` guard removed, the test FAILS with "attempt to subtract with overflow" at the `(self.count - 1)` underflow (0usize − 1); `variance` would similarly yield 0.0/0.0 = NaN. The guard is restored; the test pins it so a future regression is caught. Grade: MEASURED (boundary finiteness is asserted; the guard is the §4-family fix made testable). cargo test -p wifi-densepose-signal --no-default-features --lib field_model → 22 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * refactor(signal): de-magic adversarial thresholds + boundary tests (ADR-154 §7.4 #13) Lift the bare numeric literals buried in `check`/`check_consistency` into named, documented module consts (FIELD_MODEL_GINI_VIOLATION=0.8, ENERGY_RATIO_HIGH_VIOLATION=2.0, ENERGY_RATIO_LOW_VIOLATION=0.1, CONSISTENCY_ACTIVE_FRACTION_OF_MEAN=0.1, SCORE_W_* weights). VALUES UNCHANGED — each const equals the original literal; only names + pinning tests are new. Grade: DATA-GATED. The operating values stay empirical (defensible values need labelled spoofed/clean CSI — Wi-Spoof, §6.2/§7.3). The de-magicking + characterization tests are MEASURED: `tuning_consts_unchanged_from_literals`, `energy_ratio_high_boundary`, `energy_ratio_low_boundary`, `field_model_gini_boundary`, `consistency_active_fraction_boundary` pin the decision boundaries at/just-below/just-above each threshold, so a future data-driven retune is a visible, tested change. Fails-on-change proof: bumping ENERGY_RATIO_HIGH_VIOLATION 2.0→3.0 makes `energy_ratio_high_boundary` FAIL (restored). Operating values explicitly NOT changed. cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::adversarial → 20 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * refactor(signal): de-magic coherence drift/gate thresholds (ADR-154 §7.4 #9) Lift the bare detection literals in `coherence.rs::classify_drift` (DRIFT_STABLE_SCORE=0.85, DRIFT_STEP_CHANGE_MAX_STALE=10) and the `coherence_gate.rs` Default impl (DEFAULT_ACCEPT_THRESHOLD=0.85, DEFAULT_REJECT_THRESHOLD=0.5, DEFAULT_MAX_STALE_FRAMES=200, DEFAULT_PREDICT_ONLY_NOISE=3.0) into named, documented consts. VALUES UNCHANGED. The gate already exposed these via GatePolicyConfig (config seam); this names + pins the defaults. Grade: DATA-GATED. Operating values stay empirical (defensible Z-score thresholds need labelled stable/drifting coherence traces). De-magicking + boundary tests are MEASURED: `classify_drift_stable_score_boundary`, `classify_drift_stale_count_boundary` pin the at/just-below/just-above decisions; `drift_consts_unchanged_from_literals` / `gate_default_consts_unchanged_from_literals` pin the values. Operating values explicitly NOT changed. cargo test -p wifi-densepose-signal --no-default-features --lib ruvsense::coherence → 40 passed, 0 failed. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr-154): mark §7.4 P1 backlog cleared — Milestone-1 (#1,#10 RESOLVED; #9,#13 DATA-GATED) Update ADR-154 §7.4 backlog rows #1, #9, #10, #13 with commit refs + grades, the §7.4 intro count (four P1 items cleared, ~41 P2/P3 remain), the Horizon-ledger one-liner (Milestone-1 DONE), and the §8 honest-limits #1 line (metric now correct; threshold still DATA-GATED). Add CHANGELOG [Unreleased] entry. Grades: #1 RESOLVED (MEASURED metric / DATA-GATED threshold), #10 RESOLVED (MEASURED), #9 & #13 RESOLVED-PARTIAL (DATA-GATED — de-magicked + boundary tested, operating values unchanged). Validation: cargo test --workspace --no-default-features → 2057 passed, 0 failed; wifi-densepose-signal lib → 442 passed (no-default + --features cir); python archive/v1/data/proof/verify.py → VERDICT: PASS, hash f8e76f21…46f7a UNCHANGED (CIR ghost-tap guard is not on the deterministic proof path). Co-Authored-By: claude-flow <ruv@ruv.net> * fix(sensing-server): stop leaking internal errors in HTTP responses (ADR-080 #2) Six handlers in `main.rs` serialized the internal error `Display` straight into the JSON response body, leaking server internals to any client (ADR-080 finding #2, CWE-209; reframed onto the Rust boundary by ADR-164 G11): - edge_registry_endpoint: a panicked spawn_blocking `JoinError` ("task … panicked") in a 500, and the raw upstream error in a 503 - delete_model / delete_recording / start_recording: std::io::Error strings carrying OS detail / filesystem paths - calibration_start / calibration_stop: the FieldModel error chain New `error_response` module: `internal_error` / `internal_error_json` / `upstream_unavailable` log the full detail server-side only (tagged with a correlation id) and return a generic body (`{"error":"internal_error","correlation_id":…}`) — no `panicked`, no file paths, no Debug chain. The correlation id lets an operator join a client report to the exact server log line without ever shipping the detail. Pinned by 5 error_response tests, incl. a leak-substring guard (internal_error_body_does_not_leak_detail) verified to FAIL on the reverted old body (returns the panic message / path / "os error"). The HOMECORE sweep (ADR-161) covered homecore-server, not this crate. Co-Authored-By: claude-flow <ruv@ruv.net> * test(sensing-server): pin XFF-immunity + no-query-token (ADR-080 #1, #3) Findings #1 (XFF-spoofing bypass) and #3 (JWT-in-URL, CWE-598) were logged against the Python v1 API but are VERIFIED ABSENT on the current Rust sensing-server, so they get regression tests rather than redundant fixes: - #1 XFF: there is no IP-based rate-limiter or IP-allowlist to bypass, and neither security middleware reads a forwarded header. Added bearer_auth::xff_header_never_affects_auth_decision (spoofed X-Forwarded-For never flips a 401<->200 decision) and host_validation::forwarded_headers_never_bypass_host_allowlist (spoofed X-Forwarded-Host: localhost never lets Host: evil.com past the allowlist). - #3 JWT-in-URL: require_bearer reads the token only from the Authorization header; WS handlers take no query token; the sole Query extractor (EdgeRegistryParams) is a non-secret refresh flag. Added bearer_auth::query_string_token_is_never_accepted — ?token= / ?access_token= in the URL never authenticates (stays 401) while the header path still 200s. Verified to FAIL when a query-token path is injected into require_bearer. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr-080): mark P0 security findings #1-#3 RESOLVED; close ADR-164 G11 - ADR-080: Status note + per-finding closure (#1 XFF and #3 JWT-in-URL verified absent + regression-pinned; #2 leaked errors fixed via the error_response module). Records the v1-vs-Rust boundary distinction explicitly: v1 paths remain archived; this closure governs the shipped Rust sensing-server. - ADR-164: Gap Register G11 and the Open/Gated Backlog entry marked RESOLVED with the fix + branch reference. - CHANGELOG: [Unreleased] -> ### Security entry covering all three findings. Co-Authored-By: claude-flow <ruv@ruv.net> * docs(adr): renumber 6 displaced ADRs to resolve duplicate-number collisions (ADR-164 G1) Resolves the 5 duplicate ADR numbers (6 displaced files) flagged by ADR-164 Gap Register item G1. Canonical keeper per number = first file committed at that number (date tie-broken by inbound cross-reference count / parent-appendix relationship). Displaced files renumbered to the next free numbers (166-171): 050 keeps provisioning-tool-enhancements (5 refs vs 1) -> ADR-166-quality-engineering-security-hardening 052 keeps tauri-desktop-frontend (parent ADR) -> ADR-167-ddd-bounded-contexts (its appendix) 147 keeps nvidia-cosmos/OccWorld (the actual ADR, has Status header) -> ADR-168-benchmark-proof (proof companion, no Status) -> ADR-169-adam-mode-light-theme (was untracked) 148 keeps drone-swarm-control-system (committed #862) -> ADR-170-yoga-mode-pose-system (was untracked) 149 keeps public-community-leaderboard-huggingface (committed 16:47 vs 17:38) -> ADR-171-swarm-benchmarking-evaluation-methodology Updates in-file `# ADR-NNN` headers and intra-file self-references (yoga-modes * docs(adr): repoint inbound cross-references to renumbered ADRs (166-171) Follow-up to the ADR renumbering (ADR-164 G1). Updates every inbound reference that pointed at a displaced ADR, disambiguating shared numbers by title/slug so only references to the DISPLACED topic move and keeper references stay put. ADR-168 (was 147 benchmark-proof): README, CHANGELOG, user-guide, proof-of-capabilities, research docs 00/03 — all path/label refs updated. ADR-169 (was 147 adam-mode) / ADR-170 (was 148 yoga-mode): docs/adr/README index. ADR-171 (was 149 swarm-benchmarking): all ruview-swarm eval code+docs (Cargo.toml, evals/, eval_swarm.rs, metrics/mod/report/runner.rs), research doc 03 (every §-ref matched ADR-171 sections, not AetherArena), 00-system-review, series README, CHANGELOG, and ADR-148's forward/"open issues" pointers. ADR-166 (was 050 quality-engineering / security-hardening): disambiguated from the ADR-050 provisioning KEEPER by topic. The HMAC/secure_tdm, directory-traversal, bind-address, and OTA-PSK-auth references in code comments (wifi-densepose-hardware Cargo.toml + secure_tdm.rs, sensing-server main.rs) and in ADR-052-tauri / ADR-167 all describe the security-hardening ADR -> ADR-166. ADR-167 (was 052 ddd-appendix): inbound appendix references. Index/registry updates: docs/adr/README.md, gap-analysis/census.md (rows + header count), gap-analysis/lens-findings.md (collision table marked RESOLVED), and ADR-164 Gap Register G1 marked RESOLVED with the full renumber map. Keeper references deliberately untouched: all ADR-147 OccWorld code, all ADR-148 drone-swarm code/docs, all ADR-149 AetherArena refs (incl. ADR-150's SSL/resampling refs, which ADR-150 explicitly binds to the AetherArena benchmark), ADR-050 provisioning refs, ADR-052 tauri refs. The frozen GitHub blob URLs in docs/adr/.issue-177-body.md (pinned to an old branch) are left as historical. Comment-only code edits; no behavior change. wifi-densepose-hardware compiles clean; the sensing-server build's sole blocker is the pre-existing upstream midstreamer-temporal-compare@0.2.1 registry crate, unrelated to these edits. Co-Authored-By: claude-flow <ruv@ruv.net>
26 KiB
ADR-167 Appendix: DDD Bounded Contexts — Tauri Desktop Frontend
Appendix to ADR-052. Renumbered from ADR-052 to ADR-167 to resolve the ADR-052 duplicate-number collision (per ADR-164 Gap Register G1); the parent decision remains ADR-052.
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 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
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:
/// 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-166) |
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),SkippedOtaStrategy— enum:Sequential— one node at a time, wait for rejoinTdmSafe— update non-adjacent slots to maintain sensing coverageParallel— all at once (development only)
Value Objects
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
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:
/// 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) |
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,FailedDirection— enum:Read,WritePreset— 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,ActivityClassificationWsState— 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 firmwareModuleStatus— 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:
/// 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
-
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.