Say which check failed, and read a byte from either end

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
This commit is contained in:
The Dust Council 2026-09-07 21:36:35 -07:00
parent 706c632f47
commit dfea5cb2f2
7 changed files with 275 additions and 36 deletions

View file

@ -941,3 +941,83 @@ def test_the_gate_still_keeps_noise_out_when_there_is_no_signal():
peak = float(np.percentile(smooth, 99.99))
over = (smooth > a._noise_gate(smooth, quiet, peak)).mean()
assert over < 0.01, f"{over:.1%} of pure noise cleared the gate"
# ---------------------------------------------------------------------------
# Which end of a byte goes first
# ---------------------------------------------------------------------------
def reversed_bytes(bits):
"""The same message with each byte sent the other way round."""
return "".join(bits[i:i + 8][::-1] for i in range(0, len(bits), 8))
@pytest.mark.parametrize("order", ["as written", "least significant first"])
def test_a_message_is_read_from_whichever_end_of_a_byte_it_arrives(order):
frame = a.tower_frame(0x1A2B, 21.5, 48, "A")
sent = frame if order == "as written" else reversed_bytes(frame)
iq = keyed_at(sent, pwm_pairs(sent))
got = a.readings_from(iq, 250_000.0)
assert [r.sensor for r in got] == ["1A2B"], f"{order}: heard {got}"
def test_reading_a_byte_backwards_keeps_its_parity_and_breaks_its_sum():
"""The signature that names this fault, which is why it is worth having.
Reversing the bits of a byte does not change how many are set, so odd
parity survives it; a checksum does not. A message read from the wrong
end therefore shows every parity holding and the sum failing, on every
copy -- which is a fingerprint rather than a guess.
"""
frame = a.tower_frame(0x1A2B, 21.5, 48, "A")
backwards = reversed_bytes(frame)
plain = [int(backwards[i:i + 8], 2) for i in range(0, len(backwards), 8)]
assert all(a.parity8(byte) == 1 for byte in plain[2:-1])
assert (sum(plain[:-1]) & 0xFF) != plain[-1]
def test_only_one_of_the_two_orders_is_ever_reported():
"""Otherwise one message would corroborate itself and the rule that the
thinly checked models depend on would protect nothing."""
frame = a.frame_609(0x5C, 4.2, 80)
found = a.candidates("0" + frame)
assert len(found) == 1
# ---------------------------------------------------------------------------
# Saying which check failed, not that one did
# ---------------------------------------------------------------------------
def test_the_closest_framing_says_which_check_held_and_which_did_not():
frame = a.tower_frame(0x1A2B, 21.5, 48, "A")
good = a.near_misses("1111" + frame)[0]
assert good.read and good.score == len(good.checks)
assert dict(good.checks)["sum"] and dict(good.checks)["parity"]
assert good.hex.startswith("DA 2B")
assert "7 bytes at bit 4" in good.describe()
def test_a_broken_checksum_is_reported_as_a_broken_checksum():
frame = list(a.tower_frame(0x1A2B, 21.5, 48, "A"))
frame[-1] = "1" if frame[-1] == "0" else "0" # the checksum byte
miss = a.near_misses("1111" + "".join(frame))[0]
assert dict(miss.checks)["parity"] is True
assert dict(miss.checks)["sum"] is False
assert not miss.read
def test_a_burst_of_nonsense_reports_the_checks_all_failing():
rng = np.random.default_rng(3)
bits = "".join(rng.integers(0, 2, 60).astype(str))
misses = a.near_misses(bits)
assert misses and all(not m.read for m in misses)
def test_the_hex_of_what_was_read_is_reported_so_it_can_be_worked_out_by_hand():
"""Because the alternative is asking somebody to send a photograph of a
display, and because the bytes are what any question about a format is
actually about."""
frame = a.tower_frame(0x0C41, 3.2, 91, "B")
miss = a.near_misses("1111" + frame)[0]
assert len(miss.hex.split()) == 7
assert all(len(byte) == 2 for byte in miss.hex.split())