feat(u1): Snapmaker U1 direct-mode integration#182
Conversation
Phase 1 of Snapmaker U1 direct-mode support. The scanner pushes scan results straight to the U1's external filament-detection endpoint exposed by paxx12's Extended Firmware. No middleware, no Klipper macros — a single HTTP POST per generic-UID scan. Scope (one scanner per toolhead, fixed-channel): - New web config section "Snapmaker U1 Integration" with Enable toggle + channel picker (0-3) and a hint link to the Extended Firmware repo. - New NVS keys u1_on / u1_channel; loaded with bounds clamp on read. - publishToU1() builds the U1 info schema (VENDOR / MAIN_TYPE / SUB_TYPE / RGB_1 / ALPHA / HOTEND_*_TEMP / BED_TEMP / CARD_UID), splits material names like "PLA Matte" on first space, parses #RRGGBB into the firmware's RGB_1 integer, and emits the spool UID as a byte array. - 30s reachability backoff after a transport failure plus 250ms mutex wait with skip-if-busy so an offline U1 doesn't block the dispatch loop on every scan. - Hostname auto-suggest: picking channel N on a default-hostname scanner (empty / "spoolsense" / "spoolsense-tN") flips to "spoolsense-tN" so multi-scanner deployments get unique mDNS names without thinking. - handleSpoolmanSynced gates the publish on TagKind::GenericUidTag so smart-tag formats (TigerTag, OT3D, OpenSpool, OPT) keep their existing flow — smart-tag U1 publish is Phase 2. Requires the U1 to have "Filament Detection: External" set in http://<printer-ip>/firmware-config/. Documented in .mex/context/direct-moonraker-mode.md. Builds clean on esp32dev, esp32s3zero, esp32c3, esp32s3devkitc.
…gment
Split the U1 direct-mode integration out of ApplicationManager into a
dedicated U1Manager singleton. Same external behavior, cleaner separation,
and now covers all 6 supported tag formats — not just generic UID.
Architecture:
ApplicationManager.handleSpoolDetected -> U1Manager.publishFromDetection
ApplicationManager.handleSpoolmanSynced -> U1Manager.publishFromSpoolmanSync
U1Manager (src/U1Manager.{h,cpp}) owns:
- U1FilamentInfo wire-format struct + builders
- per-material defaults (PLA/PETG/ABS/ASA/TPU/PVA/PC/PA hotend+bed)
- MAIN_TYPE/SUB_TYPE splitter (space + hyphen, e.g. "PETG-CF")
- HTTP POST to /printer/filament_detect/set
- 30s reachability backoff + 250ms mutex wait
- Pending-augment tracking (smart tag -> Spoolman fill-in)
Smart tag flow:
1. handleSpoolDetected -> publishFromDetection: builds info from on-tag
data (vendor, material, color, temps), applies per-material defaults
for any field that's still 0, POSTs immediately. Bambu, OpenSpool,
OPT, TigerTag, OT3D all flow through this path.
2. If the result is incomplete AND Spoolman is configured, registers a
pending augment. handleSpoolmanSynced then merges Spoolman fields
and re-POSTs only if Spoolman supplied something new.
Generic UID flow: single POST from publishFromSpoolmanSync(is_uid_lookup),
unchanged in spirit from the previous implementation.
MAIN_TYPE convention matches the firmware's own OpenSpool parser
(uppercase, e.g. "PLA"), so non-recognized variants like "PETG-CF" split
into MAIN_TYPE="PETG" + SUB_TYPE="CF" — gets firmware protocol mapping
for the base type plus descriptive sub-type display in Fluidd.
ApplicationManager shrinks by ~120 lines; U1 logic is now genuinely
separable (single file removal kills the feature cleanly).
Builds clean on esp32dev, esp32s3zero, esp32c3, esp32s3devkitc.
Address Ollama remote-review findings on U1Manager: 1. Switch the pending-augment UID compare from strcmp to strncmp bounded by the buffer size. Defensive against any producer of SpoolmanSyncedPayload that fails to null-terminate spool_id — strcmp would walk past the 17-byte buffer; strncmp can't. 2. Document the single-threaded-dispatch invariant in U1Manager.h. Both entry points are only called from ApplicationManager handlers, which run on the processMessages() loop. Internal state (moonrakerBackoffUntilMs_, pendingAugment_) is intentionally unguarded; cross-thread invocation would require revisiting both fields, not just adding atomics.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds U1 direct-mode integration including U1Manager for converting spool/NFC payloads to U1 format and posting to Moonraker, configuration persistence for U1 settings, ApplicationManager dispatch hooks for spool detection/sync events, and web UI controls for enabling/configuring U1. Changes
Sequence Diagram(s)sequenceDiagram
participant App as ApplicationManager
participant U1 as U1Manager
participant MR as Moonraker API
participant NVS as ConfigurationManager
Note over App,MR: On SPOOL_DETECTED Event
App->>App: Receive SPOOL_DETECTED message
App->>U1: publishFromDetection(SpoolDetectedPayload)
U1->>U1: Parse material, convert color, select temps
U1->>U1: Check if critical fields missing
alt Fields complete
U1->>MR: HTTP POST /printer/filament_detect/set
else Register pending augment
U1->>U1: Store pending augment (UID, TTL, missing fields)
end
Note over App,MR: On SPOOLMAN_SYNCED Event
App->>App: Receive SPOOLMAN_SYNCED message
App->>U1: publishFromSpoolmanSync(SyncPayload, SpoolState)
U1->>U1: Check for pending augment match on UID
alt Pending augment found with new data
U1->>U1: Merge augment fields with sync data
U1->>MR: HTTP POST merged detection
U1->>U1: Clear pending augment
else No pending augment
U1->>MR: HTTP POST direct detection
end
opt Moonraker transport error
U1->>U1: Set backoff window (30s)
U1->>U1: Suppress subsequent posts until expiry
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Review rate limit: 0/1 reviews remaining, refill in 35 minutes and 18 seconds.Comment |
There was a problem hiding this comment.
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 `@src/U1Manager.cpp`:
- Around line 174-203: buildFromSpoolmanSync currently ignores the existing
on-tag data in CurrentSpoolState and always builds a fresh U1FilamentInfo from
SpoolmanSyncedPayload, causing later POSTs to drop fields not provided by
Spoolman; change the function to start by initializing info from the
CurrentSpoolState (use the state's vendor/card_uid/material, rgb/alpha,
hotend/beds temps, etc.) and then overlay only the non-empty/non-zero fields
from SpoolmanSyncedPayload (e.g., if s.manufacturer[0] != '\0' overwrite vendor,
otherwise keep state vendor; if s.color_hex non-empty overwrite rgb_1/alpha else
keep state rgb/alpha; if s.extruder_temp>0 overwrite hotend_min/max else keep
state temps; same for bed_temp; for spool_id only packCardUid from s.spool_id
when present, otherwise preserve state.card_uid/card_uid_len); keep calling
splitMaterialName/hexColorToRgb1/packCardUid/applyMaterialDefaults as needed but
ensure state is used as the fallback source rather than being ignored.
- Around line 337-348: The UID-only publish path initiated when
sync.is_uid_lookup posts Spoolman results without verifying the reader still has
the same generic UID tag; before calling
buildFromSpoolmanSync/postFilamentDetectSet, check the current NFC reader state
(state.present, state.kind, and state.spool_id) matches the expected generic UID
tag and the same spool ID from sync (mirror the guard used in the NFC writeback
path) and only proceed with the POST if those checks pass; otherwise skip the
publish to avoid sending stale filament info.
- Around line 357-359: The UID mismatch branch in the sync handler is
incorrectly clearing the current pending augment; instead of setting
pendingAugment_.active = false when strncmp(pendingAugment_.uid, sync.spool_id,
...) != 0, leave pendingAugment_ untouched and simply return so unrelated/late
SPOOLMAN_SYNCED events are ignored; update the mismatch branch in the function
handling sync results (the code that compares pendingAugment_.uid to
sync.spool_id) to remove or comment out the line that clears
pendingAugment_.active and ensure only a matching UID or explicit expiration
path can deactivate the pending augment.
🪄 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: d493d5d0-cf51-4196-8406-249eaf49afb8
📒 Files selected for processing (7)
src/ApplicationManager.cppsrc/ConfigHTML.hsrc/ConfigurationManager.cppsrc/ConfigurationManager.hsrc/U1Manager.cppsrc/U1Manager.hsrc/WebServerManager.cpp
…-present + don't kill late augments Three CodeRabbit findings on the augment + UID-lookup paths: 1. publishFromSpoolmanSync no longer rebuilds U1FilamentInfo from scratch for smart tags. PendingAugment now caches the exact wire-format struct that POST 1 sent; POST 2 starts from that cache and overlays only non-empty Spoolman fields via overlaySpoolmanFields(). On-tag vendor/material/temps are preserved when Spoolman has gaps. The dropped (void)state comment masked a real regression where sparse Spoolman entries clobbered rich on-tag data with material defaults. 2. Generic-UID-tag publish (is_uid_lookup) now verifies the reader still holds the same tag before posting. If the user removed the tag during the Spoolman lookup (1-3s WiFi roundtrip is enough to fall behind a user's intent), we skip the POST instead of writing stale info to the U1 channel. Mirrors the writeback guard already in handleSpoolmanSynced for the NFC-writeback path. 3. Late-arriving sync for a previous tag no longer wipes the current pendingAugment. Scanning two smart tags within ~3 seconds was sabotaging tag B's augment because tag A's late SPOOLMAN_SYNCED would hit the mismatch branch and clear pendingAugment_.active. Now the mismatch branch just returns; only matching UID consumption or expiry can deactivate the augment. Refactor: - U1FilamentInfo moved from U1Manager.cpp anonymous namespace to U1Manager.h so PendingAugment can hold a copy. Type stays implementation-detail in spirit — only U1Manager.cpp references it. - want* flags removed from PendingAugment in favor of the cached struct; overlaySpoolmanFields() decides what to overwrite by comparing values. - buildFromSpoolmanSync simplified — no longer takes CurrentSpoolState; for the generic-UID path Spoolman is the only data source, and the smart-tag augment path doesn't use it (uses postedInfo + overlay). Builds clean on esp32dev, esp32s3zero, esp32c3, esp32s3devkitc.
Summary
Phase 1 of Snapmaker U1 direct-mode integration. The scanner pushes scan results straight to the U1's external filament-detection endpoint (
/printer/filament_detect/set) provided by paxx12's Extended Firmware. No middleware, no Klipper macros — a single HTTP POST per scan.Closes part of #98 (the U1-specific path).
How it works
handleSpoolDetected(no Spoolman dependency)handleSpoolmanSyncedafter the lookupRequired printer setup
Documented in
.mex/context/direct-moonraker-mode.mdand on the new web config section. End-user steps:http://<printer-ip>/firmware-config/and set Filament Detection → ExternalNo
.cfgmacro files required on the printer. No[gcode_macro]or[delayed_gcode]blocks. The endpoint is built into the extended firmware.What's in this PR
src/U1Manager.{h,cpp}src/ApplicationManager.{cpp,h}src/ConfigurationManager.{cpp,h}u1_on,u1_channel(with bounds clamp on read)src/ConfigHTML.hsrc/WebServerManager.cpp/api/configGET/POSTChannel selection — Phase 1 scope
This PR ships fixed-channel mode only (Argus parity): each scanner is bound to one channel via NVS. Buy 1 scanner for single-color use, up to 4 for a fully-loaded U1.
Phase 2 (later PR) will add dynamic-channel mode: one scanner serves all 4 channels with keypad / web-UI / motion-sensor disambiguation. Tracked in #98 and the design doc.
Schema / firmware contract
The U1's
infopayload shape is defined in paxx-u1/docs/design/filament_detect.md and verified against the firmware patch (13-patch-rfid/patches/05-add-filament-detect-set-endpoint.patch). We match its OpenSpool parser conventions (uppercase MAIN_TYPE, RGB_1 asR<<16|G<<8|Bint, ALPHA default 255).Build status
Clean across all 4 envs:
Reviews so far
968728awithstrncmpbound + threading-invariant commentTest plan
Hardware test on a real U1 with extended firmware:
Phase 2 work (dynamic channel selection, motion-sensor auto-pick) is out of scope here — separate PR.
Summary by CodeRabbit