Skip to content

Repository files navigation

rotor

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.

Layout

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

Install (Linux)

Pure-stdlib Python 3 — no dependencies. Put the launcher on your PATH:

ln -s "$PWD/rotor" ~/.local/bin/rotor

Windows 11

Hardware-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-windows GitHub Actions workflow compiles RotorGUI.exe (compass app) and rotor.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.)

Usage

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 heading

DX headings

point 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 nameeurope, weurope, japan, australia, safrica, hawaii, caribbean, … (rotor targets lists all 31 with their headings)
  • a Maidenhead grid squarerotor point JO62, 4- or 6-character, any case
  • raw coordinatesrotor 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.

Desktop GUI

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.

Configuration

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/port are 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, point host/port at it instead; see the last section of docs/nodered-homeassistant.md for the trade-off.

Protocol (Yaesu GS-232B, as spoken by the ERC-Mini)

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.

tools/

  • poller.py — the original diagnostic poller (sends C every 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).

docs/

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

About

Linux based UI and Server to drive G-800 rotor

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages