diff --git a/goTool/README.md b/goTool/README.md index 8fc0971..893480e 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -43,7 +43,9 @@ Bench **configs** (`testdata/configs/`) list network, MACs, and serial ports (`u ```bash 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 uart_cmds go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 -v ``` diff --git a/goTool/autotest/runner.go b/goTool/autotest/runner.go index 25c4f8d..955f7b0 100644 --- a/goTool/autotest/runner.go +++ b/goTool/autotest/runner.go @@ -15,6 +15,10 @@ type MasterClient interface { ListClients() ([]*pb.ClientInfo, error) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, 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 { @@ -92,6 +96,14 @@ func runStep(bench *Bench, step Step, client MasterClient) error { return checkDeadzone(bench, step, client) case "unicast_test", "unicast": 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": return checkReset(bench, step) default: @@ -310,3 +322,203 @@ func checkUnicastTest(bench *Bench, step Step, client MasterClient) error { } 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 + } +} diff --git a/goTool/autotest/runner_test.go b/goTool/autotest/runner_test.go new file mode 100644 index 0000000..d49d85a --- /dev/null +++ b/goTool/autotest/runner_test.go @@ -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) + } + } +} diff --git a/goTool/autotest/scenario.go b/goTool/autotest/scenario.go index ef38268..a5688eb 100644 --- a/goTool/autotest/scenario.go +++ b/goTool/autotest/scenario.go @@ -45,6 +45,14 @@ type Expect struct { // unicast_test 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) { diff --git a/goTool/client_api.go b/goTool/client_api.go index cd851ca..e4e176c 100644 --- a/goTool/client_api.go +++ b/goTool/client_api.go @@ -222,3 +222,19 @@ func (s *serialPort) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadz func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) { 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) +} diff --git a/goTool/testdata/README.md b/goTool/testdata/README.md index b887b26..b89f0ce 100644 --- a/goTool/testdata/README.md +++ b/goTool/testdata/README.md @@ -44,6 +44,19 @@ Ordered steps: UART commands, delays, or esptool reset. **unicast_test** — `input`: `slave` or `client_id`, `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`). `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). -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 ```bash cd goTool 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 -list-configs +go run . test -list-scenarios ``` diff --git a/goTool/testdata/scenarios/uart_cmds.json b/goTool/testdata/scenarios/uart_cmds.json new file mode 100644 index 0000000..74d7291 --- /dev/null +++ b/goTool/testdata/scenarios/uart_cmds.json @@ -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 } + } + ] +}