rx/tx start handlers called the registry's blocking SDR open (and TX init)
directly on the asyncio loop. A USRP open shells out to uhd_find_devices and
loads its FPGA — several seconds — freezing the WebSocket keepalive long enough
for the hub to drop the agent and stop the app, with the agent terminal hung in
the blocking call. A Pluto opens fast enough to slip under the timeout, which is
why it worked where a USRP did not.
Run acquire/_apply_sdr_config (rx) and acquire/config/init_tx (tx) in a thread
via run_in_executor, keeping release/close-on-failure inside the thread. Also
build the heartbeat off the loop, since it can probe hardware (uhd_find_devices)
while idle and block the keepalive the same way.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The agent capture loop calls sdr.rx(buffer_size) per chunk. USRP inherited the
base rx() → record(), which issued start_cont/stop_cont and slept 0.1s EVERY
buffer. At 2.5 MSps that captured ~1.6 ms of IQ per ~100 ms — heavily gapped,
transient-laden, and zero-filled on timeout, which rendered as choppy/black
bands in the spectrogram.
USRP.rx() now keeps a single continuous stream running across calls:
- issues start_cont once (lazily, on first rx()),
- recv()s until the request is filled, carrying any over-read into a residual
buffer so nothing is dropped between rx() boundaries (gapless),
- tolerates overflow (samples still valid), treats repeated timeouts as a
disconnect, and stops the stream on close().
Hardware-free tests stub uhd and prove gaplessness, single start, overflow
handling, timeout->disconnect, and stop cleanup.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
heartbeat detect_devices() shelled out to uhd_find_devices every 60s. When a
USB SDR (USRP B2x0) was mid-capture, that probe disrupted the live stream and
the device briefly vanished ("No UHD Devices Found"), killing the capture.
detect_devices() gains a probe flag; heartbeat_payload passes probe=False
whenever a session is active, returning the last good enumeration (or a
driver-only list) instead of touching the hardware.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The CLI get_sdr_device path can only open a USB USRP via auto-select: its
_create_device_dict matches the identifier against raw device-dict values,
but common.py prepends "addr="/"name=" before handing it over, so no prefixed
identifier ever matches (this is also why addr=192.168.3.1 failed to match the
B210). Advertising name=<name> was therefore unusable.
detect_devices() now advertises a single USRP entry with identifier=None
(auto-select the sole device), labelled with the serial(s) found. The hub
forwards None, so the agent opens USRP() and picks the attached B210.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make `ria-agent stream` work with any SDR the agent has drivers for, with
no per-device config in the hub:
- heartbeat advertises rich `hardware` entries {device, identifier, label,
connected} via hardware.detect_devices(); USRP is enumerated into concrete
instances (uhd_find_devices), others advertise driver-only entries. The
identifier is chosen to round-trip through parse_ident (None=auto-select or
name=...), so a device address is never a bare value.
- ship udev rules (Pluto/RTL-SDR/HackRF/USRP B2x0/bladeRF) + `ria-agent
install-udev` so USB radios open without sudo — one privileged step, all
inside the toolkit.
- streamer surfaces a "run: sudo ria-agent install-udev" hint on USB
permission errors instead of the cryptic UHD message.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`websockets` is used by `ria_toolkit_oss.agent.ws_client`, which is
imported as part of `ria-agent stream`. It was only declared in the
optional `[tool.poetry.group.agent]` poetry group, so a vanilla
`pip install ria-toolkit-oss` left `ria-agent stream` failing with
`ModuleNotFoundError: No module named 'websockets'`.
Moved into PEP 621 `[project].dependencies` with the same constraint
(`>=12.0,<14.0`). The duplicate in the optional poetry group is left
in place so `poetry install --with agent` remains self-sufficient.
poetry.lock: `websockets` now joins the `main` dependency group; the
generator-header bump (2.1.4 → 2.3.4) and the `jsonschema-specifications`
version-string normalization are auto-regenerated noise from the
locally-installed poetry version.
Note: `requests` has the same packaging gap (used by the legacy agent
path, declared only in the optional `agent` group); leaving that for a
follow-up since the immediate complaint is websockets.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Most users register against the production hub, so requiring --hub on
every invocation was friction. Default the flag to https://riahub.ai
via a module-level DEFAULT_HUB_URL constant; explicit --hub still wins,
so dev and self-hosted setups (e.g. http://whitehorse:3005) keep working.
The legacy `ria-agent run` path keeps its own --hub handling unchanged
— it has a config-file fallback and existing operators rely on it.
Bumps version to 0.1.8.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Covers the User-Agent / Cloudflare fix, request timeout, structured
error reasons, human-readable default agent names, and updated CLI
help text for the per-user `ria_reg_*` registration key flow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Restore agent_id in success output. Pre-PR the user saw the hub's
canonical identifier; the merge had reduced this to just `(name)`,
which made it impossible to correlate the registered agent with
anything on the hub side without inspecting the config file.
- Add a 15s timeout to the register POST. urllib's default is none,
so a stuck hub would block the CLI indefinitely.
- Read User-Agent version from package metadata instead of hardcoding
"0.1", so it tracks releases. Also corrected the URL to the canonical
Source URL listed in pyproject.toml (was pointing at a likely-404
github.com path).
- Add two tests guarding the User-Agent. The whole point of the Cloudflare
fix was to set a non-default UA; previously no test asserted this, so
a refactor could silently reintroduce the 403 code 1010 bug.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Python's `urllib.request.Request` defaults to `Python-urllib/<ver>`,
which Cloudflare's Browser Integrity Check on `riahub.ai` blacklists
(HTTP 403 with edge error code 1010). The register POST never reached
the hub. Any non-default UA passes the check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add example output for every section 4 command (discover, init, capture,
view, annotate, convert, split, combine, generate, transform, transmit)
- Add examples for all annotate subcommands (list, add, remove, clear,
energy, threshold, cusum, separate)
- Clarify separate workflow: requires existing annotations as input;
show threshold → separate two-step example with before/after images
- Regenerate all viewer images using updated viewer (post e5a3d32 styling)
- Add images for energy, threshold, cusum, and separate annotation views,
AWGN transform output, and qam64_35 simple/full views
- Reorder annotate subcommands: manual first, auto-detection second
- Simplify section 3 workflow to one command per step with links to section 4
- Remove all italic inline option-group labels and redundant sub-headers
- Rewrite generate subcommand options as a table; consolidate capture and
transmit option lists
- Replace bare metadata["sample_rate"] access with .get() + clear
ValueError in threshold_qualifier, energy_detector, cusum_annotator,
parallel_signal_separator, and signal_isolation
- Add --sample-rate option to energy, threshold, cusum, and separate
CLI commands with a pre-flight error if sample rate is still absent
- Normalize namespaced metadata keys (e.g. BlockGenerator:Foo:sample_rate)
to standard keys on legacy .npy load
- Cap threshold_qualifier smoothing window at 1% of signal length to
prevent over-smoothing short recordings into a flat envelope
- Warn when threshold or energy detector returns 0 annotations due to
constant-envelope signal; point to cusum as the right tool
- Enforce --overwrite before any work begins; error fires before load
and detection, not after
- Fix qualify_slice off-by-one that silently dropped the last slice
- Surface split failures in parallel_signal_separator via warnings.warn
instead of swallowing them silently
- Add threshold annotation example image to getting_started docs
- Align view_simple and view_full on background colour (#161616), title
size (25pt), subtitle size (15pt), base font/tick/label sizes, grid
style (alpha=0.2), and legend fontsize (10pt)
- Spectrogram placed above IQ plot in view_simple; subplot renamed from
"Time Series" to "IQ Sample Plot"
- Frequency and spectrogram Y-axes formatted in MHz across both viewers
- Added xlabel/ylabel, subtle grids, and IQ legend to view_full subplots
- Fixed spectrogram right-side clipping in view_simple by syncing xlim
from specgram output rather than total signal duration
- Updated getting_started.rst to reference both simple and full viewer
screenshots; replaced doc images with latest renders
Map the hub's structured `{detail: {reason}}` responses (invalid_key,
expired, revoked, already_consumed) and 429 rate-limits to actionable
CLI messages so users know to mint a fresh key from Settings → RIA
Agents instead of seeing a raw HTTPError.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Consolidate installation steps into installation.rst (pip upgrade,
ria --help verification, entrypoints note, editable install note,
SDR driver table); replace getting_started §1 body with a link
- Reformat command and subcommand lists as tables with purpose
descriptions and internal ref links for navigation
- Remove redundant §6 tips and §9 cheat sheet; trim duplicate
descriptions in generate subcommand sections
- Fix inline code comments to sit beside the command they describe
- Add custom CSS for light body text, white headings, and table
header colour to suit the dark background theme