Skip to content

feat(u1): Snapmaker U1 direct-mode integration#182

Merged
sjordan0228 merged 4 commits into
devfrom
feature/u1-direct-mode
Apr 29, 2026
Merged

feat(u1): Snapmaker U1 direct-mode integration#182
sjordan0228 merged 4 commits into
devfrom
feature/u1-direct-mode

Conversation

@sjordan0228
Copy link
Copy Markdown
Contributor

@sjordan0228 sjordan0228 commented Apr 29, 2026

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

SpoolSense Scanner (any tag format) → /printer/filament_detect/set → U1 channel state
  • One scanner per toolhead (channel 0–3 stored in NVS, set per-scanner via the web config page)
  • Works with all 6 supported tag formats: OpenSpool, OpenPrintTag, TigerTag, OpenTag3D, Bambu UID, NFC+
  • Smart tags publish on detection from handleSpoolDetected (no Spoolman dependency)
  • Generic UID tags publish from handleSpoolmanSynced after the lookup
  • Smart tags missing fields (e.g. OpenSpool tags don't carry bed temp) get filled by:
    1. Per-material defaults (PLA→60°C bed, PETG→70, ABS→100, etc.)
    2. Spoolman augment when configured — second POST only fires if Spoolman supplied something the on-tag data missed

Required printer setup

Documented in .mex/context/direct-moonraker-mode.md and on the new web config section. End-user steps:

  1. Install paxx12 Extended Firmware (develop) on the U1
  2. Open http://<printer-ip>/firmware-config/ and set Filament DetectionExternal
  3. On the SpoolSense scanner config page, enable Snapmaker U1 Integration and pick a channel
  4. Scan a tag — the U1 sees the data over the documented HTTP API

No .cfg macro 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

File Change
src/U1Manager.{h,cpp} New singleton — JSON shaping, per-material defaults, MAIN_TYPE/SUB_TYPE splitter, HTTP POST, backoff, pending-augment tracking
src/ApplicationManager.{cpp,h} Two delegating call sites, U1 logic out (-129 lines net)
src/ConfigurationManager.{cpp,h} New NVS keys: u1_on, u1_channel (with bounds clamp on read)
src/ConfigHTML.h New "Snapmaker U1 Integration" section, hostname auto-suggest when channel changes
src/WebServerManager.cpp Round-trip the new config fields through /api/config GET/POST

Channel 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 info payload 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 as R<<16|G<<8|B int, ALPHA default 255).

Build status

Clean across all 4 envs:

Env RAM Flash
esp32dev 20.0% 85.0%
esp32s3zero 19.6% 82.6%
esp32c3 17.7% 83.5%
esp32s3devkitc 19.7% 82.8%

Reviews so far

  • Sonnet self-review pre-push
  • Ollama (qwen3-coder:30b) review on first commit caught a buffer-bound concern on the UID compare — fixed in 968728a with strncmp bound + threading-invariant comment

Test plan

Hardware test on a real U1 with extended firmware:

  • Flash extended firmware develop branch on the U1
  • Set Filament Detection: External in firmware-config
  • Flash this PR's firmware on a SpoolSense scanner
  • Enable U1 Integration in scanner config, pick channel 0, save
  • Scan an OpenSpool tag → verify Fluidd shows correct vendor/material/color/temps on channel 0
  • Scan a TigerTag → verify all fields populate correctly
  • Scan a generic NFC+ tag (Spoolman lookup) → verify channel 0 updates
  • Scan an OpenSpool PLA tag (no bed temp on tag) → verify BED_TEMP defaults to 60°C from the per-material table
  • Disconnect U1 / wrong moonraker URL → verify scanner still functions, doesn't block dispatch (30s backoff trips)
  • Re-enable U1 → verify next scan publishes successfully
  • (If 4 scanners available) verify per-scanner channel binding — each posts to its own channel, no cross-talk

Phase 2 work (dynamic channel selection, motion-sensor auto-pick) is out of scope here — separate PR.

Summary by CodeRabbit

  • New Features
    • Added Snapmaker U1 integration support with full configuration interface
    • New U1 settings panel including enable/disable toggle and toolhead channel selector
    • Filament detection data automatically published to Snapmaker U1 system on tag detection
    • Support for smart-tag augmentation for enhanced filament data synchronization
    • Integration with Spoolman for comprehensive filament tracking updates

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.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 29, 2026

Warning

Rate limit exceeded

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

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 @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: 0ce7d9fa-03c5-4b64-85d8-3afc429cd929

📥 Commits

Reviewing files that changed from the base of the PR and between 968728a and 1c05c96.

📒 Files selected for processing (2)
  • src/U1Manager.cpp
  • src/U1Manager.h
📝 Walkthrough

Walkthrough

Adds 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

Cohort / File(s) Summary
U1Manager Implementation
src/U1Manager.h, src/U1Manager.cpp
New U1Manager singleton with publishFromDetection and publishFromSpoolmanSync methods. Converts spool/NFC payloads to U1 wire format (material parsing, color conversion, temperature selection), handles HTTP POST to Moonraker with global semaphore and backoff logic, and manages pending augment state for smart-tag follow-up publishing.
ApplicationManager U1 Hooks
src/ApplicationManager.cpp
Integrates U1Manager dispatch calls on SPOOL_DETECTED (triggers publishFromDetection) and SPOOLMAN_SYNCED (triggers publishFromSpoolmanSync). U1 calls excluded from NATIVE_TEST builds via preprocessor guard.
Configuration Management
src/ConfigurationManager.h, src/ConfigurationManager.cpp
Extends ConfigUpdate struct and ConfigurationManager class with u1_enabled and u1_channel fields. Adds NVS persistence, range validation (0-3 for channel), and public getters (isU1Enabled, getU1Channel).
Web UI and Server
src/ConfigHTML.h, src/WebServerManager.cpp
Adds U1 configuration UI (enable checkbox, channel selector with dynamic visibility). Updates /api/config GET/POST to expose and persist u1_enabled and u1_channel with input validation and default handling.

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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested labels

size/L

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.28% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding Snapmaker U1 direct-mode integration to the scanner firmware.
Description check ✅ Passed The description is thorough and well-structured. It includes a clear summary, detailed explanation of how it works, required setup, PR contents, schema documentation, build status, test plan, and phase-2 scope. All required template sections are present and completed.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 feature/u1-direct-mode

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
Review rate limit: 0/1 reviews remaining, refill in 35 minutes and 18 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added the size/XL Extra large change (500+ lines) label Apr 29, 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: 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

📥 Commits

Reviewing files that changed from the base of the PR and between cca58b8 and 968728a.

📒 Files selected for processing (7)
  • src/ApplicationManager.cpp
  • src/ConfigHTML.h
  • src/ConfigurationManager.cpp
  • src/ConfigurationManager.h
  • src/U1Manager.cpp
  • src/U1Manager.h
  • src/WebServerManager.cpp

Comment thread src/U1Manager.cpp Outdated
Comment thread src/U1Manager.cpp
Comment thread src/U1Manager.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.
@sjordan0228 sjordan0228 merged commit 48459ba into dev Apr 29, 2026
2 checks passed
@sjordan0228 sjordan0228 deleted the feature/u1-direct-mode branch May 10, 2026 22:43
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