Files
s1d3sw1ped b67b37dc59 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).
2026-08-23 15:27:11 -05:00

110 lines
2.6 KiB
Go

package bambunfc
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"math"
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
)
const BrandBambuLab = "Bambu Lab"
// Tag is one Bambu sticker parsed from a raw MIFARE Classic 1K image.
type Tag struct {
Brand string
ChipUID string // lowercase hex of the ISO UID
TrayUID string // 32 lowercase hex chars (block 9)
Type string
Material string
MaterialID string
VariantID string
Color Color
WeightG int
DiameterMM float32
LengthM int
MinHotendC int
MaxHotendC int
BedC int
DryC int
DryHours int
Produced string
SpoolWidthRaw uint16
}
func block(image []byte, n int) []byte {
off := n * mifare.BlockSize
return image[off : off+mifare.BlockSize]
}
func cstr(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
b = b[:i]
}
return string(b)
}
func allZero(b []byte) bool {
for _, v := range b {
if v != 0 {
return false
}
}
return true
}
// 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
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))
}
b1 := block(image, 1)
b2 := block(image, 2)
b4 := block(image, 4)
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)
}
return Tag{
Brand: BrandBambuLab,
ChipUID: hex.EncodeToString(uid),
VariantID: cstr(b1[0:8]),
MaterialID: cstr(b1[8:16]),
Material: material,
Type: typ,
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])),
SpoolWidthRaw: binary.LittleEndian.Uint16(b10[4:6]),
Produced: cstr(b12),
LengthM: int(binary.LittleEndian.Uint16(b14[4:6])),
}, nil
}