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,21 @@
|
||||
# bambu-nfc
|
||||
|
||||
Go library for Bambu Lab filament NFC dumps produced by the Filament NFC dump Android app.
|
||||
|
||||
```go
|
||||
import bambunfc "git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc"
|
||||
|
||||
tags, err := bambunfc.ParseZip(r)
|
||||
spools, err := bambunfc.Merge(tags)
|
||||
keys, err := bambunfc.KeysA(uid)
|
||||
```
|
||||
|
||||
- `dump` — brand-blind `filanfc-dump/v1` JSON/zip
|
||||
- `mifare` — Classic 1K layout helpers
|
||||
- root package — Bambu Key-A HKDF, block parse, merge by `tray_uid`
|
||||
|
||||
`Brand` is `"Bambu Lab"` on a successful parse.
|
||||
|
||||
```bash
|
||||
go test -race ./...
|
||||
```
|
||||
@@ -0,0 +1,13 @@
|
||||
package bambunfc
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Color is an RGBA filament color from a Bambu tag.
|
||||
type Color struct {
|
||||
R, G, B, A uint8
|
||||
}
|
||||
|
||||
// Hex returns "#RRGGBBAA" with uppercase hex digits.
|
||||
func (c Color) Hex() string {
|
||||
return fmt.Sprintf("#%02X%02X%02X%02X", c.R, c.G, c.B, c.A)
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package bambunfc parses Bambu Lab filament NFC dumps and derives MIFARE Key-A from a chip UID.
|
||||
package bambunfc
|
||||
@@ -0,0 +1,23 @@
|
||||
# bambu-nfc Library Implementation Plan
|
||||
|
||||
> **For agentic workers:** Use TDD. Spec: `docs/superpowers/specs/2026-08-23-bambu-nfc-library-design.md`
|
||||
|
||||
**Goal:** Go module that parses filanfc-dump/v1 zips into Bambu tags/spools and derives MIFARE Key-A from a chip UID.
|
||||
|
||||
**Architecture:** Public packages `dump` (JSON/zip), `mifare` (1K layout), root `bambunfc` (HKDF, block parse, Merge).
|
||||
|
||||
**Tech Stack:** Go 1.25, stdlib `crypto/hkdf`, `encoding/json`, `archive/zip`.
|
||||
|
||||
## Global Constraints
|
||||
|
||||
- Module `git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc`
|
||||
- Brand on success: `"Bambu Lab"`
|
||||
- HKDF: IKM=UID, salt=`9a759cf2c4f7caff222cb9769b41bc96`, info=`RFID-A\0`, 16×6 Key-A
|
||||
- Diameter float32 at block 5 offset 8; tray UID 16 raw bytes hex
|
||||
- Required dump sectors for a Bambu parse: 0, 1, 2 (`sectors_fail` is sector indexes)
|
||||
- `go test -race ./...`
|
||||
- Fixtures from dump-app `docs/dumps/` → `testdata/`
|
||||
|
||||
Tasks: (1) module + mifare (2) dump JSON/zip (3) KeysA (4) ParseImage/ParseDump/ParseZip (5) Merge (6) testdata + README.
|
||||
|
||||
Executing in this session per user "do it".
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
// Package dump reads brand-blind filanfc-dump/v1 JSON and zip archives.
|
||||
package dump
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/mifare"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidDump = errors.New("invalid dump")
|
||||
ErrImageSize = errors.New("image size")
|
||||
)
|
||||
|
||||
const FormatV1 = "filanfc-dump/v1"
|
||||
|
||||
// Dump is one sticker: chip UID plus a MIFARE Classic 1K image.
|
||||
type Dump struct {
|
||||
Format string
|
||||
ScannedAt time.Time
|
||||
ChipUID string
|
||||
SAK string
|
||||
ATQA string
|
||||
Tech []string
|
||||
SectorsOK []int
|
||||
SectorsFail []int
|
||||
Image []byte
|
||||
}
|
||||
|
||||
type fileDump struct {
|
||||
Format string `json:"format"`
|
||||
ScannedAt string `json:"scanned_at"`
|
||||
ChipUID string `json:"chip_uid"`
|
||||
SAK string `json:"sak"`
|
||||
ATQA string `json:"atqa"`
|
||||
Tech []string `json:"tech"`
|
||||
SectorsOK []int `json:"sectors_ok"`
|
||||
SectorsFail []int `json:"sectors_fail"`
|
||||
DumpHex string `json:"dump_hex"`
|
||||
DumpB64 string `json:"dump_b64"`
|
||||
}
|
||||
|
||||
// ParseJSON reads one filanfc-dump/v1 object.
|
||||
func ParseJSON(r io.Reader) (Dump, error) {
|
||||
var raw fileDump
|
||||
dec := json.NewDecoder(r)
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: %v", ErrInvalidDump, err)
|
||||
}
|
||||
return parseRaw(raw)
|
||||
}
|
||||
|
||||
func parseRaw(raw fileDump) (Dump, error) {
|
||||
if raw.Format != FormatV1 {
|
||||
return Dump{}, fmt.Errorf("%w: format %q", ErrInvalidDump, raw.Format)
|
||||
}
|
||||
uid := strings.ToLower(strings.TrimSpace(raw.ChipUID))
|
||||
if uid == "" || len(uid)%2 != 0 {
|
||||
return Dump{}, fmt.Errorf("%w: chip_uid %q", ErrInvalidDump, raw.ChipUID)
|
||||
}
|
||||
if _, err := hex.DecodeString(uid); err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: chip_uid: %v", ErrInvalidDump, err)
|
||||
}
|
||||
|
||||
var imgHex, imgB64 []byte
|
||||
var err error
|
||||
if raw.DumpHex != "" {
|
||||
imgHex, err = hex.DecodeString(raw.DumpHex)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: dump_hex: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
if raw.DumpB64 != "" {
|
||||
imgB64, err = base64.StdEncoding.DecodeString(raw.DumpB64)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: dump_b64: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
var image []byte
|
||||
switch {
|
||||
case len(imgHex) != 0 && len(imgB64) != 0:
|
||||
if !bytes.Equal(imgHex, imgB64) {
|
||||
return Dump{}, fmt.Errorf("%w: dump_hex and dump_b64 disagree", ErrInvalidDump)
|
||||
}
|
||||
image = imgHex
|
||||
case len(imgHex) != 0:
|
||||
image = imgHex
|
||||
case len(imgB64) != 0:
|
||||
image = imgB64
|
||||
default:
|
||||
return Dump{}, fmt.Errorf("%w: missing image", ErrInvalidDump)
|
||||
}
|
||||
if len(image) != mifare.Size1K {
|
||||
return Dump{}, fmt.Errorf("%w: want %d got %d", ErrImageSize, mifare.Size1K, len(image))
|
||||
}
|
||||
|
||||
var scanned time.Time
|
||||
if raw.ScannedAt != "" {
|
||||
scanned, err = time.Parse(time.RFC3339Nano, raw.ScannedAt)
|
||||
if err != nil {
|
||||
scanned, err = time.Parse(time.RFC3339, raw.ScannedAt)
|
||||
if err != nil {
|
||||
return Dump{}, fmt.Errorf("%w: scanned_at: %v", ErrInvalidDump, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Dump{
|
||||
Format: raw.Format,
|
||||
ScannedAt: scanned,
|
||||
ChipUID: uid,
|
||||
SAK: raw.SAK,
|
||||
ATQA: raw.ATQA,
|
||||
Tech: raw.Tech,
|
||||
SectorsOK: raw.SectorsOK,
|
||||
SectorsFail: raw.SectorsFail,
|
||||
Image: image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ParseZip reads every {chip_uid}.json entry in a dump zip.
|
||||
func ParseZip(r io.Reader) ([]Dump, error) {
|
||||
body, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: read zip: %v", ErrInvalidDump, err)
|
||||
}
|
||||
zr, err := zip.NewReader(bytes.NewReader(body), int64(len(body)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: zip: %v", ErrInvalidDump, err)
|
||||
}
|
||||
var dumps []Dump
|
||||
for _, f := range zr.File {
|
||||
if !strings.HasSuffix(strings.ToLower(f.Name), ".json") {
|
||||
continue
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: open %s: %v", ErrInvalidDump, f.Name, err)
|
||||
}
|
||||
d, err := ParseJSON(rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %s: %v", ErrInvalidDump, f.Name, err)
|
||||
}
|
||||
dumps = append(dumps, d)
|
||||
}
|
||||
if len(dumps) == 0 {
|
||||
return nil, fmt.Errorf("%w: zip has no json entries", ErrInvalidDump)
|
||||
}
|
||||
return dumps, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package dump
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseJSONFixture(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open(filepath.Join("..", "testdata", "dumps", "e27be276.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := ParseJSON(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d.ChipUID != "e27be276" {
|
||||
t.Errorf("ChipUID = %q", d.ChipUID)
|
||||
}
|
||||
if d.Format != FormatV1 {
|
||||
t.Errorf("Format = %q", d.Format)
|
||||
}
|
||||
if len(d.Image) != 1024 {
|
||||
t.Errorf("Image len = %d", len(d.Image))
|
||||
}
|
||||
if len(d.SectorsFail) != 0 {
|
||||
t.Errorf("SectorsFail = %v", d.SectorsFail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseZipFixture(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open(filepath.Join("..", "testdata", "dumps.zip"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
dumps, err := ParseZip(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(dumps) != 6 {
|
||||
t.Errorf("len = %d; want 6", len(dumps))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONRejectsBadFormat(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ParseJSON(strings.NewReader(`{"format":"nope","chip_uid":"aa","dump_hex":"` + strings.Repeat("00", 1024) + `"}`))
|
||||
if !errors.Is(err, ErrInvalidDump) {
|
||||
t.Fatalf("err = %v; want ErrInvalidDump", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package bambunfc
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidDump = errors.New("invalid dump")
|
||||
ErrImageSize = errors.New("image size")
|
||||
ErrIncomplete = errors.New("incomplete tag")
|
||||
ErrNotBambu = errors.New("not bambu")
|
||||
ErrUID = errors.New("uid")
|
||||
ErrConflict = errors.New("tag conflict")
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
package bambunfc
|
||||
|
||||
import (
|
||||
"crypto/hkdf"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var kdfSalt = []byte{
|
||||
0x9a, 0x75, 0x9c, 0xf2, 0xc4, 0xf7, 0xca, 0xff,
|
||||
0x22, 0x2c, 0xb9, 0x76, 0x9b, 0x41, 0xbc, 0x96,
|
||||
}
|
||||
|
||||
const kdfInfo = "RFID-A\x00"
|
||||
|
||||
// KeysA derives the 16 MIFARE Key-A values for a Bambu tag from its chip UID.
|
||||
func KeysA(uid []byte) ([16][6]byte, error) {
|
||||
var keys [16][6]byte
|
||||
if len(uid) == 0 {
|
||||
return keys, fmt.Errorf("%w: empty", ErrUID)
|
||||
}
|
||||
okm, err := hkdf.Key(sha256.New, uid, kdfSalt, kdfInfo, 16*6)
|
||||
if err != nil {
|
||||
return keys, fmt.Errorf("%w: hkdf: %v", ErrUID, err)
|
||||
}
|
||||
for i := range 16 {
|
||||
copy(keys[i][:], okm[i*6:i*6+6])
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
package bambunfc
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestKeysAGolden11223344(t *testing.T) {
|
||||
t.Parallel()
|
||||
uid, err := hex.DecodeString("11223344")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []string{
|
||||
"0729F3B2D37A", "2027210D85E7", "D77B2E7C92BC", "29126A53A2EE",
|
||||
"E49850F62778", "9A9128B882BD", "45F69B980786", "90DD67095D6B",
|
||||
"EB994746CD69", "B4AEB23473E3", "445E850D699C", "5C9BD5BD9FD7",
|
||||
"1062AFE9F5F5", "81EE5259CF6B", "0920CEEEE2BC", "BAF630CCBFD3",
|
||||
}
|
||||
got, err := KeysA(uid)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for i, w := range want {
|
||||
if h := hex.EncodeToString(got[i][:]); h != toLower(w) {
|
||||
t.Errorf("key[%d] = %s; want %s", i, h, toLower(w))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toLower(s string) string {
|
||||
b := []byte(s)
|
||||
for i, c := range b {
|
||||
if c >= 'A' && c <= 'F' {
|
||||
b[i] = c + ('a' - 'A')
|
||||
}
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func TestKeysAEmpty(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := KeysA(nil)
|
||||
if !errors.Is(err, ErrUID) {
|
||||
t.Errorf("KeysA(nil) err = %v; want ErrUID", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package bambunfc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
)
|
||||
|
||||
// Spool is one physical filament roll (both stickers merged).
|
||||
type Spool struct {
|
||||
Brand string
|
||||
TrayUID string
|
||||
ChipUIDs []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 spoolKey(t Tag) string {
|
||||
return t.Brand + "\x00" + t.TrayUID
|
||||
}
|
||||
|
||||
func sameFilament(a, b Tag) bool {
|
||||
return a.Type == b.Type &&
|
||||
a.Material == b.Material &&
|
||||
a.Color == b.Color &&
|
||||
a.WeightG == b.WeightG
|
||||
}
|
||||
|
||||
func tagToSpool(t Tag) Spool {
|
||||
return Spool{
|
||||
Brand: t.Brand,
|
||||
TrayUID: t.TrayUID,
|
||||
ChipUIDs: []string{t.ChipUID},
|
||||
Type: t.Type,
|
||||
Material: t.Material,
|
||||
MaterialID: t.MaterialID,
|
||||
VariantID: t.VariantID,
|
||||
Color: t.Color,
|
||||
WeightG: t.WeightG,
|
||||
DiameterMM: t.DiameterMM,
|
||||
LengthM: t.LengthM,
|
||||
MinHotendC: t.MinHotendC,
|
||||
MaxHotendC: t.MaxHotendC,
|
||||
BedC: t.BedC,
|
||||
DryC: t.DryC,
|
||||
DryHours: t.DryHours,
|
||||
Produced: t.Produced,
|
||||
SpoolWidthRaw: t.SpoolWidthRaw,
|
||||
}
|
||||
}
|
||||
|
||||
// Merge groups tags by brand + tray UID into spools.
|
||||
func Merge(tags []Tag) ([]Spool, error) {
|
||||
order := make([]string, 0)
|
||||
groups := make(map[string][]Tag)
|
||||
for _, t := range tags {
|
||||
if t.TrayUID == "" {
|
||||
return nil, fmt.Errorf("%w: empty tray uid chip %s", ErrIncomplete, t.ChipUID)
|
||||
}
|
||||
k := spoolKey(t)
|
||||
if _, ok := groups[k]; !ok {
|
||||
order = append(order, k)
|
||||
}
|
||||
groups[k] = append(groups[k], t)
|
||||
}
|
||||
|
||||
spools := make([]Spool, 0, len(order))
|
||||
for _, k := range order {
|
||||
g := groups[k]
|
||||
s := tagToSpool(g[0])
|
||||
uids := map[string]struct{}{g[0].ChipUID: {}}
|
||||
for _, t := range g[1:] {
|
||||
if !sameFilament(g[0], t) {
|
||||
return nil, fmt.Errorf("%w: tray %s", ErrConflict, t.TrayUID)
|
||||
}
|
||||
uids[t.ChipUID] = struct{}{}
|
||||
}
|
||||
s.ChipUIDs = make([]string, 0, len(uids))
|
||||
for u := range uids {
|
||||
s.ChipUIDs = append(s.ChipUIDs, u)
|
||||
}
|
||||
slices.Sort(s.ChipUIDs)
|
||||
spools = append(spools, s)
|
||||
}
|
||||
slices.SortFunc(spools, func(a, b Spool) int {
|
||||
if a.TrayUID < b.TrayUID {
|
||||
return -1
|
||||
}
|
||||
if a.TrayUID > b.TrayUID {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
})
|
||||
return spools, nil
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package bambunfc
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMergeFixtures(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open("testdata/dumps.zip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
tags, err := ParseZip(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
spools, err := Merge(tags)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(spools) != 3 {
|
||||
t.Fatalf("len(spools) = %d; want 3", len(spools))
|
||||
}
|
||||
|
||||
byTray := map[string]Spool{}
|
||||
for _, s := range spools {
|
||||
byTray[s.TrayUID] = s
|
||||
}
|
||||
|
||||
a, ok := byTray["2fb8e0e972084e74bf737393b28e7b12"]
|
||||
if !ok {
|
||||
t.Fatal("missing spool A")
|
||||
}
|
||||
if !slices.Equal(a.ChipUIDs, []string{"52d60177", "e27be276"}) {
|
||||
t.Errorf("spool A chips %v", a.ChipUIDs)
|
||||
}
|
||||
if a.Type != "PETG Translucent" || a.Brand != BrandBambuLab {
|
||||
t.Errorf("spool A type/brand %s %s", a.Type, a.Brand)
|
||||
}
|
||||
|
||||
b, ok := byTray["aa42f322172c48e99c0ece895346ea9f"]
|
||||
if !ok {
|
||||
t.Fatal("missing spool B")
|
||||
}
|
||||
if !slices.Equal(b.ChipUIDs, []string{"92a80577", "b2d32177"}) {
|
||||
t.Errorf("spool B chips %v", b.ChipUIDs)
|
||||
}
|
||||
|
||||
c, ok := byTray["3cb568b7af4f41819412fe60afc5c446"]
|
||||
if !ok {
|
||||
t.Fatal("missing spool C")
|
||||
}
|
||||
if !slices.Equal(c.ChipUIDs, []string{"92b6fb31", "f28c58ed"}) {
|
||||
t.Errorf("spool C chips %v", c.ChipUIDs)
|
||||
}
|
||||
if c.Type != "PETG Basic" || c.Color.Hex() != "#000000FF" {
|
||||
t.Errorf("spool C %s %s", c.Type, c.Color.Hex())
|
||||
}
|
||||
|
||||
for i := 1; i < len(spools); i++ {
|
||||
if spools[i-1].TrayUID > spools[i].TrayUID {
|
||||
t.Fatalf("spools not sorted by TrayUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeConflict(t *testing.T) {
|
||||
t.Parallel()
|
||||
a := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "01", Type: "PLA", Material: "PLA", WeightG: 1000}
|
||||
b := Tag{Brand: BrandBambuLab, TrayUID: "aa", ChipUID: "02", Type: "PETG", Material: "PETG", WeightG: 1000}
|
||||
_, err := Merge([]Tag{a, b})
|
||||
if !errors.Is(err, ErrConflict) {
|
||||
t.Errorf("err = %v; want ErrConflict", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeEmptyTray(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := Merge([]Tag{{Brand: BrandBambuLab, ChipUID: "01", Type: "PLA"}})
|
||||
if !errors.Is(err, ErrIncomplete) {
|
||||
t.Errorf("err = %v; want ErrIncomplete", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Package mifare holds MIFARE Classic 1K layout helpers with no brand-specific memory map.
|
||||
package mifare
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
Sectors = 16
|
||||
Blocks = 64
|
||||
BlockSize = 16
|
||||
SectorSize = 64
|
||||
Size1K = 1024
|
||||
)
|
||||
|
||||
// SectorOf returns the sector index for a block number (0–63).
|
||||
func SectorOf(block int) int {
|
||||
return block / 4
|
||||
}
|
||||
|
||||
// BlockRange returns the half-open block range [first, last) for a sector.
|
||||
func BlockRange(sector int) (first, last int) {
|
||||
first = sector * 4
|
||||
last = first + 4
|
||||
return first, last
|
||||
}
|
||||
|
||||
// SectorBytes returns the 64-byte slice for sector (0–15) from a 1K image.
|
||||
func SectorBytes(image []byte, sector int) ([]byte, error) {
|
||||
if len(image) != Size1K {
|
||||
return nil, fmt.Errorf("image size: want %d got %d", Size1K, len(image))
|
||||
}
|
||||
if sector < 0 || sector >= Sectors {
|
||||
return nil, fmt.Errorf("sector %d out of range", sector)
|
||||
}
|
||||
off := sector * SectorSize
|
||||
return image[off : off+SectorSize], nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package mifare
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSectorOfAndBlockRange(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := SectorOf(9); got != 2 {
|
||||
t.Errorf("SectorOf(9) = %d; want 2", got)
|
||||
}
|
||||
first, last := BlockRange(2)
|
||||
if first != 8 || last != 12 {
|
||||
t.Errorf("BlockRange(2) = %d,%d; want 8,12", first, last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSectorBytes(t *testing.T) {
|
||||
t.Parallel()
|
||||
img := make([]byte, Size1K)
|
||||
img[2*SectorSize] = 0xAB
|
||||
got, err := SectorBytes(img, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got[0] != 0xAB || len(got) != SectorSize {
|
||||
t.Errorf("SectorBytes sector 2: len=%d first=%02x", len(got), got[0])
|
||||
}
|
||||
if _, err := SectorBytes(img[:1023], 0); err == nil {
|
||||
t.Fatal("short image: want error")
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package bambunfc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"git.s1d3sw1ped.com/s1d3sw1ped/bambu-nfc/dump"
|
||||
)
|
||||
|
||||
func TestParseZipFixtures(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open("testdata/dumps.zip")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
tags, err := ParseZip(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(tags) != 6 {
|
||||
t.Fatalf("len(tags) = %d; want 6", len(tags))
|
||||
}
|
||||
var translucent, basic int
|
||||
for _, tag := range tags {
|
||||
if tag.Brand != BrandBambuLab {
|
||||
t.Errorf("chip %s brand %q", tag.ChipUID, tag.Brand)
|
||||
}
|
||||
if tag.DiameterMM != 1.75 {
|
||||
t.Errorf("chip %s diameter %v", tag.ChipUID, tag.DiameterMM)
|
||||
}
|
||||
switch tag.Type {
|
||||
case "PETG Translucent":
|
||||
translucent++
|
||||
if tag.Color.Hex() != "#61B0FF80" {
|
||||
t.Errorf("translucent color %s", tag.Color.Hex())
|
||||
}
|
||||
case "PETG Basic":
|
||||
basic++
|
||||
if tag.Color.Hex() != "#000000FF" {
|
||||
t.Errorf("basic color %s", tag.Color.Hex())
|
||||
}
|
||||
default:
|
||||
t.Errorf("unexpected type %q", tag.Type)
|
||||
}
|
||||
}
|
||||
if translucent != 4 || basic != 2 {
|
||||
t.Errorf("translucent=%d basic=%d", translucent, basic)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImageErrors(t *testing.T) {
|
||||
t.Parallel()
|
||||
raw, err := os.ReadFile("testdata/dumps/e27be276.bin")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := ParseImage("e27be276", raw[:1023], nil); !errors.Is(err, ErrImageSize) {
|
||||
t.Errorf("short image err = %v; want ErrImageSize", err)
|
||||
}
|
||||
if _, err := ParseImage("e27be276", raw, []int{2}); !errors.Is(err, ErrIncomplete) {
|
||||
t.Errorf("fail sector 2 err = %v; want ErrIncomplete", err)
|
||||
}
|
||||
blank := make([]byte, 1024)
|
||||
if _, err := ParseImage("e27be276", blank, nil); !errors.Is(err, ErrNotBambu) {
|
||||
t.Errorf("blank image err = %v; want ErrNotBambu", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDumpJSON(t *testing.T) {
|
||||
t.Parallel()
|
||||
f, err := os.Open("testdata/dumps/e27be276.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
d, err := dump.ParseJSON(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tag, err := ParseDump(d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tag.TrayUID != "2fb8e0e972084e74bf737393b28e7b12" {
|
||||
t.Errorf("TrayUID = %s", tag.TrayUID)
|
||||
}
|
||||
if tag.ChipUID != "e27be276" {
|
||||
t.Errorf("ChipUID = %s", tag.ChipUID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseImageEmptyUID(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := ParseImage("", bytes.Repeat([]byte{1}, 1024), nil)
|
||||
if !errors.Is(err, ErrUID) {
|
||||
t.Errorf("err = %v; want ErrUID", err)
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:17.612080Z","chip_uid":"52d60177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"52d60177f2080400050f4bb2bb315e904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000156a277052e48d22d9c515522178c96700000000000087878769000000000000a092f47b7ddeee11ae5bd3209ea4eac7fcbc8c44b15f39c48031c63900c8c9fb970e215eb13b518763875c4f6de59c2b0000000000008787876900000000000084114ae56a384a765b1da4d0ea011fc768af8a9f5366b47fc5bfb15c3f4e97f350dfc8c51318e00eb9ca8f128a35667c000000000000878787690000000000008e579b88c536abca254e6161dfa3ecb373576204ab38589cc460d7018067bd43a89f15db64d8e5aed0a9839c458b6b3b000000000000878787690000000000003d5348fc8b55473868629b1e031d23cba4a682307f5a8a51c8416bac428b024e8775b8c071bc07e7e3df779e40b8eb5b00000000000087878769000000000000a07cc38936af652fb9c56e46129adf740d44b8fc902d588a367ef1efb5f88895bcd82eef67df0e2bbc215525a73c1d0000000000000087878769000000000000","dump_b64":"UtYBd/IIBAAFD0uyuzFekEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAFWoncFLkjSLZxRVSIXjJZwAAAAAAAIeHh2kAAAAAAACgkvR7fd7uEa5b0yCepOrH/LyMRLFfOcSAMcY5AMjJ+5cOIV6xO1GHY4dcT23lnCsAAAAAAACHh4dpAAAAAAAAhBFK5Wo4SnZbHaTQ6gEfx2ivip9TZrR/xb+xXD9Ol/NQ38jFExjgDrnKjxKKNWZ8AAAAAAAAh4eHaQAAAAAAAI5Xm4jFNqvKJU5hYd+j7LNzV2IEqzhYnMRg1wGAZ71DqJ8V22TY5a7QqYOcRYtrOwAAAAAAAIeHh2kAAAAAAAA9U0j8i1VHOGhimx4DHSPLpKaCMH9ailHIQWusQosCTod1uMBxvAfn4993nkC461sAAAAAAACHh4dpAAAAAAAAoHzDiTavZS+5xW5GEprfdA1EuPyQLViKNn7x77X4iJW82C7vZ98OK7whVSWnPB0AAAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:33.888563Z","chip_uid":"92a80577","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92a805774808040005bbb1ffee6a26904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000008d3fbd5a0eb9037c318d989139fd582600000000000087878769000000000000d22c7b4757adbcc88e3ebbc5ca1145711c97f643ebb67068e6b1bfee54057edc63f7c36e05159147152c62b270758d0700000000000087878769000000000000c42bbba5082f31656c9956313a49609810219e5bbbf372c822f1967919b663de6f20ead8ba298762d000440febc0634a000000000000878787690000000000005dc0beafe4dfeee033dcce4d5d9e232cb2aed198420f7bb2c06367b80d92210b9087789eae480234ad1f519f376ff17a00000000000087878769000000000000e4fcf2274514b9464159e8c1efc4423cfc42a0e1d2259ba287f62db47e559a405fef21496905c2db3eaedf84646d7d5e00000000000087878769000000000000319c5908b98ee01ffa1cc1713e2f9c3e0b98f6b35875aeb0c6feefa218b5a402abd20a09b7ebfbfbd0b6192cbf62c67500000000000087878769000000000000","dump_b64":"kqgFd0gIBAAFu7H/7momkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjT+9Wg65A3wxjZiROf1YJgAAAAAAAIeHh2kAAAAAAADSLHtHV628yI4+u8XKEUVxHJf2Q+u2cGjmsb/uVAV+3GP3w24FFZFHFSxisnB1jQcAAAAAAACHh4dpAAAAAAAAxCu7pQgvMWVsmVYxOklgmBAhnlu783LIIvGWeRm2Y95vIOrYuimHYtAARA/rwGNKAAAAAAAAh4eHaQAAAAAAAF3Avq/k3+7gM9zOTV2eIyyyrtGYQg97ssBjZ7gNkiELkId4nq5IAjStH1GfN2/xegAAAAAAAIeHh2kAAAAAAADk/PInRRS5RkFZ6MHvxEI8/EKg4dIlm6KH9i20flWaQF/vIUlpBcLbPq7fhGRtfV4AAAAAAACHh4dpAAAAAAAAMZxZCLmO4B/6HMFxPi+cPguY9rNYda6wxv7vohi1pAKr0goJt+v7+9C2GSy/YsZ1AAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:36.914443Z","chip_uid":"92b6fb31","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"92b6fb31ee0804000577a98edf9f09904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000475be12293a7f927bf42fa7b1aa7944a000000000000878787690000000000007f71f47a229a5dd5057f33aa2fee1531172829d75bf33a6c9f3e41365a1e844ff4f8528313f48fefaa1e6d02765e8abf0000000000008787876900000000000085b72ba08ddb600e8ce8d003758deca39e3ac4a83ef98429fc9d73a0ad596427aed0c23621aee0f7c25712a8e518e20c00000000000087878769000000000000e328f11df058429ddbd7ad67596e04800262e81492c59f53888bd32e65764727ad23acca7ed7a61f2c565595d39e4e79000000000000878787690000000000002f11ee1918f03618fd3c428911fb7b61cfed49fb7ebd25478eefe2e06fa633d2305950d0ced79ba6ede7a933ecaf81f500000000000087878769000000000000349b83bce314d86f4f02ddb897eb43e714dbec97891357686165cb66c230958a77e45cf5c1eb71d7aa9337499c59cbbc00000000000087878769000000000000","dump_b64":"krb7Me4IBAAFd6mO358JkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAR1vhIpOn+Se/Qvp7GqeUSgAAAAAAAIeHh2kAAAAAAAB/cfR6Ippd1QV/M6ov7hUxFygp11vzOmyfPkE2Wh6ET/T4UoMT9I/vqh5tAnZeir8AAAAAAACHh4dpAAAAAAAAhbcroI3bYA6M6NADdY3so546xKg++YQp/J1zoK1ZZCeu0MI2Ia7g98JXEqjlGOIMAAAAAAAAh4eHaQAAAAAAAOMo8R3wWEKd29etZ1luBIACYugUksWfU4iL0y5ldkcnrSOsyn7Xph8sVlWV055OeQAAAAAAAIeHh2kAAAAAAAAvEe4ZGPA2GP08QokR+3thz+1J+369JUeO7+Lgb6Yz0jBZUNDO15um7eepM+yvgfUAAAAAAACHh4dpAAAAAAAANJuDvOMU2G9PAt24l+tD5xTb7JeJE1doYWXLZsIwlYp35Fz1wetx16qTN0mcWcu8AAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:52.668047Z","chip_uid":"b2d32177","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"b2d321773708040005116ac96f99fd904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3eaa42f322172c48e99c0ece895346ea9f00000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f343632303236303430320000000000000000000000004a01000000000000000000000000000000008787876900000000000002000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000001000000000000000000000000000000000000000000000000000000000000001bc1b3d3eb58970ec6aa04d904b7280e00000000000087878769000000000000f7d96a1c22d18e419694cf375bc072f9f0325fdf07301faf4b7f63aec72f962863d4e42761e1d59c30292e45b8088bc800000000000087878769000000000000fadc47e3d52f8722df5e7613c9445c4f9a0a7a3a87362a4f8da04fdefbf082a98897d99124e9f61021e29c5e28c5ffd500000000000087878769000000000000fa03e9bc64418cee3eac18ac24a08e8195ebdb985b3780a570b83ac5c2b3e4576e80a5c5b4f5a44d80695cc9a1ea328700000000000087878769000000000000a56fb5e43f60cf4414a458576596307b2028bdd57e449e69cdccbdc5cfb5b119ebc6f1788549fdfbfcf0478c0f9d386700000000000087878769000000000000416768587629189df719eb6b91851f6191241892547475cddc0ce27eb6ba4f5e8bba9736223405f48296d27b5a5b82b500000000000087878769000000000000","dump_b64":"stMhdzcIBAAFEWrJb5n9kEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+qkLzIhcsSOmcDs6JU0bqnwAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM180NjIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAG8Gz0+tYlw7GqgTZBLcoDgAAAAAAAIeHh2kAAAAAAAD32WocItGOQZaUzzdbwHL58DJf3wcwH69Lf2Ouxy+WKGPU5Cdh4dWcMCkuRbgIi8gAAAAAAACHh4dpAAAAAAAA+txH49UvhyLfXnYTyURcT5oKejqHNipPjaBP3vvwgqmIl9mRJOn2ECHinF4oxf/VAAAAAAAAh4eHaQAAAAAAAPoD6bxkQYzuPqwYrCSgjoGV69uYWzeApXC4OsXCs+RXboClxbT1pE2AaVzJoeoyhwAAAAAAAIeHh2kAAAAAAAClb7XkP2DPRBSkWFdlljB7ICi91X5EnmnNzL3Fz7WxGevG8XiFSf37/PBHjA+dOGcAAAAAAACHh4dpAAAAAAAAQWdoWHYpGJ33GetrkYUfYZEkGJJUdHXN3Azifra6T16Lupc2IjQF9IKW0ntaW4K1AAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:52:02.388908Z","chip_uid":"e27be276","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"e27be2760d08040005919d6a17ddcf904730312d423000004746473031000000504554470000000000000000000000000000000000008787876900000000000050455447205472616e736c7563656e7461b0ff80e80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000342134218403e8033333333fcdcc4c3e2fb8e0e972084e74bf737393b28e7b1200000000c9000000000000000000000000000000000087878769000000000000323032365f30345f30325f31335f323132303236303430320000000000000000000000004a0100000000000000000000000000000000878787690000000000000200010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000100000000000000000000000000000000000000000000000000000000000000630fe06e572f8f0f94e2ff3bfba403190000000000008787876900000000000074eaa3ac7501ae371180023e3e834388526896deb1450667081e07af5ad55257dabb43981c79bd7263072f7d4727292f000000000000878787690000000000004f52ee7faa3adf093b19814efac2123f66a0cd95e9da3abc88cb652f284bc98e6d1098712a24e49ae0d4c682783cb6b400000000000087878769000000000000433a59c30c8920059b3d004360d17bc2670314a1a927acf0527258297d89bd72301337347a597fd7135d9af0a436e437000000000000878787690000000000006fd6cc3b82cd5c326eb0ff87104db3440322b1e578cf30410f2fc66271da62bd627347fea583a136f72599a900ccf14e000000000000878787690000000000002e07c6b457d7672aa10ed15b889180dedeb1a26fceb509fe96da5740c5c234dae1a96fe45a819a4acfbdaa225931999800000000000087878769000000000000","dump_b64":"4nvidg0IBAAFkZ1qF93PkEcwMS1CMAAAR0ZHMDEAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgVHJhbnNsdWNlbnRhsP+A6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAA0ITQhhAPoAzMzMz/NzEw+L7jg6XIITnS/c3OTso57EgAAAADJAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wNF8wMl8xM18yMTIwMjYwNDAyAAAAAAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAYw/gblcvjw+U4v87+6QDGQAAAAAAAIeHh2kAAAAAAAB06qOsdQGuNxGAAj4+g0OIUmiW3rFFBmcIHgevWtVSV9q7Q5gceb1yYwcvfUcnKS8AAAAAAACHh4dpAAAAAAAAT1Luf6o63wk7GYFO+sISP2agzZXp2jq8iMtlLyhLyY5tEJhxKiTkmuDUxoJ4PLa0AAAAAAAAh4eHaQAAAAAAAEM6WcMMiSAFmz0AQ2DRe8JnAxShqSes8FJyWCl9ib1yMBM3NHpZf9cTXZrwpDbkNwAAAAAAAIeHh2kAAAAAAABv1sw7gs1cMm6w/4cQTbNEAyKx5XjPMEEPL8ZicdpivWJzR/6lg6E29yWZqQDM8U4AAAAAAACHh4dpAAAAAAAALgfGtFfXZyqhDtFbiJGA3t6xom/OtQn+ltpXQMXCNNrhqW/kWoGaSs+9qiJZMZmYAAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Vendored
BIN
Binary file not shown.
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"format":"filanfc-dump/v1","scanned_at":"2026-08-23T19:53:49.531896Z","chip_uid":"f28c58ed","sak":"08","atqa":"0004","tech":["android.nfc.tech.MifareClassic","android.nfc.tech.NfcA"],"sectors_ok":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15],"sectors_fail":[],"dump_hex":"f28c58edcb08040005fbe0f72ba5ad904730302d4b3030004746473030000000504554470000000000000000000000000000000000008787876900000000000050455447204261736963000000000000000000ffe80300000000e03f0000000041000800000000000401e6000000000000000000000087878769000000000000000000000000000000000000cdcc4c3e3cb568b7af4f41819412fe60afc5c44600000000ab0f0000000000000000000000000000000087878769000000000000323032365f30325f32355f31365f313932365f30325f32355f31360000000000000000004a010000000000000000000000000000000087878769000000000000020001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000878787690000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008787876900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000087878769000000000000010000000000000000000000000000000000000000000000000000000000000050ee165b9ac59477618c747996394944000000000000878787690000000000000821e5296e38edaf4e579c0a3e385a718d7cd7cae8bcf1885ab51da9d642d7b5d00bd7bf9222d1cdd573728a842d9328000000000000878787690000000000004b929502ea8bdcc645a7d415cfeb6c0449095bf56227d777936a3492e256583dbd494e976e67c421c7293696fea9380700000000000087878769000000000000b96ad7d6fcd727be230357d9e228a669819e5727dfb287e0649347da066ca2e1e6b043aea72f43b7aae856d3fd1cc30b00000000000087878769000000000000be81eea3831737d3e3fdeb81e0b360206d3db6f50864effbf3e784a8547f0a9a730e3af09b052b0de6f7fe87959cee4f00000000000087878769000000000000e3f7d07cc16a75a374b81b53a73256b34921a3fcf6e6688a9925c40e2acb6786bf4457280976268f5703988b49b73a3400000000000087878769000000000000","dump_b64":"8oxY7csIBAAF++D3K6WtkEcwMC1LMDAAR0ZHMDAAAABQRVRHAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAFBFVEcgQmFzaWMAAAAAAAAAAAD/6AMAAAAA4D8AAAAAQQAIAAAAAAAEAeYAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAADNzEw+PLVot69PQYGUEv5gr8XERgAAAACrDwAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAMjAyNl8wMl8yNV8xNl8xOTI2XzAyXzI1XzE2AAAAAAAAAAAASgEAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAIAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIeHh2kAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACHh4dpAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAh4eHaQAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAUO4WW5rFlHdhjHR5ljlJRAAAAAAAAIeHh2kAAAAAAAAIIeUpbjjtr05XnAo+OFpxjXzXyui88YhatR2p1kLXtdAL17+SItHN1XNyioQtkygAAAAAAACHh4dpAAAAAAAAS5KVAuqL3MZFp9QVz+tsBEkJW/ViJ9d3k2o0kuJWWD29SU6XbmfEIccpNpb+qTgHAAAAAAAAh4eHaQAAAAAAALlq19b81ye+IwNX2eIopmmBnlcn37KH4GSTR9oGbKLh5rBDrqcvQ7eq6FbT/RzDCwAAAAAAAIeHh2kAAAAAAAC+ge6jgxc30+P964Hgs2AgbT229Qhk7/vz54SoVH8KmnMOOvCbBSsN5vf+h5Wc7k8AAAAAAACHh4dpAAAAAAAA4/fQfMFqdaN0uBtTpzJWs0kho/z25miKmSXEDirLZ4a/RFcoCXYmj1cDmItJtzo0AAAAAAAAh4eHaQAAAAAAAA=="}
|
||||
Reference in New Issue
Block a user