diff --git a/tasks/sud.py b/tasks/sud.py index 3cb3502..e89cd82 100644 --- a/tasks/sud.py +++ b/tasks/sud.py @@ -7,6 +7,34 @@ from components import APid, AStirrer from components.plant import APlant from components.sud import Sud, SudState +# Upper bound on how many (t, theta) points the forecast is thinned to +# before going out over the wire (see _send_forecast()) - a fine enough +# dt over a multi-hour brew can otherwise produce a single JSON message +# of several MB, large enough to exceed the websockets library's default +# 1 MiB max_size and get the connection closed outright (code 1009). The +# GUI's forecast plot is a few hundred pixels wide, so this many points +# is already far more resolution than it can show; self.forecast_t/ +# forecast_theta themselves stay at full simulated resolution - this +# only thins the copy actually sent to clients. +MAX_FORECAST_POINTS = 1000 + + +def _downsample(t, theta, max_points=MAX_FORECAST_POINTS): + """Returns (t, theta) thinned to at most max_points entries by simple + decimation, always keeping the first and last point - losing a few + intermediate samples doesn't matter for a plot this size, but losing + the endpoints would visibly truncate the curve or its final value.""" + n = len(t) + if n <= max_points: + return t, theta + step = -(-n // max_points) # ceil(n / max_points) + t_ds = t[::step] + theta_ds = theta[::step] + if t_ds[-1] != t[-1]: + t_ds = t_ds + [t[-1]] + theta_ds = theta_ds + [theta[-1]] + return t_ds, theta_ds + class SudTask(ATask): def __init__(self, sud: Sud, tc: APid, stirrer: AStirrer, pot: APlant, dt, interval, msg_handler: MsgIo, @@ -166,9 +194,10 @@ class SudTask(ATask): await self._send_forecast() async def _send_forecast(self): + t, theta = _downsample(self.forecast_t, self.forecast_theta) await self.send({'Forecast': { - 'T': self.forecast_t, - 'Theta': self.forecast_theta, + 'T': t, + 'Theta': theta, 'Finished': self.forecast_finished, }})