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.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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 0–2, 4–6, 8–9).
|
||||
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 0–15).
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user