feat: implement ADR-270 vendor provider beta (#1360)

This commit is contained in:
rUv 2026-07-19 00:09:50 -04:00 committed by GitHub
parent 76c80c33d7
commit 6ee1b55896
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2564 additions and 1 deletions

View File

@ -133,6 +133,7 @@ pip install "ruview[client]" # or: pip install "wifi-densepose[clie
> | **ESP32-C6 research node** ([ADR-110](docs/adr/ADR-110-esp32-c6-firmware-extension.md), [witness](docs/WITNESS-LOG-110.md), [reviewer guide](docs/ADR-110-REVIEW-GUIDE.md), [firmware v0.7.0](https://github.com/ruvnet/RuView/releases/tag/v0.7.0-esp32)) | ESP32-C6-DevKit ($610) | ~$10 | Yes (Wi-Fi 6 capable) | Same CSI pipeline as S3 with the dual-target firmware. **Firmware-side ADR-110 substrate now closed** (v0.7.0): ESP-NOW cross-board mesh quantified at **99.56 % match / 104 µs smoothed offset stdev / 3.95× EMA suppression** over a 5-min two-board soak (witness §A0.10), 32-byte UDP sync packet with operator-tunable cadence (§A0.12), ADR-018 byte 19 bit 4 wire-fix sourced from the working ESP-NOW path (§A0.13). Wire format ready for HE-LTF PPDU tagging in ADR-018 bytes 18-19 (firmware encoder + Rust + Python decoders verified end-to-end across 23 unit tests). LP-core motion-gate RISC-V program and Wi-Fi 6 soft-AP with TWT Responder both ship as opt-in code paths (default off). **Hardware-gated for measurement**: HE-LTF live subcarrier capture needs an 11ax AP (IDF v5.4 doesn't expose AP-side HE config — §A0.6); ~5 µA LP-core hibernation needs an INA meter to capture; 802.15.4 raw RX is broken in IDF v5.4 (workaround: ESP-NOW transport, shipped + measured). See witness log for the empirical / claimed split. |
> | **Research NIC** | Intel 5300 / Atheros AR9580 | ~$50-100 | Yes | Full CSI with 3x3 MIMO |
> | **Qualcomm CSI beta** ([ADR-268](docs/adr/ADR-268-qualcomm-atheros-csi-platform.md)) | QCA9300 now; QCN9074/QCN9274 experimental | ~$30-200 | Simulator now; hardware adapter gated | Rust `QCS1` codec, deterministic replay, UDP/API integration; modern ath11k/ath12k profiles do not claim public CSI export |
> | **Vendor provider beta** ([ADR-270](docs/adr/ADR-270-vendor-rf-sensing-integration-program.md)) | Origin, Plume, Mist, NETGEAR, Electric Imp, RF Solutions, Luma, Nest, Linksys, Wifigarden | Varies | Capability-dependent | Bounded Rust adapters and deterministic fixtures; telemetry/network-only/unsupported states cannot masquerade as CSI |
> | **Any WiFi** | Windows, macOS, or Linux laptop | $0 | No | RSSI-only: coarse presence and motion (see [tutorial #36](https://github.com/ruvnet/RuView/issues/36)) |
>
> No hardware? Verify the signal processing pipeline with the deterministic reference signal: `python archive/v1/data/proof/verify.py`

View File

@ -82,6 +82,30 @@ feature-gated FFI or sidecar boundary and are never redistributed without rights
- A no-go or telemetry-only ADR is a completed research outcome, not a failed port.
- Vendor status and APIs must be rechecked before each implementation begins.
## Implementation Status
The ADR-270 provider contract is implemented in Rust. Each portfolio entry has
a descriptor, bounded decoder or explicit fail-closed access state, deterministic
contract fixtures where lawful, registry coverage, and API exposure:
- Origin AI: contract-configured derived-sensing decoder and request plan.
- Plume/OpenSync: read-only OVSDB request plan and RF telemetry decoder.
- Mist/Juniper: regional request configuration, paginated RF/location decoder.
- NETGEAR Insight: regional partner request configuration and telemetry decoder.
- Electric Imp and RF Solutions: bounded scalar telemetry bridges.
- Luma: explicitly experimental generic OpenWrt telemetry bridge.
- Google Nest: network-only contract events; never represented as CSI.
- Linksys: `Unsupported` decoder because Linksys Aware is end-of-support.
- Wifigarden: `ContractRequired` decoder pending a disclosed SDK/schema.
`vendor-rf-sim` generates deterministic, provenance-labelled events for the
eight providers with a defined event contract and refuses to fabricate Linksys
or Wifigarden events. The sensing server exposes provider descriptors and latest
events under `/api/v1/rf/vendors` and accepts validated canonical simulator
events over its existing UDP port. Physical/vendor-cloud validation remains
separate from implementation completeness and is reflected by
`hardware_validated: false` until performed.
## Evidence and Links
- [ADR-268: Qualcomm strategy](ADR-268-qualcomm-atheros-csi-platform.md)

View File

@ -0,0 +1,21 @@
# RuView v0.9.3 Vendor Providers Beta 1
This beta implements ADR-270 as a capability-safe Rust provider program across
all ten researched vendors.
## Included
- Shared `VendorRfProvider` contract with bounded event validation.
- Origin AI, Plume/OpenSync, Mist/Juniper, NETGEAR Insight, Electric Imp,
RF Solutions, Luma/OpenWrt and Google Nest contract adapters.
- Explicit fail-closed Linksys (`Unsupported`) and Wifigarden
(`ContractRequired`) providers.
- Deterministic `vendor-rf-sim` JSONL/UDP fixtures for defined contracts.
- Provider registry, descriptors, latest-event REST endpoints and WebSocket
summaries through the sensing server.
## Boundary
This release implements and validates software contracts. It does not claim
vendor-cloud credentials, commercial SDK rights, physical hardware validation,
or complex CSI support for telemetry-only providers.

View File

@ -0,0 +1,60 @@
# ADR-270 Vendor RF Providers
RuView exposes a capability-safe Rust provider layer for vendor sensing and RF
telemetry. It never converts RSSI, occupancy, location or network inventory into
complex CSI.
## API
- `GET /api/v1/rf/vendors` — all provider descriptors and access states.
- `GET /api/v1/rf/vendors/latest` — latest validated event per vendor.
- `GET /api/v1/rf/vendors/:vendor/latest` — latest event for one stable vendor ID.
- `POST /api/v1/rf/vendors/:vendor/events` — ingest the vendor's documented
sidecar/webhook payload through its strict provider decoder. This `/api/v1/*`
route uses the server's bearer-token policy when configured.
Stable IDs are `origin_ai`, `plume`, `mist`, `netgear`, `electric_imp`,
`rf_solutions`, `linksys`, `luma`, `google_nest`, and `wifigarden`.
## Deterministic simulator
```bash
cd v2
cargo run -p wifi-densepose-hardware --bin vendor-rf-sim -- \
--vendor plume --frames 100 --output plume.jsonl
# Stream canonical synthetic events to the sensing server UDP port.
cargo run -p wifi-densepose-hardware --bin vendor-rf-sim -- \
--vendor mist --frames 100 --udp 127.0.0.1:5005 --realtime
```
Supported simulator names are `origin-ai`, `plume`, `mist`, `netgear`,
`electric-imp`, `rf-solutions`, `luma`, and `google-nest`. Linksys is refused
because its sensing service is discontinued. Wifigarden is refused until a
contracted event schema exists.
Every synthetic event includes `synthetic: true`, a deterministic sequence and
timestamp, and a source ending in `-sim-01`.
Canonical UDP JSON is accepted only when `synthetic: true`. Live vendor payloads
must use the HTTP ingestion route so provider-specific schemas, metric allowlists,
access states and bounds cannot be bypassed.
## Live/provider payloads
Provider decoders are strict, bounded and reject unknown schema fields. Origin
paths and credentials are supplied by the commercial contract. Plume uses a
read-only allow-listed OVSDB request plan. Mist and NETGEAR configurations use
regional HTTPS endpoints with redacted tokens. Electric Imp, RF Solutions and
Luma accept only allow-listed scalar metrics. Google Nest remains network-only.
Credentials are never embedded in fixtures or descriptors. Linksys returns
`Unsupported`; Wifigarden returns `ContractRequired`. These are usable,
test-covered provider outcomes—not simulated integrations.
## Hardware honesty
All descriptors remain `hardware_validated: false` until exact hardware/cloud
versions, lawful access, repeatable captures, calibration where applicable, and
fixture publication rights have been verified. Passing the simulator and API
tests validates RuView software only.

View File

@ -0,0 +1,232 @@
//! Deterministic ADR-270 vendor event simulator.
use clap::{Parser, ValueEnum};
use std::{
collections::BTreeMap,
fs::File,
io::{self, Write},
net::{SocketAddr, UdpSocket},
path::PathBuf,
thread,
time::Duration,
};
use wifi_densepose_hardware::vendor_rf::{
ProviderAvailability, ProviderDescriptor, RfCapability, VendorId, VendorRfEvent,
};
#[derive(Debug, Clone, Copy, ValueEnum)]
enum Vendor {
OriginAi,
Plume,
Mist,
Netgear,
ElectricImp,
RfSolutions,
Luma,
GoogleNest,
Linksys,
Wifigarden,
}
impl Vendor {
fn id(self) -> VendorId {
match self {
Self::OriginAi => VendorId::OriginAi,
Self::Plume => VendorId::Plume,
Self::Mist => VendorId::Mist,
Self::Netgear => VendorId::Netgear,
Self::ElectricImp => VendorId::ElectricImp,
Self::RfSolutions => VendorId::RfSolutions,
Self::Luma => VendorId::Luma,
Self::GoogleNest => VendorId::GoogleNest,
Self::Linksys => VendorId::Linksys,
Self::Wifigarden => VendorId::Wifigarden,
}
}
fn descriptor(self) -> ProviderDescriptor {
let (capabilities, availability, reason) = match self {
Self::OriginAi => (
vec![RfCapability::DerivedSensing],
ProviderAvailability::ContractRequired,
"Origin partner API/SDK contract required",
),
Self::Plume => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::CredentialsRequired,
"OpenSync telemetry; Plume Sense is separately gated",
),
Self::Mist => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::CredentialsRequired,
"Mist REST/webhook telemetry",
),
Self::Netgear => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::CredentialsRequired,
"Insight partner API telemetry",
),
Self::ElectricImp => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::CredentialsRequired,
"impCentral scalar telemetry only",
),
Self::RfSolutions => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::CredentialsRequired,
"RIoT environmental telemetry only",
),
Self::Luma => (
vec![RfCapability::RfTelemetry],
ProviderAvailability::Experimental,
"discontinued OpenWrt salvage fixture",
),
Self::GoogleNest => (
vec![RfCapability::NetworkOnly],
ProviderAvailability::Experimental,
"network infrastructure contract fixture only",
),
Self::Linksys => (
vec![RfCapability::Unsupported],
ProviderAvailability::Unsupported,
"Linksys Aware reached end of support",
),
Self::Wifigarden => (
vec![RfCapability::Unsupported],
ProviderAvailability::ContractRequired,
"technical SDK disclosure required",
),
};
ProviderDescriptor {
vendor: self.id(),
capabilities,
availability,
hardware_validated: false,
reason: reason.into(),
}
}
}
#[derive(Debug, Parser)]
#[command(
name = "vendor-rf-sim",
about = "Emit deterministic ADR-270 vendor RF events"
)]
struct Args {
#[arg(long, value_enum)]
vendor: Vendor,
#[arg(long, default_value_t = 100)]
frames: u64,
#[arg(long, default_value_t = 0x5255_5645_4e44_4f52)]
seed: u64,
#[arg(long, default_value_t = 100)]
interval_ms: u64,
#[arg(long)]
udp: Option<SocketAddr>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
realtime: bool,
}
fn next_random(state: &mut u64) -> f64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
(*state >> 11) as f64 / ((1u64 << 53) as f64)
}
fn event(vendor: Vendor, sequence: u64, timestamp_us: u64, state: &mut u64) -> VendorRfEvent {
let capability = vendor.descriptor().capabilities[0];
let wave = (sequence as f64 * 0.17).sin();
let noise = next_random(state) - 0.5;
let metrics = match capability {
RfCapability::DerivedSensing => BTreeMap::from([
(
"motion_score".into(),
(0.5 + 0.35 * wave + 0.05 * noise).clamp(0.0, 1.0),
),
("occupancy_count".into(), if wave > 0.0 { 2.0 } else { 1.0 }),
("confidence".into(), 0.92),
]),
RfCapability::RfTelemetry => BTreeMap::from([
("rssi_dbm".into(), -52.0 + 5.0 * wave + noise),
("client_count".into(), if wave > 0.0 { 4.0 } else { 3.0 }),
(
"channel_utilization".into(),
(0.31 + 0.08 * wave).clamp(0.0, 1.0),
),
]),
RfCapability::NetworkOnly => {
BTreeMap::from([("reachable".into(), 1.0), ("device_count".into(), 5.0)])
}
_ => BTreeMap::new(),
};
VendorRfEvent {
vendor: vendor.id(),
capability,
sequence,
timestamp_us,
source_id: format!("{}-sim-01", vendor.id().as_str()),
synthetic: true,
metrics,
label: Some("deterministic_contract_fixture".into()),
}
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
if args.udp.is_none() && args.output.is_none() {
return Err("select at least one sink with --udp or --output".into());
}
let descriptor = args.vendor.descriptor();
descriptor.validate()?;
if matches!(
descriptor.availability,
ProviderAvailability::Unsupported | ProviderAvailability::ContractRequired
) && descriptor.capabilities.contains(&RfCapability::Unsupported)
{
return Err(format!(
"{} has no simulatable event contract: {}",
descriptor.vendor.as_str(),
descriptor.reason
)
.into());
}
let socket = args.udp.map(|_| UdpSocket::bind("0.0.0.0:0")).transpose()?;
let mut output = args.output.as_ref().map(File::create).transpose()?;
let mut rng = args.seed;
let mut bytes = 0usize;
for sequence in 0..args.frames {
let value = event(
args.vendor,
sequence,
sequence * args.interval_ms * 1_000,
&mut rng,
);
value.validate(&descriptor)?;
let wire = serde_json::to_vec(&value)?;
if let (Some(socket), Some(destination)) = (&socket, args.udp) {
if socket.send_to(&wire, destination)? != wire.len() {
return Err(
io::Error::new(io::ErrorKind::WriteZero, "partial UDP datagram").into(),
);
}
}
if let Some(file) = &mut output {
file.write_all(&wire)?;
file.write_all(b"\n")?;
}
bytes += wire.len();
if args.realtime {
thread::sleep(Duration::from_millis(args.interval_ms));
}
}
eprintln!(
"emitted {} synthetic {} events ({} bytes, seed={:#x})",
args.frames,
args.vendor.id().as_str(),
bytes,
args.seed
);
Ok(())
}

View File

@ -47,6 +47,8 @@ mod esp32_parser;
// standardized report path until an OTA binding exists.
pub mod ieee80211bf;
pub mod sync_packet;
/// ADR-270 capability-safe vendor RF provider contract.
pub mod vendor_rf;
// ADR-081: Rust mirror of the firmware radio abstraction layer (L1) and
// mesh sensing plane (L3). Lets host tests, simulators, and future
@ -100,3 +102,7 @@ pub use rtl8720f::{
pub use sync_packet::{
SyncPacket, SyncPacketFlags, SYNC_PACKET_MAGIC, SYNC_PACKET_PROTO_VER, SYNC_PACKET_SIZE,
};
pub use vendor_rf::{
ProviderAvailability, ProviderDescriptor, RfCapability, VendorEventError, VendorId,
VendorRfEvent, VendorRfProvider,
};

View File

@ -0,0 +1,207 @@
//! ADR-270 vendor RF provider contract.
//!
//! This model prevents RSSI, cloud occupancy, or network inventory from being
//! represented as complex CSI. Vendor adapters may emit only declared capabilities.
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use thiserror::Error;
pub const MAX_VENDOR_METRICS: usize = 64;
pub const MAX_VENDOR_KEY_LEN: usize = 64;
pub const MAX_VENDOR_TEXT_LEN: usize = 256;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum VendorId {
OriginAi,
Plume,
Mist,
Netgear,
ElectricImp,
RfSolutions,
Linksys,
Luma,
GoogleNest,
Wifigarden,
}
impl VendorId {
pub const ALL: [Self; 10] = [
Self::OriginAi,
Self::Plume,
Self::Mist,
Self::Netgear,
Self::ElectricImp,
Self::RfSolutions,
Self::Linksys,
Self::Luma,
Self::GoogleNest,
Self::Wifigarden,
];
pub fn as_str(self) -> &'static str {
match self {
Self::OriginAi => "origin_ai",
Self::Plume => "plume",
Self::Mist => "mist",
Self::Netgear => "netgear",
Self::ElectricImp => "electric_imp",
Self::RfSolutions => "rf_solutions",
Self::Linksys => "linksys",
Self::Luma => "luma",
Self::GoogleNest => "google_nest",
Self::Wifigarden => "wifigarden",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RfCapability {
ComplexCsi,
DerivedSensing,
RfTelemetry,
NetworkOnly,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProviderAvailability {
Available,
CredentialsRequired,
ContractRequired,
Experimental,
Unsupported,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderDescriptor {
pub vendor: VendorId,
pub capabilities: Vec<RfCapability>,
pub availability: ProviderAvailability,
pub hardware_validated: bool,
pub reason: String,
}
impl ProviderDescriptor {
pub fn validate(&self) -> Result<(), VendorEventError> {
if self.capabilities.is_empty()
|| self.reason.is_empty()
|| self.reason.len() > MAX_VENDOR_TEXT_LEN
{
return Err(VendorEventError::InvalidDescriptor);
}
if self.hardware_validated && self.availability != ProviderAvailability::Available {
return Err(VendorEventError::InvalidDescriptor);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VendorRfEvent {
pub vendor: VendorId,
pub capability: RfCapability,
pub sequence: u64,
pub timestamp_us: u64,
pub source_id: String,
pub synthetic: bool,
pub metrics: BTreeMap<String, f64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl VendorRfEvent {
pub fn validate(&self, descriptor: &ProviderDescriptor) -> Result<(), VendorEventError> {
descriptor.validate()?;
if self.vendor != descriptor.vendor || !descriptor.capabilities.contains(&self.capability) {
return Err(VendorEventError::CapabilityMismatch);
}
if matches!(
self.capability,
RfCapability::ComplexCsi | RfCapability::Unsupported
) {
return Err(VendorEventError::InvalidEventCapability);
}
if self.source_id.is_empty()
|| self.source_id.len() > MAX_VENDOR_TEXT_LEN
|| self.metrics.is_empty()
|| self.metrics.len() > MAX_VENDOR_METRICS
|| self
.metrics
.iter()
.any(|(k, v)| k.is_empty() || k.len() > MAX_VENDOR_KEY_LEN || !v.is_finite())
|| self
.label
.as_ref()
.is_some_and(|v| v.len() > MAX_VENDOR_TEXT_LEN)
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
}
pub trait VendorRfProvider: Send + Sync {
fn descriptor(&self) -> ProviderDescriptor;
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError>;
}
#[derive(Debug, Error, PartialEq, Eq)]
pub enum VendorEventError {
#[error("invalid provider descriptor")]
InvalidDescriptor,
#[error("event capability does not match provider")]
CapabilityMismatch,
#[error("complex CSI and unsupported states cannot be represented as scalar vendor events")]
InvalidEventCapability,
#[error("invalid or unbounded vendor payload")]
InvalidPayload,
#[error("malformed provider payload: {0}")]
MalformedPayload(String),
#[error("provider credentials are required")]
CredentialsRequired,
#[error("commercial contract or SDK access is required")]
ContractRequired,
#[error("provider has no supported sensing interface")]
Unsupported,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_vendor_has_a_stable_identifier() {
let names: std::collections::BTreeSet<_> =
VendorId::ALL.iter().map(|v| v.as_str()).collect();
assert_eq!(names.len(), VendorId::ALL.len());
}
#[test]
fn scalar_contract_rejects_csi_masquerading() {
let descriptor = ProviderDescriptor {
vendor: VendorId::Plume,
capabilities: vec![RfCapability::RfTelemetry],
availability: ProviderAvailability::CredentialsRequired,
hardware_validated: false,
reason: "OpenSync telemetry".into(),
};
let event = VendorRfEvent {
vendor: VendorId::Plume,
capability: RfCapability::ComplexCsi,
sequence: 1,
timestamp_us: 1,
source_id: "pod-1".into(),
synthetic: true,
metrics: BTreeMap::from([("rssi_dbm".into(), -42.0)]),
label: None,
};
assert_eq!(
event.validate(&descriptor),
Err(VendorEventError::CapabilityMismatch)
);
}
}

View File

@ -34,3 +34,11 @@ pub mod sparse_inference;
#[allow(dead_code)]
pub mod trainer;
pub mod vital_signs;
/// ADR-270 Mist and NETGEAR telemetry providers.
pub mod vendor_mist_netgear;
/// ADR-270 Origin AI and Plume/OpenSync providers.
pub mod vendor_origin_plume;
/// ADR-270 scalar, network-only, and fail-closed vendor providers.
pub mod vendor_remaining;
/// ADR-270 provider registry and canonical event helpers.
pub mod vendor_rf;

View File

@ -33,13 +33,14 @@ use wifi_densepose_sensing_server::{
};
use ruvector_mincut::{DynamicMinCut, MinCutBuilder};
use std::collections::{HashMap, VecDeque};
use std::collections::{BTreeMap, HashMap, VecDeque};
use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use axum::{
body::Bytes,
extract::{
ws::{Message, WebSocket, WebSocketUpgrade},
Path, Query, State,
@ -1043,6 +1044,8 @@ struct AppStateInner {
latest_qualcomm_csi: Option<qualcomm_csi::QualcommCsiSnapshot>,
/// Instant of the last validated Qualcomm CSI UDP frame.
last_qualcomm_frame: Option<std::time::Instant>,
/// Latest bounded ADR-270 event per vendor. Complex CSI uses dedicated transports.
latest_vendor_rf: BTreeMap<String, wifi_densepose_sensing_server::vendor_rf::VendorEventSnapshot>,
tx: broadcast::Sender<String>,
// ADR-099 D2/D3/D4: real-time CSI introspection tap. Per-frame state +
// a parallel broadcast topic (`/ws/introspection`) running alongside
@ -3411,6 +3414,89 @@ async fn latest_qualcomm_csi(State(state): State<SharedState>) -> Json<serde_jso
}
}
async fn vendor_descriptors() -> Json<serde_json::Value> {
Json(
serde_json::to_value(wifi_densepose_sensing_server::vendor_rf::descriptors())
.unwrap_or_default(),
)
}
async fn latest_vendor_events(State(state): State<SharedState>) -> Json<serde_json::Value> {
let state = state.read().await;
Json(serde_json::to_value(&state.latest_vendor_rf).unwrap_or_default())
}
async fn latest_vendor_event(
State(state): State<SharedState>,
Path(vendor): Path<String>,
) -> impl IntoResponse {
let Some(vendor_id) = wifi_densepose_sensing_server::vendor_rf::vendor_from_str(&vendor) else {
return (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "unknown vendor"})));
};
let state = state.read().await;
let canonical_vendor = vendor_id.as_str();
match state.latest_vendor_rf.get(canonical_vendor) {
Some(snapshot) => (
StatusCode::OK,
Json(serde_json::to_value(snapshot).unwrap_or_default()),
),
None => (
StatusCode::OK,
Json(serde_json::json!({
"status": "no vendor RF data yet",
"vendor": canonical_vendor
})),
),
}
}
async fn ingest_vendor_events(
State(state): State<SharedState>,
Path(vendor): Path<String>,
payload: Bytes,
) -> impl IntoResponse {
let Some(vendor_id) = wifi_densepose_sensing_server::vendor_rf::vendor_from_str(&vendor) else {
return (StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "unknown vendor"})));
};
let events = match wifi_densepose_sensing_server::vendor_rf::decode_provider(vendor_id, &payload) {
Ok(events) => events,
Err(error) => {
let status = match error {
wifi_densepose_hardware::vendor_rf::VendorEventError::Unsupported => StatusCode::NOT_IMPLEMENTED,
wifi_densepose_hardware::vendor_rf::VendorEventError::ContractRequired
| wifi_densepose_hardware::vendor_rf::VendorEventError::CredentialsRequired => StatusCode::FORBIDDEN,
_ => StatusCode::BAD_REQUEST,
};
return (status, Json(serde_json::json!({"error": error.to_string(), "vendor": vendor})));
}
};
let mut accepted = 0usize;
let mut state = state.write().await;
let canonical_vendor = vendor_id.as_str().to_string();
for event in events {
match wifi_densepose_sensing_server::vendor_rf::VendorEventSnapshot::from_event(event) {
Ok(snapshot) => {
let json = serde_json::to_string(&snapshot).ok();
state.source = snapshot.source.clone();
state
.latest_vendor_rf
.insert(canonical_vendor.clone(), snapshot);
if let Some(json) = json {
let _ = state.tx.send(json);
}
accepted += 1;
}
Err(error) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": error.to_string(), "vendor": canonical_vendor})),
)
}
}
}
(StatusCode::ACCEPTED, Json(serde_json::json!({"vendor": vendor, "accepted": accepted})))
}
/// Generate WiFi-derived pose keypoints from sensing data.
///
/// Keypoint positions are modulated by real signal features rather than a pure
@ -5518,6 +5604,24 @@ async fn udp_receiver_task(state: SharedState, udp_port: u16) {
loop {
match socket.recv_from(&mut buf).await {
Ok((len, src)) => {
if len > 0 && buf[0] == b'{' {
match serde_json::from_slice::<wifi_densepose_hardware::vendor_rf::VendorRfEvent>(&buf[..len])
.map_err(|error| error.to_string())
.and_then(|event| wifi_densepose_sensing_server::vendor_rf::VendorEventSnapshot::from_event(event).map_err(|error| error.to_string()))
{
Ok(snapshot) if snapshot.event.synthetic => {
debug!("Vendor RF event from {src}: vendor={} capability={:?} seq={}", snapshot.event.vendor.as_str(), snapshot.event.capability, snapshot.event.sequence);
let json = serde_json::to_string(&snapshot).ok();
let mut state = state.write().await;
state.source = snapshot.source.clone();
state.latest_vendor_rf.insert(snapshot.event.vendor.as_str().to_string(), snapshot);
if let Some(json) = json { let _ = state.tx.send(json); }
}
Ok(_) => warn!("Rejected non-synthetic canonical vendor event from {src}; live payloads must use the provider decoder HTTP route"),
Err(error) => warn!("Rejected ADR-270 vendor event from {src}: {error}"),
}
continue;
}
if len >= 4
&& u32::from_le_bytes(buf[..4].try_into().expect("four-byte slice"))
== wifi_densepose_hardware::qualcomm_csi::QUALCOMM_CSI_MAGIC
@ -7680,6 +7784,7 @@ async fn main() {
last_mediatek_frame: None,
latest_qualcomm_csi: None,
last_qualcomm_frame: None,
latest_vendor_rf: BTreeMap::new(),
tx,
intro: wifi_densepose_sensing_server::introspection::IntrospectionState::new(),
intro_tx,
@ -7899,6 +8004,10 @@ async fn main() {
.route("/api/v1/radar/latest", get(latest_realtek_radar))
.route("/api/v1/csi/mediatek/latest", get(latest_mediatek_csi))
.route("/api/v1/csi/qualcomm/latest", get(latest_qualcomm_csi))
.route("/api/v1/rf/vendors", get(vendor_descriptors))
.route("/api/v1/rf/vendors/latest", get(latest_vendor_events))
.route("/api/v1/rf/vendors/:vendor/latest", get(latest_vendor_event))
.route("/api/v1/rf/vendors/:vendor/events", post(ingest_vendor_events))
// Per-node health endpoint
.route("/api/v1/nodes", get(nodes_endpoint))
// ADR-110 iter 29 — per-node mesh sync state for HTTP clients.

View File

@ -0,0 +1,712 @@
//! ADR-270 adapters for Mist/Juniper and NETGEAR Insight.
//!
//! These cloud APIs expose client RF telemetry and location/network context.
//! They do not expose complex channel state information (CSI), and this module
//! deliberately cannot construct a `ComplexCsi` event.
use serde::Deserialize;
use serde_json::{Map, Value};
use std::collections::BTreeMap;
use std::fmt;
use wifi_densepose_hardware::vendor_rf::{
ProviderAvailability, ProviderDescriptor, RfCapability, VendorEventError, VendorId,
VendorRfEvent, VendorRfProvider,
};
pub const MAX_VENDOR_PAYLOAD_BYTES: usize = 1024 * 1024;
pub const MAX_EVENTS_PER_PAGE: usize = 1_000;
pub const MAX_CURSOR_BYTES: usize = 512;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MistRegion {
Global,
Europe,
AsiaPacific,
Australia,
}
impl MistRegion {
pub const fn base_url(self) -> &'static str {
match self {
Self::Global => "https://api.mist.com",
Self::Europe => "https://api.eu.mist.com",
Self::AsiaPacific => "https://api.ac2.mist.com",
Self::Australia => "https://api.gc1.mist.com",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NetgearRegion {
NorthAmerica,
Europe,
Australia,
}
impl NetgearRegion {
pub const fn base_url(self) -> &'static str {
match self {
Self::NorthAmerica => "https://insight.netgear.com",
Self::Europe => "https://eu.insight.netgear.com",
Self::Australia => "https://au.insight.netgear.com",
}
}
}
/// An authentication value whose `Debug` output is always redacted.
#[derive(Clone, PartialEq, Eq)]
pub struct SecretToken(String);
impl SecretToken {
pub fn new(value: impl Into<String>) -> Result<Self, VendorEventError> {
let value = value.into();
if value.is_empty() || value.len() > 4_096 || value.chars().any(char::is_control) {
return Err(VendorEventError::InvalidPayload);
}
Ok(Self(value))
}
/// Intended only for constructing an HTTP authorization header.
pub fn expose_for_header(&self) -> &str {
&self.0
}
}
impl fmt::Debug for SecretToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("SecretToken([REDACTED])")
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct VendorRequestConfig {
pub base_url: &'static str,
pub path: String,
pub cursor: Option<String>,
pub token: SecretToken,
}
impl VendorRequestConfig {
pub fn validate(&self) -> Result<(), VendorEventError> {
if !self.base_url.starts_with("https://")
|| !self.path.starts_with('/')
|| self.path.contains("..")
|| self.path.chars().any(char::is_control)
|| self.cursor.as_deref().is_some_and(|cursor| {
cursor.is_empty()
|| cursor.len() > MAX_CURSOR_BYTES
|| cursor
.chars()
.any(|c| c.is_control() || c == '&' || c == '?' || c == '#')
})
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
}
impl fmt::Debug for VendorRequestConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("VendorRequestConfig")
.field("base_url", &self.base_url)
.field("path", &self.path)
.field("cursor", &self.cursor.as_ref().map(|_| "[PRESENT]"))
.field("token", &"[REDACTED]")
.finish()
}
}
pub fn mist_request(
region: MistRegion,
site_id: &str,
cursor: Option<String>,
token: SecretToken,
) -> Result<VendorRequestConfig, VendorEventError> {
validate_identifier(site_id)?;
let request = VendorRequestConfig {
base_url: region.base_url(),
path: format!("/api/v1/sites/{site_id}/stats/clients"),
cursor,
token,
};
request.validate()?;
Ok(request)
}
pub fn netgear_request(
region: NetgearRegion,
location_id: &str,
cursor: Option<String>,
token: SecretToken,
) -> Result<VendorRequestConfig, VendorEventError> {
validate_identifier(location_id)?;
let request = VendorRequestConfig {
base_url: region.base_url(),
path: format!("/api/v1/locations/{location_id}/clients"),
cursor,
token,
};
request.validate()?;
Ok(request)
}
fn validate_identifier(value: &str) -> Result<(), VendorEventError> {
if value.is_empty()
|| value.len() > 128
|| !value
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_'))
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub struct DecodedVendorPage {
pub events: Vec<VendorRfEvent>,
pub next_cursor: Option<String>,
}
#[derive(Debug, Default, Clone, Copy)]
pub struct MistProvider;
impl MistProvider {
pub fn decode_page(&self, payload: &[u8]) -> Result<DecodedVendorPage, VendorEventError> {
decode_page(
payload,
VendorId::Mist,
mist_descriptor(),
parse_mist_record,
)
}
}
impl VendorRfProvider for MistProvider {
fn descriptor(&self) -> ProviderDescriptor {
mist_descriptor()
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
Ok(self.decode_page(payload)?.events)
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct NetgearInsightProvider;
impl NetgearInsightProvider {
pub fn decode_page(&self, payload: &[u8]) -> Result<DecodedVendorPage, VendorEventError> {
decode_page(
payload,
VendorId::Netgear,
netgear_descriptor(),
parse_netgear_record,
)
}
}
impl VendorRfProvider for NetgearInsightProvider {
fn descriptor(&self) -> ProviderDescriptor {
netgear_descriptor()
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
Ok(self.decode_page(payload)?.events)
}
}
pub fn mist_descriptor() -> ProviderDescriptor {
ProviderDescriptor {
vendor: VendorId::Mist,
capabilities: vec![RfCapability::RfTelemetry, RfCapability::NetworkOnly],
availability: ProviderAvailability::CredentialsRequired,
hardware_validated: false,
reason: "Mist cloud client RF telemetry and location context; never CSI".into(),
}
}
pub fn netgear_descriptor() -> ProviderDescriptor {
ProviderDescriptor {
vendor: VendorId::Netgear,
capabilities: vec![RfCapability::RfTelemetry, RfCapability::NetworkOnly],
availability: ProviderAvailability::CredentialsRequired,
hardware_validated: false,
reason: "NETGEAR Insight client RF and network telemetry; never CSI".into(),
}
}
type RecordParser = fn(&Map<String, Value>, usize) -> Result<VendorRfEvent, VendorEventError>;
fn decode_page(
payload: &[u8],
vendor: VendorId,
descriptor: ProviderDescriptor,
parser: RecordParser,
) -> Result<DecodedVendorPage, VendorEventError> {
if payload.is_empty() || payload.len() > MAX_VENDOR_PAYLOAD_BYTES {
return Err(VendorEventError::InvalidPayload);
}
let root: Value = serde_json::from_slice(payload)
.map_err(|e| VendorEventError::MalformedPayload(e.to_string()))?;
let (records, next_cursor) = extract_records_and_cursor(&root)?;
if records.is_empty() || records.len() > MAX_EVENTS_PER_PAGE {
return Err(VendorEventError::InvalidPayload);
}
let mut events = Vec::with_capacity(records.len());
for (index, value) in records.iter().enumerate() {
let object = value.as_object().ok_or(VendorEventError::InvalidPayload)?;
let event = parser(object, index)?;
if event.vendor != vendor || event.capability != RfCapability::RfTelemetry {
return Err(VendorEventError::CapabilityMismatch);
}
event.validate(&descriptor)?;
events.push(event);
}
Ok(DecodedVendorPage {
events,
next_cursor,
})
}
fn extract_records_and_cursor(
root: &Value,
) -> Result<(&[Value], Option<String>), VendorEventError> {
if let Some(records) = root.as_array() {
return Ok((records, None));
}
let object = root.as_object().ok_or(VendorEventError::InvalidPayload)?;
let records = ["results", "data", "clients", "events", "items"]
.iter()
.find_map(|key| object.get(*key).and_then(Value::as_array))
.ok_or(VendorEventError::InvalidPayload)?;
let cursor_value = object
.get("next_cursor")
.or_else(|| object.get("nextPageToken"))
.or_else(|| object.get("next_page_token"))
.or_else(|| {
object
.get("pagination")
.and_then(Value::as_object)
.and_then(|p| p.get("next").or_else(|| p.get("cursor")))
});
let next_cursor = match cursor_value {
None | Some(Value::Null) => None,
Some(Value::String(value)) if valid_cursor(value) => Some(value.clone()),
Some(_) => return Err(VendorEventError::InvalidPayload),
};
Ok((records, next_cursor))
}
fn valid_cursor(value: &str) -> bool {
!value.is_empty()
&& value.len() <= MAX_CURSOR_BYTES
&& !value
.chars()
.any(|c| c.is_control() || c == '&' || c == '?' || c == '#')
}
#[derive(Debug, Deserialize)]
struct MistRecord {
#[serde(alias = "client_id", alias = "mac")]
id: String,
#[serde(default, alias = "last_seen", alias = "lastSeen")]
timestamp: Option<Value>,
#[serde(default, alias = "rssi_dbm")]
rssi: Option<f64>,
#[serde(default)]
snr: Option<f64>,
#[serde(default)]
channel: Option<f64>,
#[serde(default)]
x: Option<f64>,
#[serde(default)]
y: Option<f64>,
#[serde(default)]
site_id: Option<String>,
#[serde(default)]
ap_id: Option<String>,
#[serde(default, alias = "event_type", alias = "type")]
event: Option<String>,
}
fn parse_mist_record(
object: &Map<String, Value>,
index: usize,
) -> Result<VendorRfEvent, VendorEventError> {
let record: MistRecord = serde_json::from_value(Value::Object(object.clone()))
.map_err(|e| VendorEventError::MalformedPayload(e.to_string()))?;
validate_source(&record.id)?;
validate_optional_text(record.site_id.as_deref())?;
validate_optional_text(record.ap_id.as_deref())?;
validate_optional_text(record.event.as_deref())?;
let timestamp_us = parse_timestamp(record.timestamp.as_ref())?;
let mut metrics = BTreeMap::new();
push_metric(&mut metrics, "rssi_dbm", record.rssi, -150.0, 20.0)?;
push_metric(&mut metrics, "snr_db", record.snr, -50.0, 100.0)?;
push_metric(&mut metrics, "channel", record.channel, 1.0, 7_000.0)?;
push_metric(&mut metrics, "x_m", record.x, -1_000_000.0, 1_000_000.0)?;
push_metric(&mut metrics, "y_m", record.y, -1_000_000.0, 1_000_000.0)?;
if metrics.is_empty() {
return Err(VendorEventError::InvalidPayload);
}
Ok(VendorRfEvent {
vendor: VendorId::Mist,
capability: RfCapability::RfTelemetry,
sequence: stable_sequence(&record.id, timestamp_us, index),
timestamp_us,
source_id: record.id,
synthetic: false,
metrics,
label: context_label(&[record.site_id, record.ap_id, record.event])?,
})
}
#[derive(Debug, Deserialize)]
struct NetgearRecord {
#[serde(
alias = "clientId",
alias = "mac",
alias = "macAddress",
alias = "deviceId"
)]
id: String,
#[serde(default, alias = "lastSeen", alias = "observedAt")]
timestamp: Option<Value>,
#[serde(default, alias = "signalStrength", alias = "rssi_dbm")]
rssi: Option<f64>,
#[serde(default, alias = "signalToNoiseRatio")]
snr: Option<f64>,
#[serde(default)]
channel: Option<f64>,
#[serde(default, alias = "txRateMbps", alias = "tx_rate")]
tx_rate_mbps: Option<f64>,
#[serde(default, alias = "rxRateMbps", alias = "rx_rate")]
rx_rate_mbps: Option<f64>,
#[serde(default, alias = "locationId")]
location_id: Option<String>,
#[serde(default, alias = "accessPointId", alias = "apId")]
ap_id: Option<String>,
#[serde(default, alias = "ssidName")]
ssid: Option<String>,
}
fn parse_netgear_record(
object: &Map<String, Value>,
index: usize,
) -> Result<VendorRfEvent, VendorEventError> {
let record: NetgearRecord = serde_json::from_value(Value::Object(object.clone()))
.map_err(|e| VendorEventError::MalformedPayload(e.to_string()))?;
validate_source(&record.id)?;
validate_optional_text(record.location_id.as_deref())?;
validate_optional_text(record.ap_id.as_deref())?;
validate_optional_text(record.ssid.as_deref())?;
let timestamp_us = parse_timestamp(record.timestamp.as_ref())?;
let mut metrics = BTreeMap::new();
push_metric(&mut metrics, "rssi_dbm", record.rssi, -150.0, 20.0)?;
push_metric(&mut metrics, "snr_db", record.snr, -50.0, 100.0)?;
push_metric(&mut metrics, "channel", record.channel, 1.0, 7_000.0)?;
push_metric(
&mut metrics,
"tx_rate_mbps",
record.tx_rate_mbps,
0.0,
100_000.0,
)?;
push_metric(
&mut metrics,
"rx_rate_mbps",
record.rx_rate_mbps,
0.0,
100_000.0,
)?;
if metrics.is_empty() {
return Err(VendorEventError::InvalidPayload);
}
Ok(VendorRfEvent {
vendor: VendorId::Netgear,
capability: RfCapability::RfTelemetry,
sequence: stable_sequence(&record.id, timestamp_us, index),
timestamp_us,
source_id: record.id,
synthetic: false,
metrics,
label: context_label(&[record.location_id, record.ap_id, record.ssid])?,
})
}
fn parse_timestamp(value: Option<&Value>) -> Result<u64, VendorEventError> {
let value = value.ok_or(VendorEventError::InvalidPayload)?;
if let Some(text) = value.as_str() {
if let Ok(raw) = text.parse::<u64>() {
return normalize_integer_timestamp(raw);
}
if let Ok(raw) = text.parse::<f64>() {
return normalize_fractional_timestamp(raw);
}
let parsed = chrono::DateTime::parse_from_rfc3339(text)
.map_err(|_| VendorEventError::InvalidPayload)?;
return u64::try_from(parsed.timestamp_micros())
.map_err(|_| VendorEventError::InvalidPayload);
}
if let Some(raw) = value.as_u64() {
return normalize_integer_timestamp(raw);
}
normalize_fractional_timestamp(value.as_f64().ok_or(VendorEventError::InvalidPayload)?)
}
fn normalize_integer_timestamp(raw: u64) -> Result<u64, VendorEventError> {
if raw == 0 {
return Err(VendorEventError::InvalidPayload);
}
// Normalize seconds, milliseconds, or microseconds to microseconds.
if raw < 10_000_000_000 {
raw.checked_mul(1_000_000)
.ok_or(VendorEventError::InvalidPayload)
} else if raw < 10_000_000_000_000 {
raw.checked_mul(1_000)
.ok_or(VendorEventError::InvalidPayload)
} else if raw < 10_000_000_000_000_000 {
Ok(raw)
} else {
Err(VendorEventError::InvalidPayload)
}
}
fn normalize_fractional_timestamp(raw: f64) -> Result<u64, VendorEventError> {
if !raw.is_finite() || raw <= 0.0 {
return Err(VendorEventError::InvalidPayload);
}
let micros = if raw < 10_000_000_000.0 {
raw * 1_000_000.0
} else if raw < 10_000_000_000_000.0 {
raw * 1_000.0
} else if raw < 10_000_000_000_000_000.0 {
raw
} else {
return Err(VendorEventError::InvalidPayload);
};
if !micros.is_finite() || micros > u64::MAX as f64 {
return Err(VendorEventError::InvalidPayload);
}
Ok(micros.round() as u64)
}
fn push_metric(
metrics: &mut BTreeMap<String, f64>,
name: &str,
value: Option<f64>,
minimum: f64,
maximum: f64,
) -> Result<(), VendorEventError> {
if let Some(value) = value {
if !value.is_finite() || !(minimum..=maximum).contains(&value) {
return Err(VendorEventError::InvalidPayload);
}
metrics.insert(name.into(), value);
}
Ok(())
}
fn validate_source(value: &str) -> Result<(), VendorEventError> {
if value.is_empty() || value.len() > 256 || value.chars().any(char::is_control) {
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
fn validate_optional_text(value: Option<&str>) -> Result<(), VendorEventError> {
if value.is_some_and(|v| v.is_empty() || v.len() > 256 || v.chars().any(char::is_control)) {
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
fn context_label(parts: &[Option<String>]) -> Result<Option<String>, VendorEventError> {
let label = parts
.iter()
.filter_map(Option::as_deref)
.collect::<Vec<_>>()
.join("/");
if label.len() > 256 {
return Err(VendorEventError::InvalidPayload);
}
Ok((!label.is_empty()).then_some(label))
}
fn stable_sequence(source: &str, timestamp_us: u64, index: usize) -> u64 {
// FNV-1a gives a deterministic correlation key without process-random state.
let mut hash = 0xcbf29ce484222325_u64;
for byte in source
.bytes()
.chain(timestamp_us.to_le_bytes())
.chain((index as u64).to_le_bytes())
{
hash ^= u64::from(byte);
hash = hash.wrapping_mul(0x100000001b3);
}
hash
}
#[cfg(test)]
mod tests {
use super::*;
const MIST_FIXTURE: &[u8] = br#"{
"results": [
{"id":"aa:bb:cc:dd:ee:ff","timestamp":1710000000,"rssi":-47,"snr":31,"channel":44,"x":12.5,"y":8.25,"site_id":"site-1","ap_id":"ap-7"},
{"client_id":"station-2","last_seen":1710000000123,"rssi_dbm":-62,"event_type":"client-info"}
],
"next_cursor":"page-2"
}"#;
const NETGEAR_FIXTURE: &[u8] = br#"{
"data": [
{"clientId":"client-1","observedAt":"1710000000000000","signalStrength":-53,"signalToNoiseRatio":24,"channel":149,"txRateMbps":866.7,"rxRateMbps":721.2,"locationId":"office","accessPointId":"ap-2","ssidName":"lab"}
],
"pagination":{"next":"cursor-2"}
}"#;
#[test]
fn descriptors_are_honest_and_valid() {
for descriptor in [mist_descriptor(), netgear_descriptor()] {
descriptor.validate().unwrap();
assert!(!descriptor.hardware_validated);
assert_eq!(
descriptor.availability,
ProviderAvailability::CredentialsRequired
);
assert!(!descriptor.capabilities.contains(&RfCapability::ComplexCsi));
}
}
#[test]
fn mist_rest_and_webhook_fixture_is_deterministic() {
let provider = MistProvider;
let first = provider.decode_page(MIST_FIXTURE).unwrap();
let second = provider.decode_page(MIST_FIXTURE).unwrap();
assert_eq!(first, second);
assert_eq!(first.next_cursor.as_deref(), Some("page-2"));
assert_eq!(first.events.len(), 2);
assert_eq!(first.events[0].timestamp_us, 1_710_000_000_000_000);
assert_eq!(first.events[1].timestamp_us, 1_710_000_000_123_000);
assert_eq!(first.events[0].metrics["x_m"], 12.5);
assert!(!first.events[0].synthetic);
}
#[test]
fn netgear_page_fixture_normalizes_aliases() {
let page = NetgearInsightProvider.decode_page(NETGEAR_FIXTURE).unwrap();
assert_eq!(page.next_cursor.as_deref(), Some("cursor-2"));
assert_eq!(page.events.len(), 1);
let event = &page.events[0];
assert_eq!(event.vendor, VendorId::Netgear);
assert_eq!(event.capability, RfCapability::RfTelemetry);
assert_eq!(event.metrics["tx_rate_mbps"], 866.7);
assert_eq!(event.label.as_deref(), Some("office/ap-2/lab"));
}
#[test]
fn top_level_array_is_supported_without_pagination() {
let payload = br#"[{"mac":"a","timestamp":1710000000,"rssi":-40}]"#;
let page = MistProvider.decode_page(payload).unwrap();
assert_eq!(page.events.len(), 1);
assert_eq!(page.next_cursor, None);
}
#[test]
fn payload_and_page_bounds_are_enforced() {
assert_eq!(
MistProvider.decode(&vec![b' '; MAX_VENDOR_PAYLOAD_BYTES + 1]),
Err(VendorEventError::InvalidPayload)
);
let values = (0..=MAX_EVENTS_PER_PAGE)
.map(|_| serde_json::json!({"id":"a","timestamp":1710000000,"rssi":-40}))
.collect::<Vec<_>>();
let bytes = serde_json::to_vec(&values).unwrap();
assert_eq!(
MistProvider.decode(&bytes),
Err(VendorEventError::InvalidPayload)
);
}
#[test]
fn missing_identity_timestamp_or_metrics_fails_closed() {
for payload in [
br#"[{"timestamp":1710000000,"rssi":-40}]"#.as_slice(),
br#"[{"id":"a","rssi":-40}]"#.as_slice(),
br#"[{"id":"a","timestamp":1710000000}]"#.as_slice(),
] {
assert!(MistProvider.decode(payload).is_err());
}
}
#[test]
fn invalid_metric_cursor_and_record_schema_fail_closed() {
assert!(MistProvider
.decode(br#"[{"id":"a","timestamp":1710000000,"rssi":999}]"#)
.is_err());
assert!(NetgearInsightProvider.decode(br#"{"data":[42]}"#).is_err());
assert!(MistProvider
.decode_page(
br#"{"results":[{"id":"a","timestamp":1710000000,"rssi":-40}],"next_cursor":"x&admin=true"}"#
)
.is_err());
}
#[test]
fn request_configuration_is_regional_validated_and_redacted() {
let token = SecretToken::new("super-secret").unwrap();
let mist = mist_request(
MistRegion::Europe,
"site_1",
Some("page-2".into()),
token.clone(),
)
.unwrap();
assert_eq!(mist.base_url, "https://api.eu.mist.com");
assert_eq!(mist.path, "/api/v1/sites/site_1/stats/clients");
let netgear = netgear_request(NetgearRegion::Australia, "location-7", None, token).unwrap();
assert_eq!(netgear.base_url, "https://au.insight.netgear.com");
assert!(!format!("{netgear:?}").contains("super-secret"));
assert!(!format!("{:?}", netgear.token).contains("super-secret"));
assert!(mist_request(
MistRegion::Global,
"../other-site",
None,
SecretToken::new("token").unwrap()
)
.is_err());
}
#[test]
fn token_rejects_header_injection() {
assert!(SecretToken::new("token\r\nX-Injected: yes").is_err());
assert!(SecretToken::new("").is_err());
}
#[test]
fn timestamp_units_are_normalized_and_extremes_rejected() {
assert_eq!(
parse_timestamp(Some(&serde_json::json!(1_710_000_000))).unwrap(),
1_710_000_000_000_000
);
assert_eq!(
parse_timestamp(Some(&serde_json::json!(1_710_000_000_123_u64))).unwrap(),
1_710_000_000_123_000
);
assert!(parse_timestamp(Some(&serde_json::json!(0))).is_err());
assert!(parse_timestamp(Some(&serde_json::json!(-1))).is_err());
assert!(parse_timestamp(Some(&serde_json::json!(u64::MAX))).is_err());
assert_eq!(
parse_timestamp(Some(&serde_json::json!("2024-03-09T16:00:00Z"))).unwrap(),
1_710_000_000_000_000
);
assert_eq!(
parse_timestamp(Some(&serde_json::json!(1710000000.25))).unwrap(),
1_710_000_000_250_000
);
}
}

View File

@ -0,0 +1,598 @@
//! Capability-safe ADR-270 adapters for Origin AI and Plume/OpenSync.
//!
//! Origin publishes derived sensing results through contract-gated APIs and
//! webhooks. Public documentation does not define stable paths, so paths are
//! supplied by the contracted deployment configuration. OpenSync exports RF
//! and network telemetry; neither adapter promotes scalar data to complex CSI.
use serde::Deserialize;
use serde_json::{json, Value};
use std::collections::BTreeMap;
use wifi_densepose_hardware::vendor_rf::{
ProviderAvailability, ProviderDescriptor, RfCapability, VendorEventError, VendorId,
VendorRfEvent, VendorRfProvider,
};
const MAX_PAYLOAD_BYTES: usize = 256 * 1024;
const MAX_EVENTS_PER_PAYLOAD: usize = 256;
const MAX_ENDPOINT_LEN: usize = 2048;
const MAX_ENV_NAME_LEN: usize = 128;
/// A request plan deliberately containing a credential *reference*, not a secret.
#[derive(Debug, Clone, PartialEq)]
pub struct VendorRequest {
pub method: &'static str,
pub endpoint: String,
pub headers: BTreeMap<String, String>,
pub credential_env: Option<String>,
pub body: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OriginAiConfig {
/// Contract-provided HTTPS sensing-server base URL.
pub base_url: String,
/// Contract-provided relative event API path. No public path is assumed.
pub event_path: String,
/// Name of the environment variable holding the partner bearer token.
pub token_env: String,
}
impl OriginAiConfig {
pub fn validate(&self) -> Result<(), VendorEventError> {
validate_https_base(&self.base_url)?;
validate_relative_path(&self.event_path)?;
validate_env_name(&self.token_env)
}
/// Constructs a GET plan. The caller resolves `credential_env` at execution time.
pub fn events_request(&self) -> Result<VendorRequest, VendorEventError> {
self.validate()?;
Ok(VendorRequest {
method: "GET",
endpoint: join_endpoint(&self.base_url, &self.event_path),
headers: BTreeMap::from([("accept".into(), "application/json".into())]),
credential_env: Some(self.token_env.clone()),
body: None,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PlumeOpenSyncConfig {
/// HTTPS northbound/sandbox URL supplied by the operator.
pub base_url: String,
/// Relative endpoint accepting OVSDB JSON-RPC for this deployment.
pub ovsdb_path: String,
/// Optional environment variable used by a protected northbound endpoint.
pub token_env: Option<String>,
}
impl PlumeOpenSyncConfig {
pub fn validate(&self) -> Result<(), VendorEventError> {
validate_https_base(&self.base_url)?;
validate_relative_path(&self.ovsdb_path)?;
if let Some(name) = &self.token_env {
validate_env_name(name)?;
}
Ok(())
}
/// Builds a read-only OVSDB `select` transaction for an allow-listed table.
pub fn select_request(&self, table: &str) -> Result<VendorRequest, VendorEventError> {
self.validate()?;
if !matches!(
table,
"Wifi_Radio_State" | "Wifi_VIF_State" | "Wifi_Associated_Clients"
) {
return Err(VendorEventError::InvalidPayload);
}
Ok(VendorRequest {
method: "POST",
endpoint: join_endpoint(&self.base_url, &self.ovsdb_path),
headers: BTreeMap::from([
("accept".into(), "application/json".into()),
("content-type".into(), "application/json".into()),
]),
credential_env: self.token_env.clone(),
body: Some(json!({
"jsonrpc": "2.0",
"id": "ruview-read-only",
"method": "transact",
"params": ["Open_vSwitch", {"op": "select", "table": table, "where": []}]
})),
})
}
}
#[derive(Debug, Clone, Default)]
pub struct OriginAiProvider;
impl OriginAiProvider {
pub fn synthetic_fixture(seed: u64, count: usize) -> Vec<VendorRfEvent> {
let count = count.min(MAX_EVENTS_PER_PAYLOAD);
(0..count)
.map(|index| {
let state = splitmix64(seed.wrapping_add(index as u64));
let motion = (state & 1) as f64;
let confidence = 0.70 + ((state >> 8) % 300) as f64 / 1000.0;
VendorRfEvent {
vendor: VendorId::OriginAi,
capability: RfCapability::DerivedSensing,
sequence: index as u64,
timestamp_us: 1_700_000_000_000_000 + index as u64 * 100_000,
source_id: format!("origin-sim-{:08x}", seed as u32),
synthetic: true,
metrics: BTreeMap::from([
("motion".into(), motion),
("confidence".into(), confidence),
]),
label: Some(if motion == 1.0 { "motion" } else { "clear" }.into()),
}
})
.collect()
}
}
impl VendorRfProvider for OriginAiProvider {
fn descriptor(&self) -> ProviderDescriptor {
ProviderDescriptor {
vendor: VendorId::OriginAi,
capabilities: vec![RfCapability::DerivedSensing],
availability: ProviderAvailability::ContractRequired,
hardware_validated: false,
reason: "Origin partner API/SDK access is contract-gated; adapter accepts derived sensing only"
.into(),
}
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
check_payload(payload)?;
let envelope: OriginEnvelope = decode_json(payload)?;
if envelope.events.is_empty() || envelope.events.len() > MAX_EVENTS_PER_PAYLOAD {
return Err(VendorEventError::InvalidPayload);
}
envelope
.events
.into_iter()
.map(|event| event.into_vendor_event(&self.descriptor()))
.collect()
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct OriginEnvelope {
events: Vec<OriginEvent>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct OriginEvent {
sequence: u64,
timestamp_us: u64,
source_id: String,
kind: OriginKind,
confidence: f64,
#[serde(default)]
value: Option<f64>,
#[serde(default)]
label: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "snake_case")]
enum OriginKind {
Motion,
Occupancy,
Presence,
Fall,
BreathingRate,
}
impl OriginEvent {
fn into_vendor_event(
self,
descriptor: &ProviderDescriptor,
) -> Result<VendorRfEvent, VendorEventError> {
if self.timestamp_us == 0 || !(0.0..=1.0).contains(&self.confidence) {
return Err(VendorEventError::InvalidPayload);
}
let (metric, requires_value) = match self.kind {
OriginKind::Motion => ("motion", false),
OriginKind::Occupancy => ("occupancy", false),
OriginKind::Presence => ("presence", false),
OriginKind::Fall => ("fall", false),
OriginKind::BreathingRate => ("breathing_rate_bpm", true),
};
let value = self.value.ok_or(VendorEventError::InvalidPayload)?;
if !value.is_finite()
|| (requires_value && !(1.0..=120.0).contains(&value))
|| (!requires_value && value != 0.0 && value != 1.0)
{
return Err(VendorEventError::InvalidPayload);
}
let event = VendorRfEvent {
vendor: VendorId::OriginAi,
capability: RfCapability::DerivedSensing,
sequence: self.sequence,
timestamp_us: self.timestamp_us,
source_id: self.source_id,
synthetic: false,
metrics: BTreeMap::from([
(metric.into(), value),
("confidence".into(), self.confidence),
]),
label: self.label,
};
event.validate(descriptor)?;
Ok(event)
}
}
#[derive(Debug, Clone, Default)]
pub struct PlumeOpenSyncProvider;
impl PlumeOpenSyncProvider {
pub fn synthetic_fixture(seed: u64, count: usize) -> Vec<VendorRfEvent> {
let count = count.min(MAX_EVENTS_PER_PAYLOAD);
(0..count)
.map(|index| {
let state = splitmix64(seed.wrapping_add(index as u64));
VendorRfEvent {
vendor: VendorId::Plume,
capability: RfCapability::RfTelemetry,
sequence: index as u64,
timestamp_us: 1_700_000_000_000_000 + index as u64 * 250_000,
source_id: format!("opensync-sim-{:08x}", seed as u32),
synthetic: true,
metrics: BTreeMap::from([
("rssi_dbm".into(), -30.0 - (state % 55) as f64),
("channel".into(), 1.0 + ((state >> 8) % 165) as f64),
(
"noise_floor_dbm".into(),
-100.0 + ((state >> 16) % 12) as f64,
),
]),
label: None,
}
})
.collect()
}
}
impl VendorRfProvider for PlumeOpenSyncProvider {
fn descriptor(&self) -> ProviderDescriptor {
ProviderDescriptor {
vendor: VendorId::Plume,
capabilities: vec![RfCapability::RfTelemetry],
availability: ProviderAvailability::CredentialsRequired,
hardware_validated: false,
reason: "OpenSync radio/client telemetry only; Plume Sense is a separate gated service"
.into(),
}
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
check_payload(payload)?;
let envelope: OpenSyncEnvelope = decode_json(payload)?;
if envelope.observations.is_empty() || envelope.observations.len() > MAX_EVENTS_PER_PAYLOAD
{
return Err(VendorEventError::InvalidPayload);
}
envelope
.observations
.into_iter()
.map(|event| event.into_vendor_event(&self.descriptor()))
.collect()
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct OpenSyncEnvelope {
observations: Vec<OpenSyncObservation>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct OpenSyncObservation {
sequence: u64,
timestamp_us: u64,
source_id: String,
rssi_dbm: f64,
channel: u16,
#[serde(default)]
noise_floor_dbm: Option<f64>,
#[serde(default)]
tx_rate_mbps: Option<f64>,
#[serde(default)]
rx_rate_mbps: Option<f64>,
#[serde(default)]
clients: Option<u16>,
}
impl OpenSyncObservation {
fn into_vendor_event(
self,
descriptor: &ProviderDescriptor,
) -> Result<VendorRfEvent, VendorEventError> {
if self.timestamp_us == 0
|| !(-127.0..=0.0).contains(&self.rssi_dbm)
|| self.channel == 0
|| self.channel > 233
{
return Err(VendorEventError::InvalidPayload);
}
let mut metrics = BTreeMap::from([
("rssi_dbm".into(), self.rssi_dbm),
("channel".into(), self.channel as f64),
]);
insert_optional(
&mut metrics,
"noise_floor_dbm",
self.noise_floor_dbm,
-127.0,
0.0,
)?;
insert_optional(
&mut metrics,
"tx_rate_mbps",
self.tx_rate_mbps,
0.0,
100_000.0,
)?;
insert_optional(
&mut metrics,
"rx_rate_mbps",
self.rx_rate_mbps,
0.0,
100_000.0,
)?;
if let Some(clients) = self.clients {
metrics.insert("clients".into(), clients as f64);
}
let event = VendorRfEvent {
vendor: VendorId::Plume,
capability: RfCapability::RfTelemetry,
sequence: self.sequence,
timestamp_us: self.timestamp_us,
source_id: self.source_id,
synthetic: false,
metrics,
label: None,
};
event.validate(descriptor)?;
Ok(event)
}
}
fn insert_optional(
metrics: &mut BTreeMap<String, f64>,
key: &str,
value: Option<f64>,
minimum: f64,
maximum: f64,
) -> Result<(), VendorEventError> {
if let Some(value) = value {
if !value.is_finite() || !(minimum..=maximum).contains(&value) {
return Err(VendorEventError::InvalidPayload);
}
metrics.insert(key.into(), value);
}
Ok(())
}
fn check_payload(payload: &[u8]) -> Result<(), VendorEventError> {
if payload.is_empty() || payload.len() > MAX_PAYLOAD_BYTES {
Err(VendorEventError::InvalidPayload)
} else {
Ok(())
}
}
fn decode_json<T: for<'de> Deserialize<'de>>(payload: &[u8]) -> Result<T, VendorEventError> {
serde_json::from_slice(payload).map_err(|error| {
let message = error.to_string();
VendorEventError::MalformedPayload(message.chars().take(160).collect())
})
}
fn validate_https_base(value: &str) -> Result<(), VendorEventError> {
let authority = value
.strip_prefix("https://")
.and_then(|rest| rest.split('/').next())
.unwrap_or_default();
if value.len() > MAX_ENDPOINT_LEN
|| authority.is_empty()
|| authority.contains('@')
|| authority.chars().any(char::is_whitespace)
|| value.contains(['\r', '\n', '#', '?'])
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
fn validate_relative_path(value: &str) -> Result<(), VendorEventError> {
if value.len() > MAX_ENDPOINT_LEN
|| !value.starts_with('/')
|| value.starts_with("//")
|| value.contains(['\r', '\n', '#', '?'])
|| value.split('/').any(|segment| segment == "..")
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
fn validate_env_name(value: &str) -> Result<(), VendorEventError> {
if value.is_empty()
|| value.len() > MAX_ENV_NAME_LEN
|| !value
.bytes()
.all(|byte| byte.is_ascii_uppercase() || byte.is_ascii_digit() || byte == b'_')
|| value.as_bytes()[0].is_ascii_digit()
{
return Err(VendorEventError::InvalidPayload);
}
Ok(())
}
fn join_endpoint(base: &str, path: &str) -> String {
format!("{}{}", base.trim_end_matches('/'), path)
}
fn splitmix64(mut value: u64) -> u64 {
value = value.wrapping_add(0x9e37_79b9_7f4a_7c15);
value = (value ^ (value >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
value = (value ^ (value >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
value ^ (value >> 31)
}
#[cfg(test)]
mod tests {
use super::*;
use wifi_densepose_hardware::vendor_rf::MAX_VENDOR_TEXT_LEN;
#[test]
fn descriptors_are_honest_about_capabilities_and_access() {
let origin = OriginAiProvider.descriptor();
assert_eq!(origin.capabilities, vec![RfCapability::DerivedSensing]);
assert_eq!(origin.availability, ProviderAvailability::ContractRequired);
let plume = PlumeOpenSyncProvider.descriptor();
assert_eq!(plume.capabilities, vec![RfCapability::RfTelemetry]);
assert_eq!(
plume.availability,
ProviderAvailability::CredentialsRequired
);
assert!(!origin.capabilities.contains(&RfCapability::ComplexCsi));
assert!(!plume.capabilities.contains(&RfCapability::ComplexCsi));
}
#[test]
fn origin_decodes_only_derived_sensing() {
let payload = br#"{"events":[{"sequence":7,"timestamp_us":42,"source_id":"zone-a","kind":"occupancy","confidence":0.91,"value":1,"label":"occupied"}]}"#;
let events = OriginAiProvider.decode(payload).unwrap();
assert_eq!(events[0].capability, RfCapability::DerivedSensing);
assert_eq!(events[0].metrics["occupancy"], 1.0);
assert!(!events[0].synthetic);
}
#[test]
fn origin_rejects_unknown_raw_csi_and_bad_values() {
let raw = br#"{"events":[{"sequence":1,"timestamp_us":2,"source_id":"z","kind":"motion","confidence":1,"value":1,"raw_csi":[[1,2]]}]}"#;
assert!(OriginAiProvider.decode(raw).is_err());
let bad_confidence = br#"{"events":[{"sequence":1,"timestamp_us":2,"source_id":"z","kind":"motion","confidence":1.1,"value":1}]}"#;
assert_eq!(
OriginAiProvider.decode(bad_confidence),
Err(VendorEventError::InvalidPayload)
);
}
#[test]
fn plume_decodes_telemetry_without_inventing_csi() {
let payload = br#"{"observations":[{"sequence":9,"timestamp_us":10,"source_id":"pod-a","rssi_dbm":-54,"channel":36,"noise_floor_dbm":-94,"tx_rate_mbps":1200,"clients":4}]}"#;
let events = PlumeOpenSyncProvider.decode(payload).unwrap();
assert_eq!(events[0].capability, RfCapability::RfTelemetry);
assert_eq!(events[0].metrics["rssi_dbm"], -54.0);
assert!(!events[0].metrics.contains_key("csi"));
}
#[test]
fn malformed_empty_oversized_and_unbounded_arrays_fail_closed() {
assert!(OriginAiProvider.decode(b"{").is_err());
assert_eq!(
OriginAiProvider.decode(b""),
Err(VendorEventError::InvalidPayload)
);
assert_eq!(
PlumeOpenSyncProvider.decode(&vec![b' '; MAX_PAYLOAD_BYTES + 1]),
Err(VendorEventError::InvalidPayload)
);
let event = r#"{"sequence":1,"timestamp_us":2,"source_id":"p","rssi_dbm":-50,"channel":1}"#;
let payload = format!("{{\"observations\":[{}]}}", vec![event; 257].join(","));
assert_eq!(
PlumeOpenSyncProvider.decode(payload.as_bytes()),
Err(VendorEventError::InvalidPayload)
);
}
#[test]
fn telemetry_ranges_are_enforced() {
let payload = br#"{"observations":[{"sequence":1,"timestamp_us":2,"source_id":"p","rssi_dbm":4,"channel":36}]}"#;
assert_eq!(
PlumeOpenSyncProvider.decode(payload),
Err(VendorEventError::InvalidPayload)
);
}
#[test]
fn fixtures_are_deterministic_bounded_and_marked_synthetic() {
let a = OriginAiProvider::synthetic_fixture(19, 4);
assert_eq!(a, OriginAiProvider::synthetic_fixture(19, 4));
assert!(a.iter().all(|event| event.synthetic));
let p = PlumeOpenSyncProvider::synthetic_fixture(23, usize::MAX);
assert_eq!(p.len(), MAX_EVENTS_PER_PAYLOAD);
assert!(p.iter().all(|event| event.synthetic));
assert_eq!(p, PlumeOpenSyncProvider::synthetic_fixture(23, usize::MAX));
}
#[test]
fn request_plans_reference_secrets_without_embedding_them() {
let origin = OriginAiConfig {
base_url: "https://partner.example".into(),
event_path: "/contract/v1/events".into(),
token_env: "ORIGIN_AI_TOKEN".into(),
}
.events_request()
.unwrap();
assert_eq!(
origin.endpoint,
"https://partner.example/contract/v1/events"
);
assert_eq!(origin.credential_env.as_deref(), Some("ORIGIN_AI_TOKEN"));
assert!(format!("{origin:?}").find("Bearer ").is_none());
let plume = PlumeOpenSyncConfig {
base_url: "https://sandbox.example".into(),
ovsdb_path: "/ovsdb".into(),
token_env: Some("OPENSYNC_TOKEN".into()),
}
.select_request("Wifi_Radio_State")
.unwrap();
assert_eq!(plume.body.as_ref().unwrap()["method"], "transact");
assert_eq!(plume.credential_env.as_deref(), Some("OPENSYNC_TOKEN"));
}
#[test]
fn request_validation_rejects_injection_and_write_tables() {
let config = PlumeOpenSyncConfig {
base_url: "https://sandbox.example".into(),
ovsdb_path: "/ovsdb".into(),
token_env: None,
};
assert_eq!(
config.select_request("AWLAN_Node"),
Err(VendorEventError::InvalidPayload)
);
let bad = OriginAiConfig {
base_url: "http://insecure.example".into(),
event_path: "/events\r\nx: y".into(),
token_env: "token".into(),
};
assert_eq!(bad.events_request(), Err(VendorEventError::InvalidPayload));
}
#[test]
fn labels_and_source_ids_obey_shared_contract_bounds() {
let label = "x".repeat(MAX_VENDOR_TEXT_LEN + 1);
let payload = format!(
"{{\"events\":[{{\"sequence\":1,\"timestamp_us\":2,\"source_id\":\"z\",\"kind\":\"motion\",\"confidence\":1,\"value\":1,\"label\":\"{label}\"}}]}}"
);
assert_eq!(
OriginAiProvider.decode(payload.as_bytes()),
Err(VendorEventError::InvalidPayload)
);
}
}

View File

@ -0,0 +1,481 @@
//! ADR-270 providers whose useful integration surface is scalar telemetry,
//! network-only metadata, or an explicit no-go decision.
//!
//! None of these providers emits complex CSI. The small JSON contract in this
//! module is intended for sidecars and deterministic replay fixtures; it is not
//! a claim that a vendor exposes this exact wire format.
use serde::Deserialize;
use std::collections::BTreeMap;
use wifi_densepose_hardware::vendor_rf::{
ProviderAvailability, ProviderDescriptor, RfCapability, VendorEventError, VendorId,
VendorRfEvent, VendorRfProvider,
};
/// Upper bound applied before JSON decoding, limiting parser allocation.
pub const MAX_REMAINING_VENDOR_PAYLOAD_BYTES: usize = 64 * 1024;
/// Upper bound on events accepted in one sidecar envelope.
pub const MAX_REMAINING_VENDOR_EVENTS: usize = 256;
const ELECTRIC_IMP_METRICS: &[MetricRule] = &[
MetricRule::new("battery_v", 0.0, 100.0, false),
MetricRule::new("humidity_percent", 0.0, 100.0, false),
MetricRule::new("rssi_dbm", -127.0, 0.0, false),
MetricRule::new("temperature_c", -100.0, 200.0, false),
MetricRule::new("voltage_v", 0.0, 1_000.0, false),
];
const RF_SOLUTIONS_METRICS: &[MetricRule] = &[
MetricRule::new("battery_v", 0.0, 100.0, false),
MetricRule::new("humidity_percent", 0.0, 100.0, false),
MetricRule::new("relay_state", 0.0, 1.0, true),
MetricRule::new("rssi_dbm", -127.0, 0.0, false),
MetricRule::new("temperature_c", -100.0, 200.0, false),
];
const LUMA_METRICS: &[MetricRule] = &[
MetricRule::new("client_count", 0.0, 1_000_000.0, true),
MetricRule::new("noise_dbm", -127.0, 0.0, false),
MetricRule::new("rssi_dbm", -127.0, 0.0, false),
MetricRule::new("rx_bytes", 0.0, 9_007_199_254_740_991.0, true),
MetricRule::new("tx_bytes", 0.0, 9_007_199_254_740_991.0, true),
];
const GOOGLE_NEST_METRICS: &[MetricRule] = &[
MetricRule::new("client_count", 0.0, 1_000_000.0, true),
MetricRule::new("probe_count", 0.0, 1_000_000_000.0, true),
MetricRule::new("rx_bytes", 0.0, 9_007_199_254_740_991.0, true),
MetricRule::new("tx_bytes", 0.0, 9_007_199_254_740_991.0, true),
];
#[derive(Debug, Clone, Copy)]
struct MetricRule {
name: &'static str,
minimum: f64,
maximum: f64,
integer: bool,
}
impl MetricRule {
const fn new(name: &'static str, minimum: f64, maximum: f64, integer: bool) -> Self {
Self {
name,
minimum,
maximum,
integer,
}
}
fn accepts(self, value: f64) -> bool {
value.is_finite()
&& (self.minimum..=self.maximum).contains(&value)
&& (!self.integer || value.fract() == 0.0)
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ScalarEnvelope {
events: Vec<ScalarEvent>,
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct ScalarEvent {
sequence: u64,
timestamp_us: u64,
source_id: String,
synthetic: bool,
metrics: BTreeMap<String, f64>,
#[serde(default)]
label: Option<String>,
}
fn descriptor(
vendor: VendorId,
capability: RfCapability,
availability: ProviderAvailability,
reason: &str,
) -> ProviderDescriptor {
ProviderDescriptor {
vendor,
capabilities: vec![capability],
availability,
hardware_validated: false,
reason: reason.to_owned(),
}
}
fn decode_bounded_scalar_events(
payload: &[u8],
provider: &ProviderDescriptor,
capability: RfCapability,
allowed_metrics: &[MetricRule],
) -> Result<Vec<VendorRfEvent>, VendorEventError> {
provider.validate()?;
if !provider.capabilities.contains(&capability)
|| matches!(
capability,
RfCapability::ComplexCsi | RfCapability::Unsupported
)
{
return Err(VendorEventError::CapabilityMismatch);
}
if payload.is_empty() || payload.len() > MAX_REMAINING_VENDOR_PAYLOAD_BYTES {
return Err(VendorEventError::InvalidPayload);
}
let envelope: ScalarEnvelope = serde_json::from_slice(payload).map_err(|error| {
VendorEventError::MalformedPayload(error.to_string().chars().take(160).collect())
})?;
if envelope.events.is_empty() || envelope.events.len() > MAX_REMAINING_VENDOR_EVENTS {
return Err(VendorEventError::InvalidPayload);
}
envelope
.events
.into_iter()
.map(|event| {
// An allowlist keeps arbitrary scalar fields from silently changing
// the meaning of a provider contract (and rejects CSI-shaped data).
if event.timestamp_us == 0
|| event.source_id.chars().any(char::is_control)
|| event
.label
.as_deref()
.is_some_and(|label| label.chars().any(char::is_control))
{
return Err(VendorEventError::InvalidPayload);
}
for (key, value) in &event.metrics {
let rule = allowed_metrics
.iter()
.find(|rule| rule.name == key)
.ok_or(VendorEventError::CapabilityMismatch)?;
if !rule.accepts(*value) {
return Err(VendorEventError::InvalidPayload);
}
}
let event = VendorRfEvent {
vendor: provider.vendor,
capability,
sequence: event.sequence,
timestamp_us: event.timestamp_us,
source_id: event.source_id,
synthetic: event.synthetic,
metrics: event.metrics,
label: event.label,
};
event.validate(provider)?;
Ok(event)
})
.collect()
}
/// Electric Imp agent/impCentral scalar telemetry bridge for existing fleets.
#[derive(Debug, Clone, Copy, Default)]
pub struct ElectricImpProvider;
impl VendorRfProvider for ElectricImpProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::ElectricImp,
RfCapability::RfTelemetry,
ProviderAvailability::CredentialsRequired,
"Optional authenticated agent/impCentral scalar telemetry bridge; never CSI",
)
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
let descriptor = self.descriptor();
decode_bounded_scalar_events(
payload,
&descriptor,
RfCapability::RfTelemetry,
ELECTRIC_IMP_METRICS,
)
}
}
/// RF Solutions environmental/RIoT scalar telemetry boundary.
#[derive(Debug, Clone, Copy, Default)]
pub struct RfSolutionsProvider;
impl VendorRfProvider for RfSolutionsProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::RfSolutions,
RfCapability::RfTelemetry,
ProviderAvailability::Experimental,
"Optional non-Wi-Fi environmental telemetry fusion; excluded as a CSI source",
)
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
let descriptor = self.descriptor();
decode_bounded_scalar_events(
payload,
&descriptor,
RfCapability::RfTelemetry,
RF_SOLUTIONS_METRICS,
)
}
}
/// Generic OpenWrt telemetry fixture for already-owned discontinued Luma units.
#[derive(Debug, Clone, Copy, Default)]
pub struct LumaOpenWrtProvider;
impl VendorRfProvider for LumaOpenWrtProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::Luma,
RfCapability::RfTelemetry,
ProviderAvailability::Experimental,
"Generic OpenWrt scalar telemetry fixture for already-owned Luma hardware; no Luma CSI claim",
)
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
let descriptor = self.descriptor();
decode_bounded_scalar_events(
payload,
&descriptor,
RfCapability::RfTelemetry,
LUMA_METRICS,
)
}
}
/// Google Nest Wifi may participate as network infrastructure, not a sensor.
#[derive(Debug, Clone, Copy, Default)]
pub struct GoogleNestProvider;
impl VendorRfProvider for GoogleNestProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::GoogleNest,
RfCapability::NetworkOnly,
ProviderAvailability::Experimental,
"Network-only replay events; Device Access exposes no router CSI or RF telemetry",
)
}
fn decode(&self, payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
let descriptor = self.descriptor();
decode_bounded_scalar_events(
payload,
&descriptor,
RfCapability::NetworkOnly,
GOOGLE_NEST_METRICS,
)
}
}
/// Linksys Aware reached end of support; there is no supported sensing API.
#[derive(Debug, Clone, Copy, Default)]
pub struct LinksysProvider;
impl VendorRfProvider for LinksysProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::Linksys,
RfCapability::Unsupported,
ProviderAvailability::Unsupported,
"Linksys Aware reached end of support in 2024; no supported sensing interface",
)
}
fn decode(&self, _payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
Err(VendorEventError::Unsupported)
}
}
/// Wifigarden remains gated until its commercial SDK contract is disclosed.
#[derive(Debug, Clone, Copy, Default)]
pub struct WifigardenProvider;
impl VendorRfProvider for WifigardenProvider {
fn descriptor(&self) -> ProviderDescriptor {
descriptor(
VendorId::Wifigarden,
RfCapability::Unsupported,
ProviderAvailability::ContractRequired,
"Commercial SDK, chipset, schema, calibration and data-rights disclosure required",
)
}
fn decode(&self, _payload: &[u8]) -> Result<Vec<VendorRfEvent>, VendorEventError> {
Err(VendorEventError::ContractRequired)
}
}
/// Deterministic synthetic Electric Imp sidecar contract fixture.
pub const ELECTRIC_IMP_CONTRACT_FIXTURE: &[u8] = br#"{"events":[{"sequence":7,"timestamp_us":1700000000000000,"source_id":"imp005-fixture","synthetic":true,"metrics":{"rssi_dbm":-48.0,"temperature_c":21.5},"label":"lab"}]}"#;
/// Deterministic synthetic RF Solutions sidecar contract fixture.
pub const RF_SOLUTIONS_CONTRACT_FIXTURE: &[u8] = br#"{"events":[{"sequence":8,"timestamp_us":1700000000000100,"source_id":"riot-fixture","synthetic":true,"metrics":{"battery_v":3.1,"humidity_percent":44.0},"label":"lab"}]}"#;
/// Deterministic synthetic generic OpenWrt/Luma contract fixture.
pub const LUMA_OPENWRT_CONTRACT_FIXTURE: &[u8] = br#"{"events":[{"sequence":9,"timestamp_us":1700000000000200,"source_id":"luma-openwrt-fixture","synthetic":true,"metrics":{"client_count":3.0,"noise_dbm":-91.0,"rx_bytes":1024.0},"label":"openwrt"}]}"#;
/// Deterministic synthetic Google Nest network-only contract fixture.
pub const GOOGLE_NEST_CONTRACT_FIXTURE: &[u8] = br#"{"events":[{"sequence":10,"timestamp_us":1700000000000300,"source_id":"nest-fixture","synthetic":true,"metrics":{"client_count":4.0,"probe_count":2.0},"label":"network_activity"}]}"#;
#[cfg(test)]
mod tests {
use super::*;
fn assert_valid_fixture<P: VendorRfProvider>(
provider: P,
fixture: &[u8],
vendor: VendorId,
capability: RfCapability,
) {
let descriptor = provider.descriptor();
descriptor.validate().expect("descriptor must be valid");
let first = provider
.decode(fixture)
.expect("fixture must decode deterministically");
let second = provider.decode(fixture).expect("fixture must replay");
assert_eq!(first, second);
assert_eq!(first.len(), 1);
assert_eq!(first[0].vendor, vendor);
assert_eq!(first[0].capability, capability);
assert!(first[0].synthetic);
first[0]
.validate(&descriptor)
.expect("fixture event must satisfy provider contract");
}
#[test]
fn scalar_and_network_fixtures_are_deterministic_and_honest() {
assert_valid_fixture(
ElectricImpProvider,
ELECTRIC_IMP_CONTRACT_FIXTURE,
VendorId::ElectricImp,
RfCapability::RfTelemetry,
);
assert_valid_fixture(
RfSolutionsProvider,
RF_SOLUTIONS_CONTRACT_FIXTURE,
VendorId::RfSolutions,
RfCapability::RfTelemetry,
);
assert_valid_fixture(
LumaOpenWrtProvider,
LUMA_OPENWRT_CONTRACT_FIXTURE,
VendorId::Luma,
RfCapability::RfTelemetry,
);
assert_valid_fixture(
GoogleNestProvider,
GOOGLE_NEST_CONTRACT_FIXTURE,
VendorId::GoogleNest,
RfCapability::NetworkOnly,
);
}
#[test]
fn unavailable_providers_fail_before_interpreting_payloads() {
let linksys = LinksysProvider;
assert_eq!(
linksys.descriptor().availability,
ProviderAvailability::Unsupported
);
assert_eq!(
linksys.decode(b"not json"),
Err(VendorEventError::Unsupported)
);
let wifigarden = WifigardenProvider;
assert_eq!(
wifigarden.descriptor().availability,
ProviderAvailability::ContractRequired
);
assert_eq!(
wifigarden.decode(ELECTRIC_IMP_CONTRACT_FIXTURE),
Err(VendorEventError::ContractRequired)
);
}
#[test]
fn rejects_empty_oversized_and_excess_event_payloads() {
let provider = ElectricImpProvider;
assert_eq!(provider.decode(b""), Err(VendorEventError::InvalidPayload));
assert_eq!(
provider.decode(&vec![b' '; MAX_REMAINING_VENDOR_PAYLOAD_BYTES + 1]),
Err(VendorEventError::InvalidPayload)
);
let events = (0..=MAX_REMAINING_VENDOR_EVENTS)
.map(|sequence| {
format!(
r#"{{"sequence":{sequence},"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{{"rssi_dbm":-40.0}}}}"#
)
})
.collect::<Vec<_>>()
.join(",");
let payload = format!(r#"{{"events":[{events}]}}"#);
assert_eq!(
provider.decode(payload.as_bytes()),
Err(VendorEventError::InvalidPayload)
);
}
#[test]
fn rejects_csi_or_cross_provider_metric_masquerading() {
let fake_csi = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{"csi_real":1.0}}]}"#;
assert_eq!(
ElectricImpProvider.decode(fake_csi),
Err(VendorEventError::CapabilityMismatch)
);
let rf_metric_in_network_event = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{"rssi_dbm":-40.0}}]}"#;
assert_eq!(
GoogleNestProvider.decode(rf_metric_in_network_event),
Err(VendorEventError::CapabilityMismatch)
);
}
#[test]
fn rejects_invalid_values_bounds_and_schema_extensions() {
let non_finite = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{"rssi_dbm":1e999}}]}"#;
assert!(matches!(
ElectricImpProvider.decode(non_finite),
Err(VendorEventError::MalformedPayload(_)) | Err(VendorEventError::InvalidPayload)
));
let empty_metrics = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{}}]}"#;
assert_eq!(
ElectricImpProvider.decode(empty_metrics),
Err(VendorEventError::InvalidPayload)
);
let unknown_field = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","synthetic":true,"metrics":{"rssi_dbm":-40.0},"csi":[]}]}"#;
assert!(matches!(
ElectricImpProvider.decode(unknown_field),
Err(VendorEventError::MalformedPayload(_))
));
let missing_provenance = br#"{"events":[{"sequence":1,"timestamp_us":1,"source_id":"x","metrics":{"rssi_dbm":-40.0}}]}"#;
assert!(matches!(
ElectricImpProvider.decode(missing_provenance),
Err(VendorEventError::MalformedPayload(_))
));
}
#[test]
fn no_remaining_provider_claims_complex_csi_or_hardware_validation() {
let descriptors = [
ElectricImpProvider.descriptor(),
RfSolutionsProvider.descriptor(),
LumaOpenWrtProvider.descriptor(),
GoogleNestProvider.descriptor(),
LinksysProvider.descriptor(),
WifigardenProvider.descriptor(),
];
for descriptor in descriptors {
descriptor.validate().expect("descriptor must be valid");
assert!(!descriptor.hardware_validated);
assert!(!descriptor.capabilities.contains(&RfCapability::ComplexCsi));
}
}
}

View File

@ -0,0 +1,104 @@
//! ADR-270 provider registry and canonical event helpers.
use serde::Serialize;
use wifi_densepose_hardware::vendor_rf::{
ProviderDescriptor, VendorEventError, VendorId, VendorRfEvent, VendorRfProvider,
};
use crate::vendor_mist_netgear::{MistProvider, NetgearInsightProvider};
use crate::vendor_origin_plume::{OriginAiProvider, PlumeOpenSyncProvider};
use crate::vendor_remaining::{
ElectricImpProvider, GoogleNestProvider, LinksysProvider, LumaOpenWrtProvider,
RfSolutionsProvider, WifigardenProvider,
};
pub fn descriptor_for(vendor: VendorId) -> ProviderDescriptor {
match vendor {
VendorId::OriginAi => OriginAiProvider.descriptor(),
VendorId::Plume => PlumeOpenSyncProvider.descriptor(),
VendorId::Mist => MistProvider.descriptor(),
VendorId::Netgear => NetgearInsightProvider.descriptor(),
VendorId::ElectricImp => ElectricImpProvider.descriptor(),
VendorId::RfSolutions => RfSolutionsProvider.descriptor(),
VendorId::Linksys => LinksysProvider.descriptor(),
VendorId::Luma => LumaOpenWrtProvider.descriptor(),
VendorId::GoogleNest => GoogleNestProvider.descriptor(),
VendorId::Wifigarden => WifigardenProvider.descriptor(),
}
}
pub fn descriptors() -> Vec<ProviderDescriptor> {
VendorId::ALL.into_iter().map(descriptor_for).collect()
}
pub fn vendor_from_str(value: &str) -> Option<VendorId> {
VendorId::ALL
.into_iter()
.find(|vendor| vendor.as_str() == value)
}
pub fn decode_provider(
vendor: VendorId,
payload: &[u8],
) -> Result<Vec<VendorRfEvent>, VendorEventError> {
match vendor {
VendorId::OriginAi => OriginAiProvider.decode(payload),
VendorId::Plume => PlumeOpenSyncProvider.decode(payload),
VendorId::Mist => MistProvider.decode(payload),
VendorId::Netgear => NetgearInsightProvider.decode(payload),
VendorId::ElectricImp => ElectricImpProvider.decode(payload),
VendorId::RfSolutions => RfSolutionsProvider.decode(payload),
VendorId::Linksys => LinksysProvider.decode(payload),
VendorId::Luma => LumaOpenWrtProvider.decode(payload),
VendorId::GoogleNest => GoogleNestProvider.decode(payload),
VendorId::Wifigarden => WifigardenProvider.decode(payload),
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct VendorEventSnapshot {
pub event_type: &'static str,
pub source: String,
#[serde(flatten)]
pub event: VendorRfEvent,
}
impl VendorEventSnapshot {
pub fn from_event(event: VendorRfEvent) -> Result<Self, VendorEventError> {
let descriptor = descriptor_for(event.vendor);
event.validate(&descriptor)?;
let provenance = if event.synthetic { "simulated" } else { "live" };
Ok(Self {
event_type: "vendor_rf",
source: format!("vendor:{}:{provenance}", event.vendor.as_str()),
event,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn registry_has_exactly_one_valid_descriptor_per_vendor() {
let values = descriptors();
assert_eq!(values.len(), VendorId::ALL.len());
for (vendor, descriptor) in VendorId::ALL.into_iter().zip(values) {
assert_eq!(descriptor.vendor, vendor);
descriptor.validate().unwrap();
}
}
#[test]
fn unsupported_providers_fail_closed() {
assert_eq!(
decode_provider(VendorId::Linksys, b"{}"),
Err(VendorEventError::Unsupported)
);
assert_eq!(
decode_provider(VendorId::Wifigarden, b"{}"),
Err(VendorEventError::ContractRequired)
);
}
}