IP-connected antenna rotator controller for the Yaesu G-5500DC (and similar az/el rotators). Three self-contained tiers — a firmware field unit, a Go brain daemon, and a set of command-line tools — with a rotctld-compatible shim for satellite-tracking software such as SkyRoof, gpredict, and WSJT-X.
SkyRoof / gpredict / WSJT-X
│ rotctld :4533
rotor-hamlib
│ REST / WebSocket
rotor-brain ←──── rotor (CLI)
│ TCP :7700
field unit (TM4C123 + W5500)
│ contact closures / ADC
G-5500DC rotator controller
| Component | Part |
|---|---|
| MCU board | Texas Instruments EK-TM4C123GXL LaunchPad |
| Ethernet | WIZnet W5500 module via BOOSTXL-IOBKOUT BoosterPack |
| Rotator | Yaesu G-5500DC (or G-5500) |
GPIO assignments (abbreviated):
| Signal | MCU pin | BoosterPack |
|---|---|---|
| AZ pot wiper | PD3 (AIN4) | AZ |
| EL pot wiper | PE1 (AIN2) | AY |
| AZ CW relay | PE3 | A3 |
| AZ CCW relay | PE4 | A4 |
| EL UP relay | PF4 | A1 |
| EL DOWN relay | PE0 | A2 |
| Hardware ESTOP | PB1 | A9 (active LOW, latching) |
| Park trigger | PB0 | A8 (falling edge) |
See hardware/gpio-mapping.txt for the complete pin table.
| Setting | Value |
|---|---|
| Field unit IP | 192.168.1.5 |
| TCP port (commands) | 7700 |
| Brain HTTP / WebSocket | :8090 |
| rotctld shim | :4533 |
All of these are configurable at runtime — see Network configuration.
cd controller
cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake
cmake --build build --target flashRequires arm-none-eabi-gcc and OpenOCD.
Linux / WSL2:
./bin/rotor-brainWindows (build natively to avoid SmartScreen):
cd \\wsl.localhost\Ubuntu\home\marcel\src\rotor-controller\middleware
go build -o bin\rotor-brain.exe .\cmd\brain
.\bin\rotor-brain.exeThe brain connects to 192.168.1.5:7700 and logs:
12:34:56.123 rotor-brain dev starting
12:34:56.124 config: field unit 192.168.1.5:7700 http :8090
12:34:56.130 fieldunit: connected to 192.168.1.5:7700
12:34:56.131 fieldunit: link UP
./bin/rotor statusField unit : LINKED
Age : 12ms
State : IDLE
AZ : 180.0° (0.4000)
EL : 45.0° (0.2500)
Motion : az=stop el=stop
RF switches: VHF=off UHF=off LNA=off RXTX=off
Duty cycle : AZ 0% EL 0%
The brain reads config from (highest priority wins):
- Environment variable
BRAIN_FIELD_UNIT_HOST,BRAIN_HTTP_ADDR, etc. rotor-brain.jsonnext to the executable ← simplest for Windows%APPDATA%\rotor\brain.json(Windows)~/.rotor-brain.json(Linux / macOS)
Minimal config file (rotor-brain.json):
{
"field_unit_host": "192.168.1.5"
}rotor-brain --help lists all options and shows the resolved config file path.
rotor [--brain URL] [-v] <command> [args]
--brain overrides the brain URL for one command.
-v / --verbose prints HTTP requests and responses to stderr.
rotor move cw stop # AZ clockwise, EL stop
rotor move ccw down # AZ counter-clockwise, EL down
rotor move stop stop # stop all motion
rotor park # drive to park position (AZ 180° / EL 45°)
rotor estop # emergency stop (software)
rotor fault # clear fault / acknowledge hardware ESTOProtor status # snapshot: position, state, RF switches, duty
rotor monitor # live stream at 1 Hz (averaged)
rotor monitor -rate 5 # 5 Hz
rotor monitor -rate 20 # raw 20 Hz frames
rotor monitor -rate 0.2 # one line every 5 secondsrotor pol vhf uhf # enable VHF + UHF polarization switches
rotor pol lna # enable LNA only
rotor pol off # all switches offLimits are normalised 0–1 (not degrees). Default: full travel.
rotor limits --az-min 0.05 --az-max 0.95 --el-min 0.0 --el-max 1.0The AZ range is divided into 90 sectors of 5° each. Each sector can have a minimum elevation floor — the antenna will not be driven below that floor while in that azimuth sector. Useful for buildings, trees, and grumpy neighbours.
# Require ≥ 20° elevation when AZ is in the 45°–50° sector
rotor block set --az 45 --el 20Point the antenna at the edge of the obstacle, then:
rotor block train # records current AZ sector → current EL floor
rotor block train --margin 5 # adds 5° safety margin above recorded ELRepeat for each sector along the obstacle boundary.
rotor block show # list non-zero sectors
rotor block show --map # ASCII horizon mapExample map output:
EL
90°│·············································
81°│·············································
72°│·············································
63°│·············································
54°│·············································
45°│·············████████·····················
36°│·········████████████████···················
27°│·····████████████████████████·············
18°│···█████████████████████████████···········
9°│·█████████████████████████████████·········
└─────────────────────────────────────────────
0 45 90 135 180 225 270 315 360 405 450° AZ
rotor block clear --az 45 # clear one sector
rotor block clear-all # remove all floorsWhen the antenna is commanded EL DOWN and the current AZ sector has a floor, elevation motion stops at the floor. Azimuth continues moving. No fault is raised — this is normal operating behaviour at an obstacle boundary.
A9 is a latching emergency stop:
| Event | Effect |
|---|---|
| A9 pulled LOW | All motion stops immediately. Display shows [E-STOP!]. |
| A9 released (HIGH) | Motion remains stopped. [E-STOP!] stays on display. |
rotor fault |
Clears the latch — motion commands accepted again. |
| Hold A8 (park) 3 s | Hardware-only latch clear when brain is not connected. |
The latch cannot be cleared while A9 is still LOW — the physical signal must be released before any software acknowledge is accepted.
Change the field unit's IP without reflashing:
rotor netconfig --ip 192.168.1.20 --subnet 255.255.255.0 --gateway 192.168.1.1This:
- Programs the new IP into the field unit's EEPROM (survives power cycles)
- Updates the brain config file so
rotor-brainconnects to the new IP on restart
Restart the brain after changing the IP:
./bin/rotor-brain # reads updated config file automaticallyRevert to factory defaults (config.h values, next reboot):
rotor netconfig resetProvides a rotctld-compatible TCP server on port 4533 for satellite-tracking software (SkyRoof, gpredict, WSJT-X, fldigi, …).
./bin/rotor-hamlib # connect to brain at localhost:8090
./bin/rotor-hamlib --help # show all optionsEnvironment variables:
| Variable | Default | Description |
|---|---|---|
HAMLIB_LISTEN |
:4533 |
TCP listen address |
HAMLIB_BRAIN_URL |
http://localhost:8090 |
Brain API |
HAMLIB_AZ_RANGE |
450 |
Full AZ travel (°) |
HAMLIB_EL_RANGE |
180 |
Full EL travel (°) |
HAMLIB_TOLERANCE |
2.0 |
Stop tolerance (°) |
SkyRoof setup: point the rotator connection to localhost:4533.
Running on Linux / WSL2 (recommended over Windows .exe):
./bin/rotor-brain &
./bin/rotor-hamlib &
# SkyRoof on Windows connects to localhost:4533 via WSL2 mirrored networkingCross-compile from the development machine:
make -C middleware build-pi # produces bin/rotor-brain-pi, bin/rotor-pi, bin/rotor-hamlib-piCopy to the Pi and install as services:
scp middleware/bin/*-pi pi@raspberrypi.local:~/rotor/
# On the Pi — create /etc/systemd/system/rotor-brain.service
[Unit]
Description=Rotor Brain
After=network.target
[Service]
ExecStart=/home/pi/rotor/rotor-brain-pi
Restart=on-failure
Environment=BRAIN_FIELD_UNIT_HOST=192.168.1.5
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now rotor-brain
sudo systemctl enable --now rotor-hamlib # similar service fileping 192.168.1.5 # is it on the network?
nc -zv 192.168.1.5 7700 # is TCP port open?If ping works but port 7700 refuses: the field unit is running but not yet in LISTEN state. Wait ~3 seconds after boot or press the reset button (SW2).
Run rotor status — if Telemetry: (none) check that the brain log shows
blocks: pushed 90 sectors (indicates the TCP session completed cleanly).
The LCD initialises to 8-bit mode on power-on; it must receive the 4-bit init
sequence at its actual I2C address. If garbled after a firmware change, check
that display_init() is called after net_init() in main.c.
- Verify A9 (PB1) is HIGH — check with a multimeter.
- With brain connected:
rotor fault - Without brain: hold A8 (park button) for 3 seconds while A9 is HIGH.
- If still stuck: check that no G-5500 wiring is accidentally pulling PB1 to GND.
Build natively on Windows — locally compiled binaries have no Zone.Identifier:
go build -o rotor-hamlib.exe .\cmd\hamlibOr run the Linux binaries inside WSL2 and connect from Windows via localhost.
# Prerequisites: arm-none-eabi-gcc, cmake, openocd
cd controller
cmake -B build -DCMAKE_TOOLCHAIN_FILE=cmake/arm-none-eabi.cmake
cmake --build build # compile
cmake --build build --target flash # flash via OpenOCDDebug logging (UART0, 115200 baud):
cmake -B build -DCMAKE_TOOLCHAIN_FILE=... -DENABLE_DEBUG_LOG=ONcd middleware
make build # Linux x86-64
make build-windows # Windows x86-64 (cross-compile)
make build-pi # Raspberry Pi arm64
make all # all three platformsRequires Go ≥ 1.22. Set GOTOOLCHAIN=local to suppress automatic toolchain downloads.
VE3UKW — built with ❤️ and considerably more patience than Windows deserves.