Make Parse take raw NFC UID and 1K image.

Drop zip/JSON from the root API. dump remains an adapter for the Android app format; inventory and an AMS wand both call Parse(uid, image).
This commit is contained in:
2026-08-23 15:27:11 -05:00
parent b38f7f643b
commit b67b37dc59
7 changed files with 110 additions and 169 deletions
+33 -80
View File
@@ -5,24 +5,18 @@ import (
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"math"
"strings"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
)
const BrandBambuLab = "Bambu Lab"
// requiredSectors must not appear in SectorsFail (blocks 02, 46, 89).
var requiredSectors = []int{0, 1, 2}
// Tag is one Bambu sticker after parsing the 1K image.
// Tag is one Bambu sticker parsed from a raw MIFARE Classic 1K image.
type Tag struct {
Brand string
ChipUID string
TrayUID string
ChipUID string // lowercase hex of the ISO UID
TrayUID string // 32 lowercase hex chars (block 9)
Type string
Material string
MaterialID string
@@ -40,14 +34,6 @@ type Tag struct {
SpoolWidthRaw uint16
}
func failedSet(fail []int) map[int]bool {
s := make(map[int]bool, len(fail))
for _, n := range fail {
s[n] = true
}
return s
}
func block(image []byte, n int) []byte {
off := n * mifare.BlockSize
return image[off : off+mifare.BlockSize]
@@ -60,34 +46,24 @@ func cstr(b []byte) string {
return string(b)
}
func normalizeUID(uid string) (string, error) {
uid = strings.ToLower(strings.TrimSpace(uid))
if uid == "" || len(uid)%2 != 0 {
return "", fmt.Errorf("%w: %q", ErrUID, uid)
func allZero(b []byte) bool {
for _, v := range b {
if v != 0 {
return false
}
}
if _, err := hex.DecodeString(uid); err != nil {
return "", fmt.Errorf("%w: %v", ErrUID, err)
}
return uid, nil
return true
}
// ParseImage parses a Bambu MIFARE Classic 1K image.
// fail is the dump's sectors_fail list (sector indexes 015).
func ParseImage(chipUID string, image []byte, fail []int) (Tag, error) {
// Parse reads a Bambu tag from raw NFC: ISO chip UID and the 1024-byte Classic 1K image.
func Parse(uid, image []byte) (Tag, error) {
var zero Tag
uid, err := normalizeUID(chipUID)
if err != nil {
return zero, err
if len(uid) == 0 {
return zero, fmt.Errorf("%w: empty", ErrUID)
}
if len(image) != mifare.Size1K {
return zero, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image))
}
failed := failedSet(fail)
for _, s := range requiredSectors {
if failed[s] {
return zero, fmt.Errorf("%w: sector %d", ErrIncomplete, s)
}
}
b1 := block(image, 1)
b2 := block(image, 2)
@@ -95,16 +71,22 @@ func ParseImage(chipUID string, image []byte, fail []int) (Tag, error) {
b5 := block(image, 5)
b6 := block(image, 6)
b9 := block(image, 9)
b10 := block(image, 10)
b12 := block(image, 12)
b14 := block(image, 14)
material := cstr(b2)
typ := cstr(b4)
if material == "" && typ == "" {
return zero, ErrNotBambu
}
if allZero(b9) {
return zero, fmt.Errorf("%w: tray uid", ErrIncomplete)
}
tag := Tag{
return Tag{
Brand: BrandBambuLab,
ChipUID: uid,
ChipUID: hex.EncodeToString(uid),
VariantID: cstr(b1[0:8]),
MaterialID: cstr(b1[8:16]),
Material: material,
@@ -112,45 +94,16 @@ func ParseImage(chipUID string, image []byte, fail []int) (Tag, error) {
Color: Color{
R: b5[0], G: b5[1], B: b5[2], A: b5[3],
},
WeightG: int(binary.LittleEndian.Uint16(b5[4:6])),
DiameterMM: math.Float32frombits(binary.LittleEndian.Uint32(b5[8:12])),
TrayUID: hex.EncodeToString(b9),
DryC: int(binary.LittleEndian.Uint16(b6[0:2])),
DryHours: int(binary.LittleEndian.Uint16(b6[2:4])),
BedC: int(binary.LittleEndian.Uint16(b6[6:8])),
MaxHotendC: int(binary.LittleEndian.Uint16(b6[8:10])),
MinHotendC: int(binary.LittleEndian.Uint16(b6[10:12])),
}
if !failed[2] {
b10 := block(image, 10)
tag.SpoolWidthRaw = binary.LittleEndian.Uint16(b10[4:6])
}
if !failed[3] {
tag.Produced = cstr(block(image, 12))
b14 := block(image, 14)
tag.LengthM = int(binary.LittleEndian.Uint16(b14[4:6]))
}
return tag, nil
}
// ParseDump parses one dump JSON record as a Bambu tag.
func ParseDump(d dump.Dump) (Tag, error) {
return ParseImage(d.ChipUID, d.Image, d.SectorsFail)
}
// ParseZip reads a filanfc-dump zip and parses every sticker as Bambu.
func ParseZip(r io.Reader) ([]Tag, error) {
dumps, err := dump.ParseZip(r)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrInvalidDump, err)
}
tags := make([]Tag, 0, len(dumps))
for _, d := range dumps {
tag, err := ParseDump(d)
if err != nil {
return nil, fmt.Errorf("chip %s: %w", d.ChipUID, err)
}
tags = append(tags, tag)
}
return tags, nil
WeightG: int(binary.LittleEndian.Uint16(b5[4:6])),
DiameterMM: math.Float32frombits(binary.LittleEndian.Uint32(b5[8:12])),
TrayUID: hex.EncodeToString(b9),
DryC: int(binary.LittleEndian.Uint16(b6[0:2])),
DryHours: int(binary.LittleEndian.Uint16(b6[2:4])),
BedC: int(binary.LittleEndian.Uint16(b6[6:8])),
MaxHotendC: int(binary.LittleEndian.Uint16(b6[8:10])),
MinHotendC: int(binary.LittleEndian.Uint16(b6[10:12])),
SpoolWidthRaw: binary.LittleEndian.Uint16(b10[4:6]),
Produced: cstr(b12),
LengthM: int(binary.LittleEndian.Uint16(b14[4:6])),
}, nil
}