Skip to content
Closed
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
82 changes: 82 additions & 0 deletions docs/rfid_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,88 @@ Use the **NFC Tools** app (iOS/Android) to inspect tags:
**Compatible tag types:** NTAG213/215/216, Mifare Classic 1K
**Note:** ISO15693 tags (OpenPrintTag) are not supported

## G-code Commands for Tag Management

The extended firmware includes commands to read, write, and update RFID tags directly from G-code.

### FILAMENT_TAG_WRITE - Write Raw NDEF Data to NTAG Tag

Write pre-encoded NDEF binary data to an NTAG tag. The NDEF payload is generated client-side (e.g., by the RFID Manager web UI library) and sent as URL-safe base64.

**Syntax:**
```gcode
FILAMENT_TAG_WRITE [CHANNEL=<0-3>] DATA=<base64>
```

**Parameters:**
- `CHANNEL` - Filament channel (0-3, default: 0)
- `DATA` - Tag data starting from page 4 (TLV + payload, no CC) as URL-safe base64 encoded string.

**Example:**
```gcode
FILAMENT_TAG_WRITE CHANNEL=0 DATA=AxISUBBhcHBsaWNhdGlvbi9qc29u...
```

**Safety:**
- Only works with NTAG tags. Will reject M1 (Snapmaker) tags to prevent corruption.
- Automatically writes CC bytes (page 3) if they are blank or correctable. CC is OTP (One-Time Programmable) — bits can only be set (0→1), never cleared. If the existing CC has extra bits set that would prevent reaching the correct value, a warning is returned and the CC write is skipped. Fresh blank tags are initialized automatically.

**Typical usage:** The RFID Manager web UI handles encoding automatically. This command is the low-level transport mechanism.

### FILAMENT_TAG_ERASE - Erase NTAG Tag

Erase all data from an NTAG tag to prepare it for reprogramming.

**Syntax:**
```gcode
FILAMENT_TAG_ERASE [CHANNEL=<0-3>] CONFIRM=1
```

**Parameters:**
- `CHANNEL` - Filament channel (0-3, default: 0)
- `CONFIRM` - Must be set to 1 to confirm erase operation (required)

**Example:**
```gcode
FILAMENT_TAG_ERASE CHANNEL=0 CONFIRM=1
```

**Safety:**
- Only works with NTAG tags (will reject M1 tags)
- Requires CONFIRM=1 parameter to prevent accidental erasure
- Writes CC bytes (page 3) if blank or correctable, then writes empty NDEF TLV at page 4. This ensures fresh blank tags become NDEF-valid after erase.

## Web UI - RFID Tag Manager

The extended firmware includes a web-based RFID Tag Manager accessible at `/rfid/` (e.g., `http://yourprinter.local/rfid/`).

### Features

- **Visual tag status** - See all 4 extruders at a glance
- **Create tags** - Write new NTAG tags with OpenSpool format
- **Update tags** - Modify existing NTAG tag data
- **Export tags** - Export existing tag data in json format
- **Import tags** - Import json tag data to selected extruder
- **Erase tags** - Clear NTAG tags for reprogramming (not needed if updating a tag)
- **Real-time updates** - Automatic status refresh via Moonraker websocket
- **Color picker** - Visual color selection with transparency support
- **Multicolor support** - Add up to 5 colors for rainbow/multicolor filament

### Architecture

The web UI encodes NDEF data client-side using the [PrintTag-Web](https://github.com/paxx12/PrintTag-Web) library, then sends the raw bytes to Klipper for writing:

```
Web UI (OpenSpool JSON → NDEF encode) → Moonraker Websocket → Klipper FILAMENT_TAG_WRITE → FM175XX RFID reader → NTAG tag
```

**Static file serving** - UI files served by nginx from `/home/lava/www/rfid-manager/` via `/rfid/` location alias.

### Supported Tags

- ✅ **NTAG215/216** - Full read/write support
- ❌ **Mifare Classic 1K** - Read-only (cannot write M1 tags)

## Troubleshooting

**Tag not detected:**
Expand Down
8 changes: 6 additions & 2 deletions overlays/firmware-extended/30-rfid-support/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# 13-rfid-support (Internal API)
# 30-rfid-support (Internal API)

This overlay extends U1 RFID behavior in Klipper extras and is intended as an **internal integration API**.

## Scope

Patch set in `overlays/firmware-extended/13-rfid-support/patches`:
Patch set in `overlays/firmware-extended/30-rfid-support/patches`:

- `01-add-ntag215-support.patch`
- Extends `fm175xx_reader.py` card handling for NTAG cards.
Expand All @@ -21,6 +21,10 @@ Patch set in `overlays/firmware-extended/13-rfid-support/patches`:
- `cmd_FILAMENT_DT_SELF_TEST`: raises early error when reader is disabled.
- `05-add-filament-detect-set-endpoint.patch`
- Adds webhook endpoint `filament_detect/set`.
- `06-add-ntag-write-support.patch`
- Extends `fm175xx_reader.py` with NTAG write support (`FILAMENT_TAG_WRITE`, `FILAMENT_TAG_ERASE`).
- `07-add-card-type-tracking.patch`
- Extends `filament_detect.py` to track card type (NTAG213/215/216) for write size validation.

## API Contract

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
Including: home/lava/klipper/klippy/extras/fm175xx_reader.py
--- rootfs.original/home/lava/klipper/klippy/extras/fm175xx_reader.py 2026-01-15 02:56:33.270987293 +0000
+++ rootfs/home/lava/klipper/klippy/extras/fm175xx_reader.py 2026-01-15 02:54:53.191709974 +0000
@@ -920,6 +920,140 @@
ret.out_param = card_data_tmp
return ret

+ # Reader-A: NTAG/Ultralight, write a page (4 bytes)
+ def __reader_a_ntag_page_write(self, page:int, data:list) -> int:
+ ret = 0
+ outbuf = [0] * 6
+ inbuf = [0] * 1
+ cmd = Fm175xxCmdMetaData()
+
+ if len(data) != 4:
+ logging.error("NTAG page write requires exactly 4 bytes, got %d", len(data))
+ return FM175XX_CARD_COMM_ERR
+
+ cmd.send_crc_en = FM175XX_SET
+ cmd.recv_crc_en = FM175XX_RESET
+ cmd.send_buff = outbuf
+ cmd.recv_buff = inbuf
+ cmd.send_buff[0] = 0xA2 # WRITE command for NTAG
+ cmd.send_buff[1] = page
+ cmd.send_buff[2] = data[0]
+ cmd.send_buff[3] = data[1]
+ cmd.send_buff[4] = data[2]
+ cmd.send_buff[5] = data[3]
+ cmd.bytes_to_send = 6
+ cmd.bits_to_send = 0
+ cmd.bits_to_recv = 0
+ cmd.bytes_to_recv = 1
+ cmd.timeout = 10
+ cmd.cmd = FM175XX_CMD_TRANSCEIVE
+ result = self.__command_exe(cmd)
+ ret = result.err_code
+
+ if (result.err_code != FM175XX_OK) or (result.out_param.bits_recved != 4) or (result.out_param.recv_buff[0] & 0x0F != 0x0A):
+ if (result.err_code == FM175XX_OK):
+ ret = FM175XX_CARD_COMM_ERR
+ logging.error("NTAG page write ACK error: bits=%d, ack=0x%02X",
+ result.out_param.bits_recved, result.out_param.recv_buff[0] if result.out_param.bits_recved > 0 else 0)
+
+ return ret
+
+ # Reader-A: NTAG215, write user data area
+ # Page 3 (CC) is OTP - never write to it from this firmware.
+ # Page 4+ contains TLV records (NDEF data)
+ def __reader_a_ntag215_write_user_data(self, data:list, start_page=4, retry_times=3) -> int:
+ if len(data) > (FM175XX_NTAG215_USER_END_PAGE - start_page + 1) * FM175XX_NTAG215_BYTES_PER_PAGE:
+ logging.error("NTAG write data too large: %d bytes", len(data))
+ return FM175XX_CARD_WRITE_ERR
+
+ # Calculate number of pages to write
+ num_pages = (len(data) + FM175XX_NTAG215_BYTES_PER_PAGE - 1) // FM175XX_NTAG215_BYTES_PER_PAGE
+
+ for page_offset in range(num_pages):
+ page_no = start_page + page_offset
+ if page_no > FM175XX_NTAG215_USER_END_PAGE:
+ break
+
+ # Extract 4 bytes for this page
+ offset = page_offset * FM175XX_NTAG215_BYTES_PER_PAGE
+ page_data = data[offset : offset + FM175XX_NTAG215_BYTES_PER_PAGE]
+ page_data += [0] * (4 - len(page_data)) # Pad if needed
+
+ # Retry write
+ for retry in range(retry_times):
+ ret = self.__reader_a_ntag_page_write(page_no, page_data)
+ if ret == FM175XX_OK:
+ break
+ if ret != FM175XX_OK:
+ logging.error("NTAG page %d write failed after %d retries", page_no, retry_times)
+ return FM175XX_CARD_WRITE_ERR
+
+ return FM175XX_OK
+
+ def write_ntag_data(self, channel, data, start_page=4, retry_times=3):
+ """
+ Write data to NTAG tag on specified channel.
+ This is a synchronous blocking operation that:
+ 1. Selects the channel
+ 2. Initializes and activates the reader
+ 3. Writes the data starting at start_page (default: page 4)
+ Note: Page 3 (CC) is OTP on NTAG tags - do not write to it.
+ Returns: FM175XX_OK on success, error code on failure
+ """
+ if channel < 0 or channel >= FM175XX_CHANNEL_NUMS:
+ logging.error("Invalid channel %d for NTAG write", channel)
+ return FM175XX_PARAM_ERR
+
+ if not data or len(data) == 0:
+ logging.error("No data provided for NTAG write")
+ return FM175XX_PARAM_ERR
+
+ # Select channel and reader object (call sync versions directly)
+ self.__select_fm175xx_obj(channel)
+ self.__do_select_channel(channel)
+ time.sleep(0.2) # Allow hardware to settle
+
+ for retry in range(retry_times):
+ # Initialize reader
+ self.__picc_a.reset()
+ self.__reader_a_init()
+
+ # Enable carrier wave
+ self.__set_carrier_wave(FM175XX_CW_ENABLE)
+
+ # Activate the card
+ ret = FM175XX_ERR
+ for activate_retry in range(3):
+ ret = self.__reader_a_activate()
+ if ret == FM175XX_OK:
+ break
+
+ if ret != FM175XX_OK:
+ logging.warning("NTAG activate failed on retry %d, ret=%d", retry, ret)
+ self.__set_carrier_wave(FM175XX_CW_DISABLE)
+ continue
+
+ # Verify it's an NTAG card (SAK = 0x00)
+ if self.__picc_a.SAK[self.__picc_a.CASCADE_LEVEL] != FM175XX_MIFARE_CARD_TYPE_NTAG:
+ logging.error("Card is not NTAG type (SAK=0x%02X)", self.__picc_a.SAK[self.__picc_a.CASCADE_LEVEL])
+ self.__set_carrier_wave(FM175XX_CW_DISABLE)
+ return FM175XX_CARD_ACTIVATE_ERR
+
+ # Write the data
+ ret = self.__reader_a_ntag215_write_user_data(data, start_page=start_page)
+
+ # Disable carrier wave
+ self.__set_carrier_wave(FM175XX_CW_DISABLE)
+
+ if ret == FM175XX_OK:
+ logging.info("NTAG write successful on channel %d", channel)
+ return FM175XX_OK
+
+ logging.warning("NTAG write failed on retry %d, ret=%d", retry, ret)
+
+ logging.error("NTAG write failed after %d retries", retry_times)
+ return FM175XX_CARD_WRITE_ERR
+
# Reader-A: M1, read all data
def __reader_a_m1_read_all_data(self, uid:list, auth_mode:int, auth_key:list, retry_times = 3) -> Fm175xxReturnVal:
ret = Fm175xxReturnVal()
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
Including: home/lava/klipper/klippy/extras/filament_detect.py
--- rootfs.original/home/lava/klipper/klippy/extras/filament_detect.py 2026-01-19 20:46:51.443456566 +0000
+++ rootfs/home/lava/klipper/klippy/extras/filament_detect.py 2026-02-08 22:00:00.000000000 +0000
@@ -123,6 +123,7 @@
if (error == filament_protocol.FILAMENT_PROTO_OK):
logging.info("channel[%d] m1 parse ok....", channel)
filament_info = info
+ filament_info['CARD_TYPE'] = 'M1'
else:
logging.error("channel[%d] m1 parse err: %d", channel, error)
elif fm175xx_reader.FM175XX_MIFARE_CARD_TYPE_NTAG == card_type and fm175xx_reader.FM175XX_OK == result:
@@ -131,8 +132,13 @@
if (error == filament_protocol.FILAMENT_PROTO_OK):
logging.info("channel[%d] NDEF parse ok....", channel)
filament_info = info
+ filament_info['CARD_TYPE'] = 'NTAG215'
else:
- logging.error("channel[%d] NDEF parse err....", channel)
+ logging.info("channel[%d] NDEF parse err, using partial info with UID....", channel)
+ # Use partial info (contains UID even on parse errors)
+ if info is not None:
+ filament_info = info
+ filament_info['CARD_TYPE'] = 'NTAG215'
else:
is_clear = True

Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash

if [[ -z "$CREATE_FIRMWARE" ]]; then
echo "Error: This script should be run within the create_firmware.sh environment."
exit 1
fi

if [[ $# -ne 1 ]]; then
echo "Usage: $0 <rootfs-dir>"
exit 1
fi

set -eo pipefail

PRINTTAG_WEB_URL=https://github.com/paxx12/PrintTag-Web.git
PRINTTAG_WEB_SHA=2e2d83139d6818365b4b66a8c891ba0b461a63f2

# Pin exact versions for reproducible builds.
PICKR_VERSION=1.9.1
PICKR_CSS_FILENAME=pickr-${PICKR_VERSION}-nano.min.css
PICKR_JS_FILENAME=pickr-${PICKR_VERSION}.min.js
PICKR_CSS_URL=https://cdn.jsdelivr.net/npm/@simonwep/pickr@${PICKR_VERSION}/dist/themes/nano.min.css
PICKR_JS_URL=https://cdn.jsdelivr.net/npm/@simonwep/pickr@${PICKR_VERSION}/dist/pickr.min.js
PICKR_CSS_SHA256=cb9f82b125cc07d58bc12aac6e936f8582751c56fed3353b1d1310cf76a67a4b
PICKR_JS_SHA256=f42fb8ba223e1283a68b17b9b510fc8738977ed680e6506155e1796e3bedaa46

DEST="$1/home/lava/www/rfid-manager/lib"

echo ">> Cloning PrintTag-Web at $PRINTTAG_WEB_SHA..."
cache_git.sh "$CACHE_DIR/PrintTag-Web" "$PRINTTAG_WEB_URL" "$PRINTTAG_WEB_SHA"

echo ">> Installing PrintTag-Web JS libraries..."
mkdir -p "$DEST/PrintTag-Web"
cp "$CACHE_DIR/PrintTag-Web/public/ndef.js" "$DEST/PrintTag-Web/"
cp "$CACHE_DIR/PrintTag-Web/public/openspool.js" "$DEST/PrintTag-Web/"

echo ">> Downloading Pickr ${PICKR_VERSION}..."
cache_file.sh "$CACHE_DIR/$PICKR_CSS_FILENAME" "$PICKR_CSS_URL" "$PICKR_CSS_SHA256"
cache_file.sh "$CACHE_DIR/$PICKR_JS_FILENAME" "$PICKR_JS_URL" "$PICKR_JS_SHA256"

echo ">> Installing Pickr ${PICKR_VERSION}..."
mkdir -p "$DEST/pickr"
cp "$CACHE_DIR/$PICKR_CSS_FILENAME" "$DEST/pickr/nano.min.css"
cp "$CACHE_DIR/$PICKR_JS_FILENAME" "$DEST/pickr/pickr.min.js"
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# RFID Manager UI
location = /rfid {
return 301 /rfid/;
}

location /rfid/ {
alias /home/lava/www/rfid-manager/;
index index.html;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# NTAG tag write/erase operations
# Provides gcode commands for writing and erasing NTAG filament tags

[filament_tag]
Loading