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 }