Compare commits

...
Author SHA1 Message Date
Dragan SpiridonovandRuflo & AQE dbf4788628 fix(calibration): bind the grid the radio keeps emitting, not the sparse minority
`accept_grid` locks each node onto the densest grid it has seen and rejects
sparser frames from the feature path -- on an ESP32-C6 the ~16% HT 64-bin
minority that arrives alongside HE-SU 256-bin. `select_calibration_grid`
ordered candidates by `max_gap_s` first, so it preferred whichever grid
looked temporally smoothest. A sparse trickle at a metronomic cadence has a
smaller worst-case gap than a busy stream with one scheduling hiccup, so
selection kept choosing the grid admission is designed to discard.

Measured on four ESP32-C6 nodes: a capture bound 64sc on node 11, every node
then locked onto 256sc, the bound grid went stale with no frame for five
minutes, frame_count froze at 10,997 and `calibration_grid_is_fresh` failed.
The capture could never finalize -- `collecting` for 858 s with both minimums
long since met, no error surfaced, and a flickering UI as the client retried.
The same rig bound 256sc on a different capture and finalized with five
baseline eigenvalues.

Order by subcarrier count first so selection agrees with admission, then fall
back to the previous gap/rate/ppdu ordering to choose among equally dense
candidates.

Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
2026-09-15 14:48:08 +02:00
Dragan SpiridonovandRuflo & AQE cdad032296 fix(calibration): only the grid-bound radio may write the single-link baseline
A room binding may name several radios, and every one of them was admitted
into the field model. That model is single-link: one baseline, one set of
amplitude offsets. Averaging two radios' offsets into it flattens the
eigenstructure and leaves only a scalar energy threshold behind.

Measured on three ESP32-C6 nodes:

  4-node binding  baseline_eigenvalue_count 0, residual threshold 18.90,
                  empty room reported an occupant in 1162/1162 frames
  1-node binding  baseline_eigenvalue_count 5, residual threshold 3.65,
                  empty room absent 604/604, occupied present 616/616

`person_count_at` already refused to score a non-bound radio, calling it
"deterministic false occupancy from hardware-specific amplitude offsets" --
but only when exactly one node was bound. With more it fell through to the
shared mixed history, so the guard never fired in the case it described.

Restrict the baseline write, not the admission. Every bound radio is still
admitted and recorded as a contributor: `calibration_source_nodes_missing`
refuses to finalize until each one proves it is live on the frozen grid, so
gating them out of the feed path entirely deadlocks the capture -- measured
on hardware as 43,120 frames over 51 minutes stuck in `collecting` with
missing_source_node_ids=[12,13,14]. Only `maybe_feed_calibration` is
restricted to the grid-bound radio, and scoring follows that same radio
whatever the size of the room's node set.

This is the narrowing `bootstrap_baseline::store` already applies when it
persists `vec![binding.source_node_id]` as the frozen model source.

Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
2026-09-15 14:09:33 +02:00
Dragan SpiridonovandRuflo & AQE 0decc5c3fa test(calibration): cover calibrated presence evidence identity and refusals
Assert the exact wire schema and every identity field the Mac app matches
against its model receipt, plus that evidence agrees with the server's own
person count. Pair the refusals (no receipt, bootstrap-only authority,
expired model) with the positive case so an absent value can never be read as
proof when the path is simply broken.

Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
2026-09-14 18:54:15 +02:00
Dragan SpiridonovandRuflo & AQE d4a3c7f17f feat(calibration): emit calibrated presence evidence v2 on the sensing stream
The Mac app gates its held-out empty-room check on
`calibrated_presence_evidence` carrying
`ruview.calibration.calibrated-presence-evidence.v2`. No shipped server has
ever emitted that key, so the check fails on its first iteration with "No
fresh calibrated presence result matched this room's current model receipt",
`bootstrap/promote` is never called, and `bootstrap_baseline::store` -- the
only path that persists a calibration -- never runs. A completed 62-minute
field model is therefore lost on the next restart.

Emit the evidence bound to the active model receipt. `None` unless an
explicit calibration is fresh and the field model scores the observation
without a heuristic fallback, so a present value is always calibrated.

Extract the field-model scoring history into `scoring_history()` and reuse it
for both `person_count_at` and the evidence, so the published evidence cannot
disagree with the count derived from the same model.

Co-Authored-By: Ruflo & AQE
Claude-Session: https://claude.ai/code/session_01Qh7eir6C2U5CSwD7GR43Zv
2026-09-14 18:49:50 +02:00
Dragan Spiridonov 2c0abcc802 fix(calibration): separate receipt schema from telemetry literals 2026-09-11 20:29:42 +02:00
Dragan Spiridonov f1dbaac452 Merge remote-tracking branch 'origin/main' into codex/calibration-process-identity 2026-09-11 20:27:02 +02:00
rUv 33a9e90896 Merge pull request #1902 from ruvnet/feat/firmware-usb-onboarding-c6
firmware: USB onboarding on main + C6 console RX fix (0.8.12)
2026-09-11 13:32:02 +00:00
Dragan Spiridonov bbbb14169d fix(calibration): bind server receipts to room identity 2026-09-11 15:21:39 +02:00
ruv 4fbe58f736 fix(firmware): keep onboarding digest compatible with ESP-IDF 6 2026-09-11 09:19:33 -04:00
ruvandClaude Fable 5.1 a64e9e98fb fix(firmware): keep the C6 console RX alive and ship USB onboarding (0.8.12)
- mmwave_sensor: the default probe pins (UART1 TX=17/RX=18) put UART1 TX on
  GPIO17, which is U0RXD on the ESP32-C6. Console input was dead on every C6
  node even when no sensor was found, because uart_driver_delete() leaves the
  GPIO matrix routing in place. C6 now defaults to GPIO4/5 and any pin pair
  overlapping the console UART is refused.
- serial_onboarding_protocol: _POSIX_C_SOURCE for strtok_r under -std=c11
  -pedantic (host tests on glibc).
- main/CMakeLists: explicit led_strip REQUIRES so builds work with the
  component manager disabled (vendored components, offline builds).
- release_bins/c6-onboarding: 0.8.12 C6 images built with ESP-IDF v5.4.

Hardware-verified on an ESP32-C6 over cu.usbserial-3120 from the RuView Mac
app: flash -> RUVIEW_HELLO_V1 receipt -> RUVIEW_CONFIG_V1 -> node 3 live at
-34 dBm on the sensing server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rqz356q3Qz6sscTrpy5Jrv
2026-09-11 08:58:46 -04:00
ruv 130afabae8 feat(firmware): add secure macOS USB onboarding
(cherry picked from commit 3373e79f38afef3a1430feeb78ae034e78777c66)
2026-09-10 19:25:13 -04:00
23 changed files with 1432 additions and 75 deletions

No files matched your search

+2 -5
View File
@@ -92,14 +92,11 @@ jobs:
idf.py set-target ${{ matrix.target }}
idf.py build
- name: Build and run host-side ADR-110 unit tests
- name: Build and run host-side firmware unit tests
if: matrix.variant == 'c6-4mb'
working-directory: firmware/esp32-csi-node/test
run: |
make test_adr110
./test_adr110
make test_c6_antenna
./test_c6_antenna
make host_tests
- name: Verify binary size budget
working-directory: firmware/esp32-csi-node
@@ -0,0 +1,93 @@
# ADR 348: Native macOS USB node onboarding
## Status
Accepted and physically qualified with one attached ESP32 S3 and one attached
ESP32 C6 through an unsigned Mac Catalyst development build. Signed
distribution remains a release gate.
## Context
RuView operators currently provision ESP32 nodes with command line tooling.
The macOS app can list server accepted node identities, but it cannot establish
that an attached serial device is RuView firmware, assign a collision free node
identity, preserve its WiFi configuration, or prove that it joined the sensing
server. A UI that merely stores a friendly name would create false success.
Generic shell access from the app would also expand authority beyond the task.
Serial logs can contain operational data, and WiFi credentials must not enter
application diagnostics or provisioning receipts.
## Decision
1. Firmware 0.8.12 provides a bounded line protocol over the physical USB serial
channel. A discovery request carries a random 128 bit nonce. The device
response binds that nonce to chip family, firmware version, current node
identity, private server target, configuration state, and a pseudonymous
64 bit device digest derived from the base MAC under a RuView domain.
2. A configuration request is accepted only for the most recently issued
nonce within 60 seconds. The nonce is single use. Node identity is limited
to 1 through 255, the target is limited to RFC 1918 IPv4, and the target port
is limited to 1 through 65535.
3. Existing WiFi configuration can be preserved. New credentials use strict
base64 framing, bounded lengths, and control character rejection. Firmware
commits NVS before acknowledging and rebooting. Credentials are never
returned, logged, or included in the receipt.
4. The macOS client uses a native serial bridge. It allowlists exact character
device prefixes, configures the port directly, and does not invoke a shell,
Python, or an external process. It rechecks the device digest immediately
before configuration to reduce port substitution risk.
5. The app suggests the lowest unused server node identity and the Mac private
LAN address. Success is shown only after the configured node appears in the
server inventory as active with evidence no older than three seconds.
6. Old firmware is reported as requiring an update. The app must not claim it
provisioned a device based only on generic RuView serial logs.
7. Automatic firmware flashing is outside this decision. Flashing requires an
exact chip and port check, a private backup, a matching image, and captured
boot evidence. Release publication remains a separate maintainer action.
8. The device digest uses the PSA Crypto SHA 256 interface on ESP IDF 5.4 and
6.0. This keeps the stable domain separated identity while avoiding the
legacy Mbed TLS hash API removed by ESP IDF 6.
9. ESP32 C6 mmWave UART defaults use GPIO 4 and GPIO 5. Startup rejects any
configured mmWave pin pair that overlaps the active console RX or TX pins.
This preserves the onboarding input path after a failed mmWave probe.
## Security and privacy consequences
The app gains Mac App Sandbox USB and serial device access, but not general file
or shell authority. The protocol does not authenticate a device with a hardware
root of trust; the digest is stable identification, not a cryptographic device
certificate. Physical USB possession is therefore the trust boundary for this
version. An authenticated fleet identity remains ADR 305 scope.
The largest failure mode is a successful NVS write followed by failure to join
the LAN. The user receives an explicit bounded verification failure instead of
a success screen, while the board retains its committed settings and can be
rescanned over USB.
## Acceptance test
Connect an ESP32 with firmware 0.8.12 to a Mac that is already receiving RuView
nodes. Use only the macOS Add Sensor workflow to preserve WiFi, assign the
lowest unused identity, and set the Mac private address. Pass when the UI
reports the expected chip and firmware, the reassigned node remains fresh for
five minutes, the stale collided identity expires, app restart preserves the
profile, no server health, send, watchdog, panic, or reboot evidence appears,
and no credential appears in app logs or the provisioning receipt.
The 2026-08-31 physical run passed on an ESP32 S3. The measured witness is in
`docs/validation/2026-08-31-macos-usb-node-onboarding.md`.
The 2026-09-11 physical run passed on an ESP32 C6 over
`cu.usbserial-3120`. The Mac app flashed the image, received the nonce bound
hello receipt, preserved node identity 3, committed the server route, and
verified fresh server evidence at minus 34 dBm. The run also exposed and fixed
the GPIO 17 console RX conflict described above.
+1
View File
@@ -115,6 +115,7 @@ Statuses: **Proposed** (under discussion), **Accepted** (approved and/or impleme
| [ADR-344](ADR-344-adaptive-local-installation-discovery.md) | Adaptive Local Installation Discovery | Accepted (local software path) |
| [ADR-346](ADR-346-fail-closed-edge-occupancy-evidence.md) | Fail closed ESP32 occupancy evidence | Accepted (C6 occupancy integrity qualified) |
| [ADR-347](ADR-347-rate-aware-esp32-temporal-sensing.md) | Rate aware ESP32 temporal sensing | Accepted (C6 timing and transport qualified) |
| [ADR-348](ADR-348-native-macos-usb-node-onboarding.md) | Native macOS USB node onboarding with nonce bound discovery and fresh server verification | Accepted and physically qualified on one ESP32 S3; C6 and signed distribution pending |
| [ADR-115](ADR-115-home-assistant-integration.md) | Home Assistant integration via MQTT auto-discovery + Matter bridge (HA-DISCO + HA-FABRIC + HA-MIND) | Accepted (MQTT track) / Proposed (Matter SDK P8b) |
| [ADR-169](ADR-169-adam-mode-light-theme.md) | adam-mode — light theme toggle for the three.js realtime demo | Proposed |
| [ADR-170](ADR-170-yoga-mode-pose-system.md) | yoga-mode — yoga pose detection, classification, and scoring for the three.js realtime demo | Proposed |
@@ -0,0 +1,60 @@
# Native macOS USB onboarding firmware witness
## Evidence class
MEASURED on 2026-08-31 with one physical ESP32 S3. This is not an ESP32 C6 or
release distribution qualification.
## Hardware and firmware
The board was identified by the flashing tool as ESP32 S3 QFN56 revision 0.2
with 2 MB embedded PSRAM and 8 MB flash. Firmware 0.8.9 was built for ESP32 S3
with the displayless DevKitC defaults. The private configuration region was
backed up before flashing. The flash operation omitted NVS, preserving the
existing WiFi configuration.
No base MAC, WiFi credential, device digest, or raw CSI is retained in this
witness.
## Results
1. The initial native USB scan failed because the ESP-IDF secondary USB
Serial/JTAG console mirrors output but does not forward input to STDIN.
2. Firmware was corrected to read the bounded onboarding protocol from the
secondary VFS while retaining STDIN for UART console boards.
3. A direct nonce challenge then returned ESP32 S3, firmware 0.8.9, configured
state, the current node identity, and the private server route.
4. The Mac app changed the collided identity from 1 to unused identity 5 while
preserving WiFi. It showed success only after receiving fresh server
evidence at minus 56 dBm.
5. A five minute monitor collected 299 samples with zero node 5 missing events,
zero node 5 stale events, zero server health failures, minus 57 to minus 50
dBm RSSI, and 216 ms maximum packet age.
6. Firmware uptime exceeded 376 seconds. Captured logs advanced from CSI
callback 10,600 through 11,000, emitted a node 5 synchronization packet, and
showed ESP NOW transmit failures equal to zero without reboot, watchdog, or
panic evidence.
7. The final channel isolation hardening image was flashed and challenged
again. A following 60 sample check had zero missing, stale, or health failure
events, a maximum packet age of 100 ms, and minus 57 to minus 50 dBm RSSI.
## Software gates
The nonce and configuration parser host test passed, the complete ESP32 S3
firmware image built under ESP-IDF 5.4, and the application image occupied
941,056 bytes with 55 percent of the smallest application partition free.
## Remaining gates
New WiFi credential entry, signed Mac distribution, and cross platform serial
bridges remain unmeasured.
## ESP32 C6 follow up
On 2026-09-11, an ESP32 C6 on `cu.usbserial-3120` completed the Mac app flash,
hello, configure, and verify sequence with firmware 0.8.12. The app preserved
the existing node identity 3 and verified fresh sensing server evidence at
minus 34 dBm. The board returned `RUVIEW_HELLO_OK_V1` within the bounded serial
exchange after the C6 mmWave defaults moved from the UART0 RX conflict on GPIO
17 to GPIO 4 and GPIO 5. This closes only the physical C6 onboarding gate. It
does not qualify new WiFi credential entry or signed distribution.
+19
View File
@@ -19,6 +19,25 @@ This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 (pr
> | **Fall detection** | Phase acceleration threshold | Configurable sensitivity |
> | **Programmable sensing** | WASM modules loaded over HTTP | Hot-swap, no reflash |
## Firmware 0.8.12: guided Mac onboarding
Firmware 0.8.12 adds the protocol used by the RuView macOS Add
Sensor workflow. A directly attached S3 or C6 can identify itself through a
nonce bound USB serial receipt, preserve its current WiFi settings, accept an
unused node ID and private LAN server address, then reboot. The Mac app does
not report success until the sensing server observes that node as fresh.
This is not a firmware flashing API. A board on 0.8.8 or earlier must receive a
matching chip build through the documented backup and flash process once. The
USB protocol rejects public target addresses and invalid identifiers. New WiFi
credentials are bounded, committed only in NVS, and never included in the
receipt or diagnostic output. See
[ADR 348](../../docs/adr/ADR-348-native-macos-usb-node-onboarding.md).
On ESP32 C6, the default optional mmWave UART uses GPIO 4 and GPIO 5. Firmware
refuses any mmWave configuration that overlaps the active console pins. This
prevents a sensor probe from taking over UART0 RX and disabling Mac onboarding.
## Firmware 0.8.8 in plain language
Release 0.8.8 makes the sensing stream more internally consistent and easier
@@ -1,6 +1,7 @@
set(SRCS
"main.c" "csi_collector.c" "node_log.c"
"thermal.c" "stream_sender.c" "nvs_config.c"
"serial_onboarding.c" "serial_onboarding_protocol.c"
"edge_processing.c" "ota_update.c" "power_mgmt.c"
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
"mmwave_sensor.c"
@@ -43,6 +44,8 @@ set(REQUIRES
driver
lwip
mbedtls
efuse
led_strip
)
# ADR-110: C6-only components — pulled in when building for esp32c6.
+9
View File
@@ -25,6 +25,7 @@
#include "thermal.h"
#include "stream_sender.h"
#include "nvs_config.h"
#include "serial_onboarding.h"
#include "edge_processing.h"
#include "ota_update.h"
#include "power_mgmt.h"
@@ -341,6 +342,14 @@ void app_main(void)
* ownership of GPIO3 and GPIO14 because the default implementation is a
* no-op unless CONFIG_C6_XIAO_ANTENNA_SELECT is enabled. */
ESP_ERROR_CHECK(c6_xiao_antenna_apply());
/* The native Mac onboarding bridge must remain available even when WiFi
* credentials or the aggregator address are wrong. Start it before any
* blocking network initialization. The protocol is physical-USB-only,
* nonce-bound, bounded, and never prints credentials. */
esp_err_t onboarding_ret = serial_onboarding_start(&g_nvs_config);
if (onboarding_ret != ESP_OK) {
ESP_LOGW(TAG, "USB onboarding unavailable: %s", esp_err_to_name(onboarding_ret));
}
/* Onboard WS2812. C6 wires the LED to GPIO 8; S3 to GPIO 38 (DevKitC-1 v1.0)
* or GPIO 48 (DevKitC-1 v1.1 / N16R8 — see #962). On S3 we drive 48 (the
@@ -503,9 +503,35 @@ esp_err_t mmwave_sensor_init(int uart_tx_pin, int uart_rx_pin)
return ESP_OK;
#else
/* Default probe pins must never collide with the UART0 console. On the
* ESP32-C6 the console is TX=GPIO16 / RX=GPIO17, so the legacy 17/18 pair
* routed UART1 TX onto the console RX line and silently killed serial
* input (USB onboarding, monitor commands) even when no sensor was found,
* because uart_driver_delete() does not undo the GPIO matrix routing. */
#if defined(CONFIG_IDF_TARGET_ESP32C6)
if (uart_tx_pin < 0) uart_tx_pin = 4;
if (uart_rx_pin < 0) uart_rx_pin = 5;
#else
if (uart_tx_pin < 0) uart_tx_pin = 17;
if (uart_rx_pin < 0) uart_rx_pin = 18;
#endif
#if defined(CONFIG_ESP_CONSOLE_UART_CUSTOM)
const int console_tx = CONFIG_ESP_CONSOLE_UART_TX_GPIO;
const int console_rx = CONFIG_ESP_CONSOLE_UART_RX_GPIO;
#elif defined(CONFIG_IDF_TARGET_ESP32C6)
const int console_tx = 16, console_rx = 17; /* U0TXD / U0RXD IOMUX pins */
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
const int console_tx = 43, console_rx = 44;
#else
const int console_tx = 1, console_rx = 3;
#endif
if (uart_tx_pin == console_tx || uart_tx_pin == console_rx ||
uart_rx_pin == console_tx || uart_rx_pin == console_rx) {
ESP_LOGW(TAG, "mmWave probe pins (TX=%d, RX=%d) overlap the console UART (TX=%d, RX=%d); skipping probe",
uart_tx_pin, uart_rx_pin, console_tx, console_rx);
return ESP_ERR_INVALID_ARG;
}
/* Install UART driver at MR60 baud (will be changed during probe). */
uart_config_t uart_config = {
+1 -1
View File
@@ -109,7 +109,7 @@ void nvs_config_load(nvs_config_t *cfg)
len = sizeof(buf);
if (nvs_get_str(handle, "ssid", buf, &len) == ESP_OK && len > 1) {
strlcpy(cfg->wifi_ssid, buf, sizeof(cfg->wifi_ssid));
ESP_LOGI(TAG, "NVS override: ssid=%s", cfg->wifi_ssid);
ESP_LOGI(TAG, "NVS override: ssid=<configured>");
}
/* WiFi password */
@@ -0,0 +1,204 @@
#include "serial_onboarding.h"
#include "serial_onboarding_protocol.h"
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include "esp_app_desc.h"
#include "esp_efuse.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs.h"
#include "psa/crypto.h"
static const char *TAG = "serial_onboard";
static nvs_config_t s_current_config;
static char s_issued_nonce[RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1];
static int64_t s_nonce_issued_at_us;
#define ONBOARDING_LINE_MAX 384
#define ONBOARDING_NONCE_TTL_US (60LL * 1000LL * 1000LL)
static const char *chip_name(void)
{
#if defined(CONFIG_IDF_TARGET_ESP32C6)
return "esp32c6";
#elif defined(CONFIG_IDF_TARGET_ESP32S3)
return "esp32s3";
#else
return "esp32";
#endif
}
static void device_digest(char output[17])
{
uint8_t base_mac[6] = {0};
uint8_t digest[32] = {0};
static const uint8_t domain[] = "ruview-device-v1";
uint8_t digest_input[sizeof(domain) - 1 + sizeof(base_mac)];
size_t digest_length = 0;
memcpy(digest_input, domain, sizeof(domain) - 1);
const esp_err_t mac_result = esp_efuse_mac_get_default(base_mac);
if (mac_result == ESP_OK) {
memcpy(digest_input + sizeof(domain) - 1, base_mac, sizeof(base_mac));
}
if (mac_result == ESP_OK &&
psa_crypto_init() == PSA_SUCCESS &&
psa_hash_compute(PSA_ALG_SHA_256, digest_input, sizeof(digest_input),
digest, sizeof(digest), &digest_length) == PSA_SUCCESS &&
digest_length == sizeof(digest)) {
for (size_t index = 0; index < 8; index++) {
snprintf(output + index * 2, 3, "%02x", digest[index]);
}
} else {
memcpy(output, "0000000000000000", 17);
}
}
static void emit_hello(const char *nonce)
{
const esp_app_desc_t *description = esp_app_get_description();
char digest[17] = {0};
device_digest(digest);
const bool configured = s_current_config.wifi_ssid[0] != '\0' &&
s_current_config.target_ip[0] != '\0';
printf("RUVIEW_HELLO_OK_V1 nonce=%s chip=%s version=%s node_id=%u "
"target_ip=%s target_port=%u configured=%u device_digest=%s\n",
nonce, chip_name(), description->version,
(unsigned)s_current_config.node_id, s_current_config.target_ip,
(unsigned)s_current_config.target_port, configured ? 1U : 0U, digest);
fflush(stdout);
}
static esp_err_t commit_config(const ruview_onboarding_config_t *config)
{
nvs_handle_t handle;
esp_err_t result = nvs_open("csi_cfg", NVS_READWRITE, &handle);
if (result != ESP_OK) return result;
if (!config->preserve_wifi) {
result = nvs_set_str(handle, "ssid", config->ssid);
if (result == ESP_OK) result = nvs_set_str(handle, "password", config->password);
}
if (result == ESP_OK) result = nvs_set_str(handle, "target_ip", config->target_ip);
if (result == ESP_OK) result = nvs_set_u16(handle, "target_port", config->target_port);
if (result == ESP_OK) result = nvs_set_u8(handle, "node_id", config->node_id);
if (result == ESP_OK) result = nvs_commit(handle);
nvs_close(handle);
return result;
}
static void process_line(char *line)
{
char nonce[RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1] = {0};
if (ruview_onboarding_parse_hello(line, nonce)) {
memcpy(s_issued_nonce, nonce, sizeof(s_issued_nonce));
s_nonce_issued_at_us = esp_timer_get_time();
emit_hello(nonce);
return;
}
if (strncmp(line, "RUVIEW_CONFIG_V1 ", 17) != 0) return;
ruview_onboarding_config_t request;
const ruview_onboarding_result_t parsed = ruview_onboarding_parse_config(line, &request);
if (parsed != RUVIEW_ONBOARDING_OK) {
printf("RUVIEW_CONFIG_ERR_V1 reason=%s\n", ruview_onboarding_result_name(parsed));
fflush(stdout);
return;
}
const int64_t age_us = esp_timer_get_time() - s_nonce_issued_at_us;
if (s_issued_nonce[0] == '\0' || strcmp(request.nonce, s_issued_nonce) != 0 ||
age_us < 0 || age_us > ONBOARDING_NONCE_TTL_US) {
printf("RUVIEW_CONFIG_ERR_V1 nonce=%s reason=claim_expired\n", request.nonce);
fflush(stdout);
return;
}
s_issued_nonce[0] = '\0';
const esp_err_t result = commit_config(&request);
if (result != ESP_OK) {
printf("RUVIEW_CONFIG_ERR_V1 nonce=%s reason=nvs_commit code=%s\n",
request.nonce, esp_err_to_name(result));
fflush(stdout);
return;
}
printf("RUVIEW_CONFIG_OK_V1 nonce=%s node_id=%u rebooting=1\n",
request.nonce, (unsigned)request.node_id);
fflush(stdout);
vTaskDelay(pdMS_TO_TICKS(400));
esp_restart();
}
static void consume_bytes(const uint8_t *input, ssize_t count, char *line,
size_t *length, bool *overflow)
{
for (ssize_t index = 0; index < count; index++) {
const uint8_t byte = input[index];
if (byte == '\r') continue;
if (byte == '\n') {
if (!*overflow && *length > 0) {
line[*length] = '\0';
process_line(line);
}
*length = 0;
*overflow = false;
continue;
}
if (byte < 0x20 || byte == 0x7f) continue;
if (*length + 1 < ONBOARDING_LINE_MAX) line[(*length)++] = (char)byte;
else *overflow = true;
}
}
static void serial_onboarding_task(void *argument)
{
(void)argument;
const int original_flags = fcntl(STDIN_FILENO, F_GETFL, 0);
if (original_flags >= 0) (void)fcntl(STDIN_FILENO, F_SETFL, original_flags | O_NONBLOCK);
/* ESP-IDF secondary consoles mirror output but deliberately do not forward
* input to STDIN. Native S3/C6 USB Serial/JTAG therefore needs a bounded
* direct VFS reader while UART-console boards continue to use STDIN. */
const int usb_fd = open("/dev/secondary", O_RDONLY | O_NONBLOCK);
char primary_line[ONBOARDING_LINE_MAX];
char usb_line[ONBOARDING_LINE_MAX];
size_t primary_length = 0;
size_t usb_length = 0;
bool primary_overflow = false;
bool usb_overflow = false;
for (;;) {
uint8_t input[64];
const ssize_t count = read(STDIN_FILENO, input, sizeof(input));
if (count > 0) {
consume_bytes(input, count, primary_line, &primary_length, &primary_overflow);
}
else if (count < 0 && errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGW(TAG, "serial input unavailable: errno=%d", errno);
vTaskDelay(pdMS_TO_TICKS(500));
}
if (usb_fd >= 0) {
const ssize_t usb_count = read(usb_fd, input, sizeof(input));
if (usb_count > 0) {
consume_bytes(input, usb_count, usb_line, &usb_length, &usb_overflow);
}
}
vTaskDelay(pdMS_TO_TICKS(20));
}
}
esp_err_t serial_onboarding_start(const nvs_config_t *config)
{
if (config == NULL) return ESP_ERR_INVALID_ARG;
memcpy(&s_current_config, config, sizeof(s_current_config));
const BaseType_t created = xTaskCreate(serial_onboarding_task, "serial_onboard",
4096, NULL, 4, NULL);
if (created != pdPASS) return ESP_ERR_NO_MEM;
ESP_LOGI(TAG, "bounded serial onboarding ready");
return ESP_OK;
}
@@ -0,0 +1,15 @@
#ifndef SERIAL_ONBOARDING_H
#define SERIAL_ONBOARDING_H
#include "esp_err.h"
#include "nvs_config.h"
/** Start the bounded USB serial onboarding listener.
*
* The listener exposes device metadata and accepts one nonce-bound
* configuration transaction. It never prints WiFi credentials and reboots
* only after a successful atomic NVS commit.
*/
esp_err_t serial_onboarding_start(const nvs_config_t *config);
#endif
@@ -0,0 +1,214 @@
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L /* strtok_r under -std=c11 -pedantic on glibc */
#endif
#include "serial_onboarding_protocol.h"
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define HELLO_COMMAND "RUVIEW_HELLO_V1"
#define CONFIG_COMMAND "RUVIEW_CONFIG_V1"
static bool valid_nonce(const char *value)
{
if (value == NULL || strlen(value) != RUVIEW_ONBOARDING_NONCE_HEX_LEN) return false;
for (size_t index = 0; index < RUVIEW_ONBOARDING_NONCE_HEX_LEN; index++) {
const char c = value[index];
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F'))) return false;
}
return true;
}
static bool parse_u32(const char *value, uint32_t maximum, uint32_t *output)
{
if (value == NULL || value[0] == '\0' || output == NULL) return false;
uint32_t result = 0;
for (const char *cursor = value; *cursor != '\0'; cursor++) {
if (*cursor < '0' || *cursor > '9') return false;
const uint32_t digit = (uint32_t)(*cursor - '0');
if (result > (maximum - digit) / 10U) return false;
result = result * 10U + digit;
}
*output = result;
return true;
}
static bool valid_ipv4(const char *value)
{
if (value == NULL || value[0] == '\0' || strlen(value) > 15) return false;
unsigned octets = 0;
const char *cursor = value;
while (*cursor != '\0') {
if (octets == 4) return false;
if (*cursor == '.') return false;
uint32_t octet = 0;
unsigned digits = 0;
while (*cursor != '\0' && *cursor != '.') {
if (*cursor < '0' || *cursor > '9' || digits == 3) return false;
octet = octet * 10U + (uint32_t)(*cursor - '0');
if (octet > 255U) return false;
digits++;
cursor++;
}
if (digits == 0) return false;
octets++;
if (*cursor == '.') cursor++;
}
return octets == 4;
}
static bool valid_private_ipv4(const char *value)
{
if (!valid_ipv4(value)) return false;
unsigned first = 0;
unsigned second = 0;
if (sscanf(value, "%u.%u", &first, &second) != 2) return false;
return first == 10U || (first == 172U && second >= 16U && second <= 31U) ||
(first == 192U && second == 168U);
}
static int base64_value(char c)
{
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
static bool decode_base64(const char *encoded, uint8_t *output,
size_t output_capacity, size_t *output_length)
{
if (encoded == NULL || output == NULL || output_length == NULL) return false;
const size_t length = strlen(encoded);
if (length == 0 || length % 4 != 0) return false;
size_t written = 0;
for (size_t index = 0; index < length; index += 4) {
int values[4];
unsigned padding = 0;
for (unsigned offset = 0; offset < 4; offset++) {
const char c = encoded[index + offset];
if (c == '=') {
if (offset < 2 || index + 4 != length) return false;
values[offset] = 0;
padding++;
} else {
if (padding != 0) return false;
values[offset] = base64_value(c);
if (values[offset] < 0) return false;
}
}
if (padding > 2) return false;
const uint32_t block = ((uint32_t)values[0] << 18) |
((uint32_t)values[1] << 12) |
((uint32_t)values[2] << 6) |
(uint32_t)values[3];
const unsigned bytes = 3U - padding;
if (written + bytes > output_capacity) return false;
output[written++] = (uint8_t)(block >> 16);
if (bytes > 1) output[written++] = (uint8_t)(block >> 8);
if (bytes > 2) output[written++] = (uint8_t)block;
}
*output_length = written;
return true;
}
static bool printable_secret(const uint8_t *bytes, size_t length)
{
if (bytes == NULL) return false;
for (size_t index = 0; index < length; index++) {
if (bytes[index] < 0x20 || bytes[index] == 0x7f) return false;
}
return true;
}
int ruview_onboarding_parse_hello(
const char *line,
char nonce[RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1])
{
if (line == NULL || nonce == NULL) return 0;
const size_t prefix_length = strlen(HELLO_COMMAND);
if (strncmp(line, HELLO_COMMAND, prefix_length) != 0 || line[prefix_length] != ' ')
return 0;
const char *candidate = line + prefix_length + 1;
if (!valid_nonce(candidate)) return 0;
memcpy(nonce, candidate, RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1);
return 1;
}
ruview_onboarding_result_t ruview_onboarding_parse_config(
const char *line,
ruview_onboarding_config_t *config)
{
if (line == NULL || config == NULL || strlen(line) >= 384) return RUVIEW_ONBOARDING_ERR_FORMAT;
char copy[384];
memcpy(copy, line, strlen(line) + 1);
char *tokens[7] = {0};
size_t token_count = 0;
char *save = NULL;
for (char *token = strtok_r(copy, " ", &save); token != NULL;
token = strtok_r(NULL, " ", &save)) {
if (token_count == 7) return RUVIEW_ONBOARDING_ERR_FORMAT;
tokens[token_count++] = token;
}
if (token_count != 7 || strcmp(tokens[0], CONFIG_COMMAND) != 0)
return RUVIEW_ONBOARDING_ERR_FORMAT;
if (!valid_nonce(tokens[1])) return RUVIEW_ONBOARDING_ERR_NONCE;
uint32_t node_id = 0;
if (!parse_u32(tokens[2], 255, &node_id) || node_id == 0)
return RUVIEW_ONBOARDING_ERR_NODE_ID;
if (!valid_private_ipv4(tokens[3])) return RUVIEW_ONBOARDING_ERR_TARGET_IP;
uint32_t target_port = 0;
if (!parse_u32(tokens[4], 65535, &target_port) || target_port == 0)
return RUVIEW_ONBOARDING_ERR_TARGET_PORT;
const bool preserve_wifi = strcmp(tokens[5], "KEEP") == 0 &&
strcmp(tokens[6], "KEEP") == 0;
if ((strcmp(tokens[5], "KEEP") == 0) != (strcmp(tokens[6], "KEEP") == 0))
return RUVIEW_ONBOARDING_ERR_FORMAT;
uint8_t ssid[RUVIEW_ONBOARDING_SSID_MAX_BYTES];
size_t ssid_length = 0;
if (!preserve_wifi && (!decode_base64(tokens[5], ssid, sizeof(ssid), &ssid_length) ||
ssid_length == 0 || !printable_secret(ssid, ssid_length)))
return RUVIEW_ONBOARDING_ERR_SSID;
uint8_t password[RUVIEW_ONBOARDING_PASSWORD_MAX_BYTES];
size_t password_length = 0;
if (!preserve_wifi && (!decode_base64(tokens[6], password, sizeof(password), &password_length) ||
password_length < 8 || !printable_secret(password, password_length)))
return RUVIEW_ONBOARDING_ERR_PASSWORD;
memset(config, 0, sizeof(*config));
memcpy(config->nonce, tokens[1], RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1);
config->node_id = (uint8_t)node_id;
memcpy(config->target_ip, tokens[3], strlen(tokens[3]) + 1);
config->target_port = (uint16_t)target_port;
config->preserve_wifi = preserve_wifi;
if (!preserve_wifi) {
memcpy(config->ssid, ssid, ssid_length);
config->ssid[ssid_length] = '\0';
memcpy(config->password, password, password_length);
config->password[password_length] = '\0';
}
return RUVIEW_ONBOARDING_OK;
}
const char *ruview_onboarding_result_name(ruview_onboarding_result_t result)
{
switch (result) {
case RUVIEW_ONBOARDING_OK: return "ok";
case RUVIEW_ONBOARDING_ERR_FORMAT: return "format";
case RUVIEW_ONBOARDING_ERR_NONCE: return "nonce";
case RUVIEW_ONBOARDING_ERR_NODE_ID: return "node_id";
case RUVIEW_ONBOARDING_ERR_TARGET_IP: return "target_ip";
case RUVIEW_ONBOARDING_ERR_TARGET_PORT: return "target_port";
case RUVIEW_ONBOARDING_ERR_SSID: return "ssid";
case RUVIEW_ONBOARDING_ERR_PASSWORD: return "password";
default: return "unknown";
}
}
@@ -0,0 +1,42 @@
#ifndef SERIAL_ONBOARDING_PROTOCOL_H
#define SERIAL_ONBOARDING_PROTOCOL_H
#include <stddef.h>
#include <stdbool.h>
#include <stdint.h>
#define RUVIEW_ONBOARDING_NONCE_HEX_LEN 32
#define RUVIEW_ONBOARDING_SSID_MAX_BYTES 32
#define RUVIEW_ONBOARDING_PASSWORD_MAX_BYTES 63
typedef struct {
char nonce[RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1];
uint8_t node_id;
char target_ip[16];
uint16_t target_port;
bool preserve_wifi;
char ssid[RUVIEW_ONBOARDING_SSID_MAX_BYTES + 1];
char password[RUVIEW_ONBOARDING_PASSWORD_MAX_BYTES + 1];
} ruview_onboarding_config_t;
typedef enum {
RUVIEW_ONBOARDING_OK = 0,
RUVIEW_ONBOARDING_ERR_FORMAT,
RUVIEW_ONBOARDING_ERR_NONCE,
RUVIEW_ONBOARDING_ERR_NODE_ID,
RUVIEW_ONBOARDING_ERR_TARGET_IP,
RUVIEW_ONBOARDING_ERR_TARGET_PORT,
RUVIEW_ONBOARDING_ERR_SSID,
RUVIEW_ONBOARDING_ERR_PASSWORD,
} ruview_onboarding_result_t;
int ruview_onboarding_parse_hello(const char *line,
char nonce[RUVIEW_ONBOARDING_NONCE_HEX_LEN + 1]);
ruview_onboarding_result_t ruview_onboarding_parse_config(
const char *line,
ruview_onboarding_config_t *config);
const char *ruview_onboarding_result_name(ruview_onboarding_result_t result);
#endif
@@ -0,0 +1,4 @@
126a4b9f305e207aa2e0db93caf09e27f77b3bee4b4f618a0cb963f10a4364bc bootloader.bin
183c4557a93f6f0be1680f45988b7c7320130c8e0ed5d9c900add038d5cc84fb esp32-csi-node.bin
7d2c7ac4888bfd75cd5f56e8d61f69595121183afc81556c876732fd3782c62f ota_data_initial.bin
4c2cc4ffd52641e23b779bd57b3908014083ac3c1aab395756478c89e70d81f0 partition-table.bin
@@ -0,0 +1 @@
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Line truncated
+15 -5
View File
@@ -55,10 +55,10 @@ FUZZ_JOBS ?= 1
.PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \
test_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \
test_thermal run_thermal test_csi_sanitize run_csi_sanitize \
test_c6_antenna run_c6_antenna
test_c6_antenna run_c6_antenna test_serial_onboarding run_serial_onboarding
all: fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \
test_thermal test_csi_sanitize test_c6_antenna
test_thermal test_csi_sanitize test_c6_antenna test_serial_onboarding
# --- XIAO ESP32-C6 RF switch selection ---
# Host-side truth table for the GPIO14 antenna selector. GPIO3 is always driven
@@ -122,9 +122,19 @@ test_thermal: test_thermal_hysteresis.c $(MAIN_DIR)/thermal.h
run_thermal: test_thermal
./test_thermal
# --- Native Mac USB onboarding protocol ---
# Tests the production parser and secret validation without ESP-IDF. This pins
# nonce shape, IPv4 and node bounds, base64 handling, and control-byte denial.
test_serial_onboarding: test_serial_onboarding_protocol.c $(MAIN_DIR)/serial_onboarding_protocol.c $(MAIN_DIR)/serial_onboarding_protocol.h
cc -std=c11 -Wall -Wextra -Werror -pedantic -I$(MAIN_DIR) -o $@ \
test_serial_onboarding_protocol.c $(MAIN_DIR)/serial_onboarding_protocol.c
run_serial_onboarding: test_serial_onboarding
./test_serial_onboarding
host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize \
run_c6_antenna
@echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna)"
run_c6_antenna run_serial_onboarding
@echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna + USB onboarding)"
# --- Serialize fuzzer ---
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
@@ -161,5 +171,5 @@ run_all: run_serialize run_edge run_nvs
clean:
rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \
test_thermal test_csi_sanitize test_c6_antenna
test_thermal test_csi_sanitize test_c6_antenna test_serial_onboarding
rm -rf corpus_serialize/ corpus_edge/ corpus_nvs/
@@ -0,0 +1,68 @@
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include "../main/serial_onboarding_protocol.h"
static void test_hello(void)
{
char nonce[33] = {0};
assert(ruview_onboarding_parse_hello(
"RUVIEW_HELLO_V1 00112233445566778899aabbccddeeff", nonce));
assert(strcmp(nonce, "00112233445566778899aabbccddeeff") == 0);
assert(!ruview_onboarding_parse_hello("RUVIEW_HELLO_V1 bad", nonce));
assert(!ruview_onboarding_parse_hello(
"RUVIEW_HELLO_V1 00112233445566778899aabbccddeeff extra", nonce));
}
static void test_config(void)
{
ruview_onboarding_config_t config;
assert(ruview_onboarding_parse_config(
"RUVIEW_CONFIG_V1 00112233445566778899aabbccddeeff 5 192.168.1.166 5005 "
"UnVWaWV3IExhYg== U3Ryb25nUGFzc3dvcmQ=", &config) == RUVIEW_ONBOARDING_OK);
assert(config.node_id == 5);
assert(config.target_port == 5005);
assert(strcmp(config.target_ip, "192.168.1.166") == 0);
assert(strcmp(config.ssid, "RuView Lab") == 0);
assert(strcmp(config.password, "StrongPassword") == 0);
assert(!config.preserve_wifi);
assert(ruview_onboarding_parse_config(
"RUVIEW_CONFIG_V1 00112233445566778899aabbccddeeff 6 192.168.1.166 5005 KEEP KEEP",
&config) == RUVIEW_ONBOARDING_OK);
assert(config.preserve_wifi);
}
static void test_rejections(void)
{
ruview_onboarding_config_t config;
const char *prefix = "RUVIEW_CONFIG_V1 00112233445566778899aabbccddeeff";
char line[512];
snprintf(line, sizeof(line), "%s 0 192.168.1.2 5005 UnVWaWV3 U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_NODE_ID);
snprintf(line, sizeof(line), "%s 5 999.1.1.1 5005 UnVWaWV3 U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_TARGET_IP);
snprintf(line, sizeof(line), "%s 5 8.8.8.8 5005 UnVWaWV3 U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_TARGET_IP);
snprintf(line, sizeof(line), "%s 5 172.32.0.1 5005 UnVWaWV3 U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_TARGET_IP);
snprintf(line, sizeof(line), "%s 5 192.168.1.2 0 UnVWaWV3 U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_TARGET_PORT);
snprintf(line, sizeof(line), "%s 5 192.168.1.2 5005 !!!! U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_SSID);
snprintf(line, sizeof(line), "%s 5 192.168.1.2 5005 UnVWaWV3 c2hvcnQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_PASSWORD);
snprintf(line, sizeof(line), "%s 5 192.168.1.2 5005 UnVWaWV3 UGFzc3dvcmQxCg==", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_PASSWORD);
snprintf(line, sizeof(line), "%s 5 192.168.1.2 5005 KEEP U3Ryb25nUGFzc3dvcmQ=", prefix);
assert(ruview_onboarding_parse_config(line, &config) == RUVIEW_ONBOARDING_ERR_FORMAT);
}
int main(void)
{
test_hello();
test_config();
test_rejections();
puts("serial onboarding protocol tests passed");
return 0;
}
+1 -1
View File
@@ -1 +1 @@
0.8.10
0.8.12
@@ -15,6 +15,10 @@ use wifi_densepose_signal::ruvsense::field_model::{
use super::score_to_person_count;
/// Versioned schema for the immutable receipt emitted after field-model calibration.
pub const CALIBRATION_MODEL_RECEIPT_SCHEMA: &str =
"ruview.calibration.field-model-receipt.v1";
/// Length-only canonicalizer for calibration frames (issue #1170 pattern,
/// shared with `multistatic_bridge`). Raw ESP32 amplitudes arrive at the
/// hardware's native width (HT20 ≈ 64, HT40 ≈ 128/192); the FieldModel is
File diff suppressed because it is too large. Load diff