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:
parent
872eadac37
commit
03d3600ecc
10 changed files with 375 additions and 24 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue