fix: correct DOD field parsing and stop measurement-time resets

Two device-data bugs surfaced while scoping the live-feed work:

1. DOD parser misalignment. DOD's response has no leading counter and
   includes LE + LN1-LN5, but the parser reused the DRD field map
   (parts[0]=counter). That shifted everything: Lp was stored as the
   counter, Leq as Lp, LE as Leq, and LN1 as Lpeak (visible because
   "Lpeak" came out below Lmax, which is impossible). Parse DOD with its
   own map: Lp=0, Leq=1, Lmax=3, Lmin=4, Lpeak=10 (channel 1 = main).

2. measurement_start_time reset on every live-stream open/close. The DOD
   path tags state "Start"; the DRD stream path tags "Measure". The
   transition detector treated only "Start" as measuring, so opening the
   stream ("Start"->"Measure") read as a stop (cleared start time) and
   closing it ("Measure"->"Start") read as a start (reset to now). Every
   viewer reset the elapsed measurement time. Treat {"Start","Measure"}
   both as measuring.

LN1/LN2 (L1/L10) parsing + model/serialization is the next step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 21:53:00 +00:00
parent d6dd2e736b
commit a7983d2958
+27 -15
View File
@@ -69,10 +69,16 @@ def persist_snapshot(s: NL43Snapshot, db: Session):
logger.info(f"State transition check for {s.unit_id}: '{previous_state}' -> '{new_state}'") logger.info(f"State transition check for {s.unit_id}: '{previous_state}' -> '{new_state}'")
# Device returns "Start" when measuring, "Stop" when stopped # The device reports "Start" while measuring; the DOD path uses that string,
# Normalize to previous behavior for backward compatibility # but the DRD stream path tags snapshots "Measure" (and the DOD fallback also
is_measuring = new_state == "Start" # uses "Measure"). Treat ALL of these as "measuring" — otherwise opening and
was_measuring = previous_state == "Start" # closing the live stream flips state "Start"->"Measure"->"Start", which the
# 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).
MEASURING_STATES = {"Start", "Measure"}
is_measuring = new_state in MEASURING_STATES
was_measuring = previous_state in MEASURING_STATES
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
@@ -691,22 +697,28 @@ class NL43Client:
snap = NL43Snapshot(unit_id="", raw_payload=resp, measurement_state=measurement_state) snap = NL43Snapshot(unit_id="", raw_payload=resp, measurement_state=measurement_state)
# Parse known positions (based on NL43 communication guide - DRD format) # Parse DOD positional fields. DOD's layout is DIFFERENT from DRD: it has NO
# DRD format: d0=counter, d1=Lp, d2=Leq, d3=Lmax, d4=Lmin, d5=Lpeak, d6=LIeq, ... # leading counter and it includes LE plus LN1LN5. The device returns 4 channels
# of 16 fields each — [Lp, Leq, LE, Lmax, Lmin, LN1, LN2, LN3, LN4, LN5, Lpeak,
# LIeq, Leq_mov, Ltm5, over, under] — and channel 1 (parts[0:16]) is the main
# display. The previous code reused the DRD map (treating parts[0] as a counter),
# which shifted everything: Lp was reported as the counter, Leq as Lp, LE as Leq,
# and LN1 as Lpeak (you could spot it because "Lpeak" came out < Lmax).
try: try:
# Capture d0 (counter) for timer synchronization
if len(parts) >= 1: if len(parts) >= 1:
snap.counter = parts[0] # d0: Measurement interval counter (1-600) snap.lp = parts[0] # Lp: instantaneous sound pressure level
if len(parts) >= 2: if len(parts) >= 2:
snap.lp = parts[1] # d1: Instantaneous sound pressure level snap.leq = parts[1] # Leq: equivalent continuous level
if len(parts) >= 3: # parts[2] = LE (sound exposure level) — not currently surfaced
snap.leq = parts[2] # d2: Equivalent continuous sound level
if len(parts) >= 4: if len(parts) >= 4:
snap.lmax = parts[3] # d3: Maximum level snap.lmax = parts[3] # Lmax
if len(parts) >= 5: if len(parts) >= 5:
snap.lmin = parts[4] # d4: Minimum level snap.lmin = parts[4] # Lmin
if len(parts) >= 6: if len(parts) >= 11:
snap.lpeak = parts[5] # d5: Peak level snap.lpeak = parts[10] # Lpeak (parts[5] is LN1, NOT Lpeak)
# LN1/LN2 percentiles live at parts[5]/parts[6] (the L1/L10 display contract).
# Surfaced as snap.ln1/snap.ln2 once those fields are added to the snapshot
# dataclass + NL43Status model — next step on this branch.
except (IndexError, ValueError) as e: except (IndexError, ValueError) as e:
logger.warning(f"Error parsing DOD data points: {e}") logger.warning(f"Error parsing DOD data points: {e}")