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;
|
static const float PID_KD = 6.0f;
|
||||||
|
|
||||||
// Tiered heater cap — more power when cold, gentle near setpoint
|
// 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_COLD = 85.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_MID = 65.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_NEAR = 45.0f; // avg <3 °C below target
|
||||||
static const float HEATER_COLD_BELOW_C = 10.0f;
|
static const float HEATER_COLD_BELOW_C = 10.0f;
|
||||||
static const float HEATER_WARM_BELOW_C = 3.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
|
// Ramp-up limit (% per second) — still caps sudden jumps
|
||||||
static const float HEATER_SLEW_UP_PER_S = 18.0f;
|
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 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 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_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 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_MIX_MAX_PWM = 200; // ~78 % — cap for spread-driven mixing
|
||||||
static const uint8_t FAN_MAX_PWM = 255; // failsafe / over-temp only
|
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_HYSTERESIS_C = 0.4f;
|
||||||
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
||||||
static const float AUTOTUNE_PREHEAT_DUTY = 100.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 uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 70; // low fan for entire autotune
|
||||||
static const float AUTOTUNE_ABORT_ABOVE_C = 15.0f;
|
|
||||||
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
||||||
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
||||||
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
||||||
|
|||||||
@@ -107,18 +107,14 @@ public:
|
|||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxTempC >= setpointC_ + AUTOTUNE_ABORT_ABOVE_C) {
|
if (maxTempC >= TARGET_MAX_C - 1.0f) {
|
||||||
fail(F("autotune: abort — temperature too high"));
|
fail(F("autotune: abort — max sensor at safety limit"));
|
||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM;
|
||||||
fail(F("autotune: abort — session timeout"));
|
|
||||||
return phase_;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (phase_ == Phase::Preheat) {
|
if (phase_ == Phase::Preheat) {
|
||||||
fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM;
|
|
||||||
if (nowMs - phaseStartMs_ > AUTOTUNE_PREHEAT_TIMEOUT_MS) {
|
if (nowMs - phaseStartMs_ > AUTOTUNE_PREHEAT_TIMEOUT_MS) {
|
||||||
Serial.print(F("autotune: preheat failed — avg "));
|
Serial.print(F("autotune: preheat failed — avg "));
|
||||||
Serial.print(avgTempC, 1);
|
Serial.print(avgTempC, 1);
|
||||||
@@ -136,6 +132,11 @@ public:
|
|||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
||||||
|
fail(F("autotune: abort — session timeout"));
|
||||||
|
return phase_;
|
||||||
|
}
|
||||||
|
|
||||||
spreadSum_ += spreadC;
|
spreadSum_ += spreadC;
|
||||||
++spreadSamples_;
|
++spreadSamples_;
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
class ThermalController {
|
class ThermalController {
|
||||||
public:
|
public:
|
||||||
|
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Allow, Autotune };
|
||||||
|
|
||||||
ThermalController()
|
ThermalController()
|
||||||
: pid_(PID_KP, PID_KI, PID_KD, 0.0f, 100.0f),
|
: pid_(PID_KP, PID_KI, PID_KD, 0.0f, 100.0f),
|
||||||
autotuner_(),
|
autotuner_(),
|
||||||
@@ -27,7 +29,12 @@ public:
|
|||||||
failSafeActive_(true),
|
failSafeActive_(true),
|
||||||
heaterCycleStartMs_(0),
|
heaterCycleStartMs_(0),
|
||||||
lastHeaterUpdateMs_(0),
|
lastHeaterUpdateMs_(0),
|
||||||
heaterOn_(false) {}
|
heaterOn_(false),
|
||||||
|
lastAvgTempC_(0.0f),
|
||||||
|
heaterBlock_(HeaterBlock::None),
|
||||||
|
fanTestActive_(false),
|
||||||
|
fanTestPwm_(0),
|
||||||
|
fanTestEndMs_(0) {}
|
||||||
|
|
||||||
void begin() {
|
void begin() {
|
||||||
pinMode(FAN_PIN, OUTPUT);
|
pinMode(FAN_PIN, OUTPUT);
|
||||||
@@ -47,7 +54,7 @@ public:
|
|||||||
if (isIdle()) {
|
if (isIdle()) {
|
||||||
forceHeaterOff();
|
forceHeaterOff();
|
||||||
}
|
}
|
||||||
applyFan();
|
applyFan(millis());
|
||||||
|
|
||||||
TuningData stored;
|
TuningData stored;
|
||||||
if (tuningLoad(stored)) {
|
if (tuningLoad(stored)) {
|
||||||
@@ -150,7 +157,7 @@ public:
|
|||||||
forceHeaterOff();
|
forceHeaterOff();
|
||||||
fanIdleOverride_ = false;
|
fanIdleOverride_ = false;
|
||||||
}
|
}
|
||||||
applyFan();
|
applyFan(millis());
|
||||||
if (persist && targetC >= TARGET_MIN_C && targetC <= TARGET_MAX_C) {
|
if (persist && targetC >= TARGET_MIN_C && targetC <= TARGET_MAX_C) {
|
||||||
settingsSaveTarget(targetC);
|
settingsSaveTarget(targetC);
|
||||||
}
|
}
|
||||||
@@ -166,7 +173,7 @@ public:
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
fanIdleOverride_ = false;
|
fanIdleOverride_ = false;
|
||||||
applyFan();
|
applyFan(millis());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +185,7 @@ public:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
fanIdleOverride_ = true;
|
fanIdleOverride_ = true;
|
||||||
applyFan();
|
applyFan(millis());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isFanOff() const {
|
bool isFanOff() const {
|
||||||
@@ -214,9 +221,44 @@ public:
|
|||||||
|
|
||||||
uint8_t fanPwm() const { return fanPwm_; }
|
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) {
|
void update(float avgTempC, float maxTempC, float cornerSpreadC, uint32_t nowMs) {
|
||||||
failSafeActive_ = false;
|
failSafeActive_ = false;
|
||||||
noteSensorMax(maxTempC);
|
noteSensorMax(maxTempC);
|
||||||
|
lastAvgTempC_ = avgTempC;
|
||||||
|
heaterBlock_ = HeaterBlock::None;
|
||||||
|
|
||||||
cornerSpreadC_ =
|
cornerSpreadC_ =
|
||||||
SPREAD_EMA_ALPHA * cornerSpreadC +
|
SPREAD_EMA_ALPHA * cornerSpreadC +
|
||||||
@@ -232,7 +274,7 @@ public:
|
|||||||
cutoffActive_ = false;
|
cutoffActive_ = false;
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
lastHeaterUpdateMs_ = nowMs;
|
lastHeaterUpdateMs_ = nowMs;
|
||||||
applyFan();
|
applyFan(nowMs);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -244,14 +286,14 @@ public:
|
|||||||
|
|
||||||
lastHeaterUpdateMs_ = nowMs;
|
lastHeaterUpdateMs_ = nowMs;
|
||||||
applyHeaterBurst(nowMs);
|
applyHeaterBurst(nowMs);
|
||||||
applyFan();
|
applyFan(nowMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
void enterFailSafe() {
|
void enterFailSafe() {
|
||||||
failSafeActive_ = true;
|
failSafeActive_ = true;
|
||||||
cutoffActive_ = false;
|
cutoffActive_ = false;
|
||||||
forceHeaterOff();
|
forceHeaterOff();
|
||||||
applyFan();
|
applyFan(millis());
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
autotuner_.abort();
|
autotuner_.abort();
|
||||||
}
|
}
|
||||||
@@ -284,6 +326,7 @@ private:
|
|||||||
heaterAllowancePercent_ = duty;
|
heaterAllowancePercent_ = duty;
|
||||||
heaterOn_ = duty >= 50.0f;
|
heaterOn_ = duty >= 50.0f;
|
||||||
digitalWrite(HEATER_PIN, heaterOn_ ? HIGH : LOW);
|
digitalWrite(HEATER_PIN, heaterOn_ ? HIGH : LOW);
|
||||||
|
heaterBlock_ = duty > 0.0f ? HeaterBlock::None : HeaterBlock::Autotune;
|
||||||
writeFan(fan);
|
writeFan(fan);
|
||||||
lastHeaterUpdateMs_ = nowMs;
|
lastHeaterUpdateMs_ = nowMs;
|
||||||
commitAutotuneIfDone();
|
commitAutotuneIfDone();
|
||||||
@@ -297,6 +340,7 @@ private:
|
|||||||
heaterDutyPercent_ = 0.0f;
|
heaterDutyPercent_ = 0.0f;
|
||||||
heaterAllowancePercent_ = 0.0f;
|
heaterAllowancePercent_ = 0.0f;
|
||||||
heaterOn_ = false;
|
heaterOn_ = false;
|
||||||
|
heaterBlock_ = HeaterBlock::Cutoff;
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -307,18 +351,26 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cutoffActive_) {
|
if (cutoffActive_) {
|
||||||
|
heaterBlock_ = HeaterBlock::Cutoff;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float maxHeatStopC = maxHeatStopTemp(avgTempC);
|
if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) {
|
||||||
if (maxTempC >= maxHeatStopC) {
|
|
||||||
heaterDutyPercent_ = 0.0f;
|
heaterDutyPercent_ = 0.0f;
|
||||||
heaterAllowancePercent_ = 0.0f;
|
heaterAllowancePercent_ = 0.0f;
|
||||||
|
heaterBlock_ = HeaterBlock::Corner;
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float duty = pid_.compute(avgTempC, nowMs);
|
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);
|
const float maxDuty = heaterMaxDuty(avgTempC);
|
||||||
if (duty > maxDuty) {
|
if (duty > maxDuty) {
|
||||||
duty = maxDuty;
|
duty = maxDuty;
|
||||||
@@ -335,6 +387,7 @@ private:
|
|||||||
heaterDutyPercent_ = 0.0f;
|
heaterDutyPercent_ = 0.0f;
|
||||||
heaterAllowancePercent_ = 0.0f;
|
heaterAllowancePercent_ = 0.0f;
|
||||||
heaterOn_ = false;
|
heaterOn_ = false;
|
||||||
|
heaterBlock_ = HeaterBlock::Cutoff;
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -345,13 +398,14 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cutoffActive_) {
|
if (cutoffActive_) {
|
||||||
|
heaterBlock_ = HeaterBlock::Cutoff;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float maxHeatStopC = maxHeatStopTemp(avgTempC);
|
if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) {
|
||||||
if (maxTempC >= maxHeatStopC) {
|
|
||||||
heaterDutyPercent_ = 0.0f;
|
heaterDutyPercent_ = 0.0f;
|
||||||
heaterAllowancePercent_ = 0.0f;
|
heaterAllowancePercent_ = 0.0f;
|
||||||
|
heaterBlock_ = HeaterBlock::Corner;
|
||||||
pid_.reset();
|
pid_.reset();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -362,6 +416,9 @@ private:
|
|||||||
if (duty > heaterAllowancePercent_) {
|
if (duty > heaterAllowancePercent_) {
|
||||||
duty = heaterAllowancePercent_;
|
duty = heaterAllowancePercent_;
|
||||||
}
|
}
|
||||||
|
if (duty <= 0.0f && heaterAllowancePercent_ <= 0.0f) {
|
||||||
|
heaterBlock_ = HeaterBlock::Allow;
|
||||||
|
}
|
||||||
heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs);
|
heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,6 +434,10 @@ private:
|
|||||||
|
|
||||||
bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; }
|
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 {
|
float maxHeatStopTemp(float avgTempC) const {
|
||||||
if (avgTempC >= targetTempC_) {
|
if (avgTempC >= targetTempC_) {
|
||||||
return targetTempC_;
|
return targetTempC_;
|
||||||
@@ -389,14 +450,18 @@ private:
|
|||||||
stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f;
|
stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f;
|
||||||
}
|
}
|
||||||
|
|
||||||
const float belowCutoff = cutoffThreshold() - 0.1f;
|
const float cutoff = cutoffThreshold();
|
||||||
if (stopAt > belowCutoff) {
|
if (stopAt > cutoff) {
|
||||||
stopAt = belowCutoff;
|
stopAt = cutoff;
|
||||||
}
|
}
|
||||||
return stopAt;
|
return stopAt;
|
||||||
}
|
}
|
||||||
|
|
||||||
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
||||||
|
if (!shouldLimitMaxCorner(avgTempC)) {
|
||||||
|
return 100.0f;
|
||||||
|
}
|
||||||
|
|
||||||
if (isBalancedChamber() && avgTempC < targetTempC_) {
|
if (isBalancedChamber() && avgTempC < targetTempC_) {
|
||||||
return 100.0f;
|
return 100.0f;
|
||||||
}
|
}
|
||||||
@@ -433,10 +498,10 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
const float below = targetTempC_ - avgTempC;
|
const float below = targetTempC_ - avgTempC;
|
||||||
if (below > HEATER_COLD_BELOW_C) {
|
if (below >= HEATER_COLD_BELOW_C) {
|
||||||
return HEATER_MAX_DUTY_COLD;
|
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_MID;
|
||||||
}
|
}
|
||||||
return HEATER_MAX_DUTY_NEAR;
|
return HEATER_MAX_DUTY_NEAR;
|
||||||
@@ -470,14 +535,35 @@ private:
|
|||||||
return requestedDuty;
|
return requestedDuty;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t fanPwmForHeaterDemand() const {
|
uint8_t fanPwmForHeatUp() const {
|
||||||
if (heaterDutyPercent_ <= 0.0f) {
|
if (heaterDutyPercent_ <= 0.0f) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const uint8_t span = FAN_HEAT_MAX_PWM - FAN_HEAT_MIN_PWM;
|
const float below = targetTempC_ - lastAvgTempC_;
|
||||||
return FAN_HEAT_MIN_PWM +
|
uint8_t heatFan = 0;
|
||||||
static_cast<uint8_t>((heaterDutyPercent_ / 100.0f) * static_cast<float>(span));
|
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 {
|
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 (isIdle()) {
|
||||||
if (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
|
if (!sensorWarmValid_ || lastMaxTempC_ >= IDLE_AUTO_FAN_OFF_TEMP_C) {
|
||||||
writeFan(FAN_MAX_PWM);
|
writeFan(FAN_MAX_PWM);
|
||||||
@@ -535,16 +629,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t duty = fanPwmForHeaterDemand();
|
uint8_t duty = fanPwmForHeatUp();
|
||||||
const uint8_t mixFan = fanPwmForCornerSpread();
|
|
||||||
if (mixFan > duty) {
|
|
||||||
duty = mixFan;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastMaxTempC_ > targetTempC_ && duty < FAN_MAX_PWM) {
|
|
||||||
duty = FAN_MAX_PWM;
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFan(duty);
|
writeFan(duty);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,4 +650,9 @@ private:
|
|||||||
uint32_t heaterCycleStartMs_;
|
uint32_t heaterCycleStartMs_;
|
||||||
uint32_t lastHeaterUpdateMs_;
|
uint32_t lastHeaterUpdateMs_;
|
||||||
bool heaterOn_;
|
bool heaterOn_;
|
||||||
|
float lastAvgTempC_;
|
||||||
|
HeaterBlock heaterBlock_;
|
||||||
|
bool fanTestActive_;
|
||||||
|
uint8_t fanTestPwm_;
|
||||||
|
uint32_t fanTestEndMs_;
|
||||||
};
|
};
|
||||||
|
|||||||
34
src/main.cpp
34
src/main.cpp
@@ -125,9 +125,24 @@ void printStatus(float avgTemp, float minTemp, float maxTemp) {
|
|||||||
Serial.print(thermal.heaterAllowance(), 0);
|
Serial.print(thermal.heaterAllowance(), 0);
|
||||||
Serial.print(F("% heater="));
|
Serial.print(F("% heater="));
|
||||||
Serial.print(thermal.heaterDutyPercent(), 1);
|
Serial.print(thermal.heaterDutyPercent(), 1);
|
||||||
Serial.print(F("% fan="));
|
Serial.print(F("% htop="));
|
||||||
|
if (thermal.isIdle()) {
|
||||||
|
Serial.print(F("n/a"));
|
||||||
|
} else {
|
||||||
|
Serial.print(thermal.maxHeatStopAt(avgTemp), 1);
|
||||||
|
}
|
||||||
|
Serial.print(F("C hblk="));
|
||||||
|
Serial.print(thermal.heaterBlockReason());
|
||||||
|
Serial.print(F(" ssr="));
|
||||||
|
Serial.print(thermal.isHeaterSsrOn() ? F("on") : F("off"));
|
||||||
|
Serial.print(F(" fan="));
|
||||||
|
Serial.print(thermal.fanPwm());
|
||||||
|
Serial.print(F("/255("));
|
||||||
Serial.print((thermal.fanPwm() * 100) / 255);
|
Serial.print((thermal.fanPwm() * 100) / 255);
|
||||||
if (thermal.isIdle() && thermal.isFanOff()) {
|
Serial.print(F("%)"));
|
||||||
|
if (thermal.isFanTestActive(millis())) {
|
||||||
|
Serial.print(F(" TEST"));
|
||||||
|
} else if (thermal.isIdle() && thermal.isFanOff()) {
|
||||||
Serial.print(F("(off)"));
|
Serial.print(F("(off)"));
|
||||||
} else if (thermal.isIdleCooling()) {
|
} else if (thermal.isIdleCooling()) {
|
||||||
Serial.print(F("(cooldown)"));
|
Serial.print(F("(cooldown)"));
|
||||||
@@ -181,6 +196,7 @@ void printHelp() {
|
|||||||
Serial.println(F(" target <C> set target (0 = idle)"));
|
Serial.println(F(" target <C> set target (0 = idle)"));
|
||||||
Serial.println(F(" fan off cancel idle fan override (auto-off below 40C)"));
|
Serial.println(F(" fan off cancel idle fan override (auto-off below 40C)"));
|
||||||
Serial.println(F(" fan on idle fan 30% (optional, auto-off below 40C)"));
|
Serial.println(F(" fan on idle fan 30% (optional, auto-off below 40C)"));
|
||||||
|
Serial.println(F(" fan test N set fan PWM 0-255 for 15s (verify wiring)"));
|
||||||
Serial.println(F(" autotune [C] learn PID (default: 40C when idle)"));
|
Serial.println(F(" autotune [C] learn PID (default: 40C when idle)"));
|
||||||
Serial.println(F(" autotune stop"));
|
Serial.println(F(" autotune stop"));
|
||||||
Serial.println(F(" pid show PID / adaptive status"));
|
Serial.println(F(" pid show PID / adaptive status"));
|
||||||
@@ -259,6 +275,20 @@ void processSerialLine(const char *line) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (strncmp(line, "fan test ", 9) == 0) {
|
||||||
|
const int pwm = atoi(line + 9);
|
||||||
|
if (pwm < 0 || pwm > 255) {
|
||||||
|
Serial.println(F("ERR fan test PWM must be 0-255"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
thermal.stopFanTest();
|
||||||
|
thermal.startFanTest(static_cast<uint8_t>(pwm), millis());
|
||||||
|
Serial.print(F("OK fan PWM="));
|
||||||
|
Serial.print(pwm);
|
||||||
|
Serial.println(F(" for 15s — check speed/noise on D5"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (strcmp(line, "status") == 0) {
|
if (strcmp(line, "status") == 0) {
|
||||||
const float avgTemp = averageValidTemperature();
|
const float avgTemp = averageValidTemperature();
|
||||||
const float minTemp = minValidTemperature();
|
const float minTemp = minValidTemperature();
|
||||||
|
|||||||
Reference in New Issue
Block a user