Lume TLF+ToF sensor: a controlled bench characterization (2026-04-18 → 2026-04-19) plus continuing desktop-dashboard recordings (2026-04-22 onward). Annotations mark each operator-driven condition change.
The chart combines two distinct recording sessions, both at one sample per minute (~0.017 Hz):
update.sh). This session wasn’t designed as a controlled bench protocol; treat its annotations as field-style notes rather than a structured experiment.Three signals are plotted in stacked panels:
All four findings are drawn from the controlled bench session on 2026-04-18 → 2026-04-19. The 2026-04-22 desktop-dashboard data is visible in the chart as a second segment and operates in absolute value ranges different from the bench (TLF roughly 480–740 throughout submerged operation), so don’t expect bench thresholds to translate directly to it.
model_tof_raw is essentially bimodal in the bench recording: it sits near 25 whenever the sensor is in water and jumps to ~70 whenever the sensor is in air. Transitions between the two states are effectively step functions — a fixed threshold near 45 separates the two conditions with no ambiguity on bench data. The 2026-04-22 session stays in the ~26–29 water band throughout, consistent with this gate.
Bench air-phase sipm_mon2_raw values (roughly 2000–2600) overlap heavily with still-water bench values. The signal is temperature- and condition-sensitive, so any air/water gate built on TLF alone would need the ToF channel as a prior.
In every bench water-no-shake region the TLF reads noticeably higher than it does immediately after a twist-shake, even though model_tof_raw stays firmly in its “water” band (~25) throughout. The ToF sensor confirms that water is present against the window; the elevated TLF reading must therefore be driven by something the ToF cannot resolve — consistent with small air bubbles trapped on the optical window scattering additional light into the SiPM.
In the bench recording, each twist-shake event produces an immediate drop in sipm_mon2_raw — drops of roughly 720, 1300, and 600 across the bench shake events — well below the still-water readings that precede or follow them. This is the bubble-free baseline for that session. The fact that the baseline itself is not perfectly repeatable (720 vs 1300 vs 600) suggests that either residual turbidity, the angle the sensor was re-seated in, or partial re-bubbling between annotations still modulates the reading. The 2026-04-22 session records its own much-lower baseline (~480–500 after its twist-shake), reinforcing that the bubble-free TLF level is sensor- and reseating-specific and is best treated as a per-session reference rather than a fixed value.
A two-stage decision makes physical sense: (a) use model_tof_raw as a hard gate for air vs. water, and (b) within water, treat the TLF reading as an upper bound that can be inflated by trapped bubbles. A periodic agitation cycle or a bubble-scrub routine before measurement would make the TLF reading reflect the water itself rather than the air trapped against the window. Because the absolute TLF baseline drifts session-to-session, all bubble-detection logic should reference a recent post-disturbance baseline rather than a hard-coded threshold.
The field problem. In deployment we cannot inspect the optical window — we only see what the sensor reports. ToF tells us reliably whether the sensor is in water, but it can't tell us whether air bubbles are trapped against the window inside the water. A bubble-inflated TLF reading would be misread by the E. coli classifier as biological signal, producing false positives in microbiologically clean water.
What makes the 2026-04-18 bench dataset uniquely useful. The water in that recording is clean tap water with no measurable real turbidity and no real fluorophore content. So any TLF or turbidity signal observed while submerged (ToF in the water band) must be coming from bubbles trapped on the window — not from microbes, not from suspended sediment. The numbers below are computed only from the bench session (the 2026-04-22 session is a different sensor reseating with different absolute values and is excluded from these statistics):
Three signals separate bubbly water from bubble-free water in the bench data:
Run the existing E. coli model exactly as today, then attach a bubble-confidence layer that gates the prediction:
turb_raw while the sensor is submerged. If the current reading deviates from baseline by more than ~1.5 σ and ToF still reports water, raise the bubble flag.sipm_mon2_raw. If it exceeds the per-site post-shake std (call it σ0) by ~1.5×, raise the bubble flag.Real source water has real turbidity from suspended sediment, organic matter, and biota — so deploying these thresholds verbatim would mistake real turbidity for bubbles. The fix is to calibrate per site: on first deployment, capture (a) the natural turbidity baseline of the source under quiescent conditions, and (b) the post-agitation baseline. The delta between these two states is the bubble signal — the same quantity this bench dataset measures, just on top of a non-zero source-water turbidity floor instead of zero.
The existing E. coli model uses TLF + temperature + ToF and has no input that can distinguish bubble-driven TLF from microbial TLF. Adding the turbidity channel as a gate (rather than a feature, which would require retraining) is the lightest-weight path to deploying a bubble-aware classifier without changing the model itself.
All three panels share one timebase. Annotation rows in the source CSV contain only a timestamp and a note field; each annotation marks the start of a section that runs until the next annotation. The three plotted categories merge the raw labels: “Air”/“air” → air, “water no shake” → water no shake, “twist shake”/“submerged”/“air cleared” → twist shake. Time gaps longer than 1 hour are visually compressed in the chart with a // break marker.
Source: data.csv in this project. The interactive chart fetches the CSV directly on each page load via Plotly — refresh to pick up new data. A static fallback (plot.png) is also produced by python3 plot.py. New rows from the Lume desktop dashboard are appended via ./update.sh, which pulls lumelog.csv from SweetSenseInc/lume_desktop_dashboard (branch pc-sandbox), appends only timestamps strictly newer than the latest in data.csv, regenerates the static plot, and redeploys.