Add bench UART ports and cold-start reset to goTool autotest.

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>
This commit is contained in:
2026-05-18 23:44:58 +02:00
co-authored by Cursor
parent d24b0cb5c3
commit 0299ba44fd
10 changed files with 268 additions and 45 deletions
+19
View File
@@ -7,12 +7,23 @@ import (
"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"`
}
@@ -20,6 +31,8 @@ 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 {
@@ -50,6 +63,12 @@ func NewBench(cfg Config) (*Bench, error) {
if cfg.Network < 1 || cfg.Network > 8 {
return nil, fmt.Errorf("config %q: network must be 18 (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 {
+3
View File
@@ -7,6 +7,9 @@ func TestNewBenchClientIDFromMAC(t *testing.T) {
ID: "t",
Network: 1,
MasterMAC: "aa:bb:cc:dd:ee:ff",
UART: UARTConfig{
Master: "/dev/ttyUSB0",
},
Slaves: []SlaveNode{{
ID: "pod",
MAC: "50:78:7d:18:01:10",
+103
View File
@@ -0,0 +1,103 @@
package autotest
import (
"fmt"
"os"
"os/exec"
"time"
)
const defaultResetWaitMS = 2000
// ResetESP32 toggles EN via esptool (USB-JTAG / ttyACM console port).
func ResetESP32(port string) error {
if port == "" {
return fmt.Errorf("empty console port")
}
tries := [][]string{
{"python", "-m", "esptool", "--chip", "esp32s3", "-p", port,
"--before", "default_reset", "--after", "hard_reset", "chip_id"},
{"esptool.py", "--chip", "esp32s3", "-p", port,
"--before", "default_reset", "--after", "hard_reset", "chip_id"},
{"esptool", "--chip", "esp32s3", "-p", port,
"--before", "default_reset", "--after", "hard_reset", "chip_id"},
}
var lastErr error
for _, argv := range tries {
if _, err := exec.LookPath(argv[0]); err != nil {
lastErr = err
continue
}
cmd := exec.Command(argv[0], argv[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
lastErr = err
continue
}
return nil
}
if lastErr != nil {
return fmt.Errorf("esptool reset on %s: %w (install ESP-IDF esptool or python -m esptool)", port, lastErr)
}
return fmt.Errorf("esptool not found; cannot reset %s", port)
}
func (b *Bench) resetNode(target string) (string, error) {
switch target {
case "master", "":
if b.UART.MasterConsole == "" {
return "", fmt.Errorf("uart.master_console not set in config %q", b.ID)
}
return b.UART.MasterConsole, nil
default:
s, err := b.Slave(target)
if err != nil {
return "", err
}
if s.Console == "" {
return "", fmt.Errorf("slave %q has no console port in config", target)
}
return s.Console, nil
}
}
func (b *Bench) ResetTargets(targets []string, waitMS int) error {
if waitMS <= 0 {
waitMS = defaultResetWaitMS
}
if len(targets) == 0 {
targets = []string{"master"}
}
for _, t := range targets {
port, err := b.resetNode(t)
if err != nil {
return err
}
if err := ResetESP32(port); err != nil {
return err
}
time.Sleep(time.Duration(waitMS) * time.Millisecond)
}
return nil
}
func (b *Bench) ResetAll(waitMS int) error {
// Slaves first, master last — master discover runs after slaves are booting.
var targets []string
for i := range b.Slaves {
if b.Slaves[i].Console != "" {
targets = append(targets, b.Slaves[i].ID)
}
}
if b.UART.MasterConsole != "" {
targets = append(targets, "master")
}
if len(targets) == 0 {
return fmt.Errorf("config %q: no console ports defined for reset", b.ID)
}
return b.ResetTargets(targets, waitMS)
}
+31
View File
@@ -92,11 +92,42 @@ func runStep(bench *Bench, step Step, client MasterClient) error {
return checkDeadzone(bench, step, client)
case "unicast_test", "unicast":
return checkUnicastTest(bench, step, client)
case "reset", "reboot":
return checkReset(bench, step)
default:
return fmt.Errorf("unknown command %q", step.Command)
}
}
type resetInput struct {
Target string `json:"target"`
Slave string `json:"slave"`
All bool `json:"all"`
WaitMS int `json:"wait_ms"`
}
func checkReset(bench *Bench, step Step) error {
var in resetInput
if len(step.Input) > 0 {
if err := json.Unmarshal(step.Input, &in); err != nil {
return fmt.Errorf("input: %w", err)
}
}
if in.All {
return bench.ResetAll(in.WaitMS)
}
target := in.Target
if in.Slave != "" {
target = in.Slave
}
if target == "" {
target = "master"
}
return bench.ResetTargets([]string{target}, in.WaitMS)
}
func checkVersion(step Step, client MasterClient) error {
ver, err := client.GetVersion()
if err != nil {