|
|
|
|
@@ -0,0 +1,913 @@
|
|
|
|
|
/*
|
|
|
|
|
* rtlsdr_experimental.c — EXPERIMENTAL RTL2832U-over-USB-Host module.
|
|
|
|
|
*
|
|
|
|
|
* ============================================================================
|
|
|
|
|
* HARDWARE PASS REQUIRED — UNVERIFIED AGAINST REAL HARDWARE
|
|
|
|
|
* ============================================================================
|
|
|
|
|
* See rtlsdr_experimental.h for the full disclaimer and ../../README.md's
|
|
|
|
|
* "Workstream J" section for the complete honesty write-up. Short version:
|
|
|
|
|
* nobody working on this had physical ESP32-P4 or RTL2832U hardware, and
|
|
|
|
|
* this environment has no ESP-IDF toolchain to even compile against. Every
|
|
|
|
|
* function below is written to the *documented shape* of:
|
|
|
|
|
* (a) the ESP-IDF USB Host Library API (usb_host.h) — install/client/
|
|
|
|
|
* transfer lifecycle, callback dispatch model, control vs. bulk
|
|
|
|
|
* transfer submission, config/interface descriptor parsing helpers;
|
|
|
|
|
* (b) the RTL2832U + R820T vendor-command sequence already researched
|
|
|
|
|
* and documented in frontend/src/lib/sdr.ts (WebUSB driver for this
|
|
|
|
|
* same chipset, against public librtlsdr register docs).
|
|
|
|
|
* It has not been built or run. Register pokes, timing, and buffer sizing
|
|
|
|
|
* are carried over from sdr.ts's own "HARDWARE PASS REQUIRED" markings
|
|
|
|
|
* with no changes beyond translating WebUSB JS calls to USB Host C calls.
|
|
|
|
|
* ============================================================================
|
|
|
|
|
*
|
|
|
|
|
* Architecture (see README for the fuller picture):
|
|
|
|
|
*
|
|
|
|
|
* usb_host_lib_daemon_task — pumps usb_host_lib_handle_events() forever.
|
|
|
|
|
* Required by the USB Host Library regardless
|
|
|
|
|
* of how many clients exist.
|
|
|
|
|
* rtlsdr_exp_client_task — registers as the one USB Host *client* this
|
|
|
|
|
* module needs, pumps usb_host_client_handle_
|
|
|
|
|
* events() (which is also how *this task's*
|
|
|
|
|
* control- and bulk-transfer completion
|
|
|
|
|
* callbacks get dispatched — the ESP-IDF USB
|
|
|
|
|
* Host Library calls transfer callbacks
|
|
|
|
|
* synchronously from inside whichever task
|
|
|
|
|
* called *_handle_events(), not from an ISR
|
|
|
|
|
* or a hidden thread). On NEW_DEV it attempts
|
|
|
|
|
* bring-up; on DEV_GONE it tears down.
|
|
|
|
|
* rtlsdr_exp_forward_task — drains the IQ block queue that the bulk
|
|
|
|
|
* transfer callback feeds and hands each
|
|
|
|
|
* block to rtlsdr_exp_forward_iq_block()
|
|
|
|
|
* (the upstream-forwarding STUB).
|
|
|
|
|
*
|
|
|
|
|
* Bulk IQ reads are pipelined: `bulk_read_queue_depth` transfers are kept
|
|
|
|
|
* perpetually in flight (each callback re-submits itself), so the USB pipe
|
|
|
|
|
* doesn't idle waiting for the forward task to catch up. Completed buffers
|
|
|
|
|
* are handed off via a FreeRTOS queue; if the forward task falls behind,
|
|
|
|
|
* blocks are DROPPED (counted in stats.iq_blocks_dropped) rather than
|
|
|
|
|
* blocking the USB callback — losing samples is preferable to stalling the
|
|
|
|
|
* USB Host Library's event dispatch, which would also stall the client's
|
|
|
|
|
* disconnect detection. Whether this is an acceptable loss policy for the
|
|
|
|
|
* real product is exactly the kind of thing that needs real-hardware/real-
|
|
|
|
|
* throughput testing — see README.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include <string.h>
|
|
|
|
|
#include <stdlib.h>
|
|
|
|
|
|
|
|
|
|
#include "freertos/FreeRTOS.h"
|
|
|
|
|
#include "freertos/task.h"
|
|
|
|
|
#include "freertos/queue.h"
|
|
|
|
|
|
|
|
|
|
#include "esp_log.h"
|
|
|
|
|
#include "esp_check.h"
|
|
|
|
|
|
|
|
|
|
#include "usb/usb_host.h"
|
|
|
|
|
#include "usb/usb_helpers.h"
|
|
|
|
|
#include "usb/usb_types_ch9.h"
|
|
|
|
|
|
|
|
|
|
#include "esp_http_client.h"
|
|
|
|
|
|
|
|
|
|
#include "rtlsdr_experimental.h"
|
|
|
|
|
|
|
|
|
|
static const char *TAG = "rtlsdr_exp";
|
|
|
|
|
|
|
|
|
|
const uint16_t RTLSDR_EXP_KNOWN_PRODUCT_IDS[4] = { 0x2832, 0x2834, 0x2838, 0x2837 };
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Vendor-command constants — ported verbatim from frontend/src/lib/sdr.ts
|
|
|
|
|
* (CTRL_IN / CTRL_OUT / DEMOD / USB_EPA / SYS / PAGE_USB, lines ~37-42).
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
#define RTLSDR_CTRL_IN 0xC0u /* bmRequestType: IN | vendor | device */
|
|
|
|
|
#define RTLSDR_CTRL_OUT 0x40u /* bmRequestType: OUT | vendor | device */
|
|
|
|
|
#define RTLSDR_BLOCK_DEMOD 0x03u
|
|
|
|
|
#define RTLSDR_BLOCK_USB_EPA 0x02u
|
|
|
|
|
#define RTLSDR_BLOCK_SYS 0x09u
|
|
|
|
|
#define RTLSDR_PAGE_USB 0x01u
|
|
|
|
|
|
|
|
|
|
/* Default tuning + read-loop parameters (rtlsdr_exp_config_default). These
|
|
|
|
|
* mirror sdr.ts's open(sampleRateHz = 2_048_000) and its 98 MHz test tune;
|
|
|
|
|
* bulk_read_chunk_bytes/queue_depth are new (sdr.ts's readSamples() takes
|
|
|
|
|
* an explicit byte count per call from its sweep() caller, there is no
|
|
|
|
|
* fixed default there to inherit) and are UNVERIFIED guesses pending real
|
|
|
|
|
* throughput measurement. */
|
|
|
|
|
#define RTLSDR_EXP_DEFAULT_CENTER_HZ 98000000u
|
|
|
|
|
#define RTLSDR_EXP_DEFAULT_SAMPLE_RATE 2048000u
|
|
|
|
|
#define RTLSDR_EXP_DEFAULT_CHUNK_BYTES 16384u /* 32 * 512, USB HS multiple */
|
|
|
|
|
#define RTLSDR_EXP_DEFAULT_QUEUE_DEPTH 4u
|
|
|
|
|
#define RTLSDR_EXP_FORWARD_QUEUE_LEN 8u /* IQ blocks buffered for forward task */
|
|
|
|
|
#define RTLSDR_EXP_CTRL_XFER_TIMEOUT_MS 1000u
|
|
|
|
|
#define RTLSDR_EXP_USB_INTERFACE_NUM 0u
|
|
|
|
|
|
|
|
|
|
struct rtlsdr_exp_handle_s {
|
|
|
|
|
rtlsdr_exp_config_t cfg;
|
|
|
|
|
|
|
|
|
|
usb_host_client_handle_t client_hdl;
|
|
|
|
|
usb_device_handle_t dev_hdl;
|
|
|
|
|
bool device_open;
|
|
|
|
|
bool interface_claimed;
|
|
|
|
|
uint8_t ep_in_addr; /* bulk IN endpoint address, once found */
|
|
|
|
|
|
|
|
|
|
/* pending work handed from the client-event callback (kept short, per
|
|
|
|
|
* ESP-IDF USB Host guidance) to the client task's main loop */
|
|
|
|
|
volatile uint8_t pending_new_dev_addr; /* 0 == none pending */
|
|
|
|
|
volatile bool pending_dev_gone;
|
|
|
|
|
|
|
|
|
|
volatile bool running; /* set false by rtlsdr_exp_stop() */
|
|
|
|
|
volatile bool streaming; /* bulk read loop active */
|
|
|
|
|
|
|
|
|
|
TaskHandle_t lib_task;
|
|
|
|
|
TaskHandle_t client_task;
|
|
|
|
|
TaskHandle_t forward_task;
|
|
|
|
|
|
|
|
|
|
QueueHandle_t iq_block_queue; /* holds rtlsdr_iq_block_t* */
|
|
|
|
|
|
|
|
|
|
/* control-transfer synchronous-wait bookkeeping. Safe as plain fields
|
|
|
|
|
* (not a semaphore) because control transfers issued by this module are
|
|
|
|
|
* only ever awaited from the same task that also pumps
|
|
|
|
|
* usb_host_client_handle_events() — see file header. */
|
|
|
|
|
volatile bool ctrl_xfer_pending;
|
|
|
|
|
volatile bool ctrl_xfer_ok;
|
|
|
|
|
|
|
|
|
|
usb_transfer_t *bulk_transfers[8]; /* sized to the max we allow bulk_read_queue_depth to be */
|
|
|
|
|
|
|
|
|
|
rtlsdr_exp_stats_t stats;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
typedef struct {
|
|
|
|
|
uint8_t *data;
|
|
|
|
|
size_t len;
|
|
|
|
|
} rtlsdr_iq_block_t;
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Public: config defaults
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
void rtlsdr_exp_config_default(rtlsdr_exp_config_t *config)
|
|
|
|
|
{
|
|
|
|
|
if (!config) return;
|
|
|
|
|
memset(config, 0, sizeof(*config));
|
|
|
|
|
config->center_freq_hz = RTLSDR_EXP_DEFAULT_CENTER_HZ;
|
|
|
|
|
config->sample_rate_hz = RTLSDR_EXP_DEFAULT_SAMPLE_RATE;
|
|
|
|
|
config->bulk_read_chunk_bytes = RTLSDR_EXP_DEFAULT_CHUNK_BYTES;
|
|
|
|
|
config->bulk_read_queue_depth = RTLSDR_EXP_DEFAULT_QUEUE_DEPTH;
|
|
|
|
|
config->iq_upload_url = NULL; /* caller must set */
|
|
|
|
|
config->device_bearer_token = NULL; /* caller must set */
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Low-level control-transfer helpers.
|
|
|
|
|
* Direct port of sdr.ts's private writeReg()/demodWrite()/i2cWrite()
|
|
|
|
|
* (lines ~252-303). Same wValue/wIndex encoding, same register addresses,
|
|
|
|
|
* same repeater on/off dance for tuner I2C access. The WebUSB calls
|
|
|
|
|
*
|
|
|
|
|
* dev.controlTransferOut({ requestType: 'vendor', recipient: 'device',
|
|
|
|
|
* request: 0, value: ..., index: ... }, data)
|
|
|
|
|
*
|
|
|
|
|
* become a manually-built usb_setup_packet_t with bmRequestType =
|
|
|
|
|
* RTLSDR_CTRL_OUT (0x40 — OUT | vendor | device, matching WebUSB's
|
|
|
|
|
* 'vendor'/'device' fields), bRequest = 0, wValue/wIndex as below, wLength
|
|
|
|
|
* = payload length, submitted via usb_host_transfer_submit_control().
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
/* Synchronously wait for a previously-submitted control transfer to
|
|
|
|
|
* complete by pumping usb_host_client_handle_events() from THIS task (see
|
|
|
|
|
* file header for why that's safe/required here). Returns ESP_OK if the
|
|
|
|
|
* transfer completed successfully within the timeout. */
|
|
|
|
|
static esp_err_t rtlsdr_wait_ctrl_xfer(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
TickType_t start = xTaskGetTickCount();
|
|
|
|
|
TickType_t timeout_ticks = pdMS_TO_TICKS(RTLSDR_EXP_CTRL_XFER_TIMEOUT_MS);
|
|
|
|
|
while (h->ctrl_xfer_pending) {
|
|
|
|
|
usb_host_client_handle_events(h->client_hdl, pdMS_TO_TICKS(10));
|
|
|
|
|
if ((xTaskGetTickCount() - start) > timeout_ticks) {
|
|
|
|
|
ESP_LOGW(TAG, "control transfer timed out");
|
|
|
|
|
h->ctrl_xfer_pending = false;
|
|
|
|
|
return ESP_ERR_TIMEOUT;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return h->ctrl_xfer_ok ? ESP_OK : ESP_FAIL;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_ctrl_xfer_cb(usb_transfer_t *transfer)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)transfer->context;
|
|
|
|
|
h->ctrl_xfer_ok = (transfer->status == USB_TRANSFER_STATUS_COMPLETED);
|
|
|
|
|
h->ctrl_xfer_pending = false;
|
|
|
|
|
usb_host_transfer_free(transfer);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* value/index encoding matches sdr.ts's writeReg()/demodWrite() exactly:
|
|
|
|
|
* writeReg: wValue = (block<<8)|0x10, wIndex = address
|
|
|
|
|
* demodWrite: wValue = (DEMOD<<8)|0x10, wIndex = (page<<8)|address
|
|
|
|
|
* `length` is 1 or 2 bytes of little-endian `value`, same as sdr.ts. */
|
|
|
|
|
static esp_err_t rtlsdr_ctrl_write_raw(rtlsdr_exp_handle_t h, uint16_t wValue,
|
|
|
|
|
uint16_t wIndex, uint16_t value, uint8_t length)
|
|
|
|
|
{
|
|
|
|
|
if (!h->dev_hdl) return ESP_ERR_INVALID_STATE;
|
|
|
|
|
|
|
|
|
|
usb_transfer_t *transfer = NULL;
|
|
|
|
|
esp_err_t err = usb_host_transfer_alloc(sizeof(usb_setup_packet_t) + length, 0, &transfer);
|
|
|
|
|
if (err != ESP_OK) return err;
|
|
|
|
|
|
|
|
|
|
usb_setup_packet_t *setup = (usb_setup_packet_t *)transfer->data_buffer;
|
|
|
|
|
setup->bmRequestType = RTLSDR_CTRL_OUT;
|
|
|
|
|
setup->bRequest = 0;
|
|
|
|
|
setup->wValue = wValue;
|
|
|
|
|
setup->wIndex = wIndex;
|
|
|
|
|
setup->wLength = length;
|
|
|
|
|
|
|
|
|
|
uint8_t *payload = transfer->data_buffer + sizeof(usb_setup_packet_t);
|
|
|
|
|
payload[0] = (uint8_t)(value & 0xFF);
|
|
|
|
|
if (length > 1) payload[1] = (uint8_t)((value >> 8) & 0xFF);
|
|
|
|
|
|
|
|
|
|
transfer->device_handle = h->dev_hdl;
|
|
|
|
|
transfer->bEndpointAddress = 0; /* control endpoint */
|
|
|
|
|
transfer->num_bytes = sizeof(usb_setup_packet_t) + length;
|
|
|
|
|
transfer->callback = rtlsdr_ctrl_xfer_cb;
|
|
|
|
|
transfer->context = h;
|
|
|
|
|
transfer->timeout_ms = RTLSDR_EXP_CTRL_XFER_TIMEOUT_MS;
|
|
|
|
|
|
|
|
|
|
h->ctrl_xfer_pending = true;
|
|
|
|
|
h->ctrl_xfer_ok = false;
|
|
|
|
|
err = usb_host_transfer_submit_control(h->client_hdl, transfer);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
h->ctrl_xfer_pending = false;
|
|
|
|
|
usb_host_transfer_free(transfer);
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
return rtlsdr_wait_ctrl_xfer(h);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* writeReg() equivalent — sdr.ts private writeReg(block, address, value, length) */
|
|
|
|
|
static esp_err_t rtlsdr_reg_write(rtlsdr_exp_handle_t h, uint8_t block, uint16_t address,
|
|
|
|
|
uint16_t value, uint8_t length)
|
|
|
|
|
{
|
|
|
|
|
uint16_t wValue = (uint16_t)((block << 8) | 0x10);
|
|
|
|
|
return rtlsdr_ctrl_write_raw(h, wValue, address, value, length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* demodWrite() equivalent — sdr.ts private demodWrite(page, address, value, length) */
|
|
|
|
|
static esp_err_t rtlsdr_demod_write(rtlsdr_exp_handle_t h, uint8_t page, uint16_t address,
|
|
|
|
|
uint16_t value, uint8_t length)
|
|
|
|
|
{
|
|
|
|
|
uint16_t wValue = (uint16_t)((RTLSDR_BLOCK_DEMOD << 8) | 0x10);
|
|
|
|
|
uint16_t wIndex = (uint16_t)((page << 8) | address);
|
|
|
|
|
return rtlsdr_ctrl_write_raw(h, wValue, wIndex, value, length);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* i2cWrite() equivalent — sdr.ts private i2cWrite(i2cAddr, reg, value).
|
|
|
|
|
* Tunnels a single-byte tuner register write through the demod's I2C
|
|
|
|
|
* repeater: repeater on (demod 1/0x02 = 0x41) -> vendor write addressed to
|
|
|
|
|
* (i2cAddr<<8)|reg -> repeater off (demod 1/0x02 = 0x01). */
|
|
|
|
|
static esp_err_t rtlsdr_i2c_write(rtlsdr_exp_handle_t h, uint8_t i2c_addr, uint8_t reg, uint8_t value)
|
|
|
|
|
{
|
|
|
|
|
esp_err_t err = rtlsdr_demod_write(h, 1, 0x02, 0x41, 1); /* repeater on */
|
|
|
|
|
if (err != ESP_OK) return err;
|
|
|
|
|
|
|
|
|
|
uint16_t wValue = (uint16_t)((0x02 << 8) | 0x10);
|
|
|
|
|
uint16_t wIndex = (uint16_t)((i2c_addr << 8) | reg);
|
|
|
|
|
err = rtlsdr_ctrl_write_raw(h, wValue, wIndex, value, 1);
|
|
|
|
|
|
|
|
|
|
esp_err_t err2 = rtlsdr_demod_write(h, 1, 0x02, 0x01, 1); /* repeater off, always attempted */
|
|
|
|
|
return (err != ESP_OK) ? err : err2;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Tuning — port of sdr.ts's setFrequency()/setSampleRate() (lines
|
|
|
|
|
* ~156-180). Same simplifications, same "HARDWARE PASS REQUIRED" caveat:
|
|
|
|
|
* real librtlsdr computes exact sdm/vco from the crystal reference; this
|
|
|
|
|
* is the same reduced integer-N approximation sdr.ts uses, carried over
|
|
|
|
|
* unchanged rather than re-derived (see this project's Honesty-policy note
|
|
|
|
|
* — the browser driver's math is the primary reference, not a place to
|
|
|
|
|
* invent new, unverified math on top of already-unverified math).
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
esp_err_t rtlsdr_exp_set_frequency(rtlsdr_exp_handle_t h, uint32_t hz)
|
|
|
|
|
{
|
|
|
|
|
if (!h || !h->dev_hdl) return ESP_ERR_INVALID_STATE;
|
|
|
|
|
|
|
|
|
|
uint32_t lo_hz = hz + 3570000u; /* R820T IF offset */
|
|
|
|
|
uint32_t ref = 28800000u;
|
|
|
|
|
uint32_t mix_div = 2u;
|
|
|
|
|
uint32_t nint = lo_hz / (ref * mix_div);
|
|
|
|
|
uint32_t vco = lo_hz % (ref * mix_div);
|
|
|
|
|
uint32_t sdm = (uint32_t)(((uint64_t)vco * 65536u) / (ref * mix_div));
|
|
|
|
|
if (sdm > 0xFFFFu) sdm = 0xFFFFu;
|
|
|
|
|
uint8_t reg = (uint8_t)(nint & 0x3F);
|
|
|
|
|
|
|
|
|
|
esp_err_t err;
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1A, 0x10, reg)), TAG, "pll nint");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1A, 0x11, (sdm >> 8) & 0xFF)), TAG, "pll sdm hi");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1A, 0x12, sdm & 0xFF)), TAG, "pll sdm lo");
|
|
|
|
|
h->cfg.center_freq_hz = hz;
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t rtlsdr_exp_set_sample_rate(rtlsdr_exp_handle_t h, uint32_t hz)
|
|
|
|
|
{
|
|
|
|
|
if (!h || !h->dev_hdl) return ESP_ERR_INVALID_STATE;
|
|
|
|
|
|
|
|
|
|
uint32_t crystal = 28800000u;
|
|
|
|
|
uint32_t rsamp_ratio = (uint32_t)((((uint64_t)crystal << 22) / hz)) & 0x0FFFFFFCu;
|
|
|
|
|
|
|
|
|
|
esp_err_t err;
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x9f, (rsamp_ratio >> 16) & 0xFFFF, 2)), TAG, "rsamp hi");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0xa1, rsamp_ratio & 0xFFFF, 2)), TAG, "rsamp lo");
|
|
|
|
|
h->cfg.sample_rate_hz = hz;
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Init sequence — port of sdr.ts's open() body (lines ~110-140): demod
|
|
|
|
|
* soft reset -> demod_ctl/suspend-off register block -> standby off -> AGC
|
|
|
|
|
* mode -> R820T tuner power-up over the I2C repeater -> sample rate ->
|
|
|
|
|
* initial tune -> streaming endpoint reset -> test-mode off. Same order,
|
|
|
|
|
* same register addresses/values, unchanged.
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
static esp_err_t rtlsdr_run_init_sequence(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
esp_err_t err;
|
|
|
|
|
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x01, 0x14, 1)), TAG, "soft reset"); /* soft reset */
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x01, 0x10, 1)), TAG, "reset clear");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 0, 0x01, 0x08, 2)), TAG, "demod_ctl");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 0, 0x06, 0x80, 1)), TAG, "demod_ctl2");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x15, 0x00, 1)), TAG, "suspend off 0x15");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x16, 0x00, 1)), TAG, "suspend off 0x16");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x17, 0x00, 1)), TAG, "suspend off 0x17");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x18, 0x00, 1)), TAG, "suspend off 0x18");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x19, 0x00, 1)), TAG, "suspend off 0x19");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x1a, 0x00, 1)), TAG, "suspend off 0x1a");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x1b, 0x00, 1)), TAG, "suspend off 0x1b");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x1c, 0x00, 1)), TAG, "suspend off 0x1c");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x0d, 0x83, 1)), TAG, "standby off");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x0b, 0x1b, 1)), TAG, "AGC mode");
|
|
|
|
|
|
|
|
|
|
/* R820T power-up through I2C repeater (addr 0x1a) */
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1a, 0x05, 0x8f)), TAG, "LNA power");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1a, 0x08, 0x80)), TAG, "mixer");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_i2c_write(h, 0x1a, 0x0a, 0x10)), TAG, "IF filter");
|
|
|
|
|
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_exp_set_sample_rate(h, h->cfg.sample_rate_hz)), TAG, "sample rate");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_exp_set_frequency(h, h->cfg.center_freq_hz)), TAG, "frequency");
|
|
|
|
|
|
|
|
|
|
/* Reset streaming endpoint before bulk reads begin. */
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_reg_write(h, RTLSDR_BLOCK_USB_EPA, 0x0001, 0xFFFF, 2)), TAG, "ep reset");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x02, 0x00, 1)), TAG, "streaming pre");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 0, 0x02, 0x40, 2)), TAG, "streaming enable");
|
|
|
|
|
ESP_RETURN_ON_ERROR((err = rtlsdr_demod_write(h, 1, 0x02, 0x00, 1)), TAG, "test mode off");
|
|
|
|
|
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Bulk-IN read loop — NOT present in sdr.ts in this pipelined form (the
|
|
|
|
|
* WebUSB driver does one-shot `await dev.transferIn(...)` calls from
|
|
|
|
|
* inside its own async sweep loop; ESP-IDF's USB Host Library is callback-
|
|
|
|
|
* driven and async by design, so continuous streaming needs an explicit
|
|
|
|
|
* resubmit-on-completion pipeline instead). This is the least-verified
|
|
|
|
|
* part of this whole module — see README "What's unverified" — because
|
|
|
|
|
* correct chunk size, queue depth, and drop policy all depend on real
|
|
|
|
|
* measured USB throughput on real ESP32-P4 silicon, which nobody involved
|
|
|
|
|
* in this workstream has access to.
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
static void rtlsdr_bulk_xfer_cb(usb_transfer_t *transfer)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)transfer->context;
|
|
|
|
|
|
|
|
|
|
if (transfer->status == USB_TRANSFER_STATUS_COMPLETED && transfer->actual_num_bytes > 0) {
|
|
|
|
|
h->stats.bulk_reads_ok++;
|
|
|
|
|
|
|
|
|
|
rtlsdr_iq_block_t *block = malloc(sizeof(rtlsdr_iq_block_t));
|
|
|
|
|
uint8_t *copy = block ? malloc(transfer->actual_num_bytes) : NULL;
|
|
|
|
|
if (block && copy) {
|
|
|
|
|
memcpy(copy, transfer->data_buffer, transfer->actual_num_bytes);
|
|
|
|
|
block->data = copy;
|
|
|
|
|
block->len = (size_t)transfer->actual_num_bytes;
|
|
|
|
|
/* Non-blocking send: dropping is preferred to stalling the USB
|
|
|
|
|
* callback path (see file header). Drop is counted, not silent. */
|
|
|
|
|
if (h->iq_block_queue && xQueueSend(h->iq_block_queue, &block, 0) != pdTRUE) {
|
|
|
|
|
h->stats.iq_blocks_dropped++;
|
|
|
|
|
free(copy);
|
|
|
|
|
free(block);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
h->stats.iq_blocks_dropped++;
|
|
|
|
|
free(copy);
|
|
|
|
|
free(block);
|
|
|
|
|
}
|
|
|
|
|
} else if (transfer->status != USB_TRANSFER_STATUS_COMPLETED) {
|
|
|
|
|
h->stats.bulk_reads_failed++;
|
|
|
|
|
ESP_LOGW(TAG, "bulk IN transfer failed, status=%d", transfer->status);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Keep the pipe full: resubmit immediately unless we're stopping. */
|
|
|
|
|
if (h->streaming && h->running) {
|
|
|
|
|
esp_err_t err = usb_host_transfer_submit(transfer);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGW(TAG, "failed to resubmit bulk transfer: %d", err);
|
|
|
|
|
h->stats.bulk_reads_failed++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static esp_err_t rtlsdr_start_bulk_streaming(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
size_t depth = h->cfg.bulk_read_queue_depth;
|
|
|
|
|
if (depth == 0 || depth > (sizeof(h->bulk_transfers) / sizeof(h->bulk_transfers[0]))) {
|
|
|
|
|
depth = RTLSDR_EXP_DEFAULT_QUEUE_DEPTH;
|
|
|
|
|
}
|
|
|
|
|
if (h->cfg.bulk_read_chunk_bytes == 0 || (h->cfg.bulk_read_chunk_bytes % 512) != 0) {
|
|
|
|
|
ESP_LOGW(TAG, "bulk_read_chunk_bytes not a multiple of 512, forcing default");
|
|
|
|
|
h->cfg.bulk_read_chunk_bytes = RTLSDR_EXP_DEFAULT_CHUNK_BYTES;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (size_t i = 0; i < depth; i++) {
|
|
|
|
|
usb_transfer_t *t = NULL;
|
|
|
|
|
esp_err_t err = usb_host_transfer_alloc(h->cfg.bulk_read_chunk_bytes, 0, &t);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "failed to allocate bulk transfer %zu: %d", i, err);
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
t->device_handle = h->dev_hdl;
|
|
|
|
|
t->bEndpointAddress = h->ep_in_addr;
|
|
|
|
|
t->num_bytes = h->cfg.bulk_read_chunk_bytes;
|
|
|
|
|
t->callback = rtlsdr_bulk_xfer_cb;
|
|
|
|
|
t->context = h;
|
|
|
|
|
t->timeout_ms = 0; /* no timeout: this endpoint is expected to be
|
|
|
|
|
* continuously producing once streaming is on;
|
|
|
|
|
* UNVERIFIED whether 0 (no timeout) is the
|
|
|
|
|
* right choice vs. a bounded timeout + retry —
|
|
|
|
|
* needs real-hardware behavior to decide. */
|
|
|
|
|
h->bulk_transfers[i] = t;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h->streaming = true;
|
|
|
|
|
for (size_t i = 0; i < depth; i++) {
|
|
|
|
|
esp_err_t err = usb_host_transfer_submit(h->bulk_transfers[i]);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "failed to submit initial bulk transfer %zu: %d", i, err);
|
|
|
|
|
h->streaming = false;
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ESP_LOGI(TAG, "bulk IQ streaming started: chunk=%zuB depth=%zu",
|
|
|
|
|
h->cfg.bulk_read_chunk_bytes, depth);
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_stop_bulk_streaming(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
h->streaming = false;
|
|
|
|
|
for (size_t i = 0; i < sizeof(h->bulk_transfers) / sizeof(h->bulk_transfers[0]); i++) {
|
|
|
|
|
if (h->bulk_transfers[i]) {
|
|
|
|
|
usb_host_transfer_free(h->bulk_transfers[i]);
|
|
|
|
|
h->bulk_transfers[i] = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Upstream IQ forwarding — STUB. See README "IQ forwarding architecture
|
|
|
|
|
* decision" for the full reasoning. Summary: raw IQ at even a modest
|
|
|
|
|
* 2.048 Msps / 8-bit-per-sample-per-channel is ~4.1 MB/s, which is wildly
|
|
|
|
|
* incompatible with the regular `POST /api/device/telemetry` shape (per
|
|
|
|
|
* the ESP32-P4 Sensor Node spec's contract: readings array capped at 64
|
|
|
|
|
* entries, request body capped at 16KB, rate-limited to ~1 req/sec
|
|
|
|
|
* sustained per device) — that endpoint is sized for a handful of scalar
|
|
|
|
|
* sensor readings, not a continuous binary stream. Rather than distort
|
|
|
|
|
* that endpoint's shape (e.g. base64-stuffing IQ bytes into a JSON
|
|
|
|
|
* "reading" — which would also add ~33% overhead on top of an already
|
|
|
|
|
* too-large payload), this stub targets a SEPARATE, not-yet-implemented
|
|
|
|
|
* endpoint carrying raw binary chunks, authenticated the same way (device
|
|
|
|
|
* bearer token) but outside the JSON telemetry contract entirely.
|
|
|
|
|
*
|
|
|
|
|
* This function is intentionally inert by default (returns early unless
|
|
|
|
|
* iq_upload_url is set) because:
|
|
|
|
|
* 1. No backend endpoint for this exists anywhere in this repo yet —
|
|
|
|
|
* building one is explicitly out of scope for this workstream.
|
|
|
|
|
* 2. Even the *shape* proposed here (raw octet-stream POST per block,
|
|
|
|
|
* small fixed binary header) is a guess pending: (a) real measured
|
|
|
|
|
* USB throughput off actual hardware, (b) a product decision on
|
|
|
|
|
* whether continuous IQ upload is even desired given typical
|
|
|
|
|
* home-WiFi-uplink bandwidth, and (c) whether a WebSocket stream
|
|
|
|
|
* would suit the backend's existing async pub/sub model (see the
|
|
|
|
|
* spec's /ws/device-feed design) better than repeated POSTs.
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
|
|
|
|
|
/* Minimal fixed binary header prepended to each forwarded chunk, so the
|
|
|
|
|
* (not-yet-existing) backend endpoint can frame chunks without needing
|
|
|
|
|
* JSON/base64 parsing on a hot path. All fields little-endian.
|
|
|
|
|
* UNVERIFIED / PROPOSED — not agreed with any backend implementation. */
|
|
|
|
|
typedef struct __attribute__((packed)) {
|
|
|
|
|
uint32_t magic; /* 'QMIQ' = 0x51 0x4D 0x49 0x51 */
|
|
|
|
|
uint32_t seq;
|
|
|
|
|
uint32_t center_hz;
|
|
|
|
|
uint32_t sample_rate_hz;
|
|
|
|
|
uint32_t payload_len;
|
|
|
|
|
} rtlsdr_iq_chunk_header_t;
|
|
|
|
|
|
|
|
|
|
#define RTLSDR_IQ_CHUNK_MAGIC 0x51494D51u /* "QMIQ" */
|
|
|
|
|
|
|
|
|
|
static esp_err_t rtlsdr_exp_forward_iq_block(rtlsdr_exp_handle_t h, const uint8_t *data, size_t len)
|
|
|
|
|
{
|
|
|
|
|
if (!h->cfg.iq_upload_url || !h->cfg.device_bearer_token) {
|
|
|
|
|
/* No upload target configured — this is the expected state until a
|
|
|
|
|
* real backend endpoint exists and a caller opts in. Not an error. */
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint32_t s_seq = 0;
|
|
|
|
|
rtlsdr_iq_chunk_header_t hdr = {
|
|
|
|
|
.magic = RTLSDR_IQ_CHUNK_MAGIC,
|
|
|
|
|
.seq = s_seq++,
|
|
|
|
|
.center_hz = h->cfg.center_freq_hz,
|
|
|
|
|
.sample_rate_hz = h->cfg.sample_rate_hz,
|
|
|
|
|
.payload_len = (uint32_t)len,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
esp_http_client_config_t http_cfg = {
|
|
|
|
|
.url = h->cfg.iq_upload_url,
|
|
|
|
|
.method = HTTP_METHOD_POST,
|
|
|
|
|
.timeout_ms = 5000,
|
|
|
|
|
};
|
|
|
|
|
esp_http_client_handle_t client = esp_http_client_init(&http_cfg);
|
|
|
|
|
if (!client) return ESP_FAIL;
|
|
|
|
|
|
|
|
|
|
char auth_header[512];
|
|
|
|
|
snprintf(auth_header, sizeof(auth_header), "Bearer %s", h->cfg.device_bearer_token);
|
|
|
|
|
esp_http_client_set_header(client, "Authorization", auth_header);
|
|
|
|
|
esp_http_client_set_header(client, "Content-Type", "application/octet-stream");
|
|
|
|
|
|
|
|
|
|
esp_err_t err = esp_http_client_open(client, (int)(sizeof(hdr) + len));
|
|
|
|
|
if (err == ESP_OK) {
|
|
|
|
|
int written = esp_http_client_write(client, (const char *)&hdr, sizeof(hdr));
|
|
|
|
|
if (written == (int)sizeof(hdr)) {
|
|
|
|
|
written = esp_http_client_write(client, (const char *)data, (int)len);
|
|
|
|
|
}
|
|
|
|
|
if (written < 0) err = ESP_FAIL;
|
|
|
|
|
esp_http_client_fetch_headers(client);
|
|
|
|
|
int status = esp_http_client_get_status_code(client);
|
|
|
|
|
if (status < 200 || status >= 300) {
|
|
|
|
|
ESP_LOGW(TAG, "IQ upload got HTTP %d (endpoint likely doesn't exist yet)", status);
|
|
|
|
|
err = ESP_FAIL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
esp_http_client_close(client);
|
|
|
|
|
esp_http_client_cleanup(client);
|
|
|
|
|
|
|
|
|
|
if (err == ESP_OK) {
|
|
|
|
|
h->stats.bytes_forwarded_upstream += (uint32_t)len;
|
|
|
|
|
} else {
|
|
|
|
|
h->stats.forward_failures++;
|
|
|
|
|
}
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_forward_task(void *arg)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)arg;
|
|
|
|
|
rtlsdr_iq_block_t *block = NULL;
|
|
|
|
|
|
|
|
|
|
while (h->running) {
|
|
|
|
|
if (xQueueReceive(h->iq_block_queue, &block, pdMS_TO_TICKS(500)) == pdTRUE) {
|
|
|
|
|
rtlsdr_exp_forward_iq_block(h, block->data, block->len);
|
|
|
|
|
free(block->data);
|
|
|
|
|
free(block);
|
|
|
|
|
block = NULL;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* drain remaining queued blocks on shutdown without forwarding */
|
|
|
|
|
while (xQueueReceive(h->iq_block_queue, &block, 0) == pdTRUE) {
|
|
|
|
|
free(block->data);
|
|
|
|
|
free(block);
|
|
|
|
|
}
|
|
|
|
|
vTaskDelete(NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Device bring-up
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
static bool rtlsdr_vendor_id_matches(uint16_t vid)
|
|
|
|
|
{
|
|
|
|
|
return vid == RTLSDR_EXP_VENDOR_RTL2832U || vid == RTLSDR_EXP_VENDOR_TERRATEC;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_log_known_product_hint(uint16_t vid, uint16_t pid)
|
|
|
|
|
{
|
|
|
|
|
if (vid != RTLSDR_EXP_VENDOR_RTL2832U) return;
|
|
|
|
|
for (size_t i = 0; i < RTLSDR_EXP_NUM_KNOWN_PRODUCT_IDS; i++) {
|
|
|
|
|
if (RTLSDR_EXP_KNOWN_PRODUCT_IDS[i] == pid) return;
|
|
|
|
|
}
|
|
|
|
|
ESP_LOGI(TAG, "product id 0x%04x not in the known RTL2832U list — "
|
|
|
|
|
"attempting bring-up anyway (vendor-only match, same policy "
|
|
|
|
|
"as frontend/src/lib/sdr.ts's WebUSB device filter)", pid);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Finds the first bulk-IN endpoint on the device's active configuration's
|
|
|
|
|
* first interface. Mirrors sdr.ts's:
|
|
|
|
|
* iface = dev.configuration.interfaces[0]; alt = iface.alternates[0];
|
|
|
|
|
* ep = alt.endpoints.find(e => e.direction==='in' && e.type==='bulk') */
|
|
|
|
|
static esp_err_t rtlsdr_find_bulk_in_endpoint(usb_device_handle_t dev_hdl, uint8_t *out_ep)
|
|
|
|
|
{
|
|
|
|
|
const usb_config_desc_t *config_desc = NULL;
|
|
|
|
|
esp_err_t err = usb_host_get_active_config_descriptor(dev_hdl, &config_desc);
|
|
|
|
|
if (err != ESP_OK || !config_desc) return ESP_FAIL;
|
|
|
|
|
|
|
|
|
|
int offset = 0;
|
|
|
|
|
const usb_intf_desc_t *intf = usb_parse_interface_descriptor(config_desc, 0, 0, &offset);
|
|
|
|
|
if (!intf) return ESP_FAIL;
|
|
|
|
|
|
|
|
|
|
for (int i = 0; i < intf->bNumEndpoints; i++) {
|
|
|
|
|
int ep_offset = offset;
|
|
|
|
|
const usb_ep_desc_t *ep = usb_parse_endpoint_descriptor_by_index(
|
|
|
|
|
intf, i, config_desc->wTotalLength, &ep_offset);
|
|
|
|
|
if (!ep) continue;
|
|
|
|
|
bool is_in = (ep->bEndpointAddress & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK) != 0;
|
|
|
|
|
bool is_bulk = (ep->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK) == USB_BM_ATTRIBUTES_XFER_BULK;
|
|
|
|
|
if (is_in && is_bulk) {
|
|
|
|
|
*out_ep = ep->bEndpointAddress;
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ESP_ERR_NOT_FOUND;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_teardown_device(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_stop_bulk_streaming(h);
|
|
|
|
|
if (h->interface_claimed) {
|
|
|
|
|
usb_host_interface_release(h->client_hdl, h->dev_hdl, RTLSDR_EXP_USB_INTERFACE_NUM);
|
|
|
|
|
h->interface_claimed = false;
|
|
|
|
|
}
|
|
|
|
|
if (h->device_open) {
|
|
|
|
|
usb_host_device_close(h->client_hdl, h->dev_hdl);
|
|
|
|
|
h->device_open = false;
|
|
|
|
|
}
|
|
|
|
|
h->dev_hdl = NULL;
|
|
|
|
|
h->stats.device_present = false;
|
|
|
|
|
h->stats.bring_up_ok = false;
|
|
|
|
|
h->stats.streaming = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_try_bring_up(rtlsdr_exp_handle_t h, uint8_t dev_addr)
|
|
|
|
|
{
|
|
|
|
|
esp_err_t err = usb_host_device_open(h->client_hdl, dev_addr, &h->dev_hdl);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGW(TAG, "usb_host_device_open failed: %d", err);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
h->device_open = true;
|
|
|
|
|
|
|
|
|
|
const usb_device_desc_t *dev_desc = NULL;
|
|
|
|
|
err = usb_host_get_device_descriptor(h->dev_hdl, &dev_desc);
|
|
|
|
|
if (err != ESP_OK || !dev_desc) {
|
|
|
|
|
ESP_LOGW(TAG, "could not read device descriptor: %d", err);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!rtlsdr_vendor_id_matches(dev_desc->idVendor)) {
|
|
|
|
|
ESP_LOGD(TAG, "vendor 0x%04x is not RTL2832U/Terratec, ignoring", dev_desc->idVendor);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
rtlsdr_log_known_product_hint(dev_desc->idVendor, dev_desc->idProduct);
|
|
|
|
|
ESP_LOGI(TAG, "candidate RTL2832U-class device: vid=0x%04x pid=0x%04x",
|
|
|
|
|
dev_desc->idVendor, dev_desc->idProduct);
|
|
|
|
|
|
|
|
|
|
err = usb_host_interface_claim(h->client_hdl, h->dev_hdl, RTLSDR_EXP_USB_INTERFACE_NUM, 0);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGW(TAG, "could not claim interface %d: %d — device may be claimed by "
|
|
|
|
|
"another driver, or this isn't the RTL2832U bulk interface",
|
|
|
|
|
RTLSDR_EXP_USB_INTERFACE_NUM, err);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
h->interface_claimed = true;
|
|
|
|
|
|
|
|
|
|
err = rtlsdr_find_bulk_in_endpoint(h->dev_hdl, &h->ep_in_addr);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGW(TAG, "no bulk IN endpoint found on interface %d", RTLSDR_EXP_USB_INTERFACE_NUM);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h->stats.device_present = true;
|
|
|
|
|
|
|
|
|
|
err = rtlsdr_run_init_sequence(h);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "RTL2832U init vendor-command sequence failed at some step: %d "
|
|
|
|
|
"(UNVERIFIED sequence — see README, this is exactly the kind of "
|
|
|
|
|
"failure real hardware bring-up is expected to need to debug)", err);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
h->stats.bring_up_ok = true;
|
|
|
|
|
ESP_LOGI(TAG, "RTL2832U init sequence completed without a control-transfer error "
|
|
|
|
|
"(this does NOT confirm valid IQ is being produced — only a real "
|
|
|
|
|
"spectrum/logic-analyzer check against hardware can confirm that)");
|
|
|
|
|
|
|
|
|
|
err = rtlsdr_start_bulk_streaming(h);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "failed to start bulk IQ streaming: %d", err);
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
h->stats.streaming = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* USB Host Library plumbing: daemon task + client task + client event cb
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
static void rtlsdr_usb_lib_daemon_task(void *arg)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)arg;
|
|
|
|
|
while (h->running) {
|
|
|
|
|
uint32_t event_flags = 0;
|
|
|
|
|
usb_host_lib_handle_events(pdMS_TO_TICKS(1000), &event_flags);
|
|
|
|
|
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) {
|
|
|
|
|
ESP_LOGD(TAG, "USB host lib: no clients");
|
|
|
|
|
}
|
|
|
|
|
if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) {
|
|
|
|
|
ESP_LOGD(TAG, "USB host lib: all devices free");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
vTaskDelete(NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_client_event_cb(const usb_host_client_event_msg_t *event_msg, void *arg)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)arg;
|
|
|
|
|
/* Deliberately kept short — per ESP-IDF USB Host guidance, heavy work
|
|
|
|
|
* (device open, control transfers) is done back in the client task's
|
|
|
|
|
* main loop, not inside this callback. */
|
|
|
|
|
switch (event_msg->event) {
|
|
|
|
|
case USB_HOST_CLIENT_EVENT_NEW_DEV:
|
|
|
|
|
h->pending_new_dev_addr = event_msg->new_dev.address;
|
|
|
|
|
break;
|
|
|
|
|
case USB_HOST_CLIENT_EVENT_DEV_GONE:
|
|
|
|
|
h->pending_dev_gone = true;
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void rtlsdr_exp_client_task(void *arg)
|
|
|
|
|
{
|
|
|
|
|
rtlsdr_exp_handle_t h = (rtlsdr_exp_handle_t)arg;
|
|
|
|
|
|
|
|
|
|
usb_host_client_config_t client_config = {
|
|
|
|
|
.is_synchronous = false,
|
|
|
|
|
.max_num_event_msg = 5,
|
|
|
|
|
.async = {
|
|
|
|
|
.client_event_callback = rtlsdr_client_event_cb,
|
|
|
|
|
.callback_arg = h,
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
esp_err_t err = usb_host_client_register(&client_config, &h->client_hdl);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "usb_host_client_register failed: %d", err);
|
|
|
|
|
h->running = false;
|
|
|
|
|
vTaskDelete(NULL);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
while (h->running) {
|
|
|
|
|
/* This call is also what dispatches completion callbacks for any
|
|
|
|
|
* control/bulk transfers this client has outstanding — see file
|
|
|
|
|
* header. Timeout keeps the loop responsive to h->running. */
|
|
|
|
|
usb_host_client_handle_events(h->client_hdl, pdMS_TO_TICKS(200));
|
|
|
|
|
|
|
|
|
|
if (h->pending_new_dev_addr != 0) {
|
|
|
|
|
uint8_t addr = h->pending_new_dev_addr;
|
|
|
|
|
h->pending_new_dev_addr = 0;
|
|
|
|
|
if (!h->dev_hdl) { /* only attempt bring-up if we don't already hold a device */
|
|
|
|
|
rtlsdr_try_bring_up(h, addr);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (h->pending_dev_gone) {
|
|
|
|
|
h->pending_dev_gone = false;
|
|
|
|
|
ESP_LOGW(TAG, "RTL2832U device disconnected");
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rtlsdr_teardown_device(h);
|
|
|
|
|
usb_host_client_deregister(h->client_hdl);
|
|
|
|
|
h->client_hdl = NULL;
|
|
|
|
|
vTaskDelete(NULL);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
|
|
|
* Public lifecycle API
|
|
|
|
|
* ------------------------------------------------------------------------ */
|
|
|
|
|
esp_err_t rtlsdr_exp_init(const rtlsdr_exp_config_t *config, rtlsdr_exp_handle_t *out_handle)
|
|
|
|
|
{
|
|
|
|
|
if (!config || !out_handle) return ESP_ERR_INVALID_ARG;
|
|
|
|
|
|
|
|
|
|
rtlsdr_exp_handle_t h = calloc(1, sizeof(struct rtlsdr_exp_handle_s));
|
|
|
|
|
if (!h) return ESP_ERR_NO_MEM;
|
|
|
|
|
|
|
|
|
|
h->cfg = *config;
|
|
|
|
|
if (h->cfg.center_freq_hz == 0) h->cfg.center_freq_hz = RTLSDR_EXP_DEFAULT_CENTER_HZ;
|
|
|
|
|
if (h->cfg.sample_rate_hz == 0) h->cfg.sample_rate_hz = RTLSDR_EXP_DEFAULT_SAMPLE_RATE;
|
|
|
|
|
if (h->cfg.bulk_read_chunk_bytes == 0) h->cfg.bulk_read_chunk_bytes = RTLSDR_EXP_DEFAULT_CHUNK_BYTES;
|
|
|
|
|
if (h->cfg.bulk_read_queue_depth == 0) h->cfg.bulk_read_queue_depth = RTLSDR_EXP_DEFAULT_QUEUE_DEPTH;
|
|
|
|
|
|
|
|
|
|
h->iq_block_queue = xQueueCreate(RTLSDR_EXP_FORWARD_QUEUE_LEN, sizeof(rtlsdr_iq_block_t *));
|
|
|
|
|
if (!h->iq_block_queue) {
|
|
|
|
|
free(h);
|
|
|
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
*out_handle = h;
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t rtlsdr_exp_deinit(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
if (!h) return ESP_ERR_INVALID_ARG;
|
|
|
|
|
if (h->running) {
|
|
|
|
|
rtlsdr_exp_stop(h);
|
|
|
|
|
}
|
|
|
|
|
if (h->iq_block_queue) vQueueDelete(h->iq_block_queue);
|
|
|
|
|
free(h);
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t rtlsdr_exp_start(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
if (!h) return ESP_ERR_INVALID_ARG;
|
|
|
|
|
if (h->running) return ESP_ERR_INVALID_STATE;
|
|
|
|
|
|
|
|
|
|
/* NOTE: usb_host_install() installs a library instance shared by the
|
|
|
|
|
* whole firmware process. If Workstream I's core skeleton (or any
|
|
|
|
|
* future module) also needs USB host for something else, installing
|
|
|
|
|
* it twice is an error — this experimental module assumes it is the
|
|
|
|
|
* ONLY USB Host client in the project. Document/resolve this at merge
|
|
|
|
|
* time; see README integration notes. */
|
|
|
|
|
usb_host_config_t host_config = {
|
|
|
|
|
.skip_phy_setup = false,
|
|
|
|
|
.intr_flags = ESP_INTR_FLAG_LEVEL1,
|
|
|
|
|
};
|
|
|
|
|
esp_err_t err = usb_host_install(&host_config);
|
|
|
|
|
if (err != ESP_OK) {
|
|
|
|
|
ESP_LOGE(TAG, "usb_host_install failed: %d", err);
|
|
|
|
|
return err;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h->running = true;
|
|
|
|
|
|
|
|
|
|
BaseType_t ok = xTaskCreate(rtlsdr_usb_lib_daemon_task, "rtlsdr_usb_lib", 4096, h, 5, &h->lib_task);
|
|
|
|
|
if (ok != pdPASS) {
|
|
|
|
|
h->running = false;
|
|
|
|
|
usb_host_uninstall();
|
|
|
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ok = xTaskCreate(rtlsdr_exp_client_task, "rtlsdr_client", 6144, h, 5, &h->client_task);
|
|
|
|
|
if (ok != pdPASS) {
|
|
|
|
|
h->running = false;
|
|
|
|
|
vTaskDelete(h->lib_task);
|
|
|
|
|
usb_host_uninstall();
|
|
|
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ok = xTaskCreate(rtlsdr_forward_task, "rtlsdr_forward", 4096, h, 4, &h->forward_task);
|
|
|
|
|
if (ok != pdPASS) {
|
|
|
|
|
h->running = false;
|
|
|
|
|
vTaskDelete(h->client_task);
|
|
|
|
|
vTaskDelete(h->lib_task);
|
|
|
|
|
usb_host_uninstall();
|
|
|
|
|
return ESP_ERR_NO_MEM;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ESP_LOGI(TAG, "rtlsdr_experimental started (UNVERIFIED against real hardware — "
|
|
|
|
|
"see firmware/esp32p4-sensor-node/README.md)");
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
esp_err_t rtlsdr_exp_stop(rtlsdr_exp_handle_t h)
|
|
|
|
|
{
|
|
|
|
|
if (!h) return ESP_ERR_INVALID_ARG;
|
|
|
|
|
if (!h->running) return ESP_OK;
|
|
|
|
|
|
|
|
|
|
h->running = false;
|
|
|
|
|
/* Tasks self-delete once they observe h->running == false; give them a
|
|
|
|
|
* moment. A production version should use task-completion notification
|
|
|
|
|
* instead of a fixed delay — left as a TODO, not hardware-dependent but
|
|
|
|
|
* still unverified/untuned. */
|
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(500));
|
|
|
|
|
|
|
|
|
|
usb_host_uninstall(); /* see rtlsdr_exp_start() note re: sole USB client assumption */
|
|
|
|
|
return ESP_OK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void rtlsdr_exp_get_stats(rtlsdr_exp_handle_t h, rtlsdr_exp_stats_t *out_stats)
|
|
|
|
|
{
|
|
|
|
|
if (!h || !out_stats) return;
|
|
|
|
|
*out_stats = h->stats;
|
|
|
|
|
}
|