safety commit

This commit is contained in:
2026-07-06 20:16:51 +02:00
parent 02e51717e8
commit 28ae97fa45
8 changed files with 806 additions and 34 deletions

View File

@@ -3,6 +3,7 @@
#include <Arduino.h>
#include "config.h"
#include "fan_step_response.h"
#include "pid_autotuner.h"
#include "pid_controller.h"
#include "settings_store.h"
@@ -10,11 +11,12 @@
class ThermalController {
public:
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Allow, Autotune };
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Allow, Autotune, StepResp };
ThermalController()
: pid_(PID_KP, PID_KI, PID_KD, 0.0f, 100.0f),
autotuner_(),
stepresp_(),
targetTempC_(TARGET_TEMP_C),
heaterDutyPercent_(0.0f),
heaterAllowancePercent_(100.0f),
@@ -40,6 +42,7 @@ public:
pinMode(FAN_PIN, OUTPUT);
pinMode(HEATER_PIN, OUTPUT);
digitalWrite(HEATER_PIN, LOW);
writeFan(0);
pid_.setSetpoint(targetTempC_);
pid_.reset();
@@ -105,7 +108,7 @@ public:
bool isAdaptive() const { return adaptiveEnabled_; }
bool startAutotune(float setpointC) {
if (autotuner_.isActive()) {
if (autotuner_.isActive() || stepresp_.isActive()) {
return false;
}
adaptiveEnabled_ = false;
@@ -128,6 +131,46 @@ public:
float autotunePreheatTargetC() const { return autotuner_.preheatTargetC(); }
bool startStepResponse(float targetC, float heaterPct) {
if (autotuner_.isActive() || stepresp_.isActive()) {
return false;
}
stopFanTest();
adaptiveEnabled_ = false;
cutoffActive_ = false;
pid_.reset();
setTarget(targetC, false);
if (!stepresp_.start(targetC, heaterPct)) {
return false;
}
writeFan(0);
return true;
}
void stopStepResponse() {
stepresp_.abort();
writeFan(0);
}
bool isStepResponseActive() const { return stepresp_.isActive(); }
uint32_t stepResponseElapsedMs(uint32_t nowMs) const { return stepresp_.elapsedMs(nowMs); }
const char *stepResponsePhaseName() const { return stepresp_.phaseName(); }
uint8_t stepResponseStepIndex() const { return stepresp_.stepIndex(); }
uint8_t stepResponseStepCount() const { return stepresp_.stepCount(); }
float stepResponseHeaterPct() const { return stepresp_.heaterPct(); }
void logStepResponseIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount,
float avgTempC, float minTempC, float maxTempC, float spreadC,
uint32_t nowMs) {
stepresp_.logIfDue(sensorTemps, sensorValid, sensorCount, avgTempC, minTempC, maxTempC,
spreadC, nowMs);
}
bool commitAutotuneIfDone() {
if (autotuner_.phase() != PidAutotuner::Phase::Done) {
return false;
@@ -206,7 +249,7 @@ public:
if (isIdle()) {
return INFINITY;
}
return targetTempC_ * (1.0f + OVERTEMP_FRACTION);
return EMERGENCY_MAX_TEMP_C;
}
bool isCutoffActive() const { return cutoffActive_; }
@@ -249,6 +292,8 @@ public:
return "allow";
case HeaterBlock::Autotune:
return "autotune";
case HeaterBlock::StepResp:
return "stepresp";
default:
return "none";
}
@@ -264,6 +309,11 @@ public:
SPREAD_EMA_ALPHA * cornerSpreadC +
(1.0f - SPREAD_EMA_ALPHA) * cornerSpreadC_;
if (stepresp_.isActive()) {
updateStepResponse(avgTempC, maxTempC, nowMs);
return;
}
if (autotuner_.isActive()) {
updateAutotune(avgTempC, maxTempC, nowMs);
return;
@@ -296,6 +346,7 @@ public:
applyFan(millis());
pid_.reset();
autotuner_.abort();
stepresp_.abort();
}
void forceHeaterOff() {
@@ -307,16 +358,51 @@ public:
void writeFan(uint8_t pwm) {
fanPwm_ = pwm;
if (pwm == 0) {
// Re-assert output and stop Timer0 PWM on D5 — analogWrite(0) can leave the pin driving
pinMode(FAN_PIN, OUTPUT);
digitalWrite(FAN_PIN, LOW);
} else {
analogWrite(FAN_PIN, pwm);
pinMode(FAN_PIN, OUTPUT);
if (FAN_PWM_INVERT) {
if (pwm == 0) {
digitalWrite(FAN_PIN, HIGH);
return;
}
if (pwm >= 254) {
digitalWrite(FAN_PIN, LOW);
return;
}
analogWrite(FAN_PIN, static_cast<uint8_t>(255 - pwm));
return;
}
if (pwm == 0) {
digitalWrite(FAN_PIN, LOW);
return;
}
if (pwm >= 254) {
digitalWrite(FAN_PIN, HIGH);
return;
}
analogWrite(FAN_PIN, pwm);
}
private:
void updateStepResponse(float avgTempC, float maxTempC, uint32_t nowMs) {
float duty = 0.0f;
uint8_t fan = 0;
stepresp_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan);
heaterDutyPercent_ = duty;
heaterAllowancePercent_ = duty;
heaterBlock_ = duty > 0.0f ? HeaterBlock::StepResp : HeaterBlock::None;
applyHeaterBurst(nowMs);
writeFan(fan);
lastHeaterUpdateMs_ = nowMs;
if (stepresp_.phase() == FanStepResponse::Phase::Done ||
stepresp_.phase() == FanStepResponse::Phase::Failed) {
stepresp_.reset();
}
}
void updateAutotune(float avgTempC, float maxTempC, uint32_t nowMs) {
float duty = 0.0f;
uint8_t fan = FAN_HEAT_MIN_PWM;
@@ -439,22 +525,10 @@ private:
}
float maxHeatStopTemp(float avgTempC) const {
if (avgTempC >= targetTempC_) {
return targetTempC_;
if (!shouldLimitMaxCorner(avgTempC)) {
return EMERGENCY_MAX_TEMP_C;
}
float stopAt = targetTempC_;
if (isBalancedChamber()) {
stopAt = targetTempC_ + BALANCED_MAX_ABOVE_TARGET_C;
} else {
stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f;
}
const float cutoff = cutoffThreshold();
if (stopAt > cutoff) {
stopAt = cutoff;
}
return stopAt;
return EMERGENCY_MAX_TEMP_C - CORNER_STOP_MARGIN_C;
}
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
@@ -614,7 +688,7 @@ private:
}
if (isIdle()) {
if (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
if (sensorWarmValid_ && lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
writeFan(FAN_MAX_PWM);
} else if (fanIdleOverride_) {
writeFan(FAN_IDLE_PWM);
@@ -635,6 +709,7 @@ private:
PidController pid_;
PidAutotuner autotuner_;
FanStepResponse stepresp_;
float targetTempC_;
float heaterDutyPercent_;
float heaterAllowancePercent_;