Extend autotest to cover all UART commands except OTA upload.

Add led_ring, find_me, restart, and ota_progress steps plus uart_cmds scenario.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
simon 2026-05-19 22:41:23 +02:00
parent a9e08107b4
commit 95d5a9747a
7 changed files with 390 additions and 1 deletions

View File

@ -43,7 +43,9 @@ Bench **configs** (`testdata/configs/`) list network, MACs, and serial ports (`u
```bash ```bash
go run . test -list-configs go run . test -list-configs
go run . test -list-scenarios
go run . test -config example-lab -scenario smoke go run . test -config example-lab -scenario smoke
go run . test -config example-lab -scenario uart_cmds
go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 -v go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 -v
``` ```

View File

@ -15,6 +15,10 @@ type MasterClient interface {
ListClients() ([]*pb.ClientInfo, error) ListClients() ([]*pb.ClientInfo, error)
AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error)
EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error)
LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error)
FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error)
Restart(clientID uint32) (*pb.RestartResponse, error)
OtaSlaveProgress(clientID uint32) (*pb.OtaSlaveProgressResponse, error)
} }
type StepResult struct { type StepResult struct {
@ -92,6 +96,14 @@ func runStep(bench *Bench, step Step, client MasterClient) error {
return checkDeadzone(bench, step, client) return checkDeadzone(bench, step, client)
case "unicast_test", "unicast": case "unicast_test", "unicast":
return checkUnicastTest(bench, step, client) return checkUnicastTest(bench, step, client)
case "led_ring", "ledring":
return checkLedRing(step, client)
case "find_me", "findme":
return checkFindMe(bench, step, client)
case "restart":
return checkRestartCmd(bench, step, client)
case "ota_progress", "ota_slave_progress":
return checkOtaProgress(step, client)
case "reset", "reboot": case "reset", "reboot":
return checkReset(bench, step) return checkReset(bench, step)
default: default:
@ -310,3 +322,203 @@ func checkUnicastTest(bench *Bench, step Step, client MasterClient) error {
} }
return nil return nil
} }
type ledRingInput struct {
Mode string `json:"mode"`
Progress uint `json:"progress"`
Digit uint `json:"digit"`
R uint `json:"r"`
G uint `json:"g"`
B uint `json:"b"`
Intensity uint `json:"intensity"`
BlinkMs uint `json:"blink_ms"`
BlinkCount uint `json:"blink_count"`
}
func ledRingModeValue(mode string) (uint32, error) {
switch strings.ToLower(strings.ReplaceAll(mode, "-", "_")) {
case "clear", "":
return 0, nil
case "progress":
return 1, nil
case "digit":
return 2, nil
case "blink":
return 3, nil
case "find_me", "findme":
return 4, nil
default:
return 0, fmt.Errorf("unknown led_ring mode %q", mode)
}
}
func checkLedRing(step Step, client MasterClient) error {
var in ledRingInput
if len(step.Input) > 0 {
if err := json.Unmarshal(step.Input, &in); err != nil {
return fmt.Errorf("input: %w", err)
}
}
mode, err := ledRingModeValue(in.Mode)
if err != nil {
return err
}
if in.Mode == "" && step.Expect.Mode != nil {
mode = *step.Expect.Mode
}
req := &pb.LedRingProgressRequest{
Mode: mode,
Progress: uint32(in.Progress),
Digit: uint32(in.Digit),
R: uint32(in.R),
G: uint32(in.G),
B: uint32(in.B),
Intensity: uint32(in.Intensity),
BlinkMs: uint32(in.BlinkMs),
BlinkCount: uint32(in.BlinkCount),
}
if mode == 1 && req.GetG() == 0 && req.GetR() == 0 && req.GetB() == 0 {
req.G = 255
}
resp, err := client.LedRing(req)
if err != nil {
return err
}
e := step.Expect
if e.Success != nil && resp.GetSuccess() != *e.Success {
return fmt.Errorf("success=%v want %v", resp.GetSuccess(), *e.Success)
}
if e.Mode != nil && resp.GetMode() != *e.Mode {
return fmt.Errorf("mode=%d want %d", resp.GetMode(), *e.Mode)
}
if e.Progress != nil && resp.GetProgress() != *e.Progress {
return fmt.Errorf("progress=%d want %d", resp.GetProgress(), *e.Progress)
}
if e.Digit != nil && resp.GetDigit() != *e.Digit {
return fmt.Errorf("digit=%d want %d", resp.GetDigit(), *e.Digit)
}
return nil
}
type findMeInput struct {
ClientID *uint `json:"client_id"`
Client uint `json:"client"`
Slave string `json:"slave"`
}
func checkFindMe(bench *Bench, step Step, client MasterClient) error {
var in findMeInput
if len(step.Input) > 0 {
if err := json.Unmarshal(step.Input, &in); err != nil {
return fmt.Errorf("input: %w", err)
}
}
clientID, err := resolveClientID(bench, in.Slave, in.ClientID, in.Client)
if err != nil {
return err
}
resp, err := client.FindMe(clientID)
if err != nil {
return err
}
e := step.Expect
if e.Success != nil && resp.GetSuccess() != *e.Success {
return fmt.Errorf("success=%v want %v", resp.GetSuccess(), *e.Success)
}
if e.Slave != "" || in.Slave != "" {
wantSlave := e.Slave
if wantSlave == "" {
wantSlave = in.Slave
}
wantID, err := bench.ResolveClientID(wantSlave)
if err != nil {
return err
}
if resp.GetClientId() != wantID {
return fmt.Errorf("client_id=%d want %d", resp.GetClientId(), wantID)
}
}
return nil
}
type restartCmdInput struct {
ClientID *uint `json:"client_id"`
Client uint `json:"client"`
Slave string `json:"slave"`
}
func checkRestartCmd(bench *Bench, step Step, client MasterClient) error {
var in restartCmdInput
if len(step.Input) > 0 {
if err := json.Unmarshal(step.Input, &in); err != nil {
return fmt.Errorf("input: %w", err)
}
}
clientID, err := resolveClientID(bench, in.Slave, in.ClientID, in.Client)
if err != nil {
return err
}
resp, err := client.Restart(clientID)
if err != nil {
return err
}
e := step.Expect
if e.Success != nil && resp.GetSuccess() != *e.Success {
return fmt.Errorf("success=%v want %v", resp.GetSuccess(), *e.Success)
}
if resp.GetClientId() != clientID {
return fmt.Errorf("client_id=%d want %d", resp.GetClientId(), clientID)
}
return nil
}
type otaProgressInput struct {
ClientID *uint `json:"client_id"`
Client uint `json:"client"`
Slave string `json:"slave"`
}
func checkOtaProgress(step Step, client MasterClient) error {
var in otaProgressInput
if len(step.Input) > 0 {
if err := json.Unmarshal(step.Input, &in); err != nil {
return fmt.Errorf("input: %w", err)
}
}
clientID := uint32(in.Client)
if in.ClientID != nil {
clientID = uint32(*in.ClientID)
}
resp, err := client.OtaSlaveProgress(clientID)
if err != nil {
return err
}
e := step.Expect
if e.Active != nil && resp.GetActive() != *e.Active {
return fmt.Errorf("active=%v want %v", resp.GetActive(), *e.Active)
}
return nil
}
func resolveClientID(bench *Bench, slave string, clientID *uint, client uint) (uint32, error) {
switch {
case slave != "":
return bench.ResolveClientID(slave)
case clientID != nil:
return uint32(*clientID), nil
default:
return uint32(client), nil
}
}

View File

@ -0,0 +1,25 @@
package autotest
import "testing"
func TestLedRingModeValue(t *testing.T) {
tests := []struct {
mode string
want uint32
}{
{"clear", 0},
{"progress", 1},
{"digit", 2},
{"blink", 3},
{"find-me", 4},
}
for _, tc := range tests {
got, err := ledRingModeValue(tc.mode)
if err != nil {
t.Fatalf("mode %q: %v", tc.mode, err)
}
if got != tc.want {
t.Fatalf("mode %q = %d want %d", tc.mode, got, tc.want)
}
}
}

View File

@ -45,6 +45,14 @@ type Expect struct {
// unicast_test // unicast_test
Seq *uint32 `json:"seq,omitempty"` Seq *uint32 `json:"seq,omitempty"`
// led_ring
Mode *uint32 `json:"mode,omitempty"`
Progress *uint32 `json:"progress,omitempty"`
Digit *uint32 `json:"digit,omitempty"`
// ota_slave_progress
Active *bool `json:"active,omitempty"`
} }
func LoadScenario(path string) (*Scenario, error) { func LoadScenario(path string) (*Scenario, error) {

View File

@ -222,3 +222,19 @@ func (s *serialPort) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadz
func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) { func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
return s.espnowUnicastTest(clientID, seq) return s.espnowUnicastTest(clientID, seq)
} }
func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
return s.ledRingProgress(req)
}
func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
return s.espnowFindMe(clientID)
}
func (s *serialPort) Restart(clientID uint32) (*pb.RestartResponse, error) {
return s.restart(clientID)
}
func (s *serialPort) OtaSlaveProgress(clientID uint32) (*pb.OtaSlaveProgressResponse, error) {
return QueryOtaSlaveProgress(s, clientID)
}

View File

@ -44,6 +44,19 @@ Ordered steps: UART commands, delays, or esptool reset.
**unicast_test** — `input`: `slave` or `client_id`, `seq` **unicast_test** — `input`: `slave` or `client_id`, `seq`
`expect`: `success`, `seq` `expect`: `success`, `seq`
**led_ring** — `input`: `mode` (`clear`, `progress`, `digit`, `blink`, `find_me`), `progress`, `digit`, `r`/`g`/`b`, `intensity`, `blink_ms`, `blink_count`
`expect`: `success`, `mode`, `progress`, `digit`
**find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring)
`expect`: `success`
**restart** — `input`: `client` / `client_id` or `slave` (prefer slave in tests so UART stays up)
`expect`: `success`
**ota_progress** — query `OTA_SLAVE_PROGRESS` (no firmware upload)
`input`: optional `client_id` / `slave`
`expect`: `active` (typically `false` when idle)
**reset** — esptool hard-reset via console port from bench config (no `expect`). **reset** — esptool hard-reset via console port from bench config (no `expect`).
`input`: `target` (`master`) or `slave` (`pod-1`), or `all: true`; optional `wait_ms` after each reset (default 2000). `input`: `target` (`master`) or `slave` (`pod-1`), or `all: true`; optional `wait_ms` after each reset (default 2000).
@ -56,13 +69,19 @@ Example reset steps:
Requires `python -m esptool` or `esptool.py` on PATH (ESP-IDF). Requires `python -m esptool` or `esptool.py` on PATH (ESP-IDF).
The `smoke` scenario resets every node with a configured `console` port, waits **10 s** for boot and ESP-NOW join, then runs the UART checks. The `smoke` scenario resets every node with a configured `console` port, waits **10 s** for boot and ESP-NOW join, then runs a short UART smoke test.
The **`uart_cmds`** scenario covers all implemented UART commands **except OTA upload** (`OTA_START` / `OTA_PAYLOAD` / `OTA_END` / `OTA_START_ESPNOW`). It ends with a slave **restart** (master UART stays connected).
`CLIENT_INPUT` is not tested (not implemented in firmware).
### Example ### Example
```bash ```bash
cd goTool cd goTool
go run . test -config example-lab -scenario smoke go run . test -config example-lab -scenario smoke
go run . test -config example-lab -scenario uart_cmds
go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1
go run . test -list-configs go run . test -list-configs
go run . test -list-scenarios
``` ```

107
goTool/testdata/scenarios/uart_cmds.json vendored Normal file
View File

@ -0,0 +1,107 @@
{
"id": "uart_cmds",
"description": "All implemented UART commands (no OTA upload). Resets bench, joins ESP-NOW, exercises each cmd; restarts slave at end.",
"config": "example-lab",
"steps": [
{
"name": "reset all nodes",
"command": "reset",
"input": { "all": true, "wait_ms": 2500 }
},
{
"name": "boot and ESP-NOW join",
"delay_ms": 10000
},
{
"name": "VERSION",
"command": "version",
"expect": { "version_min": 1 }
},
{
"name": "CLIENT_INFO",
"command": "clients",
"expect": {
"min_clients": 1,
"slave": "pod-1",
"available": true
}
},
{
"name": "ACCEL_DEADZONE read master",
"command": "deadzone",
"input": { "write": false, "client": 0 },
"expect": { "success": true }
},
{
"name": "ACCEL_DEADZONE write master",
"command": "deadzone",
"input": { "write": true, "client": 0, "value": 120 },
"expect": { "success": true, "deadzone": 120 }
},
{
"name": "ACCEL_DEADZONE read master after write",
"command": "deadzone",
"input": { "write": false, "client": 0 },
"expect": { "success": true, "deadzone": 120 }
},
{
"name": "LED_RING clear",
"command": "led_ring",
"input": { "mode": "clear" },
"expect": { "success": true, "mode": 0 }
},
{
"name": "LED_RING progress",
"command": "led_ring",
"input": { "mode": "progress", "progress": 40, "g": 200 },
"expect": { "success": true, "mode": 1, "progress": 40 }
},
{
"name": "LED_RING digit",
"command": "led_ring",
"input": { "mode": "digit", "digit": 3, "r": 255 },
"expect": { "success": true, "mode": 2, "digit": 3 }
},
{
"name": "LED_RING blink",
"command": "led_ring",
"input": { "mode": "blink", "r": 0, "g": 255, "b": 0, "blink_count": 1 },
"expect": { "success": true, "mode": 3 }
},
{
"name": "ESPNOW_UNICAST_TEST",
"command": "unicast_test",
"input": { "slave": "pod-1", "seq": 99 },
"expect": { "success": true, "seq": 99 }
},
{
"name": "FIND_ME master",
"command": "find_me",
"input": { "client": 0 },
"expect": { "success": true }
},
{
"name": "FIND_ME slave",
"command": "find_me",
"input": { "slave": "pod-1" },
"expect": { "success": true }
},
{
"name": "ACCEL_DEADZONE push to slave",
"command": "deadzone",
"input": { "write": true, "slave": "pod-1", "value": 110 },
"expect": { "success": true }
},
{
"name": "OTA_SLAVE_PROGRESS idle",
"command": "ota_progress",
"expect": { "active": false }
},
{
"name": "RESTART slave (last)",
"command": "restart",
"input": { "slave": "pod-1" },
"expect": { "success": true }
}
]
}