forgo.cloud
Sign in
Repo workspace

forkjoin-ai/gnosis

Hardware setup — cache-key teleport / Skymesh / Mars channel

distributed-inference/docs/HARDWARE_SETUP.md
forkjoin-ai/gnosis

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 = 69 admission, 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 /chain mesh 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 -t

Expect a line reading Found 2 device(s): followed by both dongles. The index-to-serial mapping on this host:

  • index 0 -> SN 94092559
  • index 1 -> SN 78922015

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 78922015

Discipline rules:

  • Never start a second process against an index already in use — you get a usb_claim error. Kill the prior rtl_sdr / rtl_433 / rtl_power first (pkill -f rtl_sdr).
  • The Skymesh demo deliberately runs both indices simultaneously (-d 0 and -d 1). That is fine — they are different devices.
  • On the R1 deployment each dongle is alone on its host, so each is index 0 locally.

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 -40

Expect 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_433

This provides:

  • rtl_test — enumerate + tuner sanity (from librtlsdr)
  • rtl_sdr — raw IQ capture to a .u8 file (from librtlsdr)
  • rtl_power — power-vs-frequency sweep, for finding a strong carrier (from librtlsdr)
  • 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.dylib

On 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/build

WITH_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/build

The 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.

The full runbook is SKYMESH_DEMO.md; this is just the wiring.

  • Two NESDRs on one host (indices 0 and 1), or one NESDR per R1 (each index 0).
  • 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 0 is 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.csv

Read 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 12

8. Pluto TX bring-up sequence

All commands assume the libiio framework export is set:

export DYLD_FALLBACK_FRAMEWORK_PATH=/tmp/libiio_build/build

The 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 -40

Must 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 /healthz
  • GET /advertisement
  • GET /pluto/preflight
  • POST /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-only

The 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 --run

On 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.1

Realtek 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 55534243123456780000000000000061b000000ff000000000000000000000000000

Then 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 -20

altvoltage1 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 0

This 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 -t

Pass = 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 worked

The .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.csv

Eyeball 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 list

If 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:0 replay 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. Until dmesg | grep rtl / /dev/bus/usb confirm 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.


  • 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.