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 tasks/ Async tasks (one per component) that poll
hardware/sim state at a fixed interval and hardware/sim state at a fixed interval and
publish changes through the message dispatcher. publish changes through the message dispatcher.
sud_log.py's SudLogTask additionally records server_log.py's ServerLogTask additionally
each Sud run's measured data and forecast to records temp/power samples for the whole server
logs/*.json - see "Logging & analysis" below. 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 ws/ Minimal WebSocket pub/sub layer: server
(single- and multi-user), client, and a (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" same replay a brand-new one would — there's no "first connect"
special-casing anywhere in this path. 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". One corollary, easy to miss: this mechanism has no concept of "transient".
Anything sent through it is implicitly durable state that will be replayed 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* 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 enough to exceed the `websockets` library's default 1 MiB `max_size` and
get the connection closed outright (code 1009). `SudTask.forecast_t`/ get the connection closed outright (code 1009). `SudTask.forecast_t`/
`forecast_theta` themselves stay at full simulated resolution (used for `forecast_theta` themselves stay at full simulated resolution (used for
the exact-match truncation in `_reanchor_forecast()` below, and for the exact-match truncation in `_reanchor_forecast()` below) - only the copy
`SudLogTask`'s full-fidelity `logs/forecast_*.json` - see "Logging & actually sent to clients is thinned, which the GUI's few-hundred-pixel-wide
analysis") - only the copy actually sent to clients is thinned, which the plot can't show the difference from anyway.
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 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 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 ## Logging & analysis
`tasks/sud_log.py`'s `SudLogTask` records every Sud run's measured data - Both loggers record the same six signals the GUI's Automatic tab plots live
the same six signals the GUI's Automatic tab plots live (`temp_ist`/ (`temp_ist`/`temp_soll`, `rate_ist`/`rate_soll`, `power_set`/`power_eff`),
`temp_soll`, `rate_ist`/`rate_soll`, `power_set`/`power_eff`), each sample each sample with both simulated-elapsed-seconds and a real wall-clock
with both simulated-elapsed-seconds and a real wall-clock timestamp - to timestamp, plus any plant-param changes along the way (see
`logs/log_<date>T<time>_<sud-name>.json`, plus the forecast it was being `apply_plant_params()`) - written as JSON via `write()`, whole and only
compared against (its *final*, most-corrected version - see "Forecast vs. once, never partially mid-run.
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 `tasks/server_log.py`'s `ServerLogTask` records continuously for the whole
moment a run actually starts (`Sud.state` leaves `IDLE`/`DONE`) and is server session, regardless of Sud state - useful for verifying controller
written out whole the moment it ends (`DONE`, or aborted via `Stop`) - and heater behaviour outside of a scheduled brew. It starts on construction
never partially mid-run, since a half-written file is of no use before and is written to `logs/log_<date>T<time>.json` on shutdown.
the run is over anyway.
`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 `server/brewpi.py` also mirrors everything it prints (every component's
`print()`-based status/debug output) to a plain text log, `print()`-based status/debug output) to a plain text log,