📡 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 thatsubscribe/publishto the same topic string automatically rendezvous on the same RF pipe — no config. - publish: TX framed packets on the topic's pipe.
delivered=Truemeans 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 (
pollblocking,drainnon-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:
1. Crazyflie as PRX (real bidirectional) — recommended
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_powerandcarrier_ontransmit real RF.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file strands_radio-0.1.0.tar.gz.
File metadata
- Download URL: strands_radio-0.1.0.tar.gz
- Upload date:
- Size: 23.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
67faa464e3020aaadb218a1435596b4ec3fbc07081eac555a6a5e41a4818971d
|
|
| MD5 |
0ebb2781a02ede00eb4f2242c4629e66
|
|
| BLAKE2b-256 |
9f2b20bcf3b646dba867db3af4e4acb250b8de33f1e7b2e0070908abe678d92b
|
File details
Details for the file strands_radio-0.1.0-py3-none-any.whl.
File metadata
- Download URL: strands_radio-0.1.0-py3-none-any.whl
- Upload date:
- Size: 24.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
24065664fd4d385a4c980abedbcef0e3ae6e331e3c99ddbac0cfdded2891a987
|
|
| MD5 |
ed08255d1e36a88f409cfac78ee96b77
|
|
| BLAKE2b-256 |
2eb5757e7d223546a54058905305ff94acd0e4f46118e5cd0c2f80911ce51bda
|