Skip to content

Releases: ruvnet/RuView

Release v1613

09 Jun 15:36
992c2b2

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(firmware): correct ESP32 edge heart rate — sample-rate + harmonic lock (#987) (#988)

  • fix(firmware): correct heart-rate estimation — sample-rate + harmonic lock

The edge vitals HR was stuck at ~45 BPM regardless of true heart rate
(Apple Watch ground truth 87 BPM read as ~45) and "dropped a lot" between
frames. Two root causes:

  1. Stale fixed sample rate. estimate_bpm_zero_crossing() used a hardcoded
    sample_rate = 10.0f (and the biquads a separate fs = 20.0f). That
    constant was correct when CSI came from ~10 Hz beacons, but #985's
    self-ping raised the callback rate to a VARIABLE ~13-19 Hz. BPM scales as
    (assumed_rate / actual_rate) x true, so a true 87 read ~45, and because
    the real rate fluctuates with CSI yield while the code assumed a fixed
    value, the reported HR swung frame-to-frame (the "drops").

  2. Breathing-harmonic lock. Zero-crossing HR estimation locked onto a
    breathing harmonic — a 0.25 Hz breathing fundamental puts its 3rd
    harmonic at ~0.74 Hz ~= 44 BPM, right in the HR band — so it parked at
    ~45 BPM independent of the real heartbeat.

Fix:

  • Measure the real sample rate from inter-frame timestamps (EMA-smoothed,
    clamped 8-30 Hz); use it for both BPM conversion and biquad design, and
    re-tune the filters when the rate drifts >15% so the passbands stay in
    real Hz.
  • Replace the HR zero-crossing with estimate_hr_autocorr(): autocorrelation
    peak in the 45-180 BPM band that explicitly rejects lags within 8% of any
    breathing harmonic (k=1..6), with parabolic interpolation and a peak-
    confidence gate (returns 0 rather than a noise value).
  • Median-smooth (N=9) the emitted HR over valid estimates to kill residual
    single-frame outliers.

Validated on hardware (ESP32-S3, COM8/192.168.1.80) vs an unmodified board
(192.168.1.67) and an Apple Watch (87 BPM):

  • old firmware: HR pegged 40-52 BPM (median ~45)
  • fixed firmware: HR reaches the true 88-91 BPM range (peak 88.5, vs 87 GT)

Known limitation: under subject motion (motion=Y) HR is still noisy because
the breathing estimate degrades and misguides harmonic rejection; motion
gating + breathing robustness are follow-ups.

Co-Authored-By: claude-flow ruv@ruv.net

  • fix(firmware): robust HR harmonic rejection via autocorr breathing period (#987)

Follow-up to 332c2a98d. The HR harmonic rejection was fed the noisy
zero-crossing breathing estimate, which under motion notched the wrong
frequencies and let the autocorr lock onto the ~0.75 Hz breathing harmonic
(~45 BPM). Generalize estimate_hr_autocorr -> estimate_periodicity_autocorr
and drive HR harmonic rejection from a robust autocorrelation breathing
period instead; widen the HR median smoother to N=13.

Hardware A/B (fixed .80 vs unmodified control .67, both edge_tier=2, subject
in motion 100% of frames):

  • control (old fw): HR pegged 40-43 BPM (median 40.6)
  • fixed: HR 60-91 BPM (median 71.9) — sub-60 harmonic locks
    eliminated, spread 42->31 BPM vs previous build

Reported breathing is unchanged (still zero-crossing); the autocorr breathing
period is used only internally for HR harmonic rejection.

Co-Authored-By: claude-flow ruv@ruv.net

  • docs(changelog): record ESP32 heart-rate fix (#987)

Co-Authored-By: claude-flow ruv@ruv.net

Docker Image:
ghcr.io/ruvnet/RuView:992c2b25cb6c6fcf3ba4d80d2be906920831f54c

Release v1611

09 Jun 12:53
5789351

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(esp32): add connected-STA self-ping CSI traffic source (#954) (#985)

The ESP32 CSI engine only produces CSI for received OFDM frames (L-LTF/
HT-LTF). On a quiet network — or on a display-enabled build where the
#893 MGMT->MGMT+DATA promiscuous upgrade is skipped (has_display=true) —
the only CSI-eligible frames are sparse beacons (often non-OFDM DSSS),
so wifi_csi_callback can starve to yield=0pps -> DEGRADED -> motion=0
(#521, #954).

Fix (additive): pin a ~50 Hz OFDM unicast floor by pinging the STA's own
DHCP gateway. The router's ICMP echo replies are OFDM frames destined to
this station and drive the CSI engine regardless of promiscuous filter
state or ambient traffic. Mirrors Espressif's esp-csi csi_recv_router
reference. Promiscuous capture (#396/#893) is left fully intact so
multistatic/multi-node sensing still hears other stations' frames.

Reconciles PR #955 (which removed promiscuous entirely and conflicted
with the already-shipped #893 DATA-capture path) into an additive change
on current main.

Verified on ESP32-S3 (N16R8, COM8), ESP-IDF v5.4:
Promiscuous mode enabled (MGMT-only, RuView#396)
self-ping started -> 192.168.1.1 @50Hz (CSI OFDM source, fix #521/#954)
CSI cb #1: len=128 rssi=-40 ch=5
adaptive_ctrl: state=6 yield=13-19pps motion=1.00 presence>0 (SENSE_ACTIVE)
DEGRADED cleared; CSI yield stable ~15 pps over 60 s.

Co-authored-by: Meraj merajmehrabi@gmail.com

Docker Image:
ghcr.io/ruvnet/RuView:5789351b78f1f4c9dcd4bc15117588911b12dc2d

ESP32 firmware v0.7.1 — CSI self-ping + heart-rate fix

09 Jun 15:37
992c2b2

Choose a tag to compare

Firmware update for the ESP32-S3 / ESP32-C6 CSI sensing node. Two hardware-validated fixes since v0.7.0-esp32.

Fixes

CSI callback now fires — wifi_csi_callback no longer starves (#954 / #985)

On v0.7.0 the CSI engine could sit at yield=0pps, parking the adaptive controller in DEGRADED with motion=presence=0 (only beacon frames were CSI-eligible, and APs send those at non-OFDM DSSS rates). The node now pins a guaranteed ~50 Hz OFDM unicast floor by self-pinging its gateway, so the CSI callback fires reliably (~13–19 pps) — additive to the existing promiscuous capture. If you saw a node "stream but sense nothing," this is the fix.

Heart rate corrected — no longer stuck at ~45 BPM or dropping wildly (#987)

On-device HR (0xC5110002, edge_tier≥1) reported ~45 BPM regardless of the real rate and swung frame-to-frame. Two causes: a stale fixed sample-rate assumption (made wrong by the self-ping raising the CSI rate) and the zero-crossing estimator locking onto a breathing harmonic. Now: the real sample rate is measured from frame timestamps (and re-tunes the filters), HR uses an autocorrelation estimator that rejects breathing harmonics, and the output is median-smoothed.

Validated (ESP32-S3, edge_tier=2) against an unmodified control board + an Apple Watch (87 BPM): control pegged 40–49 BPM; fixed board reaches the true 88–91 BPM and holds a stable physiological value.

Known limitation: under heavy subject motion the HR estimate still degrades (motion gating tracked as a follow-up in #987).

Binaries

  • ESP32-S3 (8 MB, N16R8-class): esp32-csi-node-s3-8mb.bin + bootloader-s3.bin + partition-table-s3.bin + ota_data_initial-s3.binhardware-verified on COM8 this release.
  • ESP32-C6 (4 MB): esp32-csi-node-c6-4mb.bin + bootloader-c6.bin + partition-table-c6.bin + ota_data_initial-c6.bin — build-verified (no C6 hardware connected this cycle).
  • SHA256SUMS.txt — verify with sha256sum -c SHA256SUMS.txt.

Flash (ESP32-S3, 8 MB)

python -m esptool --chip esp32s3 -b 460800 --before default_reset --after hard_reset \
  write_flash --flash_mode dio --flash_size 8MB --flash_freq 80m \
  0x0 bootloader-s3.bin 0x8000 partition-table-s3.bin \
  0xf000 ota_data_initial-s3.bin 0x20000 esp32-csi-node-s3-8mb.bin

Then provision: python firmware/esp32-csi-node/provision.py --port <COM> --ssid <SSID> --password <PW> --target-ip <HOST_IP> (add --edge-tier 0 for raw-CSI calibration, default 2 for on-device vitals).

🤖 Generated with claude-flow

Release v1609

08 Jun 16:16
b6420ac

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(server): make synthetic CSI opt-in only (sibling fix to #937) (#979)

Background

Issue #937 in the cognitum-v0 appliance repo flagged that the
cognitum-csi-capture systemd unit shipped --simulate by default,
silently serving synthetic CSI tagged as production telemetry on
/api/v1/sensor/stream. That's a textbook trust-eroding pattern — the
single most-cited "where's the real data?" evidence external reviewers
(#943, #934) point at when they call the project AI-slop.

A grep across THIS tree surfaced the exact same anti-pattern in three
places:

docker/docker-compose.yml:27 # auto (default) — probe ESP32, fall back to simulation
docker/docker-entrypoint.sh:14 # CSI_SOURCE — data source: auto (default), ...
main.rs:6435 info!("No hardware detected, using simulation"); "simulate"

The sensing-server's auto source resolver at main.rs:6425-6440
silently fell back to synthetic with only an info! log line as the
signal. Downstream consumers calling /api/v1/sensing/latest or
/ws/sensing had no in-band way to know they were being served fake
data.

Fix

auto now refuses to fall back. When neither ESP32 UDP nor host WiFi
is detected, the server logs a clear error! explaining the situation
and exits 78 (EX_CONFIG). The error message names the two ways to
proceed: provision real hardware, or set --source simulated /
CSI_SOURCE=simulated explicitly. Existing operators who already use
--source simulated (or its legacy simulate alias) are unaffected —
the alias is preserved for back-compat.

Docker entrypoint comment, docker-compose comment, and the Tauri
desktop app's source-default path also updated to reflect the new
posture. The desktop app keeps its simulated default because it's
an explicit demo product — the value passed downstream is the
explicit simulated, not auto, so the server tags it correctly
and never lies about its data source.

Validation

cargo build -p wifi-densepose-sensing-server --no-default-features
cargo test -p wifi-densepose-sensing-server --no-default-features
→ 122 / 122 pass, build clean (existing pre-fix warnings unchanged).

Deployment

⚠ Breaking change for unattended deployments that relied on the
auto → simulated silent fallback. That is exactly the failure mode
this PR fixes: pretending to serve real sensing data when the source
is fake. Operators who genuinely want demo mode set
CSI_SOURCE=simulated explicitly; the error message and the
docker-compose comment both point them there.

Docker Image:
ghcr.io/ruvnet/RuView:b6420ac9bad9b0646891db460d8193a5f9dbdd21

Release v1606

08 Jun 14:49
c353255

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix: firmware cluster — wasm3 IDF v6.0 build (#946) + swarm TLS stack (#949) + Docker unauth default (#864) (#975)

  • fix(firmware,docker): clear three high-severity bugs in one sweep

Closes #946 — wasm3 fails on Xtensa GCC 15.2.0 (ESP-IDF v6.0.1)

cannot tail-call: machine description does not have a sibcall_epilogue
instruction pattern

wasm3's M3_MUSTTAIL return jumpOpImpl(...) uses
__attribute__((musttail)) which GCC 15 enforces strictly on Xtensa,
where the backend never reliably implemented sibling-call epilogues.
Define M3_NO_MUSTTAIL=1 in the wasm3 component compile-defs so the
macro expands to plain return — slightly slower per opcode dispatch
but functionally identical, and the only change needed in this tree.
Older IDF / GCC builds accept the define as a no-op so the IDF v5.4
CI build is unchanged.

Closes #949 — swarm task stack overflow on Seed TLS init

The reporter provisioned with --seed-url https://... which exercises
TLS, and the task panicked with the FreeRTOS stack-fill sentinel
0xa5a5a5a5 immediately after the bridge init line. SWARM_TASK_STACK
was 3 KB ("HTTP client uses ~2.5 KB" per the original comment) — fine
for plain HTTP, far too small for mbedTLS handshake which alone wants
4-6 KB for the cipher suite + cert chain + ECDH state, plus another
1.5-2 KB for esp_http_client. Bumped to 8192 with the why in the
comment. Plain-HTTP deployments waste ~5 KB headroom (negligible
PSRAM cost) but the bug class is closed.

Closes #864 — Docker default exposes unauthenticated sensing API + WS

docker-entrypoint.sh started the sensing-server with --bind-addr 0.0.0.0 AND empty RUVIEW_API_TOKEN AND docker-compose published
3000/3001/5005 — anyone on a reachable network segment could read
/api/v1/sensing/latest and the /ws/sensing live frame stream.

Now the entrypoint refuses to start when:
RUVIEW_API_TOKEN is empty
AND RUVIEW_ALLOW_UNAUTHENTICATED is not "1"
AND RUVIEW_BIND_ADDR is not loopback / localhost / ::1

…and prints exactly which three escape hatches the operator can take
(set the token, opt in explicitly, or pin to loopback). Also wires
RUVIEW_BIND_ADDR through to --bind-addr so the loopback escape hatch
is one env var, not a flag override. cog-ha-matter / homecore routes
are excluded from this check since they own their own auth lifecycle.
This is a breaking change for unattended LAN deployments — exactly
what the reporter asked for.

Validation

  • idf.py build for esp32s3 target — succeeds (#946 fix doesn't
    affect default IDF v5.4 build path).
  • idf.py set-target esp32c6 && idf.py build — succeeds, binary
    1015 KB / 45% partition free.
  • Hardware flash to COM12 (C6) failed with "No serial data received"
    — XIAO C6 needs manual BOOT-hold+RESET; couldn't drive that without
    operator. Code is correct per build + review; runtime validation
    needs the operator to press the BOOT button at flash time.
  • docker-entrypoint.sh changes are shell-only — exercised by reading
    the path under the four escape-hatch conditions.

Out of scope — cross-repo issues

Issues #935 (cognitum-agent mesh panics), #936 (CSI relay routing),
and #937 (cognitum-csi-capture --simulate default) reference
cognitum-agent / csi-capture / csi-relay-routes.json artifacts
that live in the cognitum-v0 appliance repo, not this tree.

Issue #954 (CSI callback never fires on S3 v0.6.5/v0.7.0) is not
addressed here — the reporter is on the S3 (COM9 in this lab) but the
hardware path needs an interactive debug session with a configurable
AP traffic source to pin the root cause (MGMT-only filter, traffic
filter MAC, or driver-level callback wiring). Will tackle in a
follow-up.

Co-Authored-By: claude-flow ruv@ruv.net

  • fix(firmware): bump LWIP UDP / WiFi TX buffer pools to ease ENOMEM

Hardware validation on COM8 (S3) and COM9 (C6) surfaced a v0.7.0
regression not captured in the existing issue tracker: stock IDF v5.4
defaults (UDP recv mbox = 6, TCPIP recv mbox = 32, WiFi dynamic TX
buffers = 32) are too small for the v0.7.0 packet mix once CSI
promiscuous mode is active. The boot trace showed
stream_sender: sendto ENOMEM — backing off for 100 ms repeating
every capture cycle, with the csi_collector path reporting fail #1..5
within seconds of associating to an AP.

Modest bumps applied (~3 KB extra heap each):

CONFIG_LWIP_UDP_RECVMBOX_SIZE 6 → 32
CONFIG_LWIP_TCPIP_RECVMBOX_SIZE 32 → 64
CONFIG_ESP_WIFI_DYNAMIC_TX_BUFFER_NUM 32 → 64

Empirical 25 s measurement on S3 / COM8 post-fix:

csi_collector fail # : 1-5 → 0 (full path drained)
stream_sender ENOMEM hits / sec : 8-15 → 8 (capped by 100 ms backoff)
CSI cb rate : ~28 cb/s, yield max 18 pps
feature_state emit failed : still present

A second, more aggressive iteration (DYNAMIC_TX=128, PBUF_POOL=32, TCP
SND/WND=16384) was tested and reverted — the ENOMEM count was
identical to the modest bump. The residual 8/s is structural: it's the
100 ms backoff window ceiling × the adaptive_controller emit cadence
which currently fires roughly every 50 ms instead of the intended 1 Hz.
Bigger buffers don't fix that — only rate-limiting the emitter does.

Code-level rate-limit refactor is tracked separately to keep this PR
scoped to the bundle that landed mechanically.

Co-Authored-By: claude-flow ruv@ruv.net

  • fix(firmware): rate-limit feature_state emit from 5 Hz → 1 Hz

Completes the ENOMEM cure that the LWIP/WiFi buffer bumps started.

Root cause (verified on COM8 / S3 + COM9 / C6)

fast_loop_cb runs every 200 ms (5 Hz) and unconditionally called
emit_feature_state(). Combined with CSI capture in promiscuous mode
(radio mostly in RX), the WiFi TX airtime got saturated and every
100 ms backoff window had at least one ENOMEM. Bumping the LWIP/WiFi
buffer pools to 4× had no effect on the ENOMEM rate because the
bottleneck was radio TX time, not pool size.

The ADR-081 spec calls out "1–10 Hz" for feature_state; 5 Hz was at
the top of the range and not necessary — operators consuming the
telemetry want a sample every second, not five times. Dropping to
1 Hz frees ~80 % of the feature_state TX traffic.

Measurement on COM8 (25 s windows, otherwise-idle environment)

csi_collector lost sends : 1-5 / 25 s → 0 / 25 s (✓ fixed)
feature_state emit failed : 75 / 25 s → 25 / 25 s (3× ↓)
total sendto ENOMEM log lines: 200/25 s → 212 / 25 s
(unchanged — bound by 100 ms backoff
window ceiling, not by emit rate)
CSI yield : 18 pps (steady)

The unchanged total ENOMEM is a measurement artifact: the backoff
window emits exactly one ENOMEM record per 100 ms when anything
collides with a TX-busy moment. The packet-loss numbers (which is
what actually matters) all dropped to zero or near-zero on the CSI
path.

Implementation

Pure-static s_emit_divider counter in fast_loop_cb. Every 5th tick
calls the emit. Zero allocation, zero extra state, zero interaction
with the existing observation snapshot under s_obs_lock. Could be
made config-driven if any operator ever wants 2-5 Hz back — out of
scope here.

Co-Authored-By: claude-flow ruv@ruv.net

Docker Image:
ghcr.io/ruvnet/RuView:c353255672c1e7f6fa4ff6140d4cebb63d08b7c9

Release v1596

04 Jun 06:35
872d759

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix: IDF v6.0 ESP-NOW callback compat (#944) + occupancy noise-floor anchor (#942) (#945)

  • fix(firmware): on_send ESP-NOW callback compat for IDF v6.0 (closes #944)

ESP-IDF v6.0 changed esp_now_send_cb_t from
void (*)(const uint8_t mac, esp_now_send_status_t status)
to
void (
)(const esp_now_send_info_t *tx_info, esp_now_send_status_t status)

The C6 sync ESP-NOW path's on_recv was already version-guarded with
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 0, 0) (lines 102-112)
but the on_send sibling missed the equivalent guard. CI runs against
IDF v5.4 so the regression slipped through; the reporter on IDF v6.0.1
with xtensa-esp-elf esp-15.2.0_20251204 hit:

c6_sync_espnow.c:182:30: error: passing argument 1 of
'esp_now_register_send_cb' from incompatible pointer type
[-Wincompatible-pointer-types]

Fix: mirror the recv guard with #if ESP_IDF_VERSION_MAJOR >= 6 since
the send-callback signature change happened at IDF v6.0 (not v5.x like
the recv-callback). Both branches ignore the address-side argument
since on_send only inspects status to bump the TX-fail counter.

Adds #include "esp_idf_version.h" so the macro is in scope.

Closes #944

Co-Authored-By: claude-flow ruv@ruv.net

  • fix(signal): anchor estimate_occupancy noise floor to calibration (closes #942)

test_estimate_occupancy_noise_only asserts that 20 noise-only frames
fed through a 50-frame calibrated FieldModel yield 0 occupancy.
Failure reported on the upstream Linux + BLAS build.

Root cause

Calibration and estimation each compute their own Marcenko-Pastur
threshold:

threshold = noise_var · (1 + sqrt(p / N))²

with noise_var = median of the bottom half of positive eigenvalues
from their own covariance. The MP ratio differs across the two phases:

calibration (50 frames, p=8): ratio = 0.16, factor ≈ 1.96
estimation (20 frames, p=8): ratio = 0.40, factor ≈ 2.66

On a small estimation window the local noise_var estimate can also
be smaller than the calibration's (fewer samples → bottom-half median
hits lower-magnitude eigenvalues). The combination of a smaller
noise_var on estimation and the larger MP factor can flip eigenvalues
on/off the "significant" line in a sample-size-dependent way, so an
identical-distribution test window scores significant > baseline_eigenvalue_count and reports phantom persons.

Fix

Persist the calibration noise_var on FieldNormalMode (new field
baseline_noise_var: f64) and use max(local_noise_var, baseline_noise_var) as the noise floor inside estimate_occupancy.
This anchors the threshold to the calibration scale and prevents the
short-window collapse without changing behavior when the local
window's own noise dominates (the real-motion case).

baseline_noise_var defaults to 0.0 in the diagonal-fallback paths;
the estimation code treats 0.0 as "no anchored floor available" and
preserves the pre-#942 single-window behavior — so older FieldNormalMode
instances deserialised from disk continue to work unchanged.

Test results

cargo test --workspace --no-default-features
→ 413 lib tests pass (signal crate), 0 fail, 1 ignored.

The actual eigenvalue-gated test still requires BLAS (not buildable
on Windows). Logic-trace via the four numerical anchors above shows
the fix flips noise_var from the smaller local value back up to the
calibration scale, dropping significant to or below
baseline_eigenvalue_count so the saturating subtraction returns 0.

Closes #942

Co-Authored-By: claude-flow ruv@ruv.net

Docker Image:
ghcr.io/ruvnet/RuView:872d7593bbeeed63524386aa60e6805bb4e1b26c

Release v1591

03 Jun 10:06
2c136ac

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(protocol): resolve 0xC511_0004 magic collision (closes #928) (#931)

  • fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action

Two real problems in the Static Application Security Testing job:

  1. It scanned a path that no longer exists. bandit -r src/ and
    semgrep … src/ pointed at the repo-root src/, but the Python code
    moved to archive/v1/src/ (64 .py files) when the runtime was rewritten
    in Rust. So the SAST scan matched nothing — a silent no-op (this is also
    why bandit-results.sarif was "Path does not exist" on recent runs).
    Fixed both to archive/v1/src/.

  2. Deprecated + redundant + flaky semgrep step. The
    returntocorp/semgrep-action@v1 step pulled returntocorp/semgrep-agent:v1
    from Docker Hub every run (intermittently timing out → red check, e.g. on
    #929) and is EOL. It was redundant: the pip semgrep --sarif step is what
    feeds GitHub Security; the action only pushed to the Semgrep cloud app via
    SEMGREP_APP_TOKEN. Removed it and folded its p/docker + p/kubernetes
    rulesets into the pip semgrep command, so coverage is preserved with no
    Docker pull.

The job stays continue-on-error: true (non-gating). YAML validated.

Co-Authored-By: claude-flow ruv@ruv.net

  • fix(protocol): resolve 0xC511_0004 magic collision (closes #928)

Background

0xC511_0004 was assigned to two different packet formats in firmware
EDGE_FUSED_MAGIC (ADR-063, 48-byte edge_fused_vitals_pkt_t) and
WASM_OUTPUT_MAGIC (ADR-040, variable-length wasm_output_pkt_t).
Both were transmitted. The sensing-server only had a WASM parser for
that magic and no fused-vitals parser, so on the ESP32-C6 + MR60BHA2
mmWave configuration the fused-vitals packet was silently misparsed
as a malformed WASM output — breathing_rate was read as
event_count, mmWave-fused vitals were lost, and spurious WASM events
were emitted to subscribers.

Fix

  1. Reassign WASM_OUTPUT_MAGIC to 0xC511_0007 (next free slot per
    the registry in rv_feature_state.h). Smaller blast radius than
    moving fused-vitals — the registry already treats 0xC511_0004 as
    fused-vitals canonical and several years of deployed feature
    tracking depends on that assignment.

  2. Add parse_edge_fused_vitals + EdgeFusedVitalsPacket in
    wifi-densepose-sensing-server::main. Byte layout taken directly
    from edge_processing.h:129, mirroring the firmware's
    _Static_assert(sizeof(edge_fused_vitals_pkt_t) == 48) so future
    firmware changes that grow the packet will break this parser
    loudly instead of silently.

  3. Add a dispatch arm in the UDP receive loop. Fused-vitals is tried
    BEFORE WASM so a stale firmware (still emitting 0xC511_0004 with
    the WASM payload) fails to parse as fused-vitals (size mismatch),
    then fails to parse as WASM (magic mismatch on the new 0x...0007),
    and gets dropped — a deliberate "fail loud" outcome rather than the
    pre-fix silent garbage.

  4. Update the registry comment in rv_feature_state.h to add the new
    0x...0007 row.

  5. Add five tests in a new issue_928_magic_collision_tests mod:

    • parse_edge_fused_vitals_extracts_fields_correctly
    • parse_edge_fused_vitals_rejects_short_buffer
    • parse_edge_fused_vitals_rejects_wrong_magic
    • parse_wasm_output_rejects_legacy_0004_magic
    • parse_wasm_output_accepts_new_0007_magic

WebSocket payload

Fused-vitals now broadcasts as {"type": "edge_fused_vitals", ...}
with the mmWave-specific block nested under mmwave. Schema is
additive — existing subscribers that only inspect type are
unaffected; subscribers that switch on type gain a new branch.

Deployment note

This is a wire-protocol change. Firmware older than this commit that
emits WASM output on 0xC511_0004 will lose its WASM event stream
against an updated host (host expects 0xC511_0007). Per the issue
discussion, "fail loud" is preferred to silent misparsing. Operators
running C6+mmWave should reflash firmware concurrent with the host
upgrade.

Test results
cargo test -p wifi-densepose-sensing-server --no-default-features
--bin sensing-server
→ 122 passed / 0 failed (5 new + 117 existing, unchanged)

Co-Authored-By: claude-flow ruv@ruv.net

Docker Image:
ghcr.io/ruvnet/RuView:2c136aca7456ee5555a21fdcf7176fae38b8cf38

Release v1590

03 Jun 09:57
2c136ac

Choose a tag to compare

Automated release from CI pipeline

Changes:
docs(changelog): record this cycle's behavior-changing fixes (#932)

Per the CLAUDE.md pre-merge checklist (item 5, "Add entry under
[Unreleased]"), several recently-merged PRs landed without CHANGELOG
entries. Backfilling the user/operator-facing ones — most importantly the
MAT triage safety fix:

  • #926 (Security/safety): survivor with a heartbeat never triaged Deceased
  • #918: per-node HA devices report each node's own presence/motion
  • #919: actionable --model load diagnostic (refs #894)
  • #920: --export-rvf no longer silently produces a placeholder model
  • #929 (Security): bearer scheme matched case-insensitively (RFC 6750)

CI-internal fixes (#925 rust-cache, #930 SAST) are intentionally omitted —
they don't change product behavior. Docs-only.

Docker Image:
ghcr.io/ruvnet/RuView:69e61e3437932f5f0d005f7d7b97b7c5eb7b2053

Release v1588

03 Jun 09:28
d9e87e1

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(ci): SAST actually scans the code + drop deprecated flaky semgrep action (#930)

Two real problems in the Static Application Security Testing job:

  1. It scanned a path that no longer exists. bandit -r src/ and
    semgrep … src/ pointed at the repo-root src/, but the Python code
    moved to archive/v1/src/ (64 .py files) when the runtime was rewritten
    in Rust. So the SAST scan matched nothing — a silent no-op (this is also
    why bandit-results.sarif was "Path does not exist" on recent runs).
    Fixed both to archive/v1/src/.

  2. Deprecated + redundant + flaky semgrep step. The
    returntocorp/semgrep-action@v1 step pulled returntocorp/semgrep-agent:v1
    from Docker Hub every run (intermittently timing out → red check, e.g. on
    #929) and is EOL. It was redundant: the pip semgrep --sarif step is what
    feeds GitHub Security; the action only pushed to the Semgrep cloud app via
    SEMGREP_APP_TOKEN. Removed it and folded its p/docker + p/kubernetes
    rulesets into the pip semgrep command, so coverage is preserved with no
    Docker pull.

The job stays continue-on-error: true (non-gating). YAML validated.

Docker Image:
ghcr.io/ruvnet/RuView:d9e87e13b4d39d8ed6a5555c0e7e4fb7230129c4

Release v1585

03 Jun 09:16
be48143

Choose a tag to compare

Automated release from CI pipeline

Changes:
fix(auth): match the Bearer scheme case-insensitively (RFC 6750) (#929)

require_bearer parsed the Authorization header with
strip_prefix("Bearer "), which is case-sensitive. Per RFC 6750 §2.1 /
RFC 7235 §2.1 the auth-scheme is case-insensitive, so a correct token sent
as Authorization: bearer <token> (or BEARER, or with extra whitespace)
was rejected with a confusing "invalid bearer token" 401 — needless friction
when setting up RUVIEW_API_TOKEN (the active #864/#924 theme).

Now the scheme is matched with eq_ignore_ascii_case and leading token
whitespace trimmed. The token comparison itself is unchanged — still exact
and constant-time (ct_eq) — so this does not weaken auth: a wrong token or
a non-Bearer scheme (Basic …) still returns 401.

New test accepts_case_insensitive_bearer_scheme covers bearer/BEARER/
extra-space (accept) and wrong-token/Basic (still reject). bearer_auth
suite: 9 passed.

Docker Image:
ghcr.io/ruvnet/RuView:be48143f774770ad1b89f2491473306f55004847