Skip to content

feat: warmup detection using heater power trend#562

Open
bergetp wants to merge 7 commits intojniebuhr:masterfrom
bergetp:feat/warmup-trend
Open

feat: warmup detection using heater power trend#562
bergetp wants to merge 7 commits intojniebuhr:masterfrom
bergetp:feat/warmup-trend

Conversation

@bergetp
Copy link

@bergetp bergetp commented Jan 29, 2026

Summary

Adds heater‑power reporting over BLE and a warm‑up readiness indicator based on heater‑power trend convergence. Indicator: red flashing = not temp‑stable, yellow = temp‑stable but power trend not yet stable, green = power trend stable (warmed up).

Algorithm

  1. Wait for temperature to be stable (existing check).
  2. Compute time‑weighted average heater power over 60‑second windows.
  3. If two consecutive window averages differ by ≤ 1%, latch as warmed up.
  4. Reset if temperature becomes unstable or setpoint changes.

Once latched, warmup stays green until a real state change (temp instability or setpoint change). This avoids flickering from normal PID duty‑cycle noise.

Changes

Controller

  • Expose Heater::getOutput() and convert 0–1000 to 0–100% in sendSensorData().
  • Add heater power as 6th field in BLE sensor data (backwards compatible — defaults to NaN if absent).

Comms

  • Append heater power to sensor CSV payload.
  • Add legacy sensor callback overload for backwards compatibility.

Display

  • Parse heater power and publish boiler:heaterPower:change event.
  • Track heater‑power windows and set isWarmedUp when the trend stabilizes.
  • Heating icon uses red/yellow/green three‑state mapping.
  • Fix opacity bug where icon could stay semi‑transparent after becoming stable.
  • Fix isTemperatureStable type from int to bool.

Testing

  • Manual testing from cold start — ~7 minutes to green, no flickering.
  • Window averages show clean convergence: 14.2 → 10.8 → 8.4 → 7.6% (latched at 0.8% diff).

Summary by CodeRabbit

  • New Features

    • Heater power monitoring: system now tracks and reports heater power (0–100%) and exposes it to the UI.
    • Extended sensor data: Bluetooth sensor readings now include heater power; legacy callback support added for compatibility.
  • Enhancements

    • Enhanced heating indicator & warmup detection: three-state visual indicator (red/yellow/green), power-trend analysis, timing windows, reset/warmup handling, and broader reactive UI updates.
  • Chores

    • Emits heaterPower change events for reactive UI updates.

@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

Thank you for your pull request and welcome to our community. We could not parse the GitHub identity of the following contributors: Per Berglund.
This is most likely caused by a git client misconfiguration; please make sure to:

  1. check if your git client is configured with an email to sign commits git config --list | grep email
  2. If not, set it up using git config --global user.email email@example.com
  3. Make sure that the git commit email is configured in your GitHub account settings, see https://github.com/settings/emails

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Heater output is exposed and propagated as a new heaterPower value across BLE and UI: Heater gains getOutput(), GaggiMateController computes heaterPower and passes it to BLE sendSensorData, NimBLE client/server types and callbacks add heaterPower, Controller stores/exposes it, and DefaultUI implements warmup detection using heater power samples.

Changes

Cohort / File(s) Summary
BLE server API
lib/NimBLEComm/src/NimBLEServerController.cpp, lib/NimBLEComm/src/NimBLEServerController.h
Added heaterPower parameter to sendSensorData; expanded CSV payload format and increased buffer size for the extra field.
BLE public types
lib/NimBLEComm/src/NimBLEComm.h
Extended sensor_read_callback_t to include float heaterPower; added legacy_sensor_read_callback_t alias with the old five-arg signature.
BLE client handling
lib/NimBLEComm/src/NimBLEClientController.cpp, lib/NimBLEComm/src/NimBLEClientController.h
Parse optional heaterPower from SENSOR_DATA notifications; pass heaterPower to callbacks; added overload to register legacy callbacks that wrap the new callback.
Controller / BLE sender
lib/GaggiMateController/src/GaggiMateController.cpp
Compute heaterPower (convert heater output 0–1000 → 0–100%) and supply it to _ble.sendSensorData calls (updated signature).
Heater interface
lib/GaggiMateController/src/peripherals/Heater.h
Added public accessor float getOutput() const to expose heater output.
Display core
src/display/core/Controller.cpp, src/display/core/Controller.h
Bluetooth sensor callback extended to accept heaterPower; Controller stores heaterPower, exposes getHeaterPower(), and emits boiler:heaterPower:change.
UI warmup logic
src/display/ui/default/DefaultUI.cpp, src/display/ui/default/DefaultUI.h
Added warmup detection using heater power window/trend (new constants, state fields, resetWarmupState()); heating indicator/reactive handlers updated to consider isWarmedUp and accumulated heater power.

Sequence Diagram(s)

sequenceDiagram
    participant Heater
    participant GaggiMateCtrl as GaggiMateController
    participant NimBLEServer
    participant BLE as BLE_Link
    participant NimBLEClient
    participant DisplayCtrl as Controller
    participant DefaultUI

    Heater->>GaggiMateCtrl: getOutput()
    GaggiMateCtrl->>GaggiMateCtrl: compute heaterPower (0-100)
    GaggiMateCtrl->>NimBLEServer: sendSensorData(..., heaterPower)
    NimBLEServer->>BLE: notify SENSOR_DATA (includes heaterPower)
    BLE->>NimBLEClient: receive SENSOR_DATA
    NimBLEClient->>NimBLEClient: parse fields (temp, pressure, ..., heaterPower)
    NimBLEClient->>DisplayCtrl: sensorCallback(..., heaterPower)
    DisplayCtrl->>DisplayCtrl: update heaterPower, emit boiler:heaterPower:change
    DisplayCtrl->>DefaultUI: reactive handler receives heaterPower update
    DefaultUI->>DefaultUI: aggregate heaterPower samples, update isWarmedUp & UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

cla-signed

Poem

🐰 I nibble code where heater numbers climb,
I hop and sample warming, byte by time.
Packets hum blue across the BLE night,
UI blinks green when the warmup feels right.
Hooray for steam, and sensors snug and bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: warmup detection using heater power trend' accurately and concisely describes the primary change: adding warmup detection based on heater power trend convergence, which is the core feature implemented across all modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@bergetp bergetp marked this pull request as ready for review January 29, 2026 20:22
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/display/ui/default/DefaultUI.cpp`:
- Around line 220-233: The heater power accumulation in the
pluginManager->on("boiler:heaterPower:change", ...) handler updates
heaterPowerTimeSum, heaterPowerTimeTotal and lastHeaterPowerSampleTime
non-atomically while updateTempStableFlag() (UI loop) reads them, causing a
race; fix by protecting these shared variables with a synchronization primitive:
add a mutex (e.g., heaterPowerMutex) and use lock_guard in the event handler
around updates to heaterPowerTimeSum, heaterPowerTimeTotal and
lastHeaterPowerSampleTime, and likewise lock_guard in updateTempStableFlag()
when reading/clearing them, or alternatively accumulate into a local accumulator
structure in the handler and perform a single atomic swap/assignment (or
protected assignment under the same mutex) so reads in updateTempStableFlag()
see a consistent snapshot.
🧹 Nitpick comments (1)
src/display/ui/default/DefaultUI.cpp (1)

91-98: Minor: Redundant reset before initialization.

Calling resetWarmupState() immediately before setting stableStartTime and heaterPowerWindowStart results in those fields being set twice. Consider either removing the redundant assignments from resetWarmupState() in this path, or directly initializing without the reset call.

This is cosmetic and doesn't affect correctness.

♻️ Optional: Inline the necessary resets
     // Initialize stable state tracking when entering or re-entering stable state
     if (isTemperatureStable && stableStartTime == 0) {
-        resetWarmupState();
+        isWarmedUp = false;
+        heaterPowerTimeSum = 0.0f;
+        heaterPowerTimeTotal = 0;
+        lastHeaterPowerSampleTime = 0;
+        lastHeaterPowerWindowAvg = NAN;
         stableStartTime = now;
         heaterPowerWindowStart = now;

@bergetp bergetp changed the title feat: refine warmup detection using heater power trend feat: warmup detection using heater power trend Jan 29, 2026
@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/display/ui/default/DefaultUI.h`:
- Around line 80-90: The implementation in DefaultUI.cpp uses the misspelled
identifier lastHearerPowerSampleTime which doesn't match the field declared as
lastHeaterPowerSampleTime in DefaultUI.h; locate all occurrences of
lastHearerPowerSampleTime in DefaultUI.cpp (e.g., in functions
updateTempHistory(), updateTempStableFlag(), resetWarmupState() or any
heater-power sampling logic) and rename them to lastHeaterPowerSampleTime so the
implementation matches the declaration and the build error is resolved.

@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

3 similar comments
@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@cla-bot
Copy link

cla-bot bot commented Jan 29, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@cla-bot
Copy link

cla-bot bot commented Jan 30, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@src/display/ui/default/DefaultUI.cpp`:
- Around line 110-118: The heating icon's opacity is only adjusted when
!isTemperatureStable, so if it was previously dimmed (heatingFlash -> LV_OPA_50)
it can remain semi-transparent after transitioning to a stable state; after
computing color using isWarmedUp and isTemperatureStable (and before returning),
explicitly set opacity for heatingIcon to LV_OPA_100 when isTemperatureStable is
true (or otherwise ensure an else branch sets lv_obj_set_style_opa(heatingIcon,
LV_OPA_100, LV_PART_MAIN | LV_STATE_DEFAULT)) so stable states always render
solid; reference symbols: heatingIcon, isWarmedUp, isTemperatureStable,
heatingFlash.
- Around line 151-164: The event handler for "boiler:heaterPower:change" only
zeroes lastHeaterPowerSampleTime on NaN or spikes, leaving heaterPowerTimeSum,
heaterPowerTimeTotal and isWarmedUp stale; update the
pluginManager->on("boiler:heaterPower:change", ...) handler so that when
currentHeaterPower is NaN or a spike (compare against
HEATER_POWER_SPIKE_THRESHOLD) you reset lastHeaterPowerSampleTime to 0, set
heaterPowerTimeSum = 0, heaterPowerTimeTotal = 0, and set isWarmedUp = false to
fully reset the warmup window and state. Ensure HEATER_POWER_SPIKE_THRESHOLD is
referenced (define it in the appropriate header if not present) and keep the
rest of the sampling logic unchanged for valid samples.
- Around line 76-105: The warmup logic can never mark isWarmedUp when
heaterPowerTimeTotal stays 0 (legacy controllers), so add a stable-time
fallback: introduce a timestamp variable (e.g., stableSinceMs) that is set when
isTemperatureStable first becomes true (and cleared in resetWarmupState()), then
in the warmup check (the block using heaterPowerWindowStart,
HEATER_POWER_WINDOW_MS, lastHeaterPowerWindowAvg, HEATER_POWER_TREND_THRESHOLD)
also check if heaterPowerTimeTotal == 0 and (now - stableSinceMs) >=
STABLE_FALLBACK_MS (set STABLE_FALLBACK_MS to 10*60*1000) and if so set
isWarmedUp = true; ensure stableSinceMs is reset whenever isTemperatureStable
becomes false or in resetWarmupState().

Comment on lines +151 to +164
pluginManager->on("boiler:heaterPower:change", [=](Event const &event) {
currentHeaterPower = event.getFloat("value");
if (currentHeaterPower != currentHeaterPower || !isTemperatureStable || isWarmedUp) {
lastHeaterPowerSampleTime = 0;
return;
}
unsigned long now = millis();
if (lastHeaterPowerSampleTime > 0) {
unsigned long dt = now - lastHeaterPowerSampleTime;
heaterPowerTimeSum += currentHeaterPower * dt;
heaterPowerTimeTotal += dt;
}
lastHeaterPowerSampleTime = now;
});
Copy link
Contributor

@coderabbitai coderabbitai bot Jan 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset warmup on heater-power spikes and invalid samples.

Right now, NaN or a heater-power spike only zeroes lastHeaterPowerSampleTime, leaving the accumulated window and isWarmedUp untouched. This can preserve a warmed‑up state even after a ≥60% spike and keep stale averages. Resetting the warmup state on NaN/spikes will align behavior with the intended “reset on spike” rule.

✅ Proposed fix
-    if (currentHeaterPower != currentHeaterPower || !isTemperatureStable || isWarmedUp) {
-        lastHeaterPowerSampleTime = 0;
-        return;
-    }
+    if (currentHeaterPower != currentHeaterPower ||
+        currentHeaterPower >= HEATER_POWER_SPIKE_THRESHOLD ||
+        !isTemperatureStable) {
+        resetWarmupState();
+        return;
+    }
+    if (isWarmedUp) {
+        lastHeaterPowerSampleTime = 0;
+        return;
+    }

(Define HEATER_POWER_SPIKE_THRESHOLD in the appropriate header/config if not already present.)

🤖 Prompt for AI Agents
In `@src/display/ui/default/DefaultUI.cpp` around lines 151 - 164, The event
handler for "boiler:heaterPower:change" only zeroes lastHeaterPowerSampleTime on
NaN or spikes, leaving heaterPowerTimeSum, heaterPowerTimeTotal and isWarmedUp
stale; update the pluginManager->on("boiler:heaterPower:change", ...) handler so
that when currentHeaterPower is NaN or a spike (compare against
HEATER_POWER_SPIKE_THRESHOLD) you reset lastHeaterPowerSampleTime to 0, set
heaterPowerTimeSum = 0, heaterPowerTimeTotal = 0, and set isWarmedUp = false to
fully reset the warmup window and state. Ensure HEATER_POWER_SPIKE_THRESHOLD is
referenced (define it in the appropriate header if not present) and keep the
rest of the sampling logic unchanged for valid samples.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not adding spike detection — it adds complexity without clear benefit. NaN samples are already handled: they zero lastHeaterPowerSampleTime, so the next valid sample won't accumulate a bogus delta. The accumulated window data stays intact, which is correct — throwing away a 60s window because of one bad sample would be worse. If the boiler actually spikes, temperature will go unstable and resetWarmupState() is called via updateTempStableFlag() anyway. Warmup is also latched intentionally to avoid flickering from PID noise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bergetp, that makes sense—your reasoning is solid. The NaN handling via lastHeaterPowerSampleTime effectively breaks the accumulation chain without discarding valid window data, and the temperature-based reset already catches real instability. Intentional latching to avoid PID noise flickering is the right trade-off for UX. Thanks for the clarification!


✏️ Learnings added
Learnt from: bergetp
Repo: jniebuhr/gaggimate PR: 562
File: src/display/ui/default/DefaultUI.cpp:151-167
Timestamp: 2026-02-02T07:20:26.862Z
Learning: In src/display/ui/default/DefaultUI.cpp, the warmup detection (`isWarmedUp`) is intentionally latched once set to avoid flickering from PID noise. NaN heater power samples zero `lastHeaterPowerSampleTime` to prevent bogus accumulation without discarding the accumulated window data. Real heater power spikes causing instability are handled by temperature-based reset via `updateTempStableFlag()` calling `resetWarmupState()`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@cla-bot
Copy link

cla-bot bot commented Jan 31, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

1 similar comment
@cla-bot
Copy link

cla-bot bot commented Feb 2, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@cla-bot
Copy link

cla-bot bot commented Feb 7, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

bergetp and others added 7 commits February 20, 2026 08:50
Replace variance-plateau warmup detection with heater power trend
analysis. Transmit heater power over BLE from controller, then on the
display side collect 30s time-weighted average power windows and declare
warmup complete when consecutive windows differ by <= 2%.

- Add heater power as 6th field in BLE sensor data (backwards compatible)
- Gate warmup on temperature stability with hysteresis to avoid flapping
- Reset warmup on instability or heater power spike (>= 60%)
- 10 min stable fallback
- Consolidate currentTemp/targetTemp from int to float, removing
  redundant float copies
- Use NAN sentinel for window state, float accumulator for thread safety

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Increase power trend window from 30s to 60s for better PID noise smoothing
- Tighten trend threshold from 2% to 1% to delay warmup until truly stable
- Fix heating icon opacity staying at 50% after transitioning to stable state
- Always reset window after evaluation (don't stall when latched)
- Fix isTemperatureStable type from int to bool
- Add debug logging for heater power events

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@cla-bot
Copy link

cla-bot bot commented Feb 20, 2026

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant