Skip to content

Add rtl_433 decoder (100 kbps Manchester) and firmware field analysis#59

Merged
andy778 merged 9 commits into
mainfrom
rtl433-decoder-100kbps
Jul 2, 2026
Merged

Add rtl_433 decoder (100 kbps Manchester) and firmware field analysis#59
andy778 merged 9 commits into
mainfrom
rtl433-decoder-100kbps

Conversation

@andy778

@andy778 andy778 commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add the rtl_433 decoder for the Uponor Clean 1 868 MHz inner/outer link, built from real RTL-SDR captures: 100 kbps line rate carrying software-Manchester data (raw 01->1, 10->0), ~40 bytes/packet, constant fd7a bababa 83 header, near-static telemetry sent as a two-packet exchange every ~62 s. Demods at 100 kbps, Manchester-decodes both bit phases and keeps the cleaner one, anchors on fd7a, emits header + raw payload hex. Ships disabled=1 until fields/CRC are pinned down.
  • docs/rtl433.md: capture procedure, empirical results, and a firmware section. Ghidra analysis of the MC9S08 flash shows the on-air byte layout is not in the CPU — it is only an SPI slave on a custom MC9S08<->8051 message bus; the nRF9E5's 8051 owns framing/Manchester. Firmware does give the decode targets (alarm-code and setpoint tables), so field mapping ([U6]) needs a display-correlated capture.
  • Add the Ghidra helper scripts used: DumpBytes, FindRefsInRange, ForceDecompile.
  • .gitignore ignores *.cu8 (large IQ captures stay local).

Test plan

  • Build inside an rtl_433 checkout (copy tools/rtl433/uponor_clean1.c into src/devices/, register in include/rtl_433_devices.h + src/CMakeLists.txt, cmake && make).
  • Run against a capture with -R <num> (decoder is disabled by default) and confirm it emits the fd7a bababa 83 header + payload hex.
  • Field semantics ([U6]) still require a display-correlated capture before enabling.

🤖 Generated with Claude Code

Add the rtl_433 decoder for the Uponor Clean 1 868 MHz inner/outer link, built
from real RTL-SDR captures: 100 kbps line rate carrying software-Manchester data
(raw 01->1, 10->0), ~40 bytes/packet, constant fd7a bababa 83 header, near-static
telemetry sent as a two-packet exchange every ~62 s. The decoder demods at
100 kbps, Manchester-decodes both bit phases and keeps the cleaner one, anchors
on fd7a, and emits header + raw payload hex until the fields and CRC are pinned
down. It ships disabled.

docs/rtl433.md records the capture procedure and empirical results, plus a
firmware section: Ghidra analysis of the MC9S08 flash shows the on-air byte
layout is not in the CPU (it is only an SPI slave on a custom MC9S08<->8051
message bus; the nRF9E5's 8051 owns framing/Manchester). Firmware does give the
decode targets (alarm-code and setpoint tables), so field mapping [U6] needs a
display-correlated capture. Add the Ghidra helper scripts used (DumpBytes,
FindRefsInRange, ForceDecompile).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread tools/rtl433/uponor_clean1.c Outdated
the display. It ships disabled until the fields and CRC are pinned down.

Flex-decoder equivalent for a first capture pass (100 kbps, then Manchester by
eye): rtl_433 -X 'n=uclean1,m=FSK_PCM,s=10,l=10,r=100'

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this if missing the -f parameter

Comment thread tools/rtl433/uponor_clean1.c Outdated
…pass

- The flex-decoder example in the header was missing the tuner frequency; add
  -f 868.20M (tune low so the carrier sits off the RTL-SDR DC spike).
- Document why we don't use rtl_433's native bitbuffer_manchester_decode():
  it aborts at the first Manchester violation and does a single phase, whereas
  real captures need violation tolerance + best-of-two-phase selection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@andy778

andy778 commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Addressed both review comments in 6748389:

  • tools/rtl433/uponor_clean1.c:64 (missing -f) — added -f 868.20M to the flex-decoder example (tune ~150 kHz low so the carrier lands off the RTL-SDR DC spike).
  • tools/rtl433/uponor_clean1.c:88 (native Manchester?) — rtl_433 does have bitbuffer_manchester_decode(), and it even uses the same convention (emits the second bit of each pair, so raw 01->1 / 10->0). I deliberately kept the custom pass: the native one aborts at the first Manchester violation and decodes a single phase alignment, whereas real captures of this link carry ~3–4% illegal pairs (noise) at an unknown bit phase, so we need violation tolerance + best-of-two-phase selection. Added an inline comment explaining this.

(Note: there's a stray empty pending review on this PR from a failed reply attempt — you may want to discard it in the UI; I wasn't permitted to delete it.)

Live capture showed the old command produced ~24 noise rows alongside the
real ~650-bit packets, and the default detector fragments the weaker
far-unit packet. -Y minmax selects the FSK peak detector (recovers 8 clean
~650-bit packets from a 4-event replay) and bits>=400 drops the sub-100-bit
noise rows. Also document why decode_mc is unusable here.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@andy778

andy778 commented Jul 2, 2026

Copy link
Copy Markdown
Owner Author

Improved the flex -X command based on the live capture output.

The old rtl_433 -f 868.20M -X 'n=uclean1,m=FSK_PCM,s=10,l=10,r=100' produced ~24 noise rows (len 2-72) mixed in with the real ~650-bit packets. Two changes clean this up:

  • -Y minmax — selects the FSK peak detector. The default detector fragments the weaker (far-unit) packet into several short rows; with minmax, a 4-event replay of the saved capture yields 8 clean ~650-bit packets (4 events × near+far).
  • bits>=400 — drops the noise rows. Real packets are ~635-654 raw bits; strays are <100 bits, so this filter removes them without touching real frames.

New command:

rtl_433 -f 868.20M -Y minmax -X 'n=uclean1,m=FSK_PCM,s=10,l=10,r=100,bits>=400'

Also documented (both in docs/rtl433.md and the decoder header) that the native decode_mc flex option does not work here — it aborts at the first Manchester violation and only tries phase 0, so these phase-offset, slightly-noisy packets collapse to empty rows. That's why the C decoder keeps its own violation-tolerant, two-phase Manchester pass.

andy778 and others added 6 commits July 2, 2026 15:28
The C decoder anchors on the Manchester-decoded fd 7a header. Map that same
sync into the flex command's raw (Manchester-encoded) domain: decoded fd 7a
encodes to raw 0x5559 0x9566, so preamble={32}55599566 aligns every packet to
the header. Verified against uclean_offset.cu8: all 8 packets anchor at fd7a
and line up byte-for-byte. (decode_mc still confirmed unusable - collapses to
0-4 bit rows - so it stays out of the flex command.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The exact 32-bit preamble match runs in the raw (Manchester-encoded) domain,
so one noise-flipped bit in the fd7a sync drops the whole packet. On a live
capture this loses the weaker far-unit packet of an exchange, showing one
message instead of two. Restore the no-preamble bits>=400 command as the
primary capture-pass recommendation (catches every message); keep preamble
only as an optional alignment aid for clean signals, with the tradeoff spelled
out. The C decoder does not have this problem: it Manchester-decodes with
tolerance first, then bit-searches fd7a.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The demodulator-level FSK_MC_ZEROBIT slicer decodes Manchester straight from
pulse timing, unlike the decode_mc post-filter (which aborts on the first
violation and is unusable here). With s=10 (half-bit) and invert to match the
01->1/10->0 convention, it prints the decoded bytes directly - the fd 7a ba ba
ba 83 header comes out verbatim - with full recall (all 8 packets of a 4-event
replay), and none of the exact-match preamble fragility. Make it the primary
flex first-pass command; keep FSK_PCM raw + optional preamble as the
inspect-raw-bits alternative. Verified against uclean_offset.cu8.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add section 0c with the two on-air frames actually recovered (poll + response),
byte-identical across ~35 min of capture. Documents the framing structure found
without display correlation: no counter/rolling code, a constant 0x1b type byte,
and a swapped pair of 3-byte node-ID fields (03 5b / 08 d2) between poll and
response. Gives future display-correlated captures a fixed diff target for [U6].

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Now that the link is confirmed Manchester (validated against the
FSK_MC_ZEROBIT flex demod), switch the r_device from FSK_PULSE_PCM with a
hand-rolled two-phase Manchester pass to FSK_PULSE_MANCHESTER_ZEROBIT. The
framework hands back already-decoded data bits, so drop the custom
uclean1_manchester()/uclean1_bit()/uclean1_bitsearch() helpers and the
illegal-pair quality gate. The decode fn now just anchors on fd7a with
bitbuffer_search (retrying inverted for polarity) and extracts header +
payload. Removes the mc_illegal output field.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Match the product's retail name "Uponor Clean 1", mapping spaces to hyphens
per the rtl_433 model-string convention (Brand-Model, no spaces).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@andy778 andy778 merged commit 7624b11 into main Jul 2, 2026
3 checks passed
@andy778 andy778 deleted the rtl433-decoder-100kbps branch July 2, 2026 14:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant