Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions src/ApplicationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <Preferences.h>
extern LEDManager ledManager;
#else
#include "platform/NativePlatform.h"
Expand Down Expand Up @@ -60,6 +61,30 @@ bool ApplicationManager::begin(DisplayI* display) {
}

Serial.println("ApplicationManager: Message queue created");

#ifndef NATIVE_TEST
// Load cached tray dashboard from NVS
bool dashEnabled = ConfigurationManager::getInstance().isBambuDashboardEnabled();
if (dashEnabled) {
Preferences prefs;
prefs.begin("spoolsense", true);
size_t len = prefs.getBytesLength("tray_dash");
if (len == sizeof(TrayDashboardState)) {
prefs.getBytes("tray_dash", &trayDashboardState_, sizeof(TrayDashboardState));
if (trayDashboardState_.has_data && display_) {
display_->showTrayDashboard(trayDashboardState_);
Serial.printf("ApplicationManager: Loaded cached tray dashboard, %d trays\n",
trayDashboardState_.tray_count);
} else if (display_) {
display_->showText("SpoolSense", "AMS Ready");
}
} else if (display_) {
display_->showText("SpoolSense", "AMS Ready");
}
prefs.end();
}
#endif

return true;
}

Expand Down Expand Up @@ -133,6 +158,22 @@ void ApplicationManager::processMessages() {
}
}

// Bambu dashboard revert: after scan interruption, return to dashboard
if (dashboardRevertAt_ != 0) {
uint32_t elapsedMs = static_cast<uint32_t>(millis() - dashboardRevertAt_);
if (elapsedMs >= DASHBOARD_REVERT_DELAY_MS) {
dashboardRevertAt_ = 0;
#ifndef NATIVE_TEST
bool dashEnabled = ConfigurationManager::getInstance().isBambuDashboardEnabled();
#else
bool dashEnabled = false;
#endif
if (dashEnabled && trayDashboardState_.has_data && display_) {
display_->showTrayDashboard(trayDashboardState_);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

}

// ── Display ─────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -254,6 +295,10 @@ void ApplicationManager::handleMessage(const AppMessage& msg) {
case AppMessageType::KEYPAD_CANCEL:
handleKeypadCancel();
break;

case AppMessageType::TRAY_UPDATE:
handleTrayUpdate();
break;
}
}

Expand Down Expand Up @@ -404,6 +449,12 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) {
else if (strcmp(s.tag_format, "OpenSpool") == 0) spool.tagType = 6;
else spool.tagType = 0;
display_->showSpool(spool);

#ifndef NATIVE_TEST
if (ConfigurationManager::getInstance().isBambuDashboardEnabled() && trayDashboardState_.has_data) {
dashboardRevertAt_ = millis();
}
#endif
} else if (display_) {
Serial.printf("ApplicationManager: Skipping LCD update for already displayed spool %s\n", msg.payload.spoolDetected.spool_id);
}
Expand Down Expand Up @@ -640,6 +691,12 @@ void ApplicationManager::handleBlankTagDetected(const AppMessage& msg) {
lastDisplayedSpoolId[0] = '\0'; // Clear smart tag display — allow re-display if tag swapped

display_->showText4("**** Spool ****", "*** Scanned ***", "Unknown Tag", "Use app to setup");

#ifndef NATIVE_TEST
if (ConfigurationManager::getInstance().isBambuDashboardEnabled() && trayDashboardState_.has_data) {
dashboardRevertAt_ = millis();
}
#endif
}

// HA MQTT: publish blank tag detected
Expand Down Expand Up @@ -680,6 +737,12 @@ void ApplicationManager::handleGenericTagDetected(const AppMessage& msg) {
lastDisplayedSpoolId[0] = '\0'; // Clear smart tag display — allow re-display if tag swapped

display_->showText4("**** Spool ****", "*** Scanned ***", "Generic Tag", "Checking Spoolman");

#ifndef NATIVE_TEST
if (ConfigurationManager::getInstance().isBambuDashboardEnabled() && trayDashboardState_.has_data) {
dashboardRevertAt_ = millis();
}
#endif
}

// HA MQTT: publish generic tag (UID only, awaiting Spoolman lookup)
Expand Down Expand Up @@ -1206,6 +1269,38 @@ void ApplicationManager::handleKeypadCancel() {
}
}

// ── Tray Dashboard ──────────────────────────────────────────────────────────

void ApplicationManager::updateTrayDashboard(const TrayDashboardState& state) {
trayDashboardState_ = state;
}

const TrayDashboardState& ApplicationManager::getTrayDashboardState() const {
return trayDashboardState_;
}

void ApplicationManager::handleTrayUpdate() {
#ifndef NATIVE_TEST
// Persist to NVS
Preferences prefs;
prefs.begin("spoolsense", false);
prefs.putBytes("tray_dash", &trayDashboardState_, sizeof(TrayDashboardState));
Comment on lines +1285 to +1287
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.

prefs.end();
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#endif

Serial.printf("ApplicationManager: Tray dashboard updated, %d trays\n",
trayDashboardState_.tray_count);

#ifndef NATIVE_TEST
bool dashEnabled = ConfigurationManager::getInstance().isBambuDashboardEnabled();
#else
bool dashEnabled = false;
#endif
if (dashEnabled && display_) {
display_->showTrayDashboard(trayDashboardState_);
}
}

bool ApplicationManager::sendAssignSpool(const char* toolNumber) {
// Send ASSIGN_SPOOL TOOL=Tn gcode to Moonraker (Klipper-AFC integration)
#ifndef NATIVE_TEST
Expand Down
10 changes: 10 additions & 0 deletions src/ApplicationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <cstdint>
#include "IPrinterStrategy.h" // for MAX_TOOLS constant
#include "TrayDashboardTypes.h"

#ifdef NATIVE_TEST
#include "platform/NativePlatform.h"
Expand Down Expand Up @@ -30,6 +31,7 @@ enum class AppMessageType {
KEYPAD_DIGIT,
KEYPAD_CONFIRM,
KEYPAD_CANCEL,
TRAY_UPDATE,
};
Comment on lines +34 to 35
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()).


enum class AutomationMode : uint8_t {
Expand Down Expand Up @@ -180,6 +182,8 @@ class ApplicationManager {
AutomationMode getAutomationMode() const { return automationMode; }
void setAutomationMode(AutomationMode mode) { automationMode = mode; }
SmartTagEnrichment getSmartTagEnrichment() const { return smartTagEnrichment_; }
void updateTrayDashboard(const TrayDashboardState& state);
const TrayDashboardState& getTrayDashboardState() const;
#ifdef NATIVE_TEST
void resetForTest() {
if (messageQueue) { vQueueDelete(messageQueue); messageQueue = nullptr; }
Expand Down Expand Up @@ -247,6 +251,11 @@ class ApplicationManager {
// Enrichment data from Spoolman UID lookup for the current smart tag
SmartTagEnrichment smartTagEnrichment_;

// Bambu AMS tray dashboard state
TrayDashboardState trayDashboardState_ = {};
uint32_t dashboardRevertAt_ = 0;
static constexpr uint32_t DASHBOARD_REVERT_DELAY_MS = 5000;

// Handlers
void handlePrintStarted(const AppMessage& msg);
void handlePrintEnded(const AppMessage& msg);
Expand All @@ -262,6 +271,7 @@ class ApplicationManager {
void handleKeypadDigit(const AppMessage& msg);
void handleKeypadConfirm();
void handleKeypadCancel();
void handleTrayUpdate();
bool sendAssignSpool(const char* toolNumber);
void finishPrint(float gramsUsed, bool canceled);
void enqueueSpoolmanSync(const SpoolDetectedPayload& spool);
Expand Down
14 changes: 13 additions & 1 deletion src/ConfigHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,16 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
<option value="gc9a01">GC9A01 (round)</option>
</select>
</div>
</div>
<h3>Bambu</h3>
<div class="field-group">
<span class="toggle-label">AMS Tray Dashboard (TFT only)</span>
<label class="toggle-switch">
<input type="checkbox" id="bambu_dashboard" />
<span class="toggle-track"></span>
</label>
</div>
<div style="display:grid;gap:10px">
<div class="toggle-row">
<span class="toggle-label">NFC Reader</span>
<select id="nfc_reader" style="padding:6px 10px;border-radius:6px;border:1px solid var(--border);background:var(--card);color:var(--text);font-size:0.95em">
Expand Down Expand Up @@ -274,6 +284,7 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
document.getElementById('tft_driver_row').style.display = this.checked ? '' : 'none';
});
if (cfg.nfc_reader) document.getElementById('nfc_reader').value = cfg.nfc_reader;
document.getElementById('bambu_dashboard').checked = !!cfg.bambu_dashboard;
maybeSetValue('moonraker_url', cfg.moonraker_url);
// Password placeholders
if (cfg.wifi_pass_set) document.getElementById('wifi_pass').placeholder = '(set) Leave blank to keep';
Expand Down Expand Up @@ -331,7 +342,8 @@ const char CONFIG_HTML[] PROGMEM = R"rawliteral(
moonraker_url: document.getElementById('moonraker_url').value.trim(),
prusalink_on: document.getElementById('prusalink_on').checked ? 1 : 0,
prusalink_url: document.getElementById('prusalink_url').value.trim(),
prusalink_api_key: document.getElementById('prusalink_api_key').value
prusalink_api_key: document.getElementById('prusalink_api_key').value,
bambu_dashboard: document.getElementById('bambu_dashboard').checked ? 1 : 0
};

fetch('/api/config', {
Expand Down
11 changes: 11 additions & 0 deletions src/ConfigurationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ static const char* NVS_KEY_PRUSALINK_KEY = "prusalink_key";
static const char* NVS_KEY_NFC_READER = "nfc_reader";
static const char* NVS_KEY_HOSTNAME = "hostname";
static const char* NVS_KEY_LOW_SPOOL = "low_spool_g";
static const char* NVS_KEY_BAMBU_DASH = "bambu_dash";

// Sanitize hostname: enforce mDNS naming constraints (lowercase alphanum + hyphens,
// no leading/trailing hyphens) and reject empty strings to avoid boot-time errors.
Expand Down Expand Up @@ -248,6 +249,10 @@ bool ConfigurationManager::loadFromNVS() {
_lowSpoolThreshold = prefs.getUShort(NVS_KEY_LOW_SPOOL, 100);
anyOverride = true;
}
if (prefs.isKey(NVS_KEY_BAMBU_DASH)) {
_bambuDashboard = prefs.getBool(NVS_KEY_BAMBU_DASH, false);
anyOverride = true;
}
Comment on lines +252 to +255
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).


prefs.end();
return anyOverride;
Expand Down Expand Up @@ -350,6 +355,10 @@ uint16_t ConfigurationManager::getLowSpoolThreshold() const {
return _lowSpoolThreshold;
}

bool ConfigurationManager::isBambuDashboardEnabled() const {
return _bambuDashboard;
}

void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
memset(&out, 0, sizeof(out));
strncpy(out.wifi_ssid, _ssid, sizeof(out.wifi_ssid) - 1);
Expand All @@ -373,6 +382,7 @@ void ConfigurationManager::getCurrentConfig(ConfigUpdate& out) const {
strncpy(out.nfc_reader, _nfcReader, sizeof(out.nfc_reader) - 1);
strncpy(out.hostname, _hostname, sizeof(out.hostname) - 1);
out.low_spool_threshold_g = _lowSpoolThreshold;
out.bambu_dashboard = _bambuDashboard ? 1 : 0;
}

#ifndef NATIVE_TEST
Expand Down Expand Up @@ -416,6 +426,7 @@ bool ConfigurationManager::saveToNVS(const ConfigUpdate& update) {
sanitizeHostname(sanitizedHostname, sizeof(sanitizedHostname)); // enforce mDNS constraints before NVS write
prefs.putString(NVS_KEY_HOSTNAME, sanitizedHostname);
prefs.putUShort(NVS_KEY_LOW_SPOOL, update.low_spool_threshold_g);
prefs.putBool(NVS_KEY_BAMBU_DASH, update.bambu_dashboard != 0);

// Invalidate Spoolman enrichment cache on config change to force re-fetch
// (config change could invalidate cached spool lookups)
Expand Down
4 changes: 4 additions & 0 deletions src/ConfigurationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct ConfigUpdate {
// mDNS / WiFi hostname
char hostname[33]; // max 32 chars + null
uint16_t low_spool_threshold_g; // grams below which LED breathes (default 100)
uint8_t bambu_dashboard;
};

class ConfigurationManager {
Expand Down Expand Up @@ -86,6 +87,8 @@ class ConfigurationManager {
// Low-spool threshold (grams) — LED breathes when remaining weight is at or below this
uint16_t getLowSpoolThreshold() const;

bool isBambuDashboardEnabled() const;

// Web config support
void getCurrentConfig(ConfigUpdate& out) const;
bool saveToNVS(const ConfigUpdate& update);
Expand Down Expand Up @@ -135,6 +138,7 @@ class ConfigurationManager {
bool _tftEnabled = false;
char _tftDriver[8] = "st7789";
uint16_t _lowSpoolThreshold = 100; // grams
bool _bambuDashboard = false;

bool _initialized = false;
};
Expand Down
4 changes: 4 additions & 0 deletions src/DeviceConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
#ifndef ENABLE_KEYPAD
#define ENABLE_KEYPAD 0
#endif
#ifndef ENABLE_BAMBU_DASHBOARD
#define ENABLE_BAMBU_DASHBOARD 0
#endif

static const DeviceConfig kDeviceConfig = {
.device_name = DEVICE_NAME,
Expand Down Expand Up @@ -46,6 +49,7 @@ static const DeviceConfig kDeviceConfig = {
.lcd_enabled = ENABLE_LCD,
.status_led_enabled = ENABLE_STATUS_LED,
.keypad_enabled = ENABLE_KEYPAD,
.bambu_dashboard = ENABLE_BAMBU_DASHBOARD,
},

.automation_mode = AUTOMATION_MODE,
Expand Down
1 change: 1 addition & 0 deletions src/DeviceConfig.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ struct PeripheralConfig {
bool lcd_enabled;
bool status_led_enabled;
bool keypad_enabled;
bool bambu_dashboard;
};

struct DeviceConfig {
Expand Down
4 changes: 4 additions & 0 deletions src/DisplayI.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include <cstdint>
#include "TrayDashboardTypes.h"

// Spool data passed to the display — enough for both LCD text and TFT graphics.
struct DisplaySpoolData {
Expand Down Expand Up @@ -32,6 +33,9 @@ class DisplayI {
// Write result — LCD shows text, TFT shows checkmark/X graphic
virtual void showWriteResult(bool success, const char* format) = 0;

// Bambu AMS tray dashboard
virtual void showTrayDashboard(const TrayDashboardState& state) {}

// Screen timeout
virtual void setScreenTimeoutMs(uint32_t timeoutMs) = 0;

Expand Down
Loading