#pragma once #include #include "config.h" class FanStepResponse { public: enum class Phase : uint8_t { Idle, Preheat, Baseline, StepHold, Done, Failed }; FanStepResponse() : phase_(Phase::Idle), targetC_(STEPRESP_DEFAULT_TEMP_C), heaterPct_(STEPRESP_DEFAULT_HEATER_PCT), stepIndex_(0), sessionStartMs_(0), phaseStartMs_(0), lastLogMs_(0), lastAvgC_(0.0f) {} Phase phase() const { return phase_; } bool isActive() const { return phase_ == Phase::Preheat || phase_ == Phase::Baseline || phase_ == Phase::StepHold; } uint32_t elapsedMs(uint32_t nowMs) const { if (sessionStartMs_ == 0) { return 0; } return nowMs - sessionStartMs_; } uint8_t stepIndex() const { return stepIndex_; } uint8_t stepCount() const { return STEPRESP_FAN_STEP_COUNT; } float targetC() const { return targetC_; } float heaterPct() const { return heaterPct_; } uint8_t currentFanPwm() const { if (stepIndex_ >= STEPRESP_FAN_STEP_COUNT) { return 0; } return STEPRESP_FAN_STEPS[stepIndex_]; } const char *phaseName() const { switch (phase_) { case Phase::Preheat: return "preheat"; case Phase::Baseline: return "baseline"; case Phase::StepHold: return "step"; default: return ""; } } bool start(float targetC, float heaterPct) { if (targetC < 25.0f || targetC > TARGET_MAX_C) { return false; } if (heaterPct < STEPRESP_MIN_HEATER_PCT || heaterPct > STEPRESP_MAX_HEATER_PCT) { return false; } targetC_ = targetC; heaterPct_ = heaterPct; stepIndex_ = 0; sessionStartMs_ = millis(); phaseStartMs_ = sessionStartMs_; lastLogMs_ = 0; lastAvgC_ = 0.0f; phase_ = Phase::Preheat; Serial.print(F("stepresp: preheat to ")); Serial.print(targetC_ - STEPRESP_PREHEAT_BAND_C, 1); Serial.print(F("-")); Serial.print(targetC_, 1); Serial.print(F("C avg, heater=")); Serial.print(heaterPct_, 0); Serial.println(F("% fan=0")); return true; } void abort() { if (isActive()) { Serial.println(F("stepresp: cancelled")); } phase_ = Phase::Idle; sessionStartMs_ = 0; } void reset() { phase_ = Phase::Idle; sessionStartMs_ = 0; } bool update(float avgTempC, float maxTempC, float spreadC, uint32_t nowMs, float &heaterDutyOut, uint8_t &fanPwmOut) { heaterDutyOut = 0.0f; fanPwmOut = 0; if (phase_ == Phase::Idle || phase_ == Phase::Done || phase_ == Phase::Failed) { return false; } if (maxTempC >= EMERGENCY_MAX_TEMP_C) { fail(F("stepresp: abort — max sensor at safety limit")); return false; } if (phase_ == Phase::Preheat) { fanPwmOut = 0; if (nowMs - phaseStartMs_ > STEPRESP_PREHEAT_TIMEOUT_MS) { fail(F("stepresp: abort — preheat timeout")); return false; } if (avgTempC >= targetC_ - STEPRESP_PREHEAT_BAND_C) { enterBaseline(nowMs); } else { heaterDutyOut = heaterPct_; } return true; } heaterDutyOut = heaterPct_; fanPwmOut = currentFanPwm(); if (phase_ == Phase::Baseline) { if (nowMs - phaseStartMs_ >= STEPRESP_BASELINE_MS) { advanceStep(nowMs); } return true; } if (phase_ == Phase::StepHold) { if (nowMs - phaseStartMs_ >= STEPRESP_STEP_HOLD_MS) { if (stepIndex_ + 1 >= STEPRESP_FAN_STEP_COUNT) { finish(nowMs, avgTempC, spreadC); } else { ++stepIndex_; enterStepHold(nowMs, true); } } return true; } return false; } void logIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount, float avgTempC, float minTempC, float maxTempC, float spreadC, uint32_t nowMs) { if (!isActive()) { return; } if (lastLogMs_ != 0 && nowMs - lastLogMs_ < STEPRESP_LOG_INTERVAL_MS) { return; } lastLogMs_ = nowMs; lastAvgC_ = avgTempC; Serial.print(F("sr,")); Serial.print(nowMs); Serial.print(','); Serial.print(phaseName()); Serial.print(','); Serial.print(stepIndex_); Serial.print('/'); Serial.print(STEPRESP_FAN_STEP_COUNT); Serial.print(','); Serial.print(heaterPct_, 0); Serial.print(','); Serial.print(currentFanPwm()); Serial.print(','); Serial.print(avgTempC, 2); Serial.print(','); Serial.print(minTempC, 2); Serial.print(','); Serial.print(maxTempC, 2); Serial.print(','); Serial.print(spreadC, 2); for (uint8_t i = 0; i < sensorCount; ++i) { Serial.print(','); if (sensorValid[i]) { Serial.print(sensorTemps[i], 2); } } Serial.println(); } private: void enterBaseline(uint32_t nowMs) { phase_ = Phase::Baseline; phaseStartMs_ = nowMs; stepIndex_ = 0; Serial.print(F("stepresp: baseline fan=")); Serial.print(currentFanPwm()); Serial.print(F(" for ")); Serial.print(STEPRESP_BASELINE_MS / 1000UL); Serial.println(F("s")); } void enterStepHold(uint32_t nowMs, bool isStep) { phase_ = Phase::StepHold; phaseStartMs_ = nowMs; Serial.print(F("stepresp: ")); if (isStep) { Serial.print(F("step ")); } Serial.print(stepIndex_ + 1); Serial.print(F("/")); Serial.print(STEPRESP_FAN_STEP_COUNT); Serial.print(F(" fan=")); Serial.print(currentFanPwm()); Serial.print(F(" (")); Serial.print((currentFanPwm() * 100) / 255); Serial.print(F("%) hold ")); Serial.print(STEPRESP_STEP_HOLD_MS / 1000UL); Serial.println(F("s")); } void advanceStep(uint32_t nowMs) { if (STEPRESP_FAN_STEP_COUNT <= 1) { finish(nowMs, lastAvgC_, 0.0f); return; } stepIndex_ = 1; enterStepHold(nowMs, true); } void finish(uint32_t nowMs, float avgTempC, float spreadC) { phase_ = Phase::Done; Serial.print(F("stepresp: done in ")); Serial.print((nowMs - sessionStartMs_) / 1000UL); Serial.println(F("s")); Serial.print(F(" target=")); Serial.print(targetC_, 1); Serial.print(F("C heater=")); Serial.print(heaterPct_, 0); Serial.print(F("% final avg=")); Serial.print(avgTempC, 1); Serial.print(F("C spread=")); Serial.print(spreadC, 1); Serial.println(F("C")); Serial.println(F(" parse sr,... lines for step response (fan PWM vs temp)")); } void fail(const __FlashStringHelper *reason) { Serial.println(reason); phase_ = Phase::Failed; sessionStartMs_ = 0; } Phase phase_; float targetC_; float heaterPct_; uint8_t stepIndex_; uint32_t sessionStartMs_; uint32_t phaseStartMs_; uint32_t lastLogMs_; float lastAvgC_; };