Log both Sense HAT thermometers, and migrate schemas that predate them

The board carries two independent thermometers and the code averaged them
into temp_raw without ever recording either. Measured over 12 samples on a
real station: HTS221 30.973 C at sd 0.060, LPS25HB 29.810 C at sd 0.443, a
standing gradient of 1.163 C with the SoC at 44.55 C.

Two things follow from that and neither is possible without the raw channels.
A plain average of a quiet sensor and one seven times noisier lands at sd
0.223 where inverse-variance weighting reaches 0.060, and the gradient between
two chips at different distances from the SoC is a second observation of
self-heating that could identify the compensator's k with no reference
thermometer. Both need history, and history cannot be backfilled, so the
columns land on their own ahead of the work that consumes them.

CREATE TABLE IF NOT EXISTS is a no-op against a table that already exists, so
adding to COLUMNS would have reached a fresh install and silently missed every
station already running, then surfaced as an OperationalError inside
insert_telemetry. That sits on the sample loop, so it takes a station down
rather than leaving a gap. Store now reconciles the table against COLUMNS on
open, which makes every future column addition safe rather than just this one.

The simulator gains the same two channels, with couplings solved so their
forward models average to exactly the k = 0.55 the compensator is tuned
against. Aggregate behaviour is unchanged; only the per-channel detail is new.
Simulated temp_raw noise does rise from 0.05 to 0.223, which is not a
regression but the end of an over-optimistic figure: it was modelling the
quiet sensor and calling it the average.
This commit is contained in:
2026-08-19 18:41:46 +01:00
parent 485affe956
commit bda42a0468
5 changed files with 159 additions and 6 deletions
+20 -2
View File
@@ -53,6 +53,21 @@ def read_cpu_temperature() -> float:
return float("nan")
# Per-chip thermal coupling to the SoC, and per-chip noise.
#
# The Sense HAT carries two independent thermometers at different distances
# from the SoC, and they are not equally good. Measured over 12 samples on a
# real board: HTS221 30.973 C at sd 0.060, LPS25HB 29.810 C at sd 0.443, a
# standing gradient of 1.163 C with the SoC at 44.55 C.
#
# These two couplings are chosen so their forward models average to exactly the
# k = 0.55 the compensator is tuned against. The aggregate behaviour is
# therefore unchanged and only the per-channel detail is new, which matters
# because that gradient is a second observation of self-heating.
K_HTS221, K_LPS25HB = 0.6164, 0.4889
SD_HTS221, SD_LPS25HB = 0.060, 0.443
class SimulatedBoard:
"""Ornstein-Uhlenbeck weather with a diurnal driver. Good enough to
exercise every code path and to sanity-check a model's skill score."""
@@ -91,9 +106,12 @@ class SimulatedBoard:
lux = max(0.0, 60000.0 * max(math.sin(math.radians(max(elev, 0.0))), 0.0)) + 8.0
cpu = temp + 22.0 + 1.5 * self.rng.normal()
# forward model must invert the compensator exactly, see scripts/simulate.py
k_true = 0.55
t_h = (temp + K_HTS221 * cpu) / (1.0 + K_HTS221) + SD_HTS221 * self.rng.normal()
t_p = (temp + K_LPS25HB * cpu) / (1.0 + K_LPS25HB) + SD_LPS25HB * self.rng.normal()
return {
"temp_raw": (temp + k_true * cpu) / (1.0 + k_true) + 0.05 * self.rng.normal(),
"temp_raw": (t_h + t_p) / 2.0,
"temp_h": t_h,
"temp_p": t_p,
"hum": rh + 0.4 * self.rng.normal(),
"press": press + 0.05 * self.rng.normal(),
"cpu_temp": cpu,