diff --git a/components/TODO.md b/components/TODO.md new file mode 100644 index 0000000..a0d9f53 --- /dev/null +++ b/components/TODO.md @@ -0,0 +1,58 @@ +# components/sud.py step lifecycle spec + +Confirmed spec for a `Sud` schedule step's internal phase order +(`components/sud.py`), from a design discussion clarifying how `ramp`/ +`hold`/user-confirm interact. + +## Phase order + +`ramp` -> `hold` -> user-confirm. Each step independently opts into +`ramp` and/or `hold` by including that key (`_build_step()`, +`sud.py:35-45`); the other phase is skipped entirely if its key is +absent. The `'ramp'` key's presence is the *only* trigger for ramping +(`_advance()`, `sud.py:192-210`) - there is no diff-based decision +(`T_actual` vs. `T_target`) anywhere that decides whether to ramp, and +no internal setpoint-trajectory limiter in the temperature controller +(`set_theta_soll()` is an instant, unconditional assignment, +`components/pid/temp_controller_base.py:81-82`). The `ramp` key both +gates ramping *and* carries its settings (`rate`); it does not merely +override settings on top of an always-on ramp. + +## Duration `0` + +Supported for hold: `hold_remaining = duration * 60.0` can be `0`; +`tick()` (`sud.py:186-190`) finishes the step on the next tick once +`hold_remaining <= 0`. + +## End-of-step branching + +`_finish_step()` (`sud.py:212-216`): no `user_wait_for_continue` -> +advances immediately to the next step; `user_wait_for_continue: true` +-> enters `WAIT_USER`, resumed only by `confirm()`. + +`WAIT_USER` is not "hold, prolonged": `HOLDING` and `WAIT_USER` are +distinct `SudState` values. The hold countdown runs to completion +*before* `WAIT_USER` begins - `tick()` only decrements +`hold_remaining` while `state == HOLDING`, so the timer is frozen +(not extended) during the wait. It delays the next step, but it's a +separate third phase, not a longer hold. + +The gating field is `user_wait_for_continue`, not "presence of a user +message": `user_message` (display text) and `user_wait_for_continue` +(the actual block) are independent fields - a step can show a message +without blocking, or block silently. + +## Open items + +- [ ] **Pure-hold steps never (re-)set the controller's target + temperature.** For a step with only `hold` (no `ramp`), + `tasks/sud.py`'s `on_step_changed` never calls `tc.set_theta_soll()` + - the guard at line 73 requires `ramp is not None`. The + controller's target temperature for a pure-hold step is whatever + was left from the previous step's ramp; there's no explicit + fallback to the hold step's own `temperature` field. `README.md` + (Mash schedules section) documents this as designed behavior + ("`SudTask` drives ... `theta_soll`/`heatrate_soll` from ramp + steps ... counts down hold steps' duration"), so it's intentional, + not a bug - but it means a schedule can't express a temperature + jump straight into a hold without a `ramp` block.