← Sunil Khorwal

Decoding CTS-SAT-1 FrontierSat: Reading Every Byte of a Days-Old CubeSat

Published on May 6, 2026

CTS-SAT-1 (callsign FrontierSat, NORAD 98318) is a 3U CubeSat built by students at the University of Calgary through the CalgaryToSpace program. It launched on May 3, 2026 — three days ago as I write this. Frames appeared on SatNOGS shortly after launch, and this post covers how I decoded all 41 telemetry fields in the beacon frame, the three wrong interpretations I corrected along the way, and how a firmware contributor's decoder independently validated the result.


The frame structure

FrontierSat beacons on 437.385 MHz. Each frame is 134 bytes, with an optional 4-byte CRC32 appended by the AX.100 transceiver making some frames 138 bytes. The layout is straightforward:

The firmware source is public: firmware/Core/Inc/comms_drivers/comms_tx.h in the CalgaryToSpace/CTS-SAT-1-OBC-Firmware repository on GitHub. Having the struct definition meant I could name every field precisely rather than guessing from observed values alone.


97 frames, three wrong interpretations

The SatNOGS archive had 97 unique frames showing RTC timestamps of April 1 and April 3, 2026. These are actually in-orbit frames from after the May 3 launch — the OBC real-time clock had not yet been synchronised, so it still displayed the date it was last set before the satellite was integrated into the launch vehicle. The team was still working on establishing two-way contact to uplink a time correction. Bytes are not self-documenting, and even with a named struct it is easy to misread fields whose units or encoding are not obvious. Here are the three that took the most work to fix.

Wrong interpretation #1 — uptime in milliseconds, not seconds

The first field after the CSP header is uptime_ms — a 32-bit unsigned integer. I initially read it as seconds. A frame with uptime = 5,272,020 then reported an OBC uptime of 61 days. Another showed 2,430 days — six and a half years. The satellite had not existed that long.

The _ms suffix in the firmware struct was the clue. Divide by 1,000: 5,272,020 ms = 87.9 minutes. 2,430,020,000 ms = 28.1 days — which, combined with in-orbit OBC reboots, is plausible once you notice the EPS has a separate uptime counter explained below.

This applies to last_uplink_ms too (milliseconds since the last received command), and the beacon interval instance — 21 seconds between beacons, not 21 milliseconds.

Wrong interpretation #2 — unix_epoch_time_ms is a u64, not two u32 fields

Bytes 19–26 hold the OBC wall-clock as milliseconds since the Unix epoch. That is a 64-bit value. Reading it as a single u8 (little-endian) gives timestamps around 1,775,000,000,000 ms, which resolve to April 2026 — not the actual current date, but the last time the OBC clock was set before launch integration.

Before I found the firmware struct, I was splitting this 8-byte region into two 32-bit fields. The low 32 bits varied frame to frame. The high 32 bits were a constant 413 across every single frame. A constant that big looked like a firmware build ID or sequence number — I labelled it firmware_id = 413 and moved on. It is not a firmware ID. It is the number of 232-millisecond epochs elapsed since 1970 up to 2026, and it is constant because all 97 frames were recorded within a span of three days.

Wrong interpretation #3 — EPS uptime is not a voltage

Bytes 39–42 hold a 32-bit unsigned integer that reads 207,000–235,000 in the April 1 session and 214,000–215,000 in the April 3 session. Those magnitudes looked like millivolts — a bus voltage in the 207–235 V range, far too high. I labeled the field bus_voltage_mv and flagged it as anomalous.

The firmware struct named this field eps_uptime_sec: the EPS (Electrical Power System) uptime in seconds. The EPS has its own dedicated power rail and does not reset when the OBC reboots. In some frames the OBC had been up for 881 seconds — about 15 minutes — while the EPS showed 214,000 seconds — about 2.5 days. This gap means the OBC had rebooted while the EPS stayed powered, a normal occurrence in early orbit operations. This relationship is encoded as a computed instance in the Kaitai Struct file (obc_rebooted: eps_uptime_sec > uptime_ms / 1000 + 60), letting ground operators detect OBC reboots directly from the beacon stream without any additional telemetry.


What the data actually showed

With the correct field interpretation, the 97 frames paint a consistent picture of the satellite in its early orbit:

Field Observed range Notes
beacon_count1–10,000+increments every ~21 seconds
unix_epoch_time_ms~1,775 × 109 s equivalentOBC RTC stale — shows April 2026, the last pre-launch sync date
eps_battery_voltage_mv15,907–16,002 mV4S LiPo, 97–100% charge
eps_battery_percent97–100%0 in sessions where EPS reporting inactive
eps_battery_temp0_cc0x7FFF (constant)sensor broken on this EPS model — confirmed by team
eps_battery_temp1_cc814–1133 cC8.1–11.3 °C in early orbit
obc_temp_cc300–2000 cC3–20 °C; varies with orbital thermal environment
eps_channels_enabled0x000000A3channels 7/5/1/0 enabled — constant
eps_fault_count3782 (constant)accumulated since first power-on
reboot_reason6brownout reset (confirmed via firmware enum)
time_sync_source5EPS RTC (confirmed via firmware enum)

The battery pack is a 4-cell (4S) LiPo; 15.9–16.0 V corresponds to a nearly full charge (nominal 14.8 V, max 16.8 V), consistent with the solar panels keeping the battery topped up in early orbit. The time_sync_source = eps_rtc and gnss_rx_mode = disabled confirm the team had not yet established two-way contact to synchronise the clock — the OBC RTC still shows the pre-launch date.


Kaitai Struct — 41 fields, 0 unparsed bytes

The complete frame is described as a .ksy file covering 40 sequential fields and 12 computed instances. The flat layout (no nested types) keeps it simple and readable. In the Kaitai Web IDE, both a 134-byte and a 138-byte frame (with CRC32 stripped) parse cleanly with zero unparsed bytes remaining.

Kaitai Struct Web IDE parsing CTS-SAT-1 FrontierSat beacon frame — full field list
All 40 sequential fields parsed in the Kaitai Web IDE — sync magic, CSP header, OBC timing, EPS telemetry, and beacon message
Kaitai Struct Web IDE showing computed instances — unix epoch, battery voltage, OBC temperature for CTS-SAT-1
Computed instances panel: unix_epoch_s = 1775849885 (OBC clock shows 2026-04-01 — stale pre-launch RTC), battery 16.0 V, OBC temp 12.25 °C, 0 unparsed bytes

A few things worth noting about the .ksy:


Cross-validation with the firmware author's decoder

On May 4, 2026 — one day after launch — parker-research, a contributor to the CTS-SAT-1-OBC-Firmware repository, submitted MR #517 to the satnogs-decoders repository, adding a ksy/frontiersat.ksy file. The field names in their submission are identical to the names in the firmware struct I had derived independently. This was a strong cross-check: arriving at the same field map through two independent paths confirms the layout.

Their MR adds value our flat decoder does not have: eight rich enumerations that decode every observed state value. A selection:

Their documentation also explicitly notes that eps_battery_temperature_0_cc is "broken on our EPS model" — the official explanation for why we always see 0x7FFF in that field.


What's open

CTS-SAT-1 is in orbit as of this writing. The first in-orbit frames from SatNOGS observers will be a different dataset to the ground tests — the GNSS receiver will be active, temperatures will swing with orbital period, and the EPS will be running from solar panels rather than bench power. The decoder is ready for it.

73, Sunil