package autotest import ( "encoding/json" "fmt" "os" "strings" ) // Config describes the bench: ESP-NOW network, master MAC, and known slaves. type Config struct { ID string `json:"id"` Description string `json:"description,omitempty"` Network uint `json:"network"` MasterMAC string `json:"master_mac"` Slaves []SlaveNode `json:"slaves"` } type SlaveNode struct { ID string `json:"id"` MAC string `json:"mac"` ClientID *uint `json:"client_id,omitempty"` } type Bench struct { Config slaveByID map[string]*SlaveNode } func LoadConfig(path string) (*Bench, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } var cfg Config if err := json.Unmarshal(data, &cfg); err != nil { return nil, fmt.Errorf("parse config: %w", err) } return NewBench(cfg) } func NewBench(cfg Config) (*Bench, error) { cfg.MasterMAC = normalizeMAC(cfg.MasterMAC) if cfg.ID == "" { return nil, fmt.Errorf("config: id is required") } if cfg.MasterMAC == "" { return nil, fmt.Errorf("config %q: master_mac is required", cfg.ID) } if cfg.Network < 1 || cfg.Network > 8 { return nil, fmt.Errorf("config %q: network must be 1–8 (DIP / IO expander)", cfg.ID) } b := &Bench{Config: cfg, slaveByID: make(map[string]*SlaveNode)} for i := range cfg.Slaves { s := &cfg.Slaves[i] if s.ID == "" { return nil, fmt.Errorf("config %q: slave missing id", cfg.ID) } if _, dup := b.slaveByID[s.ID]; dup { return nil, fmt.Errorf("config %q: duplicate slave id %q", cfg.ID, s.ID) } mac, err := parseMAC(s.MAC) if err != nil { return nil, fmt.Errorf("config %q slave %q: %w", cfg.ID, s.ID, err) } s.MAC = mac if s.ClientID == nil { parts := strings.Split(mac, ":") var last byte fmt.Sscanf(parts[5], "%02x", &last) id := uint(last) s.ClientID = &id } b.slaveByID[s.ID] = s } return b, nil } func (b *Bench) Slave(id string) (*SlaveNode, error) { s, ok := b.slaveByID[id] if !ok { return nil, fmt.Errorf("unknown slave %q (config %q)", id, b.ID) } return s, nil } func (b *Bench) ResolveClientID(slaveID string) (uint32, error) { if slaveID == "" || slaveID == "master" || slaveID == "local" { return 0, nil } s, err := b.Slave(slaveID) if err != nil { return 0, err } return uint32(*s.ClientID), nil } func normalizeMAC(s string) string { s = strings.TrimSpace(strings.ToLower(s)) s = strings.ReplaceAll(s, "-", ":") return s } func parseMAC(s string) (string, error) { s = normalizeMAC(s) parts := strings.Split(s, ":") if len(parts) != 6 { return "", fmt.Errorf("invalid mac %q", s) } out := make([]byte, 6) for i, p := range parts { var b byte if _, err := fmt.Sscanf(p, "%02x", &b); err != nil { return "", fmt.Errorf("invalid mac byte %q", p) } out[i] = b } return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", out[0], out[1], out[2], out[3], out[4], out[5]), nil } func MACEqual(a, b string) bool { return normalizeMAC(a) == normalizeMAC(b) } func MACFromProto(mac []byte) string { if len(mac) != 6 { return "" } return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]) }