A frequency-hopping OFDM radio link for drones and robots, on off-the-shelf SDR boards.
NyxHop turns a pair of software-defined-radio boards, an ADRV9364-Z7020 or an ANTSDR E200, into a private frequency-hopping data link, and gives you the software around it. The link moves blocks of bytes; what you put in them is your business: video, telemetry, files, your own protocol, all at once if you like. Anywhere between 70 MHz and 6 GHz that your hardware and your local rules allow.
On the bench it carries an H.264 camera stream at 30 fps, 28 ms from camera to screen (61 ms with the two-layer simulcast switched on, which keeps a picture through fades).
The ground app holding 5795 MHz out of its video table, with the control channel on its own table down at 2.4 GHz.
If you know the OcuSync-style digital links on commercial drones, this is the same idea: hopping OFDM, a separate control channel, adaptive modulation, retransmission of what got lost. Built from boards anyone can buy, with the source of the apps and the protocol here to read and build on.
The source code in this repository needs no licence: it is public domain
(Unlicense), do what you like with it. The radio itself is a different matter: the
board images in deploy/ (FPGA design, radio daemon) are binaries under an EULA,
and the boards need a licence file to run - the Licensing table at the bottom
says exactly which is which.
flowchart LR
CAM["Camera"] --> TX["nyx-tx<br/>PC or small SBC"]
FC["Flight controller"] <-.-> TX
TX -- "Ethernet" --> BA["SDR board"]
BA == "radio link" ==> BB["SDR board"]
BB -- "Ethernet" --> RX["nyx-rx<br/>PC or Android"]
RX <-.-> GCS["Ground control station"]
IPC["IP camera"] -. "Ethernet, no computer" .-> BA
Two boards, one at each end, each on Ethernet with the computer that runs its app. Video and data go one way over the radio link, control and telemetry the other. Both boards run the same image; which end a board plays is a flag when you flash it. The computers never touch the radio: they exchange bytes with a board over Ethernet, and everything about the air interface lives on the board. The aircraft end does not need a computer at all: with an IP camera on its Ethernet port the board sends the camera by itself (below).
Four steps: an OS on each board, NyxHop and an address on each board, the apps, a licence. Budget an hour the first time.
You need
- Two boards, ADRV9364-Z7020 or ANTSDR E200 in any mix, with antennas for the band you will use. On the bench, 1–3 m apart is fine.
- A PC on the same Ethernet as the boards, with Python 3 (
pip install paramiko) for the flashing tool. A USB camera on the transmitting side. - This repository: it carries the prebuilt board images in
deploy/. The apps you build yourself, with Rust 1.85 or newer:cargo build --releaseat the repository root, once, for all of them.
ADRV9364-Z7020: write ADI's stock Kuiper Linux 2023_r2 image to a microSD card (balenaEtcher,
or ADI's Kuiper Imager with the adrv9364z7020_lvds configuration), put it in the board, power up
with Ethernet connected. It takes an address by DHCP; user root, password analog. Find the
address on your router. Want a fixed one: give it a DHCP reservation on your router, or set a
static address on the board as on any Linux box.
ANTSDR E200: nothing to install. The stock firmware in its flash stays as it is; NyxHop boots
from a microSD card and the board is stock again the moment you take the card out. Put an empty
FAT32 card in and power up: a stock board comes up at 192.168.1.10. Another address is one
command on the board, once: ssh root@192.168.1.10, then fw_setenv ipaddr_eth 192.168.0.12
and reboot.
One command per board, from the repository root. --host is the board's address, --role is
which end it plays.
set BOARD_PW=analog # the board's ssh password (PowerShell: $env:BOARD_PW = "analog")
# ADRV9364 as the transmitting end (camera side)
python link/scripts/nyx_flash.py --host 192.168.0.10 --role tx
# E200 as the receiving end (ground side)
python link/scripts/nyx_flash.py --host 192.168.0.12 --role rx --board e200Any board can take either role. The tool copies the FPGA design, the radio daemon and its
configuration to the board (on the ADRV9364 over the Kuiper boot files, keeping the originals as
*.nyx-orig; on the E200 onto the card), reboots the board and checks that it came back. From
then on the board starts NyxHop by itself on every power-up. The addresses above are the ones
used throughout this page; yours are whatever your boards have.
Run the same command again to update a board; tables, pairing and licence are kept.
--verify-only only checks a running board. --scan lists every board it finds on the network,
with role and address, no password needed.
cargo build --release at the repository root puts them in target/release/. One app serves
either end:
nyxhopIt asks which end this computer is (Ground station: this computer shows the video;
Aircraft: this computer has the camera) and which board it is plugged into, puts the board
into the matching role (the board restarts, about ten seconds), and opens the ground or the
aircraft screen. It remembers the choice; nyxhop --mode rx --board 192.168.0.12 or
--mode tx --board 192.168.0.10 skips the question. So a pair of boards can change ends from
the apps alone: choose the other end on each computer, and both boards follow.
The settings drawer of every screen (the gear, or H) has a Board role switch too:
Aircraft (tx) / Ground (rx). It sends the same command; the board restarts in its new
role, and a nyxhop window follows by opening the other screen on its own. Change both boards
and the link comes back the other way round, tables, pairing and licence untouched.
The two screens are also programs of their own, for scripts and headless boxes:
nyx-rx --channel 192.168.0.12:7011 # ground: the receiving board's address
nyx-tx --channel 192.168.0.10:7010 # aircraft: the transmitting board's address
nyx-tx --channel 192.168.0.10:7010 --headless # no window, e.g. on an SBC next to the cameraThe aircraft screen starts on the USB camera; with none plugged in it sends a test pattern.
Source in its settings also takes an IP camera: choose IP camera (RTSP) and type the
camera's URL, rtsp://user:password@192.168.1.64:554/stream1 for instance (the address and
path are in the camera's manual; a 640x480 sub-stream is the right size). The camera's picture
is decoded and sent on like the webcam's, so the link's rate control applies to it.
Two settings underneath change how an IP camera is carried. Send the camera's H.264 as it is passes the camera's own pictures on untouched: nothing is decoded and re-encoded, which takes about 30 ms off the delay and leaves the picture exactly as the camera made it. Nothing on this side can then make the stream smaller, so Camera follows the link (ONVIF) does it at the source: over ONVIF, with the user and password of the RTSP URL, the app moves the camera's own bitrate limit up and down with the link and tells you what it asked for. Leave ONVIF address empty unless the camera answers on another address or port, and set Ceiling to hold it below a bitrate of your choosing (0 keeps the limit the camera already had). A camera that does not answer ONVIF simply keeps its own bitrate, and the reading says so.
The Android app is the same thing on a phone: it starts on the same choice, Ground station
or Aircraft, with the board's address, puts the board into the role and shows that end's
screen. As the ground end it shows the video; as the aircraft end it sends the phone's own
camera (the app asks for the camera permission once) or an IP camera (Source in the
settings, then the URL). The phone talks to the board over USB-C Ethernet or Wi-Fi. It
remembers the choice in Android/data/com.nyxhop.mobile/files/nyxhop.cfg, a plain text file
adb can edit (board 192.168.0.12, mode tx, source phone|rtsp, rtsp <url>).
The Android app is its own build: python apps/video/build_apk.py writes a signed APK to
apps/video/android/out/, and needs the Android SDK and NDK, cargo-ndk and the
aarch64-linux-android Rust target. You do not need it: the apps on a PC do the same job.
An IP camera needs no computer on the aircraft. nyx-ipcam is the transmitting app built for
the board's own ARM: it runs beside the radio, pulls the camera's RTSP stream over the board's
Ethernet port and puts the camera's H.264 on air as it is. Nothing is decoded or encoded on the
board, so the two ARM cores stay with the radio. What flies is a board, a camera and a cable.
python link/scripts/nyx_ipcam_install.py 192.168.0.10 # either board, once NyxHop runs on itThat writes the prebuilt program from deploy/ipcam/ onto the board and starts it on every
power-up (a systemd service on the ADRV9364, the SD card's start-up hook on the E200). Give the
camera an address in the board's subnet and plug it into the board, directly or through a
switch. Then tell the board where the camera is, from any PC on that network:
nyx-ipcam-ctl --board 192.168.0.10Type the camera's URL, press Apply, then Save on board: from then on the board finds its camera by itself after a power cycle, and the PC can go. The window also shows what the camera sends against what the air carries, holds the same ONVIF settings as the aircraft app (the board moves the camera's bitrate with the link), and carries the link, role, radio and licence sections of the other apps. No video passes through that PC; the picture is on the ground screen.
The program follows the board's role by itself, so it can sit on both boards of a pair: on the
receiving end it waits, and when a board becomes the aircraft it starts sending. If the camera
falls silent it lets go of the radio after five seconds, so a nyxhop Aircraft window on a PC
can take the board over without anything being switched off. Its console is TCP 7201 on the
board, the same commands as the headless app below plus save; a flight controller on the same
Ethernet sends MAVLink to the board's UDP 14557 and it goes up the link.
No camera at hand? nyx-fakecam is one on your PC: it serves your webcam (or a test pattern) as
an RTSP camera with an ONVIF service, at the bitrate and key-frame interval you give it.
nyx-fakecam --kbps 2000 --gop 60 --user admin --pass secret prints the URL to give the board.
To build the board program yourself instead of using the prebuilt one:
cargo build --release -p nyx-ipcam --target armv7-unknown-linux-gnueabihf (the Rust target and
an arm-linux-gnueabihf linker named in .cargo/config.toml); the installer takes your build
when it finds one.
Then, once in the life of a pair of boards, pair them: press Link aircraft in the ground app. A board that has never been paired accepts on its own, both pills turn linked, and the video is on the ground screen. The boards remember the pairing across power cycles.
Accept link in the transmitting app is for the other case: an aircraft that is already paired ignores a new key until someone at the aircraft presses it, which opens a 60-second window. That is what stops a stranger from taking over your aircraft. Unlink forgets the key on either end.
Everything else is in the apps' settings drawer (tap the video or press H):
- Video (aircraft): USB camera, IP camera (RTSP, a URL) or test pattern, resolution, fps, quality, codec, the simulcast base layer for reach through fades. An IP camera adds the pass-through and ONVIF settings above.
- Channel: your tables. Video MHz and Control MHz take any channels from 70 to 6000 MHz, comma separated; Apply sends them to the board, which passes them to the other end over the air. Auto holds the best channel and re-scans when it degrades; Off pins one channel. The tables are yours: check your local rules, fit antennas for the band.
- Radio: gains, antenna ports (the E200 has two per direction), AGC. Leave them alone unless you know why.
- Messages: short text between the ends. Connection: the board address.
- Readouts in the drawer header (or the
Okey) turns the figures over the video off, leaving the picture and the status pills.Hopens and closes the drawer.
The headless transmitting app takes the same settings on its console (TCP 7201 by default):
set source webcam, set source rtsp with set rtsp rtsp://..., set pass 1,
set camadapt 1, set onvif auto, set cammax 2000, set fps 20, set quality 60, stats; a nyx-tx.cfg next to the program holds them across starts.
Telemetry and data: the link also carries a two-way UDP pipe. Send datagrams into nyx-rx
UDP 14555 and they come out of nyx-tx UDP 14556 (commands, RC); send into nyx-tx UDP 14557
and they come out of nyx-rx UDP 14558 (telemetry). MAVLink is the usual payload: point your
ground control station at 14558 and your flight controller's MAVLink router at 14557. Details
and the SDK crates in docs/telemetry-sdk.md.
Every board starts with a 20-hour grace period, counted by the board itself, so you can do all of the above first. Each board needs its own licence, and the simplest way is to do each one in the app that is connected to it:
- Open Licence in the drawer and press Copy next to the board's DNA.
- Ask for a licence with it at nyxhop.com/licence.html: free for the first ten boards per email address, commercial use included (see docs/license.md). The file comes back on the page at once.
- Paste it into the box under the DNA and press Apply here. The pill turns licensed.
Do that in nyx-rx for the ground board and in nyx-tx for the aircraft board. Once video is
flowing, the ground app also shows the aircraft's DNA and a Send to aircraft button, which
saves the walk to the aircraft next time; it needs the video link, so it is not there while the
aircraft is still locked. A licence is bound to its board, works offline and never expires.
- The app cannot connect: can you ping the board? Is the PC on the same network? Forgot
the address:
python link/scripts/nyx_flash.py --scan. - Connected but no video: both pills must say linked; if not, press Link aircraft in the ground app again (and Accept link in the aircraft app if that board is already paired). A pill saying locked is a board past its grace period: licence it from the app connected to that board. Both ends must show the same tables: press Apply in Channel again. In the transmitting app, does the preview move? Try the test pattern to rule out the camera.
- Video stutters: SNR under 15 dB is antennas, distance or the wrong band; the pill shows scan while the receiver looks for a better channel.
- Looking deeper: the board console answers on TCP 7202 (
nc 192.168.0.12 7202, thenhop status,license,stats), the apps write logs next to the executable inlogs/, and the daemon log is/home/root/radio.out(ADRV9364) or/tmp/nyxhop/radio.out(E200).
| board | transceiver | status |
|---|---|---|
| ADRV9364-Z7020 | AD9364 | ready |
| ANTSDR E200 | AD9361 | ready |
| PlutoSDR | AD9363 | in progress |
Either board can be either end, and two of the same kind work. Ethernet between each board and its computer, 12 V supplies, antennas of your choice.
A pair on the bench: ADRV9364-Z7020 on the left, ANTSDR E200 on the right.
The boards transmit at the power their own front end gives, which is enough for a bench and not much more. For distance, add the RF yourself: an antenna with gain at both ends, a power amplifier on the transmitting side, a low-noise amplifier on the receiving side. That part is outside NyxHop, and the power you end up radiating is yours to keep within your local rules.
Another board, or one you would like to see on this list? Write to tacitechvn@gmail.com.
The link itself knows nothing about video. It carries blocks and tells you which ones arrived. Applications sit on top of it and each lives in its own folder:
| folder | what it does |
|---|---|
apps/video/ |
the apps: H.264 from a USB, IP or phone camera one way, a ground app, a transmitting app and an Android app for either end. They also carry the two-way UDP pipe (MAVLink or any datagrams) |
apps/nyxhop/ |
the one app for either end: asks Ground or Aircraft, sets the board's role, then hosts the screen |
apps/data/ |
mav_sim.py, which measures that pipe |
Write your own the same way: take nyx-proto and nyx-link from link/, hand the link your
bytes, read them out at the far end. It is Rust with nothing platform-specific in the core:
Windows and Linux on x86-64, Linux on ARM boards, Android 12 or later. Nothing on the PC or the
phone needs a radio driver: the modem runs on the board.
The first ten boards per email address are free, commercial use included, and cover the current feature generation with all its bug fixes, for ever. Licences are per board, not per pair, and a board runs 20 hours before it needs one at all. Past ten boards, or for anything else - another board, a feature you need, NyxHop inside something you sell - write to tacitechvn@gmail.com. Details in docs/license.md.
Write to tacitechvn@gmail.com if you have a board we do not support yet, if you need a feature or an application the link does not have, if you are putting NyxHop inside something you sell, or if you want someone to tune it for your band, your range and your airframe. Questions about using it as it is belong in Issues and Discussions, where everyone can read the answer.
link/ the transmission system: protocol, block framing, shared app support,
board console, the flashing tool
apps/video/ the apps (ground, transmitting, Android, the camera app for the board's ARM
with its PC window and a test camera) - video and the UDP data pipe
apps/data/ the tool that measures the pipe
deploy/ prebuilt board images you flash (deploy/adrv9364, deploy/e200) and the camera
app built for the boards (deploy/ipcam)
docs/ licence terms, SDK notes, legal
| what | licence |
|---|---|
Source code in this repository: link/, apps/, docs/ |
public domain (Unlicense): no licence needed |
Binaries in deploy/adrv9364 and deploy/e200: FPGA design, radio daemon, board images |
EULA |
deploy/ipcam/: a build of apps/video/nyx-ipcam for the boards |
public domain, like its source |
| Third-party components inside the board images (U-Boot, BusyBox and others) | their own licences, see THIRD-PARTY.md |
Radio use is subject to the laws of your country: you are responsible for the frequencies and the power you transmit.

