Add goTool autotest with bench configs and UART scenarios.
JSON configs describe network and node MACs; scenarios run command sequences with expect checks. Share UART client API across CLI and tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a8ae65d9dc
commit
d24b0cb5c3
15
Makefile
15
Makefile
@ -6,8 +6,12 @@ GOTOOL := $(GOTOOL_DIR)/gotool
|
||||
GOTOOL_RUN := cd $(GOTOOL_DIR) && go run . -port $(PORT)
|
||||
|
||||
.PHONY: default proto_generate proto_generate_uart proto_generate_espnow \
|
||||
gotool-build gotool-proto gotool-tidy \
|
||||
gotool-version gotool-clients gotool-unicast-test gotool-deadzone-get gotool-deadzone-set
|
||||
gotool-build gotool-proto gotool-tidy gotool-test-units \
|
||||
gotool-version gotool-clients gotool-unicast-test gotool-deadzone-get gotool-deadzone-set \
|
||||
gotool-test
|
||||
|
||||
TEST_CONFIG ?= example-lab
|
||||
TEST_SCENARIO ?= smoke
|
||||
|
||||
default:
|
||||
@echo "Targets: proto_generate gotool-build gotool-clients gotool-version …"
|
||||
@ -51,4 +55,11 @@ gotool-deadzone-set: $(GOTOOL)
|
||||
@test -n "$(DEADZONE)" || (echo "Usage: make gotool-deadzone-set DEADZONE=100 [CLIENT=16]"; exit 1)
|
||||
$(GOTOOL) -port $(PORT) deadzone -set -value $(DEADZONE) -client $(or $(CLIENT),0)
|
||||
|
||||
gotool-test-units:
|
||||
cd $(GOTOOL_DIR) && go test ./...
|
||||
|
||||
# CONFIG=example-lab SCENARIO=smoke
|
||||
gotool-test: $(GOTOOL)
|
||||
$(GOTOOL) -port $(PORT) test -config $(TEST_CONFIG) -scenario $(TEST_SCENARIO)
|
||||
|
||||
$(GOTOOL): gotool-build
|
||||
|
||||
@ -25,9 +25,23 @@ go run . -port /dev/ttyUSB0 clients
|
||||
| `version` | `0x03` | Prints `version` and `git_hash` from firmware |
|
||||
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||
|
||||
`clients` requires slaves to have responded to master discover broadcasts first.
|
||||
|
||||
### Automated tests
|
||||
|
||||
Bench **configs** (`testdata/configs/`) list master/slave MACs. **Scenarios** (`testdata/scenarios/`) run a sequence of the same UART commands with `input` / `expect` checks.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 test -list-configs
|
||||
go run . -port /dev/ttyUSB0 test -list-scenarios
|
||||
go run . -port /dev/ttyUSB0 test -config example-lab -scenario smoke
|
||||
go run . -port /dev/ttyUSB0 test -config testdata/configs/my-lab.json -scenario smoke -v
|
||||
```
|
||||
|
||||
See [`testdata/README.md`](testdata/README.md) for the JSON schema.
|
||||
|
||||
```bash
|
||||
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
||||
```
|
||||
|
||||
133
goTool/autotest/config.go
Normal file
133
goTool/autotest/config.go
Normal file
@ -0,0 +1,133 @@
|
||||
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])
|
||||
}
|
||||
29
goTool/autotest/config_test.go
Normal file
29
goTool/autotest/config_test.go
Normal file
@ -0,0 +1,29 @@
|
||||
package autotest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestNewBenchClientIDFromMAC(t *testing.T) {
|
||||
cfg := Config{
|
||||
ID: "t",
|
||||
Network: 1,
|
||||
MasterMAC: "aa:bb:cc:dd:ee:ff",
|
||||
Slaves: []SlaveNode{{
|
||||
ID: "pod",
|
||||
MAC: "50:78:7d:18:01:10",
|
||||
}},
|
||||
}
|
||||
b, err := NewBench(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s, _ := b.Slave("pod")
|
||||
if *s.ClientID != 16 {
|
||||
t.Fatalf("client_id=%d want 16", *s.ClientID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMACEqual(t *testing.T) {
|
||||
if !MACEqual("50:78:7D:18:01:10", "50-78-7d-18-01-10") {
|
||||
t.Fatal("MACEqual failed")
|
||||
}
|
||||
}
|
||||
79
goTool/autotest/paths.go
Normal file
79
goTool/autotest/paths.go
Normal file
@ -0,0 +1,79 @@
|
||||
package autotest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
ConfigDir = "testdata/configs"
|
||||
ScenarioDir = "testdata/scenarios"
|
||||
)
|
||||
|
||||
func ResolveConfigPath(name string) (string, error) {
|
||||
return resolveJSON(name, ConfigDir)
|
||||
}
|
||||
|
||||
func ResolveScenarioPath(name string) (string, error) {
|
||||
return resolveJSON(name, ScenarioDir)
|
||||
}
|
||||
|
||||
func resolveJSON(name, dir string) (string, error) {
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("empty name")
|
||||
}
|
||||
if filepath.Ext(name) == ".json" {
|
||||
if fileExists(name) {
|
||||
return name, nil
|
||||
}
|
||||
}
|
||||
base := name
|
||||
if filepath.Ext(base) != ".json" {
|
||||
base += ".json"
|
||||
}
|
||||
for _, root := range configRoots() {
|
||||
p := filepath.Join(root, dir, base)
|
||||
if fileExists(p) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("not found: %s (searched %s under gotool)", base, dir)
|
||||
}
|
||||
|
||||
func configRoots() []string {
|
||||
var roots []string
|
||||
if wd, err := os.Getwd(); err == nil {
|
||||
roots = append(roots, wd)
|
||||
}
|
||||
// When run from repo root via make.
|
||||
roots = append(roots, "goTool", filepath.Join("..", "goTool"))
|
||||
return roots
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
st, err := os.Stat(path)
|
||||
return err == nil && !st.IsDir()
|
||||
}
|
||||
|
||||
func ListJSONFiles(dir string) ([]string, error) {
|
||||
for _, root := range configRoots() {
|
||||
p := filepath.Join(root, dir)
|
||||
entries, err := os.ReadDir(p)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
if e.IsDir() || filepath.Ext(e.Name()) != ".json" {
|
||||
continue
|
||||
}
|
||||
names = append(names, strings.TrimSuffix(e.Name(), ".json"))
|
||||
}
|
||||
if len(names) > 0 {
|
||||
return names, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("directory %s not found", dir)
|
||||
}
|
||||
281
goTool/autotest/runner.go
Normal file
281
goTool/autotest/runner.go
Normal file
@ -0,0 +1,281 @@
|
||||
package autotest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
// MasterClient talks to the powerpod master over UART (implemented by main.serialPort).
|
||||
type MasterClient interface {
|
||||
GetVersion() (*pb.VersionResponse, error)
|
||||
ListClients() ([]*pb.ClientInfo, error)
|
||||
AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error)
|
||||
EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error)
|
||||
}
|
||||
|
||||
type StepResult struct {
|
||||
Index int
|
||||
Name string
|
||||
Command string
|
||||
Pass bool
|
||||
Detail string
|
||||
}
|
||||
|
||||
type RunResult struct {
|
||||
ConfigID string
|
||||
ScenarioID string
|
||||
Steps []StepResult
|
||||
Passed int
|
||||
Failed int
|
||||
}
|
||||
|
||||
func Run(bench *Bench, sc *Scenario, client MasterClient) (*RunResult, error) {
|
||||
if sc.ConfigID != bench.ID {
|
||||
return nil, fmt.Errorf("scenario %q expects config %q, got %q", sc.ID, sc.ConfigID, bench.ID)
|
||||
}
|
||||
|
||||
res := &RunResult{ConfigID: bench.ID, ScenarioID: sc.ID}
|
||||
for i, step := range sc.Steps {
|
||||
sr := StepResult{Index: i + 1, Name: step.Name, Command: step.Command}
|
||||
if step.Name == "" {
|
||||
sr.Name = fmt.Sprintf("step %d", i+1)
|
||||
}
|
||||
|
||||
if step.DelayMS > 0 && step.Command == "" {
|
||||
time.Sleep(time.Duration(step.DelayMS) * time.Millisecond)
|
||||
sr.Pass = true
|
||||
sr.Detail = fmt.Sprintf("delay %d ms", step.DelayMS)
|
||||
res.Steps = append(res.Steps, sr)
|
||||
res.Passed++
|
||||
continue
|
||||
}
|
||||
|
||||
if step.Command == "" {
|
||||
sr.Pass = false
|
||||
sr.Detail = "step needs command or delay_ms"
|
||||
res.Steps = append(res.Steps, sr)
|
||||
res.Failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if step.DelayMS > 0 {
|
||||
time.Sleep(time.Duration(step.DelayMS) * time.Millisecond)
|
||||
}
|
||||
|
||||
err := runStep(bench, step, client)
|
||||
if err != nil {
|
||||
sr.Pass = false
|
||||
sr.Detail = err.Error()
|
||||
res.Failed++
|
||||
} else {
|
||||
sr.Pass = true
|
||||
sr.Detail = "ok"
|
||||
res.Passed++
|
||||
}
|
||||
res.Steps = append(res.Steps, sr)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func runStep(bench *Bench, step Step, client MasterClient) error {
|
||||
cmd := strings.ToLower(strings.ReplaceAll(step.Command, "-", "_"))
|
||||
switch cmd {
|
||||
case "version":
|
||||
return checkVersion(step, client)
|
||||
case "clients", "client_info":
|
||||
return checkClients(bench, step, client)
|
||||
case "deadzone", "accel_deadzone":
|
||||
return checkDeadzone(bench, step, client)
|
||||
case "unicast_test", "unicast":
|
||||
return checkUnicastTest(bench, step, client)
|
||||
default:
|
||||
return fmt.Errorf("unknown command %q", step.Command)
|
||||
}
|
||||
}
|
||||
|
||||
func checkVersion(step Step, client MasterClient) error {
|
||||
ver, err := client.GetVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e := step.Expect
|
||||
if e.Version != nil && ver.GetVersion() != *e.Version {
|
||||
return fmt.Errorf("version=%d want %d", ver.GetVersion(), *e.Version)
|
||||
}
|
||||
if e.VersionMin != nil && ver.GetVersion() < *e.VersionMin {
|
||||
return fmt.Errorf("version=%d want >= %d", ver.GetVersion(), *e.VersionMin)
|
||||
}
|
||||
if e.GitHash != "" && ver.GetGitHash() != e.GitHash {
|
||||
return fmt.Errorf("git_hash=%q want %q", ver.GetGitHash(), e.GitHash)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkClients(bench *Bench, step Step, client MasterClient) error {
|
||||
clients, err := client.ListClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
e := step.Expect
|
||||
n := len(clients)
|
||||
|
||||
if e.ClientCount != nil && n != *e.ClientCount {
|
||||
return fmt.Errorf("client_count=%d want %d", n, *e.ClientCount)
|
||||
}
|
||||
if e.MinClients != nil && n < *e.MinClients {
|
||||
return fmt.Errorf("client_count=%d want >= %d", n, *e.MinClients)
|
||||
}
|
||||
if e.MaxClients != nil && n > *e.MaxClients {
|
||||
return fmt.Errorf("client_count=%d want <= %d", n, *e.MaxClients)
|
||||
}
|
||||
|
||||
slaveNames := e.Slaves
|
||||
if e.Slave != "" {
|
||||
slaveNames = append(slaveNames, e.Slave)
|
||||
}
|
||||
for _, name := range slaveNames {
|
||||
want, err := bench.Slave(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var found *pb.ClientInfo
|
||||
for _, c := range clients {
|
||||
if MACEqual(MACFromProto(c.GetMac()), want.MAC) {
|
||||
found = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return fmt.Errorf("slave %q mac %s not in client list", name, want.MAC)
|
||||
}
|
||||
if uint32(*want.ClientID) != found.GetId() {
|
||||
return fmt.Errorf("slave %q id=%d want client_id=%d", name, found.GetId(), *want.ClientID)
|
||||
}
|
||||
if e.Available != nil && found.GetAvailable() != *e.Available {
|
||||
return fmt.Errorf("slave %q available=%v want %v", name, found.GetAvailable(), *e.Available)
|
||||
}
|
||||
if e.MAC != "" && !MACEqual(MACFromProto(found.GetMac()), e.MAC) {
|
||||
return fmt.Errorf("slave %q mac=%s want %s", name, MACFromProto(found.GetMac()), e.MAC)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type deadzoneInput struct {
|
||||
Write bool `json:"write"`
|
||||
Value uint `json:"value"`
|
||||
Deadzone uint `json:"deadzone"`
|
||||
ClientID *uint `json:"client_id"`
|
||||
Client uint `json:"client"`
|
||||
Slave string `json:"slave"`
|
||||
AllClients bool `json:"all_clients"`
|
||||
}
|
||||
|
||||
func checkDeadzone(bench *Bench, step Step, client MasterClient) error {
|
||||
var in deadzoneInput
|
||||
if len(step.Input) > 0 {
|
||||
if err := json.Unmarshal(step.Input, &in); err != nil {
|
||||
return fmt.Errorf("input: %w", err)
|
||||
}
|
||||
}
|
||||
dz := in.Value
|
||||
if dz == 0 {
|
||||
dz = in.Deadzone
|
||||
}
|
||||
|
||||
var clientID uint32
|
||||
switch {
|
||||
case in.AllClients:
|
||||
// client_id 0 with all_clients set
|
||||
case in.Slave != "":
|
||||
id, err := bench.ResolveClientID(in.Slave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientID = id
|
||||
case in.ClientID != nil:
|
||||
clientID = uint32(*in.ClientID)
|
||||
default:
|
||||
clientID = uint32(in.Client)
|
||||
}
|
||||
|
||||
req := &pb.AccelDeadzoneRequest{
|
||||
Write: in.Write,
|
||||
Deadzone: uint32(dz),
|
||||
ClientId: clientID,
|
||||
AllClients: in.AllClients,
|
||||
}
|
||||
resp, err := client.AccelDeadzone(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e := step.Expect
|
||||
if e.Deadzone != nil && resp.GetDeadzone() != *e.Deadzone {
|
||||
return fmt.Errorf("deadzone=%d want %d", resp.GetDeadzone(), *e.Deadzone)
|
||||
}
|
||||
if e.Success != nil && resp.GetSuccess() != *e.Success {
|
||||
return fmt.Errorf("success=%v want %v", resp.GetSuccess(), *e.Success)
|
||||
}
|
||||
if e.SlavesUpdated != nil && resp.GetSlavesUpdated() != *e.SlavesUpdated {
|
||||
return fmt.Errorf("slaves_updated=%d want %d", resp.GetSlavesUpdated(), *e.SlavesUpdated)
|
||||
}
|
||||
if e.SlavesUpdatedMin != nil && resp.GetSlavesUpdated() < *e.SlavesUpdatedMin {
|
||||
return fmt.Errorf("slaves_updated=%d want >= %d", resp.GetSlavesUpdated(), *e.SlavesUpdatedMin)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type unicastInput struct {
|
||||
Seq uint `json:"seq"`
|
||||
ClientID *uint `json:"client_id"`
|
||||
Client uint `json:"client"`
|
||||
Slave string `json:"slave"`
|
||||
}
|
||||
|
||||
func checkUnicastTest(bench *Bench, step Step, client MasterClient) error {
|
||||
var in unicastInput
|
||||
if len(step.Input) > 0 {
|
||||
if err := json.Unmarshal(step.Input, &in); err != nil {
|
||||
return fmt.Errorf("input: %w", err)
|
||||
}
|
||||
}
|
||||
if in.Seq == 0 {
|
||||
in.Seq = 1
|
||||
}
|
||||
|
||||
var clientID uint32
|
||||
switch {
|
||||
case in.Slave != "":
|
||||
id, err := bench.ResolveClientID(in.Slave)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientID = id
|
||||
case in.ClientID != nil:
|
||||
clientID = uint32(*in.ClientID)
|
||||
default:
|
||||
clientID = uint32(in.Client)
|
||||
}
|
||||
if clientID == 0 {
|
||||
return fmt.Errorf("unicast_test: client_id or slave required")
|
||||
}
|
||||
|
||||
resp, err := client.EspnowUnicastTest(clientID, uint32(in.Seq))
|
||||
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.Seq != nil && resp.GetSeq() != *e.Seq {
|
||||
return fmt.Errorf("seq=%d want %d", resp.GetSeq(), *e.Seq)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
69
goTool/autotest/scenario.go
Normal file
69
goTool/autotest/scenario.go
Normal file
@ -0,0 +1,69 @@
|
||||
package autotest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Scenario is an ordered list of steps run against a bench Config.
|
||||
type Scenario struct {
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description,omitempty"`
|
||||
ConfigID string `json:"config"`
|
||||
Steps []Step `json:"steps"`
|
||||
}
|
||||
|
||||
type Step struct {
|
||||
Name string `json:"name,omitempty"`
|
||||
Command string `json:"command,omitempty"`
|
||||
DelayMS int `json:"delay_ms,omitempty"`
|
||||
Input json.RawMessage `json:"input,omitempty"`
|
||||
Expect Expect `json:"expect,omitempty"`
|
||||
}
|
||||
|
||||
type Expect struct {
|
||||
// version
|
||||
Version *uint32 `json:"version,omitempty"`
|
||||
VersionMin *uint32 `json:"version_min,omitempty"`
|
||||
GitHash string `json:"git_hash,omitempty"`
|
||||
|
||||
// clients
|
||||
MinClients *int `json:"min_clients,omitempty"`
|
||||
MaxClients *int `json:"max_clients,omitempty"`
|
||||
ClientCount *int `json:"client_count,omitempty"`
|
||||
Slave string `json:"slave,omitempty"`
|
||||
Slaves []string `json:"slaves,omitempty"`
|
||||
Available *bool `json:"available,omitempty"`
|
||||
MAC string `json:"mac,omitempty"`
|
||||
|
||||
// deadzone
|
||||
Deadzone *uint32 `json:"deadzone,omitempty"`
|
||||
Success *bool `json:"success,omitempty"`
|
||||
SlavesUpdated *uint32 `json:"slaves_updated,omitempty"`
|
||||
SlavesUpdatedMin *uint32 `json:"slaves_updated_min,omitempty"`
|
||||
|
||||
// unicast_test
|
||||
Seq *uint32 `json:"seq,omitempty"`
|
||||
}
|
||||
|
||||
func LoadScenario(path string) (*Scenario, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var sc Scenario
|
||||
if err := json.Unmarshal(data, &sc); err != nil {
|
||||
return nil, fmt.Errorf("parse scenario: %w", err)
|
||||
}
|
||||
if sc.ID == "" {
|
||||
return nil, fmt.Errorf("scenario: id is required")
|
||||
}
|
||||
if sc.ConfigID == "" {
|
||||
return nil, fmt.Errorf("scenario %q: config is required", sc.ID)
|
||||
}
|
||||
if len(sc.Steps) == 0 {
|
||||
return nil, fmt.Errorf("scenario %q: no steps", sc.ID)
|
||||
}
|
||||
return &sc, nil
|
||||
}
|
||||
114
goTool/client_api.go
Normal file
114
goTool/client_api.go
Normal file
@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
func (s *serialPort) getVersion() (*pb.VersionResponse, error) {
|
||||
payload, err := s.exchange(byte(pb.MessageType_VERSION), "VERSION")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var msg pb.UartMessage
|
||||
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
if msg.GetType() != pb.MessageType_VERSION {
|
||||
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
|
||||
}
|
||||
ver := msg.GetVersionResponse()
|
||||
if ver == nil {
|
||||
return nil, fmt.Errorf("missing version_response")
|
||||
}
|
||||
return ver, nil
|
||||
}
|
||||
|
||||
func (s *serialPort) listClients() ([]*pb.ClientInfo, error) {
|
||||
payload, err := s.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var msg pb.UartMessage
|
||||
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
if msg.GetType() != pb.MessageType_CLIENT_INFO {
|
||||
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
|
||||
}
|
||||
info := msg.GetClientInfoResponse()
|
||||
if info == nil {
|
||||
return nil, fmt.Errorf("missing client_info_response")
|
||||
}
|
||||
return info.GetClients(), nil
|
||||
}
|
||||
|
||||
func (s *serialPort) accelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_ACCEL_DEADZONE,
|
||||
Payload: &pb.UartMessage_AccelDeadzoneRequest{
|
||||
AccelDeadzoneRequest: req,
|
||||
},
|
||||
}
|
||||
body, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode: %w", err)
|
||||
}
|
||||
payload := append([]byte{byte(pb.MessageType_ACCEL_DEADZONE)}, body...)
|
||||
respPayload, err := s.exchangePayload(payload, "ACCEL_DEADZONE")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respMsg pb.UartMessage
|
||||
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
r := respMsg.GetAccelDeadzoneResponse()
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("missing accel_deadzone_response")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
|
||||
req := &pb.EspNowUnicastTestRequest{ClientId: clientID, Seq: seq}
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_ESPNOW_UNICAST_TEST,
|
||||
Payload: &pb.UartMessage_EspnowUnicastTestRequest{
|
||||
EspnowUnicastTestRequest: req,
|
||||
},
|
||||
}
|
||||
body, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encode: %w", err)
|
||||
}
|
||||
payload := append([]byte{byte(pb.MessageType_ESPNOW_UNICAST_TEST)}, body...)
|
||||
respPayload, err := s.exchangePayload(payload, "ESPNOW_UNICAST_TEST")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var respMsg pb.UartMessage
|
||||
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||
return nil, fmt.Errorf("decode: %w", err)
|
||||
}
|
||||
r := respMsg.GetEspnowUnicastTestResponse()
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf("missing espnow_unicast_test_response")
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (s *serialPort) GetVersion() (*pb.VersionResponse, error) { return s.getVersion() }
|
||||
|
||||
func (s *serialPort) ListClients() ([]*pb.ClientInfo, error) { return s.listClients() }
|
||||
|
||||
func (s *serialPort) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
||||
return s.accelDeadzone(req)
|
||||
}
|
||||
|
||||
func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
|
||||
return s.espnowUnicastTest(clientID, seq)
|
||||
}
|
||||
102
goTool/cmd_autotest.go
Normal file
102
goTool/cmd_autotest.go
Normal file
@ -0,0 +1,102 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"powerpod/gotool/autotest"
|
||||
)
|
||||
|
||||
func runTest(sp *serialPort, args []string) error {
|
||||
fs := flag.NewFlagSet("test", flag.ExitOnError)
|
||||
configName := fs.String("config", "", "bench config id or path (testdata/configs/)")
|
||||
scenarioName := fs.String("scenario", "", "scenario id or path (testdata/scenarios/)")
|
||||
listConfigs := fs.Bool("list-configs", false, "list available bench configs")
|
||||
listScenarios := fs.Bool("list-scenarios", false, "list available scenarios")
|
||||
verbose := fs.Bool("v", false, "verbose (log UART traffic)")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if *listConfigs {
|
||||
return printList("configs", autotest.ConfigDir)
|
||||
}
|
||||
if *listScenarios {
|
||||
return printList("scenarios", autotest.ScenarioDir)
|
||||
}
|
||||
|
||||
if *configName == "" || *scenarioName == "" {
|
||||
return fmt.Errorf("need -config and -scenario (or -list-configs / -list-scenarios)")
|
||||
}
|
||||
|
||||
configPath, err := autotest.ResolveConfigPath(*configName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scenarioPath, err := autotest.ResolveScenarioPath(*scenarioName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bench, err := autotest.LoadConfig(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load config: %w", err)
|
||||
}
|
||||
sc, err := autotest.LoadScenario(scenarioPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load scenario: %w", err)
|
||||
}
|
||||
|
||||
if !*verbose {
|
||||
log.SetOutput(io.Discard)
|
||||
}
|
||||
|
||||
fmt.Printf("bench config %q (network %d, master %s)\n",
|
||||
bench.ID, bench.Network, bench.MasterMAC)
|
||||
for _, s := range bench.Slaves {
|
||||
fmt.Printf(" slave %q mac=%s client_id=%d\n", s.ID, s.MAC, *s.ClientID)
|
||||
}
|
||||
fmt.Printf("scenario %q (%d steps)\n\n", sc.ID, len(sc.Steps))
|
||||
|
||||
result, err := autotest.Run(bench, sc, sp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
failed := false
|
||||
for _, sr := range result.Steps {
|
||||
mark := "PASS"
|
||||
if !sr.Pass {
|
||||
mark = "FAIL"
|
||||
failed = true
|
||||
}
|
||||
cmd := sr.Command
|
||||
if cmd == "" {
|
||||
cmd = "delay"
|
||||
}
|
||||
fmt.Printf("[%s] %s (%s) — %s\n", mark, sr.Name, cmd, sr.Detail)
|
||||
}
|
||||
fmt.Printf("\n%d passed, %d failed\n", result.Passed, result.Failed)
|
||||
if failed {
|
||||
return fmt.Errorf("scenario %q failed", sc.ID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func printList(kind, dir string) error {
|
||||
names, err := autotest.ListJSONFiles(dir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(names) == 0 {
|
||||
fmt.Printf("no %s found under %s\n", kind, dir)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("%s:\n", kind)
|
||||
for _, n := range names {
|
||||
fmt.Printf(" %s\n", n)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -3,33 +3,13 @@ package main
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
func runClients(sp *serialPort) error {
|
||||
payload, err := sp.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
|
||||
clients, err := sp.listClients()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msg pb.UartMessage
|
||||
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||
return fmt.Errorf("decode protobuf: %w", err)
|
||||
}
|
||||
|
||||
if msg.GetType() != pb.MessageType_CLIENT_INFO {
|
||||
return fmt.Errorf("unexpected message type %v", msg.GetType())
|
||||
}
|
||||
|
||||
info := msg.GetClientInfoResponse()
|
||||
if info == nil {
|
||||
return fmt.Errorf("response missing client_info_response")
|
||||
}
|
||||
|
||||
clients := info.GetClients()
|
||||
if len(clients) == 0 {
|
||||
fmt.Println("no clients registered")
|
||||
return nil
|
||||
|
||||
@ -4,8 +4,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
@ -19,39 +17,16 @@ func runDeadzone(sp *serialPort, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &pb.AccelDeadzoneRequest{
|
||||
Write: *write,
|
||||
Deadzone: uint32(*deadzone),
|
||||
ClientId: uint32(*clientID),
|
||||
AllClients: *all,
|
||||
}
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_ACCEL_DEADZONE,
|
||||
Payload: &pb.UartMessage_AccelDeadzoneRequest{
|
||||
AccelDeadzoneRequest: req,
|
||||
},
|
||||
}
|
||||
body, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
|
||||
payload := append([]byte{byte(pb.MessageType_ACCEL_DEADZONE)}, body...)
|
||||
respPayload, err := sp.exchangePayload(payload, "ACCEL_DEADZONE")
|
||||
r, err := sp.accelDeadzone(&pb.AccelDeadzoneRequest{
|
||||
Write: *write,
|
||||
Deadzone: uint32(*deadzone),
|
||||
ClientId: uint32(*clientID),
|
||||
AllClients: *all,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var respMsg pb.UartMessage
|
||||
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||
return fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
r := respMsg.GetAccelDeadzoneResponse()
|
||||
if r == nil {
|
||||
return fmt.Errorf("response missing accel_deadzone_response")
|
||||
}
|
||||
|
||||
fmt.Printf("deadzone=%d client_id=%d success=%v slaves_updated=%d\n",
|
||||
r.GetDeadzone(), r.GetClientId(), r.GetSuccess(), r.GetSlavesUpdated())
|
||||
return nil
|
||||
|
||||
@ -3,10 +3,6 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
func runUnicastTest(sp *serialPort, args []string) error {
|
||||
@ -20,37 +16,11 @@ func runUnicastTest(sp *serialPort, args []string) error {
|
||||
return fmt.Errorf("client id required (see `gotool clients`)")
|
||||
}
|
||||
|
||||
req := &pb.EspNowUnicastTestRequest{
|
||||
ClientId: uint32(*clientID),
|
||||
Seq: uint32(*seq),
|
||||
}
|
||||
msg := &pb.UartMessage{
|
||||
Type: pb.MessageType_ESPNOW_UNICAST_TEST,
|
||||
Payload: &pb.UartMessage_EspnowUnicastTestRequest{
|
||||
EspnowUnicastTestRequest: req,
|
||||
},
|
||||
}
|
||||
body, err := proto.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encode request: %w", err)
|
||||
}
|
||||
|
||||
payload := append([]byte{byte(pb.MessageType_ESPNOW_UNICAST_TEST)}, body...)
|
||||
respPayload, err := sp.exchangePayload(payload, "ESPNOW_UNICAST_TEST")
|
||||
r, err := sp.espnowUnicastTest(uint32(*clientID), uint32(*seq))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var respMsg pb.UartMessage
|
||||
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||
return fmt.Errorf("decode response: %w", err)
|
||||
}
|
||||
|
||||
r := respMsg.GetEspnowUnicastTestResponse()
|
||||
if r == nil {
|
||||
return fmt.Errorf("response missing espnow_unicast_test_response")
|
||||
}
|
||||
|
||||
fmt.Printf("unicast test sent: success=%v seq=%d\n", r.GetSuccess(), r.GetSeq())
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2,32 +2,13 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"powerpod/gotool/pb"
|
||||
)
|
||||
|
||||
func runVersion(sp *serialPort) error {
|
||||
payload, err := sp.exchange(byte(pb.MessageType_VERSION), "VERSION")
|
||||
ver, err := sp.getVersion()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var msg pb.UartMessage
|
||||
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||
return fmt.Errorf("decode protobuf: %w", err)
|
||||
}
|
||||
|
||||
if msg.GetType() != pb.MessageType_VERSION {
|
||||
return fmt.Errorf("unexpected message type %v", msg.GetType())
|
||||
}
|
||||
|
||||
ver := msg.GetVersionResponse()
|
||||
if ver == nil {
|
||||
return fmt.Errorf("response missing version_response")
|
||||
}
|
||||
|
||||
fmt.Printf("version: %d\n", ver.GetVersion())
|
||||
fmt.Printf("git_hash: %s\n", ver.GetGitHash())
|
||||
return nil
|
||||
|
||||
@ -15,7 +15,8 @@ func usage() {
|
||||
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
|
||||
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
|
||||
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
|
||||
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n\n")
|
||||
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
|
||||
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n\n")
|
||||
flag.PrintDefaults()
|
||||
}
|
||||
|
||||
@ -47,6 +48,8 @@ func main() {
|
||||
runErr = runDeadzone(sp, flag.Args()[1:])
|
||||
case "unicast-test", "unicast_test":
|
||||
runErr = runUnicastTest(sp, flag.Args()[1:])
|
||||
case "test", "autotest":
|
||||
runErr = runTest(sp, flag.Args()[1:])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||
usage()
|
||||
|
||||
47
goTool/testdata/README.md
vendored
Normal file
47
goTool/testdata/README.md
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
# goTool autotest fixtures
|
||||
|
||||
## Bench config (`configs/*.json`)
|
||||
|
||||
Describes your hardware bench (for documentation and slave lookup in tests).
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `id` | Config name; referenced by scenarios |
|
||||
| `network` | ESP-NOW network **1–8** (must match DIP/IO expander on all nodes) |
|
||||
| `master_mac` | Expected WiFi MAC of the master ESP (reference) |
|
||||
| `slaves[].id` | Short name used in scenario `input.slave` / `expect.slave` |
|
||||
| `slaves[].mac` | Full slave STA MAC (must match `gotool clients`) |
|
||||
| `slaves[].client_id` | Optional; default = last MAC byte |
|
||||
|
||||
Copy `example-lab.json` to e.g. `my-lab.json` and set real MACs.
|
||||
|
||||
## Scenario (`scenarios/*.json`)
|
||||
|
||||
Ordered steps: UART commands with `input` and `expect`.
|
||||
|
||||
| Step field | Meaning |
|
||||
|------------|---------|
|
||||
| `delay_ms` | Sleep only (no command) |
|
||||
| `command` | `version`, `clients`, `deadzone`, `unicast_test` |
|
||||
| `input` | Command arguments (see below) |
|
||||
| `expect` | Assertions on the response |
|
||||
|
||||
### Commands
|
||||
|
||||
**version** — `expect`: `version`, `version_min`, `git_hash`
|
||||
|
||||
**clients** — `expect`: `min_clients`, `max_clients`, `client_count`, `slave` / `slaves`, `available`
|
||||
|
||||
**deadzone** — `input`: `write`, `value`/`deadzone`, `slave` or `client`/`client_id`, `all_clients`
|
||||
`expect`: `deadzone`, `success`, `slaves_updated`, `slaves_updated_min`
|
||||
|
||||
**unicast_test** — `input`: `slave` or `client_id`, `seq`
|
||||
`expect`: `success`, `seq`
|
||||
|
||||
### Example
|
||||
|
||||
```bash
|
||||
cd goTool
|
||||
go run . -port /dev/ttyUSB0 test -config my-lab -scenario smoke
|
||||
go run . -port /dev/ttyUSB0 test -list-configs
|
||||
```
|
||||
13
goTool/testdata/configs/example-lab.json
vendored
Normal file
13
goTool/testdata/configs/example-lab.json
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"id": "example-lab",
|
||||
"description": "Example bench — replace MACs and network with your hardware",
|
||||
"network": 1,
|
||||
"master_mac": "50:78:7d:18:00:10",
|
||||
"slaves": [
|
||||
{
|
||||
"id": "pod-1",
|
||||
"mac": "50:78:7d:18:01:10",
|
||||
"client_id": 16
|
||||
}
|
||||
]
|
||||
}
|
||||
51
goTool/testdata/scenarios/smoke.json
vendored
Normal file
51
goTool/testdata/scenarios/smoke.json
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
{
|
||||
"id": "smoke",
|
||||
"description": "Basic master UART checks (no slaves required for version)",
|
||||
"config": "example-lab",
|
||||
"steps": [
|
||||
{
|
||||
"name": "firmware version",
|
||||
"command": "version",
|
||||
"expect": {
|
||||
"version_min": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "wait for ESP-NOW join",
|
||||
"delay_ms": 5000
|
||||
},
|
||||
{
|
||||
"name": "slave visible",
|
||||
"command": "clients",
|
||||
"expect": {
|
||||
"min_clients": 1,
|
||||
"slave": "pod-1",
|
||||
"available": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "unicast path",
|
||||
"command": "unicast_test",
|
||||
"input": {
|
||||
"slave": "pod-1",
|
||||
"seq": 42
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
"seq": 42
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "read local deadzone",
|
||||
"command": "deadzone",
|
||||
"input": {
|
||||
"write": false,
|
||||
"client": 0
|
||||
},
|
||||
"expect": {
|
||||
"success": true,
|
||||
"deadzone": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user