Make FSM hysteresis thresholds configurable

THRESH_HOLD_IDLE/HOLD_HEAT/IDLE_HEAT/IDLE_HOLD/HEAT_HOLD/HEAT_IDLE in
tc_constants.py were hardcoded module-level constants shared by every
controller instance, so tuning the IDLE/HOLD/HEAT hysteresis required
a code change.

Replace them with DEFAULT_THRESHOLDS plus an optional
TempCtrl.Thresholds config section, merged at construction time in
TempControllerBase.__init__ so configs that omit the section keep
behaving exactly as before. Documented the new section in
config.json.templ; left config.json.sim without it to exercise the
default-fallback path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 22:09:39 +02:00
co-authored by Claude Sonnet 4.6
parent b2a5652290
commit 4256622e9d
4 changed files with 29 additions and 20 deletions
+5 -6
View File
@@ -5,12 +5,11 @@ Findings from a design review, not yet acted on. See git history on
correction enabled, the two TempController classes deduplicated into correction enabled, the two TempController classes deduplicated into
`temp_controller_base.py`). `temp_controller_base.py`).
- [ ] **FSM thresholds aren't configurable.** `tc_constants.py`'s - [x] **FSM thresholds aren't configurable.** Moved into an optional
`THRESH_HOLD_IDLE`, `THRESH_HOLD_HEAT`, `THRESH_IDLE_HEAT`, `TempCtrl.Thresholds` config section (`HoldIdle`, `HoldHeat`,
`THRESH_IDLE_HOLD`, `THRESH_HEAT_HOLD`, `THRESH_HEAT_IDLE` are `IdleHeat`, `IdleHold`, `HeatHold`, `HeatIdle`), merged over
hardcoded module-level constants shared by every controller `DEFAULT_THRESHOLDS` in `tc_constants.py` so existing configs without
instance. Move them into `config.json`'s `TempCtrl` section like the the section keep working unchanged.
PID gains, so mash schedules can tune hysteresis without code edits.
- [ ] **`Pid.scale()` is a gain-scheduling hack, not anti-windup.** - [ ] **`Pid.scale()` is a gain-scheduling hack, not anti-windup.**
`pid.py`'s `scale(k)` multiplies every gain by `1/heatrate_soll_set` `pid.py`'s `scale(k)` multiplies every gain by `1/heatrate_soll_set`
+8 -6
View File
@@ -8,12 +8,14 @@ class States(Enum):
HOLD = 2 HOLD = 2
THRESH_HOLD_IDLE = 0.1 DEFAULT_THRESHOLDS = {
THRESH_HOLD_HEAT = 1.0 "HoldIdle": 0.1,
THRESH_IDLE_HEAT = 1.0 "HoldHeat": 1.0,
THRESH_IDLE_HOLD = 0.1 "IdleHeat": 1.0,
THRESH_HEAT_HOLD = 1.0 "IdleHold": 0.1,
THRESH_HEAT_IDLE = 1.0 "HeatHold": 1.0,
"HeatIdle": 1.0
}
class Test: class Test:
+8 -8
View File
@@ -1,7 +1,6 @@
from components import APid from components import APid
from components.pid.pid import Pid from components.pid.pid import Pid
from components.pid.tc_constants import States, THRESH_HOLD_IDLE, THRESH_HOLD_HEAT, \ from components.pid.tc_constants import States, DEFAULT_THRESHOLDS
THRESH_IDLE_HEAT, THRESH_IDLE_HOLD, THRESH_HEAT_HOLD, THRESH_HEAT_IDLE
class TempControllerBase(APid): class TempControllerBase(APid):
@@ -17,6 +16,7 @@ class TempControllerBase(APid):
self.theta_ist = 0 self.theta_ist = 0
self.heatrate_ist = 0 self.heatrate_ist = 0
self.params = params self.params = params
self.thresholds = {**DEFAULT_THRESHOLDS, **params.get('Thresholds', {})}
self.y = -1 self.y = -1
self.state = States.INIT self.state = States.INIT
self.use_kalman = True self.use_kalman = True
@@ -72,22 +72,22 @@ class TempControllerBase(APid):
if not self.is_startup: if not self.is_startup:
state_next = States.IDLE state_next = States.IDLE
elif self.state == States.IDLE: elif self.state == States.IDLE:
if diff >= THRESH_IDLE_HEAT: if diff >= self.thresholds['IdleHeat']:
state_next = States.HEAT state_next = States.HEAT
self.pid_rate.reset() self.pid_rate.reset()
elif diff >= -THRESH_IDLE_HOLD: elif diff >= -self.thresholds['IdleHold']:
state_next = States.HOLD state_next = States.HOLD
self.pid_rate.reset() self.pid_rate.reset()
elif self.state == States.HOLD: elif self.state == States.HOLD:
if diff >= THRESH_HOLD_HEAT: if diff >= self.thresholds['HoldHeat']:
state_next = States.HEAT state_next = States.HEAT
self.pid_rate.reset() self.pid_rate.reset()
elif diff <= -THRESH_HOLD_IDLE: elif diff <= -self.thresholds['HoldIdle']:
state_next = States.IDLE state_next = States.IDLE
elif self.state == States.HEAT: elif self.state == States.HEAT:
if diff <= -THRESH_HEAT_IDLE: if diff <= -self.thresholds['HeatIdle']:
state_next = States.IDLE state_next = States.IDLE
elif diff <= THRESH_HEAT_HOLD: elif diff <= self.thresholds['HeatHold']:
state_next = States.HOLD state_next = States.HOLD
self.pid_hold.reset() self.pid_hold.reset()
+8
View File
@@ -25,6 +25,14 @@
"ki": 0.01, "ki": 0.01,
"kd": 0.0, "kd": 0.0,
"kt": 1.5 "kt": 1.5
},
"Thresholds": {
"HoldIdle": 0.1,
"HoldHeat": 1.0,
"IdleHeat": 1.0,
"IdleHold": 0.1,
"HeatHold": 1.0,
"HeatIdle": 1.0
} }
}, },
"Model": { "Model": {