mirror of
https://github.com/ruvnet/RuView
synced 2026-06-09 10:13:17 +00:00
b116a99481
Raises the Nexmon path from a normalized record format to parsing what the patched Broadcom firmware actually emits, end to end. napi-c shim (ABI 1.0 -> 1.1, additive): - rvcsi_nx_csi_udp_header / rvcsi_nx_csi_udp_decode — parse the real nexmon_csi UDP payload: the 18-byte header (magic 0x1111, rssi int8, fctl, src_mac[6], seq_cnt, core/spatial-stream, Broadcom chanspec, chip_ver) + nsub complex CSI samples (modern int16 LE I/Q export — what CSIKit/csireader.py read for the BCM43455c0 / 4358 / 4366c0; nsub = (len-18)/4). rvcsi_nx_csi_udp_write to synthesize payloads for tests. rvcsi_nx_decode_chanspec — d11ac chanspec -> channel (chanspec & 0xff) / bandwidth (bits [13:11], cross-checked against the FFT size) / band (bits [15:14], cross-checked against the channel number). Still allocation-free, bounds-checked, structured errors, never panics. - ffi.rs wraps it: decode_chanspec / parse_nexmon_udp_header / decode_nexmon_udp / encode_nexmon_udp + DecodedChanspec / NexmonCsiHeader; every unsafe block documented; the ABI guard now expects 1.1. rvcsi-adapter-nexmon: - pcap.rs — a dependency-free classic-libpcap reader (all four byte-order / timestamp-resolution magics; Ethernet / raw-IPv4 / Linux-SLL link types; tolerates a truncated final record; pcapng is a follow-up) + extract_udp_payload + a synthetic_udp_pcap / synthetic_nexmon_pcap test/example generator. - NexmonPcapAdapter (a CsiSource) — reads the CSI UDP packets out of a `tcpdump -i wlan0 dst port 5500 -w csi.pcap` capture, decodes each via the C shim, stamps the frame timestamp from the pcap packet time; non-CSI packets counted as "skipped" in health. rvcsi-runtime: decode_nexmon_pcap, summarize_nexmon_pcap (+ NexmonPcapSummary: link type, CSI frame count, channels, bandwidths, subcarrier counts, chip versions, RSSI range, time span), CaptureRuntime::open_nexmon_pcap[_bytes]. rvcsi-node (napi-rs): nexmonDecodePcap, inspectNexmonPcap, decodeChanspec, RvcsiRuntime.openNexmonPcap. @ruv/rvcsi SDK + .d.ts updated (NexmonPcapSummary, DecodedChanspec). rvcsi-cli: `record --source nexmon-pcap`, `inspect-nexmon`, `decode-chanspec`. 161 rvcsi tests pass (adapter-nexmon 9->22), 0 failures, clippy-clean. ADR-096 §2.2/§2.3/§5, CHANGELOG, CLAUDE.md updated. https://claude.ai/code/session_01CdYAPvRTjcch6YrYf42n1z
314 lines
12 KiB
C
314 lines
12 KiB
C
/*
|
|
* rvCSI — Nexmon CSI compatibility shim implementation (napi-c layer).
|
|
* See rvcsi_nexmon_shim.h for the record/packet layouts and the contract.
|
|
*
|
|
* Deliberately tiny, allocation-free, and dependency-free (libc only). Every
|
|
* read is bounds-checked against the caller-supplied length; nothing here can
|
|
* scribble outside caller buffers, and nothing here panics or aborts.
|
|
*/
|
|
#include "rvcsi_nexmon_shim.h"
|
|
|
|
#include <string.h>
|
|
|
|
#define RVCSI_NX_ABI 0x00010001u /* major.minor = 1.1 (added the nexmon_csi UDP entry points) */
|
|
|
|
/* ---- little-endian load/store helpers (portable, no aliasing UB) ---- */
|
|
|
|
static uint16_t ld_u16(const uint8_t *p) {
|
|
return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8));
|
|
}
|
|
static uint32_t ld_u32(const uint8_t *p) {
|
|
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) |
|
|
((uint32_t)p[3] << 24);
|
|
}
|
|
static uint64_t ld_u64(const uint8_t *p) {
|
|
return (uint64_t)ld_u32(p) | ((uint64_t)ld_u32(p + 4) << 32);
|
|
}
|
|
static int16_t ld_i16(const uint8_t *p) { return (int16_t)ld_u16(p); }
|
|
|
|
static void st_u16(uint8_t *p, uint16_t v) {
|
|
p[0] = (uint8_t)(v & 0xFF);
|
|
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
|
}
|
|
static void st_u32(uint8_t *p, uint32_t v) {
|
|
p[0] = (uint8_t)(v & 0xFF);
|
|
p[1] = (uint8_t)((v >> 8) & 0xFF);
|
|
p[2] = (uint8_t)((v >> 16) & 0xFF);
|
|
p[3] = (uint8_t)((v >> 24) & 0xFF);
|
|
}
|
|
static void st_u64(uint8_t *p, uint64_t v) {
|
|
st_u32(p, (uint32_t)(v & 0xFFFFFFFFu));
|
|
st_u32(p + 4, (uint32_t)((v >> 32) & 0xFFFFFFFFu));
|
|
}
|
|
static void st_i16(uint8_t *p, int16_t v) { st_u16(p, (uint16_t)v); }
|
|
|
|
/* Q8.8 fixed-point <-> float, with saturation on encode (rvCSI record format). */
|
|
static float q88_to_f(int16_t v) { return (float)v / 256.0f; }
|
|
static int16_t f_to_q88(float f) {
|
|
float scaled = f * 256.0f;
|
|
if (scaled >= 32767.0f) return (int16_t)32767;
|
|
if (scaled <= -32768.0f) return (int16_t)-32768;
|
|
if (scaled >= 0.0f) return (int16_t)(scaled + 0.5f);
|
|
return (int16_t)(scaled - 0.5f);
|
|
}
|
|
|
|
/* Plain int16 <-> float for the raw nexmon_csi int16 I/Q export. */
|
|
static int16_t f_to_i16_sat(float f) {
|
|
if (f >= 32767.0f) return (int16_t)32767;
|
|
if (f <= -32768.0f) return (int16_t)-32768;
|
|
if (f >= 0.0f) return (int16_t)(f + 0.5f);
|
|
return (int16_t)(f - 0.5f);
|
|
}
|
|
|
|
uint32_t rvcsi_nx_abi_version(void) { return RVCSI_NX_ABI; }
|
|
|
|
const char *rvcsi_nx_strerror(int code) {
|
|
switch (code) {
|
|
case RVCSI_NX_OK: return "ok";
|
|
case RVCSI_NX_ERR_TOO_SHORT: return "buffer too short for header";
|
|
case RVCSI_NX_ERR_BAD_MAGIC: return "bad magic (not an rvCSI Nexmon record)";
|
|
case RVCSI_NX_ERR_BAD_VERSION: return "unsupported record version";
|
|
case RVCSI_NX_ERR_CAPACITY: return "output buffer too small for subcarrier count";
|
|
case RVCSI_NX_ERR_TRUNCATED: return "buffer shorter than the declared record";
|
|
case RVCSI_NX_ERR_ZERO_SUBCARRIERS: return "record declares zero subcarriers";
|
|
case RVCSI_NX_ERR_TOO_MANY_SUBCARRIERS: return "record declares too many subcarriers";
|
|
case RVCSI_NX_ERR_NULL_ARG: return "null argument";
|
|
case RVCSI_NX_ERR_BAD_NEXMON_MAGIC: return "nexmon_csi UDP magic mismatch (expected 0x1111)";
|
|
case RVCSI_NX_ERR_BAD_CSI_LEN: return "nexmon_csi CSI body length is not a positive multiple of 4";
|
|
case RVCSI_NX_ERR_UNKNOWN_FORMAT: return "unknown CSI body format";
|
|
default: return "unknown error";
|
|
}
|
|
}
|
|
|
|
/* ===== rvCSI record (format 1) ======================================== */
|
|
|
|
static int validate_header(const uint8_t *buf, size_t len, uint16_t *out_n,
|
|
size_t *out_total) {
|
|
if (len < (size_t)RVCSI_NX_HEADER_BYTES) return -RVCSI_NX_ERR_TOO_SHORT;
|
|
if (ld_u32(buf) != RVCSI_NX_MAGIC) return -RVCSI_NX_ERR_BAD_MAGIC;
|
|
if (buf[4] != (uint8_t)RVCSI_NX_VERSION) return -RVCSI_NX_ERR_BAD_VERSION;
|
|
uint16_t n = ld_u16(buf + 6);
|
|
if (n == 0) return -RVCSI_NX_ERR_ZERO_SUBCARRIERS;
|
|
if (n > RVCSI_NX_MAX_SUBCARRIERS) return -RVCSI_NX_ERR_TOO_MANY_SUBCARRIERS;
|
|
size_t total = (size_t)RVCSI_NX_HEADER_BYTES + (size_t)n * 4u;
|
|
if (len < total) return -RVCSI_NX_ERR_TRUNCATED;
|
|
*out_n = n;
|
|
*out_total = total;
|
|
return 0;
|
|
}
|
|
|
|
size_t rvcsi_nx_record_len(const uint8_t *buf, size_t len) {
|
|
if (buf == NULL) return 0;
|
|
uint16_t n;
|
|
size_t total;
|
|
if (validate_header(buf, len, &n, &total) < 0) return 0;
|
|
return total;
|
|
}
|
|
|
|
int rvcsi_nx_parse_record(const uint8_t *buf, size_t len, RvcsiNxMeta *meta,
|
|
float *i_out, float *q_out, size_t cap) {
|
|
if (buf == NULL || meta == NULL || i_out == NULL || q_out == NULL)
|
|
return RVCSI_NX_ERR_NULL_ARG;
|
|
|
|
uint16_t n;
|
|
size_t total;
|
|
int rc = validate_header(buf, len, &n, &total);
|
|
if (rc < 0) return -rc;
|
|
if ((size_t)n > cap) return RVCSI_NX_ERR_CAPACITY;
|
|
|
|
uint8_t flags = buf[5];
|
|
meta->subcarrier_count = n;
|
|
meta->channel = ld_u16(buf + 10);
|
|
meta->bandwidth_mhz = ld_u16(buf + 12);
|
|
meta->rssi_dbm =
|
|
(flags & RVCSI_NX_FLAG_RSSI) ? (int16_t)(int8_t)buf[8] : RVCSI_NX_ABSENT_I16;
|
|
meta->noise_floor_dbm =
|
|
(flags & RVCSI_NX_FLAG_NOISE) ? (int16_t)(int8_t)buf[9] : RVCSI_NX_ABSENT_I16;
|
|
meta->timestamp_ns = ld_u64(buf + 16);
|
|
|
|
const uint8_t *p = buf + RVCSI_NX_HEADER_BYTES;
|
|
for (uint16_t k = 0; k < n; ++k) {
|
|
i_out[k] = q88_to_f(ld_i16(p));
|
|
q_out[k] = q88_to_f(ld_i16(p + 2));
|
|
p += 4;
|
|
}
|
|
return RVCSI_NX_OK;
|
|
}
|
|
|
|
size_t rvcsi_nx_write_record(uint8_t *buf, size_t cap, const RvcsiNxMeta *meta,
|
|
const float *i_in, const float *q_in) {
|
|
if (buf == NULL || meta == NULL || i_in == NULL || q_in == NULL) return 0;
|
|
uint16_t n = meta->subcarrier_count;
|
|
if (n == 0 || n > RVCSI_NX_MAX_SUBCARRIERS) return 0;
|
|
size_t total = (size_t)RVCSI_NX_HEADER_BYTES + (size_t)n * 4u;
|
|
if (cap < total) return 0;
|
|
|
|
memset(buf, 0, RVCSI_NX_HEADER_BYTES);
|
|
st_u32(buf, RVCSI_NX_MAGIC);
|
|
buf[4] = (uint8_t)RVCSI_NX_VERSION;
|
|
uint8_t flags = 0;
|
|
if (meta->rssi_dbm != RVCSI_NX_ABSENT_I16) flags |= RVCSI_NX_FLAG_RSSI;
|
|
if (meta->noise_floor_dbm != RVCSI_NX_ABSENT_I16) flags |= RVCSI_NX_FLAG_NOISE;
|
|
buf[5] = flags;
|
|
st_u16(buf + 6, n);
|
|
buf[8] = (uint8_t)(int8_t)((flags & RVCSI_NX_FLAG_RSSI) ? meta->rssi_dbm : 0);
|
|
buf[9] = (uint8_t)(int8_t)((flags & RVCSI_NX_FLAG_NOISE) ? meta->noise_floor_dbm : 0);
|
|
st_u16(buf + 10, meta->channel);
|
|
st_u16(buf + 12, meta->bandwidth_mhz);
|
|
st_u16(buf + 14, 0);
|
|
st_u64(buf + 16, meta->timestamp_ns);
|
|
|
|
uint8_t *p = buf + RVCSI_NX_HEADER_BYTES;
|
|
for (uint16_t k = 0; k < n; ++k) {
|
|
st_i16(p, f_to_q88(i_in[k]));
|
|
st_i16(p + 2, f_to_q88(q_in[k]));
|
|
p += 4;
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/* ===== real nexmon_csi UDP payload (format 2) ========================= */
|
|
|
|
/* Map a subcarrier (FFT) count to a bandwidth in MHz, per the standard nexmon
|
|
* exports: 64->20, 128->40, 256->80, 512->160 (and the half-bands 32->10,
|
|
* 16->5). Returns 0 if `nsub` doesn't look like one of those. */
|
|
static uint16_t bw_from_nsub(uint16_t nsub) {
|
|
switch (nsub) {
|
|
case 16: return 5;
|
|
case 32: return 10;
|
|
case 64: return 20;
|
|
case 128: return 40;
|
|
case 256: return 80;
|
|
case 512: return 160;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
/* Broadcom d11ac chanspec bandwidth field (bits [13:11]) -> MHz. */
|
|
static uint16_t bw_from_chanspec(uint16_t chanspec) {
|
|
switch ((chanspec >> 11) & 0x7u) {
|
|
case 2: return 20;
|
|
case 3: return 40;
|
|
case 4: return 80;
|
|
case 5: return 160;
|
|
case 6: return 80; /* 80+80: report the per-segment width */
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
void rvcsi_nx_decode_chanspec(uint16_t chanspec, uint16_t *out_channel,
|
|
uint16_t *out_bw_mhz, uint8_t *out_is_5ghz) {
|
|
uint16_t channel = (uint16_t)(chanspec & 0x00FFu);
|
|
uint16_t bw = bw_from_chanspec(chanspec);
|
|
/* Band bits [15:14]: d11ac 5 GHz == 0b11. Cross-check with the channel number
|
|
* for robustness against older chanspec encodings. */
|
|
uint8_t band_is_5ghz = (((chanspec >> 14) & 0x3u) == 0x3u) ? 1u : 0u;
|
|
if (!band_is_5ghz && channel > 14u) band_is_5ghz = 1u;
|
|
if (band_is_5ghz && channel >= 1u && channel <= 13u && bw == 20u) {
|
|
/* almost certainly a 2.4 GHz control channel mislabeled by an old encoding */
|
|
band_is_5ghz = 0u;
|
|
}
|
|
if (out_channel) *out_channel = channel;
|
|
if (out_bw_mhz) *out_bw_mhz = bw;
|
|
if (out_is_5ghz) *out_is_5ghz = band_is_5ghz;
|
|
}
|
|
|
|
/* Validate + parse the 18-byte header; on success returns N (subcarrier count)
|
|
* and fills *out. On failure returns a negative RvcsiNxError. */
|
|
static int parse_nexmon_header(const uint8_t *payload, size_t len,
|
|
RvcsiNxUdpHeader *out, uint16_t *out_n) {
|
|
if (payload == NULL || out == NULL) return -RVCSI_NX_ERR_NULL_ARG;
|
|
if (len < (size_t)RVCSI_NX_NEXMON_HDR_BYTES) return -RVCSI_NX_ERR_TOO_SHORT;
|
|
if (ld_u16(payload) != RVCSI_NX_NEXMON_MAGIC) return -RVCSI_NX_ERR_BAD_NEXMON_MAGIC;
|
|
|
|
size_t csi_bytes = len - (size_t)RVCSI_NX_NEXMON_HDR_BYTES;
|
|
if (csi_bytes == 0u || (csi_bytes % 4u) != 0u) return -RVCSI_NX_ERR_BAD_CSI_LEN;
|
|
size_t nsub = csi_bytes / 4u;
|
|
if (nsub > RVCSI_NX_MAX_SUBCARRIERS) return -RVCSI_NX_ERR_TOO_MANY_SUBCARRIERS;
|
|
|
|
uint16_t core_stream = ld_u16(payload + 12);
|
|
uint16_t chanspec = ld_u16(payload + 14);
|
|
|
|
memset(out, 0, sizeof(*out));
|
|
out->rssi_dbm = (int16_t)(int8_t)payload[2];
|
|
out->fctl = payload[3];
|
|
memcpy(out->src_mac, payload + 4, 6);
|
|
out->seq_cnt = ld_u16(payload + 10);
|
|
out->core = (uint16_t)(core_stream & 0x7u);
|
|
out->spatial_stream = (uint16_t)((core_stream >> 3) & 0x7u);
|
|
out->chanspec = chanspec;
|
|
out->chip_ver = ld_u16(payload + 16);
|
|
rvcsi_nx_decode_chanspec(chanspec, &out->channel, &out->bandwidth_mhz, &out->is_5ghz);
|
|
out->subcarrier_count = (uint16_t)nsub;
|
|
/* Prefer the FFT-derived bandwidth when the chanspec bits are missing/odd. */
|
|
{
|
|
uint16_t bw_n = bw_from_nsub((uint16_t)nsub);
|
|
if (bw_n != 0u) out->bandwidth_mhz = bw_n;
|
|
}
|
|
*out_n = (uint16_t)nsub;
|
|
return 0;
|
|
}
|
|
|
|
int rvcsi_nx_csi_udp_header(const uint8_t *payload, size_t len,
|
|
RvcsiNxUdpHeader *out) {
|
|
uint16_t n;
|
|
int rc = parse_nexmon_header(payload, len, out, &n);
|
|
return (rc < 0) ? -rc : RVCSI_NX_OK;
|
|
}
|
|
|
|
int rvcsi_nx_csi_udp_decode(const uint8_t *payload, size_t len, int csi_format,
|
|
RvcsiNxUdpHeader *hdr_out, RvcsiNxMeta *meta,
|
|
float *i_out, float *q_out, size_t cap) {
|
|
if (meta == NULL || i_out == NULL || q_out == NULL) return RVCSI_NX_ERR_NULL_ARG;
|
|
if (csi_format != RVCSI_NX_CSI_FMT_INT16_IQ) return RVCSI_NX_ERR_UNKNOWN_FORMAT;
|
|
|
|
RvcsiNxUdpHeader hdr;
|
|
uint16_t n;
|
|
int rc = parse_nexmon_header(payload, len, &hdr, &n);
|
|
if (rc < 0) return -rc;
|
|
if ((size_t)n > cap) return RVCSI_NX_ERR_CAPACITY;
|
|
|
|
meta->subcarrier_count = n;
|
|
meta->channel = hdr.channel;
|
|
meta->bandwidth_mhz = hdr.bandwidth_mhz;
|
|
meta->rssi_dbm = hdr.rssi_dbm; /* always present in the nexmon header */
|
|
meta->noise_floor_dbm = RVCSI_NX_ABSENT_I16; /* not carried by nexmon_csi */
|
|
meta->timestamp_ns = 0u; /* the caller stamps this from the pcap packet time */
|
|
|
|
const uint8_t *p = payload + RVCSI_NX_NEXMON_HDR_BYTES;
|
|
for (uint16_t k = 0; k < n; ++k) {
|
|
i_out[k] = (float)ld_i16(p); /* real, raw int16 count */
|
|
q_out[k] = (float)ld_i16(p + 2); /* imag, raw int16 count */
|
|
p += 4;
|
|
}
|
|
if (hdr_out) *hdr_out = hdr;
|
|
return RVCSI_NX_OK;
|
|
}
|
|
|
|
size_t rvcsi_nx_csi_udp_write(uint8_t *buf, size_t cap, const RvcsiNxUdpHeader *hdr,
|
|
uint16_t subcarrier_count, const float *i_in,
|
|
const float *q_in) {
|
|
if (buf == NULL || hdr == NULL || i_in == NULL || q_in == NULL) return 0;
|
|
if (subcarrier_count == 0u || subcarrier_count > RVCSI_NX_MAX_SUBCARRIERS) return 0;
|
|
size_t total = (size_t)RVCSI_NX_NEXMON_HDR_BYTES + (size_t)subcarrier_count * 4u;
|
|
if (cap < total) return 0;
|
|
|
|
memset(buf, 0, RVCSI_NX_NEXMON_HDR_BYTES);
|
|
st_u16(buf, RVCSI_NX_NEXMON_MAGIC);
|
|
buf[2] = (uint8_t)(int8_t)hdr->rssi_dbm;
|
|
buf[3] = hdr->fctl;
|
|
memcpy(buf + 4, hdr->src_mac, 6);
|
|
st_u16(buf + 10, hdr->seq_cnt);
|
|
st_u16(buf + 12, (uint16_t)((hdr->core & 0x7u) | ((hdr->spatial_stream & 0x7u) << 3)));
|
|
st_u16(buf + 14, hdr->chanspec);
|
|
st_u16(buf + 16, hdr->chip_ver);
|
|
|
|
uint8_t *p = buf + RVCSI_NX_NEXMON_HDR_BYTES;
|
|
for (uint16_t k = 0; k < subcarrier_count; ++k) {
|
|
st_i16(p, f_to_i16_sat(i_in[k]));
|
|
st_i16(p + 2, f_to_i16_sat(q_in[k]));
|
|
p += 4;
|
|
}
|
|
return total;
|
|
}
|