Allow a Sud step to combine a ramp and a hold
A step can now carry both 'ramp' and 'hold': it ramps to the target temp, then holds there for the given duration, instead of needing two separate schedule entries. Sud.temp_reached() switches such a step from its ramp phase into its hold phase in place (re-firing the step-changed callback so SudTask/demo_sud re-apply the hold's stirrer settings); user_wait_for_continue still applies once the whole step - both phases - is done. Merges sud_0010.json's three ramp/hold pairs into combined steps.
This commit is contained in:
@@ -129,13 +129,19 @@ environment (`$WORKON_HOME`/`$BREWPI_HOME`) on a Raspberry Pi-style deployment.
|
||||
## Mash schedules
|
||||
|
||||
Files under `sude/` describe a brew's mash schedule ("Sud"): `pot_mass`,
|
||||
`pot_material` (fixed for the whole brew), and a `steps` list, each either:
|
||||
`pot_material` (fixed for the whole brew), and a `steps` list, each a ramp, a
|
||||
hold, or both:
|
||||
|
||||
- a ramp — `"ramp": {"rate": ..., "temp": ...}` — ramp to `temp` at `rate`
|
||||
(°C/min), or
|
||||
(°C/min), and/or
|
||||
- a hold — `"hold": {"duration": ...}` — hold the current target for
|
||||
`duration` minutes (omit/`0` for an immediate step).
|
||||
|
||||
A step with both ramps to `temp` and then holds there for `duration` -
|
||||
useful for a mash rest ("ramp to 63°C, then hold 40 min") without needing two
|
||||
separate schedule entries. `user_wait_for_continue`/`user_message` (below)
|
||||
apply once the whole step is done, i.e. after the hold phase if there is one.
|
||||
|
||||
Both `ramp` and `hold` carry a `stirrer` block (`speed`, `interval_time`,
|
||||
`on_ratio`): `interval_time: 0` runs the stirrer continuously at `speed`;
|
||||
`interval_time > 0` pulses it on a period of `interval_time` seconds, on for
|
||||
|
||||
@@ -130,7 +130,8 @@ class SudForecastPlot(FigureCanvasQTAgg):
|
||||
def _estimate_course(schedule, start_theta):
|
||||
"""Walks the schedule, accumulating (t, theta) waypoints: ramps take
|
||||
abs(delta)/rate minutes at the declared rate, holds last for their
|
||||
duration at the temperature the preceding ramp reached."""
|
||||
duration at the temperature the preceding ramp reached. A step may
|
||||
carry both - ramp then hold - contributing both segments in turn."""
|
||||
t = [0.0]
|
||||
theta = [start_theta]
|
||||
for step in schedule:
|
||||
@@ -142,7 +143,7 @@ class SudForecastPlot(FigureCanvasQTAgg):
|
||||
if rate > 0:
|
||||
t.append(t[-1] + abs(target - theta[-1]) / rate * 60.0)
|
||||
theta.append(target)
|
||||
elif hold is not None:
|
||||
if hold is not None:
|
||||
t.append(t[-1] + hold.get('duration', 0) * 60.0)
|
||||
theta.append(theta[-1])
|
||||
return t, theta
|
||||
|
||||
+12
-1
@@ -157,7 +157,18 @@ class Sud(AttributeChange):
|
||||
self._advance()
|
||||
|
||||
def temp_reached(self):
|
||||
if self.state == SudState.RAMPING:
|
||||
if self.state != SudState.RAMPING:
|
||||
return
|
||||
if 'hold' in self.step:
|
||||
# Same step, ramp phase done - move into its hold phase rather
|
||||
# than finishing the step. Re-assign self.step (AttributeChange
|
||||
# fires on every assignment, not just on change) to re-trigger
|
||||
# the step-changed callback so e.g. the hold's stirrer settings
|
||||
# get (re-)applied.
|
||||
self.hold_remaining = self.step['hold'].get('duration', 0) * 60.0
|
||||
self.state = SudState.HOLDING
|
||||
self.step = self.step
|
||||
else:
|
||||
self._finish_step()
|
||||
|
||||
def tick(self, dt):
|
||||
|
||||
@@ -53,8 +53,8 @@ if __name__ == '__main__':
|
||||
heater.set_on_changed('power_set', ctrl.set_model_power)
|
||||
heater.set_on_changed('power_eff', plant.set_power)
|
||||
|
||||
def apply_stirrer(step):
|
||||
stirrer_cfg = step.get('ramp', step.get('hold', {})).get('stirrer', {})
|
||||
def apply_stirrer(phase):
|
||||
stirrer_cfg = phase.get('stirrer', {})
|
||||
speed = stirrer_cfg.get('speed', 0)
|
||||
interval_time = stirrer_cfg.get('interval_time', 0)
|
||||
on_ratio = stirrer_cfg.get('on_ratio', 1.0)
|
||||
@@ -72,13 +72,17 @@ if __name__ == '__main__':
|
||||
ctrl.set_model_params(params['M'], params['C'])
|
||||
|
||||
def on_step_changed(step):
|
||||
# A step may carry both 'ramp' and 'hold' (ramp to temp, then hold);
|
||||
# sud.state tells us which phase is currently active.
|
||||
if step is not None:
|
||||
ramping = sud.state == SudState.RAMPING
|
||||
phase = step['ramp'] if ramping and 'ramp' in step else step.get('hold', {})
|
||||
print(f"Step {step['number']}: {step['descr']}")
|
||||
apply_plant_params(step)
|
||||
if 'ramp' in step:
|
||||
if ramping and 'ramp' in step:
|
||||
ctrl.set_theta_soll(step['ramp']['temp'])
|
||||
ctrl.set_heatrate_soll(step['ramp']['rate'])
|
||||
apply_stirrer(step)
|
||||
apply_stirrer(phase)
|
||||
|
||||
def on_state_changed(state):
|
||||
if state == SudState.WAIT_USER and sud.user_message:
|
||||
|
||||
+12
-22
@@ -54,20 +54,17 @@
|
||||
{
|
||||
"descr": "Einmaischen Rast",
|
||||
"hold": {
|
||||
"duration": 2000
|
||||
"duration": 20
|
||||
}
|
||||
},
|
||||
{
|
||||
"descr": "Heizen Glucose-Rast",
|
||||
"descr": "Glucose-Rast",
|
||||
"ramp": {
|
||||
"rate": 1.0,
|
||||
"temp": 63
|
||||
}
|
||||
},
|
||||
{
|
||||
"descr": "Halten Glucose-Rast",
|
||||
},
|
||||
"hold": {
|
||||
"duration": 4000,
|
||||
"duration": 40,
|
||||
"stirrer": {
|
||||
"speed": 20,
|
||||
"interval_time": 90,
|
||||
@@ -76,16 +73,13 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"descr": "Heizen Verzuckerungs-Rast",
|
||||
"descr": "Verzuckerungs-Rast",
|
||||
"ramp": {
|
||||
"rate": 1.0,
|
||||
"temp": 72
|
||||
}
|
||||
},
|
||||
{
|
||||
"descr": "Halten Verzuckerungs-Rast",
|
||||
},
|
||||
"hold": {
|
||||
"duration": 3000,
|
||||
"duration": 30,
|
||||
"stirrer": {
|
||||
"speed": 35,
|
||||
"interval_time": 120
|
||||
@@ -94,19 +88,15 @@
|
||||
},
|
||||
{
|
||||
"water_mass": 20,
|
||||
"descr": "Heizen Abmaischen",
|
||||
"descr": "Abmaischen",
|
||||
"user_message": "Quittieren zum Abmaischen",
|
||||
"user_wait_for_continue": true,
|
||||
"ramp": {
|
||||
"rate": 1.0,
|
||||
"temp": 76
|
||||
}
|
||||
},
|
||||
{
|
||||
"water_mass": 20,
|
||||
"descr": "Halten Abmaischen",
|
||||
"user_message": "Quittieren zum Abmaischen",
|
||||
"user_wait_for_continue": true,
|
||||
},
|
||||
"hold": {
|
||||
"duration": 300,
|
||||
"duration": 30,
|
||||
"stirrer": {
|
||||
"speed": 50
|
||||
}
|
||||
|
||||
+14
-6
@@ -37,8 +37,8 @@ class SudTask(ATask):
|
||||
if hasattr(self.tc, 'set_model_params'):
|
||||
self.tc.set_model_params(params['M'], params['C'])
|
||||
|
||||
def apply_stirrer(self, step):
|
||||
stirrer_cfg = step.get('ramp', step.get('hold', {})).get('stirrer', {})
|
||||
def apply_stirrer(self, phase):
|
||||
stirrer_cfg = phase.get('stirrer', {})
|
||||
speed = stirrer_cfg.get('speed', 0)
|
||||
interval_time = stirrer_cfg.get('interval_time', 0)
|
||||
on_ratio = stirrer_cfg.get('on_ratio', 1.0)
|
||||
@@ -53,20 +53,28 @@ class SudTask(ATask):
|
||||
def on_step_changed(self, step):
|
||||
ramp = step.get('ramp') if step else None
|
||||
hold = step.get('hold') if step else None
|
||||
# A step may carry both 'ramp' and 'hold' (ramp to temp, then hold) -
|
||||
# self.sud.state tells us which phase is currently active; it's
|
||||
# already up to date by the time this callback fires, both on a
|
||||
# full step transition and on the ramp->hold phase switch within
|
||||
# one step (components/sud.py's temp_reached()).
|
||||
ramping = self.sud.state == SudState.RAMPING
|
||||
phase = ramp if ramping and ramp is not None else hold
|
||||
|
||||
if step is not None:
|
||||
self.apply_plant_params(step)
|
||||
if ramp is not None:
|
||||
if ramping and ramp is not None:
|
||||
self.tc.set_theta_soll(ramp['temp'])
|
||||
self.tc.set_heatrate_soll(ramp['rate'])
|
||||
self.apply_stirrer(step)
|
||||
self.apply_stirrer(phase)
|
||||
|
||||
asyncio.create_task(self.send({'Step': {
|
||||
'Index': self.sud.index,
|
||||
'Type': 'ramp' if ramp is not None else 'hold' if hold is not None else None,
|
||||
'Type': 'ramp' if ramping and ramp is not None else 'hold' if hold is not None else None,
|
||||
'Descr': step.get('descr') if step else None,
|
||||
'Temp': ramp.get('temp') if ramp else None,
|
||||
'Rate': ramp.get('rate') if ramp else None,
|
||||
'Duration': hold.get('duration') if hold else None,
|
||||
'Duration': hold.get('duration') if (hold is not None and not ramping) else None,
|
||||
'WaitForUser': step.get('user_wait_for_continue', False) if step else None,
|
||||
}}))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user