Their sensors are on the air, five of them, and every message was arriving
intact. The bytes they sent, recovered from their own capture:
A7 1B 44 A6 09 4B 00 channel B id 271B 22.7 C 38%
F9 35 44 2B 09 C9 6F channel A id 3935 22.5 C 43%
21 E5 84 1E 0A 9F 51 channel C id 21E5 31.1 C 30% battery low
F9 7D 44 28 09 50 3B channel A id 397D 23.2 C 40%
0D D6 44 A9 09 CF A8 channel C id 0DD6 23.1 C 41%
Every checksum correct, every message type 0x04, every reading plausible. The
framing was right, the bit offset was right, the byte order was right, the
pulses had been recovered perfectly for days. One bit of convention was
wrong: the parity in the top bit of each payload byte is even, and this
required it to be odd. Twenty payload bytes across five independent messages,
every one of them even, which is not something twenty bytes do by chance.
That is the whole fault. Everything else changed in this and the two commits
before it was real and worth doing, and none of it was why nothing decoded.
Three things follow.
The five messages are now a test, checked byte for byte against the weather
they carry. They are worth more than everything else in that file put
together: every other test there puts a reading in through an encoder written
from the same description as the decoder, so the two agree by construction and
agree about anything they are both wrong about -- which is exactly what
happened. An encoder tested against its own decoder cannot find a fault in
the description they share, and no amount of it would ever have found this.
The emptiness check earns its place now. Odd parity rejects a byte of all
zeroes; even parity accepts one, so a run of silence read as zeroes satisfies
both the parity and a sum of zero, and the only thing standing between that
and a display full of sensors is the test that some byte is non-zero. It was
there for tidiness and is now load-bearing; the comment says so.
And the readings of a burst are tried in order and the search stops at the
first that yields anything, rather than pooling them. Half a dozen readings
at two byte orders is sixteen times the chances for a coincidence to satisfy a
twelve-bit check, and sensors that were not there began appearing in the
invented garden the moment the alternatives went in -- caught by the test that
asks whether everything heard is something that exists. Stopping early costs
nothing: a burst that reads correctly the ordinary way never reaches the
alternatives, and one that does not reaches them exactly as before.
Full suite 2351 passed, checked against three more deliberately broken builds.
Sixty seconds of receiver noise yields nothing and eight hundred seconds of
the invented garden yields no sensor that is not there. Built as
2026-09-07_05.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
Their diagnosis came back and the radio end is faultless. Every burst is
textbook: four sync pulses at 612 microseconds, then 216 and 403 microsecond
data pulses with gaps that complete each bit period, six of them a second,
sliced cleanly at the right lengths. The pulses this had been failing to read
were being recovered perfectly all along and the fault is in the arithmetic
after them.
Two things follow from that, and neither is a guess about their sensors.
The first is that "nothing framed" is not a diagnosis, it is the absence of
one, and it was all this could say. A message whose checksum holds and whose
parity fails is a different fault from one where neither holds, and both
differ again from a burst that never lined up on a byte boundary -- three
faults, three fixes, one message. So the diagnosis now reports the closest
framing it found, which of its checks held, and the bytes themselves in
hexadecimal, which is what any question about a format is actually about and
saves asking somebody to read numbers off a screen.
The second is that this still assumed something it had no business assuming:
which end of a byte goes down the air first. Both orders are tried now and
the checksums say which, like everything else here. That one has a
fingerprint worth knowing and worth having said in the manual: reversing the
bits of a byte does not change how many of them are set, so odd parity
survives it and a checksum does not -- a message read from the wrong end shows
every parity holding and every sum failing, on every copy, which is a
signature rather than a coincidence.
Also fixed, and found by building their burst from the timings they sent: a
real transmitter closes the last bit with a terminating pulse, so a message of
fifty-six bits arrives as sixty-one pulses rather than sixty. Nothing here
had ever seen one, the simulator not sending it, and every test in this file
was therefore one pulse short of what comes off the air. It happens to be
handled correctly, which is luck rather than design, so it is now what the
tests are written against.
Full suite 2339 passed. Sixty seconds of receiver noise still yields nothing,
and four hundred seconds of the invented garden still yields no sensor that is
not there, both rechecked after adding the second byte order. Built as
2026-09-07_04.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
Still nothing, with rtl-433 receiving the same sensors on the same aerial from
a different receiver. That settles where the fault is not: not the aerial,
not the sensors, not the band. So the sensible thing is to stop differing
from the configuration known to work on that aerial, and this differed from it
in three ways, every one of them mine.
It tuned a quarter of a megahertz to one side of 433.92 and shifted the signal
back in software, to keep the receiver's own spike off a signal that works by
being switched off. That is a real effect and avoiding it this way is a bad
trade: at the sample rate this ran at, the shift is followed by a filter, and
a filter narrow enough to reject the spike is narrow enough to lose a
transmitter that has drifted -- or to lose the signal outright if a dongle
presents its samples the other way round, which is not a thing to depend on.
The spike is a steady addition to the envelope and a burst rises clear of it.
Tuning straight at the sensors now, which is what the established tools do.
It sampled at a megasample a second where a quarter of one is plenty: the
shortest pulse these send is two hundred microseconds, which is fifty samples
at the lowest rate a dongle will do. The extra rate bought nothing but the
room for that filter to exist in. At 250 kS/s nothing after the mixer is
narrower than the band, so the offset now does nothing at all whatever it is
set to, and says so.
And it turned on the RTL2832's digital gain control along with the tuner's.
The two pump: the gain winds up through the silence between one burst and the
next, lifting the noise towards the signal and squeezing the very difference
the burst detector works on. It matters here in a way it does not for
aircraft, where a frame is found by correlating a preamble over microseconds
rather than by comparing a burst with the quiet around it.
Three more faults found while going over the rest of it, all the same mistake
in different clothes -- treating the middle of a distribution as though it
were the quiet part of one.
The check that skips an empty block measured the peak against the median. A
recording that is mostly burst measures its own burst against its own burst,
finds no difference and is discarded as silence, which is what happened to
every short capture. The gate's scatter had the same trouble one level down
and could come out above the peak, which is the one setting that cannot be
right, so it is now capped below it.
And the rule deciding where one message ends keyed on the middle gap in a
burst. Where a one is drawn as a gap three times a zero and most of the bits
are zeroes, the middle gap is the short one, twice it still falls inside the
message, and every one-bit ended a burst -- the message coming apart into
pieces of three pulses. It keys on the widest gap now, which is a fact about
the message rather than about the data it happened to carry.
The pieces of a message are also put back together after being sliced rather
than before. Grouping has to be tight, because the group is what fixes the
threshold and a group holding two sensors of unequal strength fixes it on the
louder; but the gaps inside one message run from two hundred microseconds on
the newer sensors to four thousand on the oldest, and a grouping tight enough
for the first tears the second into a bit at a time. So each piece is
measured at its own amplitude and joined to its neighbours afterwards, and
anything that comes out longer than the longest message there is gets cut at
its largest gaps.
Measured rather than argued: across four hundred and eighty combinations of
pulse and gap timing, 464 now read where 417 did; across twenty-one gap-keyed
combinations, 18 where 12 did; and eighty seconds of receiver noise still
yields nothing at all.
--from-iq FILE reads a saved capture instead of the receiver, so a recording
made where the aerial is can be worked on anywhere, as many times as it takes.
Everything downstream of the dongle is the real thing, which is what tells a
receiver problem and a decoder problem apart. --save-iq writes the settings
beside the samples, a file of raw samples with no record of its rate being
unreadable by anything. The invented garden now waits like a dongle instead
of running as fast as the machine allows, which it should have done from the
start: --seconds meant nothing against it and a capture came out fifty times
too large.
Full suite 2331 passed; this work checked against sixteen deliberately broken
builds, two of which it survived until the tests were made to catch them, and
one change was removed for being unable to earn a test at all. Built as
2026-09-07_03.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
Reported as reading nothing at all with several sensors in range. Two faults,
either of which is enough on its own, and both of them things I assumed rather
than checked -- this was written against messages I generated myself and never
against a sensor.
The first is the threshold. Bursts were found by setting one level per second
of band, halfway between the noise floor and the loudest thing in that second.
That is the obvious way to write it and it is wrong: a sensor on the windowsill
and a sensor at the end of the garden differ by forty decibels, so a level set
halfway to the near one sits above everything the far one ever does. The far
ones do not come through weakly, they vanish -- and vanish only while the near
one is transmitting, which is as confusing a symptom as radio produces. A
block with one loud sensor in it yielded exactly one sensor however many were
out there.
Finding bursts is now two passes. The first asks only where anything happened
at all and asks it against the noise -- the bottom fifth of the second, which
is noise however busy the rest was, and which does not move when something
loud arrives. Whatever clears that is grouped into regions, and the second
pass re-thresholds each region against its own high and low. Every sensor is
sliced at its own amplitude. Six sensors spanning eighty times in strength
now all come back from one second of band.
The second fault is that the slicer knew how a bit is drawn. It read a pulse
by comparing it with the gap that followed, which is right when the gap is the
complement of the pulse so that every bit takes the same time, and wrong when
the gap is a fixed spacer: a two-hundred-and-twenty microsecond pulse against
a two-hundred microsecond spacer is the longer of the two and reads as a one,
which is the wrong bit, and then every message fails its checksum having said
nothing about why. Nothing is assumed now -- not which of the pulse and the
gap carries the bit, not whether the gap is a complement or a spacer, not
which of long and short means one. The same burst is read half a dozen ways
and the checksums say which reading it was, at most one being able to satisfy
one. Copies are counted per message rather than per reading, or two readings
of one burst would corroborate each other and the rule protecting the two
thinly-checked models would protect nothing.
Both were caught the same way: by measuring, rather than by reading the code
again. A thousand seconds of the invented garden still yields no sensor that
is not there, and reception of the ones that are is up by a quarter, because
bursts that used to be masked now decode.
And, because none of the above should have needed me: `bandsaunter weather
--diagnose` prints each second taken apart stage by stage -- the noise, the
level a burst must clear, the loudest thing in the block, then every burst
with the lengths of its pulses and gaps and whatever was made of them. Those
lengths are the useful part: a real message has two or three of them and
nothing in between, which says at a glance whether the trouble is the radio or
the arithmetic. At the end it says which of five things it was: nothing
arriving, nothing above the noise, something never keyed, bursts that framed
as nothing, or messages that framed and arrived only once. `--save-iq FILE`
keeps the raw samples for whatever that cannot settle.
Full suite 2286 passed; the new work checked against six deliberately broken
builds. Built as 2026-09-07_02.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016PsWPTweCT6pwxKngvVxcg
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