Hardware setup — cache-key teleport / Skymesh / Mars channel
The bench guide for the radios behind the teleport work. Read this once before you wire anything; it is the physical-layer companion to the three demo docs:
- TELEPORT.md — the overview and index for the whole cache-key teleport
arc (what is real vs. modeled, the
66 XOR 7 = 69admission, the proof table). - SKYMESH_DEMO.md — the two-air-gapped-receivers runbook (2 NESDRs, no TX, no link between them). Antenna and witness guidance there assumes the wiring here.
- MARS_DELAY_TOLERANT_TELEPORT.md — the Pluto TX into NESDR RX loopback that puts the framed key over the air and survives Mars-grade Eb/N0.
- R1_RESILIENT_MESH.md — the
/chainmesh the teleport amortizes; see the R1 note at the end of this file for the radio-on-R1 status.
Status convention throughout: PROVEN (done on this bench, verified this session),
BUILT (code exists, runs), OPEN (honest unknown, not yet qualified). The numbers
below were verified on this host; where a value depends on your venue or your exact silicon,
the doc says "verify with <command>" rather than inventing one.
The honest core, same as the demo docs: nothing here breaks the speed of light. The radios recover a small key over RF; a validated key selects a frozen cached answer that was computed once. The RF reception and the admission are real; interplanetary distance is modeled.
1. Bill of materials
| Qty | Item | Role | Notes |
|---|---|---|---|
| 2 | Nooelec NESDR SMArt v5 RTL-SDR dongle | Receivers (RX only) | Realtek RTL2832U + Rafael Micro R820T tuner. Receive-only, roughly 24 MHz–1.7 GHz, 0.5 ppm TCXO. Device 0 = SN 94092559, device 1 = SN 78922015. |
| 1 | ADALM-Pluto (Analog Devices) | Transmitter (the only TX) | USB VID 0x0456 PID 0xb673, libiio URI usb:0.5.5. AD9361 front end, tunes up to 6 GHz. Min sample rate ~2.083 MS/s. |
| 1 | Good antenna | Equalize the weak station | The telescoping / higher-gain whip. Goes on whichever station hears the witness worst (see section 6). |
| 1–2 | Stock whip antennas | Strong station + Pluto | The NESDR kit whips and the Pluto SMA whips. |
| 1+ | SMA / coax as needed | Pluto TX -> NESDR RX bench loop | For the Mars loopback you can couple TX to RX with a short coax + attenuator instead of over the air; keeps TX gain trivially low. |
| 2 | Rabbit R1 (Android) | Mesh nodes | Run /chain distributed inference. RTL-SDR over USB-OTG on the R1 is OPEN — see section 11. |
The RTL-SDRs cannot transmit. Every transmit leg in this work goes through the Pluto. This is on-theme: deep-space operation is receive-dominant — you listen and serve locally.
2. USB enumeration and device-index discipline
The two NESDRs are independent USB devices. They are addressed by device index (0,
1), and exactly one process can hold a given index at a time. The two dongles do not
contend with each other — separate USB endpoints — so two captures can run at once, one per
index. The Pluto is a separate device entirely and never collides with the NESDR indices.
Enumerate the receivers:
rtl_test -tExpect a line reading Found 2 device(s): followed by both dongles. The index-to-serial
mapping on this host:
- index
0-> SN94092559 - index
1-> SN78922015
If the indices come up swapped on your machine (USB enumeration order is not guaranteed),
do not fight it — read the serials off rtl_test -t and treat the serial as ground
truth, mapping it to whichever index it landed on. The serial is stamped; the index is just
the slot it plugged into.
Per-device capture always passes the index explicitly:
rtl_sdr -d 0 -f <hz> -s <rate> -n <samples> /tmp/cap0.u8 # SN 94092559
rtl_sdr -d 1 -f <hz> -s <rate> -n <samples> /tmp/cap1.u8 # SN 78922015Discipline rules:
- Never start a second process against an index already in use — you get a
usb_claimerror. Kill the priorrtl_sdr/rtl_433/rtl_powerfirst (pkill -f rtl_sdr). - The Skymesh demo deliberately runs both indices simultaneously (
-d 0and-d 1). That is fine — they are different devices. - On the R1 deployment each dongle is alone on its host, so each is index
0locally.
Enumerate the transmitter (after the libiio build in section 4):
export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/build
iio_info -u usb:0.5.5 | head -40Expect the ad9361-phy and cf-ad9361-* devices to list. If iio_info cannot find the
context, see the troubleshooting table (section 10).
3. Driver and tool install (Homebrew)
The RTL-SDR tools are in homebrew-core. On this host they are the librtlsdr and rtl_433
formulae, symlinked into /opt/homebrew/bin:
brew install librtlsdr rtl_433This provides:
rtl_test— enumerate + tuner sanity (fromlibrtlsdr)rtl_sdr— raw IQ capture to a.u8file (fromlibrtlsdr)rtl_power— power-vs-frequency sweep, for finding a strong carrier (fromlibrtlsdr)rtl_433— ISM frame decoder, for the 433.92 MHz witness path (separate formula)
The shared library lives at /opt/homebrew/lib/librtlsdr.dylib. Verify what is present:
which rtl_test rtl_sdr rtl_power rtl_433
ls -l /opt/homebrew/lib/librtlsdr.dylibOn this host these are librtlsdr 2.0.2 and rtl_433 25.12; verify your versions with
brew list --versions librtlsdr rtl_433.
4. libiio (Pluto TX) — built from source
libiio is not in homebrew-core. The Pluto TX path needs libiio and its iio_* tools,
and they are built from source here. The build tree is at /tmp/libiio_build, which is
ephemeral — macOS wipes /tmp across reboots. If the tools stop working, rebuild before
debugging anything else (this is the first troubleshooting entry too).
Build it
# pick a workspace; /tmp/libiio_build is where this host put it
git clone --depth 1 --branch v0.25 https://github.com/analogdevicesinc/libiio /tmp/libiio_build
cmake -S /tmp/libiio_build -B /tmp/libiio_build/build \
-DWITH_USB_BACKEND=ON -DWITH_TESTS=ON -DHAVE_DNS_SD=OFF
cmake --build /tmp/libiio_build/buildWITH_USB_BACKEND=ON is mandatory — the USB backend is how we reach the Pluto on macOS
(see below). WITH_TESTS=ON builds the iio_info, iio_attr, iio_writedev command-line
tools we use. HAVE_DNS_SD=OFF avoids the mDNS dependency we do not need on the bench.
Make the tools find the framework
The built tools load iio.framework out of the build tree, so every shell that runs an
iio_* command must export:
export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/buildThe built binaries are under /tmp/libiio_build/build/tests/ (e.g. iio_info, iio_attr,
iio_writedev). Either call them by full path or add that dir to PATH.
Why USB, not IP
The Pluto normally presents a USB-ethernet gadget at ip:192.168.2.1. macOS does not
bring that gadget up. So we use the libiio USB backend and address the Pluto by its
USB URI, usb:0.5.5, everywhere — never ip:192.168.2.1 on this host. Confirm the URI
with iio_info -s (lists discovered contexts) if usb:0.5.5 ever changes.
5. Per-role setup
Two physical layouts. Pick by which demo you are running.
Role A — Skymesh (2 NESDRs, no TX, no link)
The full runbook is SKYMESH_DEMO.md; this is just the wiring.
- Two NESDRs on one host (indices
0and1), or one NESDR per R1 (each index0). - No transmitter anywhere. The Pluto is not in this loop.
- Both dongles tune the same ambient carrier (section 7) and each independently derives the witness; nothing is exchanged between them.
- Antennas: good antenna on the weaker station (section 6).
- Receiving is unlicensed — no TX license needed. This is a feature of the RX-only demo, not a workaround (section 9).
Role B — Mars loopback (Pluto TX -> NESDR RX)
The full runbook is MARS_DELAY_TOLERANT_TELEPORT.md; this is the wiring and bring-up.
- Pluto transmits (section 8); one NESDR receives (
-d 0is fine — only one RX needed). - Modem sample rate is 2.4 MS/s — above the Pluto floor (~2.083 MS/s) and well inside the NESDR range, so the same rate is valid on both ends. Do not drop below the Pluto floor.
- Couple TX to RX by short coax + attenuator for a clean benchtop loop, or over the air at low TX gain. Keep TX gain low regardless (section 8).
- Frequency: 433.92 MHz ISM for the framed-key and protocol69-tone work (section 7).
6. Antenna placement
The rule that makes witnesses lock: put the good antenna on the weaker-signal station.
- In Skymesh, both receivers must reduce the live carrier to the same witness value. That only happens reliably when both have a usable copy. Equalize the link budget by giving the marginal station the higher-gain antenna; let the strong station run a stock whip.
- In the Mars loopback, the receiving NESDR gets the good antenna if you are going over the air; for a coax-coupled bench loop the antenna is irrelevant and you can attenuate instead.
- Separate the two Skymesh stations far enough that the air gap is obviously real to an audience, but keep both inside copy range of the chosen carrier.
- Keep antennas clear of the Pluto and of switching power supplies; both are local noise.
7. Frequency selection and scanning for a strong carrier
The frequencies used in this work:
| Freq | Use | Notes |
|---|---|---|
| 433.92 MHz | ISM. The protocol69 tone demo and the framed rf-cache-key (Mars loopback). | In this venue, 433 ISM is quiet — rtl_433 decoded nothing in a 12 s listen. Good for Pluto loopback (you own the signal); poor as an ambient witness here unless you have an always-on ISM sensor. |
| 102.49 MHz | The strongest local FM carrier on this bench — the Skymesh witness anchor. | Find your own. This value is venue-specific. Scan before you commit (below). |
| 137 MHz | NOAA weather-satellite downlink — the dramatic "from orbit" Skymesh witness variant. | Pass-timed only; schedule the demo to a pass. |
Anchor on a strong carrier, not an empty slot
The witness must come from a signal both receivers actually copy. An empty FM slot is just noise and the witnesses will not agree. Anchor on the strongest carrier you can find. Find it with a sweep:
# strongest FM carrier in the broadcast band (one-shot sweep, 50 kHz bins)
rtl_power -f 88M:108M:50k -1 /tmp/fm-scan.csvRead the CSV and pick the bin with the highest power; that center frequency is your witness
anchor. On this bench that came out at 102.49 MHz — yours will differ, so verify with the
rtl_power sweep above rather than copying the number. For ISM, confirm something is
actually transmitting before relying on it:
# listen for ISM frames; if nothing decodes, do not use 433 as the ambient witness here
rtl_433 -f 433920000 -T 128. Pluto TX bring-up sequence
All commands assume the libiio framework export is set:
export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/buildThe control surface is ad9361-phy (LO + gain) plus the on-chip DDS core
cf-ad9361-dds-core-lpc (tone generator). Tools are under /tmp/libiio_build/build/tests/.
Step 1 — confirm the context
iio_info -u usb:0.5.5 | head -40Must list ad9361-phy and the cf-ad9361-dds-core-lpc device. If not, see section 10.
Optional Skymesh WiFi endpoint
skymesh-wifi-pluto is the homespun Rust endpoint for making the Pluto bridge visible on
the local WiFi segment while keeping RF transmit gated:
cd open-source/gnosis/distributed-inference
cargo run --bin skymesh-wifi-pluto -- \
--http-bind 0.0.0.0:8732 \
--public-host <this-host-ip>It broadcasts gnosis.skymesh.wifi-pluto.advertisement.v1 JSON to
239.69.0.69:8732 and serves:
GET /healthzGET /advertisementGET /pluto/preflightPOST /pluto/transmit
Real Pluto transmit is fail-closed. Without --enable-tx, /pluto/transmit only returns
the exact iio_attr / iio_writedev commands it would run. Add --enable-tx only after
/pluto/preflight is ok:true and the IQ file is the intended signed-int16 interleaved
Pluto TX buffer.
Visible SSID / BSSID AP mode
The multicast endpoint above is service discovery inside an existing WiFi network. A
phone's WiFi settings will only show skymesh if a WiFi adapter is running real AP mode.
On Linux, skymesh-wifi-pluto can generate the hostapd config:
cargo run --bin skymesh-wifi-pluto -- \
--ap-mode \
--ap-interface wlan0 \
--ap-ssid skymesh \
--ap-channel 6 \
--ap-config-out /tmp/skymesh-hostapd.conf \
--ap-config-onlyThe operator script wraps the same checks and handles the common Realtek
0bda:1a2b driver-disk mode when usb_modeswitch is installed:
sudo scripts/skymesh-linux-ap.sh --iface wlan0 --public-host 192.168.69.1
sudo scripts/skymesh-linux-ap.sh --iface wlan0 --public-host 192.168.69.1 --runOn a Linux host with hostapd installed and a WiFi adapter whose iw list includes
AP, it can also spawn hostapd:
sudo target/debug/skymesh-wifi-pluto \
--ap-mode \
--ap-interface wlan0 \
--ap-ssid skymesh \
--ap-channel 6 \
--ap-run-hostapd \
--http-bind 0.0.0.0:8732 \
--public-host 192.168.69.1Realtek adapters that enumerate as 0bda:1a2b / DISK are still in driver-disk mode,
not WiFi mode. On Linux, switch them first:
sudo usb_modeswitch -v 0bda -p 1a2b \
-M 55534243123456780000000000000061b000000ff000000000000000000000000000Then re-check:
lsusb
iw dev
iw list | grep -A20 'Supported interface modes'macOS does not expose this low-level AP/hostapd path for arbitrary USB WiFi dongles. On macOS, use Internet Sharing or an external router for the visible SSID, then run the Skymesh endpoint on that network.
Step 2 — set the TX LO and gain
# TX local oscillator -> 433.92 MHz
iio_attr -u usb:0.5.5 -c ad9361-phy altvoltage1 frequency 433920000
# TX hardware gain -> -20 dB for benchtop (range -89.75 .. 0 dB; lower to sweep down)
iio_attr -u usb:0.5.5 -c ad9361-phy voltage0 hardwaregain -20altvoltage1 frequency is the TX LO; voltage0 hardwaregain is the TX gain. Start at
-20 dB for benchtop loopback and lower it (more negative) to sweep the link margin down.
Never run higher than you need.
Step 3a — emit a single tone (protocol69 symbol demo)
The on-chip DDS puts a clean tone out without streaming a file. Drive both the I and Q
single-tone generators (altvoltage0 = TX1_I_F1, altvoltage2 = TX1_Q_F1) and set their
scale to emit:
# tone ON (scale 0.5 emits)
iio_attr -u usb:0.5.5 -c cf-ad9361-dds-core-lpc altvoltage0 scale 0.5
iio_attr -u usb:0.5.5 -c cf-ad9361-dds-core-lpc altvoltage2 scale 0.5
# tone OFF (scale 0 stops)
iio_attr -u usb:0.5.5 -c cf-ad9361-dds-core-lpc altvoltage0 scale 0
iio_attr -u usb:0.5.5 -c cf-ad9361-dds-core-lpc altvoltage2 scale 0This is the path that put symbol 66 out as a tone in the protocol69 OTA proof.
Step 3b — stream bytes (the framed cache key)
For an actual byte payload, stop the DDS tone first (set both scales to 0), then stream
a premodulated int16 interleaved-IQ file into the same DDS core channels. iio_writedev
with -c is cyclic (it loops the buffer), which is what you want for a repeating frame:
# premodulate the key into interleaved int16 IQ first (the modem builds this file;
# see rtlsdr-mock-sim pluto-tx.ts / run.mts mars-tx), then:
iio_writedev -u usb:0.5.5 -b <samples> -c \
cf-ad9361-dds-core-lpc voltage0 voltage1 < key-iq.int16-b <samples> is the buffer size in samples; voltage0 voltage1 are the I and Q DAC
channels. The modem-side helper that writes key-iq.int16 and orchestrates this is in
../../rtlsdr-mock-sim/ (pluto-tx.ts, run.mts mars-tx); see TELEPORT.md section "The
Mars-channel modem."
Step 4 — capture on the NESDR and decode
# 2.4 MS/s matches the modem and is valid on both Pluto and NESDR
rtl_sdr -d 0 -f 433920000 -s 2400000 -n 12000000 /tmp/mars-capture.u8
# then run the mars-rx decoder over the capture (see MARS doc / rtlsdr-mock-sim)Always set TX gain back down / tone off when done.
9. First-light smoke test
Run this end-to-end before any demo. It proves the chain without needing the full modem.
(a) Receivers enumerate
rtl_test -tPass = Found 2 device(s): with both serials (94092559, 78922015). This also runs a quick
tuner gain sanity per device.
(b) Capture raw IQ
# 10 s at the FM band, device 0 (substitute your witness frequency from section 7)
rtl_sdr -d 0 -f 102490000 -s 2048000 -n 20480000 /tmp/firstlight.u8
ls -l /tmp/firstlight.u8 # ~20 MB; nonzero size = capture workedThe .u8 is unsigned 8-bit interleaved IQ. A nonzero file of the expected size means the
USB path and tuner are alive.
(c) FFT / spectrum sanity
# is there real signal where you tuned, or just noise floor?
rtl_power -f 88M:108M:50k -1 /tmp/fm-scan.csvEyeball the CSV: a strong carrier shows a clear peak well above the noise floor. If everything is flat noise, your antenna/feed is the problem before any demo can lock a witness. (The Skymesh witnesses will not agree on noise.)
(d) TX present (Mars role only)
export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/build
iio_info -u usb:0.5.5 | head -40 # ad9361-phy + cf-ad9361-dds-core-lpc must listIf all four pass, the bench is ready. Hand off to the relevant demo runbook.
10. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
usb_claim / "device busy" on rtl_sdr -d N |
Another process already holds index N (rtl_sdr/rtl_433/rtl_power) |
Only one process per index. pkill -f rtl_sdr (or the offending tool) and retry. The two NESDRs do not contend with each other — different indices are fine simultaneously. |
| Indices swapped vs. expected serial | USB enumeration order is not guaranteed | Read serials from rtl_test -t and treat the serial as truth; map it to whatever index it got. |
iio_info / iio_attr / iio_writedev: "image not found" / framework load error |
DYLD_FALLBACK_FRAMEWORK_PATH not exported in this shell |
export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/build and rerun. Every new shell needs it. |
iio_* tools gone / /tmp/libiio_build missing |
/tmp is ephemeral; wiped on reboot |
Rebuild from source — section 4 (git clone v0.25, cmake with -DWITH_USB_BACKEND=ON -DWITH_TESTS=ON -DHAVE_DNS_SD=OFF, build). Do this first; do not debug other Pluto symptoms until the build is back. |
Pluto not found at usb:0.5.5 |
macOS does not raise the USB-ethernet gadget; or URI changed | Use the USB backend (never ip:192.168.2.1 here). Re-discover with iio_info -s and use the listed usb:x.x.x. Replug the Pluto if it is absent entirely. |
| Pluto logs "PLL not locked" on bring-up | Cosmetic transient during LO settling | Cosmetic — ignore if the LO frequency reads back correctly via iio_attr ... altvoltage1 frequency and the tone/stream is recovered downstream. Not a stop condition. |
| Skymesh witnesses do not agree (A != B) | One station's copy too weak; unequal link budget | Move the good antenna to the weaker station (section 6); re-aim; widen the capture window (-n). |
| Witnesses agree only on noise / never lock | Anchored on an empty slot, not a real carrier | Re-scan with rtl_power -f 88M:108M:50k -1 and anchor on the strongest carrier (section 7). An empty FM channel is just noise. |
| 433 ISM witness never decodes | This venue's 433 ISM is quiet (no always-on sender) | Confirm with rtl_433 -f 433920000 -T 12; if nothing, switch the witness to a strong FM carrier (102.49 MHz here) or NOAA. |
| Mars-channel decode fails / garbage frame | Sample-rate mismatch between TX and RX | Use 2.4 MS/s on both ends. It is above the Pluto floor (~2.083 MS/s) and inside the NESDR range; mismatched rates corrupt the demod. Never set Pluto below its floor. |
| Pluto TX too strong / desensing the RX | Default gain too high | Set voltage0 hardwaregain to -20 dB and lower (more negative) as needed; for a coax loop, add an external attenuator. |
11. R1 mesh note
The Rabbit R1 Android devices run the mesh nodes (/chain distributed inference); see
R1_RESILIENT_MESH.md for the mesh itself.
Honest status of radio-on-R1:
- PROVEN: the mesh node, the protocol69 admission gate, and the
geodesicLength:0replay all run on the R1 today — the compute half of a station works on-device. - OPEN: RTL-SDR over USB-OTG on the R1 is not yet qualified. The R1 has USB-A
OTG, but kernel
rtl2832/ USB-bulk support under the R1's kernel + SELinux policy is untested. Untildmesg | grep rtl//dev/bus/usbconfirm the dongle enumerates, do RF capture on a host and feed the IQ/witness to the R1 station process (the bridge-host fallback in SKYMESH_DEMO.md section 6).
Do not present on-R1 RF capture as working until it enumerates; the bridge-host fallback is the runnable configuration today.
12. Safety and legal
- Receiving is unlicensed and safe. The NESDRs are receive-only; listening to ambient FM / ISM / NOAA broadcasts you are already bathed in needs no authorization. This is why the Skymesh demo (RX-only) carries no TX license burden — a feature of the design, not a workaround, and the same receive-dominant topology as real deep-space operation.
- Transmitting (Pluto) is operator-gated. Only the Pluto can transmit. 433.92 MHz is an ISM band; keep TX gain low (start at -20 dB) for benchtop loopback, prefer a coax-coupled loop with an attenuator over radiating, and turn the tone/stream off when done. Do not transmit on bands or at powers you are not authorized for.