Move to Pioarduino#759
Conversation
…-framework) (#681) * port(ble): NanoPbComm transport + ble_ota_dfu to esp-nimble-cpp 2.x API Port the BLE transport layer from NimBLE-Arduino 1.4.0 to the esp-nimble-cpp 2.x API surface, retargeting the esp-nimble-cpp migration onto master's NanoPbComm framed-protocol comms (which replaced NimBLEComm in #726). Client transport: - setPower(ESP_PWR_LVL_P9) -> setPower(9): 2.x takes int8_t dBm; the enum (=11 on ESP32-S3) quantizes to +12 dBm, 9 preserves +9 dBm. - NimBLEAdvertisedDeviceCallbacks -> NimBLEScanCallbacks base; const onResult. - setAdvertisedDeviceCallbacks(cb,wantDuplicates) -> setScanCallbacks(cb,false) (we own the object; wantDuplicates covered by setDuplicateFilter(false)). - scan start(0,nullptr,false) -> start(0,false,false); onDisconnect(+int reason). Server transport: - setPower(9); setScanResponse -> enableScanResponse. - onConnect/onDisconnect/onWrite/onSubscribe gain NimBLEConnInfo& (and int reason on disconnect); ble_gap_conn_desc* -> NimBLEConnInfo&. ble_ota_dfu already carries the 2.x onWrite(NimBLEConnInfo&) signature. Co-Authored-By: Claude Code <noreply@anthropic.com> * build(ble)!: re-migrate to pioarduino + esp-nimble-cpp on NanoPbComm Re-apply the esp-nimble-cpp / pioarduino build migration on top of master, which had diverged (replaced NimBLEComm with NanoPbComm, stayed on espressif32@6.12.0 + NimBLE-Arduino 1.4.0). Master never adopted pioarduino, so this is a re-migration, not a rebase. Platform / build: - platform -> pioarduino 55.03.31-2 (Arduino 3.3.1 / IDF 5.5.1); 55.03.32+ ships a libbtdm_app.a that corrupts TLSF metadata on ESP32-S3 v0.2. - framework = arduino, espidf (dual-framework); IDF drives the build via sdkconfig.common + per-class (gaggimate/controller) defaults. - src/idf_component.yml registers h2zero/esp-nimble-cpp (~2.5.0); drop h2zero/NimBLE-Arduino from all lib_deps + library.json. - scripts/pioarduino_env.py injects IDF bt/NimBLE + managed-component include paths into the Arduino C++ pass (LDF can't see IDF REQUIRES propagation). - C++17 -> C++20; root + src CMakeLists; partitions/*.csv. - Keep master's nanopb codegen (custom_nanopb_protos) + NanoPbComm + the native_autotune test env. Bump AsyncTCP 3.4.10 / ESPAsyncWebServer 3.10.3 / HomeSpan 2.1.7 / GFX 1.6.5; add esp-memoryMonitor; scales -> esp-nimble-cpp fork. BREAKING: remove display-headless-4m env + esp32-s3-supermini board (last pure-Arduino target). Migrate to display-headless (seeed_xiao_esp32s3, 8 MB). Source compat (Core 3.x / GCC 14 / IDF 5.5): - xTaskHandle -> TaskHandle_t; sntp_* -> esp_sntp_*; OTA setCACertBundle length arg; HomeSpan 2.x PImpl in HomekitPlugin; GFX 1.6.5 driver API; lambda [=]->[this] captures; LOG_TAG const char*. Observability + fixes folded in: esp-memoryMonitor heap monitor (MemoryMonitor + WS status fields), homekit WiFi-watchdog boot-loop fix + nano-newlib disable, OTA CA-bundle reattach, ble-scale 2 s reconnect throttle, deferred Settings NVS load. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(build): resolve esp-nimble-cpp dual-framework build errors Compile fixes found building -e controller and -e display green under pioarduino 55.03.31-2 (Python 3.13 penv via uv): - NanoPbComm/library.json: drop residual h2zero/NimBLE-Arduino dep — it dragged NimBLE-Arduino into the controller build alongside esp-nimble-cpp (duplicate NimBLE + nimble_npl_os.h esp_timer_handle_t error). - DigitalInput.h: xTaskHandle -> TaskHandle_t (residual; master's debounce rewrite reintroduced it post branch-merge; removed in Core 3.x). - BleClientTransport: clearDuplicateCache() -> clearResults(); esp-nimble-cpp 2.x NimBLEScan has no clearDuplicateCache(). - sdkconfig.gaggimate.defaults: CONFIG_COMPILER_CXX_EXCEPTIONS=y — the esp-arduino-ble-scales RemoteScales dtor uses try/catch; IDF defaults to -fno-exceptions (arduino-esp32 built with them on). Display-class only; controller stays -fno-exceptions (no scales link). Builds: controller 12.5% flash / 11.3% RAM; display 84.6% flash / 19.2% RAM. Co-Authored-By: Claude Code <noreply@anthropic.com> * refactor(build): dedup platformio env config + tidy BLE callback params /simplify cleanup (behavior-preserving; controller 12.5% / display 84.6% flash unchanged): - platformio.ini: hoist the duplicated custom_component_remove + lib_ignore + custom_nanopb_protos/options into a shared [idf_common] anchor (mirrors the existing [display_common] pattern); controller references it and appends its one extra strip (espressif/libsodium). Drop redundant per-env 'framework = arduino, espidf' (inherited from [env]) and the byte-identical cmake_extra_args in display-headless-8m (inherited from display-headless). - NanoPbComm BLE transports: drop (void) casts on unused NimBLE callback params in favour of unnamed parameters, matching the adjacent ble_ota_dfu style. Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(ota,ble): code-review findings — controller OTA CA bundle + scan dup-filter - GitHubOTA::update(): attach the CA bundle before the controller .bin HTTPS download. The per-call-site CA-bundle refactor (replacing the one-time ctor attach) covered update_firmware/update_filesystem but missed the controller path, so controller OTA could hit GitHub with no trusted roots → TLS handshake failure (masked only when a prior call left the bundle attached to the client). - BleClientTransport::scan(): pass wantDuplicates=true to setScanCallbacks (2.x arg is wantDuplicates, NOT deleteCallbacks as the comment claimed). false set the duplicate filter ON, the opposite of the 1.x intent; it was saved only by the later setDuplicateFilter(false). Now both agree and the comment is correct. Both envs build green (controller 12.5% / display 84.6% flash). Co-Authored-By: Claude Code <noreply@anthropic.com> * fix: address PR review comments (CodeRabbit) - platformio.ini: pin esp-arduino-ble-scales fork to commit SHA 1daafb4 instead of the mutable feat/esp-nimble-cpp branch ref, for reproducible builds (temp, pending upstream gaggimate/esp-arduino-ble-scales#28). - platformio.ini: drop redundant -std=c++20 in display_common + controller build_flags (gnu++20 already wins; GCC uses the last -std). - MemoryMonitor.cpp: g_ready -> std::atomic<bool> (written by init() on the setup task, read from WebUI/OTA tasks — was a data race on a plain bool). - HomekitPlugin.cpp: actionRequired -> std::atomic<bool> (set in the HomeSpan autoPoll task callback, read+cleared in the Arduino loop task). - ble_ota_dfu.cpp: call pServiceOTA->start() in configure_OTA() — a no-op under esp-nimble-cpp 2.x (services register via NimBLEServer::start()) but kept for symmetry with the comms service and to document intent. Both envs build green (controller 12.5% / display 84.6% flash). Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(ble-scale): drop runtime scan gates; rely on sdkconfig PSRAM routing Addresses maintainer review on PR #681. - Remove the AP-mode scan gate (autoScanAllowed() returned false in WiFi AP mode) — it blocked BLE scale use during setup/AP, a regression vs master. - Remove the kMinInternalForScan=40000 runtime heap-floor defer in scan(). Both gates (ff9ec5e) were added on the old pure-Arduino build with no PSRAM routing. The dual-framework sdkconfig now routes the NimBLE host + WiFi/LWIP to PSRAM and reserves 16 KB internal for the DMA-bound BT controller, so the internal-heap pressure is handled at the config layer, not a runtime bandaid. - Remove the now-pointless controller:wifi:connect handler and unwrap the bluetooth:connect / mode:change handlers to master's unconditional scan(). - Kept: the 2 s reconnect throttle and scale-metadata polling (unrelated). - Reword the setPower(9) comments: +9 dBm preserves master's setPower(ESP_PWR_LVL_P9) (1.4.x was +9 dBm); the +12 note is the avoided trap of passing the enum value to the 2.x dBm API, not a behavior change. Both envs build green (controller 12.5% / display 84.6% flash). Co-Authored-By: Claude Code <noreply@anthropic.com> * refactor: drop orphaned Controller::isApMode() getter Its only caller was BLEScalePlugin::autoScanAllowed(), removed in the previous commit. The isApConnection member stays (used internally). Restores parity with master, which has no such getter. /code-review cleanup; no behavior change. * fix(review): address CodeRabbit findings - TemperatureSensor.h: add virtual destructor to the abstract interface (deleting a concrete sensor via TemperatureSensor* was UB); mirrors Pump.h. - platformio.ini: drop unused -DPARTITION_CSV_PATH from env:display cmake_extra_args. No CMakeLists reads it and the headless envs already omit it; board_build.partitions is the authoritative partition source. HomeSpan-ownership comment and ble_ota_dfu service start were already present; the -std=c++20 redundancy was already removed in the dedup refactor. Co-Authored-By: [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Jochen Niebuhr <kontakt@ju-hh.de>
…ne Kconfig restore (supersedes #743) (#740) * fix(sdkconfig): enable FATFS long filenames for SD reads IDF 5.5's Kconfig default is FATFS_LFN_NONE (8.3 names only). The dual-framework pioarduino build compiles IDF from source and inherits that default, where the old pure-Arduino build inherited arduino-esp32's prebuilt sdkconfig with LFN enabled. Under LFN_NONE the SD card cannot open or list 4-char extensions (.slog/.json), so /api/history/<id>.slog GETs miss serveStatic and fall through to the SPA catch-all serving index.html — the web UI then fails with "Bad magic: ... got 0x6f64213c" (the bytes "<!do"). index.bin (8.3-compliant) was unaffected, which is why the shot list loaded but individual shots did not. Restore LFN via HEAP allocation (keeps the work buffer off the async webserver task stack) plus UTF-8 / codepage 850 to match arduino-esp32. Add scripts/assert_sdkconfig.py post-build guard, wired into the display and controller envs, to fail the build if FATFS_LFN_NONE ever regresses — the dual-framework build silently drifts to IDF defaults otherwise. Refs #681 Co-Authored-By: Claude Code <noreply@anthropic.com> * perf(sdkconfig): restore 240 MHz, -Os, and PSRAM malloc threshold The pioarduino dual-framework build compiles ESP-IDF from source and inherits IDF Kconfig defaults for anything not pinned in our .defaults, which differs from the prebuilt sdkconfig the old pure-Arduino build used. Three such drifts caused the maintainer's HTTP regressions vs the arduino "Nightly 77" baseline: - CPU ran at 160 MHz, not 240. board.json's f_cpu does not set the IDF Kconfig in a dual-framework build, so the unpinned default leaked through — a ~33% clock cut behind the uniform ~34% HTTP throughput loss across all endpoints. Pin CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240. (240 MHz is the ESP32-S3 rated max and the board's declared clock.) - Compiler built at -Og (IDF debug default) instead of -Os. Pin CONFIG_COMPILER_OPTIMIZATION_SIZE to match arduino-esp32. - CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL was 512 (arduino baseline 4096), shattering internal DRAM into tiny blocks — startup fragmentation 15% -> 64%, starving the contiguous internal allocs AsyncTCP needs. Restore 4096. Extend scripts/assert_sdkconfig.py to assert CPU 240 MHz and -Os so these can't silently regress to IDF defaults again. Also correct the header comment in sdkconfig.common.defaults: the merged sdkconfig.<env> cache is gitignored and `-t fullclean` does NOT regenerate it — you must delete it to re-merge .defaults edits. Refs #681 Co-Authored-By: Claude Code <noreply@anthropic.com> * fix(sdkconfig): restore CONFIG_LCD_RGB_RESTART_IN_VSYNC dropped on the dual-framework move Arduino's precompiled framework-arduinoespressif32-libs ship CONFIG_LCD_RGB_RESTART_IN_VSYNC=y in their sdkconfig, so arduino-only builds were getting per-VBlank GDMA restart for free. The dual-framework sdkconfig defaults didn't carry this flag, and the IDF Kconfig default is n — so any single PSRAM-bus contention event (WiFi/BT alloc, TLS handshake, big WS payload) could desync LCD_CAM from the panel's HSYNC and permanently shift the RGB-panel image until reboot. Same regression shape as CONFIG_FATFS_LFN_STACK. Symptom HW-observed on a Pro running the integration branch after ~16h uptime: image suddenly shifted right ~30-50 px, dial bars clipped on the left, pressure reading clipped on the right. Power-cycle restored. No reboot, no errors in serial log, heap healthy. ESP-IDF docs explicitly recommend this flag to prevent the "permanently shifted image" failure mode; the alternative is the bounce-buffer mode, which would also need a per-driver code change in the panel configs. The Kconfig is panel-width-independent so this covers both the LilyGo T-RGB and Waveshare RGB-panel display boards. * fix(sdkconfig): restore the rest of the Arduino-baseline Kconfig defaults Continuing the cleanup started by the LCD_RGB_RESTART_IN_VSYNC commit. All of these are flags the precompiled framework-arduinoespressif32-libs sets implicitly; the dual-framework move silently inherits the IDF Kconfig defaults, which are more conservative / off for several user-visible behaviors. - CPU freq dropped 240 -> 160 MHz (~33% slower across CPU-bound paths; confirmed on the daily-driver boot log: `cpu freq: 160000000 Hz`). - Task WDT no longer panics; offending tasks soft-wedge instead of clean-rebooting. - Stack-overflow detection (compiler + FreeRTOS HW watchpoint) disabled, so overflows surface as random data corruption. - Heap canary bytes disabled (HEAP_POISONING_LIGHT), so small overflows / use-after-free silently propagate. - esp_timer + FreeRTOS sw-timer task stacks shrunk to the Kconfig minimum. - lwIP TCPIP task: stack reduced 4096 -> 3072, max sockets 16 -> 10, and affinity floats across cores instead of pinning to CPU0. - BT_NIMBLE_NVS_PERSIST off: every paired BLE peer re-pairs on each boot. Excluded intentionally: - APP_ROLLBACK_ENABLE — needs an app-side esp_ota_mark_app_valid_cancel_rollback() call, or every OTA boot rolls back. Separate change. - ARDUHAL_ESP_LOG — flipping changes log routing, entangled with our remote-syslog plugin. Needs design pass. - ESPTOOLPY_FLASHMODE_QIO — per-board concern via board.json / board_build.flash_mode, not appropriate to force in sdkconfig. Cross-reference for impact and IDF docs: see INTEGRATION-KCONFIG-AUDIT.html. * refactor(sdkconfig): dedup extra_scripts into idf_common The `post:scripts/assert_sdkconfig.py` guard wiring (added alongside the pre: scripts) was duplicated verbatim in [env:display] and [env:controller]. Hoist the whole extra_scripts block into the shared [idf_common] namespace and reference it via ${idf_common.extra_scripts} from both envs — matching the existing ${idf_common.*} pattern. Single source of truth: any future script addition now propagates to every dual-framework env (incl. the display-headless variants that inherit it via `extends`) instead of being special-cased per env. Resolved `pio project config` is byte-identical across display, display-headless, display-headless-8m, and controller — behavior-preserving. Also normalize the guard's success print to an f-string (was the lone %-format among f-strings in the file). Refs #681 Co-Authored-By: Claude Code <noreply@anthropic.com> --------- Co-authored-by: Claude Code <noreply@anthropic.com> Co-authored-by: Dave <dave@centercirclesolutions.com>
📝 WalkthroughWalkthroughThis PR migrates the firmware toward combined Arduino/ESP-IDF builds, updates NimBLE integrations, standardizes FreeRTOS task handles, adds memory and heap diagnostics, adjusts OTA trust-store handling, and modifies display drivers, plugins, and UI behavior. ChangesFirmware build and runtime migration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (10)
lib/ble_ota_dfu/src/ble_ota_dfu.cpp (1)
201-209:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDon't pack filesystem sizes through
uint16_t.
FLASH.totalBytes()andFLASH.usedBytes()are emitted as three-byte values, but both locals areuint16_t, so anything above 65535 is truncated before encoding. On normal ESP32 SPIFFS sizes, the client receives bogus capacity numbers.Suggested fix
- uint16_t total_size = FLASH.totalBytes(); - uint16_t used_size = FLASH.usedBytes(); + uint32_t total_size = FLASH.totalBytes(); + uint32_t used_size = FLASH.usedBytes();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/ble_ota_dfu/src/ble_ota_dfu.cpp` around lines 201 - 209, The code truncates FLASH.totalBytes() and FLASH.usedBytes() by storing them in uint16_t (total_size, used_size) before packing into flash_size; change those locals to uint32_t (e.g., uint32_t total_size = FLASH.totalBytes(); uint32_t used_size = FLASH.usedBytes();) and keep the packing into flash_size using the same byte shifts and static_cast<uint8_t> so the three high/med/low bytes are encoded correctly (refer to total_size, used_size, FLASH.totalBytes(), FLASH.usedBytes(), and the flash_size array).lib/OTA/src/ControllerOTA.cpp (3)
141-163:⚠️ Potential issue | 🟠 Major | ⚡ Quick winMake
fillBuffer()report short-read failure to its callers.After 300 timeouts,
fillBuffer()returns early, but the callers still assumelenbytes were produced.downloadFile()then writesbufferSizebytes anyway, andsendPart()transmits whatever remains in the stack buffer. That can silently corrupt the staged firmware image.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/OTA/src/ControllerOTA.cpp` around lines 141 - 163, The function fillBuffer currently returns early on timeout but provides no signal to callers (fillBuffer) leading downloadFile and sendPart to assume the full len/ bufferSize was filled and proceed; change fillBuffer(const Stream &in, uint8_t *buffer, uint16_t len) to return a status (e.g., bool success or size_t bytesRead) and ensure it returns false or the actual bytesRead when timeout_failures triggers (using bufferLen); update callers downloadFile and sendPart to check the return value (or bytes read) and abort/handle the short-read (do not write/transmit more than the returned bytes, and surface an error) so partial reads cannot corrupt the staged firmware image, referencing function names fillBuffer, downloadFile, sendPart and the variables len, bufferLen, bufferSize, timeout_failures.
27-32:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAbort the OTA flow when the download or staged file open fails.
update()logs the failure, but it still opens/board-firmware.binand callsrunUpdate(file, file.size()). That converts a transient HTTP/SPIFFS error into an invalid BLE transfer instead of a clean abort.Suggested fix
void ControllerOTA::update(WiFiClientSecure &wifi_client, const String &release_url) { if (SPIFFS.exists("/board-firmware.bin")) { ESP_LOGI("ControllerOTA", "Removing previous update file"); SPIFFS.remove("/board-firmware.bin"); } if (!downloadFile(wifi_client, release_url)) { ESP_LOGE("ControllerOTA", "Download of firmware file failed"); + return; } File file = SPIFFS.open("/board-firmware.bin", FILE_READ); + if (!file) { + ESP_LOGE("ControllerOTA", "Failed to open staged firmware"); + return; + } runUpdate(file, file.size()); file.close(); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/OTA/src/ControllerOTA.cpp` around lines 27 - 32, Abort the OTA flow when the download or staged file open fails: if downloadFile(wifi_client, release_url) returns false, log the error and immediately return/abort instead of continuing; after attempting File file = SPIFFS.open("/board-firmware.bin", FILE_READ), check that the File is valid (truthy) and file.size() > 0, log an error and return/abort if it isn’t, and only call runUpdate(file, file.size()) when both checks pass. Ensure the changes touch the downloadFile check, the SPIFFS.open/File validity check, and the runUpdate call so no further OTA steps run on failure.
209-210:⚠️ Potential issue | 🟠 Major | ⚡ Quick winAdd minimum-length guards on the updated NimBLE callback payloads in
lib/OTA/src/ControllerOTA.cppandlib/ble_ota_dfu/src/ble_ota_dfu.cpp.ControllerOTA::onReceiveandBLEOverTheAirDeviceFirmwareUpdate::onWriteboth assume the callback buffer is non-empty, and the DFU command handler further assumes fixed-width payloads for0xFC,0xFE, and0xFF. Empty or truncated BLE packets can therefore read past the buffer and destabilize the OTA flow in both files.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/OTA/src/ControllerOTA.cpp` around lines 209 - 210, ControllerOTA::onReceive and BLEOverTheAirDeviceFirmwareUpdate::onWrite currently assume pData has enough bytes and read past the buffer; add explicit length guards: in ControllerOTA::onReceive check length >= 1 before reading pData[0] (return/ignore if not), and in BLEOverTheAirDeviceFirmwareUpdate::onWrite inspect the first byte (command) only after verifying length >= 1 and then validate length against the expected fixed payload size for commands 0xFC, 0xFE, and 0xFF (e.g., require N bytes for each command) before reading additional indexes; on any undersize packet, safely bail out (log/ignore) instead of dereferencing out-of-bounds data.lib/NanoPbComm/src/ble/BleServerTransport.cpp (1)
46-51:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDelay the usable-connection state until the TX path is actually ready.
On Line 57 and Line 60, the transport becomes “connected” as soon as the GATT link opens, but controller→display traffic still depends on
_txChar->notify(). BetweenonConnect()and the later subscribe event,isConnected()reports usable andsend()returnstrueunconditionally on Line 51 even if the notification was not handed off, so early outbound frames can be lost silently. Gate the connected state on the notify-ready condition and return the actual notify result instead of always succeeding.Also applies to: 56-78
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/NanoPbComm/src/ble/BleServerTransport.cpp` around lines 46 - 51, The transport currently marks connection usable as soon as onConnect() sets _connected, but send(const uint8_t *data, size_t length) returns true even if _txChar->notify() isn't ready; change the logic so the usable/connected state is gated by TX-notify readiness (e.g., track a _txReady flag set when the subscribe/notify-ready callback runs) and update onConnect()/subscribe handler to only set _connected (or set _txReady and then _connected) when notifications are actually available; in BleServerTransport::send, check both _connected/_txChar and the new _txReady, call _txChar->setValue(...); return the actual result of _txChar->notify() (or its boolean) instead of always returning true so callers see real failure when notify can't hand off the packet.lib/OTA/src/GitHubOTA.cpp (1)
41-74:⚠️ Potential issue | 🟠 Major | ⚡ Quick winReattach the CA bundle before the update-check requests too.
After removing the constructor-level
attach_ca_bundle(_wifi_client), Line 44 and Line 63 still send_wifi_clientthrough the HTTPS lookup helpers with no trust store attached. That leavescheckForUpdates()as a broken path even thoughupdate(),update_firmware(), andupdate_filesystem()were fixed. Attach the CA bundle before the first helper here, or move that step into the shared HTTP helper layer so every GitHub HTTPS call is covered.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/OTA/src/GitHubOTA.cpp` around lines 41 - 74, checkForUpdates() calls get_updated_base_url_via_redirect(_wifi_client, _release_url) and get_updated_version_via_txt_file(_wifi_client, _latest_url) without re-attaching the CA bundle; before the first network call in GitHubOTA::checkForUpdates, call attach_ca_bundle(_wifi_client) (or move attach_ca_bundle into the shared HTTPS helpers) so _wifi_client has the trust store attached for both get_updated_base_url_via_redirect and get_updated_version_via_txt_file; ensure this is done before any early returns so both code paths use the attached CA bundle.src/display/core/Controller.cpp (1)
410-415:⚠️ Potential issue | 🟠 MajorRemove the redundant SNTP re-initialization after
configTzTime(src/display/core/Controller.cpp ~410-415)
configTzTime(resolve_timezone(...), NTP_SERVER)already stops/reconfigures SNTP, sets the NTP server name(s), callssntp_init(), and appliesTZviasetenv+tzset. The subsequentesp_sntp_setservername(0, NTP_SERVER)+esp_sntp_init()re-initializes the same SNTP client a second time, making the final configuration order-dependent and increasing risk of inconsistent behavior. Keep either the ArduinoconfigTzTime(...)flow (and remove theesp_sntp_*calls) or switch fully to an explicitesp_sntp_*sequence (and don’t callconfigTzTime). (esp32-hal-time.c configTzTime, ESP-IDF system time SNTP API)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/display/core/Controller.cpp` around lines 410 - 415, The code calls configTzTime(resolve_timezone(settings.getTimezone()), NTP_SERVER) which already sets TZ (via setenv+tzset), configures SNTP servers and calls sntp_init, so remove the redundant esp_sntp_setservername(0, NTP_SERVER) and esp_sntp_init() calls to avoid re-initializing SNTP and order-dependent behavior; keep the configTzTime + setenv + tzset flow (or if you prefer manual control remove configTzTime and replace it with an explicit esp_sntp_setservername/esp_sntp_init sequence), and ensure only one SNTP initialization path (referencing configTzTime, esp_sntp_setservername, esp_sntp_init, setenv, tzset).src/display/core/Settings.cpp (1)
119-129:⚠️ Potential issue | 🟠 Major | ⚡ Quick winHandle
xTaskCreatefailure or settings stop persisting after boot.
load()now owns creation of the async save task, but thexTaskCreate(...)result is ignored. On a low-memory boot path,taskHandlestays null and every latersave()call only setsdirty = true; nothing ever flushes it becauseController::setup()only callssettings.load()once. That turns all subsequent setting changes into silent data loss on the next reboot.💡 Suggested fix
- xTaskCreate(loopTask, "Settings::loop", configMINIMAL_STACK_SIZE * 6, this, 1, &taskHandle); + if (xTaskCreate(loopTask, "Settings::loop", configMINIMAL_STACK_SIZE * 6, this, 1, &taskHandle) != pdPASS) { + taskHandle = nullptr; + ESP_LOGE("Settings", "Failed to start Settings::loop; falling back to synchronous saves"); + } } void Settings::save(bool noDelay) { - if (noDelay) { + if (noDelay || taskHandle == nullptr) { doSave(); return; } dirty = true; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/display/core/Settings.cpp` around lines 119 - 129, The async save task creation result from xTaskCreate in Settings::load is ignored causing taskHandle to remain null on low-memory boots and later save() calls only flip dirty without flushing; update Settings::load to check xTaskCreate's return (compare to pdPASS) and if creation fails set taskHandle = nullptr and immediately fall back to a synchronous persist (call doSave()) or mark a retry strategy; additionally update Settings::save to detect a null/invalid taskHandle and perform doSave() (or a safe immediate write) instead of only setting dirty so settings still persist even if the async task couldn't be created; reference xTaskCreate, Settings::load, Settings::save, taskHandle, doSave, dirty and Controller::setup when making the change.src/display/ui/default/DefaultUI.h (1)
55-57:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInitialize the task handles before exposing them to the health check.
isTaskHealthy()callseTaskGetState()on both members unconditionally, but these handles are still left indeterminate untilinit()creates the tasks. If the health path runs earlier, or task creation fails, this becomes an invalid FreeRTOS handle read on a monitoring code path.Suggested fix
- bool isTaskHealthy() const { - return is_task_healthy(eTaskGetState(taskHandle)) && is_task_healthy(eTaskGetState(profileTaskHandle)); - } + bool isTaskHealthy() const { + return taskHandle != nullptr && profileTaskHandle != nullptr && + is_task_healthy(eTaskGetState(taskHandle)) && + is_task_healthy(eTaskGetState(profileTaskHandle)); + } ... - TaskHandle_t taskHandle; + TaskHandle_t taskHandle = nullptr; ... - TaskHandle_t profileTaskHandle; + TaskHandle_t profileTaskHandle = nullptr;Also applies to: 146-148
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/display/ui/default/DefaultUI.h` around lines 55 - 57, isTaskHealthy() currently calls eTaskGetState() on taskHandle and profileTaskHandle unconditionally, which can read indeterminate FreeRTOS handles before init() creates them; update isTaskHealthy() (and the similar checks around the 146-148 block) to first verify each handle is initialized/valid (e.g., non-null/valid FreeRTOS handle) before calling eTaskGetState, returning false or treating uninitialized handles as unhealthy if not yet created; reference the taskHandle and profileTaskHandle members and the init() path so you only call eTaskGetState(taskHandle) or eTaskGetState(profileTaskHandle) when those handles are known to be initialized.src/display/drivers/AmoledDisplayDriver.cpp (1)
68-90:⚠️ Potential issue | 🟠 Major | ⚡ Quick winRestore
lcd_enon failed probes.
isCompatible()tries multiple hardware configs back-to-back, buttestHw()now driveshwConfig.lcd_enHIGH before probing and never undoes that on afalsereturn. A failed first probe can therefore change the electrical state seen by the later probes and bias auto-detection.Suggested cleanup path
bool AmoledDisplayDriver::testHw(AmoledHwConfig hwConfig) { + auto cleanupProbeState = [&]() { + Wire.end(); + if (hwConfig.lcd_en != -1) { + digitalWrite(hwConfig.lcd_en, LOW); + } + }; + // Cold-boot peripherals need a moment; also recover from any prior // Wire.end()/begin() that left hal-i2c-ng in ESP_ERR_INVALID_STATE. Wire.end(); @@ - if (!Wire.begin(hwConfig.i2c_sda, hwConfig.i2c_scl)) { - return false; - } + if (!Wire.begin(hwConfig.i2c_sda, hwConfig.i2c_scl)) { + cleanupProbeState(); + return false; + } @@ - Wire.end(); - return pcf8563Found && touchFound; + const bool compatible = pcf8563Found && touchFound; + if (!compatible) { + cleanupProbeState(); + } else { + Wire.end(); + } + return compatible; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/display/drivers/AmoledDisplayDriver.cpp` around lines 68 - 90, testHw() currently drives hwConfig.lcd_en HIGH before probing and returns false on failure without restoring that pin, which can bias subsequent isCompatible() probes; fix by saving whether you asserted the pin (hwConfig.lcd_en != -1), and on any early return path (probe failure) ensure you undo the assertion (digitalWrite(hwConfig.lcd_en, LOW) or restore the prior state) and call Wire.end() before returning; update testHw() so both success and failure paths clean up the lcd_en pin and I2C (use the same detectI2CDevice/PCF8563_DEVICE_ADDRESS, CST92XX_DEVICE_ADDRESS, FT3168_DEVICE_ADDRESS symbols to find the probe logic).
🧹 Nitpick comments (3)
CMakeLists.txt (1)
1-3: Clarify theIDF_PATHrequirement for non-PlatformIO CMake runs
CMakeLists.txtincludesinclude($ENV{IDF_PATH}/tools/cmake/project.cmake), soIDF_PATHmust be set when CMake is invoked. With PlatformIO (framework = arduino, espidf), PlatformIO setsIDF_PATHfor the ESP-IDF CMake configure step, so PlatformIO builds shouldn’t require users to export it manually. The repo docs don’t mentionIDF_PATH, so add a brief note for any workflow that runs ESP-IDF CMake outside PlatformIO.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CMakeLists.txt` around lines 1 - 3, Add a brief note to the repo docs explaining that CMake requires the IDF_PATH environment variable when invoking the ESP-IDF CMake include (the include($ENV{IDF_PATH}/tools/cmake/project.cmake) line in CMakeLists.txt), and that while PlatformIO sets IDF_PATH automatically for framework = arduino, espidf builds, users running CMake/ESP-IDF directly must export IDF_PATH (or source the ESP-IDF export script) before running cmake; mention the exact env var name IDF_PATH and a short example command to set it.sdkconfig.controller.defaults (1)
5-11: ⚡ Quick winExplicitly disable the unused NimBLE roles for the controller build.
Lines 5-11 reduce connections/activity slots, but the controller still inherits central/observer/broadcaster from
sdkconfig.common.defaults. If this target is really peripheral-only, leaving those roles enabled keeps unused NimBLE host code and memory in the controller image.♻️ Proposed fix
# 1 connection: single display consumer. CONFIG_BT_NIMBLE_MAX_CONNECTIONS=1 + +# Controller is peripheral-only; drop the inherited display-side roles. +# CONFIG_BT_NIMBLE_ROLE_CENTRAL is not set +# CONFIG_BT_NIMBLE_ROLE_OBSERVER is not set +# CONFIG_BT_NIMBLE_ROLE_BROADCASTER is not set # 2 BLE link-layer activity slots: 1 peripheral connection + 1 advertising set. # Common base leaves the default (6), sized for the display's central + peripheral # workload. Narrowing it here reclaims ~1 KB static BSS. CONFIG_BT_CTRL_BLE_MAX_ACT=2🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@sdkconfig.controller.defaults` around lines 5 - 11, The controller currently reduces connections/activity but still inherits central/observer/broadcaster roles; explicitly disable those unused NimBLE roles by adding the corresponding CONFIG flags (e.g., disable CONFIG_BT_NIMBLE_ROLE_CENTRAL, CONFIG_BT_NIMBLE_ROLE_OBSERVER and CONFIG_BT_NIMBLE_ROLE_BROADCASTER or the controller-equivalent config names) in the defaults so the build omits central/observer/broadcaster code and reclaims memory; update the sdkconfig defaults near CONFIG_BT_NIMBLE_MAX_CONNECTIONS and CONFIG_BT_CTRL_BLE_MAX_ACT to set those role flags to disabled/0/“n”.src/CMakeLists.txt (1)
3-11: ⚡ Quick winConsider replacing
GLOB_RECURSEwith explicit source lists.
GLOB_RECURSEis discouraged by CMake best practices because it does not automatically trigger reconfiguration when source files are added or removed, which can lead to stale builds.Alternative approach
Explicitly list source files or use a pattern that triggers reconfiguration:
set(app_sources ${CMAKE_SOURCE_DIR}/src/controller/main.cpp ${CMAKE_SOURCE_DIR}/src/controller/other.cpp # ... list all files )Alternatively, document the limitation and ensure developers run a clean reconfigure when adding files.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/CMakeLists.txt` around lines 3 - 11, Replace the FILE(GLOB_RECURSE ...) usage for app_sources with an explicit source list to avoid stale builds: remove the two FILE(GLOB_RECURSE app_sources ...) blocks and instead define app_sources via set(app_sources ...) listing each source file under src/controller/ or src/display (refer to the app_sources variable and the FILE(GLOB_RECURSE) invocations to locate the change), or if you cannot enumerate files, add a clear comment documenting the GLOB_RECURSE limitation and require developers to run a clean reconfigure when adding/removing files so builds stay correct.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/display/plugins/HomekitPlugin.cpp`:
- Around line 158-167: The loop() currently reads and later clears the
cross-task flag impl->actionRequired via a load-then-store sequence which can
drop a new request; change the code to atomically consume the flag using
impl->actionRequired.exchange(false) (or the equivalent
std::atomic<bool>::exchange) at the start of HomekitPlugin::loop() and only
proceed when the exchanged value was true, then continue using impl->controller
and impl->accessory as before (i.e., call deactivateStandby/activateStandby and
setTargetTemp) so any action arriving during processing is preserved.
In `@src/display/plugins/WebUIPlugin.cpp`:
- Around line 93-102: When apMode is true the code skips calling
ota->checkForUpdates() but also skips updating the UI status, leaving the UI
stuck in "Checking..."; modify the apMode branch so that even when skipping
network checks you explicitly resolve the OTA UI by calling
pluginManager->trigger("ota:update:status", "value", false) (or otherwise
indicate no update available) and call updateOTAStatus(ota->getCurrentVersion())
so handleOTASettings() is not left waiting; keep the existing behavior for the
non-AP branch that uses ota->checkForUpdates(), ota->isUpdateAvailable(), and
updateOTAStatus().
---
Outside diff comments:
In `@lib/ble_ota_dfu/src/ble_ota_dfu.cpp`:
- Around line 201-209: The code truncates FLASH.totalBytes() and
FLASH.usedBytes() by storing them in uint16_t (total_size, used_size) before
packing into flash_size; change those locals to uint32_t (e.g., uint32_t
total_size = FLASH.totalBytes(); uint32_t used_size = FLASH.usedBytes();) and
keep the packing into flash_size using the same byte shifts and
static_cast<uint8_t> so the three high/med/low bytes are encoded correctly
(refer to total_size, used_size, FLASH.totalBytes(), FLASH.usedBytes(), and the
flash_size array).
In `@lib/NanoPbComm/src/ble/BleServerTransport.cpp`:
- Around line 46-51: The transport currently marks connection usable as soon as
onConnect() sets _connected, but send(const uint8_t *data, size_t length)
returns true even if _txChar->notify() isn't ready; change the logic so the
usable/connected state is gated by TX-notify readiness (e.g., track a _txReady
flag set when the subscribe/notify-ready callback runs) and update
onConnect()/subscribe handler to only set _connected (or set _txReady and then
_connected) when notifications are actually available; in
BleServerTransport::send, check both _connected/_txChar and the new _txReady,
call _txChar->setValue(...); return the actual result of _txChar->notify() (or
its boolean) instead of always returning true so callers see real failure when
notify can't hand off the packet.
In `@lib/OTA/src/ControllerOTA.cpp`:
- Around line 141-163: The function fillBuffer currently returns early on
timeout but provides no signal to callers (fillBuffer) leading downloadFile and
sendPart to assume the full len/ bufferSize was filled and proceed; change
fillBuffer(const Stream &in, uint8_t *buffer, uint16_t len) to return a status
(e.g., bool success or size_t bytesRead) and ensure it returns false or the
actual bytesRead when timeout_failures triggers (using bufferLen); update
callers downloadFile and sendPart to check the return value (or bytes read) and
abort/handle the short-read (do not write/transmit more than the returned bytes,
and surface an error) so partial reads cannot corrupt the staged firmware image,
referencing function names fillBuffer, downloadFile, sendPart and the variables
len, bufferLen, bufferSize, timeout_failures.
- Around line 27-32: Abort the OTA flow when the download or staged file open
fails: if downloadFile(wifi_client, release_url) returns false, log the error
and immediately return/abort instead of continuing; after attempting File file =
SPIFFS.open("/board-firmware.bin", FILE_READ), check that the File is valid
(truthy) and file.size() > 0, log an error and return/abort if it isn’t, and
only call runUpdate(file, file.size()) when both checks pass. Ensure the changes
touch the downloadFile check, the SPIFFS.open/File validity check, and the
runUpdate call so no further OTA steps run on failure.
- Around line 209-210: ControllerOTA::onReceive and
BLEOverTheAirDeviceFirmwareUpdate::onWrite currently assume pData has enough
bytes and read past the buffer; add explicit length guards: in
ControllerOTA::onReceive check length >= 1 before reading pData[0]
(return/ignore if not), and in BLEOverTheAirDeviceFirmwareUpdate::onWrite
inspect the first byte (command) only after verifying length >= 1 and then
validate length against the expected fixed payload size for commands 0xFC, 0xFE,
and 0xFF (e.g., require N bytes for each command) before reading additional
indexes; on any undersize packet, safely bail out (log/ignore) instead of
dereferencing out-of-bounds data.
In `@lib/OTA/src/GitHubOTA.cpp`:
- Around line 41-74: checkForUpdates() calls
get_updated_base_url_via_redirect(_wifi_client, _release_url) and
get_updated_version_via_txt_file(_wifi_client, _latest_url) without re-attaching
the CA bundle; before the first network call in GitHubOTA::checkForUpdates, call
attach_ca_bundle(_wifi_client) (or move attach_ca_bundle into the shared HTTPS
helpers) so _wifi_client has the trust store attached for both
get_updated_base_url_via_redirect and get_updated_version_via_txt_file; ensure
this is done before any early returns so both code paths use the attached CA
bundle.
In `@src/display/core/Controller.cpp`:
- Around line 410-415: The code calls
configTzTime(resolve_timezone(settings.getTimezone()), NTP_SERVER) which already
sets TZ (via setenv+tzset), configures SNTP servers and calls sntp_init, so
remove the redundant esp_sntp_setservername(0, NTP_SERVER) and esp_sntp_init()
calls to avoid re-initializing SNTP and order-dependent behavior; keep the
configTzTime + setenv + tzset flow (or if you prefer manual control remove
configTzTime and replace it with an explicit
esp_sntp_setservername/esp_sntp_init sequence), and ensure only one SNTP
initialization path (referencing configTzTime, esp_sntp_setservername,
esp_sntp_init, setenv, tzset).
In `@src/display/core/Settings.cpp`:
- Around line 119-129: The async save task creation result from xTaskCreate in
Settings::load is ignored causing taskHandle to remain null on low-memory boots
and later save() calls only flip dirty without flushing; update Settings::load
to check xTaskCreate's return (compare to pdPASS) and if creation fails set
taskHandle = nullptr and immediately fall back to a synchronous persist (call
doSave()) or mark a retry strategy; additionally update Settings::save to detect
a null/invalid taskHandle and perform doSave() (or a safe immediate write)
instead of only setting dirty so settings still persist even if the async task
couldn't be created; reference xTaskCreate, Settings::load, Settings::save,
taskHandle, doSave, dirty and Controller::setup when making the change.
In `@src/display/drivers/AmoledDisplayDriver.cpp`:
- Around line 68-90: testHw() currently drives hwConfig.lcd_en HIGH before
probing and returns false on failure without restoring that pin, which can bias
subsequent isCompatible() probes; fix by saving whether you asserted the pin
(hwConfig.lcd_en != -1), and on any early return path (probe failure) ensure you
undo the assertion (digitalWrite(hwConfig.lcd_en, LOW) or restore the prior
state) and call Wire.end() before returning; update testHw() so both success and
failure paths clean up the lcd_en pin and I2C (use the same
detectI2CDevice/PCF8563_DEVICE_ADDRESS, CST92XX_DEVICE_ADDRESS,
FT3168_DEVICE_ADDRESS symbols to find the probe logic).
In `@src/display/ui/default/DefaultUI.h`:
- Around line 55-57: isTaskHealthy() currently calls eTaskGetState() on
taskHandle and profileTaskHandle unconditionally, which can read indeterminate
FreeRTOS handles before init() creates them; update isTaskHealthy() (and the
similar checks around the 146-148 block) to first verify each handle is
initialized/valid (e.g., non-null/valid FreeRTOS handle) before calling
eTaskGetState, returning false or treating uninitialized handles as unhealthy if
not yet created; reference the taskHandle and profileTaskHandle members and the
init() path so you only call eTaskGetState(taskHandle) or
eTaskGetState(profileTaskHandle) when those handles are known to be initialized.
---
Nitpick comments:
In `@CMakeLists.txt`:
- Around line 1-3: Add a brief note to the repo docs explaining that CMake
requires the IDF_PATH environment variable when invoking the ESP-IDF CMake
include (the include($ENV{IDF_PATH}/tools/cmake/project.cmake) line in
CMakeLists.txt), and that while PlatformIO sets IDF_PATH automatically for
framework = arduino, espidf builds, users running CMake/ESP-IDF directly must
export IDF_PATH (or source the ESP-IDF export script) before running cmake;
mention the exact env var name IDF_PATH and a short example command to set it.
In `@sdkconfig.controller.defaults`:
- Around line 5-11: The controller currently reduces connections/activity but
still inherits central/observer/broadcaster roles; explicitly disable those
unused NimBLE roles by adding the corresponding CONFIG flags (e.g., disable
CONFIG_BT_NIMBLE_ROLE_CENTRAL, CONFIG_BT_NIMBLE_ROLE_OBSERVER and
CONFIG_BT_NIMBLE_ROLE_BROADCASTER or the controller-equivalent config names) in
the defaults so the build omits central/observer/broadcaster code and reclaims
memory; update the sdkconfig defaults near CONFIG_BT_NIMBLE_MAX_CONNECTIONS and
CONFIG_BT_CTRL_BLE_MAX_ACT to set those role flags to disabled/0/“n”.
In `@src/CMakeLists.txt`:
- Around line 3-11: Replace the FILE(GLOB_RECURSE ...) usage for app_sources
with an explicit source list to avoid stale builds: remove the two
FILE(GLOB_RECURSE app_sources ...) blocks and instead define app_sources via
set(app_sources ...) listing each source file under src/controller/ or
src/display (refer to the app_sources variable and the FILE(GLOB_RECURSE)
invocations to locate the change), or if you cannot enumerate files, add a clear
comment documenting the GLOB_RECURSE limitation and require developers to run a
clean reconfigure when adding/removing files so builds stay correct.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5bb3f5b4-691c-4c8a-b26f-125dd8ffd111
⛔ Files ignored due to path filters (2)
partitions/default_16mb.csvis excluded by!**/*.csvpartitions/default_8mb.csvis excluded by!**/*.csv
📒 Files selected for processing (61)
.gitignoreCMakeLists.txtboards/esp32-s3-supermini.jsonlib/GaggiMateController/library.jsonlib/GaggiMateController/src/peripherals/DigitalInput.hlib/GaggiMateController/src/peripherals/DimmedPump.hlib/GaggiMateController/src/peripherals/DistanceSensor.hlib/GaggiMateController/src/peripherals/Heater.hlib/GaggiMateController/src/peripherals/Max31855Thermocouple.hlib/GaggiMateController/src/peripherals/PressureSensor.hlib/GaggiMateController/src/peripherals/Pump.hlib/GaggiMateController/src/peripherals/SimplePump.hlib/GaggiMateController/src/peripherals/TemperatureSensor.hlib/NanoPbComm/library.jsonlib/NanoPbComm/src/ble/BleClientTransport.cpplib/NanoPbComm/src/ble/BleClientTransport.hlib/NanoPbComm/src/ble/BleServerTransport.cpplib/NanoPbComm/src/ble/BleServerTransport.hlib/OTA/library.propertieslib/OTA/src/ControllerOTA.cpplib/OTA/src/GitHubOTA.cpplib/OTA/src/GitHubOTA.hlib/OTA/src/common.cpplib/OTA/src/common.hlib/ble_ota_dfu/library.propertieslib/ble_ota_dfu/src/ble_ota_dfu.cpplib/ble_ota_dfu/src/ble_ota_dfu.hppplatformio.iniscripts/assert_sdkconfig.pyscripts/pioarduino_env.pysdkconfig.common.defaultssdkconfig.controller.defaultssdkconfig.gaggimate.defaultssrc/CMakeLists.txtsrc/display/core/Controller.cppsrc/display/core/Controller.hsrc/display/core/MemoryMonitor.cppsrc/display/core/MemoryMonitor.hsrc/display/core/Settings.cppsrc/display/core/Settings.hsrc/display/core/utils.cppsrc/display/core/utils.hsrc/display/drivers/AmoledDisplay/Amoled_DisplayPanel.cppsrc/display/drivers/AmoledDisplay/CO5300.cppsrc/display/drivers/AmoledDisplay/CO5300.hsrc/display/drivers/AmoledDisplayDriver.cppsrc/display/drivers/Driver.hsrc/display/drivers/LilyGo-T-RGB/LilyGo_RGBPanel.cppsrc/display/drivers/Waveshare/WavesharePanel.cppsrc/display/plugins/AutoWakeupPlugin.cppsrc/display/plugins/BLEScalePlugin.cppsrc/display/plugins/BLEScalePlugin.hsrc/display/plugins/HomekitPlugin.cppsrc/display/plugins/HomekitPlugin.hsrc/display/plugins/MQTTPlugin.cppsrc/display/plugins/ShotHistoryPlugin.hsrc/display/plugins/WebUIPlugin.cppsrc/display/plugins/WebUIPlugin.hsrc/display/ui/default/DefaultUI.cppsrc/display/ui/default/DefaultUI.hsrc/idf_component.yml
💤 Files with no reviewable changes (2)
- boards/esp32-s3-supermini.json
- lib/GaggiMateController/library.json
| void HomekitPlugin::loop() { | ||
| if (!actionRequired || controller == nullptr || accessory == nullptr) | ||
| if (!impl->actionRequired || impl->controller == nullptr || impl->accessory == nullptr) | ||
| return; | ||
| if (accessory->getState() && controller->getMode() == MODE_STANDBY) { | ||
| controller->deactivateStandby(); | ||
| } else if (!accessory->getState() && controller->getMode() != MODE_STANDBY) { | ||
| controller->activateStandby(); | ||
| if (impl->accessory->getState() && impl->controller->getMode() == MODE_STANDBY) { | ||
| impl->controller->deactivateStandby(); | ||
| } else if (!impl->accessory->getState() && impl->controller->getMode() != MODE_STANDBY) { | ||
| impl->controller->activateStandby(); | ||
| } | ||
| controller->setTargetTemp(accessory->getTargetTemperature()); | ||
| actionRequired = false; | ||
| impl->controller->setTargetTemp(impl->accessory->getTargetTemperature()); | ||
| impl->actionRequired = false; |
There was a problem hiding this comment.
Consume actionRequired with exchange(false).
This is a cross-task flag, but the current load-then-store sequence can drop a new HomeKit action that arrives while loop() is processing. If the callback sets actionRequired = true between Line 159 and Line 167, Line 167 clears the newer request and the latest HomeKit change is never applied.
Suggested atomic handoff
void HomekitPlugin::loop() {
- if (!impl->actionRequired || impl->controller == nullptr || impl->accessory == nullptr)
+ if (impl->controller == nullptr || impl->accessory == nullptr)
return;
+ if (!impl->actionRequired.exchange(false))
+ return;
if (impl->accessory->getState() && impl->controller->getMode() == MODE_STANDBY) {
impl->controller->deactivateStandby();
} else if (!impl->accessory->getState() && impl->controller->getMode() != MODE_STANDBY) {
impl->controller->activateStandby();
}
impl->controller->setTargetTemp(impl->accessory->getTargetTemperature());
- impl->actionRequired = false;
}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/display/plugins/HomekitPlugin.cpp` around lines 158 - 167, The loop()
currently reads and later clears the cross-task flag impl->actionRequired via a
load-then-store sequence which can drop a new request; change the code to
atomically consume the flag using impl->actionRequired.exchange(false) (or the
equivalent std::atomic<bool>::exchange) at the start of HomekitPlugin::loop()
and only proceed when the exchanged value was true, then continue using
impl->controller and impl->accessory as before (i.e., call
deactivateStandby/activateStandby and setTargetTemp) so any action arriving
during processing is preserved.
| if ((lastUpdateCheck == 0 || now > lastUpdateCheck + UPDATE_CHECK_INTERVAL)) { | ||
| ota->checkForUpdates(); | ||
| pluginManager->trigger("ota:update:status", "value", ota->isUpdateAvailable()); | ||
| // Skip OTA check in AP mode: no route to GitHub, and mbedtls handshake | ||
| // allocates ~30 KB internal heap that the pioarduino 55.x prebuilt | ||
| // can't spare — spike triggers BLE_INIT malloc failures. | ||
| if (!apMode) { | ||
| ota->checkForUpdates(); | ||
| pluginManager->trigger("ota:update:status", "value", ota->isUpdateAvailable()); | ||
| updateOTAStatus(ota->getCurrentVersion()); | ||
| } | ||
| lastUpdateCheck = now; |
There was a problem hiding this comment.
Resolve OTA status explicitly in AP mode.
Skipping the network check here is fine, but this branch now suppresses the follow-up status update too. handleOTASettings() still puts the UI into a "Checking..." state, so in AP mode a manual check never resolves until the network mode changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/display/plugins/WebUIPlugin.cpp` around lines 93 - 102, When apMode is
true the code skips calling ota->checkForUpdates() but also skips updating the
UI status, leaving the UI stuck in "Checking..."; modify the apMode branch so
that even when skipping network checks you explicitly resolve the OTA UI by
calling pluginManager->trigger("ota:update:status", "value", false) (or
otherwise indicate no update available) and call
updateOTAStatus(ota->getCurrentVersion()) so handleOTASettings() is not left
waiting; keep the existing behavior for the non-AP branch that uses
ota->checkForUpdates(), ota->isUpdateAvailable(), and updateOTAStatus().
The config burst (PID/pressure/pump coeffs) sent on a reconnect can be lost in the unstable BLE window right after the link comes up, and a spurious ACK then stops the reliable layer from retrying -- leaving the controller on firmware- default gains (Kff 0.000) so the boiler never heats after a controller reboot. Re-send the config burst for ~8s after each (re)connect at a 1s cadence; a copy lands once the link settles, and coalescing keeps it to one queued payload per type. HW-validated on this branch: controller-reset config delivery 30/30 pass (vs ~78% fail baseline), the re-send delivering config on the cycles that would otherwise fail. Claude-Session: https://claude.ai/code/session_01MUmX6ZL8t269p38f24GSqp Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
|
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. |
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/display/core/Controller.cpp (1)
340-344: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winExtract duplicated config-sending sequence into a helper method.
The config burst (
setPressureScale→ parse PID →sendPidSettings→setPumpModelCoeffs) is duplicated verbatim inonSystemInfo(lines 340-344) andloop(lines 455-459). If a new config parameter is added, one site can easily be missed. Extract asendControllerConfig()method and call it from both locations.♻️ Proposed refactor
// In Controller.h (private section): +void sendControllerConfig(); // In Controller.cpp: +void Controller::sendControllerConfig() { + setPressureScale(); + float pid[4]; + parseFloatCsv(settings.getPid(), pid, 4, 0.0f); + comms.sendPidSettings(pid[0], pid[1], pid[2], pid[3]); + setPumpModelCoeffs(); +} void Controller::onSystemInfo(const char *hardware, const char *version, uint32_t protocolVersion, bool dimming, bool pressure, bool ledControl, bool tof) { // ... } else { - setPressureScale(); - float pid[4]; - parseFloatCsv(settings.getPid(), pid, 4, 0.0f); - comms.sendPidSettings(pid[0], pid[1], pid[2], pid[3]); - setPumpModelCoeffs(); + sendControllerConfig(); configResendUntil = millis() + CONFIG_RESEND_WINDOW_MS; lastConfigResend = millis(); } // In loop(): - if (comms.isConnected() && now < configResendUntil && (now - lastConfigResend) >= CONFIG_RESEND_INTERVAL_MS) { - setPressureScale(); - float pid[4]; - parseFloatCsv(settings.getPid(), pid, 4, 0.0f); - comms.sendPidSettings(pid[0], pid[1], pid[2], pid[3]); - setPumpModelCoeffs(); + if (comms.isConnected() && now < configResendUntil && (now - lastConfigResend) >= CONFIG_RESEND_INTERVAL_MS) { + sendControllerConfig(); lastConfigResend = now; }Also applies to: 455-459
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/display/core/Controller.cpp` around lines 340 - 344, Extract the duplicated configuration sequence from onSystemInfo and loop into a sendControllerConfig() helper method. Move setPressureScale, PID parsing, comms.sendPidSettings, and setPumpModelCoeffs into the helper, then replace both call sites with sendControllerConfig() while preserving their existing behavior and order.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@src/display/core/Controller.cpp`:
- Around line 340-344: Extract the duplicated configuration sequence from
onSystemInfo and loop into a sendControllerConfig() helper method. Move
setPressureScale, PID parsing, comms.sendPidSettings, and setPumpModelCoeffs
into the helper, then replace both call sites with sendControllerConfig() while
preserving their existing behavior and order.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 18b34811-7b26-467f-9214-1b05fef96ba6
📒 Files selected for processing (2)
src/display/core/Controller.cppsrc/display/core/Controller.h
🚧 Files skipped from review as they are similar to previous changes (1)
- src/display/core/Controller.h



Summary by CodeRabbit
/api/debug/heapendpoint for live heap/PSRAM stats.