diff options
author | Manuel Coenen <manuel@duet3d.com> | 2021-01-25 14:51:51 +0300 |
---|---|---|
committer | Manuel Coenen <manuel@duet3d.com> | 2021-01-25 14:51:51 +0300 |
commit | 1a9bfaec7448b46c1e000144877560577eb9bff4 (patch) | |
tree | a84dfd2fea8867581a4129a94f27cdaf35e79406 /src/Heating | |
parent | 3578600a1dde59727142ad7b54b92609b31b6d0f (diff) | |
parent | 3da46dfed8a41ee2e697bd207ae816e3b0c009c2 (diff) |
Merge remote-tracking branch 'origin/3.3-dev' into wil-convert-same70-to-coren2g
Diffstat (limited to 'src/Heating')
-rw-r--r-- | src/Heating/Heat.cpp | 20 | ||||
-rw-r--r-- | src/Heating/Heat.h | 5 | ||||
-rw-r--r-- | src/Heating/Heater.cpp | 227 | ||||
-rw-r--r-- | src/Heating/Heater.h | 75 | ||||
-rw-r--r-- | src/Heating/LocalHeater.cpp | 221 | ||||
-rw-r--r-- | src/Heating/LocalHeater.h | 45 | ||||
-rw-r--r-- | src/Heating/RemoteHeater.cpp | 286 | ||||
-rw-r--r-- | src/Heating/RemoteHeater.h | 43 |
8 files changed, 628 insertions, 294 deletions
diff --git a/src/Heating/Heat.cpp b/src/Heating/Heat.cpp index 076f81c0..6fab76b7 100644 --- a/src/Heating/Heat.cpp +++ b/src/Heating/Heat.cpp @@ -750,12 +750,12 @@ void Heat::Standby(int heater, const Tool *tool) noexcept } } -void Heat::PrintCoolingFanPwmChanged(unsigned int heater, float pwmChange) const noexcept +void Heat::FeedForwardAdjustment(unsigned int heater, float fanPwmChange, float extrusionChange) const noexcept { const auto h = FindHeater(heater); if (h.IsNotNull()) { - h->PrintCoolingFanPwmChanged(pwmChange); + h->FeedForwardAdjustment(fanPwmChange, extrusionChange); } } @@ -1092,13 +1092,6 @@ size_t Heat::GetNumSensorsToReport() const noexcept return s->GetSensorNumber() + 1; } -// Get the temperature of a heater -float Heat::GetHeaterTemperature(size_t heater) const noexcept -{ - const auto h = FindHeater(heater); - return (h.IsNull()) ? ABS_ZERO : h->GetTemperature(); -} - // Suspend the heaters to conserve power or while doing Z probing void Heat::SuspendHeaters(bool sus) noexcept { @@ -1241,6 +1234,15 @@ void Heat::ProcessRemoteHeatersReport(CanAddress src, const CanMessageHeatersSta ); } +void Heat::ProcessRemoteHeaterTuningReport(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept +{ + const auto h = FindHeater(msg.heater); + if (h.IsNotNull()) + { + h->UpdateHeaterTuning(src, msg); + } +} + #endif #if SUPPORT_REMOTE_COMMANDS diff --git a/src/Heating/Heat.h b/src/Heating/Heat.h index 3d86ed5e..872c72dc 100644 --- a/src/Heating/Heat.h +++ b/src/Heating/Heat.h @@ -104,8 +104,6 @@ public: float GetAveragePWM(size_t heater) const noexcept // Return the running average PWM to the heater as a fraction in [0, 1]. pre(heater < MaxHeaters); - float GetHeaterTemperature(size_t heater) const noexcept; // Result is in degrees Celsius - const Tool* GetLastStandbyTool(int heater) const noexcept pre(heater >= 0; heater < MaxHeaters) { @@ -132,7 +130,7 @@ public: GCodeResult Activate(int heater, const StringRef& reply) noexcept; // Turn on a heater void Standby(int heater, const Tool* tool) noexcept; // Set a heater to standby void SwitchOff(int heater) noexcept; // Turn off a specific heater - void PrintCoolingFanPwmChanged(unsigned int heater, float pwmChange) const noexcept; + void FeedForwardAdjustment(unsigned int heater, float fanPwmChange, float extrusionChange) const noexcept; #if HAS_MASS_STORAGE bool WriteModelParameters(FileStore *f) const noexcept; // Write heater model parameters to file returning true if no error @@ -142,6 +140,7 @@ public: #if SUPPORT_CAN_EXPANSION void ProcessRemoteSensorsReport(CanAddress src, const CanMessageSensorTemperatures& msg) noexcept; void ProcessRemoteHeatersReport(CanAddress src, const CanMessageHeatersStatus& msg) noexcept; + void ProcessRemoteHeaterTuningReport(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept; #endif #if SUPPORT_REMOTE_COMMANDS diff --git a/src/Heating/Heater.cpp b/src/Heating/Heater.cpp index 952f50ce..b0dc08f2 100644 --- a/src/Heating/Heater.cpp +++ b/src/Heating/Heater.cpp @@ -11,6 +11,7 @@ #include "Heat.h" #include "HeaterMonitor.h" #include "Sensors/TemperatureSensor.h" +#include <GCodes/GCodes.h> #include <GCodes/GCodeBuffer/GCodeBuffer.h> #include <GCodes/GCodeException.h> @@ -60,8 +61,47 @@ DEFINE_GET_OBJECT_MODEL_TABLE(Heater) #endif +// Static members of class Heater + +float Heater::tuningPwm; // the PWM to use, 0..1 +float Heater::tuningTargetTemp; // the target temperature + +DeviationAccumulator Heater::tuningStartTemp; // the temperature when we turned on the heater +DeviationAccumulator Heater::dHigh; +DeviationAccumulator Heater::dLow; +DeviationAccumulator Heater::tOn; +DeviationAccumulator Heater::tOff; +DeviationAccumulator Heater::heatingRate; +DeviationAccumulator Heater::coolingRate; +DeviationAccumulator Heater::tuningVoltage; // sum of the voltage readings we take during the heating phase + +uint32_t Heater::tuningBeginTime; // when we started the tuning process +uint32_t Heater::lastOffTime; +uint32_t Heater::lastOnTime; +float Heater::peakTemp; // max or min temperature +uint32_t Heater::peakTime; // the time at which we recorded peakTemp +float Heater::afterPeakTemp; // temperature after max from which we start timing the cooling rate +uint32_t Heater::afterPeakTime; // the time at which we recorded afterPeakTemp +float Heater::lastCoolingRate; +FansBitmap Heater::tuningFans; +unsigned int Heater::tuningPhase; +uint8_t Heater::idleCyclesDone; + +Heater::HeaterParameters Heater::fanOffParams, Heater::fanOnParams; + +// Clear all the counters except tuning voltage and start temperature +/*static*/ void Heater::ClearCounters() noexcept +{ + dHigh.Clear(); + dLow.Clear(); + tOn.Clear(); + tOff.Clear(); + heatingRate.Clear(); + coolingRate.Clear(); +} + Heater::Heater(unsigned int num) noexcept - : heaterNumber(num), sensorNumber(-1), activeTemperature(0.0), standbyTemperature(0.0), + : tuned(false), heaterNumber(num), sensorNumber(-1), activeTemperature(0.0), standbyTemperature(0.0), maxTempExcursion(DefaultMaxTempExcursion), maxHeatingFaultTime(DefaultMaxHeatingFaultTime), active(false), modelSetByUser(false), monitorsSetByUser(false) { @@ -170,17 +210,17 @@ GCodeResult Heater::SetOrReportModel(unsigned int heater, GCodeBuffer& gb, const } // Set the process model returning true if successful -GCodeResult Heater::SetModel(float heatingRate, float coolingRateFanOff, float coolingRateFanOn, float td, float maxPwm, float voltage, bool usePid, bool inverted, const StringRef& reply) noexcept +GCodeResult Heater::SetModel(float hr, float coolingRateFanOff, float coolingRateFanOn, float td, float maxPwm, float voltage, bool usePid, bool inverted, const StringRef& reply) noexcept { GCodeResult rslt; - if (model.SetParameters(heatingRate, coolingRateFanOff, coolingRateFanOn, td, maxPwm, GetHighestTemperatureLimit(), voltage, usePid, inverted)) + if (model.SetParameters(hr, coolingRateFanOff, coolingRateFanOn, td, maxPwm, GetHighestTemperatureLimit(), voltage, usePid, inverted)) { if (model.IsEnabled()) { rslt = UpdateModel(reply); if (rslt == GCodeResult::ok) { - const float predictedMaxTemp = heatingRate/coolingRateFanOff + NormalAmbientTemperature; + const float predictedMaxTemp = hr/coolingRateFanOff + NormalAmbientTemperature; const float noWarnTemp = (GetHighestTemperatureLimit() - NormalAmbientTemperature) * 1.5 + 50.0; // allow 50% extra power plus enough for an extra 50C if (predictedMaxTemp > noWarnTemp) { @@ -192,6 +232,7 @@ GCodeResult Heater::SetModel(float heatingRate, float coolingRateFanOff, float c else { ResetHeater(); + tuned = false; rslt = GCodeResult::ok; } } @@ -205,6 +246,184 @@ GCodeResult Heater::SetModel(float heatingRate, float coolingRateFanOff, float c return rslt; } +// Start an auto tune cycle for this heater +GCodeResult Heater::StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) +{ + // Get the target temperature (required) + gb.MustSee('S'); + const float targetTemp = gb.GetFValue(); + + // Get the optional PWM + const float maxPwm = (gb.Seen('P')) ? gb.GetFValue() : GetModel().GetMaxPwm(); + if (maxPwm < 0.1 || maxPwm > 1.0) + { + reply.copy("Invalid PWM value"); + return GCodeResult::error; + } + + if (!GetModel().IsEnabled()) + { + reply.printf("heater %u cannot be auto tuned while it is disabled", GetHeaterNumber()); + return GCodeResult::error; + } + + const float limit = GetHighestTemperatureLimit(); + if (targetTemp >= limit) + { + reply.printf("heater %u target temperature must be below the temperature limit for this heater (%.1fC)", GetHeaterNumber(), (double)limit); + return GCodeResult::error; + } + + TemperatureError err; + const float currentTemp = reprap.GetHeat().GetSensorTemperature(GetSensorNumber(), err); + if (err != TemperatureError::success) + { + reply.printf("heater %u reported error '%s' at start of auto tuning", GetHeaterNumber(), TemperatureErrorString(err)); + return GCodeResult::error; + } + + const bool seenA = gb.Seen('A'); + const float ambientTemp = (seenA) ? gb.GetFValue() : currentTemp; + if (ambientTemp + 20 >= targetTemp) + { + reply.printf("Target temperature must be at least 20C above ambient temperature"); + } + + const GCodeResult rslt = StartAutoTune(reply, fans, targetTemp, maxPwm, seenA, ambientTemp); + if (rslt == GCodeResult::ok) + { + reply.printf("Auto tuning heater %u using target temperature %.1f" DEGREE_SYMBOL "C and PWM %.2f - do not leave printer unattended", + GetHeaterNumber(), (double)targetTemp, (double)maxPwm); + } + return rslt; +} + +const char *const Heater::TuningPhaseText[] = +{ + "checking temperature is stable", + "heating up", + "heating system settling", + "tuning with fan off", +#if TUNE_WITH_HALF_FAN + "tuning with 50% fan", +#endif + "tuning with fan on" +}; + +// Get the auto tune status or last result +void Heater::GetAutoTuneStatus(const StringRef& reply) const noexcept +{ + if (GetStatus() == HeaterStatus::tuning) + { + // Phases are: 1 = stabilising, 2 = heating, 3 = settling, 4 = cycling with fan off, 5 = cycling with fan on + const unsigned int numPhases = (tuningFans.IsEmpty()) ? 4 : ARRAY_SIZE(TuningPhaseText); + reply.printf("Heater %u is being tuned, phase %u of %u, %s", GetHeaterNumber(), tuningPhase + 1, numPhases, TuningPhaseText[tuningPhase]); + } + else if (tuned) + { + reply.printf("Heater %u tuning succeeded, use M307 H%u to see result", GetHeaterNumber(), GetHeaterNumber()); + } + else + { + reply.printf("Heater %u tuning failed", GetHeaterNumber()); + } +} + +// Tell the user what's happening, called after the tuning phase has been updated +void Heater::ReportTuningUpdate() noexcept +{ + if (tuningPhase < ARRAY_SIZE(TuningPhaseText)) + { + reprap.GetPlatform().MessageF(GenericMessage, "Auto tune starting phase %u, %s\n", tuningPhase + 1, TuningPhaseText[tuningPhase]); + } +} + +void Heater::CalculateModel(HeaterParameters& params) noexcept +{ + if (reprap.Debug(moduleHeat)) + { +#define PLUS_OR_MINUS "\xC2\xB1" + reprap.GetPlatform().MessageF(GenericMessage, + "tOn %ld" PLUS_OR_MINUS "%ld, tOff %ld" PLUS_OR_MINUS "%ld," + " dHigh %ld" PLUS_OR_MINUS "%ld, dLow %ld" PLUS_OR_MINUS "%ld," + " R %.3f" PLUS_OR_MINUS "%.3f, C %.3f" PLUS_OR_MINUS "%.3f," +#if HAS_VOLTAGE_MONITOR + " V %.1f" PLUS_OR_MINUS "%.1f," +#endif + " cycles %u\n", + lrintf(tOn.GetMean()), lrintf(tOn.GetDeviation()), + lrintf(tOff.GetMean()), lrintf(tOff.GetDeviation()), + lrintf(dHigh.GetMean()), lrintf(dHigh.GetDeviation()), + lrintf(dLow.GetMean()), lrintf(dLow.GetDeviation()), + (double)heatingRate.GetMean(), (double)heatingRate.GetDeviation(), + (double)coolingRate.GetMean(), (double)coolingRate.GetDeviation(), +#if HAS_VOLTAGE_MONITOR + (double)tuningVoltage.GetMean(), (double)tuningVoltage.GetDeviation(), +#endif + coolingRate.GetNumSamples() + ); + } + + const float cycleTime = tOn.GetMean() + tOff.GetMean(); // in milliseconds + const float averageTemperatureRiseHeating = tuningTargetTemp - 0.5 * (TuningHysteresis - TuningPeakTempDrop) - tuningStartTemp.GetMean(); + const float averageTemperatureRiseCooling = tuningTargetTemp - TuningPeakTempDrop - 0.5 * TuningHysteresis - tuningStartTemp.GetMean(); + params.deadTime = (((dHigh.GetMean() * tOff.GetMean()) + (dLow.GetMean() * tOn.GetMean())) * MillisToSeconds)/cycleTime; // in seconds + params.coolingRate = coolingRate.GetMean()/averageTemperatureRiseCooling; // in seconds + params.heatingRate = (heatingRate.GetMean() + (coolingRate.GetMean() * averageTemperatureRiseHeating/averageTemperatureRiseCooling)) / tuningPwm; + params.numCycles = dHigh.GetNumSamples(); +} + +void Heater::SetAndReportModel(bool usingFans) noexcept +{ + const float hRate = (usingFans) ? (fanOffParams.heatingRate + fanOnParams.heatingRate) * 0.5 : fanOffParams.heatingRate; + const float deadTime = (usingFans) ? (fanOffParams.deadTime + fanOnParams.deadTime) * 0.5 : fanOffParams.deadTime; + const float fanOnCoolingRate = (usingFans) ? fanOnParams.coolingRate : fanOffParams.coolingRate; + String<StringLength256> str; + const GCodeResult rslt = SetModel( hRate, + fanOffParams.coolingRate, fanOnCoolingRate, + deadTime, + tuningPwm, +#if HAS_VOLTAGE_MONITOR + tuningVoltage.GetMean(), +#else + 0.0, +#endif + true, false, str.GetRef()); + if (rslt == GCodeResult::ok || rslt == GCodeResult::warning) + { + tuned = true; + str.printf("Auto tuning heater %u completed after %u idle and %u tuning cycles in %" PRIu32 " seconds. This heater needs the following M307 command:\n" + " M307 H%u R%.3f C%.1f", + GetHeaterNumber(), + idleCyclesDone, + (usingFans) ? fanOffParams.numCycles + fanOnParams.numCycles : fanOffParams.numCycles, + (millis() - tuningBeginTime)/(uint32_t)SecondsToMillis, + GetHeaterNumber(), (double)GetModel().GetHeatingRate(), (double)(1.0/GetModel().GetCoolingRateFanOff()) + ); + if (usingFans) + { + str.catf(":%.1f", (double)(1.0/GetModel().GetCoolingRateFanOn())); + } + str.catf(" D%.2f S%.2f V%.1f\n", (double)GetModel().GetDeadTime(), (double)GetModel().GetMaxPwm(), (double)GetModel().GetVoltage()); + reprap.GetPlatform().Message(LoggedGenericMessage, str.c_str()); + if (reprap.GetGCodes().SawM501InConfigFile()) + { + reprap.GetPlatform().Message(GenericMessage, "Send M500 to save this command in config-override.g\n"); + } + else + { + reprap.GetPlatform().MessageF(GenericMessage, "Edit the M307 H%u command in config.g to match this. Omit the V parameter if the heater is not powered from VIN.\n", GetHeaterNumber()); + } + } + else + { + reprap.GetPlatform().MessageF(WarningMessage, "Auto tune of heater %u failed due to bad curve fit (R=%.3f, 1/C=%.4f:%.4f, D=%.1f)\n", + GetHeaterNumber(), (double)hRate, + (double)fanOffParams.coolingRate, (double)fanOnCoolingRate, + (double)fanOffParams.deadTime); + } +} + GCodeResult Heater::SetFaultDetectionParameters(float pMaxTempExcursion, float pMaxFaultTime, const StringRef& reply) noexcept { maxTempExcursion = pMaxTempExcursion; diff --git a/src/Heating/Heater.h b/src/Heating/Heater.h index eaed5621..dc9d5e8e 100644 --- a/src/Heating/Heater.h +++ b/src/Heating/Heater.h @@ -14,12 +14,16 @@ #include "HeaterMonitor.h" #include <GCodes/GCodeResult.h> #include <ObjectModel/ObjectModel.h> +#include <Math/DeviationAccumulator.h> #if SUPPORT_CAN_EXPANSION # include "CanId.h" #endif +#define TUNE_WITH_HALF_FAN 0 + class HeaterMonitor; +struct CanMessageHeaterTuningReport; struct CanHeaterReport; // Enumeration to describe the status of a heater. Note that the web interface returns the numerical values, so don't change them. @@ -37,19 +41,18 @@ public: virtual GCodeResult SetPwmFrequency(PwmFrequency freq, const StringRef& reply) = 0; virtual GCodeResult ReportDetails(const StringRef& reply) const noexcept = 0; - virtual float GetTemperature() const noexcept = 0; // Get the current temperature + virtual float GetTemperature() const noexcept = 0; // Get the current temperature and error status virtual float GetAveragePWM() const noexcept = 0; // Return the running average PWM to the heater. Answer is a fraction in [0, 1]. virtual GCodeResult ResetFault(const StringRef& reply) noexcept = 0; // Reset a fault condition - only call this if you know what you are doing virtual void SwitchOff() noexcept = 0; virtual void Spin() noexcept = 0; - virtual GCodeResult StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) = 0; // Start an auto tune cycle for this heater - virtual void GetAutoTuneStatus(const StringRef& reply) const noexcept = 0; // Get the auto tune status or last result virtual void Suspend(bool sus) noexcept = 0; // Suspend the heater to conserve power or while doing Z probing virtual float GetAccumulator() const noexcept = 0; // Get the inertial term accumulator - virtual void PrintCoolingFanPwmChanged(float pwmChange) noexcept = 0; + virtual void FeedForwardAdjustment(float fanPwmChange, float extrusionChange) noexcept = 0; #if SUPPORT_CAN_EXPANSION virtual void UpdateRemoteStatus(CanAddress src, const CanHeaterReport& report) noexcept = 0; + virtual void UpdateHeaterTuning(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept = 0; #endif HeaterStatus GetStatus() const noexcept; // Get the status of the heater @@ -60,6 +63,8 @@ public: float GetStandbyTemperature() const noexcept { return standbyTemperature; } GCodeResult Activate(const StringRef& reply) noexcept; // Switch from idle to active void Standby() noexcept; // Switch from active to idle + GCodeResult StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException); // Start an auto tune cycle for this heater + void GetAutoTuneStatus(const StringRef& reply) const noexcept; // Get the auto tune status or last result void GetFaultDetectionParameters(float& pMaxTempExcursion, float& pMaxFaultTime) const noexcept { pMaxTempExcursion = maxTempExcursion; pMaxFaultTime = maxHeatingFaultTime; } @@ -106,24 +111,78 @@ protected: lastTuningMode = tuning3 }; -protected: + struct HeaterParameters + { + float heatingRate; + float coolingRate; + float deadTime; + unsigned int numCycles; + }; + virtual void ResetHeater() noexcept = 0; virtual HeaterMode GetMode() const noexcept = 0; virtual GCodeResult SwitchOn(const StringRef& reply) noexcept = 0; virtual GCodeResult UpdateModel(const StringRef& reply) noexcept = 0; virtual GCodeResult UpdateFaultDetectionParameters(const StringRef& reply) noexcept = 0; virtual GCodeResult UpdateHeaterMonitors(const StringRef& reply) noexcept = 0; + virtual GCodeResult StartAutoTune(const StringRef& reply, FansBitmap fans, float targetTemp, float pwm, bool seenA, float ambientTemp) noexcept = 0; int GetSensorNumber() const noexcept { return sensorNumber; } void SetSensorNumber(int sn) noexcept; float GetMaxTemperatureExcursion() const noexcept { return maxTempExcursion; } float GetMaxHeatingFaultTime() const noexcept { return maxHeatingFaultTime; } float GetTargetTemperature() const noexcept { return (active) ? activeTemperature : standbyTemperature; } - GCodeResult SetModel(float heatingRate, float coolingRateFanOff, float coolingRateFanOn, float td, float maxPwm, float voltage, bool usePid, bool inverted, const StringRef& reply) noexcept; // Set the process model - - HeaterMonitor monitors[MaxMonitorsPerHeater]; // embedding them in the Heater uses less memory than dynamic allocation + GCodeResult SetModel(float hr, float coolingRateFanOff, float coolingRateFanOn, float td, float maxPwm, float voltage, bool usePid, bool inverted, const StringRef& reply) noexcept; + // set the process model + void ReportTuningUpdate() noexcept; // tell the user what's happening + void CalculateModel(HeaterParameters& params) noexcept; // calculate G, td and tc from the accumulated readings + void SetAndReportModel(bool usingFans) noexcept; + + HeaterMonitor monitors[MaxMonitorsPerHeater]; // embedding them in the Heater uses less memory than dynamic allocation + bool tuned; // true if tuning was successful + + // Constants used during heater tuning + static constexpr uint32_t TempSettleTimeout = 20000; // how long we allow the initial temperature to settle + static constexpr unsigned int TuningHeaterMinIdleCycles = 3; // minimum number of idle cycles after heating up, including the initial overshoot and cool down + static constexpr unsigned int TuningHeaterMaxIdleCycles = 10; + static constexpr unsigned int MinTuningHeaterCycles = 5; + static constexpr unsigned int MaxTuningHeaterCycles = 25; + static constexpr float TuningHysteresis = 5.0; + static constexpr float TuningPeakTempDrop = 2.0; // must be well below TuningHysteresis + static constexpr float FeedForwardMultiplier = 1.3; // how much we over-compensate feedforward to allow for heat reservoirs during tuning + static constexpr float HeaterSettledCoolingTimeRatio = 0.93; + + // Variables used during heater tuning + static float tuningPwm; // the PWM to use, 0..1 + static float tuningTargetTemp; // the target temperature + static DeviationAccumulator tuningStartTemp; // the temperature when we turned on the heater + static uint32_t tuningBeginTime; // when we started the tuning process + static DeviationAccumulator dHigh; + static DeviationAccumulator dLow; + static DeviationAccumulator tOn; + static DeviationAccumulator tOff; + static DeviationAccumulator heatingRate; + static DeviationAccumulator coolingRate; + static DeviationAccumulator tuningVoltage; // sum of the voltage readings we take during the heating phase + + static uint32_t lastOffTime; + static uint32_t lastOnTime; + static float peakTemp; // max or min temperature + static uint32_t peakTime; // the time at which we recorded peakTemp + static float afterPeakTemp; // temperature after max from which we start timing the cooling rate + static uint32_t afterPeakTime; // the time at which we recorded afterPeakTemp + static float lastCoolingRate; + static FansBitmap tuningFans; + static unsigned int tuningPhase; + static uint8_t idleCyclesDone; + + static HeaterParameters fanOffParams, fanOnParams; + + static void ClearCounters() noexcept; private: + static const char* const TuningPhaseText[]; + FopDt model; unsigned int heaterNumber; int sensorNumber; // the sensor number used by this heater diff --git a/src/Heating/LocalHeater.cpp b/src/Heating/LocalHeater.cpp index 949c6096..d1480263 100644 --- a/src/Heating/LocalHeater.cpp +++ b/src/Heating/LocalHeater.cpp @@ -14,51 +14,6 @@ #include "RepRap.h" #include <Tools/Tool.h> -#define TUNE_WITH_HALF_FAN 0 - -// Private constants -const uint32_t InitialTuningReadingInterval = 250; // the initial reading interval in milliseconds -const uint32_t TempSettleTimeout = 20000; // how long we allow the initial temperature to settle - -// Variables used during heater tuning -static float tuningPwm; // the PWM to use, 0..1 -static float tuningTargetTemp; // the target temperature -static DeviationAccumulator tuningStartTemp; // the temperature when we turned on the heater -static uint32_t tuningBeginTime; // when we started the tuning process -static DeviationAccumulator dHigh; -static DeviationAccumulator dLow; -static DeviationAccumulator tOn; -static DeviationAccumulator tOff; -static DeviationAccumulator heatingRate; -static DeviationAccumulator coolingRate; -static uint32_t lastOffTime; -static uint32_t lastOnTime; -static float peakTemp; // max or min temperature -static uint32_t peakTime; // the time at which we recorded peakTemp -static float afterPeakTemp; // temperature after max from which we start timing the cooling rate -static uint32_t afterPeakTime; // the time at which we recorded afterPeakTemp -static float lastCoolingRate; -static FansBitmap tuningFans; -static unsigned int tuningPhase; -static uint8_t idleCyclesDone; - -static LocalHeater::HeaterParameters fanOffParams, fanOnParams; - -#if HAS_VOLTAGE_MONITOR -static DeviationAccumulator tuningVoltage; // sum of the voltage readings we take during the heating phase -#endif - -// Clear all the counters except tuning voltage and start temperature -static void ClearCounters() noexcept -{ - dHigh.Clear(); - dLow.Clear(); - tOn.Clear(); - tOff.Clear(); - heatingRate.Clear(); - coolingRate.Clear(); -} - // Member functions and constructors LocalHeater::LocalHeater(unsigned int heaterNum) noexcept : Heater(heaterNum), mode(HeaterMode::off) @@ -99,7 +54,6 @@ void LocalHeater::ResetHeater() noexcept previousTemperatureIndex = 0; iAccumulator = 0.0; badTemperatureCount = 0; - tuned = false; averagePWM = lastPwm = 0.0; heatingFaultCount = 0; temperature = BadErrorTemperature; @@ -478,64 +432,21 @@ float LocalHeater::GetExpectedHeatingRate() const noexcept } // Auto tune this heater. The caller has already checked that on other heater is being tuned. -GCodeResult LocalHeater::StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) +GCodeResult LocalHeater::StartAutoTune(const StringRef& reply, FansBitmap fans, float targetTemp, float pwm, bool seenA, float ambientTemp) noexcept { - // Get the target temperature (required) - gb.MustSee('S'); - const float targetTemp = gb.GetFValue(); - - // Get the optional PWM - const float maxPwm = (gb.Seen('P')) ? gb.GetFValue() : GetModel().GetMaxPwm(); - if (maxPwm < 0.1 || maxPwm > 1.0) - { - reply.copy("Invalid PWM value"); - return GCodeResult::error; - } - - if (!GetModel().IsEnabled()) - { - reply.printf("heater %u cannot be auto tuned while it is disabled", GetHeaterNumber()); - return GCodeResult::error; - } - if (lastPwm > 0.0 || GetAveragePWM() > 0.02) { reply.printf("heater %u must be off and cold before auto tuning it", GetHeaterNumber()); return GCodeResult::error; } - const float limit = GetHighestTemperatureLimit(); - if (targetTemp >= limit) - { - reply.printf("heater %u target temperature must be below the temperature limit for this heater (%.1fC)", GetHeaterNumber(), (double)limit); - return GCodeResult::error; - } - - const TemperatureError err = ReadTemperature(); - if (err != TemperatureError::success) - { - reply.printf("heater %u reported error '%s' at start of auto tuning", GetHeaterNumber(), TemperatureErrorString(err)); - return GCodeResult::error; - } - - const bool seenA = gb.Seen('A'); - const float ambientTemp = (seenA) ? gb.GetFValue() : temperature; - if (ambientTemp + 20 >= targetTemp) - { - reply.printf("Target temperature must be at least 20C above ambient temperature"); - } - - reply.printf("Auto tuning heater %u using target temperature %.1f" DEGREE_SYMBOL "C and PWM %.2f - do not leave printer unattended", - GetHeaterNumber(), (double)targetTemp, (double)maxPwm); - tuningFans = fans; reprap.GetFansManager().SetFansValue(tuningFans, 0.0); - tuningPwm = maxPwm; + tuningPwm = pwm; tuningTargetTemp = targetTemp; tuningStartTemp.Clear(); tuningBeginTime = millis(); - tuningPhase = 0; tuned = false; // assume failure if (seenA) @@ -544,46 +455,25 @@ GCodeResult LocalHeater::StartAutoTune(GCodeBuffer& gb, const StringRef& reply, ClearCounters(); timeSetHeating = millis(); lastPwm = tuningPwm; // turn on heater at specified power + tuningPhase = 1; mode = HeaterMode::tuning1; + ReportTuningUpdate(); } else { + tuningPhase = 0; mode = HeaterMode::tuning0; } return GCodeResult::ok; } -// Get the auto tune status or last result -void LocalHeater::GetAutoTuneStatus(const StringRef& reply) const noexcept -{ - if (mode >= HeaterMode::tuning0) - { - // Phases are: 1 = stabilising, 2 = heating, 3 = settling, 4 = cycling with fan off, 5 = cycling with fan on - const unsigned int numPhases = (tuningFans.IsEmpty()) ? 4 -#if TUNE_WITH_HALF_FAN - : 6; -#else - : 5; -#endif - reply.printf("Heater %u is being tuned, phase %u of %u", GetHeaterNumber(), tuningPhase + 1, numPhases); - } - else if (tuned) - { - reply.printf("Heater %u tuning succeeded, use M307 H%u to see result", GetHeaterNumber(), GetHeaterNumber()); - } - else - { - reply.printf("Heater %u tuning failed", GetHeaterNumber()); - } -} - // Call this when the PWM of a cooling fan has changed. If there are multiple fans, caller must divide pwmChange by the number of fans. -void LocalHeater::PrintCoolingFanPwmChanged(float pwmChange) noexcept +void LocalHeater::FeedForwardAdjustment(float fanPwmChange, float extrusionChange) noexcept { if (mode == HeaterMode::stable) { - const float coolingRateIncrease = GetModel().GetCoolingRateChangeFanOn() * pwmChange; + const float coolingRateIncrease = GetModel().GetCoolingRateChangeFanOn() * fanPwmChange; const float boost = (coolingRateIncrease * (GetTargetTemperature() - NormalAmbientTemperature) * FeedForwardMultiplier)/GetModel().GetHeatingRate(); #if 0 if (reprap.Debug(moduleHeat)) @@ -690,13 +580,11 @@ void LocalHeater::DoTuningStep() noexcept SetHeater(0.0); peakTemp = afterPeakTemp = temperature; lastOffTime = peakTime = afterPeakTime = now; -#if HAS_VOLTAGE_MONITOR tuningVoltage.Clear(); -#endif idleCyclesDone = 0; mode = HeaterMode::tuning2; tuningPhase = 2; - reprap.GetPlatform().Message(GenericMessage, "Auto tune starting phase 2, heater settling\n"); + ReportTuningUpdate(); } } return; @@ -726,7 +614,7 @@ void LocalHeater::DoTuningStep() noexcept if (idleCyclesDone == TuningHeaterMaxIdleCycles || (idleCyclesDone >= TuningHeaterMinIdleCycles && currentCoolingRate >= lastCoolingRate * HeaterSettledCoolingTimeRatio)) { tuningPhase = 3; - reprap.GetPlatform().Message(GenericMessage, "Auto tune starting phase 3, fan off\n"); + ReportTuningUpdate(); } else { @@ -762,11 +650,10 @@ void LocalHeater::DoTuningStep() noexcept ClearCounters(); #if TUNE_WITH_HALF_FAN reprap.GetFansManager().SetFansValue(tuningFans, 0.5); // turn fans on at half PWM - reprap.GetPlatform().Message(GenericMessage, "Auto tune starting phase 3, fan 50%\n"); #else reprap.GetFansManager().SetFansValue(tuningFans, 1.0); // turn fans on at full PWM - reprap.GetPlatform().Message(GenericMessage, "Auto tune starting phase 3, fan on\n"); #endif + ReportTuningUpdate(); } } #if TUNE_WITH_HALF_FAN @@ -776,7 +663,7 @@ void LocalHeater::DoTuningStep() noexcept tuningPhase = 5; ClearCounters(); reprap.GetFansManager().SetFansValue(tuningFans, 1.0); // turn fans fully on - reprap.GetPlatform().Message(GenericMessage, "Auto tune starting phase 4, fan 100%\n"); + ReportTuningUpdate(); } #endif else @@ -837,92 +724,6 @@ void LocalHeater::DoTuningStep() noexcept } // Calculate the heater model from the accumulated heater parameters -void LocalHeater::CalculateModel(HeaterParameters& params) noexcept -{ - if (reprap.Debug(moduleHeat)) - { -#define PLUS_OR_MINUS "\xC2\xB1" - reprap.GetPlatform().MessageF(GenericMessage, - "tOn %ld" PLUS_OR_MINUS "%ld, tOff %ld" PLUS_OR_MINUS "%ld," - " dHigh %ld" PLUS_OR_MINUS "%ld, dLow %ld" PLUS_OR_MINUS "%ld," - " R %.3f" PLUS_OR_MINUS "%.3f, C %.3f" PLUS_OR_MINUS "%.3f," -#if HAS_VOLTAGE_MONITOR - " V %.1f" PLUS_OR_MINUS "%.1f," -#endif - " cycles %u\n", - lrintf(tOn.GetMean()), lrintf(tOn.GetDeviation()), - lrintf(tOff.GetMean()), lrintf(tOff.GetDeviation()), - lrintf(dHigh.GetMean()), lrintf(dHigh.GetDeviation()), - lrintf(dLow.GetMean()), lrintf(dLow.GetDeviation()), - (double)heatingRate.GetMean(), (double)heatingRate.GetDeviation(), - (double)coolingRate.GetMean(), (double)coolingRate.GetDeviation(), -#if HAS_VOLTAGE_MONITOR - (double)tuningVoltage.GetMean(), (double)tuningVoltage.GetDeviation(), -#endif - coolingRate.GetNumSamples() - ); - } - - const float cycleTime = tOn.GetMean() + tOff.GetMean(); // in milliseconds - const float averageTemperatureRiseHeating = tuningTargetTemp - 0.5 * (TuningHysteresis - TuningPeakTempDrop) - tuningStartTemp.GetMean(); - const float averageTemperatureRiseCooling = tuningTargetTemp - TuningPeakTempDrop - 0.5 * TuningHysteresis - tuningStartTemp.GetMean(); - params.deadTime = (((dHigh.GetMean() * tOff.GetMean()) + (dLow.GetMean() * tOn.GetMean())) * MillisToSeconds)/cycleTime; // in seconds - params.coolingRate = coolingRate.GetMean()/averageTemperatureRiseCooling; // in seconds - params.heatingRate = (heatingRate.GetMean() + (coolingRate.GetMean() * averageTemperatureRiseHeating/averageTemperatureRiseCooling)) / tuningPwm; - params.numCycles = dHigh.GetNumSamples(); -} - -void LocalHeater::SetAndReportModel(bool usingFans) noexcept -{ - const float hRate = (usingFans) ? (fanOffParams.heatingRate + fanOnParams.heatingRate) * 0.5 : fanOffParams.heatingRate; - const float deadTime = (usingFans) ? (fanOffParams.deadTime + fanOnParams.deadTime) * 0.5 : fanOffParams.deadTime; - const float fanOnCoolingRate = (usingFans) ? fanOnParams.coolingRate : fanOffParams.coolingRate; - String<StringLength256> str; - const GCodeResult rslt = SetModel( hRate, - fanOffParams.coolingRate, fanOnCoolingRate, - deadTime, - tuningPwm, -#if HAS_VOLTAGE_MONITOR - tuningVoltage.GetMean(), -#else - 0.0, -#endif - true, false, str.GetRef()); - if (rslt == GCodeResult::ok || rslt == GCodeResult::warning) - { - tuned = true; - str.printf("Auto tuning heater %u completed after %u idle and %u tuning cycles in %" PRIu32 " seconds. This heater needs the following M307 command:\n" - " M307 H%u R%.3f C%.1f", - GetHeaterNumber(), - idleCyclesDone, - (usingFans) ? fanOffParams.numCycles + fanOnParams.numCycles : fanOffParams.numCycles, - (millis() - tuningBeginTime)/(uint32_t)SecondsToMillis, - GetHeaterNumber(), (double)GetModel().GetHeatingRate(), (double)(1.0/GetModel().GetCoolingRateFanOff()) - ); - if (usingFans) - { - str.catf(":%.1f", (double)(1.0/GetModel().GetCoolingRateFanOn())); - } - str.catf(" D%.2f S%.2f V%.1f\n", (double)GetModel().GetDeadTime(), (double)GetModel().GetMaxPwm(), (double)GetModel().GetVoltage()); - reprap.GetPlatform().Message(LoggedGenericMessage, str.c_str()); - if (reprap.GetGCodes().SawM501InConfigFile()) - { - reprap.GetPlatform().Message(GenericMessage, "Send M500 to save this command in config-override.g\n"); - } - else - { - reprap.GetPlatform().MessageF(GenericMessage, "Edit the M307 H%u command in config.g to match this. Omit the V parameter if the heater is not powered from VIN.\n", GetHeaterNumber()); - } - } - else - { - reprap.GetPlatform().MessageF(WarningMessage, "Auto tune of heater %u failed due to bad curve fit (R=%.3f, 1/C=%.4f:%.4f, D=%.1f)\n", - GetHeaterNumber(), (double)hRate, - (double)fanOffParams.coolingRate, (double)fanOnCoolingRate, - (double)fanOffParams.deadTime); - } -} - // Suspend the heater, or resume it void LocalHeater::Suspend(bool sus) noexcept { diff --git a/src/Heating/LocalHeater.h b/src/Heating/LocalHeater.h index 0486bccf..02fde4f9 100644 --- a/src/Heating/LocalHeater.h +++ b/src/Heating/LocalHeater.h @@ -17,7 +17,6 @@ #include "TemperatureError.h" #include <Hardware/IoPorts.h> #include <GCodes/GCodeResult.h> -#include <Math/DeviationAccumulator.h> class HeaterMonitor; @@ -26,14 +25,6 @@ class LocalHeater : public Heater static const size_t NumPreviousTemperatures = 4; // How many samples we average the temperature derivative over public: - struct HeaterParameters - { - float heatingRate; - float coolingRate; - float deadTime; - unsigned int numCycles; - }; - LocalHeater(unsigned int heaterNum) noexcept; ~LocalHeater() noexcept; @@ -41,35 +32,33 @@ public: GCodeResult SetPwmFrequency(PwmFrequency freq, const StringRef& reply) noexcept override; GCodeResult ReportDetails(const StringRef& reply) const noexcept override; - void Spin() noexcept override; // Called in a tight loop to keep things running - void SwitchOff() noexcept override; // Not even standby - all heater power off - GCodeResult ResetFault(const StringRef& reply) noexcept override; // Reset a fault condition - only call this if you know what you are doing - float GetTemperature() const noexcept override; // Get the current temperature - float GetAveragePWM() const noexcept override; // Return the running average PWM to the heater. Answer is a fraction in [0, 1]. - float GetAccumulator() const noexcept override; // Return the integral accumulator - GCodeResult StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) override; // Start an auto tune cycle for this heater - void GetAutoTuneStatus(const StringRef& reply) const noexcept override; // Get the auto tune status or last result - void Suspend(bool sus) noexcept override; // Suspend the heater to conserve power or while doing Z probing - void PrintCoolingFanPwmChanged(float pwmChange) noexcept override; + void Spin() noexcept override; // Called in a tight loop to keep things running + void SwitchOff() noexcept override; // Not even standby - all heater power off + GCodeResult ResetFault(const StringRef& reply) noexcept override; // Reset a fault condition - only call this if you know what you are doing + float GetTemperature() const noexcept override; // Get the latest temperature + float GetAveragePWM() const noexcept override; // Return the running average PWM to the heater. Answer is a fraction in [0, 1]. + float GetAccumulator() const noexcept override; // Return the integral accumulator + void Suspend(bool sus) noexcept override; // Suspend the heater to conserve power or while doing Z probing + void FeedForwardAdjustment(float fanPwmChange, float extrusionChange) noexcept override; #if SUPPORT_CAN_EXPANSION void UpdateRemoteStatus(CanAddress src, const CanHeaterReport& report) noexcept override { } + void UpdateHeaterTuning(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept override { } #endif protected: void ResetHeater() noexcept override; HeaterMode GetMode() const noexcept override { return mode; } - GCodeResult SwitchOn(const StringRef& reply) noexcept override; // Turn the heater on and set the mode - GCodeResult UpdateModel(const StringRef& reply) noexcept override; // Called when the heater model has been changed + GCodeResult SwitchOn(const StringRef& reply) noexcept override; // Turn the heater on and set the mode + GCodeResult UpdateModel(const StringRef& reply) noexcept override; // Called when the heater model has been changed GCodeResult UpdateFaultDetectionParameters(const StringRef& reply) noexcept override { return GCodeResult::ok; } GCodeResult UpdateHeaterMonitors(const StringRef& reply) noexcept override { return GCodeResult::ok; } + GCodeResult StartAutoTune(const StringRef& reply, FansBitmap fans, float targetTemp, float pwm, bool seenA, float ambientTemp) noexcept override; // Start an auto tune cycle for this heater private: void SetHeater(float power) const noexcept; // Power is a fraction in [0,1] TemperatureError ReadTemperature() noexcept; // Read and store the temperature of this heater void DoTuningStep() noexcept; // Called on each temperature sample when auto tuning - void CalculateModel(HeaterParameters& params) noexcept; // Calculate G, td and tc from the accumulated readings - void SetAndReportModel(bool usingFans) noexcept; float GetExpectedHeatingRate() const noexcept; // Get the minimum heating rate we expect void RaiseHeaterFault(const char *format, ...) noexcept; @@ -87,19 +76,9 @@ private: uint8_t previousTemperaturesGood; // Bitmap indicating which previous temperature were good readings HeaterMode mode; // Current state of the heater - bool tuned; // True if tuning was successful uint8_t badTemperatureCount; // Count of sequential dud readings static_assert(sizeof(previousTemperaturesGood) * 8 >= NumPreviousTemperatures, "too few bits in previousTemperaturesGood"); - - static constexpr unsigned int TuningHeaterMinIdleCycles = 3; // minimum number of idle cycles after heating up, including the initial overshoot and cool down - static constexpr unsigned int TuningHeaterMaxIdleCycles = 10; - static constexpr unsigned int MinTuningHeaterCycles = 5; - static constexpr unsigned int MaxTuningHeaterCycles = 25; - static constexpr float TuningHysteresis = 5.0; - static constexpr float TuningPeakTempDrop = 2.0; // must be well below TuningHysteresis - static constexpr float FeedForwardMultiplier = 1.3; // how much we over-compensate feedforward to allow for heat reservoirs during tuning - static constexpr float HeaterSettledCoolingTimeRatio = 0.93; }; #endif /* SRC_LOCALHEATER_H_ */ diff --git a/src/Heating/RemoteHeater.cpp b/src/Heating/RemoteHeater.cpp index 4012e387..34badf58 100644 --- a/src/Heating/RemoteHeater.cpp +++ b/src/Heating/RemoteHeater.cpp @@ -17,8 +17,14 @@ #include <CanMessageFormats.h> #include <CanMessageBuffer.h> +// Static variables used only during tuning +uint32_t RemoteHeater::timeSetHeating; +float RemoteHeater::currentCoolingRate; +unsigned int RemoteHeater::tuningCyclesDone; +bool RemoteHeater::newTuningResult = false; + RemoteHeater::RemoteHeater(unsigned int num, CanAddress board) noexcept - : Heater(num), boardAddress(board), lastMode(HeaterMode::offline), averagePwm(0), lastTemperature(0.0), whenLastStatusReceived(0) + : Heater(num), boardAddress(board), lastMode(HeaterMode::offline), averagePwm(0), tuningState(TuningState::notTuning), lastTemperature(0.0), whenLastStatusReceived(0) { } @@ -33,12 +39,163 @@ RemoteHeater::~RemoteHeater() noexcept void RemoteHeater::Spin() noexcept { - // Nothing needed here unless we want to copy the sensor temperature across. For now we don't store the temperature locally. + const uint32_t now = millis(); + switch (tuningState) + { + case TuningState::notTuning: + break; + + case TuningState::stabilising: + if (tuningStartTemp.GetNumSamples() < 5000/HeatSampleIntervalMillis) + { + tuningStartTemp.Add(lastTemperature); // take another reading until we have samples temperatures for 5 seconds + } + else if (tuningStartTemp.GetDeviation() <= 2.0) + { + timeSetHeating = now; + ClearCounters(); + timeSetHeating = millis(); + String<StringLength100> reply; + if (SendTuningCommand(reply.GetRef(), true) == GCodeResult::ok) + { + tuningState = TuningState::heatingUp; + tuningPhase = 1; + ReportTuningUpdate(); + } + else + { + reprap.GetPlatform().Message(ErrorMessage, "Failed to start heater tuning\n"); + tuningState = TuningState::notTuning; + } + } + else if (now - tuningBeginTime >= 20000) // allow up to 20 seconds for starting temperature to settle + { + reprap.GetPlatform().Message(GenericMessage, "Auto tune cancelled because starting temperature is not stable\n"); + StopTuning(); + } + break; + + case TuningState::heatingUp: + { + const bool isBedOrChamberHeater = reprap.GetHeat().IsBedOrChamberHeater(GetHeaterNumber()); + const uint32_t heatingTime = now - timeSetHeating; + const float extraTimeAllowed = (isBedOrChamberHeater) ? 120.0 : 30.0; + if (heatingTime > (uint32_t)((GetModel().GetDeadTime() + extraTimeAllowed) * SecondsToMillis) && (lastTemperature - tuningStartTemp.GetMean()) < 3.0) + { + reprap.GetPlatform().Message(GenericMessage, "Auto tune cancelled because temperature is not increasing\n"); + StopTuning(); + break; + } + + const uint32_t timeoutMinutes = (isBedOrChamberHeater) ? 30 : 7; + if (heatingTime >= timeoutMinutes * 60 * (uint32_t)SecondsToMillis) + { + reprap.GetPlatform().Message(GenericMessage, "Auto tune cancelled because target temperature was not reached\n"); + StopTuning(); + break; + } + + if (lastTemperature >= tuningTargetTemp) // if reached target + { + // Move on to next phase + peakTemp = afterPeakTemp = lastTemperature; + lastOffTime = peakTime = afterPeakTime = now; + tuningVoltage.Clear(); + idleCyclesDone = 0; + newTuningResult = false; + tuningState = TuningState::idleCycles; + tuningPhase = 2; + ReportTuningUpdate(); + } + } + break; + + case TuningState::idleCycles: + if (newTuningResult) + { + // To allow for heat reservoirs, we do idle cycles until the cooling rate decreases by no more than a certain amount in a single cycle + if (idleCyclesDone == TuningHeaterMaxIdleCycles || (idleCyclesDone >= TuningHeaterMinIdleCycles && currentCoolingRate >= lastCoolingRate * HeaterSettledCoolingTimeRatio)) + { + tuningPhase = 3; + tuningState = TuningState::cycling; + ReportTuningUpdate(); + } + else + { + lastCoolingRate = currentCoolingRate; + ClearCounters(); + ++idleCyclesDone; + } + newTuningResult = false; + } + break; + + case TuningState::cycling: + if (newTuningResult) + { + if (coolingRate.GetNumSamples() >= MinTuningHeaterCycles) + { + const bool isConsistent = dLow.DeviationFractionWithin(0.2) + && dHigh.DeviationFractionWithin(0.2) + && heatingRate.DeviationFractionWithin(0.1) + && coolingRate.DeviationFractionWithin(0.1); + if (isConsistent || coolingRate.GetNumSamples() == MaxTuningHeaterCycles) + { + if (!isConsistent) + { + reprap.GetPlatform().Message(WarningMessage, "heater behaviour was not consistent during tuning\n"); + } + + if (tuningPhase == 3) + { + CalculateModel(fanOffParams); + if (tuningFans.IsEmpty()) + { + SetAndReportModel(false); + StopTuning(); + break; + } + else + { + tuningPhase = 4; + ClearCounters(); +#if TUNE_WITH_HALF_FAN + reprap.GetFansManager().SetFansValue(tuningFans, 0.5); // turn fans on at half PWM +#else + reprap.GetFansManager().SetFansValue(tuningFans, 1.0); // turn fans on at full PWM +#endif + ReportTuningUpdate(); + } + } +#if TUNE_WITH_HALF_FAN + else if (tuningPhase == 4) + { + CalculateModel(fanOnParams); + tuningPhase = 5; + ClearCounters(); + reprap.GetFansManager().SetFansValue(tuningFans, 1.0); // turn fans fully on + ReportTuningUpdate(); + } +#endif + else + { + reprap.GetFansManager().SetFansValue(tuningFans, 0.0); // turn fans off + CalculateModel(fanOnParams); + SetAndReportModel(true); + StopTuning(); + break; + } + } + } + newTuningResult = false; + } + break; + } } void RemoteHeater::ResetHeater() noexcept { - //TODO + // This is only called by UpdateModel. Nothing needed here. } GCodeResult RemoteHeater::ConfigurePortAndSensor(const char *portName, PwmFrequency freq, unsigned int sn, const StringRef& reply) @@ -69,10 +226,11 @@ GCodeResult RemoteHeater::ReportDetails(const StringRef& reply) const noexcept void RemoteHeater::SwitchOff() noexcept { + constexpr const char *errMsg = "Failed to switch off remote heater %u: %s\n"; CanMessageBuffer * const buf = CanMessageBuffer::Allocate(); if (buf == nullptr) { - reprap.GetPlatform().MessageF(ErrorMessage, "Failed to switch off remote heater %u: no CAN buffer available\n", GetHeaterNumber()); + reprap.GetPlatform().MessageF(ErrorMessage, errMsg, GetHeaterNumber(), "no CAN buffer available"); } else { @@ -84,7 +242,7 @@ void RemoteHeater::SwitchOff() noexcept String<StringLength100> reply; if (CanInterface::SendRequestAndGetStandardReply(buf, rid, reply.GetRef()) != GCodeResult::ok) { - reprap.GetPlatform().MessageF(ErrorMessage, "Failed to switch off remote heater %u: %s\n", GetHeaterNumber(), reply.c_str()); + reprap.GetPlatform().MessageF(ErrorMessage, errMsg, GetHeaterNumber(), reply.c_str()); } } } @@ -128,20 +286,68 @@ float RemoteHeater::GetAccumulator() const noexcept return 0.0; // not supported } -GCodeResult RemoteHeater::StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) +GCodeResult RemoteHeater::StartAutoTune(const StringRef& reply, FansBitmap fans, float targetTemp, float pwm, bool seenA, float ambientTemp) noexcept { - reply.copy("remote heater auto tune not implemented"); - return GCodeResult::error; -} + CanMessageBuffer * const buf = CanMessageBuffer::Allocate(); + if (buf == nullptr) + { + reply.copy("No CAN buffer"); + return GCodeResult::error; + } -void RemoteHeater::GetAutoTuneStatus(const StringRef& reply) const noexcept -{ - reply.copy("remote heater auto tune not implemented"); + tuningFans = fans; + reprap.GetFansManager().SetFansValue(tuningFans, 0.0); + + tuningPwm = pwm; + tuningTargetTemp = targetTemp; + tuningStartTemp.Clear(); + tuningBeginTime = millis(); + tuned = false; + + if (seenA) + { + tuningStartTemp.Add(ambientTemp); + ClearCounters(); + timeSetHeating = millis(); + GCodeResult rslt = SendTuningCommand(reply, true); + if (rslt != GCodeResult::ok) + { + return rslt; + } + tuningState = TuningState::heatingUp; + tuningPhase = 1; + ReportTuningUpdate(); + } + else + { + tuningState = TuningState::stabilising; + tuningPhase = 0; + } + + return GCodeResult::ok; } -void RemoteHeater::PrintCoolingFanPwmChanged(float pwmChange) noexcept +void RemoteHeater::FeedForwardAdjustment(float fanPwmChange, float extrusionChange) noexcept { - //TODO send a CAN message to remote + constexpr const char* warnMsg = "Failed to make heater feedforward adjustment: %s\n"; + CanMessageBuffer * const buf = CanMessageBuffer::Allocate(); + if (buf == nullptr) + { + reprap.GetPlatform().MessageF(WarningMessage, warnMsg, "no CAN buffer"); + } + else + { + const CanRequestId rid = CanInterface::AllocateRequestId(boardAddress); + auto msg = buf->SetupRequestMessage<CanMessageHeaterFeedForward>(rid, CanInterface::GetCanAddress(), boardAddress); + msg->heaterNumber = GetHeaterNumber(); + msg->fanPwmAdjustment = fanPwmChange; + msg->extrusionAdjustment = extrusionChange; + String<StringLength100> reply; + if (CanInterface::SendRequestAndGetStandardReply(buf, rid, reply.GetRef()) != GCodeResult::ok) + { + reprap.GetPlatform().MessageF(WarningMessage, reply.c_str()); + } + } } void RemoteHeater::Suspend(bool sus) noexcept @@ -161,7 +367,9 @@ void RemoteHeater::Suspend(bool sus) noexcept Heater::HeaterMode RemoteHeater::GetMode() const noexcept { - return (millis() - whenLastStatusReceived < RemoteStatusTimeout) ? lastMode : HeaterMode::offline; + return (tuningState != TuningState::notTuning) ? HeaterMode::tuning0 + : (millis() - whenLastStatusReceived < RemoteStatusTimeout) ? lastMode + : HeaterMode::offline; } // This isn't just called to turn the heater on, it is called when the temperature needs to be updated @@ -249,6 +457,54 @@ void RemoteHeater::UpdateRemoteStatus(CanAddress src, const CanHeaterReport& rep } } +void RemoteHeater::UpdateHeaterTuning(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept +{ + if (src == boardAddress && tuningState >= TuningState::idleCycles && !newTuningResult) + { + tOn.Add((float)msg.ton); + tOff.Add((float)msg.toff); + dHigh.Add((float)msg.dhigh); + dLow.Add((float)msg.dlow); + heatingRate.Add(msg.heatingRate); + coolingRate.Add(msg.coolingRate); + tuningVoltage.Add(msg.voltage); + currentCoolingRate = msg.coolingRate; + tuningCyclesDone = msg.cyclesDone; + newTuningResult = true; + } +} + +GCodeResult RemoteHeater::SendTuningCommand(const StringRef& reply, bool on) noexcept +{ + CanMessageBuffer * const buf = CanMessageBuffer::Allocate(); + if (buf == nullptr) + { + reply.copy("No CAN buffer"); + return GCodeResult::error; + } + + const CanRequestId rid = CanInterface::AllocateRequestId(boardAddress); + auto msg = buf->SetupRequestMessage<CanMessageHeaterTuningCommand>(rid, CanInterface::GetCanAddress(), boardAddress); + msg->heaterNumber = GetHeaterNumber(); + msg->on = on; + msg->highTemp = tuningTargetTemp; + msg->lowTemp = tuningTargetTemp - TuningHysteresis; + msg->pwm = tuningPwm; + msg->peakTempDrop = TuningPeakTempDrop; + return CanInterface::SendRequestAndGetStandardReply(buf, rid, reply); +} + +void RemoteHeater::StopTuning() noexcept +{ + tuningState = TuningState::notTuning; + String<StringLength100> reply; + if (SendTuningCommand(reply.GetRef(), false) != GCodeResult::ok) + { + reprap.GetPlatform().MessageF(ErrorMessage, "%s\n", reply.c_str()); + reprap.GetPlatform().MessageF(ErrorMessage, "DANGER! Failed to stop tuning heater %u on CAN board %u, suggest turn power off\n", GetHeaterNumber(), boardAddress); + } +} + #endif // End diff --git a/src/Heating/RemoteHeater.h b/src/Heating/RemoteHeater.h index 907908e7..548abe9e 100644 --- a/src/Heating/RemoteHeater.h +++ b/src/Heating/RemoteHeater.h @@ -22,34 +22,53 @@ public: GCodeResult SetPwmFrequency(PwmFrequency freq, const StringRef& reply) override; GCodeResult ReportDetails(const StringRef& reply) const noexcept override; - void Spin() noexcept override; // Called in a tight loop to keep things running - void SwitchOff() noexcept override; // Not even standby - all heater power off - GCodeResult ResetFault(const StringRef& reply) noexcept override; // Reset a fault condition - only call this if you know what you are doing - float GetTemperature() const noexcept override; // Get the current temperature - float GetAveragePWM() const noexcept override; // Return the running average PWM to the heater. Answer is a fraction in [0, 1]. - float GetAccumulator() const noexcept override; // Return the integral accumulator - GCodeResult StartAutoTune(GCodeBuffer& gb, const StringRef& reply, FansBitmap fans) THROWS(GCodeException) override; // Start an auto tune cycle for this heater - void GetAutoTuneStatus(const StringRef& reply) const noexcept override; // Get the auto tune status or last result - void Suspend(bool sus) noexcept override; // Suspend the heater to conserve power or while doing Z probing - void PrintCoolingFanPwmChanged(float pwmChange) noexcept override; + void Spin() noexcept override; // Called in a tight loop to keep things running + void SwitchOff() noexcept override; // Not even standby - all heater power off + GCodeResult ResetFault(const StringRef& reply) noexcept override; // Reset a fault condition - only call this if you know what you are doing + float GetTemperature() const noexcept override; // Get the latest temperature + float GetAveragePWM() const noexcept override; // Return the running average PWM to the heater. Answer is a fraction in [0, 1]. + float GetAccumulator() const noexcept override; // Return the integral accumulator + void Suspend(bool sus) noexcept override; // Suspend the heater to conserve power or while doing Z probing + void FeedForwardAdjustment(float fanPwmChange, float extrusionChange) noexcept override; void UpdateRemoteStatus(CanAddress src, const CanHeaterReport& report) noexcept override; + void UpdateHeaterTuning(CanAddress src, const CanMessageHeaterTuningReport& msg) noexcept override; protected: void ResetHeater() noexcept override; HeaterMode GetMode() const noexcept override; - GCodeResult SwitchOn(const StringRef& reply) noexcept override; // Turn the heater on and set the mode - GCodeResult UpdateModel(const StringRef& reply) noexcept override; // Called when the heater model has been changed + GCodeResult SwitchOn(const StringRef& reply) noexcept override; // Turn the heater on and set the mode + GCodeResult UpdateModel(const StringRef& reply) noexcept override; // Called when the heater model has been changed GCodeResult UpdateFaultDetectionParameters(const StringRef& reply) noexcept override; GCodeResult UpdateHeaterMonitors(const StringRef& reply) noexcept override; + GCodeResult StartAutoTune(const StringRef& reply, FansBitmap fans, float targetTemp, float pwm, bool seenA, float ambientTemp) noexcept override; // Start an auto tune cycle for this heater private: + enum class TuningState : uint8_t + { + notTuning = 0, + stabilising, + heatingUp, + idleCycles, + cycling + }; + + GCodeResult SendTuningCommand(const StringRef& reply, bool on) noexcept; + void StopTuning() noexcept; + static constexpr uint32_t RemoteStatusTimeout = 2000; CanAddress boardAddress; HeaterMode lastMode; uint8_t averagePwm; + TuningState tuningState; float lastTemperature; uint32_t whenLastStatusReceived; + + // Variables used only during tuning + static uint32_t timeSetHeating; // When we turned on the heater at the start of auto tuning + static float currentCoolingRate; + static unsigned int tuningCyclesDone; + static bool newTuningResult; }; #endif |