diff --git a/include/config.h b/include/config.h index 7d38102..1b5287f 100644 --- a/include/config.h +++ b/include/config.h @@ -51,6 +51,7 @@ static const float APPROACH_BAND_C = 4.0f; // When spread is good, allow hottest corner slightly above target so avg can reach setpoint static const float GOOD_SPREAD_C = 5.0f; static const float BALANCED_MAX_ABOVE_TARGET_C = 2.0f; +static const float SPREAD_HEADROOM_FACTOR = 0.5f; // extra max-corner °C per °C of spread static const uint16_t HEATER_CYCLE_MS = 3000; @@ -71,10 +72,13 @@ static const float SPREAD_EMA_ALPHA = 0.45f; // PID auto-tune (relay method) — run with: autotune 45 static const float AUTOTUNE_HYSTERESIS_C = 0.4f; static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f; -static const float AUTOTUNE_PREHEAT_DUTY = 80.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_CYCLES_REQUIRED = 6; -static const uint32_t AUTOTUNE_TIMEOUT_MS = 1800000UL; +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_RELAY_PERIOD_MAX_MS = 2400000UL; // count periods up to 40 min // --------------------------------------------------------------------------- // Timing diff --git a/include/pid_autotuner.h b/include/pid_autotuner.h index 8823973..b736ab5 100644 --- a/include/pid_autotuner.h +++ b/include/pid_autotuner.h @@ -24,6 +24,7 @@ public: spreadSamples_(0), cycleCount_(0), aboveSetpoint_(false), + sessionStartMs_(0), phaseStartMs_(0), resultKp_(PID_KP), resultKi_(PID_KI), @@ -34,6 +35,29 @@ public: bool isActive() const { return phase_ == Phase::Preheat || phase_ == Phase::Relay; } + uint32_t elapsedMs(uint32_t nowMs) const { + if (sessionStartMs_ == 0) { + return 0; + } + return nowMs - sessionStartMs_; + } + + uint8_t cycleCount() const { return cycleCount_; } + uint8_t periodCount() const { return periodCount_; } + + float preheatTargetC() const { return setpointC_ - AUTOTUNE_PREHEAT_BAND_C; } + + const char *phaseName() const { + switch (phase_) { + case Phase::Preheat: + return "preheat"; + case Phase::Relay: + return "relay"; + default: + return ""; + } + } + bool start(float setpointC) { if (setpointC < 25.0f || setpointC > TARGET_MAX_C) { return false; @@ -44,10 +68,13 @@ public: relayLow_ = setpointC - AUTOTUNE_HYSTERESIS_C; resetMeasurements(); phase_ = Phase::Preheat; - phaseStartMs_ = millis(); + sessionStartMs_ = millis(); + phaseStartMs_ = sessionStartMs_; Serial.print(F("autotune: preheat to ")); + Serial.print(preheatTargetC(), 1); + Serial.print(F("-")); Serial.print(setpointC_, 1); - Serial.println(F("C")); + Serial.println(F("C avg")); return true; } @@ -56,9 +83,13 @@ public: Serial.println(F("autotune: cancelled")); } phase_ = Phase::Idle; + sessionStartMs_ = 0; } - void reset() { phase_ = Phase::Idle; } + void reset() { + phase_ = Phase::Idle; + sessionStartMs_ = 0; + } float setpoint() const { return setpointC_; } @@ -81,13 +112,23 @@ public: return phase_; } - if (nowMs - phaseStartMs_ > AUTOTUNE_TIMEOUT_MS) { - fail(F("autotune: abort — timeout")); + if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) { + fail(F("autotune: abort — session timeout")); return phase_; } if (phase_ == Phase::Preheat) { - if (avgTempC >= setpointC_ - AUTOTUNE_PREHEAT_BAND_C) { + fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM; + if (nowMs - phaseStartMs_ > AUTOTUNE_PREHEAT_TIMEOUT_MS) { + Serial.print(F("autotune: preheat failed — avg ")); + Serial.print(avgTempC, 1); + Serial.print(F("C after ")); + Serial.print((nowMs - sessionStartMs_) / 60000UL); + Serial.println(F(" min")); + fail(F("autotune: abort — preheat timeout")); + return phase_; + } + if (avgTempC >= preheatTargetC()) { enterRelay(avgTempC, nowMs); } else { heaterDutyOut = AUTOTUNE_PREHEAT_DUTY; @@ -132,7 +173,9 @@ private: peakSinceCross_ = avgTempC; valleySinceCross_ = avgTempC; lastCrossMs_ = 0; - Serial.println(F("autotune: relay test started")); + Serial.print(F("autotune: relay test started (")); + Serial.print((nowMs - sessionStartMs_) / 1000UL); + Serial.println(F("s preheat)")); } void resetMeasurements() { @@ -158,13 +201,18 @@ private: Serial.print(F("autotune: cycle ")); Serial.print(cycleCount_); + Serial.print(F("/")); + Serial.print(AUTOTUNE_CYCLES_REQUIRED); Serial.print(F(" amp=")); - Serial.println(amplitude, 2); + Serial.print(amplitude, 2); + Serial.print(F("C elapsed=")); + Serial.print((nowMs - sessionStartMs_) / 1000UL); + Serial.println(F("s")); } if (lastCrossMs_ > 0) { const uint32_t period = nowMs - lastCrossMs_; - if (period > 8000 && period < 900000) { + if (period > 8000 && period < AUTOTUNE_RELAY_PERIOD_MAX_MS) { periodSumMs_ += period; ++periodCount_; } @@ -173,11 +221,11 @@ private: peakSinceCross_ = valleySinceCross_; if (cycleCount_ >= AUTOTUNE_CYCLES_REQUIRED && periodCount_ >= 3 && amplitudeCount_ >= 3) { - finish(); + finish(nowMs); } } - void finish() { + void finish(uint32_t nowMs) { const float avgPeriodSec = static_cast(periodSumMs_ / periodCount_) / 1000.0f; const float avgAmplitude = amplitudeSum_ / static_cast(amplitudeCount_); @@ -212,7 +260,9 @@ private: } phase_ = Phase::Done; - Serial.println(F("autotune: done")); + Serial.print(F("autotune: done in ")); + Serial.print((nowMs - sessionStartMs_) / 1000UL); + Serial.println(F("s")); Serial.print(F(" Kp=")); Serial.print(resultKp_, 3); Serial.print(F(" Ki=")); @@ -226,6 +276,7 @@ private: void fail(const __FlashStringHelper *reason) { Serial.println(reason); phase_ = Phase::Failed; + sessionStartMs_ = 0; } Phase phase_; @@ -243,6 +294,7 @@ private: uint16_t spreadSamples_; uint8_t cycleCount_; bool aboveSetpoint_; + uint32_t sessionStartMs_; uint32_t phaseStartMs_; float resultKp_; float resultKi_; diff --git a/include/thermal_controller.h b/include/thermal_controller.h index 0dd2929..a06ab1e 100644 --- a/include/thermal_controller.h +++ b/include/thermal_controller.h @@ -104,6 +104,16 @@ public: bool isAutotuning() const { return autotuner_.isActive(); } + uint32_t autotuneElapsedMs(uint32_t nowMs) const { return autotuner_.elapsedMs(nowMs); } + + const char *autotunePhaseName() const { return autotuner_.phaseName(); } + + uint8_t autotuneCycleCount() const { return autotuner_.cycleCount(); } + + uint8_t autotunePeriodCount() const { return autotuner_.periodCount(); } + + float autotunePreheatTargetC() const { return autotuner_.preheatTargetC(); } + bool commitAutotuneIfDone() { if (autotuner_.phase() != PidAutotuner::Phase::Done) { return false; @@ -358,10 +368,22 @@ private: bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; } float maxHeatStopTemp(float avgTempC) const { - if (isBalancedChamber() && avgTempC < targetTempC_) { - return targetTempC_ + BALANCED_MAX_ABOVE_TARGET_C; + if (avgTempC >= targetTempC_) { + return targetTempC_; } - return targetTempC_; + + float stopAt = targetTempC_; + if (isBalancedChamber()) { + stopAt = targetTempC_ + BALANCED_MAX_ABOVE_TARGET_C; + } else { + stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f; + } + + const float belowCutoff = cutoffThreshold() - 0.1f; + if (stopAt > belowCutoff) { + stopAt = belowCutoff; + } + return stopAt; } float allowanceFromMaxCorner(float maxTempC, float avgTempC) const { diff --git a/src/main.cpp b/src/main.cpp index a72cffc..a9b3186 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -138,7 +138,17 @@ void printStatus(float avgTemp, float minTemp, float maxTemp) { Serial.print(thermal.isFailSafeActive() ? F("YES") : F("no")); Serial.print(F(" mode=")); if (thermal.isAutotuning()) { - Serial.print(F("autotune")); + Serial.print(F("autotune/")); + Serial.print(thermal.autotunePhaseName()); + Serial.print(F(" ")); + Serial.print(thermal.autotuneElapsedMs(millis()) / 1000UL); + Serial.print(F("s ")); + Serial.print(thermal.autotuneCycleCount()); + Serial.print(F("/")); + Serial.print(AUTOTUNE_CYCLES_REQUIRED); + Serial.print(F("cyc pre>=")); + Serial.print(thermal.autotunePreheatTargetC(), 0); + Serial.print(F("C")); } else if (thermal.isAdaptive()) { Serial.print(F("learned")); } else { @@ -284,7 +294,7 @@ void processSerialLine(const char *line) { Serial.println(F("ERR autotune already running")); return; } - Serial.println(F("OK autotune started — keep chamber closed, wait ~10-20 min")); + Serial.println(F("OK autotune started — preheat then relay, typically 15-40 min")); return; }