Skip to content

Repository files navigation

GHClient

A Linux client for a Green Heron Everyware station — the remote antenna switch panel (4 switches × 9 antennas) and an attached rotator such as an RT-21 — speaking the device's undocumented TCP protocol on port 10000. Both arrive on one connection, so one client serves both.

Three front ends over one client library — a terminal panel, a GTK4 desktop app, and an MQTT bridge with Home Assistant discovery.

./gh-panel 192.0.2.10          # curses TUI; your server's address
./gh-panel 192.0.2.10 --position 2    # just your position's column
./gh-gui   192.0.2.10          # GTK4 window: antenna grid + rotator dial
./gh-gui   192.0.2.10 --position 2    # just your operating position's column
./gh-gui   192.0.2.10 --rotor-only    # just the dial
./gh-mqtt  192.0.2.10 --broker 192.0.2.20
export GH_SWITCH_HOST=...      # ...or set this and pass no address

The GTK4 panel

Beam-20 is live on AS-84F-2; the other three switches show it as in use · AS-84F-2 and cannot select it. Everything else sits on OFF. On the right, the rotator: the orange needle is where the antenna actually is, the green marker is what was asked for.

The same grid in the terminal:

Green Heron switch panel — 192.0.2.10:10000              connected 0m08s

Antenna              AS-84F-1  AS-84F-2  AS-84F-3  AS-84F-4
Beam-10                 ·         ·         ·         ·
Beam-15                 ·         ·         ·         ·
Beam-20                ◦ 2       ● ON      ◦ 2       ◦ 2
EFHW-40                 ·         ·         ·         ·
...
OFF                    ● ON       ·        ● ON      ● ON

  Rotor    62.9°   asked  64.3°   Δ1.4°

↑↓ port · ←→ switch · ⏎ select · o OFF · t turn · r raw · q quit

t prompts for a heading and sends it to the rotator; an empty line cancels. Two steps, for the same reason the GTK dial has them — there is no stop command, so no single keystroke may aim an antenna. With more than one rotator, n picks which.

--position works here too, and p cycles it without leaving the panel. The header says which column you are looking at, so hidden switches never read as missing ones. The key line only offers what is actually available — no t when no rotator is reported.

--dry-run exercises the panel without transmitting anything. --no-keepalive omits the 5 s NUL the official client sends.

Install

Everything but the GTK4 window is pure standard library. The window needs PyGObject and GTK 4, which are distro packages rather than wheels:

Fedora sudo dnf install python3-gobject gtk4
Debian / Ubuntu sudo apt install python3-gi gir1.2-gtk-4.0
Arch sudo pacman -S python-gobject gtk4
git clone https://github.com/<you>/everyware-linux && cd everyware-linux
export GH_SWITCH_HOST=192.0.2.10     # your Everyware server
./gh-gui

There is nothing to build and nothing to install into site-packages; the launchers run from the checkout. To get a desktop launcher, copy packaging/gh-gui.desktop into ~/.local/share/applications/ and either put GH_SWITCH_HOST in your session environment (~/.config/environment.d/gh.conf) or edit its Exec= line to name your server.

Only Fedora is verified — that is what this was developed and hardware-tested on. The other two package sets are the standard names for GTK 4 with Python bindings, not something that has been run here.

Your operating position

One position and the rotator

Four columns are useful for seeing the whole station; at the radio you are only ever one of them. --position 2 shows that switch alone, next to the rotator — in the GTK window and the curses panel both, changed live from the header-bar dropdown or with p.

It takes either the position number or the full switch name (--position AS-84F-2), and it is a display filter and nothing else: all four switches are still tracked, so Beam-20 still greys out as in use · AS-84F-3 when a neighbour takes it. A position the device is not reporting falls back to showing all four, because a mistyped position should not produce a blank panel.

Rotator

The rotator dial on its own

Rotator support is in the GTK4 client, alongside the antenna grid or on its own with --rotor-only. It was developed against an RT-21.

Confirmed end to end on that hardware in both directions: the panel tracks the RT-21's reported heading, and pressing Turn moved it — 55.6° to a settled 68.5° on a commanded 67.6°. See NOTES.md for the sampled timeline.

One thing to know before you trust a heading: the rotator stops about two degrees short, along whichever way it was turning. Out and back on one run measured 2.5° and 2.1° short, opposite signs in absolute terms, so correcting for a fixed offset would double the error one way. Its position reading also wanders over a ±3.8° band while mechanically stationary — wider than that error, and visible in the official Windows client too, so it is the rotator and not this software. Together they are why every front end here shows you asked 64.3° Δ2.1° and never tells you that you have arrived.

Once it is inside its deadband, sending the same heading again does nothing — the controller already believes it is there. Nudge it by eye instead of looping.

A dial appears for each rotator the server reports, keyed by its configured name — the rotator here is called Rotor, which is a name and not a type. No rotator on the server means no dial: the device announces one only while its controller is powered on, so silence is how absence is reported, and the panel takes it at face value.

Choosing a heading and sending one are separate gestures. Clicking the dial, or pressing a compass preset, only proposes a heading — it appears as the dashed grey marker and fills the entry box. Turn (or Enter) is what transmits. That is not caution for its own sake: nothing resembling a stop or park command exists anywhere in this protocol, so a turn that starts cannot be recalled in software. There is deliberately no double-click-to-send.

The needle is the reported heading, never the commanded one. This rotator overshoots, then settles about two degrees short along whichever way it was turning, and dithers by up to a couple of degrees at rest. So 62.9° under a command of 64.3° is the hardware behaving normally. The panel shows both numbers and the difference, and never claims "on target" — at the measured dither, any such threshold would flicker.

If the reported heading goes quiet for ten seconds the dial greys out and Turn is disabled. Powering the rotator controller off does not drop the connection — the switch records keep coming — so silence is the only evidence that the controller is gone.

Status

Working. The protocol is decoded in both directions and verified against the hardware: this client's SET_SWITCH bytes are byte-identical to those of the official GH Everyware Client, and the device responds to both identically.

  • Live state for all four switches, refreshed every ~0.5–3.4 s
  • Antenna selection, confirmed by the device in ~123 ms
  • Lock display — antennas held by another switch are marked ◦ N
  • Reconnect with backoff; last known state stays visible, flagged stale
  • Rotator heading and TURN, in the GTK4 client — decoded, then sent to a real RT-21 and confirmed to move it

A few SWITCHADD sub-fields still have no established meaning; they are parsed and carried but nothing branches on them. See NOTES.md for the full protocol.

Home Assistant

pip install -r requirements-mqtt.txt
./gh-mqtt 192.0.2.10 --broker 192.0.2.20 --username ha --password ...

Each switch is announced as a select entity whose options are the antenna names the device advertises, so all four appear under one HA device as four dropdowns. That is the entity type that matches the hardware: a switch is on exactly one of nine ports, which is a choice, not a set of toggles. Nothing to add to configuration.yaml — discovery configs are published retained, so Home Assistant repopulates on its own after a restart.

topic
greenheron/availability online / offline — also the last will
greenheron/as_84f_1/state selected port, retained
greenheron/as_84f_1/set write a port name here to select it
greenheron/as_84f_1/attributes JSON: locks, holder, ports, wireless signal
greenheron/rotor/rotor/heading reported heading in degrees, retained
greenheron/rotor/rotor/target last heading commanded, retained
greenheron/rotor/rotor/set write a heading here to turn
greenheron/rotor/rotor/attributes JSON: status, seconds since last report, announced
greenheron/rotor/rotor/availability per rotor, separate from the bridge's own

A rotator becomes two entities, not one: a read-only sensor for the heading it reports, and a number for the heading you ask for. That split is the hardware talking. This rotator settles a couple of degrees short and does not close the gap when you send the same heading again, so a single slider bound to the reported heading would drift under your hand and never sit where you put it. The number echoes what was asked; the sensor says where the antenna is; nothing pretends they are the same. For the same reason the number steps in whole degrees — the sensor resolves about half of one.

Rotor entities carry their own availability, because a rotator controller that is switched off stops reporting while the switches carry on. Ten seconds of silence and they go unavailable on their own, rather than leaving a heading on screen for an antenna nobody is driving.

Entities go unavailable when the bridge loses either the broker or the switch, so a stale dropdown never looks live. Commands are published with optimistic: false — HA waits for the device to confirm rather than assuming the relay moved.

Credentials are read from $GH_MQTT_USERNAME / $GH_MQTT_PASSWORD as well as flags, which keeps them out of your shell history and out of ps.

To run it as a service, see packaging/gh-mqtt.service.

Layout

greenheron/protocol.py framing and record parsing — pure, no I/O
greenheron/client.py socket, state tracking, reconnect, keepalive
greenheron/tui.py curses panel — antenna grid and rotator
greenheron/gui.py GTK4 panel — antenna grid and rotator
greenheron/compass.py the rotator dial, drawn with Cairo — no device knowledge
greenheron/mqtt_bridge.py MQTT + Home Assistant discovery — switches and rotators
packaging/ systemd user unit, desktop entry, environment files
ghprobe.py hexdump whatever the device sends — the debugging oracle
tools/ pcap extraction and protocol experiments
NOTES.md the protocol, how each claim was established, and open questions

Tests

python3 -m venv --system-site-packages .venv && .venv/bin/pip install pytest
.venv/bin/python -m pytest tests/

--system-site-packages is what lets the venv see PyGObject, which is a distro package rather than a wheel. Without it the GTK tests skip and the rest still run.

Protocol fixtures are verbatim bytes captured off the device, not hand-written. Connection tests run against a local stand-in server, including records delivered one byte at a time and mid-session disconnects. No hardware needed.

Addresses in the docs use 192.0.2.10 — the RFC 5737 documentation range, not a real host.

License

MIT. Not affiliated with or endorsed by Green Heron Engineering; the protocol was determined by observing traffic between their client and their device for interoperability.

About

Linux client for the Green Heron Engineering remote antenna switch panel — curses TUI plus a reverse-engineered protocol spec for GH Everyware

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages