Say how strongly each sensor is being heard, and how much of it arrives

Two numbers rather than one, because "how well is this sensor coming in" is
two questions and they can disagree in a way that is worth seeing.

The first is strength: how far the sensor's burst stood above the noise of the
second it arrived in, in decibels, on every reading and in the log and the
spreadsheet.  The burst detector already worked this out and threw it away --
it is the ratio the per-burst threshold is set from -- so this is carrying a
number through rather than measuring a new one.

What it is not is a power at the aerial, and the docstring says so where
somebody will read it.  A dongle has no reference level and, with the tuner
left on automatic, no fixed gain either; anything in dBm would be invention.
A ratio of two amplitudes off the same receiver in the same second is the
honest quantity, and it is enough for the three things anybody wants a signal
reading for: comparing two sensors now, watching one over an evening, and
pointing an aerial.  A fixed --gain makes it comparable between runs as well,
which the help now says.

It is coloured red, amber or green, it is on the live display as well as the
report, and it is kept on a narrow terminal when other columns are dropped --
because somebody moving a whip about while a number climbs is not doing it on
a wide window, and that is the most useful thing this does.

The second is the share of what a sensor sent that actually arrives, which
comes out of the timing for nothing.  These transmit on a fixed cycle, so the
shortest wait ever seen between two of a sensor's messages is that cycle, and
the average wait is the cycle divided by the fraction getting through: one
over the other is the fraction, with no need to know the model or how often it
is meant to speak.

Read together they say more than either does alone.  A strong signal with a
low share is interference or a collision rather than distance.  A weak signal
at a hundred per cent is a sensor at the edge that is getting through anyway
and is best left alone.

The strongest of the three copies of a message is the one reported, not the
first: they go out milliseconds apart and arrive at whatever the fading does
to each.  A reading with no strength -- an older log, a block with no
measurable noise floor to be a ratio to -- leaves the last known figure alone
rather than overwriting it with a zero.

Full suite 2369 passed, checked against five deliberately broken builds
including the one that reports decibels as a power ratio, which is off by a
factor of two and looks entirely reasonable.  Built as 2026-09-07_06.

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 23:50:37 -07:00
parent 872eadac37
commit 03d3600ecc
10 changed files with 375 additions and 24 deletions

View file

@ -1541,3 +1541,119 @@ def test_the_defaults_are_the_ones_the_established_tools_use():
assert options.rate == 250_000.0
assert options.offset == 0.0
assert options.frequency == a.ACURITE_HZ
# ---------------------------------------------------------------------------
# How well each sensor is being heard
# ---------------------------------------------------------------------------
def loud(strength, sensor=0x1A2B, at=1_000.0):
got = reading(sensor=sensor, at=at)
got.snr = strength
return got
def test_a_station_keeps_the_latest_the_best_and_the_worst_strength():
garden = wx.Garden()
for i, strength in enumerate((30.0, 18.0, 24.0)):
garden.add(loud(strength, at=1_000.0 + i * 16))
station = garden.stations["tower/1A2B"]
assert station.snr == 24.0 # the latest, which is what is shown
assert station.best_snr == 30.0
assert station.worst_snr == 18.0
def test_a_reading_with_no_strength_does_not_wipe_the_one_before_it():
"""Logs written before this existed read back with nothing in them."""
garden = wx.Garden()
garden.add(loud(30.0, at=1_000.0))
garden.add(reading(at=1_016.0)) # snr 0, meaning unknown
assert garden.stations["tower/1A2B"].snr == 30.0
def test_what_share_of_a_sensors_messages_is_arriving_is_worked_out():
"""The shortest wait between two of its messages is its cycle; the
average wait is that cycle divided by the share that gets through."""
garden = wx.Garden()
# Sends every 16 s; every other one arrives.
for i, at in enumerate((0.0, 16.0, 48.0, 64.0, 96.0, 112.0)):
garden.add(loud(30.0, at=1_000.0 + at))
station = garden.stations["tower/1A2B"]
assert station.closest == pytest.approx(16.0)
assert station.share == pytest.approx(16.0 / station.gap, abs=0.01)
assert 0.6 < station.share < 0.8
def test_a_sensor_heard_every_time_is_reported_as_heard_every_time():
garden = wx.Garden()
for i in range(8):
garden.add(loud(30.0, at=1_000.0 + i * 16.0))
assert garden.stations["tower/1A2B"].share == pytest.approx(1.0)
def test_the_share_says_nothing_until_there_is_something_to_say():
garden = wx.Garden()
for i in range(3):
garden.add(loud(30.0, at=1_000.0 + i * 16.0))
assert garden.stations["tower/1A2B"].share == 0.0
@pytest.mark.parametrize("strength,colour", [
(8.0, "red"), (18.0, "yellow"), (34.0, "green"),
])
def test_the_strength_is_coloured_so_an_aerial_can_be_aimed_by_it(strength,
colour):
assert colour in wx.signal_text(strength)
assert f"{strength:.0f} dB" in wx.signal_text(strength)
def test_no_strength_is_shown_as_nothing_rather_than_as_zero():
assert wx.signal_text(0.0) == ""
def test_a_signal_well_below_its_best_says_what_its_best_was():
"""Which is the difference between a sensor that has moved and one that
was always like that."""
assert "best 34" in wx.signal_text(20.0, 34.0)
assert "best" not in wx.signal_text(32.0, 34.0)
def test_the_strength_survives_the_log_and_reaches_the_spreadsheet(tmp_path,
book):
log = wl.WeatherLog(tmp_path / "weather_x.jsonl")
log.append(loud(27.4, at=1_000.0))
log.close()
back = wl.read_logs([log.path])
assert back[0].snr == pytest.approx(27.4)
where = wl.write_csv(tmp_path / "w.csv", back, book)
head, row = where.read_text().splitlines()[:2]
assert "signal (dB)" in head
assert row.split(",")[head.split(",").index("signal (dB)")] == "27.4"
def test_an_older_log_without_strengths_still_reads(tmp_path, book):
log = wl.WeatherLog(tmp_path / "weather_x.jsonl")
log.append(reading(at=1_000.0)) # nothing to record
log.close()
assert "snr" not in log.path.read_text().splitlines()[1]
assert wl.read_logs([log.path])[0].snr == 0.0
def test_the_report_says_how_strong_and_how_complete(book):
garden = wx.Garden()
for i in range(8):
garden.add(loud(31.0, at=1_000.0 + i * 16.0))
out = rendered(garden, book)
assert "31 dB" in out and "100%" in out
def test_the_display_keeps_the_strength_on_a_narrow_terminal(book):
"""It is what somebody moving an aerial about is watching, and they are
not doing it on a wide window."""
garden = wx.Garden()
now = time.time()
garden.add(loud(29.0, at=now))
for width in (68, 84, 120):
out = shown(garden, book, width=width, now=now)
assert "29 dB" in out, f"lost at {width} columns"
assert max(len(line) for line in out.splitlines()) <= width