Several sude/*.json schedules can exist on disk; only one runs at a
time, chosen by a client Load. Sud no longer takes a path at all - it
starts empty (EMPTY_SUD) and the server config no longer names a
specific schedule file, removing the implicit link to sud_0010.json
in particular. Reading/writing sude/*.json is now entirely the
client's job (e.g. the GUI's Load/Save file dialogs); the server's Sud
only ever holds whatever was last Load()ed, in memory, until replaced
or the server restarts.
Updates demo_sud.py/demo_sud_save_load.py to construct an empty Sud()
and load() a schedule explicitly, matching the new constructor.
A step's target temperature applies to the whole step (it's what a
ramp ramps to and what a hold then holds at), not just its ramp phase
- promote it from a nested "ramp": {"temp": ...} to a step-level
"temperature" field, defaulted like descr/grain_mass/etc. Updates all
consumers (Sud._build_step, SudTask, the GUI's forecast estimator,
demo_sud.py) and migrates sude/sud_0010.json and the README.
A ramp step whose target is below the current actual temperature can
only be reached by ambient cooling - the heater can't actively cool.
SudTask now detects this once per ramp phase and calls the controller's
new set_cooling() override (forces y=0, bypassing the FSM) instead of
waiting for its hysteresis-based IDLE transition. The GUI shows a
blinking blue "Cool down" label in the status bar while this is active.
Progression already only advances once theta_ist matches theta_soll
regardless of direction, so no change was needed there.
A step can now carry both 'ramp' and 'hold': it ramps to the target
temp, then holds there for the given duration, instead of needing two
separate schedule entries. Sud.temp_reached() switches such a step
from its ramp phase into its hold phase in place (re-firing the
step-changed callback so SudTask/demo_sud re-apply the hold's stirrer
settings); user_wait_for_continue still applies once the whole step -
both phases - is done.
Merges sud_0010.json's three ramp/hold pairs into combined steps.
Renames everywhere the concept appears: Sud.download()/upload() ->
save()/load(), the 'Download'/'Upload' websocket messages ->
'Save'/'Load', the brewpi_gui menu actions, handlers and
sud_download_path -> sud_save_path, and the demo script. Behavior is
unchanged - this is naming only, from the client's perspective (save
to / load from a local file) rather than the server's.
Sud.download() returns the on-disk document; Sud.upload() validates,
persists, and resets the schedule, refusing mid-brew or malformed input
so the current schedule is never left half-applied.
grain_mass and water_mass now live on each step (defaulted from
default.step) instead of being fixed for the whole brew, since both
change over a mash (malt going in, water boiling off).
Sud.derive_plant_params() takes them as arguments so it can be
recomputed per step; the demo re-applies the resulting M/C to both the
real plant and the controller's Smith-predictor model on every step
change via the new Pot.set_thermal_params()/
TempController.set_model_params().
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgR9tPaSzFkAwRAUyeaaCD
Steps now declare a "ramp" or "hold" key instead of a flat "type", and
unspecified fields (including nested stirrer settings) fall back to a
new top-level default.step structure. Stirrer config is now
interval_time/on_ratio, mapping directly onto AStirrer's cycle
time/duty cycle instead of separate on/off durations. Update
components/sud.py, tasks/sud.py, the Sud demo, sude/sud_0010.json, and
the README to match.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgR9tPaSzFkAwRAUyeaaCD
Add Sud.derive_plant_params(), which combines pot_mass/pot_material/
grain_mass/water_mass into a single lumped (mass, specific heat) pair
using approximate specific-heat constants for water, grain, and (by a
small material lookup table) the pot itself. demo_sud.py now builds
its plant_params from this instead of hardcoded C/M values, keeping
L/Td as the only manually-tuned plant parameters.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
sude/sud_0010.json moved from a list of combined ramp+hold+wait
"Rasten" with global stirrer constants to a flat "schedule" list of
explicit {"type": "heat"|"hold", ...} steps, each carrying its own
stirrer_speed/stirrer_time_on/stirrer_time_off and an optional
user_wait_for_continue (+ user_message).
- components/sud.py: Sud now tracks the current `step` instead of
`rast`; "heat" steps enter RAMPING (advanced externally via
temp_reached()), "hold" steps enter HOLDING and count down
`duration` minutes (0 if omitted). Either step type can pause in
WAIT_USER via user_wait_for_continue, surfaced through the new
user_message attribute.
- tasks/sud.py: SudTask only pushes theta_soll/heatrate_soll on
"heat" steps, converts each step's stirrer_time_on/off into a
duty_cycle/cycle_time on every step change (replacing the old
state-based RAMPING/HOLDING stirrer switching), and broadcasts
user_message changes. Stirring is now fully schedule-driven instead
of implicitly stopped on WAIT_USER/DONE (DONE still stops it, since
there's no step left to read settings from).
- scripts/demos/sud/demo_sud.py: mirrors the same step-driven wiring.
- README's Mash schedules section rewritten for the new schema.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop the now-stale "config.json.templ/.sim still carry dead
Model.kn/Plant.kn keys" note (they were deleted), note that Pot now
always starts at theta_amb (no separate initial-temp param) and that
dropping the gain (efficiency) factor entirely is a partial realism
regression, not just a fix. Point the config-validation item at the
gain key that's still dead in config.json.sim. Also update demo_sud.py
to match the Pot params Pot.__init__ actually reads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drives a real Smith-predictor TempController, Pot, HeaterSim, and
StirrerSim through sude/sud_0010.json end-to-end (mirroring brewpi.py's
wiring, including the model/plant power-discretization mismatch and
tasks/sud.py's stirrer-switching rules), auto-confirming waitForUser
pauses so it completes unattended. Plots theta/heatrate (actual vs.
target), heater power_set/power_eff, Sud state, rest index, and
stirrer speed/duty cycle over time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
brewpi.py passed a literal 20 as theta_amb to Pot, and
temp_controller_smith.py's two internal Pot models (and every demo's
Pot/TempController instantiation) silently relied on Pot's default of
20 instead. Add a top-level ambient_temperature key to
config.json(.templ/.sim), thread it through brewpi.py into both the
real plant and the Smith predictor's models via a new theta_amb param
on TempController, and make the demos pass it explicitly too.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[Sensor] - replace TempSensorSim's hardcoded k_noise with configurable
temp_offset/variance, accept **kwargs on Max31865 too so both sensors
share a constructor signature usable from TempSensorFactory.create()
[Temp Controller] - low-pass filter the backward-difference heat-rate
estimate (alpha=0.1) in both temp_controller.py and
temp_controller_smith.py instead of using the raw, noisy derivative
[Pot] - drop kn from demo_pot.py's params, matching the unused-field
removal already made to pot.py itself
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
temp_controller_smith.py still relied on Kalman filtering and Pot's old
get_temperature_intermediate(), both removed by the recent Pot/Kalman
simplification. Rebuild it using two Pot model copies (zero-delay and
delayed) and the same backward-difference heat rate as temp_controller.py,
combined into the classic Smith correction:
theta_ist = theta_model_fast + (theta_plant - theta_model_delay).
Also fix set_model_power() never being called, so the internal model
actually receives the controller's power output, and fill in
demo_temp_controller_smith.py to exercise it end-to-end with an extra
plot of plant vs. fast-model vs. delayed-model temperature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
temp_controller.py, temp_controller_smith.py, and kalman.py imported
matplotlib at module level just to support eyeballed-plot __main__
blocks, coupling the live server's import graph to a GUI plotting lib
it never uses at runtime. Relocate those demos (and kalman_eval.py) to
scripts/demos/pid/ and strip the now-unused imports/__main__ blocks
from the production files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>