From e17189280405ef17d18ef98ded612e3170572866 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 21 Jun 2026 00:05:55 +0200 Subject: [PATCH] Keep the plant/controller model in sync as the Sud advances SudTask.on_step_changed() set the controller's target temp/rate on every step, but never touched the real Pot's or the Smith predictor's internal model's thermal mass (M/C) - those stayed frozen at brewpi.py's startup DEFAULT_PLANT_PARAMS for the whole brew, even though grain_mass/water_mass change per step (malt going in, water boiling off) and demo_sud.py already recomputes them every step via derive_plant_params(). Production SudTask just never got the same treatment. Adds SudTask.apply_plant_params(), called alongside the existing theta_soll/heatrate_soll push, mirroring the demo. Needs the real Pot now, so SudTask takes a `pot` constructor arg; set_model_params() is only called if the configured controller actually defines one (the "Normal" pid_type doesn't). --- brewpi/brewpi.py | 2 +- tasks/sud.py | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/brewpi/brewpi.py b/brewpi/brewpi.py index 533c028..9996fd2 100755 --- a/brewpi/brewpi.py +++ b/brewpi/brewpi.py @@ -74,7 +74,7 @@ if __name__ == '__main__': sud_path = config.get('sud') if sud_path: sud = Sud(sud_path) - taskmgr.add(SudTask(sud, tc, stirrer, DT_TASK, dispatcher.msgio_get("Sud"))) + taskmgr.add(SudTask(sud, tc, stirrer, pot, DT_TASK, dispatcher.msgio_get("Sud"))) # Tracer taskmgr.add(TracerTask(sensor, heater, tc, trace_tc, DT_TASK_TRACER, dispatcher.msgio_get("Tracer"))) diff --git a/tasks/sud.py b/tasks/sud.py index d879359..3f91ba7 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -3,6 +3,7 @@ from tasks import ATask from ws.message import MsgIo from utils.value import ChangedFloat from components import APid, AStirrer +from components.plant import APlant from components.sud import Sud, SudState # How close theta_ist needs to be to theta_soll_set to count as "reached". @@ -10,14 +11,25 @@ TEMP_REACHED_TOLERANCE = 0.2 class SudTask(ATask): - def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, interval, msg_handler: MsgIo): + def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, interval, msg_handler: MsgIo): ATask.__init__(self, interval) self.sud = sud self.tc = tc self.stirrer = stirrer + self.pot = pot self.msg_handler = msg_handler msg_handler.set_recv_handler(self.recv) + def apply_plant_params(self, step): + """Keeps the real plant's and the controller's internal model's + lumped (M, C) in sync with the step's grain_mass/water_mass, since + those vary over the course of a brew (malt going in, water boiling + off) - mirrors demo_sud.py's apply_plant_params().""" + params = self.sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0)) + self.pot.set_thermal_params(params['M'], params['C']) + if hasattr(self.tc, 'set_model_params'): + self.tc.set_model_params(params['M'], params['C']) + def apply_stirrer(self, step): stirrer_cfg = step.get('ramp', step.get('hold', {})).get('stirrer', {}) speed = stirrer_cfg.get('speed', 0) @@ -35,6 +47,7 @@ class SudTask(ATask): ramp = step.get('ramp') if step else None hold = step.get('hold') if step else None if step is not None: + self.apply_plant_params(step) if ramp is not None: self.tc.set_theta_soll(ramp['temp']) self.tc.set_heatrate_soll(ramp['rate'])