-
-
Notifications
You must be signed in to change notification settings - Fork 106
Implement flow-based thermal feedforward for heater control #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -65,6 +65,23 @@ void Heater::setTunings(float Kp, float Ki, float Kd) { | |
| } | ||
| } | ||
|
|
||
|
|
||
| void Heater::setThermalFeedforward(float *pumpFlowPtr, float incomingWaterTemp, bool *valveStatusPtr) { | ||
| pumpFlowRate = pumpFlowPtr; | ||
| valveStatus = valveStatusPtr; | ||
| this->incomingWaterTemp = incomingWaterTemp; | ||
|
|
||
| ESP_LOGI(LOG_TAG, "Thermal feedforward setup - incoming water temp: %.1f°C, valve tracking: %s", | ||
| incomingWaterTemp, valveStatusPtr ? "enabled" : "disabled"); | ||
| ESP_LOGI(LOG_TAG, "Feedforward will be %s based on Kff value (currently %.3f)", | ||
| combinedKff > 0.0f ? "ENABLED" : "DISABLED", combinedKff); | ||
| } | ||
|
|
||
| void Heater::setFeedforwardScale(float combinedKff) { | ||
| this->combinedKff = combinedKff; | ||
| ESP_LOGI(LOG_TAG, "Combined feedforward gain (Kff) set to: %.3f output units per watt", combinedKff); | ||
| } | ||
|
|
||
| void Heater::autotune(int goal, int windowSize) { | ||
| setupAutotune(goal, windowSize); | ||
| autotuning = true; | ||
|
|
@@ -73,7 +90,35 @@ void Heater::autotune(int goal, int windowSize) { | |
| void Heater::loopPid() { | ||
| softPwm(TUNER_OUTPUT_SPAN); | ||
| temperature = sensor->read(); | ||
| if (simplePid->update()) { | ||
|
|
||
| // Calculate and set disturbance feedforward BEFORE PID update | ||
| // Only apply thermal feedforward when Kf>0, valve is open, and water is flowing | ||
| if (combinedKff > 0.0f && pumpFlowRate && *pumpFlowRate > 0.01f && valveStatus && *valveStatus) { | ||
| float currentFlowRate = *pumpFlowRate; // Use raw flow rate for fast response | ||
| float disturbanceGain = calculateDisturbanceFeedforwardGain(); | ||
|
|
||
| // Apply smoothed temperature-based safety scaling | ||
| float tempError = temperature - setpoint; | ||
| float rawSafetyFactor = calculateSafetyScaling(tempError); | ||
|
|
||
| // Smooth safety factor transitions to reduce oscillations | ||
| const float safetyAlpha = 0.85f; // Faster response for quicker feedforward | ||
| float safetyFactor = safetyAlpha * rawSafetyFactor + (1.0f - safetyAlpha) * lastSafetyFactor; | ||
| lastSafetyFactor = safetyFactor; | ||
|
|
||
| disturbanceGain *= safetyFactor; | ||
|
|
||
| // Set the disturbance feedforward in SimplePID | ||
| simplePid->setDisturbanceFeedforward(currentFlowRate, disturbanceGain); | ||
|
|
||
| } else { | ||
| simplePid->setDisturbanceFeedforward(0.0f, 0.0f); | ||
| } | ||
|
|
||
| // Now run PID with proper feedforward integrated | ||
| bool pidUpdated = simplePid->update(); | ||
|
|
||
| if (pidUpdated) { | ||
| plot(output, 1.0f, 1); | ||
| } | ||
| } | ||
|
|
@@ -151,6 +196,43 @@ void Heater::plot(float optimumOutput, float outputScale, uint8_t everyNth) { | |
| plotCount++; | ||
| } | ||
|
|
||
| float Heater::calculateDisturbanceFeedforwardGain() { | ||
| if (combinedKff <= 0.0f || !pumpFlowRate || *pumpFlowRate <= 0.01f) { | ||
| return 0.0f; | ||
| } | ||
|
|
||
| float currentFlowRate = *pumpFlowRate; // Use raw flow rate for fast response | ||
|
|
||
| // Calculate temperature difference (target - incoming water temperature) | ||
| float tempDelta = setpoint - incomingWaterTemp; | ||
| if (tempDelta <= 0.0f) return 0.0f; | ||
|
|
||
| // Calculate thermal power needed per ml/s of flow (Watts per ml/s) | ||
| float powerPerFlowRate = WATER_DENSITY * WATER_SPECIFIC_HEAT * tempDelta + (heatLossWatts / currentFlowRate); | ||
| powerPerFlowRate /= heaterEfficiency; | ||
|
|
||
| // Apply combined Kff directly (output units per watt) | ||
| float gainPerFlowRate = powerPerFlowRate * combinedKff; | ||
|
|
||
| return gainPerFlowRate; | ||
| } | ||
|
|
||
| float Heater::calculateSafetyScaling(float tempError) { | ||
| // tempError = temperature - setpoint | ||
| // Use smoother, less aggressive safety scaling to reduce oscillations | ||
| if (tempError > 1.0f) { | ||
| return 0.0f; // No FF if more than 1.0°C above setpoint | ||
| } else if (tempError > 0.0f) { | ||
| // Gradual reduction: 100% at 0°C error, 70% at +1.0°C error | ||
| return 0.7f + 0.3f * (1.0f - tempError / 1.0f); | ||
| } else if (tempError > -1.0f) { | ||
| // Scale from 70% to 100% as temperature drops below setpoint | ||
| return 0.7f + 0.3f * std::abs(tempError) / 1.0f; | ||
| } else { | ||
| return 1.0f; // Full FF when more than 1.0°C below setpoint | ||
| } | ||
| } | ||
|
Comment on lines
+199
to
+234
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thermal gain math is reasonable, but fix The disturbance gain computation and safety scaling logic are generally sound and well guarded, but there are two nits worth fixing:
In lib/GaggiMateController/src/peripherals/Heater.cpp around lines 199–234, add |
||
|
|
||
| void Heater::loopTask(void *arg) { | ||
| TickType_t lastWake = xTaskGetTickCount(); | ||
| auto *heater = static_cast<Heater *>(arg); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,12 @@ class SimplePID { | |
| void setKi(float val) { gainKi = val; }; | ||
| void setKd(float val) { gainKd = val; }; | ||
| void setKFF(float val) { gainFF = val; }; | ||
|
|
||
| // Disturbance feedforward methods | ||
| void setDisturbanceFeedforward(float disturbance, float gainDFF); | ||
| void setDisturbanceGain(float gainDFF) { gainDistFF = gainDFF; }; | ||
| float getDisturbanceGain() { return gainDistFF; }; | ||
| void activateDisturbanceFeedForward(bool flag) { isDisturbanceFeedForwardActive = flag; }; | ||
|
Comment on lines
+47
to
+51
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Verify the interaction between The Consider either removing the separate activation method or adding validation to ensure 🤖 Prompt for AI Agents |
||
|
|
||
| private: | ||
| // setpoint filtering | ||
|
|
@@ -66,6 +72,12 @@ class SimplePID { | |
| float gainKi = 0.0f; // Integral gain (multiplies by Kp if Kp,Ki,Kd are strictly parallèle (no factoring by Kp)) | ||
| float gainKd = 0.0f; // Derivative gain (by default no derivative term) | ||
| float gainFF = 0.5 * 1000.0f / 2.5f; // Feedforward gain | ||
|
|
||
| // Disturbance feedforward variables | ||
| float gainDistFF = 0.0f; // Disturbance feedforward gain | ||
| float currentDisturbance = 0.0f; // Current disturbance value | ||
| bool isDisturbanceFeedForwardActive = false; // Flag to activate disturbance feedforward | ||
|
|
||
| float feedback_integralState = 0.0f; // Integral state | ||
| float prevError = 0.0f; // Previous error for derivative calculation | ||
| float prevOutput = 0.0f; // Previous output for derivative calculation | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Unsafe type cast in
getValveStatusPtr().Line 25 uses
reinterpret_cast<bool*>(&_valveStatus)where_valveStatusis declared asint(line 52). This is unsafe and leads to undefined behavior:intis typically 4 bytes, whileboolis typically 1 bytebool*pointing to the first byte of theint's memory_valveStatusstores values beyond 0/1 or if endianness mattersRecommended solutions:
Option 1 (preferred): Change
_valveStatustobooltype if it only stores true/false values.Option 2: Return
int*and let the caller handle the type conversion safely:Option 3: Add a separate
boolmember that shadows_valveStatus:🤖 Prompt for AI Agents