Skip to content

refactor: reduce cyclomatic complexity in NFCManager + WebServerManager#121

Merged
sjordan0228 merged 9 commits into
devfrom
refactor/nfc-manager-complexity
Apr 7, 2026
Merged

refactor: reduce cyclomatic complexity in NFCManager + WebServerManager#121
sjordan0228 merged 9 commits into
devfrom
refactor/nfc-manager-complexity

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 7, 2026

Summary

  • Extract per-format write functions from executeWrite() (126 → ~15 branch points)
  • Extract readAndProcessISO14443Tag() from scanLoop() (90 → ~45)
  • Extract per-tag-type serializers from handleApiStatus() (74 → ~10)
  • Extract enrichment save steps from handleApiSpoolmanSaveEnrichment() (57 → ~12)
  • Extract NDEF TLV parsing into findNdefMediaRecord() + readNdefPayload() (5 levels → 2)
  • Shared helpers: buildNdefTlv(), validateWriteUid(), forceRescan(), serializeEnrichment()

Test plan

  • Scan TigerTag — detected correctly
  • Scan OpenSpool tag — detected correctly
  • Scan plain NTAG (NFC+) — detected correctly
  • Write tag from writer page — write flow works
  • /api/status returns correct data
  • All 3 build targets compile clean

Summary by CodeRabbit

  • Improvements

    • More reliable NFC tag reads/writes with broader tag-format support and steadier behavior.
    • More consistent API status payloads and smoother spool enrichment flows.
  • Bug Fixes

    • Fixed incomplete NDEF reads and intermittent enrichment save failures.
  • Refactor

    • Internal reorganization of NFC, web-server, and spool handling for maintainability.
  • Chores

    • Updated repository ignore rules for internal documentation.

…Write()

- buildNdefTlv() — shared NDEF TLV wrapper (was duplicated in OpenTag3D + OpenSpool)
- validateWriteUid() — shared mutex + UID validation (was repeated 6 times)
- forceRescan() — shared rescan trigger
- executeTigerTagWrite(), executeOpenTag3DWrite(), executeOpenSpoolWrite(), executeAtomicWrite()
- executeWrite() is now a 30-line dispatcher

Complexity: 126 → ~15 for executeWrite(), extracted functions are 15-40 each.
Moves the 220-line ISO14443A tag classification block (TigerTag →
OpenTag3D → OpenSpool → GenericUID detection, NDEF TLV parsing,
state update, and message dispatch) out of scanLoop() into its own
method. scanLoop() now calls readAndProcessISO14443Tag() in one line.

scanLoop complexity: ~90 → ~45 branch points.
- serializeTigerTagStatus(), serializeOpenTag3DStatus(),
  serializeOpenSpoolStatus(), serializeGenericUidStatus(),
  serializeOpenPrintTagStatus() — each ~15-40 lines
- serializeEnrichment() — shared, was duplicated twice
- handleApiStatus() is now a 30-line dispatcher with switch

Complexity: 74 → ~10 for handleApiStatus()
…ichment()

- enrichFindOrCreateVendor() — search by name, create if not found
- enrichFindOrCreateFilament() — client-side match by material+color, create if not found
- enrichFindSpoolByUid() — fetch all spools, match nfc_id client-side
- enrichUpdateSpool() / enrichCreateSpool() — PATCH or POST with used_weight
- handleApiSpoolmanSaveEnrichment() is now a 50-line orchestrator

Complexity: 57 → ~12 for the handler
…fPayload

The 130-line, 5-level-deep NDEF scan in readAndProcessISO14443Tag is
now two reusable helpers:
- findNdefMediaRecord() — scans TLV records, returns MIME type + payload position
- readNdefPayload() — reads payload bytes with extended page read fallback

The inline OpenTag3D and OpenSpool detection blocks now each call these
helpers instead of duplicating the TLV walk and payload extraction.
Max nesting: 5 → 2.
@github-actions github-actions Bot added the size/XL Extra large change (500+ lines) label Apr 7, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 7, 2026

Warning

Rate limit exceeded

@sjordan0228 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 9 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 9 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 60aaa458-6360-4215-8db0-90f31db8e0a8

📥 Commits

Reviewing files that changed from the base of the PR and between 2296197 and d82d80a.

📒 Files selected for processing (1)
  • src/PrinterManager.cpp
📝 Walkthrough

Walkthrough

Refactors NFCManager by extracting ISO14443 NDEF read/processing and splitting write execution into per-format helpers; refactors WebServerManager by extracting /api/status serializers and replacing Spoolman enrichment inline logic with helper HTTP operations; replaces stream-based spool lookup with ArduinoJson filter parsing.

Changes

Cohort / File(s) Summary
Build
/.gitignore
Added docs/REFACTORING_GUIDE.md to .gitignore.
NFC Manager (read/write)
src/NFCManager.cpp, src/NFCManager.h
Replaced monolithic scan/write logic with helpers: ISO14443 read pipeline (readAndProcessISO14443Tag, findNdefMediaRecord, readNdefPayload), RF lifecycle helpers (prepareRF, isSkippableDuplicate, handleNewTag, handleTagAbsent, forceRescan), and write dispatcher + per-format writers (validateWriteUid, executeTigerTagWrite, executeOpenTag3DWrite, executeOpenSpoolWrite, executeAtomicWrite, updated executeWrite). Added buildNdefTlv for TLV construction.
Web Server (status & enrichment)
src/WebServerManager.cpp, src/WebServerManager.h
Extracted per-tag /api/status serializers (serialize*Status) and a unified serializeEnrichment; replaced inline Spoolman save-enrichment sequence with helper HTTP operations (enrichFindOrCreateVendor, enrichFindOrCreateFilament, enrichFindSpoolByUid, enrichUpdateSpool, enrichCreateSpool). Header additions: ArduinoJson/HTTPClient includes and new private method declarations.
Spoolman lookup & details
src/SpoolmanManager.cpp
Replaced custom streaming JSON scanner with ArduinoJson deserialization using a Filter for id, archived, extra.nfc_id in streamFindSpoolByNfcId(); simplified matching and return codes. Rewrote getSpoolDetails() to parse full JSON into JsonDocument and populate SpoolDetails via direct field lookups.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant WebServerManager
  participant Helper as EnrichHelpers
  participant HTTPClient
  participant SpoolmanAPI

  Client->>WebServerManager: POST /api/spoolman/save-enrichment (uid + payload)
  WebServerManager->>Helper: enrichFindOrCreateVendor(...)
  Helper->>HTTPClient: GET/POST /vendors
  HTTPClient->>SpoolmanAPI: HTTP request
  SpoolmanAPI-->>HTTPClient: vendor response
  HTTPClient-->>Helper: vendor id
  WebServerManager->>Helper: enrichFindOrCreateFilament(...)
  Helper->>HTTPClient: GET/POST /filaments
  HTTPClient->>SpoolmanAPI: HTTP request
  SpoolmanAPI-->>HTTPClient: filament response
  HTTPClient-->>Helper: filament id
  WebServerManager->>Helper: enrichFindSpoolByUid(...)
  Helper->>HTTPClient: GET /spools?nfc_id="uid"
  HTTPClient->>SpoolmanAPI: HTTP request
  SpoolmanAPI-->>HTTPClient: spools list
  HTTPClient-->>Helper: spool match / initial weight
  alt existing spool found
    WebServerManager->>Helper: enrichUpdateSpool(...existingId...)
    Helper->>HTTPClient: PATCH /spools/{id}
    HTTPClient->>SpoolmanAPI: PATCH request
    SpoolmanAPI-->>HTTPClient: 200/err
    HTTPClient-->>WebServerManager: result
  else no spool
    WebServerManager->>Helper: enrichCreateSpool(...)
    Helper->>HTTPClient: POST /spools
    HTTPClient->>SpoolmanAPI: POST request
    SpoolmanAPI-->>HTTPClient: 201/err
    HTTPClient-->>WebServerManager: result
  end
  WebServerManager-->>Client: 200/400 based on result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% 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 accurately describes the main objective of the PR—refactoring to reduce cyclomatic complexity in NFCManager and WebServerManager, which is supported by the substantial code reorganization across those modules.
Description check ✅ Passed The PR description includes a comprehensive summary of extraction-based refactorings with complexity metrics, mentions a test plan with all items checked, but lacks explicit sections matching the required template (Summary/Changes/How to Test/Checklist).

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/nfc-manager-complexity

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.

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: 3

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

Inline comments:
In @.gitignore:
- Line 13: You added docs/REFACTORING_GUIDE.md to .gitignore but the file
doesn't exist yet; decide whether this guide should be committed as part of the
repo (if it documents refactoring patterns like helper extraction or
architecture decisions) or truly be a personal/WIP note — then either remove
docs/REFACTORING_GUIDE.md from .gitignore and add the file, or keep it ignored
and add a short inline comment in .gitignore explaining it's for personal/WIP
notes only so intent is clear to future maintainers.

In `@src/NFCManager.cpp`:
- Around line 358-391: In the TigerTag handling block (where currentSpool.kind
is set to TagKind::TigerTag and lastTigerTag_ / lastTigerTagValid_ are updated),
also clear the OpenSpool validity by setting lastOpenSpoolValid_ = false so
stale OpenSpool data can't persist; locate the TigerTag branch around
currentSpool and lastTigerTag_ and add the missing lastOpenSpoolValid_ = false
alongside the existing lastOpenTag3DValid_ = false.

In `@src/WebServerManager.cpp`:
- Around line 1908-1934: The DynamicJsonDocument buffer (16384) in
enrichFindSpoolByUid is too small for up to 200 spools and can cause
deserializeJson to fail; after calling http.getString() allocate the JSON buffer
based on the response size (e.g., DynamicJsonDocument sDoc(response.length() +
1024) or response.length()*1.2) and then call deserializeJson(sDoc, response)
checking its return value for success, or alternatively reduce the "?limit=200"
to a smaller value—adjust the code around the response, DynamicJsonDocument
sDoc, and the deserializeJson call in enrichFindSpoolByUid accordingly.
🪄 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: 8860db08-03de-4eea-be2c-23d02c02d48b

📥 Commits

Reviewing files that changed from the base of the PR and between c044e39 and 6848a03.

📒 Files selected for processing (5)
  • .gitignore
  • src/NFCManager.cpp
  • src/NFCManager.h
  • src/WebServerManager.cpp
  • src/WebServerManager.h

Comment thread .gitignore
docs/opentag3d_architecture.md
docs/tigertag-architecture.md
docs/deep-thoughts.md
docs/REFACTORING_GUIDE.md
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check if REFACTORING_GUIDE.md exists and preview its content to assess value.

if [ -f "docs/REFACTORING_GUIDE.md" ]; then
  echo "=== File exists. First 30 lines: ==="
  head -n 30 "docs/REFACTORING_GUIDE.md"
  echo ""
  echo "=== Word count: ==="
  wc -w "docs/REFACTORING_GUIDE.md"
else
  echo "File does not exist yet."
fi

Repository: SpoolSense/spoolsense_scanner

Length of output: 97


Clarify the intent of ignoring a non-existent documentation file.

The docs/REFACTORING_GUIDE.md file doesn't currently exist, but you're adding it to .gitignore, suggesting it will be created as temporary or personal content. If this guide will document the complexity reduction patterns established in this PR—such as the helper extraction strategies or architectural decisions—consider committing it instead to help future maintainers understand the refactoring principles. If it's intended for temporary work-in-progress notes, ignoring it is appropriate.

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

In @.gitignore at line 13, You added docs/REFACTORING_GUIDE.md to .gitignore but
the file doesn't exist yet; decide whether this guide should be committed as
part of the repo (if it documents refactoring patterns like helper extraction or
architecture decisions) or truly be a personal/WIP note — then either remove
docs/REFACTORING_GUIDE.md from .gitignore and add the file, or keep it ignored
and add a short inline comment in .gitignore explaining it's for personal/WIP
notes only so intent is clear to future maintainers.

Comment thread src/NFCManager.cpp
Comment on lines +358 to +391
if (isTigerTag) {
currentSpool.kind = TagKind::TigerTag;
currentSpool.tag_data_valid = false;
lastTigerTag_ = tigerData;
lastTigerTagValid_ = true;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: TigerTag detected — %s %s %s\n",
tigerData.brand_name, tigerData.material_name, tigerData.aspect1_name);
} else if (isOpenTag3D) {
currentSpool.kind = TagKind::OpenTag3D;
currentSpool.tag_data_valid = false;
lastOpenTag3D_ = ot3dData;
lastOpenTag3DValid_ = true;
lastTigerTagValid_ = false;
lastOpenSpoolValid_ = false;
Serial.printf("NFCManager: OpenTag3D detected — %s %s %.2fmm %ug\n",
ot3dData.manufacturer, ot3dData.base_material,
opentag3d_diameter_mm(&ot3dData), ot3dData.target_weight_g);
} else if (isOpenSpool) {
currentSpool.kind = TagKind::OpenSpoolTag;
currentSpool.tag_data_valid = false;
lastOpenSpool_ = openSpoolData;
lastOpenSpoolValid_ = true;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: OpenSpool detected — %s %s #%s\n",
openSpoolData.brand, openSpoolData.material, openSpoolData.color_hex);
} else {
currentSpool.kind = TagKind::GenericUidTag;
currentSpool.tag_data_valid = false;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
lastOpenSpoolValid_ = false;
}
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

Missing lastOpenSpoolValid_ = false in TigerTag branch.

When a TigerTag is detected (lines 358-365), lastOpenTag3DValid_ is set to false but lastOpenSpoolValid_ is not cleared. The OpenTag3D branch (lines 366-375) correctly clears both other validity flags. This inconsistency could cause stale OpenSpool data to persist when switching from an OpenSpool tag to a TigerTag.

Proposed fix
         if (isTigerTag) {
             currentSpool.kind = TagKind::TigerTag;
             currentSpool.tag_data_valid = false;
             lastTigerTag_ = tigerData;
             lastTigerTagValid_ = true;
             lastOpenTag3DValid_ = false;
+            lastOpenSpoolValid_ = false;
             Serial.printf("NFCManager: TigerTag detected — %s %s %s\n",
                           tigerData.brand_name, tigerData.material_name, tigerData.aspect1_name);
📝 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
if (isTigerTag) {
currentSpool.kind = TagKind::TigerTag;
currentSpool.tag_data_valid = false;
lastTigerTag_ = tigerData;
lastTigerTagValid_ = true;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: TigerTag detected — %s %s %s\n",
tigerData.brand_name, tigerData.material_name, tigerData.aspect1_name);
} else if (isOpenTag3D) {
currentSpool.kind = TagKind::OpenTag3D;
currentSpool.tag_data_valid = false;
lastOpenTag3D_ = ot3dData;
lastOpenTag3DValid_ = true;
lastTigerTagValid_ = false;
lastOpenSpoolValid_ = false;
Serial.printf("NFCManager: OpenTag3D detected — %s %s %.2fmm %ug\n",
ot3dData.manufacturer, ot3dData.base_material,
opentag3d_diameter_mm(&ot3dData), ot3dData.target_weight_g);
} else if (isOpenSpool) {
currentSpool.kind = TagKind::OpenSpoolTag;
currentSpool.tag_data_valid = false;
lastOpenSpool_ = openSpoolData;
lastOpenSpoolValid_ = true;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: OpenSpool detected — %s %s #%s\n",
openSpoolData.brand, openSpoolData.material, openSpoolData.color_hex);
} else {
currentSpool.kind = TagKind::GenericUidTag;
currentSpool.tag_data_valid = false;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
lastOpenSpoolValid_ = false;
}
if (isTigerTag) {
currentSpool.kind = TagKind::TigerTag;
currentSpool.tag_data_valid = false;
lastTigerTag_ = tigerData;
lastTigerTagValid_ = true;
lastOpenTag3DValid_ = false;
lastOpenSpoolValid_ = false;
Serial.printf("NFCManager: TigerTag detected — %s %s %s\n",
tigerData.brand_name, tigerData.material_name, tigerData.aspect1_name);
} else if (isOpenTag3D) {
currentSpool.kind = TagKind::OpenTag3D;
currentSpool.tag_data_valid = false;
lastOpenTag3D_ = ot3dData;
lastOpenTag3DValid_ = true;
lastTigerTagValid_ = false;
lastOpenSpoolValid_ = false;
Serial.printf("NFCManager: OpenTag3D detected — %s %s %.2fmm %ug\n",
ot3dData.manufacturer, ot3dData.base_material,
opentag3d_diameter_mm(&ot3dData), ot3dData.target_weight_g);
} else if (isOpenSpool) {
currentSpool.kind = TagKind::OpenSpoolTag;
currentSpool.tag_data_valid = false;
lastOpenSpool_ = openSpoolData;
lastOpenSpoolValid_ = true;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
Serial.printf("NFCManager: OpenSpool detected — %s %s #%s\n",
openSpoolData.brand, openSpoolData.material, openSpoolData.color_hex);
} else {
currentSpool.kind = TagKind::GenericUidTag;
currentSpool.tag_data_valid = false;
lastTigerTagValid_ = false;
lastOpenTag3DValid_ = false;
lastOpenSpoolValid_ = false;
}
🧰 Tools
🪛 Clang (14.0.6)

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

(clang)


[note] 366-366: +1, nesting level increased to 2

(clang)


[note] 376-376: +1, nesting level increased to 2

(clang)


[note] 385-385: +1, nesting level increased to 2

(clang)

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

In `@src/NFCManager.cpp` around lines 358 - 391, In the TigerTag handling block
(where currentSpool.kind is set to TagKind::TigerTag and lastTigerTag_ /
lastTigerTagValid_ are updated), also clear the OpenSpool validity by setting
lastOpenSpoolValid_ = false so stale OpenSpool data can't persist; locate the
TigerTag branch around currentSpool and lastTigerTag_ and add the missing
lastOpenSpoolValid_ = false alongside the existing lastOpenTag3DValid_ = false.

Comment thread src/WebServerManager.cpp
Comment on lines +1908 to +1934
int WebServerManager::enrichFindSpoolByUid(WiFiClient& client, HTTPClient& http,
const char* baseUrl, const char* quotedUid, float& outInitialWeight) {
char url[256];
snprintf(url, sizeof(url), "%s/api/v1/spool?limit=200", baseUrl);
http.begin(client, url);
http.setTimeout(5000);
int code = http.GET();
int spoolId = -1;
outInitialWeight = 0.0f;
if (code == 200) {
String response = http.getString();
DynamicJsonDocument sDoc(16384);
if (!deserializeJson(sDoc, response)) {
JsonArray arr = sDoc.as<JsonArray>();
for (size_t i = 0; i < arr.size(); i++) {
const char* nfcId = arr[i]["extra"]["nfc_id"] | "";
if (strcmp(nfcId, quotedUid) != 0) continue;
if (arr[i]["archived"] | false) continue;
spoolId = arr[i]["id"] | -1;
outInitialWeight = arr[i]["initial_weight"] | 0.0f;
break;
}
}
}
http.end();
return spoolId;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider larger buffer for enrichFindSpoolByUid when handling many spools.

The function fetches up to 200 spools (?limit=200) but uses a 16KB DynamicJsonDocument. With typical spool JSON (~100-200 bytes each), 200 spools could exceed 16KB, causing deserialization to fail silently. Consider either reducing the limit or increasing the buffer.

Proposed adjustment
-        DynamicJsonDocument sDoc(16384);
+        DynamicJsonDocument sDoc(32768);  // 32KB for up to 200 spools

Alternatively, reduce the limit:

-    snprintf(url, sizeof(url), "%s/api/v1/spool?limit=200", baseUrl);
+    snprintf(url, sizeof(url), "%s/api/v1/spool?limit=100", baseUrl);
📝 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
int WebServerManager::enrichFindSpoolByUid(WiFiClient& client, HTTPClient& http,
const char* baseUrl, const char* quotedUid, float& outInitialWeight) {
char url[256];
snprintf(url, sizeof(url), "%s/api/v1/spool?limit=200", baseUrl);
http.begin(client, url);
http.setTimeout(5000);
int code = http.GET();
int spoolId = -1;
outInitialWeight = 0.0f;
if (code == 200) {
String response = http.getString();
DynamicJsonDocument sDoc(16384);
if (!deserializeJson(sDoc, response)) {
JsonArray arr = sDoc.as<JsonArray>();
for (size_t i = 0; i < arr.size(); i++) {
const char* nfcId = arr[i]["extra"]["nfc_id"] | "";
if (strcmp(nfcId, quotedUid) != 0) continue;
if (arr[i]["archived"] | false) continue;
spoolId = arr[i]["id"] | -1;
outInitialWeight = arr[i]["initial_weight"] | 0.0f;
break;
}
}
}
http.end();
return spoolId;
}
int WebServerManager::enrichFindSpoolByUid(WiFiClient& client, HTTPClient& http,
const char* baseUrl, const char* quotedUid, float& outInitialWeight) {
char url[256];
snprintf(url, sizeof(url), "%s/api/v1/spool?limit=200", baseUrl);
http.begin(client, url);
http.setTimeout(5000);
int code = http.GET();
int spoolId = -1;
outInitialWeight = 0.0f;
if (code == 200) {
String response = http.getString();
DynamicJsonDocument sDoc(32768); // 32KB for up to 200 spools
if (!deserializeJson(sDoc, response)) {
JsonArray arr = sDoc.as<JsonArray>();
for (size_t i = 0; i < arr.size(); i++) {
const char* nfcId = arr[i]["extra"]["nfc_id"] | "";
if (strcmp(nfcId, quotedUid) != 0) continue;
if (arr[i]["archived"] | false) continue;
spoolId = arr[i]["id"] | -1;
outInitialWeight = arr[i]["initial_weight"] | 0.0f;
break;
}
}
}
http.end();
return spoolId;
}
🧰 Tools
🪛 Clang (14.0.6)

[warning] 1908-1908: use a trailing return type for this function

(modernize-use-trailing-return-type)


[warning] 1908-1908: method 'enrichFindSpoolByUid' can be made static

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


[warning] 1908-1908: 2 adjacent parameters of 'enrichFindSpoolByUid' of similar type ('int &') are easily swapped by mistake

(bugprone-easily-swappable-parameters)


[note] 1908-1908: the first parameter in the range is 'client'

(clang)


[note] 1908-1908: the last parameter in the range is 'http'

(clang)


[warning] 1909-1909: 2 adjacent parameters of 'enrichFindSpoolByUid' of similar type ('const char *') are easily swapped by mistake

(bugprone-easily-swappable-parameters)


[note] 1909-1909: the first parameter in the range is 'baseUrl'

(clang)


[note] 1909-1909: the last parameter in the range is 'quotedUid'

(clang)


[warning] 1910-1910: do not declare C-style arrays, use std::array<> instead

(modernize-avoid-c-arrays)


[warning] 1914-1914: variable 'code' is not initialized

(cppcoreguidelines-init-variables)


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

(readability-uppercase-literal-suffix)


[warning] 1918-1918: variable 'response' is not initialized

(cppcoreguidelines-init-variables)


[warning] 1919-1919: variable 'sDoc' is not initialized

(cppcoreguidelines-init-variables)


[warning] 1921-1921: variable 'arr' 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/WebServerManager.cpp` around lines 1908 - 1934, The DynamicJsonDocument
buffer (16384) in enrichFindSpoolByUid is too small for up to 200 spools and can
cause deserializeJson to fail; after calling http.getString() allocate the JSON
buffer based on the response size (e.g., DynamicJsonDocument
sDoc(response.length() + 1024) or response.length()*1.2) and then call
deserializeJson(sDoc, response) checking its return value for success, or
alternatively reduce the "?limit=200" to a smaller value—adjust the code around
the response, DynamicJsonDocument sDoc, and the deserializeJson call in
enrichFindSpoolByUid accordingly.

…filter

streamFindSpoolByNfcId() was a 190-line, 64-branch-point character-level
state machine. Replaced with ArduinoJson's DeserializationOption::Filter
which parses only id, archived, and extra.nfc_id from the stream — same
low memory usage (~4KB vs ~600B), dramatically simpler code.

190 lines → 50 lines. 64 branch points → 8.
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/SpoolmanManager.cpp`:
- Around line 255-285: The current use of an unbounded JsonDocument
(JsonDocument doc) when calling deserializeJson(...,
DeserializationOption::Filter(filter)) can exhaust heap for large spool lists;
replace it with a bounded StaticJsonDocument sized to a safe upper bound
(estimate per-spool filtered size ~50 bytes × max expected spools) and pass that
to deserializeJson, or detect DeserializationError::NoMemory from err and handle
it (log and fall back to a streaming/iterative parse or return an error). Update
references to JsonDocument doc to the new StaticJsonDocument instance and keep
the existing deserialization and iteration logic (deserializeJson,
DeserializationError err, streamHttp.getStreamPtr(), doc.as<JsonArray>(), spool
loop) so overflow is caught and controlled.
🪄 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: e8b3cd50-3479-4890-944f-47199bbc3cfa

📥 Commits

Reviewing files that changed from the base of the PR and between 6848a03 and 9338994.

📒 Files selected for processing (1)
  • src/SpoolmanManager.cpp

Comment thread src/SpoolmanManager.cpp
Comment on lines +255 to 285
// Filter: only extract id, archived, and extra.nfc_id from each spool
JsonDocument filter;
filter[0]["id"] = true;
filter[0]["archived"] = true;
filter[0]["extra"]["nfc_id"] = true;

int bestMatchId = -1;
int currentId = -1;
bool nfcIdMatched = false; // true if nfc_id matched in current spool object
int depth = 0;
bool inExtra = false;
int extraDepth = 0;

char buf[512];
char keyBuf[16] = {};
int keyPos = 0;
char valBuf[80] = {};
int valPos = 0;
bool inString = false;
bool escaped = false;
bool inKey = false;
bool inValue = false;
bool inNumValue = false;

unsigned long lastData = millis();
bool timedOut = false;

while (stream->available() || stream->connected()) {
if (millis() - lastData > 10000) {
Serial.println("SpoolmanManager: streamFind timeout");
timedOut = true;
break;
}

int avail = stream->available();
if (avail <= 0) { delay(1); continue; }
lastData = millis();

int toRead = avail > (int)sizeof(buf) ? (int)sizeof(buf) : avail;
int bytesRead = stream->readBytes(buf, toRead);

for (int i = 0; i < bytesRead; i++) {
char c = buf[i];

if (escaped) {
escaped = false;
if (inString && inKey && keyPos < (int)sizeof(keyBuf) - 1)
keyBuf[keyPos++] = c;
if (inString && inValue && valPos < (int)sizeof(valBuf) - 1)
valBuf[valPos++] = c;
continue;
}

if (c == '\\' && inString) {
escaped = true;
if (inValue && valPos < (int)sizeof(valBuf) - 1)
valBuf[valPos++] = c;
continue;
}
JsonDocument doc;
DeserializationError err = deserializeJson(doc, *streamHttp.getStreamPtr(),
DeserializationOption::Filter(filter));
streamHttp.end();

if (c == '"') {
if (!inString) {
inString = true;
if (!inValue) {
inKey = true;
keyPos = 0;
memset(keyBuf, 0, sizeof(keyBuf));
} else {
valPos = 0;
memset(valBuf, 0, sizeof(valBuf));
}
} else {
inString = false;
if (inKey) {
inKey = false;
keyBuf[keyPos] = '\0';
}
if (inValue) {
valBuf[valPos] = '\0';
if (inExtra && strcmp(keyBuf, "nfc_id") == 0) {
// Strip escaped inner quotes: \"UUID\" → UUID
char* nfcVal = valBuf;
int nfcLen = strlen(nfcVal);
if (nfcLen >= 2 && nfcVal[0] == '\\' && nfcVal[1] == '"') {
nfcVal += 2; nfcLen -= 2;
}
if (nfcLen >= 2 && nfcVal[nfcLen - 2] == '\\' && nfcVal[nfcLen - 1] == '"') {
nfcVal[nfcLen - 2] = '\0';
}
nfcLen = strlen(nfcVal);
if (nfcLen >= 2 && nfcVal[0] == '"' && nfcVal[nfcLen - 1] == '"') {
nfcVal[nfcLen - 1] = '\0';
nfcVal++;
}
if (strcasecmp(nfcVal, uuid) == 0) {
nfcIdMatched = true;
}
}
inValue = false;
}
}
continue;
}
if (err) {
Serial.printf("SpoolmanManager: streamFind parse error: %s\n", err.c_str());
return -2;
}

if (inString) {
if (inKey && keyPos < (int)sizeof(keyBuf) - 1)
keyBuf[keyPos++] = c;
if (inValue && valPos < (int)sizeof(valBuf) - 1)
valBuf[valPos++] = c;
continue;
}
// Build quoted UID for comparison ("04A651AD8F6180")
char quotedUuid[130];
snprintf(quotedUuid, sizeof(quotedUuid), "\"%s\"", uuid);

// Structural characters outside strings
if (c == '{') {
depth++;
inValue = false; // opening brace ends the value context
inNumValue = false;
if (depth == 1) {
currentId = -1;
nfcIdMatched = false;
inExtra = false;
} else if (depth >= 2 && strcmp(keyBuf, "extra") == 0) {
inExtra = true;
extraDepth = depth;
}
} else if (c == '}') {
if (inExtra && depth == extraDepth) {
inExtra = false;
}
// Clear numeric state on every closing brace to prevent
// nested id values (filament.id, vendor.id) from leaking
inValue = false;
inNumValue = false;
depth--;
if (depth == 0) {
// Deferred match — both id and nfc_id are now known
if (nfcIdMatched && currentId > bestMatchId) {
bestMatchId = currentId;
}
}
} else if (c == ':') {
inValue = true;
valPos = 0;
memset(valBuf, 0, sizeof(valBuf));
inNumValue = false;
} else if (c == '[') {
inValue = false;
inNumValue = false;
} else if (c == ',' || c == ']') {
if (inNumValue && depth == 1 && strcmp(keyBuf, "id") == 0) {
valBuf[valPos] = '\0';
currentId = atoi(valBuf);
}
inValue = false;
inNumValue = false;
} else if (inValue && c >= '0' && c <= '9') {
inNumValue = true;
if (valPos < (int)sizeof(valBuf) - 1)
valBuf[valPos++] = c;
} else if (inValue && (c == '-' || c == '.')) {
if (valPos < (int)sizeof(valBuf) - 1)
valBuf[valPos++] = c;
}
int bestMatchId = -1;
for (JsonObject spool : doc.as<JsonArray>()) {
if (spool["archived"] | false) continue;
const char* nfcId = spool["extra"]["nfc_id"] | "";
// nfc_id is stored double-quoted in Spoolman: "\"UUID\""
// Compare both with and without outer quotes
if (strcasecmp(nfcId, uuid) == 0 || strcasecmp(nfcId, quotedUuid) == 0) {
int id = spool["id"] | -1;
if (id > bestMatchId) bestMatchId = id;
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

🌐 Web query:

ArduinoJson deserializeJson Filter option array syntax

💡 Result:

In ArduinoJson, the Filter for an array is expressed by creating an array in the filter document with only one element at index 0. That element is used as the filter for every item in the input array; any other filter elements ([1], [2], …) are ignored. [1], [2]

Array of objects (keep selected fields from each object)

StaticJsonDocument<256> filter;
filter["items"][0]["id"] = true;
filter["items"][0]["name"] = true;

deserializeJson(doc, input, DeserializationOption::Filter(filter));

This keeps id and name for each object in the input items array. [1], [2]

Array of primitives (keep the values)

StaticJsonDocument<64> filter;
filter["values"][0] = true;

deserializeJson(doc, input, DeserializationOption::Filter(filter));

This keeps the values array contents (primitives) while still allowing other parts of the JSON to be filtered out. [1]

Nested arrays

You apply the same rule at each array level:

StaticJsonDocument<256> filter;
filter["outer"][0]["inner"][0]["x"] = true;

Meaning: for every element of outer, look at inner (array) and for every element of inner, keep field x. [1], [2]

Notes

  • You must pass the filter as DeserializationOption::Filter(filter). [1]
  • Filtering needs a bit of extra JsonDocument capacity (keys are stored temporarily). [1]

Sources: ArduinoJson deserializeJson() docs and filtering notes [1], ArduinoJson filtering announcement (array rule) [2].


🏁 Script executed:

# Find and read SpoolmanManager.cpp around lines 255-285
fd SpoolmanManager.cpp

Repository: SpoolSense/spoolsense_scanner

Length of output: 96


🏁 Script executed:

# Check for ArduinoJson version in project configuration
find . -type f \( -name "CMakeLists.txt" -o -name "platformio.ini" -o -name "library.json" -o -name "package.json" \) | head -20

Repository: SpoolSense/spoolsense_scanner

Length of output: 161


🏁 Script executed:

# Read SpoolmanManager.cpp around the specified lines (250-290)
sed -n '250,290p' src/SpoolmanManager.cpp

Repository: SpoolSense/spoolsense_scanner

Length of output: 1580


🏁 Script executed:

# Check platformio.ini for ArduinoJson version and dependencies
cat platformio.ini

Repository: SpoolSense/spoolsense_scanner

Length of output: 1598


Clean refactoring using ArduinoJson Filter—well done.

The Filter approach correctly extracts only needed fields (id, archived, extra.nfc_id) from the array response. The UUID matching logic properly handles both raw and double-quoted storage formats, and strcasecmp is appropriate for case-insensitive hex UID comparison.

Consider bounded document sizing for memory-constrained environments: JsonDocument doc; uses dynamic sizing. For Spoolman instances with large spool catalogs, the filtered document may still consume significant heap (each spool contributes ~40–60 bytes for extracted fields). If memory becomes constrained, consider implementing chunked streaming or a bounded document capacity with overflow detection.

🧰 Tools
🪛 Clang (14.0.6)

[warning] 256-256: variable 'filter' is not initialized

(cppcoreguidelines-init-variables)


[warning] 261-261: variable 'doc' is not initialized

(cppcoreguidelines-init-variables)


[warning] 262-262: variable 'err' is not initialized

(cppcoreguidelines-init-variables)


[warning] 272-272: do not declare C-style arrays, use std::array<> instead

(modernize-avoid-c-arrays)

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

In `@src/SpoolmanManager.cpp` around lines 255 - 285, The current use of an
unbounded JsonDocument (JsonDocument doc) when calling deserializeJson(...,
DeserializationOption::Filter(filter)) can exhaust heap for large spool lists;
replace it with a bounded StaticJsonDocument sized to a safe upper bound
(estimate per-spool filtered size ~50 bytes × max expected spools) and pass that
to deserializeJson, or detect DeserializationError::NoMemory from err and handle
it (log and fall back to a streaming/iterative parse or return an error). Update
references to JsonDocument doc to the new StaticJsonDocument instance and keep
the existing deserialization and iteration logic (deserializeJson,
DeserializationError err, streamHttp.getStreamPtr(), doc.as<JsonArray>(), spool
loop) so overflow is caught and controlled.

…noJson

Single spool response is ~700 bytes — no need for a streaming parser.
Replaced 190-line hand-rolled json_reader with direct deserializeJson().

190 lines → 40 lines. ~40 branch points → ~8.
scanLoop() was ~215 lines with RF management, tag detection, classification
dispatch, and tag removal all inline. Now a 30-line main loop that calls:
- prepareRF() — watchdog checks, RF field management
- isSkippableDuplicate() — duplicate + write suppression check
- handleNewTag() — classify and dispatch to format handler
- handleTagAbsent() — clear state, send TAG_REMOVED

scanLoop complexity: ~45 → ~10 branch points.
…ing helper

- optMaterialToString() — switch extracted as static lookup, PA variants collapsed
- sendPrinterWarning() — shared message builder (was duplicated for type + temp)
- checkFilamentMismatch() flattened with early returns, 93 → 30 lines

Complexity: ~33 → ~10 branch points.
@sjordan0228 sjordan0228 merged commit 21aebc0 into dev Apr 7, 2026
1 check passed
@sjordan0228 sjordan0228 deleted the refactor/nfc-manager-complexity 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/XL Extra large change (500+ lines)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant