Skip to content

feat: Bambu AMS tray dashboard on TFT display (#148)#149

Merged
sjordan0228 merged 10 commits into
devfrom
feature/bambu-tft-dashboard
Apr 10, 2026
Merged

feat: Bambu AMS tray dashboard on TFT display (#148)#149
sjordan0228 merged 10 commits into
devfrom
feature/bambu-tft-dashboard

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 10, 2026

Summary

  • Adds a persistent TFT dashboard showing Bambu AMS tray status (color, material, weight) in a colored grid
  • HA blueprint sends cmd/tray_update MQTT command with flat JSON array of tray states
  • Auto-detects grid layout from tray count: 2x2 (1-4 trays), 2x4 (5-8), 4x4 (9-16)
  • NVS caches last tray state for instant display on reboot
  • 5-second revert timer returns to dashboard after tag scans
  • New "Bambu Dashboard" toggle on config page (disabled by default)

Design

  • Scanner is a dumb display — topology-blind, HA owns all Bambu mapping logic
  • DisplayI::showTrayDashboard() with default empty body (LCD gets no-op automatically)
  • TFTDashboard helper class isolates grid rendering from TFTManager
  • TRAY_UPDATE signal message through existing AppMessage queue
  • Contrast text via luminance formula: white on dark backgrounds, black on light

New Files

File Purpose
src/TrayDashboardTypes.h TrayData/TrayDashboardState structs
src/TFTDashboard.h/.cpp Grid renderer (LovyanGFX sprites)

Test plan

  • Enable Bambu Dashboard in config page, verify toggle persists after reboot
  • Send cmd/tray_update via MQTT with 4 trays — verify 2x2 grid renders with correct colors
  • Send update with 16 trays — verify 4x4 grid renders
  • Scan a tag while dashboard active — verify spool details show, then dashboard returns after 5s
  • Reboot scanner — verify cached dashboard appears immediately
  • Verify compile: esp32dev, esp32s3zero

Summary by CodeRabbit

  • New Features

    • Bambu AMS Tray Dashboard: displays tray material, weight, color, and tray count
    • Remote tray updates via MQTT
    • New display rendering for tray grid (adaptive layouts)
  • Behavior

    • Dashboard state persists across restarts
    • Dashboard auto-reverts to prior state after ~5s on tag events
  • Configuration

    • Added option to enable/disable the dashboard in device config/UI

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 10, 2026

📝 Walkthrough

Walkthrough

Adds a Bambu AMS tray dashboard feature: new tray data types, MQTT tray_update handling, TFT rendering, display integration, NVS persistence for cached dashboard state, a dashboard-revert timer on tag events, and a config toggle.

Changes

Cohort / File(s) Summary
Types
src/TrayDashboardTypes.h
New TrayData, TrayDashboardState, and MAX_TRAYS definitions for tray dashboard state.
Config & Build
src/ConfigurationManager.h, src/ConfigurationManager.cpp, src/ConfigHTML.h, src/DeviceConfig.h, src/DeviceConfig.cpp, src/WebServerManager.cpp
Adds bambu_dashboard config field, NVS-backed persistence, web API and HTML checkbox, build-time ENABLE_BAMBU_DASHBOARD default mapping, and accessor isBambuDashboardEnabled().
Application Manager
src/ApplicationManager.h, src/ApplicationManager.cpp
Introduces trayDashboardState_, revert timer (DASHBOARD_REVERT_DELAY_MS), message type TRAY_UPDATE, public APIs updateTrayDashboard() / getTrayDashboardState(), NVS load/save of tray state, and revert-on-scan logic.
MQTT / Home Assistant
src/HomeAssistantManager.cpp
Adds tray_update command parsing JSON array of trays (color hex → RGB), builds TrayDashboardState, calls ApplicationManager::updateTrayDashboard() and enqueues TRAY_UPDATE message; expands MQTT buffers.
Display Interface & TFT
src/DisplayI.h, src/TFTManager.h, src/TFTManager.cpp, src/TFTDashboard.h, src/TFTDashboard.cpp
Adds DisplayI::showTrayDashboard(), TFTManager::TrayDashboard state and showTrayDashboard() enqueue, and TFTDashboard implementation to render grid cells with color/contrast, material, and weight.
Minor API surface
src/ApplicationManager.*, src/DisplayI.h, src/TFTManager.*
New handler handleTrayUpdate() and integration points to immediately show persisted/updated tray dashboard when enabled.

Sequence Diagram(s)

sequenceDiagram
    actor MQTT as MQTT Broker
    participant HAM as HomeAssistantManager
    participant AppMgr as ApplicationManager
    participant TFTMgr as TFTManager
    participant TFT as TFT Display

    MQTT->>HAM: tray_update command (JSON array)
    HAM->>HAM: Parse JSON, extract trays & colors
    HAM->>AppMgr: updateTrayDashboard(state)
    AppMgr->>AppMgr: Persist state to NVS (non-test)
    HAM->>AppMgr: Send TRAY_UPDATE message
    AppMgr->>TFTMgr: showTrayDashboard(state)
    TFTMgr->>TFT: Render via TFTDashboard
Loading
sequenceDiagram
    participant Init as System Init
    participant NVS as NVS Storage
    participant AppMgr as ApplicationManager
    participant LCD as Display
    participant Timer as Revert Timer

    Init->>AppMgr: begin()
    AppMgr->>NVS: Load cached TrayDashboardState
    alt cached data && bambu_dashboard enabled
        AppMgr->>LCD: showTrayDashboard(cached_state)
    else
        AppMgr->>LCD: show "AMS Ready"
    end

    Note over Timer: Tag-scan event
    AppMgr->>Timer: arm dashboardRevertAt (millis)
    loop process messages
        AppMgr->>AppMgr: if revert elapsed -> restore persisted tray state & show
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

Possibly related PRs

Suggested labels

size/XL

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.21% 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
Title check ✅ Passed The title 'feat: Bambu AMS tray dashboard on TFT display' directly and clearly describes the main feature addition—a Bambu AMS tray dashboard for TFT displays—which aligns with the primary objective of the changeset.
Description check ✅ Passed The description covers all required template sections: Summary (feature overview), Changes (list of modifications), How to Test (verification steps), and Checklist (testing requirements). All key aspects of the implementation are documented.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/bambu-tft-dashboard

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.

@github-actions github-actions Bot added the size/L Large change (200-500 lines) label Apr 10, 2026
Copy link
Copy Markdown

@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: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/WebServerManager.cpp (1)

764-783: ⚠️ Potential issue | 🟡 Minor

Clear bambu_dashboard when TFT is disabled.

This handler persists the dashboard flag independently of tft_enabled, so a direct POST /api/config can save a configuration the device cannot render. Keep that invariant server-side instead of relying on the page behavior.

Suggested fix
     update.tft_enabled = doc["tft_enabled"] | (uint8_t)0;
     strncpy(update.tft_driver, doc["tft_driver"] | "st7789", sizeof(update.tft_driver) - 1);
     // TFT and LCD share GPIO 22/23 on WROOM — auto-disable LCD when TFT enabled
     if (update.tft_enabled && update.lcd_enabled) {
         update.lcd_enabled = 0;
     }
@@
-    update.bambu_dashboard = doc["bambu_dashboard"] | 0;
+    update.bambu_dashboard = update.tft_enabled
+        ? static_cast<uint8_t>(doc["bambu_dashboard"] | 0)
+        : static_cast<uint8_t>(0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/WebServerManager.cpp` around lines 764 - 783, The handler currently
writes update.bambu_dashboard regardless of update.tft_enabled; add logic after
parsing update.tft_enabled (and after the existing TFT/LCD mutual-disable block)
to force update.bambu_dashboard = 0 when update.tft_enabled is false so the
device never persists a dashboard flag it cannot render; locate the code that
reads/writes update.tft_enabled and update.bambu_dashboard in
WebServerManager.cpp and insert the conditional clearing there (use the existing
update.* fields: update.tft_enabled and update.bambu_dashboard).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/ApplicationManager.cpp`:
- Around line 1284-1286: Current code calls Preferences::putBytes every
TRAY_UPDATE which causes frequent flash writes; change this by adding a
debounce/compare strategy: keep a cached copy of the last-persisted
TrayDashboardState (e.g., lastSavedTrayDashboardState_) and a timestamp (e.g.,
lastTraySaveTs_), then in the TRAY_UPDATE handler (where trayDashboardState_ is
set) only call prefs.putBytes("tray_dash", &trayDashboardState_,
sizeof(TrayDashboardState)) when the new trayDashboardState_ differs from
lastSavedTrayDashboardState_ or when a configured minimum interval has elapsed
(e.g., save no more often than once every X seconds). Update
lastSavedTrayDashboardState_ and lastTraySaveTs_ when persisting and ensure
Preferences prefs.begin("spoolsense", false) is only called at save time; this
reduces writes by persisting on real changes or at a low cadence.
- Around line 161-175: The dashboard revert timing uses a direct >= comparison
with millis(), which breaks on rollover; replace the direct check "if (now >=
dashboardRevertAt_)" with a wrap-safe elapsed comparison like "if ((int32_t)(now
- dashboardRevertAt_) >= 0)" so the condition is correct across millis()
rollovers, and apply the same change to the other occurrences mentioned (the
checks around dashboardRevertAt_ at the other paths/locations and the
blank/generic tag paths) referencing dashboardRevertAt_, millis(),
display_->showTrayDashboard and trayDashboardState_ for locating the code.
- Around line 1282-1287: The persistence code in
ApplicationManager::handleTrayUpdate unconditionally uses Preferences which is
only available when not building NATIVE_TEST; wrap the entire Preferences usage
(the instantiation, begin/putBytes/end) in an `#ifndef` NATIVE_TEST / `#endif` block
like the read path does so native-test builds don't reference Preferences;
ensure you guard the block that writes trayDashboardState_ (the
prefs.begin("spoolsense", false); prefs.putBytes("tray_dash",
&trayDashboardState_, sizeof(TrayDashboardState)); prefs.end(); sequence) with
the same conditional used at the top of the file.

In `@src/ApplicationManager.h`:
- Around line 34-35: The queue currently enqueues only the TRAY_UPDATE token
while updateTrayDashboard() writes to trayDashboardState_, causing race/ordering
issues; modify AppMessage to include a TrayDashboardState payload (or
alternatively protect trayDashboardState_ with a mutex) and change
updateTrayDashboard() to enqueue an AppMessage containing the snapshot, and
update handleTrayUpdate() to consume that payload instead of reading
trayDashboardState_ directly (refer to TrayDashboardState, AppMessage,
updateTrayDashboard(), trayDashboardState_, TRAY_UPDATE, handleTrayUpdate()).

In `@src/ConfigurationManager.cpp`:
- Around line 252-255: Initialize _bambuDashboard from the DeviceConfig-derived
setting before applying NVS overrides: in the code path where
loadFromDeviceConfig() is used, seed _bambuDashboard with
cfg.peripherals.bambu_dashboard (or
DeviceConfig::cfg.peripherals.bambu_dashboard) prior to the
prefs.isKey(NVS_KEY_BAMBU_DASH) check so that first-boot defaults from
loadFromDeviceConfig() are respected and only overwritten when NVS contains an
explicit value via prefs.getBool(NVS_KEY_BAMBU_DASH, false).

In `@src/HomeAssistantManager.cpp`:
- Around line 931-936: The code currently calls
ApplicationManager::getInstance().updateTrayDashboard(...) then fire-and-forgets
ApplicationManager::getInstance().sendMessage(msg) and immediately publishes
success with publishCommandResponse(...); change this to check the return value
of sendMessage (which indicates enqueue success/backpressure) and only call
publishCommandResponse(command, true, ...) when sendMessage returns true; if
sendMessage fails, publishCommandResponse(command, false, /* optional error
payload */) so the caller sees the queue backpressure instead of a false
success. Reference updateTrayDashboard, sendMessage,
AppMessageType::TRAY_UPDATE, and publishCommandResponse in your change.
- Around line 890-896: The tray_update path is constrained by three buffer sizes
causing 6+ tray payloads to truncate and fail; update mqttCallback() to enlarge
its local payload buffer (replace char buf[384] with a larger size like 2048 to
avoid truncation), increase the MQTT client/PubSubClient buffer capacity to
match that larger payload (adjust the buffer size used when initializing or
configuring the MQTT client), and raise the StaticJsonDocument capacity used in
tray handling (change StaticJsonDocument<768> trayDoc to a larger capacity such
as StaticJsonDocument<2048>) so the deserializer can handle 6–16 tray JSON
payloads end-to-end (ensure all three sizes are consistent).

In `@src/TFTDashboard.cpp`:
- Around line 33-34: The label buffer is too small to hold tray IDs like
"T255"/"T256" plus the null terminator; change the fixed buffer used where label
and snprintf are in TFTDashboard.cpp (the variables label and the snprintf call
that formats with tray.tray_index/TrayData::tray_index) to a larger size (at
least 5 bytes) or use a small std::string/std::array to ensure space for "T###"
plus '\0', and keep using snprintf (or std::to_string + concatenation) so the
formatted tray id cannot be truncated; also optionally validate the tray index
range before formatting.
- Around line 62-101: The render function (TFTDashboard::render) uses hard-coded
240 and fixed center coordinates (120,120) which breaks layouts on non-240x240
displays; update TFTDashboard::render to derive widths/heights from the provided
sprite (use sprite->width() and sprite->height() for totalW/totalH), compute
cellW and cellH using totalW/totalH instead of 240, and center the "No Trays"
text using totalW/2 and totalH/2; ensure any placement logic that computes x/y
for renderCell and renderEmptyCell uses these derived totals so the grid scales
and centers correctly for any panel size.

---

Outside diff comments:
In `@src/WebServerManager.cpp`:
- Around line 764-783: The handler currently writes update.bambu_dashboard
regardless of update.tft_enabled; add logic after parsing update.tft_enabled
(and after the existing TFT/LCD mutual-disable block) to force
update.bambu_dashboard = 0 when update.tft_enabled is false so the device never
persists a dashboard flag it cannot render; locate the code that reads/writes
update.tft_enabled and update.bambu_dashboard in WebServerManager.cpp and insert
the conditional clearing there (use the existing update.* fields:
update.tft_enabled and update.bambu_dashboard).
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a854cf1a-a7ab-4cb4-aef9-1b557de8ed88

📥 Commits

Reviewing files that changed from the base of the PR and between c24488d and 3238111.

📒 Files selected for processing (15)
  • src/ApplicationManager.cpp
  • src/ApplicationManager.h
  • src/ConfigHTML.h
  • src/ConfigurationManager.cpp
  • src/ConfigurationManager.h
  • src/DeviceConfig.cpp
  • src/DeviceConfig.h
  • src/DisplayI.h
  • src/HomeAssistantManager.cpp
  • src/TFTDashboard.cpp
  • src/TFTDashboard.h
  • src/TFTManager.cpp
  • src/TFTManager.h
  • src/TrayDashboardTypes.h
  • src/WebServerManager.cpp

Comment thread src/ApplicationManager.cpp
Comment thread src/ApplicationManager.cpp
Comment on lines +1284 to +1286
Preferences prefs;
prefs.begin("spoolsense", false);
prefs.putBytes("tray_dash", &trayDashboardState_, sizeof(TrayDashboardState));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Debounce the tray-cache writes.

This writes the full dashboard blob on every TRAY_UPDATE. Because the payload includes tray weights, Home Assistant can drive this path many times during a print, turning a reboot cache into frequent flash writes. Persist only when the state actually changes and at a much lower cadence.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 1284-1284: variable 'prefs' is not initialized

(cppcoreguidelines-init-variables)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ApplicationManager.cpp` around lines 1284 - 1286, Current code calls
Preferences::putBytes every TRAY_UPDATE which causes frequent flash writes;
change this by adding a debounce/compare strategy: keep a cached copy of the
last-persisted TrayDashboardState (e.g., lastSavedTrayDashboardState_) and a
timestamp (e.g., lastTraySaveTs_), then in the TRAY_UPDATE handler (where
trayDashboardState_ is set) only call prefs.putBytes("tray_dash",
&trayDashboardState_, sizeof(TrayDashboardState)) when the new
trayDashboardState_ differs from lastSavedTrayDashboardState_ or when a
configured minimum interval has elapsed (e.g., save no more often than once
every X seconds). Update lastSavedTrayDashboardState_ and lastTraySaveTs_ when
persisting and ensure Preferences prefs.begin("spoolsense", false) is only
called at save time; this reduces writes by persisting on real changes or at a
low cadence.

Comment thread src/ApplicationManager.h
Comment on lines +34 to 35
TRAY_UPDATE,
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Carry TrayDashboardState inside AppMessage.

The new flow writes trayDashboardState_ through updateTrayDashboard() and then enqueues only a TRAY_UPDATE token. That breaks the queue’s synchronization boundary: two queued updates can both render the last state written, and readers can observe a partially copied TrayDashboardState while another task is writing it. Put the snapshot on the message payload (or guard this member with a mutex) and let handleTrayUpdate() consume the queued data.

Also applies to: 185-186

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ApplicationManager.h` around lines 34 - 35, The queue currently enqueues
only the TRAY_UPDATE token while updateTrayDashboard() writes to
trayDashboardState_, causing race/ordering issues; modify AppMessage to include
a TrayDashboardState payload (or alternatively protect trayDashboardState_ with
a mutex) and change updateTrayDashboard() to enqueue an AppMessage containing
the snapshot, and update handleTrayUpdate() to consume that payload instead of
reading trayDashboardState_ directly (refer to TrayDashboardState, AppMessage,
updateTrayDashboard(), trayDashboardState_, TRAY_UPDATE, handleTrayUpdate()).

Comment on lines +252 to +255
if (prefs.isKey(NVS_KEY_BAMBU_DASH)) {
_bambuDashboard = prefs.getBool(NVS_KEY_BAMBU_DASH, false);
anyOverride = true;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Seed _bambuDashboard from DeviceConfig before applying NVS overrides.

This override path only runs when "bambu_dash" already exists in NVS, but loadFromDeviceConfig() never initializes _bambuDashboard. On first boot, boards that enable the dashboard via cfg.peripherals.bambu_dashboard will still report it disabled until the setting is written once.

🔧 Suggested fix
 void ConfigurationManager::loadFromDeviceConfig() {
     const DeviceConfig& cfg = getDeviceConfig();
@@
     _lcdEnabled = cfg.peripherals.lcd_enabled;
     _ledEnabled = cfg.peripherals.status_led_enabled;
     _keypadEnabled = cfg.peripherals.keypad_enabled;
+    _bambuDashboard = cfg.peripherals.bambu_dashboard;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/ConfigurationManager.cpp` around lines 252 - 255, Initialize
_bambuDashboard from the DeviceConfig-derived setting before applying NVS
overrides: in the code path where loadFromDeviceConfig() is used, seed
_bambuDashboard with cfg.peripherals.bambu_dashboard (or
DeviceConfig::cfg.peripherals.bambu_dashboard) prior to the
prefs.isKey(NVS_KEY_BAMBU_DASH) check so that first-boot defaults from
loadFromDeviceConfig() are respected and only overwritten when NVS contains an
explicit value via prefs.getBool(NVS_KEY_BAMBU_DASH, false).

Comment thread src/HomeAssistantManager.cpp Outdated
Comment on lines +931 to +936
ApplicationManager::getInstance().updateTrayDashboard(state);
AppMessage msg = {};
msg.type = AppMessageType::TRAY_UPDATE;
ApplicationManager::getInstance().sendMessage(msg);

publishCommandResponse(command, true, nullptr);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Propagate app-queue backpressure before returning success.

src/ApplicationManager.cpp only caches the state in updateTrayDashboard(). The render/persist work happens later when AppMessageType::TRAY_UPDATE is handled, so ignoring sendMessage() failures can silently drop the visible update while still publishing success.

Suggested fix
         ApplicationManager::getInstance().updateTrayDashboard(state);
         AppMessage msg = {};
         msg.type = AppMessageType::TRAY_UPDATE;
-        ApplicationManager::getInstance().sendMessage(msg);
+        if (!ApplicationManager::getInstance().sendMessage(msg, 50)) {
+            publishCommandResponse(command, false, "app_queue_full");
+            return;
+        }
 
         publishCommandResponse(command, true, nullptr);
         return;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HomeAssistantManager.cpp` around lines 931 - 936, The code currently
calls ApplicationManager::getInstance().updateTrayDashboard(...) then
fire-and-forgets ApplicationManager::getInstance().sendMessage(msg) and
immediately publishes success with publishCommandResponse(...); change this to
check the return value of sendMessage (which indicates enqueue
success/backpressure) and only call publishCommandResponse(command, true, ...)
when sendMessage returns true; if sendMessage fails,
publishCommandResponse(command, false, /* optional error payload */) so the
caller sees the queue backpressure instead of a false success. Reference
updateTrayDashboard, sendMessage, AppMessageType::TRAY_UPDATE, and
publishCommandResponse in your change.

Comment thread src/TFTDashboard.cpp Outdated
Comment thread src/TFTDashboard.cpp
Comment on lines +62 to +101
void TFTDashboard::render(LGFX_Sprite* sprite, const TrayDashboardState& state) {
sprite->fillScreen(0x000000);

if (state.tray_count == 0) {
sprite->setTextColor(COLOR_EMPTY_TEXT);
sprite->setTextDatum(MC_DATUM);
sprite->setTextSize(2);
sprite->drawString("No Trays", 120, 120);
sprite->pushSprite(0, 0);
return;
}

// Determine grid dimensions
uint8_t cols, rows;
bool small;
if (state.tray_count <= 4) {
cols = 2; rows = 2; small = false;
} else if (state.tray_count <= 8) {
cols = 2; rows = 4; small = true;
} else {
cols = 4; rows = 4; small = true;
}

int cellW = (240 - (cols + 1) * CELL_GAP) / cols;
int cellH = (240 - (rows + 1) * CELL_GAP) / rows;

for (uint8_t i = 0; i < rows * cols; i++) {
uint8_t col = i % cols;
uint8_t row = i / cols;
int x = CELL_GAP + col * (cellW + CELL_GAP);
int y = CELL_GAP + row * (cellH + CELL_GAP);

if (i < state.tray_count && state.trays[i].populated) {
renderCell(sprite, x, y, cellW, cellH, state.trays[i], small);
} else {
renderEmptyCell(sprite, x, y, cellW, cellH, small);
}
}

sprite->pushSprite(0, 0);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Derive the layout from the sprite size instead of hard-coding 240.

This renderer centers "No Trays" at (120, 120) and computes every cell from 240, so it will be clipped or off-center on any supported panel that is not 240x240. Using sprite->width() / sprite->height() here keeps the dashboard aligned with whatever driver TFTManager instantiated.

♻️ Suggested refactor
 void TFTDashboard::render(LGFX_Sprite* sprite, const TrayDashboardState& state) {
-    sprite->fillScreen(0x000000);
+    sprite->fillScreen(0x000000);
+    const int screenW = sprite->width();
+    const int screenH = sprite->height();

     if (state.tray_count == 0) {
         sprite->setTextColor(COLOR_EMPTY_TEXT);
         sprite->setTextDatum(MC_DATUM);
         sprite->setTextSize(2);
-        sprite->drawString("No Trays", 120, 120);
+        sprite->drawString("No Trays", screenW / 2, screenH / 2);
         sprite->pushSprite(0, 0);
         return;
     }
@@
-    int cellW = (240 - (cols + 1) * CELL_GAP) / cols;
-    int cellH = (240 - (rows + 1) * CELL_GAP) / rows;
+    int cellW = (screenW - (cols + 1) * CELL_GAP) / cols;
+    int cellH = (screenH - (rows + 1) * CELL_GAP) / rows;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void TFTDashboard::render(LGFX_Sprite* sprite, const TrayDashboardState& state) {
sprite->fillScreen(0x000000);
if (state.tray_count == 0) {
sprite->setTextColor(COLOR_EMPTY_TEXT);
sprite->setTextDatum(MC_DATUM);
sprite->setTextSize(2);
sprite->drawString("No Trays", 120, 120);
sprite->pushSprite(0, 0);
return;
}
// Determine grid dimensions
uint8_t cols, rows;
bool small;
if (state.tray_count <= 4) {
cols = 2; rows = 2; small = false;
} else if (state.tray_count <= 8) {
cols = 2; rows = 4; small = true;
} else {
cols = 4; rows = 4; small = true;
}
int cellW = (240 - (cols + 1) * CELL_GAP) / cols;
int cellH = (240 - (rows + 1) * CELL_GAP) / rows;
for (uint8_t i = 0; i < rows * cols; i++) {
uint8_t col = i % cols;
uint8_t row = i / cols;
int x = CELL_GAP + col * (cellW + CELL_GAP);
int y = CELL_GAP + row * (cellH + CELL_GAP);
if (i < state.tray_count && state.trays[i].populated) {
renderCell(sprite, x, y, cellW, cellH, state.trays[i], small);
} else {
renderEmptyCell(sprite, x, y, cellW, cellH, small);
}
}
sprite->pushSprite(0, 0);
void TFTDashboard::render(LGFX_Sprite* sprite, const TrayDashboardState& state) {
sprite->fillScreen(0x000000);
const int screenW = sprite->width();
const int screenH = sprite->height();
if (state.tray_count == 0) {
sprite->setTextColor(COLOR_EMPTY_TEXT);
sprite->setTextDatum(MC_DATUM);
sprite->setTextSize(2);
sprite->drawString("No Trays", screenW / 2, screenH / 2);
sprite->pushSprite(0, 0);
return;
}
// Determine grid dimensions
uint8_t cols, rows;
bool small;
if (state.tray_count <= 4) {
cols = 2; rows = 2; small = false;
} else if (state.tray_count <= 8) {
cols = 2; rows = 4; small = true;
} else {
cols = 4; rows = 4; small = true;
}
int cellW = (screenW - (cols + 1) * CELL_GAP) / cols;
int cellH = (screenH - (rows + 1) * CELL_GAP) / rows;
for (uint8_t i = 0; i < rows * cols; i++) {
uint8_t col = i % cols;
uint8_t row = i / cols;
int x = CELL_GAP + col * (cellW + CELL_GAP);
int y = CELL_GAP + row * (cellH + CELL_GAP);
if (i < state.tray_count && state.trays[i].populated) {
renderCell(sprite, x, y, cellW, cellH, state.trays[i], small);
} else {
renderEmptyCell(sprite, x, y, cellW, cellH, small);
}
}
sprite->pushSprite(0, 0);
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 62-62: method 'render' can be made static

(readability-convert-member-functions-to-static)


[warning] 75-75: multiple declarations in a single statement reduces readability

(readability-isolate-declaration)


[warning] 75-75: variable 'cols' is not initialized

(cppcoreguidelines-init-variables)


[warning] 75-75: variable 'rows' is not initialized

(cppcoreguidelines-init-variables)


[warning] 76-76: variable 'small' is not initialized

(cppcoreguidelines-init-variables)


[warning] 91-91: variable name 'x' is too short, expected at least 3 characters

(readability-identifier-length)


[warning] 92-92: variable name 'y' is too short, expected at least 3 characters

(readability-identifier-length)


[warning] 94-94: if with identical then and else branches

(bugprone-branch-clone)


[note] 96-96: else branch starts here

(clang)

🪛 Cppcheck (2.20.0)

[style] 62-62: The function 'render' is never used.

(unusedFunction)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/TFTDashboard.cpp` around lines 62 - 101, The render function
(TFTDashboard::render) uses hard-coded 240 and fixed center coordinates
(120,120) which breaks layouts on non-240x240 displays; update
TFTDashboard::render to derive widths/heights from the provided sprite (use
sprite->width() and sprite->height() for totalW/totalH), compute cellW and cellH
using totalW/totalH instead of 240, and center the "No Trays" text using
totalW/2 and totalH/2; ensure any placement logic that computes x/y for
renderCell and renderEmptyCell uses these derived totals so the grid scales and
centers correctly for any panel size.

- Guard handleTrayUpdate NVS writes with #ifndef NATIVE_TEST
- Make dashboard revert timer millis() wrap-safe (store start, check elapsed)
- Increase MQTT client buffer to 2048 and callback buffer to 1536 for 16-tray payloads
- Increase StaticJsonDocument to 2048 for full tray array parsing
- Widen tray label buffer from 4 to 6 chars for T10+ indices
@sjordan0228 sjordan0228 merged commit 1ed406a into dev Apr 10, 2026
1 of 2 checks passed
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/HomeAssistantManager.cpp`:
- Around line 919-925: The color parsing code accepts non-hex strings (e.g.,
"GGGGGG") because it only checks length then calls strtoul; fix by validating
the 6-character string contains only [0-9a-fA-F] (e.g., using strspn or a small
loop) or by using strtoul with an endptr and verifying endptr points at the
string terminator before using the result; if validation fails, set a safe
default (e.g., 0x000000) or skip assigning to tray.color to avoid silently
treating invalid input as black. Ensure you update the logic around colorHex /
strtoul / tray.color to perform this validation and fallback.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 37e3f9f2-ccca-49a2-b427-5e030bc3ed91

📥 Commits

Reviewing files that changed from the base of the PR and between 3238111 and 3e14d9d.

📒 Files selected for processing (3)
  • src/ApplicationManager.cpp
  • src/HomeAssistantManager.cpp
  • src/TFTDashboard.cpp

Comment on lines +919 to +925
const char* colorHex = obj["color"] | "333333";
if (strlen(colorHex) == 6) {
uint32_t c = strtoul(colorHex, nullptr, 16);
tray.color[0] = (c >> 16) & 0xFF;
tray.color[1] = (c >> 8) & 0xFF;
tray.color[2] = c & 0xFF;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Color hex parsing silently accepts invalid input.

If colorHex contains non-hex characters (e.g., "GGGGGG"), strtoul will return 0 or a partial parse without error indication. Consider validating the hex string or documenting that invalid colors default to black.

🛡️ Optional validation
             const char* colorHex = obj["color"] | "333333";
-            if (strlen(colorHex) == 6) {
+            if (strlen(colorHex) == 6 &&
+                strspn(colorHex, "0123456789ABCDEFabcdef") == 6) {
                 uint32_t c = strtoul(colorHex, nullptr, 16);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/HomeAssistantManager.cpp` around lines 919 - 925, The color parsing code
accepts non-hex strings (e.g., "GGGGGG") because it only checks length then
calls strtoul; fix by validating the 6-character string contains only
[0-9a-fA-F] (e.g., using strspn or a small loop) or by using strtoul with an
endptr and verifying endptr points at the string terminator before using the
result; if validation fails, set a safe default (e.g., 0x000000) or skip
assigning to tray.color to avoid silently treating invalid input as black.
Ensure you update the logic around colorHex / strtoul / tray.color to perform
this validation and fallback.

@sjordan0228 sjordan0228 deleted the feature/bambu-tft-dashboard branch April 21, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/L Large change (200-500 lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant