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) 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 { 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) 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: 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 { 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 } 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 case "color", "solid", "fill": return 5, 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 } }