fix: ignore garbled measurement-state reads (phantom STOPPED/STARTED)

A buffer desync on the shared persistent connection (commonly right after
a DRD/DOD test) can make a Measure? read return a stray value. The state
classifier treated anything not in {"Start","Measure"} as "not measuring",
so a garbled read logged a phantom STOPPED, the next clean read logged
STARTED, and that reset measurement_start_time — producing constant
STOPPED/STARTED device-log pairs and a drifting elapsed timer.

Now only recognized states drive transitions: {"Start","Measure"} =
measuring, {"Stop"} = stopped, anything else = no change. Garbled reads
are also not persisted as the cached state, so they can't poison the next
transition check. Builds on the earlier Start<->Measure normalization.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 22:50:18 +00:00
parent b954eb8c89
commit 8c17af4849
+19 -5
View File
@@ -75,13 +75,24 @@ def persist_snapshot(s: NL43Snapshot, db: Session):
# but the DRD stream path tags snapshots "Measure" (and the DOD fallback also # but the DRD stream path tags snapshots "Measure" (and the DOD fallback also
# uses "Measure"). Treat ALL of these as "measuring" — otherwise opening and # uses "Measure"). Treat ALL of these as "measuring" — otherwise opening and
# closing the live stream flips state "Start"->"Measure"->"Start", which the # closing the live stream flips state "Start"->"Measure"->"Start", which the
# old equality check misread as stop-then-start and RESET measurement_start_time # old equality check misread as stop-then-start and reset measurement_start_time.
# every single time (the "elapsed time keeps resetting / shows wrong value on #
# another computer" bug — and each extra viewer made it worse). # Also: only act on RECOGNIZED states. A buffer desync on the shared connection
# (e.g. right after a DRD/DOD test) can make a Measure? read return a stray,
# garbled value; treating that as "not measuring" produced constant phantom
# "STOPPED -> STARTED" log pairs and reset the timer. Ignore unknown reads.
MEASURING_STATES = {"Start", "Measure"} MEASURING_STATES = {"Start", "Measure"}
is_measuring = new_state in MEASURING_STATES STOPPED_STATES = {"Stop"}
was_measuring = previous_state in MEASURING_STATES was_measuring = previous_state in MEASURING_STATES
if new_state in MEASURING_STATES:
is_measuring = True
elif new_state in STOPPED_STATES:
is_measuring = False
else:
logger.warning(f"Ignoring unrecognized measurement state for {s.unit_id}: {new_state!r}")
is_measuring = was_measuring # garbled/unknown read — no transition
if not was_measuring and is_measuring: if not was_measuring and is_measuring:
# Measurement just started - record the start time # Measurement just started - record the start time
row.measurement_start_time = datetime.utcnow() row.measurement_start_time = datetime.utcnow()
@@ -103,7 +114,10 @@ def persist_snapshot(s: NL43Snapshot, db: Session):
except Exception: except Exception:
pass pass
row.measurement_state = new_state # Only persist a recognized state so one garbled read can't poison the next
# transition check (which would manufacture the phantom STOPPED/STARTED pair).
if new_state in MEASURING_STATES or new_state in STOPPED_STATES:
row.measurement_state = new_state
row.counter = s.counter row.counter = s.counter
row.lp = s.lp row.lp = s.lp
row.leq = s.leq row.leq = s.leq