Files
bambu-nfc/parse.go
T
s1d3sw1ped b38f7f643b Add bambu-nfc dump parse, Key-A HKDF, and spool merge.
Parse filanfc-dump/v1 zip/JSON, Bambu 1K images, and merge stickers by tray UID. dump and mifare packages stay importable for a later common library.
2026-08-23 15:23:11 -05:00

157 lines
3.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bambunfc
import (
"bytes"
"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.
type Tag struct {
Brand string
ChipUID string
TrayUID string
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 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]
}
func cstr(b []byte) string {
if i := bytes.IndexByte(b, 0); i >= 0 {
b = b[:i]
}
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)
}
if _, err := hex.DecodeString(uid); err != nil {
return "", fmt.Errorf("%w: %v", ErrUID, err)
}
return uid, nil
}
// 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) {
var zero Tag
uid, err := normalizeUID(chipUID)
if err != nil {
return zero, err
}
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)
b4 := block(image, 4)
b5 := block(image, 5)
b6 := block(image, 6)
b9 := block(image, 9)
material := cstr(b2)
typ := cstr(b4)
if material == "" && typ == "" {
return zero, ErrNotBambu
}
tag := Tag{
Brand: BrandBambuLab,
ChipUID: 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])),
}
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
}