Custom Snapmaker U1 Firmware

Spoolman Integration Design

Spoolman Custom Fields

Entity Key Purpose
spool card_uids Comma-separated uppercase hex NFC UIDs associated with the spool
filament variant Filament subtype / variant (e.g. “Silk”, “Matte”)

Both fields use Spoolman’s double-serialised string convention: the inner value is JSON-encoded before storage, so a single UID "AABBCCDD" is stored as "\"AABBCCDD\"". spoollink decodes these on read and re-encodes on write.

card_uids Custom Field

NFC tag UIDs are stored in a Spoolman custom field named card_uids on the spool entity.

Field definition (created on first connection test)

{
  "key": "card_uids",
  "name": "Card UIDs",
  "entity_type": "spool",
  "field_type": "text",
  "order": 1,
  "default_value": "\"\""
}

Wire format

The extra.card_uids value is a JSON-encoded string — the string value itself is wrapped in JSON quotes by the client before sending, so it arrives as a JSON string containing another JSON string:

extra.card_uids = "\"AABBCCDD,11223344\""

When decoded, the inner string is a comma-separated list of uppercase hex UIDs:

AABBCCDD,11223344

Encoding rule

Before writing, the comma-separated UID string must be JSON-encoded:

jsonEncode("AABBCCDD,11223344") → "\"AABBCCDD,11223344\""

Decoding rule

On read, strip the outer JSON quotes if present; handle both encoded and raw forms:

raw = "\"AABBCCDD,11223344\""
decoded = AABBCCDD,11223344
uids = ["AABBCCDD", "11223344"]

PATCH body for card_uids

{
  "extra": {
    "card_uids": "\"AABBCCDD,11223344\""
  }
}

UID Format

NFC tag UIDs are formatted as uppercase hex with no separators:

AABBCCDD        (4-byte UID)
04A1B2C3D4E5F6  (7-byte UID)

Sync Logic (“Add to current, remove from others”)

When an NFC tag is scanned or assigned to a spool:

  1. Fetch the target spool via GET api/v1/spool/{id}.
  2. Append the tag UID to extra.card_uids if not already present.
  3. Write the updated UID list via PATCH api/v1/spool/{id}.
  4. Search for other spools that contain the same UID by fetching all spools (limit=1000&allow_archived=true) and filtering client-side on card_uids.
  5. Remove the UID from each other spool’s card_uids and write back via PATCH.

variant Custom Field (Filament)

The filament variant (e.g. “Silk”, “Matte”) is stored in a Spoolman custom field named variant on the filament entity.

Field definition (created on first connection test)

{
  "key": "variant",
  "name": "Variant",
  "entity_type": "filament",
  "field_type": "text",
  "order": 1,
  "default_value": "\"\""
}

The value follows the same JSON-encoded string convention as card_uids:

extra.variant = "\"Silk\""

Create Filament body with variant

{
  "name": "Galaxy Black Silk",
  "material": "PLA",
  "extra": {
    "variant": "\"Silk\""
  }
}

spoollink Component Flow

spoollink is a Moonraker component (moonraker/components/spoollink.py), loaded when a [spoollink] section is present in the Moonraker config. It runs in Moonraker’s event loop and uses the built-in http_client, klippy_apis, and spoolman components directly, so it does not maintain its own WebSocket connection.

  1. On construction, spoollink calls server.register_remote_method("spoollink_resolve_spool", ...); Moonraker re-registers it with Klipper on every Klippy ready, so the binding survives Klipper restarts.
  2. On startup (component_init), spoollink calls GET api/v1/field/spool and GET api/v1/field/filament to verify the card_uids and variant custom fields exist, creating them via POST api/v1/field/{entity}/{key} if missing.
  3. Klipper (or AFC) calls spoollink_resolve_spool with channel, spool_id, and/or card_uid.
  4. spoollink resolves the spool from Spoolman:
    • By ID: GET api/v1/spool/{id}
    • By card UID: GET api/v1/spool?limit=1000, then filter client-side on extra.card_uids (comma-separated, JSON-encoded uppercase hex UIDs).
  5. If both spool_id and card_uid are given and the card is not yet in the spool’s card_uids, spoollink appends it via PATCH api/v1/spool/{id} with {"extra": {"card_uids": "\"UID1,UID2\""}} (JSON-encoded string, as required by Spoolman’s custom field API). Any other spool that already carries the same UID has it removed via a subsequent PATCH — a UID can only belong to one spool at a time.
  6. On success, spoollink pushes the resolved filament info into Klipper by calling the spoollink/set endpoint (registered by the Klipper [spoollink] router), which merges it onto filament_protocol.FILAMENT_INFO_STRUCT and applies it to print_task_config.
  7. When SpoolLink force_generic_vendor is enabled, non-Snapmaker filament metadata sent to Klipper uses Generic as the vendor and clears the variant. The resolved Spoolman object, spool ID, card UID, cache data, active spool tracking, and usage reporting remain unchanged.
  8. Klipper stores the metadata in print_task_config and notifies subscribers. AFC_lane.get_status() surfaces spool_id to Fluidd/Mainsail.