Redesign Sud schedule format: separate ramp/hold keys with defaults

Steps now declare a "ramp" or "hold" key instead of a flat "type", and
unspecified fields (including nested stirrer settings) fall back to a
new top-level default.step structure. Stirrer config is now
interval_time/on_ratio, mapping directly onto AStirrer's cycle
time/duty cycle instead of separate on/off durations. Update
components/sud.py, tasks/sud.py, the Sud demo, sude/sud_0010.json, and
the README to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgR9tPaSzFkAwRAUyeaaCD
This commit is contained in:
2026-06-20 07:20:52 +02:00
co-authored by Claude Sonnet 4.6
parent 2dcf644bed
commit 123318ac85
5 changed files with 172 additions and 107 deletions
+30 -3
View File
@@ -20,6 +20,31 @@ class SudState(Enum):
DONE = 4
def _merge_defaults(default, override):
"""Deep-merges override onto default, recursing into nested dicts so
values missing from override fall back to default's at every level."""
merged = dict(default)
for key, value in override.items():
if key in merged and isinstance(merged[key], dict) and isinstance(value, dict):
merged[key] = _merge_defaults(merged[key], value)
else:
merged[key] = value
return merged
def _build_step(defaults, raw_step):
"""Fills in a schedule step with default.step's values. A step is a
ramp or a hold depending on which of those keys it specifies; the other
is left out rather than synthesized from defaults."""
step = {}
for key in ('descr', 'user_message', 'user_wait_for_continue'):
step[key] = raw_step.get(key, defaults.get(key))
for key in ('ramp', 'hold'):
if key in raw_step:
step[key] = _merge_defaults(defaults.get(key, {}), raw_step[key])
return step
class Sud(AttributeChange):
def __init__(self, path):
AttributeChange.__init__(self)
@@ -28,7 +53,9 @@ class Sud(AttributeChange):
self.name = data.get('Name', '')
self.description = data.get('Description', '')
self.schedule = data['schedule']
step_defaults = data.get('default', {}).get('step', {})
self.schedule = [_build_step(step_defaults, raw) for raw in data['steps']]
self.pot_mass = data.get('pot_mass', 0)
self.pot_material = data.get('pot_material')
@@ -84,10 +111,10 @@ class Sud(AttributeChange):
return
next_step = self.schedule[self.index]
if next_step['type'] == 'heat':
if 'ramp' in next_step:
self.state = SudState.RAMPING
else:
self.hold_remaining = next_step.get('duration', 0)
self.hold_remaining = next_step['hold'].get('duration', 0)
self.state = SudState.HOLDING
self.user_message = next_step.get('user_message')