Read and set a Yaesu G-800 antenna rotator over the network — from Linux or Windows 11.
The rotor is driven by an ERC-Mini (Easy Rotor Control) controller set to
Yaesu GS-232B emulation. A terminal server exposes that serial port as a raw
TCP socket. PSTRotatorAz runs on a Windows PC and shares the rotor among
several operating positions; this tool reads and sets alongside it without
stealing the line.
| File | Role |
|---|---|
rotorlib.py |
shared RotorClient — socket + GS-232B protocol |
rotorconf.py |
saved settings (grid square, host, port) |
geo.py |
Maidenhead grids, great-circle bearings, DX target table |
rotorcli.py |
CLI implementation (read/watch/set/point/bearing/gui) |
rotor |
Linux launcher (thin shim over rotorcli) |
rotor_gui.py |
Tkinter compass GUI |
tools/poller.py |
diagnostic poller |
Pure-stdlib Python 3 — no dependencies. Put the launcher on your PATH:
ln -s "$PWD/rotor" ~/.local/bin/rotorHardware-verified 2026-07-21: the Windows 11 client read the live bearing and commanded the rotor to a new heading end-to-end on real hardware (ERC-Mini in GS-232B mode, over the shared line).
The client runs on Windows with a normal Python 3 install (py rotor_gui.py),
but you can also get standalone .exe files that need no Python installed:
-
Prebuilt: the
build-windowsGitHub Actions workflow compilesRotorGUI.exe(compass app) androtor.exe(CLI) with PyInstaller on a Windows runner. Grab them from the run's Artifacts (rotor-windows-x64). -
Build it yourself on a Windows box:
py -m pip install pyinstaller pyinstaller --onefile --windowed --name RotorGUI --hidden-import rotorlib --hidden-import rotorconf --hidden-import geo rotor_gui.py pyinstaller --onefile --console --name rotor --hidden-import rotorlib --hidden-import rotorconf --hidden-import geo --hidden-import rotor_gui rotorcli.py
The executables land in
dist\. (PyInstaller can't cross-compile from Linux — it must run on Windows, which is why the CI runner does it.)
rotor read # print current bearing once (e.g. 269)
rotor watch # live bearing, timestamped; --interval S (default 1s)
rotor set 270 # turn to 270° — prompts, then watches it arrive
rotor set 270 --yes # skip the confirmation prompt
rotor set 270 --wait 0 # fire-and-forget (don't poll for arrival)
rotor gui # desktop compass UI (Tkinter)
rotor point europe # turn the beam at Europe — great circle from your grid
rotor point japan --yes # ...skipping the confirmation
rotor point europe --long # aim the long path instead
rotor bearing japan # just print the heading; don't move anything
rotor targets # list every built-in target and its headingpoint and bearing compute the great-circle heading from your grid square
to a target, so "turn the beam at Europe" becomes a number. From EN53 that's
about 46°; Japan is 322° (over the pole, not east).
$ rotor bearing europe
EN53 -> Europe (central) — Frankfurt, Germany (50.11, 8.68)
short path 45.9° true 6919 km (4299 mi)
long path 225.9° true 33111 km
A target can be any of:
- a region name —
europe,weurope,japan,australia,safrica,hawaii,caribbean, … (rotor targetslists all 31 with their headings) - a Maidenhead grid square —
rotor point JO62, 4- or 6-character, any case - raw coordinates —
rotor point 50.11,8.68, southern/western negatives included (rotor bearing -33.87,151.21)
A region isn't a point, so each built-in target names the specific city its
heading is computed to (Europe → Frankfurt, Africa → Kinshasa) — rotor targets
prints them, and you can substitute a grid or lat/lon whenever a different spot
in the region suits you better.
All headings are true bearings, not magnetic — the rotor reads true, so
don't apply declination. point moves the antenna and therefore confirms first,
exactly like set; bearing never touches the rotor.
In the GUI, the DX row under the presets (EU / Afr / JA / VK / SA / KH6) previews a target's heading on the dial and shows the city and distance; hit Turn to commit. The "Go to" box also accepts a target name or grid square, not just a number.
rotor gui opens a compass dial (rotor_gui.py, Tkinter — stdlib only):
- orange needle = live antenna heading (updates ~1 Hz)
- click the dial to preview a heading (fills "Go to"); double-click to turn
- green marker = committed target; status shows the delta and "on target"
- presets N/NE/E/SE/S/SW/W/NW for one-click aiming
- all rotor I/O runs on a background thread, so the window never freezes
set shares the serial line with PSTRotator's polling and physically moves the
antenna, so it confirms by default. --tolerance (default 2°) sets the arrival
window.
Your grid square and the rotor's network target are saved in a small JSON file, so you set them once:
rotor config --grid EN53 --host 192.168.115.99 --port 4001
rotor config # show current settings and where each came from| Setting | Meaning | Default |
|---|---|---|
grid |
your Maidenhead locator — the origin for every DX heading | EN53 |
host |
IP of the rotor's TCP endpoint | 192.168.115.99 |
port |
its TCP port | 4001 |
The file lives at ~/.config/rotor/config.json (Linux/macOS, honouring
XDG_CONFIG_HOME) or %APPDATA%\rotor\config.json (Windows); ROTOR_CONFIG
points somewhere else entirely.
Environment variables still override the file for a single command, so the old one-liners keep working:
ROTOR_HOST=192.168.1.50 ROTOR_PORT=4001 rotor read
ROTOR_GRID=FN31 rotor bearing europe
host/portare the ERC-Mini terminal server by default — the path this repo has verified against hardware, and the one that keeps working when the Windows box is off. If you'd rather go through PSTRotator's own GS-232/TCP server, pointhost/portat it instead; see the last section ofdocs/nodered-homeassistant.mdfor the trade-off.
ASCII, carriage-return-terminated, over raw TCP. Confirmed live 2026-07-21 by capturing the ERC-Mini's replies to PSTRotator's polling on the shared line.
| Action | Send | Reply |
|---|---|---|
| Read bearing | C + CR |
AZ=ddd + CR/LF — literal AZ= then 3 ASCII digits, e.g. AZ=188 = 188° |
| Set azimuth | Mddd + CR |
(none) — e.g. M270 turns to 270° |
Degrees are zero-padded to 3 digits (M005 = 5°). Valid set range 0–359. In the
360–450° overlap zone the controller may report a 3-digit value above 359
(e.g. AZ=380); the client passes that through unclamped.
The terminal server broadcasts the serial RX to every connected TCP client, so
rotor read/watch see the same bearing stream PSTRotator does — reading is
non-intrusive. Writing (set) does contend with PSTRotator's ~1 Hz polling on
the shared line, so avoid hammering it.
poller.py— the original diagnostic poller (sendsCevery second, prints raw hex + parsed digits, auto-reconnects). Handy for confirming the readback path is alive at the hardware level.nodered-rotor-flow.json— importable Node-RED flow that polls the bearing to MQTT and turns the rotor from an MQTT topic (see the docs entry below).
nodered-homeassistant.md— interface Node-RED and Home Assistant to the rotor over the same GS-232B-over-TCP path, alongside PSTRotator.overlap.md— why the 450° overlap is a hardware feature of the G-800 that no client (this tool or PSTRotator) can control.stopper-heading-checklist.md— how to relocate the mechanical stopper/dead-zone at the equipment (shared-rig, tower work).