Merge branch 'main' of tesserakt.pro:alex/arduino-filament-dryer
This commit is contained in:
@@ -33,11 +33,13 @@ static const float PID_KI = 0.05f;
|
||||
static const float PID_KD = 6.0f;
|
||||
|
||||
// Tiered heater cap — more power when cold, gentle near setpoint
|
||||
static const float HEATER_MAX_DUTY_COLD = 65.0f; // avg >10 °C below target
|
||||
static const float HEATER_MAX_DUTY_MID = 50.0f; // avg 3–10 °C below target
|
||||
static const float HEATER_MAX_DUTY_NEAR = 42.0f; // avg <3 °C below target
|
||||
static const float HEATER_MAX_DUTY_COLD = 85.0f; // avg >=10 °C below target
|
||||
static const float HEATER_MAX_DUTY_MID = 65.0f; // avg 3–10 °C below target
|
||||
static const float HEATER_MAX_DUTY_NEAR = 45.0f; // avg <3 °C below target
|
||||
static const float HEATER_COLD_BELOW_C = 10.0f;
|
||||
static const float HEATER_WARM_BELOW_C = 3.0f;
|
||||
// Below this band from target, only the hard cutoff limits max-corner (full heat-up)
|
||||
static const float CORNER_LIMIT_BAND_C = 10.0f;
|
||||
|
||||
// Ramp-up limit (% per second) — still caps sudden jumps
|
||||
static const float HEATER_SLEW_UP_PER_S = 18.0f;
|
||||
@@ -59,8 +61,10 @@ static const uint16_t HEATER_CYCLE_MS = 3000;
|
||||
static const uint8_t FAN_IDLE_PWM = 77; // ~30 % — optional override via "fan on"
|
||||
static const float IDLE_AUTO_FAN_OFF_TEMP_C = 40.0f; // idle: fans off when max corner below this
|
||||
static const uint8_t FAN_MIX_MIN_PWM = 70; // ~27 % — light mixing when spread rises
|
||||
static const uint8_t FAN_HEAT_MIN_PWM = 100; // ~39 % — floor while heating
|
||||
static const uint8_t FAN_HEAT_MIN_PWM = 100; // ~39 % — floor while heating (near setpoint)
|
||||
static const uint8_t FAN_HEAT_MAX_PWM = 140; // ~55 % — cap during heat-up
|
||||
static const float FAN_OFF_BELOW_TARGET_C = 8.0f; // no heat-up fan when avg this far below target
|
||||
static const float FAN_RAMP_BELOW_TARGET_C = 15.0f; // fan ramps in between this and FAN_OFF_BELOW
|
||||
static const uint8_t FAN_MIX_MAX_PWM = 200; // ~78 % — cap for spread-driven mixing
|
||||
static const uint8_t FAN_MAX_PWM = 255; // failsafe / over-temp only
|
||||
|
||||
@@ -73,8 +77,7 @@ static const float SPREAD_EMA_ALPHA = 0.45f;
|
||||
static const float AUTOTUNE_HYSTERESIS_C = 0.4f;
|
||||
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
||||
static const float AUTOTUNE_PREHEAT_DUTY = 100.0f;
|
||||
static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 70; // light mixing only while preheating
|
||||
static const float AUTOTUNE_ABORT_ABOVE_C = 15.0f;
|
||||
static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 70; // low fan for entire autotune
|
||||
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
||||
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
||||
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
||||
|
||||
@@ -107,18 +107,14 @@ public:
|
||||
return phase_;
|
||||
}
|
||||
|
||||
if (maxTempC >= setpointC_ + AUTOTUNE_ABORT_ABOVE_C) {
|
||||
fail(F("autotune: abort — temperature too high"));
|
||||
if (maxTempC >= TARGET_MAX_C - 1.0f) {
|
||||
fail(F("autotune: abort — max sensor at safety limit"));
|
||||
return phase_;
|
||||
}
|
||||
|
||||
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
||||
fail(F("autotune: abort — session timeout"));
|
||||
return phase_;
|
||||
}
|
||||
fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM;
|
||||
|
||||
if (phase_ == Phase::Preheat) {
|
||||
fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM;
|
||||
if (nowMs - phaseStartMs_ > AUTOTUNE_PREHEAT_TIMEOUT_MS) {
|
||||
Serial.print(F("autotune: preheat failed — avg "));
|
||||
Serial.print(avgTempC, 1);
|
||||
@@ -136,6 +132,11 @@ public:
|
||||
return phase_;
|
||||
}
|
||||
|
||||
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
||||
fail(F("autotune: abort — session timeout"));
|
||||
return phase_;
|
||||
}
|
||||
|
||||
spreadSum_ += spreadC;
|
||||
++spreadSamples_;
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user