269 lines
6.6 KiB
C++
269 lines
6.6 KiB
C++
#pragma once
|
|
|
|
#include <Arduino.h>
|
|
|
|
#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_;
|
|
};
|