Add a Progress tab: one step plate per schedule step, stacked
A glance at the Automatic tab's forecast plot couldn't show where the brew actually stands within its own schedule - just a curve. The new Progress tab stacks one StepPlate per step instead: an LED for its status (off/done/active-ramping/active-holding), its resolved target temp, masses and ramp rate, plus, for whichever step is currently active, live actual temp and stirrer state (frozen at their last value once that step finishes) and a forecast-based remaining-time countdown. The countdown needs to know where each step actually begins in the forecast's timeline, which the server didn't track before - SudForecastEstimator.estimate() now returns per-step start times alongside t/theta, and SudTask threads them through send_forecast()/ _reanchor_forecast() the same way (including the same generation-guard against the concurrent-call race) as a new 'StepStarts' field on the 'Forecast' message. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx
This commit is contained in:
@@ -66,12 +66,17 @@ class SudForecastEstimator:
|
||||
self.theta_amb = theta_amb
|
||||
|
||||
def estimate(self, doc, start_theta=None):
|
||||
"""Returns (t, theta, final_state): t/theta are parallel lists of
|
||||
elapsed simulated seconds and temperature, covering doc['steps']
|
||||
from the start all the way to the end (final_state is
|
||||
SudState.DONE), or, in the pathological case of a step whose
|
||||
target can never actually be reached, wherever MAX_TICKS cut the
|
||||
simulation off.
|
||||
"""Returns (t, theta, final_state, step_starts): t/theta are
|
||||
parallel lists of elapsed simulated seconds and temperature,
|
||||
covering doc['steps'] from the start all the way to the end
|
||||
(final_state is SudState.DONE), or, in the pathological case of
|
||||
a step whose target can never actually be reached, wherever
|
||||
MAX_TICKS cut the simulation off. step_starts maps each step's
|
||||
index in doc['steps'] (0-based, local to this doc - the caller
|
||||
rebases onto its own absolute schedule indices/timeline, same as
|
||||
it does for t/theta themselves) to the simulated second it began
|
||||
at - consulted by tasks/sud.py's SudTask to show each step's
|
||||
predicted total/remaining duration (the GUI's Progress tab).
|
||||
|
||||
A step requiring user confirmation doesn't stop the simulation
|
||||
either - a human's response time genuinely can't be forecast,
|
||||
@@ -90,7 +95,7 @@ class SudForecastEstimator:
|
||||
|
||||
sud = Sud()
|
||||
if not sud.load(doc) or not sud.schedule:
|
||||
return [0.0], [start_theta], SudState.DONE
|
||||
return [0.0], [start_theta], SudState.DONE, {}
|
||||
|
||||
# Plant params (M/C/L/Td) are deliberately *not* seeded here from
|
||||
# any default - sud.start() below synchronously fires the first
|
||||
@@ -142,9 +147,16 @@ class SudForecastEstimator:
|
||||
pulse_counter = 0
|
||||
return power_low if (power == 0 or pulse_counter >= on_count) else power_high
|
||||
|
||||
step_starts = {}
|
||||
|
||||
def on_step_changed(step):
|
||||
if step is None:
|
||||
return
|
||||
# setdefault: this callback also re-fires for the same step's
|
||||
# ramp->hold phase switch (components/sud.py's temp_reached()
|
||||
# re-assigns self.step to retrigger it) - only the *first* call
|
||||
# for a given index is its actual start.
|
||||
step_starts.setdefault(sud.index, t[-1])
|
||||
params = sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0))
|
||||
pot.set_plant_params(params)
|
||||
if hasattr(tc, 'set_model_plant_params'):
|
||||
@@ -181,4 +193,4 @@ class SudForecastEstimator:
|
||||
theta.append(pot.get_temperature())
|
||||
ticks += 1
|
||||
|
||||
return t, theta, sud.state
|
||||
return t, theta, sud.state, step_starts
|
||||
|
||||
Reference in New Issue
Block a user