Request for UWE-3 Telemetry Protocol Specification

Hello @bali

I am currently working on building a telemetry decoder for the UWE-3 satellite. As part of this effort, I have been reverse engineering the telemetry frames using data available from the satnogs database.

So far, I have retrieved telemetry data from the satnogs DB, filtered and reconstructed valid frame fragments, Identified frame structure and signature and successfully decoded several fields such as battery voltage, bus voltage, and system counters

While the decoding pipeline is working, I would like to validate my assumptions and ensure correctness before proceeding further.

I wanted to check if you could share the official UWE-3 telemetry protocol specification (frame format, field definitions, scaling, etc.) or point me to any available documentation.

This would greatly help in verifying field mappings, correctly interpreting sensor data and completing a reliable open decoder for the community

Thanks in advance for your support.

Best regards,
Sunil

as a follow-up i wanted to share the parameters and assumptions I am currently using for decoding uwe-3 telemetry, so you can review or correct them if needed.

Data Source i used satnogs db (telemetry observations for norad - 39446)

Frame Reconstruction

  1. Extracted from fragmented packets
  2. Frame length: 41 bytes
  3. Frame signature: 0x53 0x20 0x64 0x64
  4. First byte treated as packet counter

Field Mapping (current understanding)

  1. Byte 0: Packet counter
  2. Bytes 1–2: Signature (0x53 0x20)
  3. Bytes 3–4: Protocol marker (0x64 0x64)
  4. Bytes 6–7: Battery voltage (uint16, big endian)
  5. Bytes 7–8: Bus voltage (uint16, big endian)
  6. Bytes 9–10: Tick / counter (uint16)
  7. Bytes 11–12: Secondary counter / uptime
  8. Bytes 13–14: Mode / state field
  9. Bytes 15–17: Temperature values (raw, int8 / uint8 mix)
  10. Remaining bytes (18–40): Not yet fully identified

Filtering / Validation

  1. Battery voltage constrained to 3000–9000 mV
  2. Bus voltage observed around 8450 mV
  3. Duplicate frames removed
  4. Only frames matching dominant length retained

The decoding pipeline is producing consistent values, but I would like to confirm -

  1. Correct byte offsets for each field
  2. Scaling factors for voltage and temperature
  3. Meaning of remaining payload bytes

any confirmation or documentation would be extremely helpful

hello @sunilkhorwal , i dont have much information about this sat. but maybe you can get some information from UWE-4 here

73!

1 Like

Hi Sunil,

You may ask DK3WN Mike.
He created an offline decoder [uwe3_online.zip (84.4 KB)] back in the days when UWE-3 was new in orbit.

Daniel

2 Likes

Hello @DK3WN

I’m currently rebuilding a telemetry decoder for UWE-3 using satnogs data and was referred to you by daniel.

I’ve been able to reconstruct frames and reverse engineer much of the structure, but I’d like to validate a few points to ensure correctness.

From my current analysis:

  1. Frame length appears to be 41 bytes
  2. Header looks like: [counter][0x53 0x20][0x64 0x64]
  3. Battery voltage: bytes 6–7 (uint16)
  4. Bus voltage: bytes 7–8 (uint16)

I had a few questions:

  1. Is the frame structure and header interpretation correct?
  2. Are the byte offsets for battery and bus voltage accurate?
  3. What scaling factors are used for voltage and temperature fields?
  4. Do you have mapping or documentation for the remaining payload bytes (18–40)?

I’ve already built a working pipeline that reconstructs frames from satnogs data and decodes several fields, but I want to ensure everything is aligned with the original specification.

If it helps, I’d be happy to share my current decoded output or findings.

Thank you for your time, and I really appreciate your work on the original decoder.

Sunil

thanks, that’s helpful!, I’ll take a look and compare its telemetry format to what I’ve reverse engineered for 3. Hopefully there’s some overlap in structure or field definitions.

Thanks @bali & @dl7ndr

Hi @DK3WN

from 129,000 packets recorded in the satnogs database, I was able to reconstruct 270 valid frames and decode them. The layout below has been cross-validated against the UWE-4 KSY decoder,

Bytes Field Type Unit
0 counter u1 -–
10 beacon_rate_s u1 s
11–13 uptime_s u24le s
18 obc_temp_c s1 °C
19 exterior_temp_c s1 °C
20 batt_a_soc u1 %
21 batt_b_soc u1 %
22–23 batt_a_voltage_mv u2le mV
24–25 batt_a_current_ma s2le mA
26 batt_a_temp_c s1 °C
27–28 batt_b_voltage_mv u2le mV
29–30 batt_b_current_ma s2le mA
31 batt_b_temp_c s1 °C
32–33 power_mw u2le mW
34–39 panel temps (±X/±Y/±Z) s1Ɨ6 °C
9 vals_out_of_range u1 count
Bytes Field Type Unit

Multi-byte values are interpreted as little-endian.

Bytes 15–17 remain unidentified (possibly part of a timestamp or subsystem status bitmap).

Please let me know if this aligns with the original specification or if any corrections are needed.

Sunil 73!

1 Like

attaching Kaitai Struct file

uwe3_bin_and_key_for_kaitai_validation.zip (2.9 KB)

2 Likes

Great job!

Decode_Frame

{
ā€œdest_callsignā€: ā€œDD0UWEā€,
ā€œsrc_callsignā€: ā€œDP0UWGā€,
ā€œsrc_ssidā€: 0,
ā€œdest_ssidā€: 0,
ā€œctlā€: 3,
ā€œpidā€: 240,
ā€œbeacon_header_flags1ā€: 9,
ā€œbeacon_header_flags2ā€: 65,
ā€œbeacon_header_packet_idā€: 32,
ā€œbeacon_header_fm_system_idā€: 100,
ā€œbeacon_header_fm_subsystem_idā€: 100,
ā€œbeacon_header_to_system_idā€: 195,
ā€œbeacon_header_to_subsystem_idā€: 11,
ā€œbeacon_header_apiā€: 33,
ā€œbeacon_payload_commandā€: 2,
ā€œbeacon_payload_vals_out_of_rangeā€: 255,
ā€œbeacon_payload_beacon_rateā€: 39,
ā€œbeacon_payload_uptimeā€: 142692,
ā€œbeacon_payload_subsystem_statusā€: 24996,
ā€œbeacon_payload_obc_tempā€: -30,
ā€œbeacon_payload_exterior_tempā€: -53,
ā€œbeacon_payload_batt_a_state_of_chargeā€: 100,
ā€œbeacon_payload_batt_b_state_of_chargeā€: 100,
ā€œbeacon_payload_batt_a_voltageā€: 4216,
ā€œbeacon_payload_batt_a_currentā€: 7,
ā€œbeacon_payload_batt_a_tempā€: 42,
ā€œbeacon_payload_batt_b_voltageā€: 4360,
ā€œbeacon_payload_batt_b_currentā€: 0,
ā€œbeacon_payload_batt_b_tempā€: 42,
ā€œbeacon_payload_power_consumptionā€: 307,
ā€œbeacon_payload_panel_pos_x_tempā€: 47,
ā€œbeacon_payload_panel_neg_x_tempā€: 50,
ā€œbeacon_payload_panel_pos_y_tempā€: 45,
ā€œbeacon_payload_panel_neg_y_tempā€: 70,
ā€œbeacon_payload_panel_pos_z_tempā€: 37,
ā€œbeacon_payload_panel_neg_z_tempā€: 51,
ā€œbeacon_payload_crcā€: 0
}

test file.zip (254 Bytes)

versus

While the temperature values are just 1 byte signed integers (for UWE-4) and are displayed as they are parsed, for UWE-3 it seems to be different.

Did you miss the RTC?

2 Likes

Thanks for catching that, you are right, after comparing against DK3WN, UWE-3 battery and panel temperatures are signed byte divided by 2 (0.5°C resolution), not raw s8 like UWE-4. fixed in both the KSY (added _degc instances with /2.0 scaling) and the Python decoder. obc_temp and exterior_temp stay as plain s8 - they can go negative (eclipse interior/exterior) and the raw values match the documented ranges directly.

On the RTC: I think DK3WN is displaying the PC receive timestamp, not decoding it from the frame?

Screenshot shows 2020/04/29 but UWE-3 was last active around 2016, so that date can only come from the host clock.

All 33 payload bytes are accounted for and none of them encode a timestamp. Am I missing something, or is that just DK3WN logging when the frame was decoded?

2 Likes

I’ve used the frame from 2020-04-29 20:20:10 UTC from JA0CAW.
So the displayed RTC (2020-04-29 20:27) matches pretty good.
Must be somewhere hidden in the frame.
The export all file shows the last frame from 2020-05-01.

1 Like

Thanks for the details! I pulled the exact frame from SatNOGS, JA0CAW, 2020-04-29T20:20:10Z:

From telemetry API -
{ā€œnextā€:null,ā€œpreviousā€:null,ā€œresultsā€:[{ā€œsat_idā€:ā€œAAXX-7141-8998-4196-8344ā€,ā€œnorad_cat_idā€:39446,ā€œtransmitterā€:ā€œā€,ā€œapp_sourceā€:ā€œsidsā€,ā€œdecodedā€:ā€œā€,ā€œframeā€:ā€œ888860AAAE8A6088A060AAAE8EE103F00941206464C30B2102FF27642D0200A46154E2CB6464781007002A081100002A33012F322D46253300ā€,ā€œobserverā€:ā€œJA0CAW-PM97nwā€,ā€œtimestampā€:ā€œ2020-04-29T20:20:10Zā€,ā€œversionā€:ā€œā€,ā€œobservation_idā€:null,ā€œstation_idā€:null,ā€œassociated_satellitesā€:[]}]}

888860AAAE8A6088A060AAAE8EE103F00941206464C30B21
02FF27642D0200A46154E2CB6464781007002A081100002A
33012F322D46253300

I scanned every 4-byte window in both endiannesses and there is no Unix timestamp for 2020 anywhere in the frame. The byte 0x5E (which would need to appear in any 2020 Unix timestamp) is simply not present.

my guess: DK3WN is displaying the PC’s real-time clock in the ā€œRTCā€ field, it’s showing what time it was on your computer when you decoded the frame. a 7-minute offset between frame reception (20:20) and decode time (20:27) is exactly the kind of drift you’d expect from downloading the frame and opening DK3WN. If the RTC were decoded from the frame, it would match within a second or two, not 7 minutes.

Do you know if the DK3WN source uses GetSystemTime or similar for that field? or is there a UWE-3 protocol document that describes an on-board RTC in the housekeeping payload?

Also, do you happen to have a way to reach DK3WN directly, email or otherwise? Since you’ve been testing with his decoder, he’d be the quickest way to confirm whether that RTC field is from the frame or the PC clock. I’ve tagged him in the thread but haven’t heard back yet.

Got the email. I reached out to DK3WN directly at mail@mike-rupprecht.de, hopefully he can confirm where that RTC field comes from.

can you share here step by step, how as noob can use your decoder, with example. thank you!

Happy to help! Here are two ways to use it depending on how technical you want to get.

**Option A — Easiest: Kaitai Web IDE (no install needed)
**
Go to ide.kaitai.io in your browser

  1. Click ā€œLoad .ksyā€ and upload uwe3.ksy

  2. Click ā€œLoad binaryā€ and upload your raw frame file

  3. The tree on the right shows every decoded field instantly

To get a raw frame from SatNOGS:

  • Go to db.satnogs.org/api/telemetry/?satellite=39446

  • Copy the frame value (it’s hex)

  • Paste it into a hex editor or use Python to convert to binary:

open("frame.bin", "wb").write(bytes.fromhex("YOUR_HEX_HERE"))

Option B — Python script (recommended for batch decoding)

Requirements: Python 3, and the requests library

pip install requests

Test it immediately with built-in sample frames:

python3 uwe3_frame_analyzer.py --sample

You should see output like:

batt_a_temp_c: 21.0
batt_b_temp_c: 21.0
batt_a_voltage_mv: 4216
power_mw: 307
...

Fetch real frames from SatNOGS (free account needed):

  1. Sign up at satnogs.org

  2. Get your API token from your profile page

  3. Run:

python3 uwe3_frame_analyzer.py --token YOUR_TOKEN_HERE

Decode from a local JSON file (if you saved frames from the API):

python3 uwe3_frame_analyzer.py --file frames.json


Both files (uwe3.ksy and uwe3_frame_analyzer.py) are attached to the post. Any questions just ask!

Also — I’m working on submitting this to the official satnogs-decoders repository as a PR, so it will eventually be available there too. Will update the thread once it’s merged!


uwe3_ksy_and_frame_analyzer.zip (6.5 KB)

1 Like

That’s right.

But I’ve found that the bytes 33, 34 and 35 are counting from 1900-01-01 00:0x on in 4 minutes steps.

x depends on the value of the 32th byte.

If you add 1 to ? (a? cd ef), DK3WN’s decoder goes up by 4 minutes.

DK3WN didn’t let me in on his programming.
I also wasn’t able to find anything about the UWE-3 telemetry protocol.

1 Like

This is a brilliant find, thank you @dl7ndr !, the 1900 epoch with 4-minute steps makes perfect sense, that’s essentially a compressed NTP timestamp.

I pulled the actual JA0CAW frame from satnogs and tried to locate bytes encoding 2020-04-29 20:24 (the nearest 4-minute boundary before 20:27). The expected 3-byte big-endian counter would be F1 6B 12, i checked but those bytes don’t appear anywhere in the 57-byte frame.

could you clarify which byte numbering you used? Specifically:

  1. Are you counting from 0 or 1?
  2. Starting from the very first AX.25 byte, or from somewhere inside the frame (e.g. after the callsigns, or after the beacon header)?
  3. And which specific frame did you test on? (satnogs observation id or raw hex would help)

Once we know the exact offsets, we can add the RTC as a proper computed instance in the .ksy file. This would be the last missing piece.

88 88 60 AA AE 8A 60 88 A0 60 AA AE 8E E1 03 F0 09 41 20 64 64 C3 0B 21 02 FF 27 64 2D 02 00 A4 61 54 E2 CB 64 64 78 10 07 00 2A 08 11 00 00 2A 33 01 2F 32 2D 46 25 33 00

I was counting from 1 on.

1 Like

Thanks for catching that, you were right. I was scanning for Unix timestamps and missed it entirely because the epoch is 1900, not 1970.

Verified on the JA0CAW frame (2020-04-29T20:20:10Z): bytes 32–35 (1-indexed) are A4 61 54 E2, which as a little-endian NTP value gives 3,797,180,836. Subtract the NTP Unix offset (2,208,988,800) and you get 2020-04-29 20:27:16 UTC, exactly what DK3WN showed. The ā€œ4-minute stepsā€ you described is the upper byte changing by one LSB every 256 seconds (~4.27 min), which makes perfect sense.

The KSY and Python decoder are now corrected: subsystem_status + unknown + obc_temp in that region are gone, replaced by a single beacon_payload_rtc (u4le) field with a beacon_payload_rtc_unix computed instance (NTP āˆ’ 2,208,988,800). Panel axis order is also fixed to āˆ’X/+X/āˆ’Y/+Y/āˆ’Z/+Z to match DK3WN.

One thing still unclear: the byte immediately after the RTC (frame byte 36, 1-indexed)

I’m calling it state for now. In this frame it’s 0xCB.

Do you happen to know what it represents?

2 Likes

frame_hex

888860AAAE8A6088A060AAAE8EE103F00941206464C30B2102FF27642D0200A46154E2CB6464781007002A081100002A33012F322D46253300

Decoded Frame -

counter: 9
command: 0x02
vals_out_of_range: 255
beacon_rate_s: 39
uptime_s: 142692
rtc_ntp: 3797180836
rtc_utc: 2020-04-29 20:27:16 (DK3WN showed 20:27)
state: 0xCB
batt_a_soc: 100%
batt_b_soc: 100%
batt_a_voltage_mv: 4216
batt_a_current_ma: 7
batt_a_temp_c: 21.0 (raw=42, DK3WN=21)
batt_b_voltage_mv: 4360
batt_b_current_ma: 0
batt_b_temp_c: 21.0 (raw=42, DK3WN=21)
power_mw: 307
obc_temp_c: 47
panel_neg_x_c: 25.0
panel_pos_x_c: 22.5
panel_neg_y_c: 35.0
panel_pos_y_c: 18.5
panel_neg_z_c: 25.5
panel_pos_z_c: 0.0

2 Likes