Merge branch 'main' of tesserakt.pro:alex/arduino-filament-dryer

This commit is contained in:
2026-07-05 19:02:18 +02:00
parent 3ab1b3e8fc
commit f614f75153
4 changed files with 171 additions and 47 deletions

View File

@@ -10,6 +10,8 @@
class ThermalController {
public:
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Allow, Autotune };
ThermalController()
: pid_(PID_KP, PID_KI, PID_KD, 0.0f, 100.0f),
autotuner_(),
@@ -27,7 +29,12 @@ public:
failSafeActive_(true),
heaterCycleStartMs_(0),
lastHeaterUpdateMs_(0),
heaterOn_(false) {}
heaterOn_(false),
lastAvgTempC_(0.0f),
heaterBlock_(HeaterBlock::None),
fanTestActive_(false),
fanTestPwm_(0),
fanTestEndMs_(0) {}
void begin() {
pinMode(FAN_PIN, OUTPUT);
@@ -47,7 +54,7 @@ public:
if (isIdle()) {
forceHeaterOff();
}
applyFan();
applyFan(millis());
TuningData stored;
if (tuningLoad(stored)) {
@@ -150,7 +157,7 @@ public:
forceHeaterOff();
fanIdleOverride_ = false;
}
applyFan();
applyFan(millis());
if (persist && targetC >= TARGET_MIN_C && targetC <= TARGET_MAX_C) {
settingsSaveTarget(targetC);
}
@@ -166,7 +173,7 @@ public:
return false;
}
fanIdleOverride_ = false;
applyFan();
applyFan(millis());
return true;
}
@@ -178,7 +185,7 @@ public:
return;
}
fanIdleOverride_ = true;
applyFan();
applyFan(millis());
}
bool isFanOff() const {
@@ -214,9 +221,44 @@ public:
uint8_t fanPwm() const { return fanPwm_; }
bool isHeaterSsrOn() const { return heaterOn_; }
bool isFanTestActive(uint32_t nowMs) const {
return fanTestActive_ && nowMs < fanTestEndMs_;
}
bool startFanTest(uint8_t pwm, uint32_t nowMs, uint32_t durationMs = 15000) {
fanTestPwm_ = pwm;
fanTestEndMs_ = nowMs + durationMs;
fanTestActive_ = true;
writeFan(pwm);
return true;
}
void stopFanTest() { fanTestActive_ = false; }
float maxHeatStopAt(float avgTempC) const { return maxHeatStopTemp(avgTempC); }
const char *heaterBlockReason() const {
switch (heaterBlock_) {
case HeaterBlock::Cutoff:
return "cutoff";
case HeaterBlock::Corner:
return "corner";
case HeaterBlock::Allow:
return "allow";
case HeaterBlock::Autotune:
return "autotune";
default:
return "none";
}
}
void update(float avgTempC, float maxTempC, float cornerSpreadC, uint32_t nowMs) {
failSafeActive_ = false;
noteSensorMax(maxTempC);
lastAvgTempC_ = avgTempC;
heaterBlock_ = HeaterBlock::None;
cornerSpreadC_ =
SPREAD_EMA_ALPHA * cornerSpreadC +
@@ -232,7 +274,7 @@ public:
cutoffActive_ = false;
pid_.reset();
lastHeaterUpdateMs_ = nowMs;
applyFan();
applyFan(nowMs);
return;
}
@@ -244,14 +286,14 @@ public:
lastHeaterUpdateMs_ = nowMs;
applyHeaterBurst(nowMs);
applyFan();
applyFan(nowMs);
}
void enterFailSafe() {
failSafeActive_ = true;
cutoffActive_ = false;
forceHeaterOff();
applyFan();
applyFan(millis());
pid_.reset();
autotuner_.abort();
}
@@ -284,6 +326,7 @@ private:
heaterAllowancePercent_ = duty;
heaterOn_ = duty >= 50.0f;
digitalWrite(HEATER_PIN, heaterOn_ ? HIGH : LOW);
heaterBlock_ = duty > 0.0f ? HeaterBlock::None : HeaterBlock::Autotune;
writeFan(fan);
lastHeaterUpdateMs_ = nowMs;
commitAutotuneIfDone();
@@ -297,6 +340,7 @@ private:
heaterDutyPercent_ = 0.0f;
heaterAllowancePercent_ = 0.0f;
heaterOn_ = false;
heaterBlock_ = HeaterBlock::Cutoff;
pid_.reset();
return;
}
@@ -307,18 +351,26 @@ private:
}
if (cutoffActive_) {
heaterBlock_ = HeaterBlock::Cutoff;
return;
}
const float maxHeatStopC = maxHeatStopTemp(avgTempC);
if (maxTempC >= maxHeatStopC) {
if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) {
heaterDutyPercent_ = 0.0f;
heaterAllowancePercent_ = 0.0f;
heaterBlock_ = HeaterBlock::Corner;
pid_.reset();
return;
}
float duty = pid_.compute(avgTempC, nowMs);
const float below = targetTempC_ - avgTempC;
if (below > 8.0f) {
const float floor = below > 15.0f ? 75.0f : 60.0f;
if (duty < floor) {
duty = floor;
}
}
const float maxDuty = heaterMaxDuty(avgTempC);
if (duty > maxDuty) {
duty = maxDuty;
@@ -335,6 +387,7 @@ private:
heaterDutyPercent_ = 0.0f;
heaterAllowancePercent_ = 0.0f;
heaterOn_ = false;
heaterBlock_ = HeaterBlock::Cutoff;
pid_.reset();
return;
}
@@ -345,13 +398,14 @@ private:
}
if (cutoffActive_) {
heaterBlock_ = HeaterBlock::Cutoff;
return;
}
const float maxHeatStopC = maxHeatStopTemp(avgTempC);
if (maxTempC >= maxHeatStopC) {
if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) {
heaterDutyPercent_ = 0.0f;
heaterAllowancePercent_ = 0.0f;
heaterBlock_ = HeaterBlock::Corner;
pid_.reset();
return;
}
@@ -362,6 +416,9 @@ private:
if (duty > heaterAllowancePercent_) {
duty = heaterAllowancePercent_;
}
if (duty <= 0.0f && heaterAllowancePercent_ <= 0.0f) {
heaterBlock_ = HeaterBlock::Allow;
}
heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs);
}
@@ -377,6 +434,10 @@ private:
bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; }
bool shouldLimitMaxCorner(float avgTempC) const {
return avgTempC >= targetTempC_ - CORNER_LIMIT_BAND_C;
}
float maxHeatStopTemp(float avgTempC) const {
if (avgTempC >= targetTempC_) {
return targetTempC_;
@@ -389,14 +450,18 @@ private:
stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f;
}
const float belowCutoff = cutoffThreshold() - 0.1f;
if (stopAt > belowCutoff) {
stopAt = belowCutoff;
const float cutoff = cutoffThreshold();
if (stopAt > cutoff) {
stopAt = cutoff;
}
return stopAt;
}
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
if (!shouldLimitMaxCorner(avgTempC)) {
return 100.0f;
}
if (isBalancedChamber() && avgTempC < targetTempC_) {
return 100.0f;
}
@@ -433,10 +498,10 @@ private:
}
const float below = targetTempC_ - avgTempC;
if (below > HEATER_COLD_BELOW_C) {
if (below >= HEATER_COLD_BELOW_C) {
return HEATER_MAX_DUTY_COLD;
}
if (below > HEATER_WARM_BELOW_C) {
if (below >= HEATER_WARM_BELOW_C) {
return HEATER_MAX_DUTY_MID;
}
return HEATER_MAX_DUTY_NEAR;
@@ -470,14 +535,35 @@ private:
return requestedDuty;
}
uint8_t fanPwmForHeaterDemand() const {
uint8_t fanPwmForHeatUp() const {
if (heaterDutyPercent_ <= 0.0f) {
return 0;
}
const uint8_t span = FAN_HEAT_MAX_PWM - FAN_HEAT_MIN_PWM;
return FAN_HEAT_MIN_PWM +
static_cast<uint8_t>((heaterDutyPercent_ / 100.0f) * static_cast<float>(span));
const float below = targetTempC_ - lastAvgTempC_;
uint8_t heatFan = 0;
if (below <= FAN_OFF_BELOW_TARGET_C) {
const uint8_t span = FAN_HEAT_MAX_PWM - FAN_HEAT_MIN_PWM;
heatFan = FAN_HEAT_MIN_PWM +
static_cast<uint8_t>((heaterDutyPercent_ / 100.0f) * static_cast<float>(span));
} else if (below < FAN_RAMP_BELOW_TARGET_C) {
const float spanC = FAN_RAMP_BELOW_TARGET_C - FAN_OFF_BELOW_TARGET_C;
const float t = (FAN_RAMP_BELOW_TARGET_C - below) / spanC;
heatFan = static_cast<uint8_t>(t * static_cast<float>(FAN_HEAT_MIN_PWM));
}
uint8_t mixFan = 0;
if (below <= FAN_OFF_BELOW_TARGET_C || cornerSpreadC_ > GOOD_SPREAD_C) {
mixFan = fanPwmForCornerSpread();
}
uint8_t duty = heatFan > mixFan ? heatFan : mixFan;
if (lastAvgTempC_ >= targetTempC_ - 2.0f && lastMaxTempC_ > targetTempC_ &&
duty < FAN_MAX_PWM) {
duty = FAN_MAX_PWM;
}
return duty;
}
uint8_t fanPwmForCornerSpread() const {
@@ -518,7 +604,15 @@ private:
}
}
void applyFan() {
void applyFan(uint32_t nowMs) {
if (fanTestActive_) {
if (nowMs < fanTestEndMs_) {
writeFan(fanTestPwm_);
return;
}
fanTestActive_ = false;
}
if (isIdle()) {
if (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
writeFan(FAN_MAX_PWM);
@@ -535,16 +629,7 @@ private:
return;
}
uint8_t duty = fanPwmForHeaterDemand();
const uint8_t mixFan = fanPwmForCornerSpread();
if (mixFan > duty) {
duty = mixFan;
}
if (lastMaxTempC_ > targetTempC_ && duty < FAN_MAX_PWM) {
duty = FAN_MAX_PWM;
}
uint8_t duty = fanPwmForHeatUp();
writeFan(duty);
}
@@ -565,4 +650,9 @@ private:
uint32_t heaterCycleStartMs_;
uint32_t lastHeaterUpdateMs_;
bool heaterOn_;
float lastAvgTempC_;
HeaterBlock heaterBlock_;
bool fanTestActive_;
uint8_t fanTestPwm_;
uint32_t fanTestEndMs_;
};