From a Missing Safety Check to AntennaGuardian for Flex Radios
How a question about antenna selection became a tested Windows application, a Raspberry Pi service, and an open-source human-AI collaboration
I did not set out to build another radio application.
I developed AntennaGuardian in an explicit collaboration with OpenAI Codex. I was the project owner, amateur-radio domain expert, and operator of the physical station. I set the requirements, made the safety decisions, evaluated the user experience, and controlled every live-radio test. Codex worked as my research and coding collaborator: inspecting documentation and source code, challenging assumptions, implementing changes, constructing tests, and automating builds and releases. In the narrative that follows, “I” refers to my decisions and experience, while “we” refers specifically to that defined collaboration.
I was looking at AetherSDR, an impressive alternative user interface for software-defined radios, and thinking about how I actually operate my FlexRadio. My station has more than one antenna, and those antennas are not appropriate for every band. The radio has its own hardware protection, but I wanted an additional, explicit rule at the station level: if the selected antenna is not approved for the current band, do not transmit.
That sounds like a checkbox in an existing program. It was not.
The deeper we looked, the more important the location of that check became. A warning inside one user interface would only protect transmissions initiated through that interface. It might do nothing when WSJT-X keyed the radio through SmartSDR CAT, when SmartSDR itself requested PTT, or when another client was in control.
The real question was not, “Can we add an antenna matrix to AetherSDR?”
It was, “Where can one decision reliably cover every software path to transmit?”
Answering that question led to AntennaGuardian: a compact Windows sidecar with an always-visible status overlay and policy editor. It also led to AntennaGuardianPiLite, a compiled C service for Linux and Raspberry Pi. Both projects are open source, both enforce the same basic safety policy, and both grew from one deliberately small bench experiment.
Finding the right boundary
Our first ideas were reasonable but incomplete.
Flex profiles can store a great deal of station state. A carefully designed profile might select the right antenna for a band, and operators can certainly use profiles as part of a disciplined workflow. But profiles are presets, not an independent interlock. They can be loaded incorrectly, changed manually, or bypassed by another client. WSJT-X does not become safer merely because the correct profile usually happens to be active.
A CAT proxy was another possibility. If every PTT request passed through software we controlled, the proxy could evaluate a rule before forwarding it. The problem was in the word “every.” SmartSDR CAT was already the path used by some applications, but it was not the only possible source of PTT. Building a new choke point in front of one control path would still leave other paths outside the boundary.
FlexRadio’s Ethernet API offered a better mechanism: a dynamic antenna interlock registered directly with the radio. An interlock client can declare that it covers specific antenna ports. When transmit is requested, the radio asks whether that interlock is ready. If the client withholds readiness, the radio does not enter the transmitting state.
That changed the shape of the project. AntennaGuardian did not need to replace SmartSDR, AetherSDR, CAT, or WSJT-X. It could sit beside them, observe the radio-authoritative transmit frequency and antenna, evaluate a policy, and answer the radio’s interlock request.
The enforcement point was now below the applications that could initiate PTT.
Prove one thing before building everything
This was safety-related software, so we resisted the temptation to start with a polished interface. The first deliverable was a throwaway Python spike with one narrow purpose:
Can a standalone API client prevent software-originated PTT from reaching RF by registering an antenna interlock and withholding
ready?
The prototype had an observe mode and a block mode. Block mode created the interlock, explicitly sent not_ready, and had no code path capable of sending ready. That last constraint mattered. Before experimenting with policy decisions, we wanted the first live test to fail closed by construction.
The bench process also established an important collaboration rule. Codex could inspect repositories, read protocol documentation, write code, build packages, and operate fake radios, but it would not contact my physical radio until I explicitly approved the test. I controlled the station and keyed PTT. The software captured the protocol transcript and state transitions.
The result was decisive.
The radio reported two PTT_REQUESTED events with a software source. The spike withheld readiness. The radio reported that an interlock was preventing transmission, and no TRANSMITTING state followed. The dynamic interlock was then removed, and a cleanup observation confirmed that the radio had returned to its normal ready state.
The spike uncovered a documentation compatibility detail as well. Flex examples alternated between model= and name= when creating a dynamic interlock. My radio rejected the model= form but accepted name=. That is exactly the kind of fact that is difficult to settle through architectural discussion and easy to settle with a controlled experiment and a transcript.
At that point we had not proved the product. We had proved the mechanism. That was enough to move forward.
Turning the experiment into a safety model
The production design started with an explicit allow matrix. Each antenna has a set of bands on which it may transmit. Anything not present in that matrix is denied.
The policy is deliberately fail closed:
- An unknown frequency is denied.
- An unknown antenna is denied.
- A frequency outside the known band catalog is denied.
- An antenna and band combination not explicitly enabled is denied.
- A policy change returns the interlock to
not_readybefore allowing anything else. - An unkey event returns the interlock to
not_ready. - A reported transmission outside policy becomes a fault and reasserts
not_ready.
Most importantly, only one part of the system is allowed to produce a command that makes the interlock ready.
The Windows application was divided into three modules. AntennaGuardian.Core owns the band catalog, policy engine, and deterministic state machine. AntennaGuardian.Flex translates SmartSDR protocol traffic into domain events and carries commands back to the radio. AntennaGuardian.App owns the WPF interface, tray integration, settings, and persistence.
That separation is more than tidy project organization. The UI cannot decide to permit transmission. Protocol strings do not flow directly into controls that manipulate the interlock. Radio input becomes a typed event, the controller evaluates it against policy, and the controller is the only component that can emit SetInterlockReady.
The visible states follow the same model: offline, registering, protected, allowing, blocked, transmitting, and faulted. This made the overlay a view of the actual safety state rather than a collection of labels inferred independently by the UI.
Designing something that can stay on screen
The technical mechanism worked, but station software lives on crowded desktops. An application that protects every transmission but covers the waterfall, logbook, or digital-mode controls will quickly be minimized or closed.
The interface therefore had to be quiet, compact, and readable at a glance.
The overlay can remain above other applications, move freely, remember its position, and adjust opacity. It uses color only when color carries operational meaning: gray for offline, green for protected or permitted operation, red for a block or fault, and amber while connecting or registering.
The shield becomes more colorful when protection is active. The application version appears beside the title, and the status line includes both the radio nickname and configured address so an operator with more than one radio can see exactly what is protected.
Get John’s stories in your inbox
Join Medium for free to get updates from this writer.
The settings interface went through several rounds of direct criticism, which improved it. Early checkboxes looked dated and consumed too much vertical space. They became compact band toggles arranged as two antenna rows. The m suffix was removed from each button because the surrounding context already made the unit clear. ANT1 and ANT2 gained optional vanity names without changing the Flex identifiers used by the protocol.
A large ENABLE PROTECTION control replaced a weak checkbox. The settings window received sensible default dimensions, a styled scrollbar for smaller displays, and remembered size. Click-through mode gained an escape route through the system tray, because a perfectly click-through overlay without another control surface is effectively an application the operator cannot control.
These were not cosmetic details added after the “real” work. They determined whether the safety tool could remain present during normal operation.
Collaboration in short feedback loops
I built AntennaGuardian with OpenAI Codex as an engineering collaborator. The useful part of that collaboration was not the volume of generated code. It was the speed with which an observed behavior could become a reproducible case, a focused change, and a verified release.
I supplied the operating context, made safety decisions, controlled access to the radio, and tested the software in the station. Codex inspected code and documentation, proposed boundaries, implemented changes, generated tests, built release artifacts, and kept the repositories and documentation synchronized.
Just as importantly, the collaboration included disagreement and correction. We abandoned ideas that could not cover every PTT path. We corrected misleading status language in observe mode. We changed UI controls that technically worked but felt wrong. We refused to treat profiles as an interlock simply because they could approximate part of the workflow.
The process was conversational, but the durable record was engineering evidence: commits, protocol transcripts, unit tests, fake-radio integration tests, screenshots, build logs, and release artifacts.
The bugs that made the product better
Several important improvements came only after realistic use.
The first GitHub-built Windows executable did not launch on a clean machine. The application itself was self-contained, but native WPF runtime libraries were missing from the published single-file package. The release workflow was corrected and a new artifact was produced. This was a reminder that “build succeeded” and “a user can launch the download” are different acceptance criteria.
Disabling protection exposed a duplicate shutdown path that could terminate the application. Cleanup was made idempotent so the same disconnect could be requested safely from more than one UI path.
Disconnecting SmartSDR or AetherSDR initially left stale frequency and antenna data visible as TX BLOCKED ... INVALID. The protocol and controller were changed to clear transmit context when the last client disappeared, returning the overlay to an honest offline or waiting state.
Click-through mode did exactly what its name promised and immediately raised the practical question: “How the hell do I control the app?” The tray menu became the persistent control point.
There was also the unavoidable Windows distribution issue. An unsigned executable downloaded from a new GitHub project triggers Microsoft Defender SmartScreen’s unfamiliar-app warning. We added a real application icon, release documentation, privacy and security policies, and prepared a SignPath Foundation integration. The code remained unsigned while enrollment was pending rather than pretending that packaging tricks could substitute for trust and reputation.
Each of these changes made the project less like a successful demonstration and more like software another operator could reasonably install.
From desktop companion to Raspberry Pi service
Once the Windows application was stable, a second use case became obvious. Some operators would rather put the guardian on a Raspberry Pi, start it at boot, and never depend on a Windows desktop session.
AntennaGuardianPiLite carries the same policy into a small terminal-only C11 application. Python remained valuable as the original proof, but it was not used as the runtime. PiLite compiles to a single native executable for 64-bit ARM, 32-bit ARM, and x86–64 Linux.
Its configuration is strict JSON. Unknown keys, malformed antenna identifiers, duplicate bands, missing policy rows, and invalid selectors are rejected before a radio connection is opened. The installer creates a restricted service account and a hardened systemd unit, but deliberately does not enable protection until the operator edits and validates the policy.
Normal service control uses systemctl start, stop, restart, status, and enable. A clean SIGTERM removes the dynamic interlock when the connection is still available. An --observe mode subscribes and reports without creating an interlock, and its language says WOULD ALLOW, WOULD BLOCK, and OBSERVED TRANSMITTING so no one mistakes observation for enforcement.
Field testing again changed the design.
AetherSDR could produce a burst of transmit updates during a band change. The initial implementation sent repeated not_ready commands while prior command responses were still arriving. That filled a small response cache, produced warnings, and reset the connection. We reproduced the burst with a fake radio, coalesced pending interlock updates, and verified that a burst now produces at most the necessary final command.
The first reconnect controls also mixed two different concerns: how often to attempt a connection and how often to write another failure into the journal. Those were separated into bounded retry timing and log suppression. Later, a user suggested a better model based on the radio’s own UDP discovery broadcasts.
PiLite 0.2.0 now listens for validated Flex VITA-49 discovery announcements on UDP port 4992. It can pin a radio by serial number, advertised IP address, or both. A serial pin survives DHCP changes; a dual pin requires both values to match the same announcement. Malformed packets, the wrong VITA class, and nonmatching radios are ignored. When the radio is off, PiLite does not keep polling. It waits for the next matching broadcast and then connects to the endpoint advertised by that radio.
That is a simpler operational model because it follows the radio’s lifecycle instead of guessing it with a timer.
Testing without treating the station as a test fixture
The live bench test was necessary, but most testing should not require an RF device.
The Windows solution has unit tests for band mapping, policy decisions, state transitions, protocol parsing, settings migration, and overlay visuals. A loopback fake Flex server tests command sequencing and dynamic interlock behavior. GitHub Actions restores, tests, and publishes a self-contained Windows executable for version tags.
PiLite has core configuration and policy tests plus a Python fake-radio integration harness. The harness accepts the TCP connection, verifies subscriptions, creates a fake interlock, injects allowed and blocked PTT events, delays responses, sends AetherSDR-like bursts, and confirms clean removal on shutdown.
For UDP discovery, the test sends synthetic VITA-49 datagrams. It first sends malformed packets, the wrong packet class, the wrong serial, and the wrong IP, confirming that none opens a TCP session. Only a packet matching every configured selector is allowed to trigger the connection.
Both normal and AddressSanitizer/UndefinedBehaviorSanitizer builds run in continuous integration. Release workflows cross-compile PiLite for all three Linux architectures, package installation files and documentation, and publish SHA-256 checksums with the archives.
This test structure let us reserve live-radio contact for questions only the radio could answer.
Delivery is part of the feature
Both projects are public on GitHub under the MIT License:
The repositories cross-reference each other near the top of their README files. Windows users get a self-contained .exe and a visual policy editor. Linux users get architecture-specific archives, an installer, a restricted service account, a systemd unit, and checksums. We create version tags after the test suites pass, and those tags automatically build and publish the release artifacts.
The Windows project reached version 0.1.8 through a sequence of small operational improvements. PiLite reached 0.2.0 with pinned UDP discovery. The version numbers are modest because the projects are still young, but the safety mechanism is no longer hypothetical. It has been bench-proven against a physical FlexRadio and exercised continuously against deterministic protocol simulations.
What I took away from the project
The most important decision was not the language, UI framework, or deployment target. It was choosing the radio’s interlock as the enforcement boundary.
Once that decision was right, the rest of the architecture could become straightforward. Applications report intent. The radio reports authoritative state. A small policy engine decides whether a known antenna and known frequency are explicitly allowed. Everything else remains not ready.
The second lesson was to build evidence in layers. Research narrowed the possibilities. A deliberately incapable prototype proved blocking without creating an accidental allow path. A state machine turned the result into explicit rules. Fake radios made edge cases repeatable. Real operators and real clients exposed timing and usability failures that unit tests alone would not have invented.
The third lesson was that collaboration works best when responsibilities are clear. I brought decades of software experience, the amateur-radio context, and the accountability of operating the station. Codex brought rapid codebase analysis, implementation, test construction, and release automation. Neither substituted for the other. The product emerged from the feedback loop between domain judgment and verifiable engineering work.
I was first licensed in Germany in 1995 while serving in U.S. Air Force Communications Intelligence, and I operated as DA4KI and DA2KI for nearly a decade. Today I operate as W3JFK and have worked professionally in software development for more than 25 years. AntennaGuardian sits at a satisfying intersection of those two parts of my life.
It began with a small concern: the wrong antenna should not be used on the wrong band.
The finished answer is also small by design. It stays out of the way, watches the radio, and says no when no is the safe answer.
AntennaGuardian is an additional software safeguard. It does not replace radio hardware protection, correct station wiring and configuration, or responsible RF operation.






