at-os3 and PTGS: a Tiny Linux-Driven TinyGS Station
TinyGS stations are usually thought of as single embedded devices: an ESP32, a LoRa radio, Wi-Fi credentials, MQTT/TLS, satellite scheduling, radio control, packet forwarding, and status reporting all living inside the same firmware.
For my own station, I wanted a different split.
The radio should stay small, deterministic, and close to the antenna. The network side should stay on Linux, where TLS, MQTT credentials, logs, TLE data, and station management are easier to inspect and replace.
That is the role of at-os3 and PTGS:
- at-os3 is the CH32V003 + SX1278 LoRa modem firmware;
- PTGS is the Linux process that speaks TinyGS MQTT and drives one or more at-os3 modems over serial.
Together they form a TinyGS-compatible station architecture built from a very small external RF modem and a normal Linux host.
Source repositories:

Early at-os3 hardware: a CH32V003 board wired to an SX1278/Ebyte-style LoRa module and a simple antenna setup.
What at-os3 Is
at-os3 is a raw LoRa AT modem firmware for a CH32V003 connected to an SX1278-compatible radio module.
It exposes a UART command interface at 115200 8N1 and keeps the modem path
small:
UART byte / radio IRQ
-> event queue
-> event loop
-> AT parser / LoRa FSM
-> bounded radio action
The firmware is intentionally not a TinyGS implementation. It does not know about MQTT, TLS, satellite schedules, station credentials, JSON, or TLE data. It only owns the deterministic radio work:
- UART AT command parsing;
- SX1278 register configuration over SPI;
- LoRa RX/TX;
- radio IRQ handling;
- received packet reporting with RSSI, SNR, and frequency error;
- a table-driven LoRa FSM running on the OS3 event kernel.
That narrow scope is the point. The modem remains replaceable and easy to reason about. If the Linux process changes, the CH32V003 firmware does not need to carry that complexity.
What PTGS Does
PTGS is the host side of the station.
It connects to the TinyGS MQTT backend, subscribes to global and station command
topics, decodes modem configuration commands, applies them to the serial modem,
and publishes received LoRa packets back as TinyGS tele/rx payloads.
PTGS owns the parts that are better kept off the microcontroller:
- MQTT/TLS connection to
mqtt.tinygs.com; - TinyGS command decoding and acknowledgements;
- station identity and location;
- TLE/TLX handling;
- Doppler correction;
- logs;
- packet forwarding;
- multi-radio selection when several at-os3 modems are connected.
The split looks like this:
TinyGS backend
|
| MQTT/TLS, commands, schedules, telemetry
v
Linux host running PTGS
|
| serial AT commands, packet notifications
v
CH32V003 running at-os3
|
| SPI, DIO IRQ, reset, RF controls
v
SX1278 LoRa module + antenna
PTGS applies backend modem profiles to the radio by issuing commands such as:
AT+MODE=1
AT+BAND=<frequency_hz>
AT+PARAMETER=<sf>,<bw_code>,<cr_code>,<preamble>
AT+PKT=<crc>,<ldro>,<implicit_len>
AT+SYNCWORD=<byte>
AT+IQI=0|1
AT+CRFOP=<power>
AT+MODE=0
Received LoRa packets arrive from at-os3 as unsolicited serial notifications:
+RCV=<addr>,<len>,<hex_payload>,<rssi_dbm>,<snr_db>,<freq_err_hz>
PTGS then turns that into the TinyGS receive payload expected by the backend.
Doppler Tuning
The useful part of this architecture is that the station can still behave like a TinyGS node without making the modem firmware responsible for orbital tracking.
When the backend sends a satellite pass configuration with TLE/TLX data, PTGS
computes the current Doppler offset on the Linux host. If the satellite is above
the horizon and the correction changes enough, PTGS retunes the serial modem by
issuing a new AT+BAND=<frequency_hz> and reapplying the radio profile.
The CH32V003 only sees the result: a concrete frequency and a concrete LoRa configuration. It does not need an SGP4 implementation, floating point orbital math, TLS certificates, or MQTT state.

PTGS applying a backend-driven radio profile, including Doppler correction and SX1278 register readback after entering RX.
A Few Days of Runtime
This is still experimental software, but it is no longer just a bench test.
The current PTGS node has been running for several days with at-os3 modems as
the LoRa front ends. The TinyGS console shows the station online, listening to
Tianqi-33, with the radio marked ready.
Visible station state at the time of the capture:
- station:
netmonkch32v003-2; - status: online;
- listening:
Tianqi-33; - version:
2603242; - location:
48.74, 1.659; - QTH locator:
JN08tr; - band:
396.6 - 474.6 MHz; - telemetry packets:
197; - confirmed packets:
1250; - record distance:
3462.0 km; - radio status: ready.

The packet view also shows recent reception activity and the geographic spread
of received satellite packets. This is the important validation point: the
station is not only answering AT; it is being driven by the TinyGS backend,
retuned for passes, and producing packets visible in the network.

Multi-Modem Mode
PTGS can drive more than one at-os3 modem at the same time.
Each radio is configured as a named serial node:
{
"radios": [
{
"name": "radio0",
"serial": "/dev/ttyACM0",
"baud": 115200
},
{
"name": "radio1",
"serial": "/dev/ttyACM1",
"baud": 115200
}
]
}
In the current implementation, when several nodes are active, PTGS fans out the
backend modem profile to every radio. Doppler retunes are also applied to every
radio. Received packets include the ptgs_node field so the source modem can
be identified.
That enables a simple diversity mode today. If multiple radios receive the same logical packet within a short window, PTGS publishes the best copy instead of uploading duplicates. The current selection rule is deliberately simple: RSSI first, then SNR as a tie breaker.
This already makes it possible to test several antennas, placements, or front-end boards while keeping a single TinyGS station identity.
The next useful direction is not only diversity, but parallel listening. A single ground station could run several at-os3 modems on different frequencies at the same time: for example one modem following a satellite telemetry downlink while another listens to a payload or data downlink from the same spacecraft. The host would still own the station identity and TinyGS session, but the RF edge would become a set of small independent LoRa front ends instead of a single retuned radio.
Why Keep the Modem This Small
The CH32V003 is not a comfortable target. It has 16 KiB of flash and 2 KiB of SRAM. That is exactly why it is useful here.
Putting only the deterministic modem work on the microcontroller forces a clean boundary:
- the firmware does not own credentials;
- the firmware does not parse JSON;
- the firmware does not need network recovery logic;
- the firmware does not schedule satellite passes;
- the firmware does not hide background work behind a scheduler.
at-os3 is built on the OS3 model: explicit events, bounded handlers, table-driven FSMs, and minimal ISRs. A radio IRQ becomes an event. A UART byte becomes an event. Protocol logic runs later from the event loop.
For a modem, that model is a good fit. The radio path stays auditable, while the host can evolve quickly.
Current Limitations
This is not a drop-in replacement for every TinyGS station setup.
Current practical limits:
- PTGS is experimental and hardware-tested, not a polished appliance;
- the at-os3 radio path is LoRa-only;
- FSK profiles are ignored by the LoRa front end;
- OTA firmware update is not implemented;
- TX is intentionally conservative and disabled unless explicitly allowed;
- RF performance depends on the actual SX1278 module front end and antenna, not only on the synthesizer range accepted by firmware;
- the current station behavior mirrors the TinyGS backend enough for this use case, but it is still a reimplementation outside the official ESP32 firmware.
That is a reasonable tradeoff for the experiment: keep the modem deterministic, keep the station logic visible on Linux, and make the whole system easy to debug when something goes wrong.
The Interesting Part
The result is a TinyGS station where the radio modem is almost disposable.
A cheap CH32V003 and an SX1278 module sit at the RF edge. Linux handles the
network, schedules, credentials, Doppler math, logging, and packet forwarding.
The interface between them is a plain serial AT protocol that can be inspected with
minicom, scripted with Python, or replaced by another implementation later.
That is the useful property of the design: the complicated parts stay where they are observable, and the timing-sensitive parts stay where they are small.