Objective
This started as a simple SDR question: which of the visible signals in the 700–870 MHz range are real communication targets, and which are just noise, overload, idle carriers, or decoys?
The final target turned out to be wireless M-Bus T1 traffic around 868.95 MHz. The useful part of the exercise was not only the decode itself, but the path to get there: several convincing false positives had to be eliminated before the real signal became obvious.
The goal was to build a passive receiver from first principles:
- find the right signal,
- characterize the modulation,
- demodulate it from IQ,
- recover the line coding,
- validate frames with strong CRCs,
- fingerprint the meters in range,
- and document where the work stopped.
No raw meter database, raw IQ capture, exact installation location, or full serial-number census is published here.
Setup
The receive setup was intentionally simple:
| Component | Detail |
|---|---|
| SDR | bladeRF 2.0 micro |
| Antenna | Discone antenna from an apartment balcony |
| OS | Kali Linux |
| SDR application | SDR++ |
| Analysis | Python with NumPy/SciPy |
| Cross-checks | rtl_433 and wmbusmeters |
The receiver was written as custom Python tooling rather than relying only on turnkey decoders. Existing tools were still useful as controls and sanity checks, but the main value came from understanding each stage of the chain.
The first lesson: the loudest signal was not the target
A large part of the work was spent proving what the signal was not.
Several strong or visually attractive signals appeared in the waterfall:
| Observation | Interpretation |
|---|---|
| Broad activity around the 700–800 MHz range | Cellular downlink-style broadband energy, not a narrowband target for this project |
| Activity around 865–867 MHz | EU UHF RFID reader-like channel activity |
| A strong 869.5 MHz two-tone signal | A non-packet 2-FSK toggle or local beacon/RFI-like signal |
| Short bursts around 868.95 MHz | The real wireless M-Bus T1/C1 target |
The 869.5 MHz signal was the trap. It was strong, clean, and visually satisfying. It also wasted time. Its shape was closer to a regular carrier toggle than framed packet data.
The actual smart-meter traffic was not the brightest feature. It was sparse, short, and easy to ignore unless you knew where wireless M-Bus T1/C1 normally lives.
False positives that looked real
Two false positives were useful because they forced stricter validation.
Phantom weather sensor
A weak-checksum OOK decoder produced a plausible-looking weather-sensor result on an RFID-like channel. The decoded values were physically absurd, and the checksum was too weak to trust. A weak checksum can be satisfied by noise often enough to fool you.
Phantom LoRa-style frames
A LoRa/CSS decoder was also tested against a signal that was actually 2-FSK. The result was exactly what should be expected from the wrong demodulator: plausible-looking garbage.
The rule that emerged from both cases was simple:
If the protocol has a strong CRC, require it. If it does not, treat the decode as a hypothesis, not a result.
That rule became the foundation for trusting the final wireless M-Bus output.
Wireless M-Bus signal characterization
The real target was wireless M-Bus T1/C1-style traffic near 868.95 MHz.
Measured characteristics:
| Property | Observation |
|---|---|
| Modulation | 2-FSK |
| Deviation | approximately ±41 kHz |
| Chip rate | 100 kchip/s |
| Line coding | 3-of-6 |
| Effective data rate | roughly 66.6 kbps |
| Burst length | approximately 2–4 ms |
| Framing | Format A |
| Integrity | CRC-16/EN-13757 |
The 3-of-6 line coding was a key fingerprint. Once the receiver started producing valid 3-of-6 codewords and CRC-valid frames, the signal stopped being a blob in the waterfall and became a real protocol.
Receiver chain
The receiver ended up as a staged pipeline:
IQ capture
-> autotune / channel selection
-> mix to baseband
-> low-pass filtering
-> burst detection
-> FM discriminator
-> clock recovery at 100 kchip/s
-> 3-of-6 decode
-> L-field / nibble alignment search
-> Format A deframing
-> CRC validation
-> DLL and application-layer parsing
Each stage was kept replaceable. That made debugging much easier: when a stage produced implausible output, it could be isolated without rewriting the full receiver.
Debugging arc
The receiver did not work cleanly on the first try. The useful bugs were these:
| Symptom | Cause | Fix |
|---|---|---|
| Very high codeword error rate | Signal captured too close to the band edge | Recenter the SDR on the target signal |
| Decode quality degraded toward the end of frames | Clock recovery drifted away from 100 kchip/s | Lock the baud/chip rate |
| Decoder found plausible but wrong starts | Start search was too narrow | Use a wider deterministic start search |
| Headers looked plausible but full-frame CRCs failed | Leading filler nibbles shifted byte assembly | Add nibble-offset search before deframing |
| High gain made results worse | Front-end compression and clipping | Reduce gain and optimize for SNR |
| Low absolute ADC level looked alarming | In-band SNR was still good | Trust SNR over raw amplitude |
The most important downstream bug was the nibble alignment issue. The CRC parameters were not the problem. The byte stream was simply assembled from the wrong offset.
What could be decoded
The final receiver produced CRC-validated wireless M-Bus headers and meter fingerprints.
At public-reporting level, the observed population included:
| Vendor family | Device class | Public-safe summary |
|---|---|---|
| Sappel / Diehl-family devices | Heat metering | Dominant population, clustered installation batch |
| Techem-family devices | Heat-cost allocation class | Longer frames, smaller population |
| Other meter family | Water-meter-like header observed | Header-level observation only |
The important point is not the exact serial numbers. The important point is that passive reception was enough to identify a local population of metering devices by manufacturer family, device type, version pattern, and transmission cadence.
That is already a meaningful visibility finding.
Application-layer ceiling
The receiver could parse standard wireless M-Bus structures, and synthetic standard OMS-style frames decoded correctly.
The local heat-meter payloads were different. They used a manufacturer-specific application layer. The payload looked structured and low-entropy, not like modern high-entropy encryption, but public tooling did not decode the consumption values.
wmbusmeters matched a related Diehl/Sappel-family driver, but the specific descrambling path did not fit the observed heat-meter format. The practical conclusion was:
Passive fingerprinting: achieved
Consumption decoding: not achieved
This distinction matters. The project demonstrated observability and fingerprinting. It did not demonstrate public recovery of consumption readings for this specific heat-meter format.
Privacy and publication boundary
Wireless M-Bus traffic is transmitted over RF, but public reception is not the same as publishing a deanonymized dataset.
For that reason, this post intentionally avoids publishing:
- exact receiver location,
- raw IQ captures,
- complete meter serial numbers,
- raw packet databases,
- full packet logs,
- complete per-meter transmission timelines,
- and any derived table that could act as a local meter census.
The research value is in the method and the validation process, not in naming every nearby device.
What passive collection can still reveal
Even without consumption decoding, the visibility risk is real.
| Passive observation | Why it matters |
|---|---|
| Manufacturer and device class | Reveals building metering technology |
| Repeated serial-like identifiers | Enables long-term device tracking |
| Transmission cadence | Shows device presence and periodic behavior |
| Installation clustering | Suggests batch deployment or shared infrastructure |
| Protocol family and media type | Helps target further research |
| CRC-valid identity headers | Stronger than guesses from signal shape alone |
The security finding is therefore not “all meter values are readable.” The more accurate finding is:
A passive SDR receiver can fingerprint nearby wireless M-Bus devices with high confidence, even when application-layer consumption values remain proprietary or obfuscated.
Reusable principles
This project produced a few lessons I would reuse in any SDR protocol hunt.
Strong CRCs beat attractive lies
Weak checksums and permissive parsers can manufacture believable decodes from noise. Strong CRCs are the line between “interesting” and “confirmed.”
The loudest signal may be irrelevant
The 869.5 MHz signal looked important because it was strong. It was not the useful target. Protocol knowledge mattered more than waterfall brightness.
Center the capture
A signal near a capture edge can lose a sideband and become undecodable in ways that look like algorithmic failure. Before blaming the decoder, fix the capture.
Gain for SNR, not for maximum amplitude
More gain is not automatically better. Compression and overload can create convincing artifacts. A real signal should scale sensibly as gain changes.
Match the demodulator to the modulation
A LoRa decoder applied to 2-FSK can still output bytes. That does not make the bytes meaningful.
Preserve uncertainty
A CRC-valid header is a fact. A guessed application-layer interpretation is not. Keeping those two categories separate prevents overclaiming.
Tooling produced
The project resulted in three useful tool categories:
| Tool type | Purpose |
|---|---|
| T1 receiver | IQ to burst detection, demodulation, 3-of-6 decoding, CRC validation, and header parsing |
| Differential analyzer | Compare grouped payloads over time and classify columns as constant, linear, step-like, or variable |
| Frame carvers | Extract candidate meter frames from larger captures for offline validation |
The tooling was useful not because it replaced established decoders, but because it made the decoding assumptions visible and testable.
Current status
Achieved:
- wireless M-Bus T1 signal identification,
- from-scratch 2-FSK receiver chain,
- 3-of-6 decoding,
- Format A framing,
- CRC-16/EN-13757 validation,
- passive meter fingerprinting,
- cross-checking against existing tools.
Not achieved:
- public recovery of consumption values from the observed manufacturer-specific heat-meter payloads.
Open path, for authorized/lab-owned meters only:
- long time-series capture,
- differential payload analysis,
- comparison against known physical meter readings,
- controlled validation before claiming a consumption register.
Conclusion
This was a good reminder that SDR research is mostly disciplined elimination.
The project did not start with a clean wireless M-Bus decode. It started with confusing waterfall blobs, false positives, wrong demodulators, overload symptoms, alignment bugs, and one very loud signal that was not the target.
The final result was stronger because of that path: a passive receiver that could identify and fingerprint nearby wireless M-Bus devices using strong protocol validation, while honestly documenting the boundary where proprietary application-layer decoding stopped.
The useful finding is not that consumption data was recovered. It was not.
The useful finding is that public RF metadata and CRC-valid identity headers were enough to build a high-confidence picture of nearby metering infrastructure.