mirror of
https://github.com/ruvnet/RuView.git
synced 2026-10-21 22:22:33 -05:00
feat(firmware): add secure macOS USB onboarding
(cherry picked from commit 3373e79f38afef3a1430feeb78ae034e78777c66)
This commit is contained in:
1 parent
00a42d7cda
commit
130afabae8
15 files changed
+714
-12
No files matched your search
@@ -92,14 +92,11 @@ jobs:
|
|||||||
idf.py set-target ${{ matrix.target }}
|
idf.py set-target ${{ matrix.target }}
|
||||||
idf.py build
|
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'
|
if: matrix.variant == 'c6-4mb'
|
||||||
working-directory: firmware/esp32-csi-node/test
|
working-directory: firmware/esp32-csi-node/test
|
||||||
run: |
|
run: |
|
||||||
make test_adr110
|
make host_tests
|
||||||
./test_adr110
|
|
||||||
make test_c6_antenna
|
|
||||||
./test_c6_antenna
|
|
||||||
|
|
||||||
- name: Verify binary size budget
|
- name: Verify binary size budget
|
||||||
working-directory: firmware/esp32-csi-node
|
working-directory: firmware/esp32-csi-node
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# ADR 348: Native macOS USB node onboarding
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted and physically qualified with one attached ESP32 S3 and an unsigned
|
||||||
|
Mac Catalyst development build. ESP32 C6 qualification and signed distribution
|
||||||
|
remain release gates.
|
||||||
|
|
||||||
|
## 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.9 adds 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.
|
||||||
|
|
||||||
|
## 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.9 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`.
|
||||||
@@ -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-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-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-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-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-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 |
|
| [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,50 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
Physical ESP32 C6, new WiFi credential entry, signed Mac distribution, and
|
||||||
|
cross platform serial bridges remain unmeasured.
|
||||||
@@ -19,6 +19,21 @@ This firmware captures WiFi Channel State Information (CSI) from an ESP32-S3 (pr
|
|||||||
> | **Fall detection** | Phase acceleration threshold | Configurable sensitivity |
|
> | **Fall detection** | Phase acceleration threshold | Configurable sensitivity |
|
||||||
> | **Programmable sensing** | WASM modules loaded over HTTP | Hot-swap, no reflash |
|
> | **Programmable sensing** | WASM modules loaded over HTTP | Hot-swap, no reflash |
|
||||||
|
|
||||||
|
## Development firmware 0.8.9: guided Mac onboarding
|
||||||
|
|
||||||
|
The unreleased 0.8.9 source 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).
|
||||||
|
|
||||||
## Firmware 0.8.8 in plain language
|
## Firmware 0.8.8 in plain language
|
||||||
|
|
||||||
Release 0.8.8 makes the sensing stream more internally consistent and easier
|
Release 0.8.8 makes the sensing stream more internally consistent and easier
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
set(SRCS
|
set(SRCS
|
||||||
"main.c" "csi_collector.c"
|
"main.c" "csi_collector.c"
|
||||||
"thermal.c" "stream_sender.c" "nvs_config.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"
|
"edge_processing.c" "ota_update.c" "power_mgmt.c"
|
||||||
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
|
"wasm_runtime.c" "wasm_upload.c" "rvf_parser.c"
|
||||||
"mmwave_sensor.c"
|
"mmwave_sensor.c"
|
||||||
@@ -40,6 +41,7 @@ set(REQUIRES
|
|||||||
driver
|
driver
|
||||||
lwip
|
lwip
|
||||||
mbedtls
|
mbedtls
|
||||||
|
efuse
|
||||||
)
|
)
|
||||||
|
|
||||||
# ADR-110: C6-only components — pulled in when building for esp32c6.
|
# ADR-110: C6-only components — pulled in when building for esp32c6.
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
#include "thermal.h"
|
#include "thermal.h"
|
||||||
#include "stream_sender.h"
|
#include "stream_sender.h"
|
||||||
#include "nvs_config.h"
|
#include "nvs_config.h"
|
||||||
|
#include "serial_onboarding.h"
|
||||||
#include "edge_processing.h"
|
#include "edge_processing.h"
|
||||||
#include "ota_update.h"
|
#include "ota_update.h"
|
||||||
#include "power_mgmt.h"
|
#include "power_mgmt.h"
|
||||||
@@ -336,6 +337,14 @@ void app_main(void)
|
|||||||
* ownership of GPIO3 and GPIO14 because the default implementation is a
|
* ownership of GPIO3 and GPIO14 because the default implementation is a
|
||||||
* no-op unless CONFIG_C6_XIAO_ANTENNA_SELECT is enabled. */
|
* no-op unless CONFIG_C6_XIAO_ANTENNA_SELECT is enabled. */
|
||||||
ESP_ERROR_CHECK(c6_xiao_antenna_apply());
|
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)
|
/* 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
|
* or GPIO 48 (DevKitC-1 v1.1 / N16R8 — see #962). On S3 we drive 48 (the
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ void nvs_config_load(nvs_config_t *cfg)
|
|||||||
len = sizeof(buf);
|
len = sizeof(buf);
|
||||||
if (nvs_get_str(handle, "ssid", buf, &len) == ESP_OK && len > 1) {
|
if (nvs_get_str(handle, "ssid", buf, &len) == ESP_OK && len > 1) {
|
||||||
strlcpy(cfg->wifi_ssid, buf, sizeof(cfg->wifi_ssid));
|
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 */
|
/* WiFi password */
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
#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 "mbedtls/sha256.h"
|
||||||
|
#include "nvs.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";
|
||||||
|
mbedtls_sha256_context context;
|
||||||
|
mbedtls_sha256_init(&context);
|
||||||
|
if (esp_efuse_mac_get_default(base_mac) == ESP_OK &&
|
||||||
|
mbedtls_sha256_starts(&context, 0) == 0 &&
|
||||||
|
mbedtls_sha256_update(&context, domain, sizeof(domain) - 1) == 0 &&
|
||||||
|
mbedtls_sha256_update(&context, base_mac, sizeof(base_mac)) == 0 &&
|
||||||
|
mbedtls_sha256_finish(&context, digest) == 0) {
|
||||||
|
for (size_t index = 0; index < 8; index++) {
|
||||||
|
snprintf(output + index * 2, 3, "%02x", digest[index]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
memcpy(output, "0000000000000000", 17);
|
||||||
|
}
|
||||||
|
mbedtls_sha256_free(&context);
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
@@ -55,10 +55,10 @@ FUZZ_JOBS ?= 1
|
|||||||
.PHONY: all clean run_serialize run_edge run_nvs run_all test_adr110 run_adr110 \
|
.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_vitals run_vitals test_mmwave_detect run_mmwave_detect host_tests \
|
||||||
test_thermal run_thermal test_csi_sanitize run_csi_sanitize \
|
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 \
|
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 ---
|
# --- XIAO ESP32-C6 RF switch selection ---
|
||||||
# Host-side truth table for the GPIO14 antenna selector. GPIO3 is always driven
|
# 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
|
run_thermal: test_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 \
|
host_tests: run_adr110 run_vitals run_mmwave_detect run_thermal run_csi_sanitize \
|
||||||
run_c6_antenna
|
run_c6_antenna run_serial_onboarding
|
||||||
@echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna)"
|
@echo "Host tests passed (ADR-110 + CSI sanitation + vitals + mmwave + thermal + C6 antenna + USB onboarding)"
|
||||||
|
|
||||||
# --- Serialize fuzzer ---
|
# --- Serialize fuzzer ---
|
||||||
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
|
# Tests csi_serialize_frame() with random wifi_csi_info_t inputs.
|
||||||
@@ -161,5 +171,5 @@ run_all: run_serialize run_edge run_nvs
|
|||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f fuzz_serialize fuzz_edge fuzz_nvs test_adr110 test_vitals test_mmwave_detect \
|
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/
|
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.11
|
||||||
Reference in new issue
Block a user