Read the weather sensors on 433 MHz, and let them be given names
A consumer weather station is two things. The display on the kitchen wall is one of them; the other is a plastic box on a fence post that says what it can see every sixteen seconds, in the clear, to anyone who happens to be listening. This reads the box. A section of its own, like the aircraft one, and for the same reason: it does not fit through the scanner. A sensor message is a burst of a carrier switched on and off, a fifth of a second long, and the scan path is a squelch and a recorder -- it would record the bursts as clicks in a WAV file and decode nothing. `bandsaunter weather` listens, `bandsaunter readings` reads a log back, `bandsaunter sensors` says what is out there. Item 6 in the main menu is the same thing without a command line. Five families: the Tower 592TXR, the 5-in-1, the 6045M lightning detector, the 609TXC and the 606TX. Temperature, humidity, wind speed and direction, rainfall, strike counts, how far off the storm is, and battery state from all of them. Every one is implemented from its published description and checked against frames built from the same description, which proves the framing, the parity, the checksums and the arithmetic and is not the same as having held one of each. The naming is the point. A sensor broadcasts an identity, and that identity is a number that came out of a hat in a factory; it tells one sensor from another and is no use at all for telling which is which. So press n while listening: the display comes down, the sensors are listed, you name one, and it goes back up, with the receiver running throughout. That is the moment it is possible -- the sensor is on the screen saying 3.1 degrees, and the person watching is the one who knows that the cold one is the shed. An hour later it is a list of hexadecimal again. Names are written the instant they are given rather than at exit, to a neighbouring file renamed over the old one, and one given before a sensor has ever been heard waits under its identity and moves across when the first message says which model it is. Four things keep the neighbours' doorbells off the display. The checks the message carries; a second copy, for the two models that carry only one byte of check between them; a plausibility range, because a checksum can be satisfied by a message the hardware could not send; and where in the burst the message sits. That last one is the one that is easy to miss: a seven-byte message read out of the front of a real eight-byte one is made of that message's own payload bytes, whose parity is already correct, so the parity bits contribute nothing and one byte of sum is all that is left -- and corroboration cannot help, the three copies being identical. What gives that window away every time is that it ends a whole byte before the burst does. The Atlas is nine bytes like the lightning detector and lays its payload out differently, so every decoder insists on a message type it knows. Anything else that frames correctly is reported with its identity and no weather, because wrong weather under somebody's sensor name is a worse answer than none. ism.py now delegates to this rather than keeping a second implementation of the tower sensor, which fixes the channel letters -- A is 3, B is 2, C is 0, and there is no D -- and the battery bit, which is set while the battery is good. The two thinly-checked models are not reported from a scan at all: a scan hears one burst, and they need two. The option menus are now handed the module that owns the options rather than importing the aircraft one, so one set of screens drives both sections and will drive a third. 169 new tests, checked against nineteen deliberately broken builds; two of the tests were too weak to notice their own mutation and were rewritten. Full suite 2252 passed. Built as 2026-09-07_01. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
This commit is contained in:
parent
f01de4117f
commit
65cc03b78d
18 changed files with 6006 additions and 123 deletions
309
README.md
309
README.md
|
|
@ -7,6 +7,11 @@ built-in US band plan — and it sweeps them, stops on anything above the noise
|
|||
floor, records it, and works out what kind of signal it was. CW/Morse is
|
||||
decoded to text.
|
||||
|
||||
Two things get sections of their own, because neither fits through a scan:
|
||||
[the aircraft overhead](#aircraft) on 1090 MHz, drawn on a moving map, and
|
||||
[the weather sensors](#weather-sensors-on-433-mhz) on 433 MHz, which you can
|
||||
give names to as they arrive.
|
||||
|
||||
Two programs: `bandsaunter` scans, and
|
||||
[`saunterbrowse`](#browsing-what-you-recorded) reads back what it collected —
|
||||
transcripts, identifications and playback, in one screen.
|
||||
|
|
@ -240,6 +245,8 @@ bandsaunter # the menus: set up and scan
|
|||
bandsaunter scan -b 2m -b marine-vhf # band-plan presets
|
||||
bandsaunter scan -r 144M-148M -r 420M-450M # your own ranges
|
||||
bandsaunter scan -b 2m --simulate # try it without hardware
|
||||
bandsaunter adsb --window # aircraft overhead, on a map
|
||||
bandsaunter weather # the weather sensors on 433 MHz
|
||||
```
|
||||
|
||||
## Two ways to drive it
|
||||
|
|
@ -253,11 +260,18 @@ table of settings, so neither can offer something the other cannot.
|
|||
2 Band plan 107 US presets
|
||||
3 Settings record no limit, hang 6s, squelch +12 dB, keep voice, cw
|
||||
4 Saved settings and profiles
|
||||
5 Aircraft (ADS-B) listen on 1090 MHz, draw where they went
|
||||
6 Weather sensors listen on 433 MHz, name what is out there
|
||||
h Help
|
||||
s Start scanning
|
||||
q Quit
|
||||
```
|
||||
|
||||
Items 5 and 6 are sections of their own, with their own options and their own
|
||||
menus, because neither fits through a scan: ADS-B is a megabit a second and a
|
||||
scan channel is 12.5 kHz wide, and a weather sensor message is a burst of a
|
||||
carrier switched on and off that a scan would record as clicks.
|
||||
|
||||
Settings are grouped, show their current value against the built-in default,
|
||||
and carry their own help:
|
||||
|
||||
|
|
@ -1784,30 +1798,299 @@ form of every one.
|
|||
copy between machines. API keys are deliberately **not** kept there — see
|
||||
[Knowing the leg for certain](#knowing-the-leg-for-certain).
|
||||
|
||||
## Meters and weather sensors
|
||||
## Weather sensors on 433 MHz
|
||||
|
||||
Two things on the ISM bands are worth naming rather than reporting as hex.
|
||||
A consumer weather station is two things. The display on the kitchen wall is
|
||||
one of them; the other is a plastic box on a fence post that says what it can
|
||||
see every sixteen seconds, in the clear, on 433.92 MHz, to anyone who happens
|
||||
to be listening. `bandsaunter weather` reads the box.
|
||||
|
||||
```
|
||||
bandsaunter weather listen, and name things as they arrive
|
||||
bandsaunter weather --simulate a garden of sensors that are not there
|
||||
bandsaunter readings --csv turn a log into something plottable
|
||||
bandsaunter sensors what is out there, and what it is called
|
||||
```
|
||||
|
||||
It is a section of its own, like the aircraft one, and for the same reason:
|
||||
it does not fit through the scanner. A sensor message is a burst of a carrier
|
||||
switched on and off, a fifth of a second long, and the scan path is a squelch
|
||||
and a recorder — it would record the bursts as clicks in a WAV file and decode
|
||||
nothing.
|
||||
|
||||
```
|
||||
╭──────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ 433.92 MHz 6 sensors 148 messages 41:02 weather_2026-09-07_11_31_04.jsonl │
|
||||
│ n to name (2 unnamed) control-C to stop │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────╯
|
||||
# name id model readings batt msgs ago
|
||||
1 back fence 1A2B Tower 592TXR temperature 8.4 C humidity 88% ok 41 0:07
|
||||
2 mast 0777 5-in-1 06014RM wind 14.2 km/h wind from 248° WSW ok 36 0:11
|
||||
rain 41.4 mm temperature 8.1 C
|
||||
humidity 90%
|
||||
3 greenhouse 0C41 Tower 592TXR temperature 15.9 C humidity 61% ok 39 0:04
|
||||
4 unnamed 0311 Lightning 6045M temperature 8.6 C humidity 87% ok 24 0:19
|
||||
strikes 4 storm 19 km
|
||||
5 shed 5C 609TXC temperature 3.1 C humidity 94% low 21 0:12
|
||||
6 unnamed 93 606TX temperature -18.2 C ok 19 0:22
|
||||
```
|
||||
|
||||
### Naming things while they arrive
|
||||
|
||||
A sensor broadcasts an identity, and that identity is a number that came out
|
||||
of a hat in a factory — or a different number out of the same hat the next
|
||||
time the batteries were changed. It is enough to tell one sensor from another
|
||||
and it is no use at all for telling which is which. `1A2B` is not a place.
|
||||
|
||||
So **press `n` while listening**. The display comes down, the sensors are
|
||||
listed with numbers, you pick one and type a name, and it goes back up. The
|
||||
receiver keeps running throughout: a slow typist loses a few seconds of
|
||||
weather and nothing else.
|
||||
|
||||
That is the moment it is possible to do. The sensor is on the screen saying
|
||||
3.1 degrees, and the person watching is the one who knows that the cold one is
|
||||
the shed. An hour later it is a list of hexadecimal again.
|
||||
|
||||
Names can also be given from the command line, before or after anything has
|
||||
been heard:
|
||||
|
||||
```
|
||||
bandsaunter weather --name 1A2B="back fence" --name 5C=shed
|
||||
bandsaunter sensors --name 0311="lightning detector" --note 0311="south gable"
|
||||
bandsaunter sensors --forget 93
|
||||
```
|
||||
|
||||
A name given before the sensor has ever been received waits under its
|
||||
identity alone — nothing yet knows which model it is, because that is in the
|
||||
message and there has not been one — and moves across the moment the first
|
||||
message arrives. So naming the garden from a list and then listening works,
|
||||
and the display says `shed` from the first reception rather than listing the
|
||||
shed and the sensor on it as two separate things.
|
||||
|
||||
They live in `sensors.yaml` beside the settings, one entry per sensor, meant
|
||||
to be opened and edited by hand — it is a list of things in a garden, and
|
||||
typing them is often quicker than tagging them one at a time off the air.
|
||||
Names are written the moment they are given rather than when the program
|
||||
exits, because it exits on control-C; and they are written to a neighbouring
|
||||
file which is then renamed over the old one, so a machine losing power halfway
|
||||
through leaves either the old names or the new ones and never half of each.
|
||||
|
||||
Nothing is ever dropped for being stale. A sensor whose battery ran out two
|
||||
winters ago keeps its name, because the alternative is that putting a battery
|
||||
back in loses it.
|
||||
|
||||
### What it reads
|
||||
|
||||
Five families, each with its own framing and its own check:
|
||||
|
||||
| model | bytes | what it says |
|
||||
|---|---|---|
|
||||
| Tower 592TXR / 06002RM | 7 | temperature, humidity |
|
||||
| 5-in-1 06014RM / VN1TXC | 8 | wind speed, wind direction, rainfall / wind speed, temperature, humidity |
|
||||
| Lightning 6045M | 9 | temperature, humidity, strike count, how far off the storm is |
|
||||
| 609TXC | 5 | temperature, humidity |
|
||||
| 606TX | 4 | temperature |
|
||||
|
||||
Battery state comes from all of them. The 5-in-1 has more to say than fits in
|
||||
one message and alternates two of them, so its last message is always either
|
||||
the wind and the rain or the temperature and the humidity, never both — the
|
||||
display keeps the newest value of each quantity rather than the newest
|
||||
message, so the line is the whole sensor rather than half of it flickering.
|
||||
|
||||
**What is not read.** The Atlas, the 986 and 515 fridge thermometers, the
|
||||
00275rm room monitor and the 899 standalone rain gauge. They are on the same
|
||||
band and are not decoded. A message from one of them whose framing happens to
|
||||
match is reported as an unknown message type with its identity and nothing
|
||||
else, rather than guessed at — because knowing that something is out there
|
||||
transmitting, with an identity that stays the same, is worth a line on its
|
||||
own. `--no-unknown` leaves them off.
|
||||
|
||||
These formats are implemented from their published descriptions and are
|
||||
checked against frames built from the same descriptions. That proves the
|
||||
framing, the parity, the checksums and the arithmetic; it is not the same as
|
||||
having held every one of these sensors.
|
||||
|
||||
### Why nothing false gets through
|
||||
|
||||
433 MHz is a crowded band. Doorbells, car keys, tyre-pressure sensors, garage
|
||||
doors and the neighbours' weather station are all on it, and a decoder that
|
||||
looks at every bit offset of every burst will find a message in noise if it is
|
||||
allowed to. Four things stop it.
|
||||
|
||||
**The checks the message carries.** The three newer models have an eight-bit
|
||||
sum plus odd parity in the top bit of every payload byte — twelve to fourteen
|
||||
bits of check on a message of seven to nine bytes.
|
||||
|
||||
**Arriving twice.** The two older models carry one byte of check between them,
|
||||
which is one false message in two hundred and fifty-six, and that is not a
|
||||
rate a display can be trusted at. So those two are only believed when the same
|
||||
message arrives twice. It costs nothing: these sensors send everything three
|
||||
times in a row, for exactly this reason.
|
||||
|
||||
**A plausibility range.** A checksum can be satisfied by a message the
|
||||
hardware could not have sent. Nothing outside −40 to 70 °C, 0 to 100% or a
|
||||
wind the anemometer cannot physically report is accepted.
|
||||
|
||||
**Where the message sits.** This is the one that is easy to get wrong. A
|
||||
seven-byte message read out of the front of a real eight-byte one is made of
|
||||
that message's own payload bytes, whose parity is *already correct* — so the
|
||||
parity bits contribute nothing at all and what is left is one byte of sum.
|
||||
Corroboration does not help either, because the copies of a message are
|
||||
identical, so a coincidence in one copy is a coincidence in all three. What
|
||||
gives that window away every time is that it ends a whole byte before the
|
||||
burst does. A real message begins after the sync and runs to the end.
|
||||
|
||||
### Off the air
|
||||
|
||||
Three things happen between the aerial and a bit, in this order.
|
||||
|
||||
The receiver is tuned a little to one side of 433.92 MHz, because every
|
||||
RTL-SDR puts a spike of its own at whatever it is tuned to, and a spike
|
||||
sitting on top of a signal that works by being switched on and off is the one
|
||||
thing that stops it being off. The sensors are shifted back to the middle in
|
||||
software, which puts the spike out at the edge instead. `--offset 0` tunes
|
||||
straight at them, which is worth trying once to see what the spike was costing.
|
||||
|
||||
Then a running average of the complex samples, long enough that its first null
|
||||
lands on the spike. It is a crude filter and a deliberately crude one: what it
|
||||
has to reject is one tone at a frequency this end chose.
|
||||
|
||||
Only then is the magnitude taken. Filtering before detection rather than after
|
||||
is what keeps the neighbours — a doorbell, a tyre sensor, a car key — from
|
||||
adding themselves to the envelope of the sensor.
|
||||
|
||||
Slicing the envelope into bits never measures anything against a clock. The
|
||||
newer sensors vary the length of the pulse and keep the gaps even; the two
|
||||
older ones keep the pulse even and vary the gap. Both readings of the same
|
||||
pulses are tried and the checksums say which it was — rather than deciding
|
||||
from the timings, which is guessing, and wrong on a weak burst where the edges
|
||||
have moved. A transmitter running ten per cent fast is read correctly and
|
||||
never noticed, which matters: these transmitters are unlocked and drift tens
|
||||
of kilohertz and a few per cent of rate with the temperature. An outdoor
|
||||
sensor in January is not the one that was on the fence in July.
|
||||
|
||||
### Afterwards
|
||||
|
||||
```
|
||||
sensors heard
|
||||
name id model ch msgs every battery last heard
|
||||
back fence 1A2B Tower 592TXR A 148 16 s ok 23:14:07
|
||||
mast 0777 5-in-1 06014RM A 131 18 s ok 23:14:02
|
||||
greenhouse 0C41 Tower 592TXR B 140 17 s ok 23:14:05
|
||||
shed 5C 609TXC 76 30 s low 23:13:58
|
||||
|
||||
what they said
|
||||
sensor reading first last lowest highest
|
||||
back fence temperature 8.4 C 6.1 C 5.9 C 9.2 C
|
||||
humidity 88% 94% 86% 94%
|
||||
mast wind 14.2 km/h 3.1 km/h 0.0 km/h 38.6 km/h
|
||||
wind from 248° WSW 202° SSW
|
||||
rain 41.4 mm 43.9 mm 41.4 mm 43.9 mm
|
||||
```
|
||||
|
||||
The two tables are split on purpose. The first is about reception — who, how
|
||||
often, how well — and is the one to look at when something is missing; `every`
|
||||
is the honest measure of an aerial, because these transmit on a fixed cycle,
|
||||
so thirty seconds from a sensor that sends every sixteen means half of them
|
||||
are being missed. The second is about the weather, and is the one to look at
|
||||
when nothing is.
|
||||
|
||||
There is no average. These arrive every sixteen seconds when the sensor is in
|
||||
range and not at all when it is not, and rain and cold both shorten the range
|
||||
of a 433 MHz transmitter — so the mean of what was received is the mean of a
|
||||
sample whose gaps are themselves the weather, and it would read like a number
|
||||
when it is not one. A bearing gets no lowest or highest either: north is 0 and
|
||||
also 360.
|
||||
|
||||
`--csv`, or `bandsaunter readings --csv` afterwards, writes a column per
|
||||
quantity and a row per reading, with the sensor's name in the second column
|
||||
and the unit in the heading rather than beside every number — a column of
|
||||
"21.5 C" is text, and a column of 21.5 is a temperature.
|
||||
|
||||
The log keeps the raw bytes of every message underneath whatever was made of
|
||||
them, because the message is the evidence and the rest of the line is an
|
||||
opinion about it. A later version of this program that reads a model this one
|
||||
cannot will be able to go back through old logs and read them properly.
|
||||
|
||||
Readings are converted once, on the way in, to degrees Celsius, kilometres an
|
||||
hour, millimetres and kilometres — different models report in different units,
|
||||
and the tower sends Celsius while the 5-in-1 and the lightning detector send
|
||||
Fahrenheit. The log holds the converted value, so `--units imperial` changes
|
||||
only what is shown and can be changed afterwards on an old log.
|
||||
|
||||
### Every weather option
|
||||
|
||||
| option | flag | default | what it does |
|
||||
|---|---|---|---|
|
||||
| Receiver | `--device` | 0 | which receiver, when more than one is plugged in |
|
||||
| Gain | `--gain` | auto | tuner gain in dB, or automatic |
|
||||
| Sample rate | `--rate` | 1.024 MS/s | how fast to sample; 250 kS/s is the minimum |
|
||||
| Sensors on | `--frequency`, `--freq` | 433.92 MHz | where the sensors transmit |
|
||||
| Tuning offset | `--offset` | 250 kHz | how far to one side of them to tune |
|
||||
| Invent a garden | `--simulate` / `--no-simulate` | no | six sensors that are not there |
|
||||
| Listen for | `--seconds` | until stopped | how long before stopping |
|
||||
| Write a log | `--log` / `--no-log` | yes | one line of JSON per message |
|
||||
| Print every message | `--messages` / `--no-messages` | no | a stream of lines instead of a table |
|
||||
| Keep on screen for | `--hold` | 1800 s | how long a sensor stays after its last message |
|
||||
| Show readings in | `--units` | metric | metric or imperial, for the display and the export |
|
||||
| Only named sensors | `--only-named` / `--all-sensors` | no | ignore anything without a name |
|
||||
| Show unreadable models | `--unknown` / `--no-unknown` | yes | list sensors whose model cannot be read |
|
||||
| Report at the end | `--report` / `--no-report` | yes | print what each sensor said |
|
||||
| Also write a spreadsheet | `--csv` / `--no-csv` | no | CSV beside the log |
|
||||
|
||||
`--name ID=NAME` is not a setting: it names a sensor and is repeatable.
|
||||
|
||||
Saved in `weather.yaml` beside the other settings, from the menu's **s** or by
|
||||
hand. `bandsaunter readings` also takes `--sensor NAME` to narrow a log to one
|
||||
sensor, and `--no-report` to write the spreadsheet and say nothing.
|
||||
|
||||
### Without a sensor
|
||||
|
||||
`--simulate` puts six sensors on a fence that does not exist — one of every
|
||||
model — transmitting real messages with real checksums, keyed on and off as a
|
||||
real one does, through the real filter, the real slicer and the real decoders.
|
||||
Nothing touches the receiver. The naming, the log, the report and the export
|
||||
can all be tried before an aerial exists.
|
||||
|
||||
The weather moves at roughly a real afternoon's pace, which is slower than it
|
||||
sounds like it should be: about a degree an hour of drift, wind gusting around
|
||||
a mean, rain falling a tenth of a millimetre at a time. A garden that swung
|
||||
six degrees a minute would exercise everything downstream perfectly well and
|
||||
would look, to anyone running `--simulate` to see what the program does, like
|
||||
a program that cannot read a thermometer.
|
||||
|
||||
### If nothing is heard
|
||||
|
||||
These are a few milliwatts. A quarter-wave whip for 433.92 MHz is 17 cm of
|
||||
wire, and the stock telescopic aerial set to about that length works well.
|
||||
Indoors, behind a wall, with the dongle in the back of a machine, is usually
|
||||
the problem. Try `--gain 40` if the automatic gain control is not finding
|
||||
them, and `--messages` to watch individual receptions arrive while moving the
|
||||
aerial about.
|
||||
|
||||
## Meters on 900 MHz
|
||||
|
||||
A scan of 902–928 MHz that turns up a burst gets it named rather than reported
|
||||
as hex.
|
||||
|
||||
```
|
||||
915.0 MHz decoded: electricity meter 12345678 reading 987654
|
||||
433.92 MHz decoded: AcuRite sensor 1234 temperature 21.5 C humidity 48% channel A
|
||||
433.92 MHz decoded: Tower 592TXR 1A2B temperature 21.5 C humidity 48% channel A
|
||||
```
|
||||
|
||||
**Utility meters.** The Itron ERT modules fitted to electricity, gas and water
|
||||
meters across North America broadcast their reading every thirty seconds or so
|
||||
on 902–928 MHz, in the clear, so a van can drive past and read a street. The
|
||||
message says which meter, what kind, what the register reads, and whether the
|
||||
tamper switches have been tripped.
|
||||
tamper switches have been tripped. It is not guessed at: the message carries a
|
||||
16-bit BCH check and nothing is reported that has not satisfied it.
|
||||
|
||||
**AcuRite sensors.** The 433.92 MHz outdoor sensors sold with every consumer
|
||||
weather station send temperature, humidity, battery state and a channel letter
|
||||
every sixteen seconds.
|
||||
|
||||
Neither is guessed at: a meter message carries a 16-bit BCH check and a sensor
|
||||
message a checksum and four parity bits, and nothing is reported that has not
|
||||
satisfied them. Both are implemented from their published descriptions and
|
||||
checked against frames built from the same descriptions — which proves the
|
||||
framing and the arithmetic, and is not the same as having held a meter.
|
||||
**Weather sensors.** A scan of 433 MHz that catches one of these names it the
|
||||
same way, using the same decoders as
|
||||
[the weather section](#weather-sensors-on-433-mhz) — which is where they
|
||||
belong, because a scan catches one burst and the weather is a night of them.
|
||||
The two thinly-checked models are not reported from a scan at all: they need
|
||||
the same message twice before they are believed, and a scan hears one.
|
||||
|
||||
## Hex into words
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue