Bench configs define command and console serial paths; scenarios can reset nodes via esptool before tests. Smoke resets all nodes then waits for ESP-NOW join. Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
3.9 KiB
Go
153 lines
3.9 KiB
Go
package autotest
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"strings"
|
||
)
|
||
|
||
// UARTConfig holds serial paths for gotool commands and per-node reset (USB console).
|
||
type UARTConfig struct {
|
||
// Baud for uart.master (default 921600).
|
||
Baud uint `json:"baud,omitempty"`
|
||
// Master command UART (external adapter on GPIO2/3), e.g. /dev/ttyUSB0.
|
||
Master string `json:"master"`
|
||
// Master USB console / JTAG serial for esptool reset, e.g. /dev/ttyACM0.
|
||
MasterConsole string `json:"master_console,omitempty"`
|
||
}
|
||
|
||
// 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"`
|
||
UART UARTConfig `json:"uart"`
|
||
Slaves []SlaveNode `json:"slaves"`
|
||
}
|
||
|
||
type SlaveNode struct {
|
||
ID string `json:"id"`
|
||
MAC string `json:"mac"`
|
||
ClientID *uint `json:"client_id,omitempty"`
|
||
// USB console port for esptool reset (optional), e.g. /dev/ttyACM1.
|
||
Console string `json:"console,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)
|
||
}
|
||
if cfg.UART.Master == "" {
|
||
return nil, fmt.Errorf("config %q: uart.master is required (gotool command port)", cfg.ID)
|
||
}
|
||
if cfg.UART.Baud == 0 {
|
||
cfg.UART.Baud = 921600
|
||
}
|
||
|
||
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])
|
||
}
|