Skip to content

How It Works

ha-taggle edited this page Nov 3, 2026 · 3 revisions

How it works

A friendly tour of what happens between the water pipe and your dashboard. No maths needed. The precise details are in PROTOCOL.md.

Signal flow: the meter and Taggle transmitter, through radio decoding, to your own meter's state, anonymous neighbourhood aggregates, MQTT and Home Assistant

Protocol summary

The supported receive path samples I/Q at 2.4 MS/s with a default SDR centre of 922.2 MHz, targeting a carrier near 921.995 MHz. It despreads a 4095-chip PN sequence, demodulates BPSK, decodes rate-1/2 convolutional coding and validates two CRC-8 checks. Clear 18/21-byte frames supply three 16-bit counters, giving two hourly pulse differences. The starting offset and litres per pulse require calibration. Encrypted 24-byte frames supply only the clear ID and RF diagnostics; ha-taggle does not decrypt usage.

Only the 18-byte clear format has field confirmation, on one Gladstone transmitter. The 21-byte clear and 24-byte encrypted paths have generated tests only.

1. The meter makes pulses

A mechanical water meter turns as water flows. A sensor near the dial notices each turn and sends a pulse to the Taggle transmitter. On the tested unit it's a thin cable to a pulse/Hall-effect style sensor. The transmitter counts the pulses. Its counter wraps and starts at an unknown value relative to the dial; litres per pulse are unknown too. ha-taggle uses differences for hourly usage, and meter_count adds the differences it recovers. Neither is the physical dial reading. See Calibration.

2. The transmitter broadcasts once an hour

On the supported receive path, the transmitter sends a short radio burst near 922 MHz, typically once an hour. Other Taggle products can use different channels or cellular radio. Taggle's SP3C datasheet describes this kind of device: battery-powered, one-way communication, direct sequence spread spectrum, generally hourly. The burst contains:

  • the transmitter's ID
  • an hourly sequence number
  • the last three counter values (in clear-format frames)
  • two checksums

Note

Why three counters? Each message repeats the previous two hours' counters. From three counters ha-taggle gets two hourly differences. So if one message is missed, the next one still covers that hour.

3. Spreading the signal out (DSSS)

The transmitter is tiny and battery-powered, so its signal is weak. Taggle uses DSSS (direct sequence spread spectrum) to get it through anyway.

Analogy: imagine whispering a message in a noisy room, but you've agreed with your friend on a long secret rhythm. Every bit of the message is tapped out using that rhythm. Your friend, who knows the rhythm, can pick it out of the noise. Everyone else just hears more noise.

The "rhythm" is a PN code (pseudo-noise code). Here it's 4,095 "chips" long for every single bit. It comes from a simple circuit called a linear-feedback shift register, and this particular kind is a maximum-length sequence. It looks random, but it's completely predictable. Multiplying the received signal by the same code "despreads" it and lifts it out of the noise. See DSSS on Wikipedia.

Note

Why is SNR measured "after despreading"? Before despreading, the signal can be buried below the noise. The SNR that ha-taggle reports is measured afterwards, which is why it doesn't match what you'd see on a waterfall display.

4. Flipping the wave (BPSK)

Each chip is sent by BPSK (binary phase-shift keying): the radio wave is either "normal" or "flipped upside down".

Analogy: a skipping rope swung in one direction means 0. Flip your wrist so it swings the opposite way, and that means 1.

ha-taggle locks on to the wave with a Costas loop, a standard way to track BPSK, and reads the flips. See phase-shift keying on Wikipedia.

5. Fixing errors (convolutional code + Viterbi)

Before sending, the transmitter adds extra "redundant" bits with a convolutional code. Every data bit becomes two coded bits, each mixed with the previous few bits.

Analogy: like spelling a name over the phone as "A for apple, B for bravo". Extra information lets the listener fix a word they misheard.

To undo it, ha-taggle uses the Viterbi algorithm. It works out the most likely original message even if a few bits were corrupted. If too many bits needed fixing (more than 12), the frame is rejected rather than trusted. See convolutional codes on Wikipedia.

6. Checking the message (CRC)

Each frame has two CRCs (cyclic redundancy checks): one protects the ID, one protects the whole frame.

Analogy: like the check digit on a credit card number. One small calculation tells you if any digit is wrong.

If either check fails, the frame is thrown away. A CRC catches accidents. It doesn't prove who sent the message. See CRC on Wikipedia.

7. Three kinds of frame

Frame What ha-taggle gets
18 bytes, clear ID, three counters, hourly usage, signal (field-tested on one unit)
21 bytes, clear Same (generated tests only)
24 bytes, encrypted ID and signal only. The body is never decrypted or attacked.

8. From counters to water use

  • Each counter is 16 bits, so it wraps from 65,535 back to 0. ha-taggle handles a single wrap between neighbouring counters.
  • Overlapping hours (each message repeats two old counters) are counted once.
  • A later frame can recover one missed transmission. Intervals still unrecovered are left as gaps; nothing is guessed.
  • "Today" adds intervals by the local date the interval ended. It's not a midnight-to-midnight dial reading.

9. Publishing to Home Assistant

Your meter's figures go to your MQTT broker with Home Assistant discovery messages, so the sensors appear by themselves. Other meters take the anonymous neighbourhood path: their IDs are scrambled, and only group figures over at least 3 qualifying meters are published.

Radio numbers at a glance

Parameter Value
Centre frequency (SDR) 922.2 MHz
Target carrier about 921.995 MHz
Sample rate 2.4 million samples/second
Modulation BPSK
Spreading 4,095-chip m-sequence per coded bit, about 4.8 Mchip/s
Error coding Rate-½, constraint length 7 convolutional code
Checks Header CRC + frame CRC-8 (polynomial 0x07)

Further reading

Clone this wiki locally