feat(firmware): add experimental RTL-SDR USB-host module (Workstream J)

Self-contained ESP-IDF component (firmware/esp32p4-sensor-node/components/
rtlsdr_experimental/) exploring RTL2832U-over-USB-host on the ESP32-P4,
ported from frontend/src/lib/sdr.ts's researched WebUSB protocol sequence
(vendor commands, I2C-repeater tuner init) to the ESP-IDF USB Host Library.

Implements: USB Host Library install/client lifecycle, RTL2832U/Terratec
vendor-ID device matching, the demod+R820T init vendor-command sequence
over control transfers, a pipelined bulk-IN read loop for raw IQ, and an
inert-by-default upstream IQ-forwarding stub targeting a proposed separate
binary endpoint (not the JSON telemetry shape — reasoning documented in
the README) since no such backend endpoint exists yet.

Off by default (RTLSDR_EXP_ENABLE Kconfig, default n). Unverified against
real hardware and never compiled (no ESP-IDF toolchain in this
environment) — marked as such in every source file and in a dedicated
"Workstream J" section of firmware/esp32p4-sensor-node/README.md, which
this commit also creates since Workstream I's core skeleton (owned by a
separate, unmerged worktree) hadn't created one yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Indiana
2026-07-24 01:10:21 +00:00
parent cf817e5241
commit abd7174c9c
5 changed files with 1440 additions and 0 deletions

View File

@@ -0,0 +1,151 @@
/*
* rtlsdr_experimental.h — EXPERIMENTAL RTL2832U-over-USB-Host module for the
* ESP32-P4 sensor node (Workstream J, ESP32-P4 Sensor Node spec).
*
* ============================================================================
* HARDWARE PASS REQUIRED — UNVERIFIED AGAINST REAL HARDWARE
* ============================================================================
* This entire module (this header + rtlsdr_experimental.c) has been written
* from documented ESP-IDF USB Host Library API shape and the RTL2832U/R820T
* register sequence already researched for this project in
* frontend/src/lib/sdr.ts (a WebUSB driver, real and documented against
* librtlsdr's public register map). It has NOT been compiled against a real
* ESP-IDF toolchain (none is available in this environment) and has NOT run
* against real ESP32-P4 + RTL2832U dongle hardware. Treat every register
* value, timing constant, buffer size, and API call signature here as
* "structurally reasoned, not verified." See the "Workstream J" section of
* ../../README.md for the full honesty write-up, including exactly what a
* real hardware bring-up session needs to check before this is trusted.
*
* Scope reminder: this is a self-contained ESP-IDF *component*, deliberately
* kept out of `main/` so it can be dropped into Workstream I's firmware
* project skeleton (WiFi/HTTP/BME280/presence-sensor core) without touching
* any file that workstream owns. Integration is one line in the consuming
* project: add this directory to EXTRA_COMPONENT_DIRS (or copy it under that
* project's own components/) and call rtlsdr_exp_init()/rtlsdr_exp_start()
* from wherever app_main() sets up its other sensor drivers.
* ============================================================================
*/
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/* ---------------------------------------------------------------------------
* USB identification — ported 1:1 from frontend/src/lib/sdr.ts
* (RTL2832U_VENDOR / RTL2832U_PRODUCTS / TERRATEC_VENDOR, lines ~30-34).
*
* sdr.ts deliberately filters WebUSB's device picker by *vendor* id only
* (see its requestDevice() comment: "many dongles report product ids
* outside the handful we know, so filtering by productId hides them from
* the picker"). We mirror that: RTLSDR_EXP_KNOWN_PRODUCT_IDS below is
* informational only (used for a startup log line), never a hard match
* filter. Any device presenting vendor id RTL2832U or TERRATEC is treated
* as a candidate and bring-up is attempted.
* ------------------------------------------------------------------------ */
#define RTLSDR_EXP_VENDOR_RTL2832U 0x0BDAu
#define RTLSDR_EXP_VENDOR_TERRATEC 0x0CCDu
/* Known RTL2832U product ids (informational/logging only — see above). */
extern const uint16_t RTLSDR_EXP_KNOWN_PRODUCT_IDS[4];
#define RTLSDR_EXP_NUM_KNOWN_PRODUCT_IDS 4
/* ---------------------------------------------------------------------------
* Configuration
* ------------------------------------------------------------------------ */
typedef struct {
/* Tuning defaults, applied once bring-up completes. Matches sdr.ts's
* open(sampleRateHz = 2_048_000) default and its FM-band test tune of
* 98 MHz — pick whatever the product actually wants to listen to. */
uint32_t center_freq_hz; /* e.g. 98_000_000 */
uint32_t sample_rate_hz; /* e.g. 2_048_000 */
/* Bulk-IN read tuning. bulk_read_chunk_bytes MUST be a multiple of 512
* (USB high-speed bulk max packet size) — this mirrors sdr.ts's own
* assumption ("Length must be multiple of 512" on readSamples()).
* UNVERIFIED: real chunk size vs. USB Host Library heap/DMA limits and
* actual achievable throughput needs a real device. See README. */
size_t bulk_read_chunk_bytes; /* default suggestion: 16384 */
size_t bulk_read_queue_depth; /* in-flight pipelined transfers, e.g. 4 */
/* Upstream IQ forwarding (see rtlsdr_exp_forward_iq_block() and the
* "IQ forwarding architecture" section of the README for the reasoning
* behind a *separate* raw-binary stream rather than reusing the JSON
* sensor telemetry endpoint). NOTE: the backend endpoint referenced
* here does not exist yet anywhere in this repo as of this workstream —
* this is a client-side stub against a *proposed* shape, not a wired
* integration. */
const char *iq_upload_url; /* e.g. "https://host/api/device/iq-stream" */
const char *device_bearer_token; /* same raw pairing token used for
* the regular telemetry POST loop */
} rtlsdr_exp_config_t;
/* Fills in documented, conservative defaults. Caller still MUST set
* iq_upload_url / device_bearer_token before starting. */
void rtlsdr_exp_config_default(rtlsdr_exp_config_t *config);
typedef struct rtlsdr_exp_handle_s *rtlsdr_exp_handle_t;
/* ---------------------------------------------------------------------------
* Lifecycle
* ------------------------------------------------------------------------ */
/* Allocates internal state. Does NOT touch USB hardware yet. */
esp_err_t rtlsdr_exp_init(const rtlsdr_exp_config_t *config, rtlsdr_exp_handle_t *out_handle);
esp_err_t rtlsdr_exp_deinit(rtlsdr_exp_handle_t handle);
/* Installs the USB Host Library, registers a client, and spawns the
* background tasks that watch for a matching RTL2832U device, run the
* init vendor-command sequence on attach, and (once bring-up succeeds)
* start the bulk-IN read/forward loop. Non-blocking — returns once the
* tasks are created, not once a device is found.
*
* UNVERIFIED beyond "this is the documented shape of the ESP-IDF USB Host
* Library API" — see README for exactly what needs a real board to check. */
esp_err_t rtlsdr_exp_start(rtlsdr_exp_handle_t handle);
/* Stops streaming, releases the USB interface/device if held, deregisters
* the USB Host client, and tears down the background tasks. Does NOT call
* usb_host_uninstall() if other USB Host clients might still be active in
* the wider firmware project (Workstream I owns whether anything else in
* the project also uses USB host) — see README integration notes. */
esp_err_t rtlsdr_exp_stop(rtlsdr_exp_handle_t handle);
/* Best-effort retune while streaming — same caveats as sdr.ts's
* setFrequency()/setSampleRate() (simplified PLL/resampler math, marked
* HARDWARE PASS REQUIRED there too). */
esp_err_t rtlsdr_exp_set_frequency(rtlsdr_exp_handle_t handle, uint32_t hz);
esp_err_t rtlsdr_exp_set_sample_rate(rtlsdr_exp_handle_t handle, uint32_t hz);
/* ---------------------------------------------------------------------------
* Observability — deliberately exposed so this experimental module can
* report its own health (e.g. folded into the regular sensor_type registry
* as a synthetic "sdr_status" reading) rather than fail silently.
* ------------------------------------------------------------------------ */
typedef struct {
bool device_present; /* a matching-vendor USB device is enumerated */
bool bring_up_ok; /* init vendor-command sequence completed without
* a control-transfer error (does NOT mean the
* tuner/demod are actually producing valid IQ —
* only real hardware + a spectrum check can
* confirm that) */
bool streaming;
uint32_t bulk_reads_ok;
uint32_t bulk_reads_failed;
uint32_t iq_blocks_dropped; /* forward queue was full — see README */
uint32_t bytes_forwarded_upstream;
uint32_t forward_failures;
} rtlsdr_exp_stats_t;
void rtlsdr_exp_get_stats(rtlsdr_exp_handle_t handle, rtlsdr_exp_stats_t *out_stats);
#ifdef __cplusplus
}
#endif