← Sunil Khorwal

Reverse-Engineering UWE-3 CubeSat Telemetry: Building an Open Decoder from Scratch

Published on February 25, 2026

UWE-3 (NORAD 39446) is a 1U CubeSat built by students at the University of Würzburg, Germany. Launched in November 2013, it transmitted housekeeping beacons on 437.385 MHz that were picked up by amateur radio operators around the world and archived on SatNOGS. The only publicly known decoder was a closed-source Windows VB6 application by DK3WN. This post documents how I reverse-engineered the frame format and built an open decoder.


Why bother?

The DK3WN decoder is a GUI application from the early 2010s. It requires Wine on Linux, a registered OCX component, and AGW Packet Engine. There is no command-line mode, no CSV export without clicking through menus, and no way to batch-process the hundreds of frames archived on SatNOGS. I wanted something scriptable and open.


Step 1 — Getting the raw frames

SatNOGS exposes a public API at db.satnogs.org. Querying for NORAD 39446 between 2013 and 2016 returned 270+ frames, each base64-encoded. A small Python fetcher pulled them all down and decoded them into raw bytes.

The first thing to check was frame length. The vast majority were 57 bytes — 15 bytes of AX.25 header, 1 byte PID, 8 bytes of beacon header, and 33 bytes of housekeeping payload. That 33-byte payload became the main target.

Raw UWE-3 frames from SatNOGS
Raw AX.25 frames fetched from the SatNOGS API

Step 2 — Mapping the payload

With 270 frames and no protocol document, the approach was empirical. Fields were identified by:

The layout that emerged:

Bytes Field Notes
0commandalways 0x02
1vals_out_of_rangesensor error count
2beacon_rateseconds between beacons
3–5uptime24-bit LE, seconds since boot
6uptime_padalways 0x00
7–10rtc4-byte LE NTP timestamp (seconds since 1900-01-01)
11statesatellite state byte
12–13batt A/B state of charge%
14–23battery voltages, currents, tempsA and B
24–25power consumptionmW
26obc_tempon-board computer temperature (direct °C)
27–32six panel temperatures−X, +X, −Y, +Y, −Z, +Z
33-byte housekeeping payload decoded
All 33 payload bytes shown with hex, decimal, and signed interpretations

This was encoded as a Kaitai Struct .ksy file, which compiles to parsers in Python, C++, Java, and others.

Python analyzer output showing UWE-3 telemetry
The Python analyzer decoding a real frame, showing scaled physical temperatures and the NTP timestamp correctly matching DK3WN

Step 3 — Running the original decoder to validate

To verify the field mapping, I needed to compare output against the DK3WN decoder on the same frame. Rather than installing Wine manually, I built a Docker container:

The decoded values matched on all numeric fields — uptime, voltages, currents, state of charge, power consumption.

Decoded UWE-3 telemetry values
Live decoded telemetry: battery_mv, bus_mv, temperatures, and counters

Step 4 — The temperature bug

A community member compared my decoder output against DK3WN side by side and spotted something immediately: every battery and panel temperature was exactly double the expected value. My decoder showed batt_a_temp = 42, DK3WN showed 21.

The fix: UWE-3 temperatures are not raw signed bytes like UWE-4. They are signed bytes divided by 2, giving 0.5°C resolution. So 42 raw → 21.0°C physical.

This applies to all battery and panel temperatures. obc_temp is exempt — it is a plain signed byte giving direct °C with no scaling.

The Kaitai Struct file was updated to store the raw s1 value and expose scaled _degc instances via /2.0.


Step 5 — The RTC field

The DK3WN decoder displays an "RTC" field showing 2020/04/29 20:27 alongside a frame received at 20:20:10 UTC. I initially dismissed this as the PC clock, because scanning for Unix timestamps in the frame turned up nothing. A forum member, dl7ndr, corrected this directly: the encoding is NTP, not Unix. NTP counts seconds since 1900-01-01, not 1970-01-01.

The frame (JA0CAW, 2020-04-29T20:20:10Z):

888860AAAE8A6088A060AAAE8EE103F0
0941206464C30B2102FF27642D0200A4
6154E2CB6464781007002A081100002A
33012F322D46253300
NTP timestamp hex bytes in UWE-3 frame
The NTP bytes A4 61 54 E2 correctly parsed in the Kaitai Web IDE hex viewer

Payload bytes 7–10 (frame bytes 31–34, 0-indexed) are A4 61 54 E2. As a 4-byte little-endian NTP value:

ntp = 0xA4 | (0x61 << 8) | (0x54 << 16) | (0xE2 << 24) # = 3,797,180,836
unix = ntp - 2_208_988_800 # = 1,588,192,036
# datetime.utcfromtimestamp(1_588_192_036) → 2020-04-29 20:27:16 UTC

DK3WN showed 20:27. The satellite RTC was 7 minutes 6 seconds ahead of the SatNOGS ground-station timestamp — consistent with a satellite clock that has drifted slightly but is tracking real time. The 7-minute gap that initially made this look like a PC download delay is actually satellite clock drift.

The upper byte of the NTP value changes by one LSB every 256 seconds (~4.27 minutes), which is what dl7ndr described as "4-minute steps". The .ksy now exposes beacon_payload_rtc (raw u32le NTP) and beacon_payload_rtc_unix (NTP − 2208988800) as a computed instance.

Final Result: Validation of the .ksy file

With the temperature offsets fixed and the NTP RTC fully mapped, the open-source Kaitai Struct parser now successfully extracts all telemetry from the raw payload. The image below shows the final validation.

Kaitai Web IDE final validation of UWE-3 telemetry
Final validation: The Kaitai Web IDE parsing the frame using the completed .ksy definition

What's open

All code and the .ksy file are available on request. If you have UWE-3 frames or know anything about the protocol, get in touch.

73, Sunil