mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-19 05:10:13 -05:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbf4788628 | ||
|
|
cdad032296 | ||
|
|
0decc5c3fa | ||
|
|
d4a3c7f17f | ||
|
|
2c0abcc802 | ||
|
|
f1dbaac452 | ||
|
|
33a9e90896 | ||
|
|
bbbb14169d | ||
|
|
4fbe58f736 | ||
|
|
a64e9e98fb | ||
|
|
130afabae8 |
No files matched your search
@@ -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.
|
||||
@@ -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,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.
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
�������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������Line truncated
|
||||
Binary file not shown.
@@ -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 @@
|
||||
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
Reference in new issue
Block a user