Skip to main content

📡 strands-radio

Turn a Crazyradio PA (Nordic nRF24 / Enhanced ShockBurst, 2.4 GHz) USB dongle into a general-purpose radio and a lightweight pub/sub message bus — exposed as a single Strands agent tool.

Built and tested on a Thor device with Crazyradio PA firmware 5.4, cflib 0.1.32, pyusb 1.3.1.

🔌 Use as an MCP server

Use strands-radio from Claude Code, Claude Desktop, Cursor, Kiro, or any MCP client — the strands_radio tool (spectrum sweeps, link quality, CRTP) becomes an MCP tool.

claude mcp add radio -- uvx strands-radio

Claude Desktop config:

{
  "mcpServers": {
    "radio": {
      "command": "uvx",
      "args": ["strands-radio"]
    }
  }
}

Options:

strands-radio --http --port 8000   # HTTP mode, multi-client

What it does

Three layers, one tool (strands_radio):

Layer Actions Purpose
RAW info, set_power, scan_spectrum, send_raw, carrier_on/off Direct RF control: channels 0–125, rates 250K/1M/2M, power −18…0 dBm, spectrum scan, continuous-carrier RF test
PUB/SUB publish, subscribe, unsubscribe, poll, drain, listen Send/receive messages over the air. Topic name → deterministic (channel, address) pipe
CRTP scan_drones, drone_info, drone_setpoint Talk to Crazyflie drones via cflib's stack
PRX prx_start/send/poll/stop, listen_start/poll/stop True receive-side: Crazyflie-as-PRX (appchannel) or reassembling dongle listener
SPECTRUM capabilities, probe_sweep, quality_sweep Honest 2.4GHz sensing: nRF24 device discovery + per-channel link quality (NO passive RSSI — see capabilities)

How pub/sub works over nRF24

The Crazyradio is a PTX (primary transmitter): it transmits and receives data back through the nRF24 auto-ACK payload. strands-radio uses this:

  • Topic → pipe: sha256(topic) deterministically yields a channel (2–99) and a 5-byte pipe address. Any two nodes that subscribe/publish to the same topic string automatically rendezvous on the same RF pipe — no config.
  • publish: TX framed packets on the topic's pipe. delivered=True means a peer ACKed. Auto-chunks payloads > 28 bytes.
  • subscribe: a background poller round-robins over subscribed topics, sending PINGs and draining any ACK-payload data peers piggyback. Messages land in a per-topic inbox (poll blocking, drain non-blocking).

Frame (≤32-byte nRF24 budget): [magic 0x5A][type][topic-hash][seq][payload…]

Install

pip install -e .        # needs cflib, pyusb (auto-installed)

Linux: add a udev rule so non-root can access the dongle:

# /etc/udev/rules.d/99-crazyradio.rules
SUBSYSTEM=="usb", ATTRS{idVendor}=="1915", ATTRS{idProduct}=="7777", MODE="0664", GROUP="plugdev"

sudo udevadm control --reload-rules && sudo udevadm trigger

Use as an agent tool

from strands import Agent
from strands_radio import strands_radio

agent = Agent(tools=[strands_radio])
agent("publish 'motors armed' to topic swarm/cmd")
agent("listen on topic telemetry for 5 seconds")
agent("scan for crazyflie drones")

Or with DevDuck: manage_tools(action='add', tools='/path/to/tools/radio_tool.py')

Direct calls

from strands_radio import strands_radio as R

R(action="info")
R(action="set_power", power="0")                       # max TX power
R(action="scan_spectrum", start=0, stop=125)           # find active channels
R(action="publish", topic="telemetry", message="v=3.7")
R(action="subscribe", topic="telemetry")
R(action="poll", topic="telemetry", timeout=2)
R(action="send_raw", channel=80, address="E7E7E7E7E7", hexdata="ff00aa")
R(action="carrier_on", channel=42)                     # RF test tone
R(action="scan_drones")
R(action="drone_info", uri="radio://0/80/2M/E7E7E7E7E7")

Two-node loopback test

Run python tests/loopback.py pub on one radio node and python tests/loopback.py sub on another (each with its own Crazyradio) — they'll exchange messages on topic strands.loopback with zero configuration.

PRX / Listener mode (true receive-side)

Hardware truth: stock Crazyradio firmware is PTX-only — there is no vendor command to make a bare dongle a primary receiver. So two bare dongles cannot hear each other directly. strands-radio provides two genuine listener paths:

A powered Crazyflie is a receiver. Its appchannel carries arbitrary app data both ways with true async delivery:

R(action="prx_start", uri="radio://0/80/2M/E7E7E7E7E7")  # open appchannel listener
R(action="prx_send",  uri="radio://0/80/2M/E7E7E7E7E7", message="hello drone")
R(action="prx_poll",  uri="radio://0/80/2M/E7E7E7E7E7", timeout=2)  # read RX
R(action="prx_stop",  uri="radio://0/80/2M/E7E7E7E7E7")

Requires firmware on the CF that uses the app-channel API (Bitcraze app_channel example / your own app layer).

2. Dongle-side reassembling listener

For dongle↔dongle (peer piggybacks data on nRF24 ACK payloads). Adds fragment reassembly (T_DATA_CONT… + final T_DATA) so multi-packet messages arrive whole, plus a dedicated streaming thread:

R(action="listen_start", topic="telemetry")           # background reassembler
R(action="listen_poll",  topic="telemetry", timeout=2) # complete messages only
R(action="listen_stop",  topic="telemetry")

Demos: python tests/prx_demo.py cf <uri> or python tests/prx_demo.py listen <topic>.

Spectrum / sensing — what's GENUINELY possible

A bare Crazyradio (nRF24L01+, fw 5.4) is not an SDR. It has no passive RSSI readout and cannot demodulate WiFi/BLE — so it cannot draw a real spectrum waterfall. Run R(action="capabilities") for the honest boundary.

What it CAN do (all implemented, all real):

Action What it measures Detects
probe_sweep TX a probe on every channel×rate, record ACK + RPD bit (~370 probes/s) nRF24/ESB devices that auto-ack on an address: Crazyflies, other Crazyradios in PRX, nRF24 HID dongles
quality_sweep ACK ratio + mean retransmits per channel to a KNOWN peer Channel congestion/interference — the same signal Crazyflie firmware uses to pick a clean channel
carrier_on/off Emit a continuous carrier TX/RF test, jamming-style tone (use responsibly)

The RPD (powerDet) bit is the nRF24 "received power > −64 dBm" flag — 1 bit, and only valid when an ACK comes back. quality_sweep renders an ASCII heatmap:

ch 80 2480MHz |████████████████████████████████████████| 100% retry~0.1
ch 60 2460MHz |████████████████████████················|  60% retry~2.1
ch 11 2411MHz |████████································|  20% retry~4.5

So — what can we genuinely achieve with this radio?

  • Mesh messaging between Thor nodes / drones (pub/sub, this tool)
  • Device discovery — find any nRF24/ESB responder in range across 125 channels
  • Automatic channel selection — pick the cleanest 2.4 GHz channel via quality_sweep
  • Full Crazyflie control — telemetry, params, commander, app-channel (PRX)
  • Link-quality monitoring for a live drone link (retry/ACK trends)
  • RF test tones via continuous carrier

For a true passive spectrum analyzer / packet sniffer you'd pair this with an RTL-SDR, HackRF, or an nRF52840 in promiscuous mode — documented in capabilities() rather than faked here.

Notes / limits

  • One dongle = one owner at a time. The CRTP layer auto-releases the raw bus before cflib grabs the USB device (avoids Resource busy).
  • True async RX is bounded by the PTX/ACK-payload model; for high-throughput bidirectional links, pair with a second dongle or a Crazyflie acting as PRX.
  • Respect local RF regulations; set_power and carrier_on transmit real RF.

License

MIT

Metadata

Release files for strands-radio 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for strands-radio 0.1.0
File Size Uploaded
strands_radio-0.1.0.tar.gz 23.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for strands-radio 0.1.0
File Interpreter ABI Platform
strands_radio-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 47.9 kB

Release files / strands_radio-0.1.0.tar.gz

Download URL strands_radio-0.1.0.tar.gz
Size 23.8 kB
Tags Source
SHA-256 checksum
How to use checksums
67faa464e3020aaadb218a1435596b4ec3fbc07081eac555a6a5e41a4818971d
BLAKE2b-256 checksum
How to use checksums
9f2b20bcf3b646dba867db3af4e4acb250b8de33f1e7b2e0070908abe678d92b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.11

Release files / strands_radio-0.1.0-py3-none-any.whl

Download URL strands_radio-0.1.0-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
24065664fd4d385a4c980abedbcef0e3ae6e331e3c99ddbac0cfdded2891a987
BLAKE2b-256 checksum
How to use checksums
2eb5757e7d223546a54058905305ff94acd0e4f46118e5cd0c2f80911ce51bda
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.11

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page