From 189d1e24d27ba8a092f8da5e50ac126cb09ea7e2 Mon Sep 17 00:00:00 2001 From: Jens Ahrensfeld Date: Sun, 21 Jun 2026 00:27:14 +0200 Subject: [PATCH] Scale the Sud forecast progress by the server's actual warp factor SudTask now computes warp_factor = dt/interval (the same simulated-vs- wall-clock split Pot/TempController/Stirrer already use internally) and sends it once at startup as 'WarpFactor'. The GUI uses it to scale real elapsed time into the forecast's simulated-schedule time axis, so the progress line lines up with the actual sim speed instead of assuming real time. Also fixes SudTask.tick() to decrement hold_remaining by dt (simulated seconds) instead of interval (wall-clock seconds) - it was the only place in the sim using the wall-clock interval for something meant to track simulated time, so hold steps were taking warp_factor times longer in real time than their declared duration intends. Without this fix the GUI's new warp-scaled progress line would race ahead of the real server during holds. --- brewpi/brewpi.py | 2 +- client/brewpi_gui.py | 12 +++++++++++- tasks/sud.py | 18 +++++++++++++++--- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/brewpi/brewpi.py b/brewpi/brewpi.py index 9996fd2..f1f827b 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, pot, DT_TASK, dispatcher.msgio_get("Sud"))) + taskmgr.add(SudTask(sud, tc, stirrer, pot, DT, 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/client/brewpi_gui.py b/client/brewpi_gui.py index dcdfc84..9187088 100755 --- a/client/brewpi_gui.py +++ b/client/brewpi_gui.py @@ -190,6 +190,10 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_loaded = False self.sud_state = None self.sud_user_message = None + # Simulated-seconds-per-real-second, from SudTask's 'WarpFactor' + # message; defaults to 1.0 (no scaling) until that arrives, or for + # a server that doesn't send it at all. + self.sud_warp_factor = 1.0 # time.monotonic() of the last IDLE/DONE -> running transition, used # to position the forecast plot's progress line; None while not # running. sud_paused_total accumulates time spent PAUSED so the @@ -287,7 +291,10 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): # While paused, use the moment the pause began as "now" so the # line freezes instead of drifting ahead of actual progress. now = self.sud_pause_started_at if self.sud_pause_started_at is not None else time.monotonic() - elapsed_min = (now - self.sud_start_time - self.sud_paused_total) / 60.0 + elapsed_real_s = now - self.sud_start_time - self.sud_paused_total + # The forecast's x-axis is simulated-schedule time; scale real + # elapsed time by the server's warp factor to match it. + elapsed_min = elapsed_real_s * self.sud_warp_factor / 60.0 self.forecast_plot.set_progress(elapsed_min) else: self.forecast_plot.clear_progress() @@ -314,6 +321,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.sud_loaded = False self.sud_state = None self.sud_user_message = None + self.sud_warp_factor = 1.0 self.sud_start_time = None self.sud_paused_total = 0.0 self.sud_pause_started_at = None @@ -483,6 +491,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow): self.update_sud_forecast(msg['Json']) elif "Name" in key: self.statusBar().showMessage("Sud schedule updated: {}".format(msg['Name']), 5000) + elif "WarpFactor" in key: + self.sud_warp_factor = msg['WarpFactor'] elif "State" in key: prev_state = self.sud_state self.sud_state = msg['State'] diff --git a/tasks/sud.py b/tasks/sud.py index 3f91ba7..f890356 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -11,12 +11,20 @@ TEMP_REACHED_TOLERANCE = 0.2 class SudTask(ATask): - def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, interval, msg_handler: MsgIo): + def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, dt, interval, msg_handler: MsgIo): ATask.__init__(self, interval) self.sud = sud self.tc = tc self.stirrer = stirrer self.pot = pot + # Simulated seconds per tick, vs. interval's wall-clock seconds per + # tick - same dt/interval split Pot/TempController/Stirrer already + # use internally. Their warp-induced speedup falls out naturally + # from ticking real physics at simulated dt; Sud has no physics of + # its own, so hold_remaining must be ticked by dt explicitly to get + # the same speedup instead of running in real time. + self.dt = dt + self.warp_factor = dt / interval self.msg_handler = msg_handler msg_handler.set_recv_handler(self.recv) @@ -104,7 +112,11 @@ class SudTask(ATask): self.sud.set_on_changed('user_message', self.on_user_message_changed) self.sud.set_on_changed('hold_remaining', ChangedFloat(self.on_hold_remaining_changed, prec=0).set) - asyncio.create_task(self.send({'Name': self.sud.name, 'Description': self.sud.description})) + asyncio.create_task(self.send({ + 'Name': self.sud.name, + 'Description': self.sud.description, + 'WarpFactor': self.warp_factor, + })) while True: if self.sud.state == SudState.RAMPING: @@ -113,5 +125,5 @@ class SudTask(ATask): # the previous step for one tick after a new target is pushed. if abs(self.tc.get_theta_ist() - self.tc.get_theta_soll_set()) < TEMP_REACHED_TOLERANCE: self.sud.temp_reached() - self.sud.tick(self.interval) + self.sud.tick(self.dt) await asyncio.sleep(self.interval)