From 51add6fd568c372cd08e1f1672959858026218a7 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Mon, 22 Jun 2026 19:37:20 +0200 Subject: [PATCH] Apply a loaded Sud's plant params immediately, not just once a run starts on_step_changed() only re-applies plant params (pot mass/material, L, Td, grain/water mass) once Sud.step actually changes to a real step - which doesn't happen on Load (Sud.load() resets step to None), so the controller kept using whatever the previously loaded Sud (or the generic startup baseline) had until Start. SudTask.recv()'s Load handler now calls apply_plant_params() against the new schedule's first step right away, so the controller already matches the expected plant the moment a Sud is loaded. Also replaces server/brewpi.py's hardcoded DEFAULT_PLANT_PARAMS with a baseline derived from Sud's own (still-unloaded) generic defaults (EMPTY_SUD's pot_mass/L/Td), used only for the brief startup window before any real Sud is ever loaded - numerically identical to the old constant. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk --- server/brewpi.py | 27 +++++++++++++-------------- tasks/sud.py | 14 +++++++++++++- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/server/brewpi.py b/server/brewpi.py index 5d4b9ab..cfd84ee 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -17,15 +17,6 @@ from tasks import TaskManager, TempSensorTask, HeaterTask, PotTask, TcTask, Stir import argparse as ap -# Default lumped thermal params, shared by both the real Pot plant and the -# temperature controller's internal Smith-predictor model. -DEFAULT_PLANT_PARAMS = { - "C": 4190, - "M": 25, - "L": 0.2, - "Td": 30 -} - class Tee: """Duplicates writes to multiple streams - used to mirror stdout/stderr @@ -70,6 +61,17 @@ if __name__ == '__main__': theta_amb = config['ambient_temperature'] plant_sim = "sim" in config['Controller']['plant_name'] + # Mash schedule - starts out empty; a client loads one of sude/*.json's + # several schedules onto it later (see components/sud.py's Sud). Also + # doubles as the source of baseline plant params for the Plant/ + # Temperature Controller below, before any real schedule is loaded - + # derive_plant_params() falls back to Sud's own (generic) defaults + # while unloaded (see components/sud.py's EMPTY_SUD); water_mass=25 + # is just a placeholder thermal mass standing in for "some water in + # the pot," not a literal recipe. + sud = Sud() + baseline_plant_params = sud.derive_plant_params(0, 25) + # Sensor sensor = TempSensorFactory.create(config['Controller']['sensor_name'], temp_offset=-0.15, variance=0.01) sensor_task = TempSensorTask(sensor, DT_TASK, dispatcher.msgio_get("Sensor")) @@ -77,7 +79,7 @@ if __name__ == '__main__': # Plant pot = Pot(DT) - pot.set_plant_params(DEFAULT_PLANT_PARAMS) + pot.set_plant_params(baseline_plant_params) pot.set_ambient_temperature(theta_amb) taskmgr.add(PotTask(pot, DT_TASK, dispatcher.msgio_get("Pot"))) @@ -93,7 +95,7 @@ if __name__ == '__main__': # "Normal" has no internal model/ambient - see temp_controller.py vs. # temp_controller_smith.py. if hasattr(tc, 'set_model_plant_params'): - tc.set_model_plant_params(DEFAULT_PLANT_PARAMS) + tc.set_model_plant_params(baseline_plant_params) if hasattr(tc, 'set_ambient_temperature'): tc.set_ambient_temperature(theta_amb) tc_task = TcTask(tc, DT_TASK, dispatcher.msgio_get("TempCtrl")) @@ -104,9 +106,6 @@ if __name__ == '__main__': stirrer_task = StirrerTask(stirrer, DT_TASK, dispatcher.msgio_get("Stirrer")) taskmgr.add(stirrer_task) - # Mash schedule - starts out empty; a client loads one of sude/*.json's - # several schedules onto it later (see components/sud.py's Sud). - sud = Sud() # Predicts how long a loaded schedule will actually take, by simulating # it with the same kind of plant/controller (and params) as above - # see components/sud_forecast.py. diff --git a/tasks/sud.py b/tasks/sud.py index ef24744..a9331b2 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -61,7 +61,10 @@ class SudTask(ATask): from it (constant for the whole brew), while M/C also fold in the step's grain_mass/water_mass, which vary over the brew's course (malt going in, water boiling off) - mirrors demo_sud.py's - apply_plant_params().""" + apply_plant_params(). Called both on every real step transition + (via on_step_changed()) and once immediately on Load (see recv()), + so the controller's behavior already matches the expected plant + as soon as a Sud is loaded, not just once a run actually starts.""" params = self.sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0)) self.pot.set_plant_params(params) if hasattr(self.tc, 'set_model_plant_params'): @@ -258,6 +261,15 @@ class SudTask(ATask): if self.sud.load(pair[1]): await self.send({'Name': self.sud.name, 'Description': self.sud.description}) await self.send({'Json': pair[1]}) + # on_step_changed() only re-applies plant params once a + # real step starts (Start) - apply them right away too, + # so the controller already matches this Sud's own pot/ + # L/Td (and its first step's expected grain/water mass) + # from the moment it's loaded, rather than whatever the + # previously loaded Sud (or the generic startup + # baseline - see server/brewpi.py) left behind. + if self.sud.schedule: + self.apply_plant_params(self.sud.schedule[0]) await self.send_forecast(pair[1]) else: # Sud.load() refuses while a run is in progress (state