Add explicit initial grain_mass/water_mass to the Sud doc
Sud now parses top-level grain_mass/water_mass (sibling to pot_mass/ pot_material/L/Td, defaulting to 0/0) - what's actually in the pot before the brew starts, as opposed to default.step's grain_mass/water_mass, which is just inert template filler. All sude/*.json docs gain explicit values matching what their first step already resolved to. tasks/sud.py's apply_plant_params() now takes grain_mass/water_mass directly instead of a step dict: on_step_changed() still passes the active step's own (these vary as malt goes in/water boils off), but the Load handler now reads Sud.grain_mass/water_mass directly instead of parsing schedule[0]. The GUI's status-line mass preview (shown immediately on Load, before Start) does the same. Also fixes a latent bug found along the way: _continue_forecast_after_confirm()'s reconstructed sub-doc was missing L/Td/grain_mass/water_mass entirely, silently falling back to generic defaults instead of the real Sud's own values - invisible only because every current sude/*.json happens to use those same defaults. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DkkuG48uHFCGKe6dPSERFk
This commit is contained in:
+48
-18
@@ -285,6 +285,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.sud_name = ''
|
self.sud_name = ''
|
||||||
self.sud_schedule = []
|
self.sud_schedule = []
|
||||||
self.sud_pot_mass = 0
|
self.sud_pot_mass = 0
|
||||||
|
self.sud_grain_mass = 0
|
||||||
|
self.sud_water_mass = 0
|
||||||
self.sud_step_index = None
|
self.sud_step_index = None
|
||||||
self.sud_step_descr = None
|
self.sud_step_descr = None
|
||||||
self.sud_step_type = None
|
self.sud_step_type = None
|
||||||
@@ -488,6 +490,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.sud_name = ''
|
self.sud_name = ''
|
||||||
self.sud_schedule = []
|
self.sud_schedule = []
|
||||||
self.sud_pot_mass = 0
|
self.sud_pot_mass = 0
|
||||||
|
self.sud_grain_mass = 0
|
||||||
|
self.sud_water_mass = 0
|
||||||
self.sud_step_index = None
|
self.sud_step_index = None
|
||||||
self.sud_step_descr = None
|
self.sud_step_descr = None
|
||||||
self.sud_step_type = None
|
self.sud_step_type = None
|
||||||
@@ -847,37 +851,63 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
|
|||||||
self.forecast_plot.show_forecast(t_min, forecast['Theta'], self.sud_name, forecast['Finished'])
|
self.forecast_plot.show_forecast(t_min, forecast['Theta'], self.sud_name, forecast['Finished'])
|
||||||
|
|
||||||
def update_status_step_label(self):
|
def update_status_step_label(self):
|
||||||
if not self.sud_step_descr:
|
if self.sud_step_descr:
|
||||||
|
text = "Step {}: {}".format(self.sud_step_index, self.sud_step_descr)
|
||||||
|
if self.sud_step_type == 'hold':
|
||||||
|
remaining = max(self.sud_hold_remaining, 0.0)
|
||||||
|
text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60)
|
||||||
|
# sud_step_index is the same 0-based index Sud.index uses into
|
||||||
|
# its own schedule list (see components/sud.py) - self.
|
||||||
|
# sud_schedule is built from the very same doc, so it lines up
|
||||||
|
# directly; grain_mass/water_mass are already resolved against
|
||||||
|
# default.step there (see components/sud.py's _build_step()).
|
||||||
|
if 0 <= self.sud_step_index < len(self.sud_schedule):
|
||||||
|
step = self.sud_schedule[self.sud_step_index]
|
||||||
|
grain, water = step.get('grain_mass', 0), step.get('water_mass', 0)
|
||||||
|
else:
|
||||||
|
grain, water = self.sud_grain_mass, self.sud_water_mass
|
||||||
|
elif self.sud_schedule:
|
||||||
|
# Loaded but not started yet (no real Step message has
|
||||||
|
# arrived) - preview the doc's own initial grain_mass/
|
||||||
|
# water_mass right away instead of leaving the status line
|
||||||
|
# blank until Start, mirroring tasks/sud.py's SudTask.
|
||||||
|
# apply_plant_params() applying it to the real plant/
|
||||||
|
# controller immediately on Load too.
|
||||||
|
text = ""
|
||||||
|
grain, water = self.sud_grain_mass, self.sud_water_mass
|
||||||
|
else:
|
||||||
self.statusBar().clearMessage()
|
self.statusBar().clearMessage()
|
||||||
return
|
return
|
||||||
text = "Step {}: {}".format(self.sud_step_index, self.sud_step_descr)
|
|
||||||
if self.sud_step_type == 'hold':
|
pot = self.sud_pot_mass
|
||||||
remaining = max(self.sud_hold_remaining, 0.0)
|
mass_text = "Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg".format(
|
||||||
text += " ({:.0f}:{:02.0f} remaining)".format(remaining // 60, remaining % 60)
|
pot, water, grain, pot + water + grain)
|
||||||
# sud_step_index is the same 0-based index Sud.index uses into its
|
text = text + " " + mass_text if text else mass_text
|
||||||
# own schedule list (see components/sud.py) - self.sud_schedule is
|
|
||||||
# built from the very same doc, so it lines up directly; grain_mass/
|
|
||||||
# water_mass are already resolved against default.step there (see
|
|
||||||
# components/sud.py's _build_step()).
|
|
||||||
if self.sud_step_index is not None and 0 <= self.sud_step_index < len(self.sud_schedule):
|
|
||||||
step = self.sud_schedule[self.sud_step_index]
|
|
||||||
pot = self.sud_pot_mass
|
|
||||||
water = step.get('water_mass', 0)
|
|
||||||
grain = step.get('grain_mass', 0)
|
|
||||||
text += " Pot: {:.2f} kg, Water: {:.2f} kg, Grain {:.2f} kg, total {:.2f} kg".format(
|
|
||||||
pot, water, grain, pot + water + grain)
|
|
||||||
self.statusBar().showMessage(text)
|
self.statusBar().showMessage(text)
|
||||||
|
|
||||||
def update_sud_forecast(self, doc):
|
def update_sud_forecast(self, doc):
|
||||||
try:
|
try:
|
||||||
name, _, schedule, pot_mass, _, _, _ = Sud._parse_data(doc)
|
name, _, schedule, pot_mass, _, _, _, grain_mass, water_mass = Sud._parse_data(doc)
|
||||||
except (KeyError, TypeError):
|
except (KeyError, TypeError):
|
||||||
return
|
return
|
||||||
self.sud_name = name
|
self.sud_name = name
|
||||||
self.sud_schedule = schedule
|
self.sud_schedule = schedule
|
||||||
self.sud_pot_mass = pot_mass
|
self.sud_pot_mass = pot_mass
|
||||||
|
self.sud_grain_mass = grain_mass
|
||||||
|
self.sud_water_mass = water_mass
|
||||||
|
# A fresh Load means no step has actually started yet, regardless
|
||||||
|
# of whatever the previously loaded Sud last reported - don't wait
|
||||||
|
# for the server's own 'Step' (None) reset (asyncio.create_task()
|
||||||
|
# there means its arrival order relative to this doc isn't
|
||||||
|
# guaranteed) to clear a stale step from the *previous* schedule
|
||||||
|
# before showing this one's preview below.
|
||||||
|
self.sud_step_descr = None
|
||||||
|
self.sud_step_index = None
|
||||||
|
self.sud_step_type = None
|
||||||
|
self.sud_hold_remaining = 0.0
|
||||||
self.sud_empty = len(schedule) == 0
|
self.sud_empty = len(schedule) == 0
|
||||||
self.update_sud_actions()
|
self.update_sud_actions()
|
||||||
|
self.update_status_step_label()
|
||||||
if self.sud_empty:
|
if self.sud_empty:
|
||||||
self.forecast_plot.show_no_schedule()
|
self.forecast_plot.show_no_schedule()
|
||||||
return
|
return
|
||||||
|
|||||||
+19
-6
@@ -66,6 +66,14 @@ EMPTY_SUD = {
|
|||||||
# values brewpi.py used to hardcode for every brew alike.
|
# values brewpi.py used to hardcode for every brew alike.
|
||||||
'L': 0.2,
|
'L': 0.2,
|
||||||
'Td': 30,
|
'Td': 30,
|
||||||
|
# Initial grain_mass/water_mass - what's actually in the pot before
|
||||||
|
# the brew starts (e.g. water added but no malt yet), as opposed to
|
||||||
|
# default.step's grain_mass/water_mass, which is just inert template
|
||||||
|
# filler for steps that don't override it. Lets a caller derive
|
||||||
|
# starting plant params directly (see SudTask.recv()'s Load handler)
|
||||||
|
# without having to parse the first step out of the schedule.
|
||||||
|
'grain_mass': 0,
|
||||||
|
'water_mass': 0,
|
||||||
'steps': [],
|
'steps': [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +88,8 @@ class Sud(AttributeChange):
|
|||||||
self._data = EMPTY_SUD
|
self._data = EMPTY_SUD
|
||||||
|
|
||||||
(self.name, self.description, self.schedule, self.pot_mass,
|
(self.name, self.description, self.schedule, self.pot_mass,
|
||||||
self.pot_material, self.L, self.Td) = self._parse_data(self._data)
|
self.pot_material, self.L, self.Td, self.grain_mass,
|
||||||
|
self.water_mass) = self._parse_data(self._data)
|
||||||
|
|
||||||
self._paused_from = None
|
self._paused_from = None
|
||||||
self._reset_run_state()
|
self._reset_run_state()
|
||||||
@@ -88,9 +97,10 @@ class Sud(AttributeChange):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _parse_data(data):
|
def _parse_data(data):
|
||||||
"""Parses a sud.json document into the (name, description, schedule,
|
"""Parses a sud.json document into the (name, description, schedule,
|
||||||
pot_mass, pot_material, L, Td) tuple Sud needs. Computed up front
|
pot_mass, pot_material, L, Td, grain_mass, water_mass) tuple Sud
|
||||||
rather than assigned straight onto self, so a malformed load()
|
needs. Computed up front rather than assigned straight onto self,
|
||||||
can't leave a half-applied schedule in place."""
|
so a malformed load() can't leave a half-applied schedule in
|
||||||
|
place."""
|
||||||
name = data.get('Name', '')
|
name = data.get('Name', '')
|
||||||
description = data.get('Description', '')
|
description = data.get('Description', '')
|
||||||
|
|
||||||
@@ -101,8 +111,10 @@ class Sud(AttributeChange):
|
|||||||
pot_material = data.get('pot_material')
|
pot_material = data.get('pot_material')
|
||||||
L = data.get('L', EMPTY_SUD['L'])
|
L = data.get('L', EMPTY_SUD['L'])
|
||||||
Td = data.get('Td', EMPTY_SUD['Td'])
|
Td = data.get('Td', EMPTY_SUD['Td'])
|
||||||
|
grain_mass = data.get('grain_mass', EMPTY_SUD['grain_mass'])
|
||||||
|
water_mass = data.get('water_mass', EMPTY_SUD['water_mass'])
|
||||||
|
|
||||||
return name, description, schedule, pot_mass, pot_material, L, Td
|
return name, description, schedule, pot_mass, pot_material, L, Td, grain_mass, water_mass
|
||||||
|
|
||||||
def _reset_run_state(self):
|
def _reset_run_state(self):
|
||||||
"""Resets run-time progress back to a freshly-loaded, not-yet-started
|
"""Resets run-time progress back to a freshly-loaded, not-yet-started
|
||||||
@@ -136,7 +148,8 @@ class Sud(AttributeChange):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
(self.name, self.description, self.schedule, self.pot_mass,
|
(self.name, self.description, self.schedule, self.pot_mass,
|
||||||
self.pot_material, self.L, self.Td) = parsed
|
self.pot_material, self.L, self.Td, self.grain_mass,
|
||||||
|
self.water_mass) = parsed
|
||||||
self._data = data
|
self._data = data
|
||||||
|
|
||||||
self._reset_run_state()
|
self._reset_run_state()
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"pot_material": "Edelstahl 18/10",
|
"pot_material": "Edelstahl 18/10",
|
||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
|
"grain_mass": 5.21,
|
||||||
|
"water_mass": 22,
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
"descr": "Put description here",
|
"descr": "Put description here",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"pot_material": "Edelstahl 18/10",
|
"pot_material": "Edelstahl 18/10",
|
||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
|
"grain_mass": 5.21,
|
||||||
|
"water_mass": 22,
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
"descr": "Put description here",
|
"descr": "Put description here",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"pot_material": "Edelstahl 18/10",
|
"pot_material": "Edelstahl 18/10",
|
||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
|
"grain_mass": 0,
|
||||||
|
"water_mass": 22,
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
"descr": "Put description here",
|
"descr": "Put description here",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"pot_material": "Edelstahl 18/10",
|
"pot_material": "Edelstahl 18/10",
|
||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
|
"grain_mass": 0,
|
||||||
|
"water_mass": 22,
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
"descr": "Put description here",
|
"descr": "Put description here",
|
||||||
|
|||||||
@@ -5,6 +5,8 @@
|
|||||||
"pot_material": "Edelstahl 18/10",
|
"pot_material": "Edelstahl 18/10",
|
||||||
"L": 0.2,
|
"L": 0.2,
|
||||||
"Td": 30,
|
"Td": 30,
|
||||||
|
"grain_mass": 0,
|
||||||
|
"water_mass": 10,
|
||||||
"default": {
|
"default": {
|
||||||
"step": {
|
"step": {
|
||||||
"descr": "Put description here",
|
"descr": "Put description here",
|
||||||
|
|||||||
+19
-12
@@ -55,17 +55,20 @@ class SudTask(ATask):
|
|||||||
self.forecast_confirm_marks = {}
|
self.forecast_confirm_marks = {}
|
||||||
msg_handler.set_recv_handler(self.recv)
|
msg_handler.set_recv_handler(self.recv)
|
||||||
|
|
||||||
def apply_plant_params(self, step):
|
def apply_plant_params(self, grain_mass, water_mass):
|
||||||
"""Keeps the real plant's and the controller's internal model's
|
"""Keeps the real plant's and the controller's internal model's
|
||||||
plant params in sync with this Sud's own doc - L/Td come straight
|
plant params in sync with this Sud's own doc - L/Td come straight
|
||||||
from it (constant for the whole brew), while M/C also fold in the
|
from it (constant for the whole brew), while M/C also fold in the
|
||||||
step's grain_mass/water_mass, which vary over the brew's course
|
given grain_mass/water_mass, which vary over the brew's course
|
||||||
(malt going in, water boiling off) - mirrors demo_sud.py's
|
(malt going in, water boiling off) - mirrors demo_sud.py's
|
||||||
apply_plant_params(). Called both on every real step transition
|
apply_plant_params(). Called both on every real step transition
|
||||||
(via on_step_changed()) and once immediately on Load (see recv()),
|
(via on_step_changed(), with that step's own grain_mass/water_mass)
|
||||||
so the controller's behavior already matches the expected plant
|
and once immediately on Load (see recv(), with the doc's own
|
||||||
as soon as a Sud is loaded, not just once a run actually starts."""
|
initial Sud.grain_mass/water_mass - no need to parse the first
|
||||||
params = self.sud.derive_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0))
|
step out of the schedule for that), so the controller's behavior
|
||||||
|
already matches the expected plant as soon as a Sud is loaded,
|
||||||
|
not just once a run actually starts."""
|
||||||
|
params = self.sud.derive_plant_params(grain_mass, water_mass)
|
||||||
self.pot.set_plant_params(params)
|
self.pot.set_plant_params(params)
|
||||||
if hasattr(self.tc, 'set_model_plant_params'):
|
if hasattr(self.tc, 'set_model_plant_params'):
|
||||||
self.tc.set_model_plant_params(params)
|
self.tc.set_model_plant_params(params)
|
||||||
@@ -95,7 +98,7 @@ class SudTask(ATask):
|
|||||||
phase = ramp if ramping else hold
|
phase = ramp if ramping else hold
|
||||||
|
|
||||||
if step is not None:
|
if step is not None:
|
||||||
self.apply_plant_params(step)
|
self.apply_plant_params(step.get('grain_mass', 0), step.get('water_mass', 0))
|
||||||
if ramping and step['temperature'] is not None:
|
if ramping and step['temperature'] is not None:
|
||||||
self.tc.set_theta_soll(step['temperature'])
|
self.tc.set_theta_soll(step['temperature'])
|
||||||
self.tc.set_heatrate_soll(ramp['rate'])
|
self.tc.set_heatrate_soll(ramp['rate'])
|
||||||
@@ -214,6 +217,10 @@ class SudTask(ATask):
|
|||||||
'Description': self.sud.description,
|
'Description': self.sud.description,
|
||||||
'pot_mass': self.sud.pot_mass,
|
'pot_mass': self.sud.pot_mass,
|
||||||
'pot_material': self.sud.pot_material,
|
'pot_material': self.sud.pot_material,
|
||||||
|
'L': self.sud.L,
|
||||||
|
'Td': self.sud.Td,
|
||||||
|
'grain_mass': self.sud.grain_mass,
|
||||||
|
'water_mass': self.sud.water_mass,
|
||||||
'steps': schedule[index:],
|
'steps': schedule[index:],
|
||||||
}
|
}
|
||||||
start_theta = self.tc.get_theta_ist()
|
start_theta = self.tc.get_theta_ist()
|
||||||
@@ -264,12 +271,12 @@ class SudTask(ATask):
|
|||||||
# on_step_changed() only re-applies plant params once a
|
# on_step_changed() only re-applies plant params once a
|
||||||
# real step starts (Start) - apply them right away too,
|
# real step starts (Start) - apply them right away too,
|
||||||
# so the controller already matches this Sud's own pot/
|
# so the controller already matches this Sud's own pot/
|
||||||
# L/Td (and its first step's expected grain/water mass)
|
# L/Td/initial grain_mass/water_mass from the moment
|
||||||
# from the moment it's loaded, rather than whatever the
|
# it's loaded, rather than whatever the previously
|
||||||
# previously loaded Sud (or the generic startup
|
# loaded Sud (or the generic startup baseline - see
|
||||||
# baseline - see server/brewpi.py) left behind.
|
# server/brewpi.py) left behind.
|
||||||
if self.sud.schedule:
|
if self.sud.schedule:
|
||||||
self.apply_plant_params(self.sud.schedule[0])
|
self.apply_plant_params(self.sud.grain_mass, self.sud.water_mass)
|
||||||
await self.send_forecast(pair[1])
|
await self.send_forecast(pair[1])
|
||||||
else:
|
else:
|
||||||
# Sud.load() refuses while a run is in progress (state
|
# Sud.load() refuses while a run is in progress (state
|
||||||
|
|||||||
Reference in New Issue
Block a user