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:

CH32V003 and SX1278 at-os3 prototype wired as a LoRa modem

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 console showing backend retune, Doppler correction, and SX1278 register verification

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.

TinyGS console showing the PTGS node online after several days of operation

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.

TinyGS packet statistics and map for the PTGS node

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.