docs: refresh README for sud/server log split and reconnect pump task

The Logging & analysis section and a couple of cross-references still
described the old SudLogTask (forecast_*.json pairing) that predated
the ServerLogTask refactor and the just-reintroduced per-run SudLogTask.
Also documents the state_pump() task's role in reconnect behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
This commit is contained in:
2026-07-01 09:18:23 +02:00
co-authored by Claude Sonnet 5
parent 0b2a2be035
commit aeeecc4ddc
+35 -19
View File
@@ -72,9 +72,12 @@ components/ Pluggable building blocks behind factories:
tasks/ Async tasks (one per component) that poll
hardware/sim state at a fixed interval and
publish changes through the message dispatcher.
sud_log.py's SudLogTask additionally records
each Sud run's measured data and forecast to
logs/*.json - see "Logging & analysis" below.
server_log.py's ServerLogTask additionally
records temp/power samples for the whole server
session, and sud_log.py's SudLogTask (a thin
subclass reusing the same sample format) does
the same but only while a Sud run is active -
see "Logging & analysis" below.
ws/ Minimal WebSocket pub/sub layer: server
(single- and multi-user), client, and a
@@ -184,6 +187,14 @@ currently-known state for that channel in one message, via `User.send()`'s
same replay a brand-new one would — there's no "first connect"
special-casing anywhere in this path.
A single `state_pump()` task (started once, independent of any connection)
drains the dispatcher's queue and folds every message into `global_state`
regardless of how many clients are connected. This matters for a client
that silently drops without a close frame (e.g. an iPad going to sleep):
nothing queues up waiting for it, so on reconnect it gets the one
`global_state` replay above, not a burst of every message broadcast while
it was gone.
One corollary, easy to miss: this mechanism has no concept of "transient".
Anything sent through it is implicitly durable state that will be replayed
to whoever connects next, even long after the fact. A one-shot *event*
@@ -432,10 +443,9 @@ can otherwise produce a single `Forecast` message of several MB, large
enough to exceed the `websockets` library's default 1 MiB `max_size` and
get the connection closed outright (code 1009). `SudTask.forecast_t`/
`forecast_theta` themselves stay at full simulated resolution (used for
the exact-match truncation in `_reanchor_forecast()` below, and for
`SudLogTask`'s full-fidelity `logs/forecast_*.json` - see "Logging &
analysis") - only the copy actually sent to clients is thinned, which the
GUI's few-hundred-pixel-wide plot can't show the difference from anyway.
the exact-match truncation in `_reanchor_forecast()` below) - only the copy
actually sent to clients is thinned, which the GUI's few-hundred-pixel-wide
plot can't show the difference from anyway.
The one wrinkle is a step with `user_wait_for_continue`: a human's response
time genuinely can't be forecast. Rather than stall the whole estimate
@@ -675,18 +685,24 @@ running" if this is ever reachable off a trusted LAN.
## Logging & analysis
`tasks/sud_log.py`'s `SudLogTask` records every Sud run's measured data -
the same six signals the GUI's Automatic tab plots live (`temp_ist`/
`temp_soll`, `rate_ist`/`rate_soll`, `power_set`/`power_eff`), each sample
with both simulated-elapsed-seconds and a real wall-clock timestamp - to
`logs/log_<date>T<time>_<sud-name>.json`, plus the forecast it was being
compared against (its *final*, most-corrected version - see "Forecast vs.
actual duration" above) to a paired `logs/forecast_<date>T<time>_
<sud-name>.json`, sharing the same date-time token. A fresh pair starts the
moment a run actually starts (`Sud.state` leaves `IDLE`/`DONE`) and is
written out whole the moment it ends (`DONE`, or aborted via `Stop`) -
never partially mid-run, since a half-written file is of no use before
the run is over anyway.
Both loggers record the same six signals the GUI's Automatic tab plots live
(`temp_ist`/`temp_soll`, `rate_ist`/`rate_soll`, `power_set`/`power_eff`),
each sample with both simulated-elapsed-seconds and a real wall-clock
timestamp, plus any plant-param changes along the way (see
`apply_plant_params()`) - written as JSON via `write()`, whole and only
once, never partially mid-run.
`tasks/server_log.py`'s `ServerLogTask` records continuously for the whole
server session, regardless of Sud state - useful for verifying controller
and heater behaviour outside of a scheduled brew. It starts on construction
and is written to `logs/log_<date>T<time>.json` on shutdown.
`tasks/sud_log.py`'s `SudLogTask` subclasses `ServerLogTask` to reuse the
same sample format, but only records while a Sud run is actually active. A
fresh run starts recording on a genuine `Start` from `IDLE`/`DONE` (a
`Pause`→resume `Start` is a no-op - it's still the same run) and is written
out on `Stop` or natural completion (`DONE`), to
`logs/log_<date>T<time>_<sud-name>.log`.
`server/brewpi.py` also mirrors everything it prints (every component's
`print()`-based status/debug output) to a plain text log,