From 0382bb01aa418e0e86127d324a4f5c7b46302554 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Wed, 1 Jul 2026 20:53:50 +0200 Subject: [PATCH] fix: stamp PlantParams with each logger's own elapsed, not Sud.elapsed log_plant_params() stored whatever elapsed it was handed - always Sud.elapsed - regardless of which logger's own Samples.t timeline it needed to match. SudLogTask._elapsed() is Sud.elapsed, so its PlantParams lined up fine, but ServerLogTask._elapsed() is a separate tick counter since server startup - using Sud.elapsed there put PlantParams.t on a different scale than its own Samples.t entirely, causing utils/replay_sim.py to apply grain/water-mass changes at the wrong point when replaying a server-session log (spotted by comparing its output for log_latest.json vs log_latest_sud.json from the same session). Fixed by dropping the elapsed parameter - log_plant_params(params) now stamps with self._elapsed(), so each logger always uses its own correct timeline. Cascades into SudTask.set_on_plant_params()'s callback signature and its wiring in server/brewpi.py. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01JFDeoTKBDkXRhPfnyDEoBt --- server/brewpi.py | 8 ++++---- tasks/server_log.py | 12 ++++++++++-- tasks/sud.py | 11 ++++++++--- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/server/brewpi.py b/server/brewpi.py index 22b6510..3b46c05 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -148,7 +148,7 @@ if __name__ == '__main__': # and every log_interval seconds in between. server_log_task = ServerLogTask(tc, heater, heater_task, DT_TASK, args.logdir, config=config, dt=DT, log_interval=log_interval, sim_warp_factor=sim_warp_factor) if startup_params is not None: - server_log_task.log_plant_params(0, startup_params) + server_log_task.log_plant_params(startup_params) taskmgr.add(server_log_task) # Per-run log - only records while a Sud is actually playing; written to @@ -157,9 +157,9 @@ if __name__ == '__main__': sud_log_task = SudLogTask(tc, heater, heater_task, sud, DT_TASK, args.logdir, config=config, dt=DT, log_interval=log_interval, sim_warp_factor=sim_warp_factor) taskmgr.add(sud_log_task) - sud_task.set_on_plant_params(lambda elapsed, params: ( - server_log_task.log_plant_params(elapsed, params), - sud_log_task.log_plant_params(elapsed, params))) + sud_task.set_on_plant_params(lambda params: ( + server_log_task.log_plant_params(params), + sud_log_task.log_plant_params(params))) sud_task.set_on_reanchor(sud_log_task.log_reanchor) # Assign data flow diff --git a/tasks/server_log.py b/tasks/server_log.py index 844a0a0..4c1179e 100644 --- a/tasks/server_log.py +++ b/tasks/server_log.py @@ -32,8 +32,16 @@ class ServerLogTask(ATask): self._run_id = time.strftime("%Y%m%dT%H%M%S", time.localtime()) self._last_write = time.monotonic() - def log_plant_params(self, elapsed, params): - self._param_events.append({'t': elapsed, 'params': params}) + def log_plant_params(self, params): + # Stamped with this logger's *own* _elapsed() - not, say, Sud.elapsed + # passed in from the caller - so PlantParams' 't' always lines up + # with this same log's own Samples' 't'. SudLogTask's _elapsed() IS + # Sud.elapsed, but ServerLogTask's is its own independent counter + # since server startup, on a completely different timeline (the + # server usually starts well before any Sud does) - using the + # wrong one made utils/replay_sim.py apply plant-param changes at + # the wrong point in ServerLogTask's own Samples entirely. + self._param_events.append({'t': self._elapsed(), 'params': params}) async def on_process(self): while True: diff --git a/tasks/sud.py b/tasks/sud.py index de40141..a2c2df0 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -117,8 +117,13 @@ class SudTask(ATask): msg_handler.set_recv_handler(self.recv) def set_on_plant_params(self, callback): - """Register a callback invoked whenever apply_plant_params() fires, - so external observers (e.g. SudLogTask) can record each change.""" + """Register a callback invoked with (params) whenever + apply_plant_params() fires, so external observers (e.g. + SudLogTask/ServerLogTask) can record each change. Deliberately + doesn't pass elapsed - Sud.elapsed is meaningful for a SudLogTask + (its own Samples' 't' timeline) but not a ServerLogTask (a + separate timeline since server startup); each logger stamps its + own _elapsed() itself (see ServerLogTask.log_plant_params()).""" self._on_plant_params = callback def set_on_reanchor(self, callback): @@ -148,7 +153,7 @@ class SudTask(ATask): self.pot.set_plant_params(params) self.tc.set_model_plant_params(params) if self._on_plant_params: - self._on_plant_params(self.sud.elapsed, params) + self._on_plant_params(params) def apply_stirrer(self, phase): stirrer_cfg = phase.get('stirrer', {})