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:

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_power and carrier_on transmit 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

strands_radio-0.1.0.tar.gz (23.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

strands_radio-0.1.0-py3-none-any.whl (24.1 kB view details)

Uploaded Python 3

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

Hashes for strands_radio-0.1.0.tar.gz
Algorithm Hash digest
SHA256 67faa464e3020aaadb218a1435596b4ec3fbc07081eac555a6a5e41a4818971d
MD5 0ebb2781a02ede00eb4f2242c4629e66
BLAKE2b-256 9f2b20bcf3b646dba867db3af4e4acb250b8de33f1e7b2e0070908abe678d92b

See more details on using hashes here.

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

Hashes for strands_radio-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 24065664fd4d385a4c980abedbcef0e3ae6e331e3c99ddbac0cfdded2891a987
MD5 ed08255d1e36a88f409cfac78ee96b77
BLAKE2b-256 2eb5757e7d223546a54058905305ff94acd0e4f46118e5cd0c2f80911ce51bda

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page