Serial monitor redesign: Test box button, InfluxDB row in the checklist

- Test box: restart + start-up capture + online InfluxDB check (data newer
  than the restart), with a status strip; checklist and findings are the
  main view, live log / guided troubleshooting / box details are tabs,
  rarely used actions in a More menu
- light palette on all systems (macOS dark mode made the colours unreadable)
- new rule influx.invalid_value: -inf from a silent microphone makes
  InfluxDB reject every upload (HTTP 400); guide end names it
- exclusive serial port on macOS/Linux: two tool windows no longer split
  the log between them
- InfluxDB check: `since` filter, neutral wording when nothing new arrives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KrzyszurandClaude Opus 5.5 committed 2026-10-01 00:06:48 +02:00
1 parent 66cb804102
commit bb933ff748
13 files changed
+422 -83

No files matched your search

+14 -6
View File
@@ -28,6 +28,7 @@ DEFAULT_BUCKET = "sensorbox"
LOOKBACK = "-30d"
FRESH_MIN = 15 # like SEEN_OK_MIN in the service system
FUTURE_TOLERANCE_MIN = 10 # newer than now + this: the box clock is ahead
SINCE_SLACK_S = 60 # clock difference box / laptop when counting "since the test started"
TIMEOUT_S = 20
# Sensor groups that every box must upload: name -> measurement prefixes.
@@ -122,13 +123,17 @@ def _ago(now: dt.datetime, t: dt.datetime) -> str:
return f"{minutes / 1440:.0f} days ago"
def classify(latest: dict[str, dt.datetime], now: dt.datetime, fresh_min: int = FRESH_MIN) -> InfluxCheck:
def classify(latest: dict[str, dt.datetime], now: dt.datetime, fresh_min: int = FRESH_MIN,
since: dt.datetime | None = None) -> InfluxCheck:
"""`since`: only data newer than this counts (the box was restarted then)."""
def newest(prefixes: tuple[str, ...]) -> dt.datetime | None:
times = [t for m, t in latest.items() if m.startswith(prefixes)]
return max(times) if times else None
def fresh(t: dt.datetime | None) -> bool:
return t is not None and (now - t).total_seconds() <= fresh_min * 60
if t is None or (now - t).total_seconds() > fresh_min * 60:
return False
return since is None or (since - t).total_seconds() <= SINCE_SLACK_S
details = []
for label, prefixes in (("Device status", (STATUS,)), ("Microphone", (MIC,)),
@@ -155,9 +160,11 @@ def classify(latest: dict[str, dt.datetime], now: dt.datetime, fresh_min: int =
+ ", ".join(missing), latest, missing, details)
if lagging:
# Offline now, but the history already shows the fault.
return InfluxCheck(SENSORS_MISSING, f"Box is offline now (last status {_ago(now, status_t)}); "
"before that its status arrived without sensor data: " + ", ".join(missing),
return InfluxCheck(SENSORS_MISSING, f"Nothing new in InfluxDB (last status {_ago(now, status_t)}); "
"before that the status arrived without sensor data: " + ", ".join(missing),
latest, missing, details)
if since is not None:
return InfluxCheck(SILENT, "No new data in InfluxDB since the test started", latest, missing, details)
return InfluxCheck(SILENT, f"No recent data in InfluxDB (nothing in the last {fresh_min} min)",
latest, missing, details)
@@ -173,7 +180,8 @@ def query_latest(cfg: InfluxConfig, mac: str) -> dict[str, dt.datetime]:
return parse_csv(resp.read().decode("utf-8"))
def check_device(cfg: InfluxConfig | None, mac: str, now: dt.datetime | None = None) -> InfluxCheck:
def check_device(cfg: InfluxConfig | None, mac: str, now: dt.datetime | None = None,
since: dt.datetime | None = None) -> InfluxCheck:
"""Blocking; run it off the UI thread. Never raises."""
mac = "".join(c for c in (mac or "") if c.isalnum()).upper()
if cfg is None or not cfg.token:
@@ -191,4 +199,4 @@ def check_device(cfg: InfluxConfig | None, mac: str, now: dt.datetime | None = N
return InfluxCheck(UNAVAILABLE, f"InfluxDB not reachable from this computer ({reason})")
except (ValueError, csv.Error) as exc:
return InfluxCheck(UNAVAILABLE, f"Unexpected InfluxDB answer ({exc})")
return classify(latest, now or dt.datetime.now(dt.timezone.utc))
return classify(latest, now or dt.datetime.now(dt.timezone.utc), since=since)