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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ CODE-REVIEW.md
test-preview/
.gstack/
build/
.worktrees/
84 changes: 70 additions & 14 deletions src/ApplicationManager.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,9 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) {
msg.payload.spoolDetected.spool_id,
msg.payload.spoolDetected.material_type,
msg.payload.spoolDetected.kg_remaining);


smartTagEnrichment_ = SmartTagEnrichment{}; // clear on new tag

#ifndef NATIVE_TEST
if (ConfigurationManager::getInstance().isLedEnabled()) {
// Set target first so task restores it after the flash
Expand Down Expand Up @@ -414,6 +416,25 @@ void ApplicationManager::handleSpoolDetected(const AppMessage& msg) {
enqueueSpoolmanSync(msg.payload.spoolDetected);
}
#endif

#ifndef NATIVE_TEST
// Trigger UID lookup for Spoolman enrichment on smart tags (read-only, any mode)
if (SpoolmanManager::getInstance().isConfigured() &&
!msg.payload.spoolDetected.suppress_spoolman_sync) {
const char* fmt = msg.payload.spoolDetected.tag_format;
bool isSmart = (strcmp(fmt, "TigerTag") == 0 ||
strcmp(fmt, "OpenTag3D") == 0 ||
strcmp(fmt, "OpenSpool") == 0 ||
strcmp(fmt, "OpenPrintTag") == 0);
if (isSmart) {
SpoolmanSyncRequest req = {};
strncpy(req.spool_id, msg.payload.spoolDetected.spool_id,
sizeof(req.spool_id) - 1);
req.lookup_only = true;
SpoolmanManager::getInstance().enqueueSync(req);
}
}
#endif
}

void ApplicationManager::handleSpoolUpdated(const AppMessage& msg) {
Expand Down Expand Up @@ -702,21 +723,52 @@ void ApplicationManager::handleSpoolmanSynced(const AppMessage& msg) {

if (display_) {
if (msg.payload.spoolmanSynced.success && msg.payload.spoolmanSynced.is_uid_lookup) {
// UID lookup — show spool graphic with Spoolman data (tag had no data)
DisplaySpoolData spool{};
strncpy(spool.brand, msg.payload.spoolmanSynced.manufacturer, sizeof(spool.brand) - 1);
strncpy(spool.material, materialName, sizeof(spool.material) - 1);
const char* colorSrc = msg.payload.spoolmanSynced.color_hex;
if (colorSrc[0] == '#') colorSrc++;
strncpy(spool.colorHex, colorSrc, sizeof(spool.colorHex) - 1);
spool.remainingWeight = kgRemaining * 1000.0f;
spool.totalWeight = msg.payload.spoolmanSynced.initial_weight_g;
spool.tagType = 5;
display_->showSpool(spool);
// Determine if the current tag is a generic UID tag or a smart tag
#ifndef NATIVE_TEST
CurrentSpoolState state;
bool gotState = NFCManager::getInstance().getCurrentSpoolState(state);
bool isGeneric = !gotState || state.kind == TagKind::GenericUidTag;
#else
bool isGeneric = true;
#endif
if (isGeneric) {
// Original path: show spool graphic with Spoolman data (tag had no data)
DisplaySpoolData spool{};
strncpy(spool.brand, msg.payload.spoolmanSynced.manufacturer, sizeof(spool.brand) - 1);
strncpy(spool.material, materialName, sizeof(spool.material) - 1);
const char* colorSrc = msg.payload.spoolmanSynced.color_hex;
if (colorSrc[0] == '#') colorSrc++;
strncpy(spool.colorHex, colorSrc, sizeof(spool.colorHex) - 1);
spool.remainingWeight = kgRemaining * 1000.0f;
spool.totalWeight = msg.payload.spoolmanSynced.initial_weight_g;
spool.tagType = 5;
display_->showSpool(spool);
} else {
// Smart tag enrichment — store result, don't change display
smartTagEnrichment_.valid = true;
smartTagEnrichment_.spoolman_id = msg.payload.spoolmanSynced.spoolman_id;
smartTagEnrichment_.remaining_g = kgRemaining * 1000.0f;
smartTagEnrichment_.bed_temp = msg.payload.spoolmanSynced.bed_temp;
smartTagEnrichment_.extruder_temp = msg.payload.spoolmanSynced.extruder_temp;
smartTagEnrichment_.density = msg.payload.spoolmanSynced.density;
smartTagEnrichment_.diameter_mm = msg.payload.spoolmanSynced.diameter_mm;
Serial.printf("ApplicationManager: Smart tag enrichment stored — spool %d, %.0fg remaining\n",
smartTagEnrichment_.spoolman_id, smartTagEnrichment_.remaining_g);
}
Comment on lines 724 to +757
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

Ignore stale lookup results, and do not gate the cache on display_.

The new smartTagEnrichment_ write sits inside if (display_), so display-disabled units never populate /api/status.spoolman. This block also only inspects state.kind; it does not verify state.present and state.spool_id == msg.payload.spoolmanSynced.spool_id, so a delayed lookup for tag A can overwrite the cache or LCD while tag B is on the reader. Reuse the same presence/UID guard from Lines 701-709 before updating either the cache or the display.

Also applies to: 761-770

🧰 Tools
🪛 Clang (14.0.6)

[note] 724-724: +1, including nesting penalty of 0, nesting level increased to 1

(clang)


[note] 725-725: +2, including nesting penalty of 1, nesting level increased to 2

(clang)


[note] 725-725: +1

(clang)


[note] 730-730: +1

(clang)


[note] 734-734: +3, including nesting penalty of 2, nesting level increased to 3

(clang)


[note] 740-740: +4, including nesting penalty of 3, nesting level increased to 4

(clang)


[note] 746-746: +1, nesting level increased to 3

(clang)


[warning] 724-724: implicit conversion 'DisplayI *' -> bool

(readability-implicit-bool-conversion)


[warning] 740-740: statement should be inside braces

(readability-braces-around-statements)


[warning] 742-742: floating point literal has suffix 'f', which is not uppercase

(readability-uppercase-literal-suffix)

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

In `@src/ApplicationManager.cpp` around lines 724 - 757, The smartTagEnrichment_
update is incorrectly gated by if (display_) and lacks the presence/UID guard,
so move the smartTagEnrichment_ writes out of the display_ conditional (so
display-disabled units still populate /api/status.spoolman) and
before/independent of display_->showSpool; additionally reuse the same
CurrentSpoolState presence/UID check used earlier (call
NFCManager::getInstance().getCurrentSpoolState(state) and verify state.present
&& state.spool_id == msg.payload.spoolmanSynced.spool_id) before updating either
smartTagEnrichment_ or calling display_->showSpool to prevent stale/delayed
lookup A from overwriting data for tag B.

} else if (msg.payload.spoolmanSynced.success) {
// Smart tag — don't overwrite, handleSpoolDetected already showed correct data
} else if (msg.payload.spoolmanSynced.is_uid_lookup) {
display_->showText("Generic Tag", "Not in Spoolman");
#ifndef NATIVE_TEST
CurrentSpoolState state;
bool gotState = NFCManager::getInstance().getCurrentSpoolState(state);
bool isGeneric = !gotState || state.kind == TagKind::GenericUidTag;
#else
bool isGeneric = true;
#endif
if (isGeneric) {
display_->showText("Generic Tag", "Not in Spoolman");
}
// For smart tags: lookup failed, but tag already showed its own data — no display change needed
} else {
char line1[17];
snprintf(line1, sizeof(line1), "Updated: %.0fg",
Expand All @@ -732,7 +784,7 @@ void ApplicationManager::handleSpoolmanSynced(const AppMessage& msg) {

#ifndef NATIVE_TEST
// Update recent spool sync status
if (msg.payload.spoolmanSynced.success) {
if (msg.payload.spoolmanSynced.success && !msg.payload.spoolmanSynced.is_uid_lookup) {
NFCManager::getInstance().updateRecentSpoolSyncStatus(
msg.payload.spoolmanSynced.spool_id, true);
}
Expand Down Expand Up @@ -763,6 +815,10 @@ void ApplicationManager::handleSpoolmanSynced(const AppMessage& msg) {
void ApplicationManager::handleTagRemoved(const AppMessage& msg) {
Serial.printf("EVENT: TagRemoved - spool_id=%s\n",
msg.payload.tagRemoved.spool_id);

// smartTagEnrichment_ NOT cleared here — persists until next tag scan
// so the reader page can pick it up even after tag removal

// LED intentionally not changed — keep showing last filament color until next scan

// Clear displayed spool so next scan re-displays
Expand Down
17 changes: 17 additions & 0 deletions src/ApplicationManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ enum class AutomationMode : uint8_t {

enum class AppState { IDLE, MONITORING_PRINT };

struct SmartTagEnrichment {
bool valid = false;
int32_t spoolman_id = -1;
float remaining_g = 0.0f;
int16_t bed_temp = 0;
int16_t extruder_temp = 0;
float density = 0.0f;
float diameter_mm = 0.0f;
};

struct SpoolDetectedPayload {
char spool_id[17]; // UID hex string
uint8_t material_type; // OPT_MATERIAL_TYPE_PLA, etc.
Expand Down Expand Up @@ -89,6 +99,8 @@ struct SpoolmanSyncedPayload {
char color_hex[8]; // "#RRGGBB" (populated for UID lookups)
int16_t extruder_temp; // Spoolman settings_extruder_temp (0 = not set)
int16_t bed_temp; // Spoolman settings_bed_temp (0 = not set)
float density; // g/cm³ (0 = not available)
float diameter_mm; // mm (0 = not available)
bool is_uid_lookup; // True = result of a UID-only lookup (generic tag)
};

Expand Down Expand Up @@ -167,6 +179,7 @@ class ApplicationManager {
bool hasSpoolChangedDuringPrint() const { return spoolChangedDuringPrint; }
AutomationMode getAutomationMode() const { return automationMode; }
void setAutomationMode(AutomationMode mode) { automationMode = mode; }
SmartTagEnrichment getSmartTagEnrichment() const { return smartTagEnrichment_; }
#ifdef NATIVE_TEST
void resetForTest() {
if (messageQueue) { vQueueDelete(messageQueue); messageQueue = nullptr; }
Expand All @@ -187,6 +200,7 @@ class ApplicationManager {
keypadBuffer_[0] = '\0';
keypadBufferLen_ = 0;
lastHAStateJson_[0] = '\0';
smartTagEnrichment_ = SmartTagEnrichment{};
}
#endif

Expand Down Expand Up @@ -230,6 +244,9 @@ class ApplicationManager {
// Last published HA spool state — cached for retention on tag removal
char lastHAStateJson_[512] = {0};

// Enrichment data from Spoolman UID lookup for the current smart tag
SmartTagEnrichment smartTagEnrichment_;

// Handlers
void handlePrintStarted(const AppMessage& msg);
void handlePrintEnded(const AppMessage& msg);
Expand Down
51 changes: 51 additions & 0 deletions src/OpenPrintTagWriterHTML.h
Original file line number Diff line number Diff line change
Expand Up @@ -192,10 +192,18 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(

<div class="write-warning">Keep the tag still &mdash; do not remove until writing is complete.</div>

<div id="readPrompt" class="hidden write-warning" style="background:#0d2a1a;border-color:#2a7a4a;color:#4adf8a;text-align:center;padding:10px">
Place tag on reader&hellip; <span style="font-size:11px;color:#5a9a6a">hold still until detected</span>
</div>

<div class="actions">
<button type="submit" class="btn-primary" id="writeBtn">Write Tag</button>
<button type="button" class="btn-secondary" id="readBtn">Read</button>
<button type="reset" class="btn-ghost">Clear</button>
</div>
<p class="card-subtitle" style="margin-top:4px;font-size:11px">
Use <strong>Read</strong> to load an existing tag before overwriting.
</p>
</form>
</div>
</section>
Expand Down Expand Up @@ -607,6 +615,49 @@ const char OPENPRINTTAG_WRITER_HTML[] PROGMEM = R"rawliteral(
bed_max: 'max_bed_temp',
spoolman_id: 'spoolman_id'
});

var readBtn = document.getElementById('readBtn');
var writeBtn = document.getElementById('writeBtn');
var readWaiting = false;

function setReadWaiting(active) {
readWaiting = active;
writeBtn.disabled = active;
if (active) {
readBtn.textContent = 'Cancel';
readBtn.onclick = cancelRead;
document.getElementById('readPrompt').classList.remove('hidden');
} else {
readBtn.textContent = 'Read';
readBtn.onclick = startRead;
document.getElementById('readPrompt').classList.add('hidden');
}
}

function cancelRead() {
readWaiting = false;
setReadWaiting(false);
}

async function startRead() {
setReadWaiting(true);
var deadline = Date.now() + 30000;
while (readWaiting && Date.now() < deadline) {
try {
var status = await fetch('/api/status').then(r => r.json());
if (status.present && status.tag_kind === 'OpenPrintTag') {
prefillFromStatus(status);
break;
} else if (status.present) {
break; // wrong format
}
} catch(e) {}
await new Promise(r => setTimeout(r, 500));
}
setReadWaiting(false);
}

readBtn.onclick = startRead;
</script>
</body>
</html>
Expand Down
Loading