Compare commits
3 Commits
e95097085d
...
0299ba44fd
| Author | SHA1 | Date | |
|---|---|---|---|
| 0299ba44fd | |||
| d24b0cb5c3 | |||
| a8ae65d9dc |
15
Makefile
15
Makefile
@ -6,8 +6,12 @@ GOTOOL := $(GOTOOL_DIR)/gotool
|
|||||||
GOTOOL_RUN := cd $(GOTOOL_DIR) && go run . -port $(PORT)
|
GOTOOL_RUN := cd $(GOTOOL_DIR) && go run . -port $(PORT)
|
||||||
|
|
||||||
.PHONY: default proto_generate proto_generate_uart proto_generate_espnow \
|
.PHONY: default proto_generate proto_generate_uart proto_generate_espnow \
|
||||||
gotool-build gotool-proto gotool-tidy \
|
gotool-build gotool-proto gotool-tidy gotool-test-units \
|
||||||
gotool-version gotool-clients gotool-unicast-test gotool-deadzone-get gotool-deadzone-set
|
gotool-version gotool-clients gotool-unicast-test gotool-deadzone-get gotool-deadzone-set \
|
||||||
|
gotool-test
|
||||||
|
|
||||||
|
TEST_CONFIG ?= example-lab
|
||||||
|
TEST_SCENARIO ?= smoke
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@echo "Targets: proto_generate gotool-build gotool-clients gotool-version …"
|
@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)
|
@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) -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
|
$(GOTOOL): gotool-build
|
||||||
|
|||||||
@ -25,9 +25,24 @@ go run . -port /dev/ttyUSB0 clients
|
|||||||
| `version` | `0x03` | Prints `version` and `git_hash` from firmware |
|
| `version` | `0x03` | Prints `version` and `git_hash` from firmware |
|
||||||
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
||||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
| `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.
|
`clients` requires slaves to have responded to master discover broadcasts first.
|
||||||
|
|
||||||
|
### Automated tests
|
||||||
|
|
||||||
|
Bench **configs** (`testdata/configs/`) list network, MACs, and serial ports (`uart.master` for commands, `*_console` for esptool reset). **Scenarios** run UART commands plus optional `reset` steps.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . test -list-configs
|
||||||
|
go run . test -config example-lab -scenario smoke
|
||||||
|
go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1 -v
|
||||||
|
```
|
||||||
|
|
||||||
|
With a complete bench config, `-port` is optional for `test` (uses `uart.master` from JSON).
|
||||||
|
|
||||||
|
See [`testdata/README.md`](testdata/README.md) for the JSON schema.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
||||||
```
|
```
|
||||||
|
|||||||
152
goTool/autotest/config.go
Normal file
152
goTool/autotest/config.go
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
package autotest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
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])
|
||||||
|
}
|
||||||
32
goTool/autotest/config_test.go
Normal file
32
goTool/autotest/config_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package autotest
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestNewBenchClientIDFromMAC(t *testing.T) {
|
||||||
|
cfg := Config{
|
||||||
|
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",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
103
goTool/autotest/reset.go
Normal file
103
goTool/autotest/reset.go
Normal 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)
|
||||||
|
}
|
||||||
312
goTool/autotest/runner.go
Normal file
312
goTool/autotest/runner.go
Normal file
@ -0,0 +1,312 @@
|
|||||||
|
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)
|
||||||
|
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
|
||||||
|
}
|
||||||
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)
|
||||||
|
}
|
||||||
125
goTool/cmd_autotest.go
Normal file
125
goTool/cmd_autotest.go
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"powerpod/gotool/autotest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runTest(portOverride string, baudOverride int, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
port := portOverride
|
||||||
|
if port == "" {
|
||||||
|
port = bench.UART.Master
|
||||||
|
}
|
||||||
|
baud := baudOverride
|
||||||
|
if baud <= 0 {
|
||||||
|
baud = int(bench.UART.Baud)
|
||||||
|
}
|
||||||
|
|
||||||
|
sp, err := openSerial(port, baud)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open %s: %w", port, err)
|
||||||
|
}
|
||||||
|
defer sp.Close()
|
||||||
|
|
||||||
|
if !*verbose {
|
||||||
|
log.SetOutput(io.Discard)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("bench config %q (network %d, master %s)\n",
|
||||||
|
bench.ID, bench.Network, bench.MasterMAC)
|
||||||
|
fmt.Printf(" uart.master %s (baud %d)\n", bench.UART.Master, bench.UART.Baud)
|
||||||
|
if bench.UART.MasterConsole != "" {
|
||||||
|
fmt.Printf(" uart.master_console %s\n", bench.UART.MasterConsole)
|
||||||
|
}
|
||||||
|
for _, s := range bench.Slaves {
|
||||||
|
line := fmt.Sprintf(" slave %q mac=%s client_id=%d", s.ID, s.MAC, *s.ClientID)
|
||||||
|
if s.Console != "" {
|
||||||
|
line += fmt.Sprintf(" console=%s", s.Console)
|
||||||
|
}
|
||||||
|
fmt.Println(line)
|
||||||
|
}
|
||||||
|
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 (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"powerpod/gotool/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func runClients(sp *serialPort) error {
|
func runClients(sp *serialPort) error {
|
||||||
payload, err := sp.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
|
clients, err := sp.listClients()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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 {
|
if len(clients) == 0 {
|
||||||
fmt.Println("no clients registered")
|
fmt.Println("no clients registered")
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -4,8 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"powerpod/gotool/pb"
|
"powerpod/gotool/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -19,39 +17,16 @@ func runDeadzone(sp *serialPort, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.AccelDeadzoneRequest{
|
r, err := sp.accelDeadzone(&pb.AccelDeadzoneRequest{
|
||||||
Write: *write,
|
Write: *write,
|
||||||
Deadzone: uint32(*deadzone),
|
Deadzone: uint32(*deadzone),
|
||||||
ClientId: uint32(*clientID),
|
ClientId: uint32(*clientID),
|
||||||
AllClients: *all,
|
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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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",
|
fmt.Printf("deadzone=%d client_id=%d success=%v slaves_updated=%d\n",
|
||||||
r.GetDeadzone(), r.GetClientId(), r.GetSuccess(), r.GetSlavesUpdated())
|
r.GetDeadzone(), r.GetClientId(), r.GetSuccess(), r.GetSlavesUpdated())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -3,10 +3,6 @@ package main
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"powerpod/gotool/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func runUnicastTest(sp *serialPort, args []string) error {
|
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`)")
|
return fmt.Errorf("client id required (see `gotool clients`)")
|
||||||
}
|
}
|
||||||
|
|
||||||
req := &pb.EspNowUnicastTestRequest{
|
r, err := sp.espnowUnicastTest(uint32(*clientID), uint32(*seq))
|
||||||
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")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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())
|
fmt.Printf("unicast test sent: success=%v seq=%d\n", r.GetSuccess(), r.GetSeq())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,32 +2,13 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
|
|
||||||
"powerpod/gotool/pb"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func runVersion(sp *serialPort) error {
|
func runVersion(sp *serialPort) error {
|
||||||
payload, err := sp.exchange(byte(pb.MessageType_VERSION), "VERSION")
|
ver, err := sp.getVersion()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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("version: %d\n", ver.GetVersion())
|
||||||
fmt.Printf("git_hash: %s\n", ver.GetGitHash())
|
fmt.Printf("git_hash: %s\n", ver.GetGitHash())
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -10,12 +10,14 @@ import (
|
|||||||
const defaultBaud = 921600
|
const defaultBaud = 921600
|
||||||
|
|
||||||
func usage() {
|
func usage() {
|
||||||
fmt.Fprintf(os.Stderr, "usage: gotool -port /dev/ttyUSB0 <command>\n\n")
|
fmt.Fprintf(os.Stderr, "usage: gotool [-port /dev/ttyUSB0] <command>\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " test uses uart.master from bench config when -port is omitted\n\n")
|
||||||
fmt.Fprintf(os.Stderr, "commands:\n")
|
fmt.Fprintf(os.Stderr, "commands:\n")
|
||||||
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
|
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, " clients registered ESP-NOW slaves on the master\n")
|
||||||
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\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()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,20 +26,28 @@ func main() {
|
|||||||
baud := flag.Int("baud", defaultBaud, "UART baud rate")
|
baud := flag.Int("baud", defaultBaud, "UART baud rate")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if *portName == "" || flag.NArg() < 1 {
|
if flag.NArg() < 1 {
|
||||||
usage()
|
usage()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd := flag.Arg(0)
|
cmd := flag.Arg(0)
|
||||||
|
|
||||||
|
var runErr error
|
||||||
|
switch cmd {
|
||||||
|
case "test", "autotest":
|
||||||
|
runErr = runTest(*portName, *baud, flag.Args()[1:])
|
||||||
|
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test":
|
||||||
|
if *portName == "" {
|
||||||
|
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
||||||
|
usage()
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
sp, err := openSerial(*portName, *baud)
|
sp, err := openSerial(*portName, *baud)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("open serial: %v", err)
|
log.Fatalf("open serial: %v", err)
|
||||||
}
|
}
|
||||||
defer sp.Close()
|
defer sp.Close()
|
||||||
|
|
||||||
var runErr error
|
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "version":
|
case "version":
|
||||||
runErr = runVersion(sp)
|
runErr = runVersion(sp)
|
||||||
@ -47,6 +57,7 @@ func main() {
|
|||||||
runErr = runDeadzone(sp, flag.Args()[1:])
|
runErr = runDeadzone(sp, flag.Args()[1:])
|
||||||
case "unicast-test", "unicast_test":
|
case "unicast-test", "unicast_test":
|
||||||
runErr = runUnicastTest(sp, flag.Args()[1:])
|
runErr = runUnicastTest(sp, flag.Args()[1:])
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
fmt.Fprintf(os.Stderr, "unknown command %q\n\n", cmd)
|
||||||
usage()
|
usage()
|
||||||
|
|||||||
68
goTool/testdata/README.md
vendored
Normal file
68
goTool/testdata/README.md
vendored
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
# goTool autotest fixtures
|
||||||
|
|
||||||
|
## Bench config (`configs/*.json`)
|
||||||
|
|
||||||
|
Describes your hardware bench: network, MACs, and serial ports.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `id` | Config name; referenced by scenarios |
|
||||||
|
| `network` | ESP-NOW network **1–8** (DIP / IO expander on all nodes) |
|
||||||
|
| `master_mac` | Master WiFi STA MAC (reference) |
|
||||||
|
| `uart.baud` | Command UART baud (default **921600**) |
|
||||||
|
| `uart.master` | **gotool** port — external UART adapter on master GPIO2/3 (e.g. `/dev/ttyUSB0`) |
|
||||||
|
| `uart.master_console` | Master USB-JTAG/console for **reset** via esptool (e.g. `/dev/ttyACM0`) |
|
||||||
|
| `slaves[].id` | Short name for scenarios (`input.slave`, `expect.slave`) |
|
||||||
|
| `slaves[].mac` | Slave STA MAC (must match `gotool clients`) |
|
||||||
|
| `slaves[].client_id` | Optional; default = last MAC byte |
|
||||||
|
| `slaves[].console` | Slave USB console for **reset** (optional, e.g. `/dev/ttyACM1`) |
|
||||||
|
|
||||||
|
Copy `example-lab.json` → `my-lab.json` and set real paths (`ls /dev/ttyUSB* /dev/ttyACM*`).
|
||||||
|
|
||||||
|
`gotool test` uses `uart.master` when `-port` is omitted. Override with `-port /dev/…`.
|
||||||
|
|
||||||
|
## Scenario (`scenarios/*.json`)
|
||||||
|
|
||||||
|
Ordered steps: UART commands, delays, or esptool reset.
|
||||||
|
|
||||||
|
| Step field | Meaning |
|
||||||
|
|------------|---------|
|
||||||
|
| `delay_ms` | Sleep only (no command) |
|
||||||
|
| `command` | See below |
|
||||||
|
| `input` | Command arguments |
|
||||||
|
| `expect` | Assertions (UART commands only) |
|
||||||
|
|
||||||
|
### 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`
|
||||||
|
|
||||||
|
**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).
|
||||||
|
|
||||||
|
Example reset steps:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "name": "reset master", "command": "reset", "input": { "target": "master", "wait_ms": 3000 } },
|
||||||
|
{ "name": "reset all", "command": "reset", "input": { "all": true, "wait_ms": 2500 } }
|
||||||
|
```
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd goTool
|
||||||
|
go run . test -config example-lab -scenario smoke
|
||||||
|
go run . test -config my-lab -scenario smoke -port /dev/ttyUSB1
|
||||||
|
go run . test -list-configs
|
||||||
|
```
|
||||||
19
goTool/testdata/configs/example-lab.json
vendored
Normal file
19
goTool/testdata/configs/example-lab.json
vendored
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"id": "example-lab",
|
||||||
|
"description": "Example bench — replace MACs, network, and serial paths",
|
||||||
|
"network": 1,
|
||||||
|
"master_mac": "50:78:7d:18:00:10",
|
||||||
|
"uart": {
|
||||||
|
"baud": 921600,
|
||||||
|
"master": "/dev/ttyUSB0",
|
||||||
|
"master_console": "/dev/ttyACM0"
|
||||||
|
},
|
||||||
|
"slaves": [
|
||||||
|
{
|
||||||
|
"id": "pod-1",
|
||||||
|
"mac": "50:78:7d:18:01:10",
|
||||||
|
"client_id": 16,
|
||||||
|
"console": "/dev/ttyACM1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
59
goTool/testdata/scenarios/smoke.json
vendored
Normal file
59
goTool/testdata/scenarios/smoke.json
vendored
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"id": "smoke",
|
||||||
|
"description": "Cold-start smoke: reset all nodes, wait for join, then UART checks",
|
||||||
|
"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": "master UART up",
|
||||||
|
"command": "version",
|
||||||
|
"expect": {
|
||||||
|
"version_min": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "slave registered",
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -50,13 +50,43 @@ Pins (`powerpod.h`):
|
|||||||
| UART TX | 3 |
|
| UART TX | 3 |
|
||||||
| UART RX | 2 |
|
| UART RX | 2 |
|
||||||
| LED ring | 7 |
|
| LED ring | 7 |
|
||||||
|
| BMA456 INT | 10 |
|
||||||
|
|
||||||
Startup order:
|
Startup order:
|
||||||
|
|
||||||
1. Read DIP + IO expander → `app_config`
|
1. Read DIP + IO expander → `app_config`
|
||||||
2. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
|
2. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
|
||||||
3. `led_ring_init()`
|
3. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
|
||||||
4. **Master only:** command queue, UART, registered commands (e.g. VERSION)
|
4. `led_ring_init()`
|
||||||
|
5. **Master only:** command queue, UART, registered commands (e.g. VERSION)
|
||||||
|
|
||||||
|
## BMA456 accelerometer (`bosch456.c`)
|
||||||
|
|
||||||
|
Powerpod uses the Bosch **BMA456H** (hearable) variant, not the generic `bma456w` examples in the vendor tree.
|
||||||
|
|
||||||
|
| Item | Value |
|
||||||
|
|------|--------|
|
||||||
|
| Project wrapper | `main/bosch456.c`, `main/bosch456.h` |
|
||||||
|
| Vendor component | `components/bma456` — only `bma4.c` + `bma456h.c` are linked |
|
||||||
|
| I2C | Shared bus with IO expander (SCL/SDA GPIO 5/6), address **0x18**, **100 kHz** |
|
||||||
|
| Interrupt | **GPIO 10**, active high, tap events (single / double / triple) |
|
||||||
|
| Polling | FreeRTOS task `bma456_poll`, **10 Hz** accel read |
|
||||||
|
|
||||||
|
**Boot:** `init_bma456(bus_handle)` runs on **master and slave** after the IO expander. If the sensor is missing or init fails, firmware logs `BMA456 init skipped` and continues (`bma456_is_ready() == false`).
|
||||||
|
|
||||||
|
**Accel logging:** Samples are printed only when any axis changes by more than the **deadzone** (raw LSB) since the last logged sample (default **100**). This is a **software** filter on top of the sensor; it does not change BMA456 hardware thresholds.
|
||||||
|
|
||||||
|
**Configuration paths:**
|
||||||
|
|
||||||
|
| Path | Effect |
|
||||||
|
|------|--------|
|
||||||
|
| UART `ACCEL_DEADZONE` with `client_id = 0` | `bma456_set_accel_deadzone()` on the local node |
|
||||||
|
| ESP-NOW `SET_ACCEL_DEADZONE` | Same on a slave (no-op log path if sensor not installed) |
|
||||||
|
| `make gotool-deadzone-set DEADZONE=… CLIENT=0` | Host shortcut for local deadzone |
|
||||||
|
|
||||||
|
**Logs:** `[BMA456] ACC X=… Y=… Z=…` when deadzone exceeded; `[BMA456] tap: single|double|triple` on interrupt.
|
||||||
|
|
||||||
|
Regenerate nanopb only when changing protos; sensor code has no code generation step.
|
||||||
|
|
||||||
## ESP-NOW discovery
|
## ESP-NOW discovery
|
||||||
|
|
||||||
@ -177,7 +207,7 @@ Encoding: `uart_send_uart_message()` in `uart_proto.c`.
|
|||||||
|
|
||||||
### ACCEL_DEADZONE command
|
### ACCEL_DEADZONE command
|
||||||
|
|
||||||
Filters BMA456 logs: a new accel line is emitted only when any axis changes by more than `deadzone` raw LSB since the last reported sample (default **100**).
|
Sets the **software** deadzone used by `bosch456.c` when logging accel (see [BMA456 accelerometer](#bma456-accelerometer-bosch456c)). Default **100** LSB.
|
||||||
|
|
||||||
**Request:** framed `06` + nanopb `UartMessage` with `accel_deadzone_request`:
|
**Request:** framed `06` + nanopb `UartMessage` with `accel_deadzone_request`:
|
||||||
|
|
||||||
@ -279,6 +309,7 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
|||||||
| `cmd_version.c/h` | VERSION handler |
|
| `cmd_version.c/h` | VERSION handler |
|
||||||
| `cmd_client_info.c/h` | CLIENT_INFO handler |
|
| `cmd_client_info.c/h` | CLIENT_INFO handler |
|
||||||
| `client_registry.c/h` | Registered slave table |
|
| `client_registry.c/h` | Registered slave table |
|
||||||
|
| `bosch456.c/h` | BMA456H I2C driver, accel poll, tap INT, deadzone filter |
|
||||||
| `led_ring.c/h` | LED digit display |
|
| `led_ring.c/h` | LED digit display |
|
||||||
| `proto/uart_messages.proto` | UART protocol schema |
|
| `proto/uart_messages.proto` | UART protocol schema |
|
||||||
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
|
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
|
||||||
|
|||||||
410
main/bosch456.c
410
main/bosch456.c
@ -1,134 +1,90 @@
|
|||||||
|
/**
|
||||||
|
* BMA456H integration for Powerpod (ESP-IDF I2C master + Bosch SensorAPI).
|
||||||
|
*
|
||||||
|
* Polls accelerometer at 10 Hz; tap events arrive on BMA456_INT_GPIO.
|
||||||
|
* Accel logging is filtered in software (deadzone); see ACCEL_DEADZONE UART command.
|
||||||
|
*/
|
||||||
|
|
||||||
#include "bosch456.h"
|
#include "bosch456.h"
|
||||||
#include "bma4.h"
|
#include "bma4.h"
|
||||||
#include "bma456h.h"
|
|
||||||
#include "bma4_defs.h"
|
#include "bma4_defs.h"
|
||||||
|
#include "bma456h.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "driver/i2c_master.h"
|
#include "driver/i2c_master.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/idf_additions.h"
|
#include "freertos/idf_additions.h"
|
||||||
#include "hal/gpio_types.h"
|
|
||||||
#include <rom/ets_sys.h>
|
#include <rom/ets_sys.h>
|
||||||
#include <stdint.h>
|
#include <string.h>
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
static const char *TAG = "[BMA456]";
|
static const char *TAG = "[BMA456]";
|
||||||
|
|
||||||
static i2c_master_dev_handle_t bma456_dev_handle;
|
#define BMA4_READ_WRITE_LEN UINT8_C(46)
|
||||||
|
#define BMA4_I2C_MAX_WRITE (1u + BMA4_READ_WRITE_LEN)
|
||||||
|
|
||||||
|
#define SENSOR_POLL_MS 100
|
||||||
|
|
||||||
|
static i2c_master_dev_handle_t s_bma456_dev;
|
||||||
static bool s_bma456_ready;
|
static bool s_bma456_ready;
|
||||||
static struct bma4_dev bma456_struct;
|
static struct bma4_dev s_bma456;
|
||||||
static uint32_t s_accel_deadzone = BMA456_DEFAULT_ACCEL_DEADZONE;
|
static uint32_t s_accel_deadzone = BMA456_DEFAULT_ACCEL_DEADZONE;
|
||||||
static int16_t s_last_x;
|
static int16_t s_last_x;
|
||||||
static int16_t s_last_y;
|
static int16_t s_last_y;
|
||||||
static int16_t s_last_z;
|
static int16_t s_last_z;
|
||||||
static bool s_have_last_sample;
|
static bool s_have_last_sample;
|
||||||
|
|
||||||
volatile uint8_t interrupt_status = 0;
|
static volatile bool s_int_pending;
|
||||||
uint8_t int_line;
|
|
||||||
struct bma4_int_pin_config pin_config = {0};
|
|
||||||
uint16_t int_status = 0;
|
|
||||||
|
|
||||||
#define BMA4_READ_WRITE_LEN UINT8_C(46)
|
static esp_err_t check_bma4(const char *api_name, int8_t rslt);
|
||||||
#define BMA456W_INT_PIN 10
|
|
||||||
#define BMA456W_INT_PIN2 9
|
|
||||||
|
|
||||||
static void interrupt_callback(void *) {
|
/* Bosch SensorAPI platform hooks (intf_ptr → i2c_master_dev_handle_t *). */
|
||||||
interrupt_status = 1;
|
|
||||||
// ESP_LOGI("INTERRUPT", "STEP DETECTED");
|
|
||||||
}
|
|
||||||
|
|
||||||
/******************************************************************************/
|
|
||||||
/*! User interface functions */
|
|
||||||
|
|
||||||
/*!
|
|
||||||
* I2C read function map to ESP platform
|
|
||||||
*/
|
|
||||||
BMA4_INTF_RET_TYPE bma4_i2c_read(uint8_t reg_addr, uint8_t *reg_data,
|
BMA4_INTF_RET_TYPE bma4_i2c_read(uint8_t reg_addr, uint8_t *reg_data,
|
||||||
uint32_t len, void *intf_ptr) {
|
uint32_t len, void *intf_ptr) {
|
||||||
if (bma456_dev_handle == NULL) {
|
if (s_bma456_dev == NULL) {
|
||||||
return BMA4_E_COM_FAIL;
|
return BMA4_E_COM_FAIL;
|
||||||
}
|
}
|
||||||
esp_err_t err = i2c_master_transmit_receive(bma456_dev_handle, ®_addr, 1,
|
esp_err_t err = i2c_master_transmit_receive(s_bma456_dev, ®_addr, 1,
|
||||||
reg_data, len, -1);
|
reg_data, len, -1);
|
||||||
return (err == ESP_OK) ? BMA4_OK : BMA4_E_COM_FAIL;
|
return (err == ESP_OK) ? BMA4_OK : BMA4_E_COM_FAIL;
|
||||||
// return BMA4_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* I2C write function map to ESP platform
|
|
||||||
*/
|
|
||||||
BMA4_INTF_RET_TYPE bma4_i2c_write(uint8_t reg_addr, const uint8_t *reg_data,
|
BMA4_INTF_RET_TYPE bma4_i2c_write(uint8_t reg_addr, const uint8_t *reg_data,
|
||||||
uint32_t len, void *intf_ptr) {
|
uint32_t len, void *intf_ptr) {
|
||||||
if (bma456_dev_handle == NULL) {
|
(void)intf_ptr;
|
||||||
|
if (s_bma456_dev == NULL || reg_data == NULL) {
|
||||||
|
return BMA4_E_COM_FAIL;
|
||||||
|
}
|
||||||
|
if (len > BMA4_READ_WRITE_LEN) {
|
||||||
return BMA4_E_COM_FAIL;
|
return BMA4_E_COM_FAIL;
|
||||||
}
|
}
|
||||||
uint8_t *buffer = malloc(len + 1);
|
|
||||||
if (!buffer)
|
|
||||||
return BMA4_E_NULL_PTR;
|
|
||||||
|
|
||||||
|
uint8_t buffer[BMA4_I2C_MAX_WRITE];
|
||||||
buffer[0] = reg_addr;
|
buffer[0] = reg_addr;
|
||||||
|
|
||||||
ESP_LOGI("I2CWrite", "Message Length: %d", len);
|
|
||||||
|
|
||||||
memcpy(&buffer[1], reg_data, len);
|
memcpy(&buffer[1], reg_data, len);
|
||||||
|
|
||||||
esp_err_t err = i2c_master_transmit(bma456_dev_handle, buffer, len + 1, -1);
|
esp_err_t err =
|
||||||
free(buffer);
|
i2c_master_transmit(s_bma456_dev, buffer, (size_t)(len + 1), -1);
|
||||||
|
|
||||||
return (err == ESP_OK) ? BMA4_OK : BMA4_E_COM_FAIL;
|
return (err == ESP_OK) ? BMA4_OK : BMA4_E_COM_FAIL;
|
||||||
// return BMA4_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* Delay function map to ESP platform
|
|
||||||
*/
|
|
||||||
void bma4_delay_us(uint32_t period, void *intf_ptr) {
|
void bma4_delay_us(uint32_t period, void *intf_ptr) {
|
||||||
|
(void)intf_ptr;
|
||||||
uint32_t wait_ms = period / 1000;
|
uint32_t wait_ms = period / 1000;
|
||||||
uint32_t wait_us = period % 1000;
|
uint32_t wait_us = period % 1000;
|
||||||
if (wait_ms) {
|
if (wait_ms > 0) {
|
||||||
vTaskDelay(pdMS_TO_TICKS(wait_ms));
|
vTaskDelay(pdMS_TO_TICKS(wait_ms));
|
||||||
}
|
}
|
||||||
|
if (wait_us > 0) {
|
||||||
ets_delay_us(wait_us);
|
ets_delay_us(wait_us);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*!
|
|
||||||
* @brief Prints the execution status of the APIs.
|
|
||||||
*/
|
|
||||||
void bma4_error_codes_print_result(const char api_name[], int8_t rslt) {
|
void bma4_error_codes_print_result(const char api_name[], int8_t rslt) {
|
||||||
if (rslt == BMA4_OK) {
|
if (rslt == BMA4_OK) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
ESP_LOGW(TAG, "%s failed: %d", api_name, (int)rslt);
|
||||||
ESP_LOGI("BMA4_I2C", "%s\t", api_name);
|
|
||||||
|
|
||||||
switch (rslt) {
|
|
||||||
case BMA4_E_NULL_PTR:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Null pointer\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_COM_FAIL:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Communication failure\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_CONFIG_STREAM_ERROR:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Invalid configuration stream\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_SELF_TEST_FAIL:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Self test failed\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_INVALID_SENSOR:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Device not found\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_OUT_OF_RANGE:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Out of Range\r\n", rslt);
|
|
||||||
break;
|
|
||||||
case BMA4_E_AVG_MODE_INVALID_CONF:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Invalid bandwidth/ODR combination\r\n",
|
|
||||||
rslt);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
ESP_LOGI("BMA4_I2C", "Error [%d] : Unknown error code\r\n", rslt);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t check_bma4(const char *api_name, int8_t rslt) {
|
static esp_err_t check_bma4(const char *api_name, int8_t rslt) {
|
||||||
@ -159,17 +115,14 @@ void bma456_set_accel_deadzone(uint32_t deadzone_lsb) {
|
|||||||
s_accel_deadzone = deadzone_lsb;
|
s_accel_deadzone = deadzone_lsb;
|
||||||
s_have_last_sample = false;
|
s_have_last_sample = false;
|
||||||
if (s_bma456_ready) {
|
if (s_bma456_ready) {
|
||||||
ESP_LOGI(TAG, "accel deadzone applied: %lu LSB", (unsigned long)deadzone_lsb);
|
ESP_LOGI(TAG, "accel deadzone %lu LSB", (unsigned long)deadzone_lsb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; }
|
uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; }
|
||||||
|
|
||||||
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z) {
|
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z) {
|
||||||
if (!s_bma456_ready) {
|
if (!s_bma456_ready || !sample_exceeds_deadzone(x, y, z)) {
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!sample_exceeds_deadzone(x, y, z)) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,165 +135,107 @@ void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z) {
|
|||||||
(unsigned long)s_accel_deadzone);
|
(unsigned long)s_accel_deadzone);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void remove_bma456_device(void) {
|
static void IRAM_ATTR bma456_int_isr(void *arg) {
|
||||||
if (bma456_dev_handle != NULL) {
|
(void)arg;
|
||||||
i2c_master_bus_rm_device(bma456_dev_handle);
|
s_int_pending = true;
|
||||||
bma456_dev_handle = NULL;
|
|
||||||
}
|
|
||||||
s_bma456_ready = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void read_sensor_task(void *params) {
|
static void handle_tap_interrupt(void) {
|
||||||
int8_t ret;
|
uint16_t int_status = 0;
|
||||||
struct bma4_accel sens_data = {0};
|
int8_t ret = bma456h_read_int_status(&int_status, &s_bma456);
|
||||||
|
if (ret != BMA4_OK) {
|
||||||
|
bma4_error_codes_print_result("bma456h_read_int_status", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct bma456h_out_state tap_out = {0};
|
||||||
|
ret = bma456h_output_state(&tap_out, &s_bma456);
|
||||||
|
if (ret != BMA4_OK) {
|
||||||
|
bma4_error_codes_print_result("bma456h_output_state", ret);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tap_out.single_tap) {
|
||||||
|
ESP_LOGI(TAG, "tap: single");
|
||||||
|
} else if (tap_out.double_tap) {
|
||||||
|
ESP_LOGI(TAG, "tap: double");
|
||||||
|
} else if (tap_out.triple_tap) {
|
||||||
|
ESP_LOGI(TAG, "tap: triple");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void remove_bma456_device(void) {
|
||||||
|
if (s_bma456_ready) {
|
||||||
|
gpio_isr_handler_remove(BMA456_INT_GPIO);
|
||||||
|
}
|
||||||
|
if (s_bma456_dev != NULL) {
|
||||||
|
i2c_master_bus_rm_device(s_bma456_dev);
|
||||||
|
s_bma456_dev = NULL;
|
||||||
|
}
|
||||||
|
s_bma456_ready = false;
|
||||||
|
s_int_pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void read_sensor_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
|
||||||
if (!s_bma456_ready) {
|
if (!s_bma456_ready) {
|
||||||
vTaskDelete(NULL);
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct bma4_accel sens_data = {0};
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
ret = bma4_read_accel_xyz(&sens_data, &bma456_struct);
|
int8_t ret = bma4_read_accel_xyz(&sens_data, &s_bma456);
|
||||||
if (ret == BMA4_OK) {
|
if (ret == BMA4_OK) {
|
||||||
bma456_report_accel_if_changed(sens_data.x, sens_data.y, sens_data.z);
|
bma456_report_accel_if_changed(sens_data.x, sens_data.y, sens_data.z);
|
||||||
} else {
|
} else {
|
||||||
bma4_error_codes_print_result("bma4_read_accel_xyz", ret);
|
bma4_error_codes_print_result("bma4_read_accel_xyz", ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (interrupt_status) {
|
if (s_int_pending) {
|
||||||
ESP_LOGI("INTERRUPT", "Da war der Interrupt resetting");
|
s_int_pending = false;
|
||||||
interrupt_status = 0;
|
handle_tap_interrupt();
|
||||||
ret = bma456h_read_int_status(&int_status, &bma456_struct);
|
|
||||||
bma4_error_codes_print_result("bma456w_step_counter_output status", ret);
|
|
||||||
|
|
||||||
int8_t rslt;
|
|
||||||
struct bma456h_out_state tap_out = {0};
|
|
||||||
|
|
||||||
rslt = bma456h_output_state(&tap_out, &bma456_struct);
|
|
||||||
|
|
||||||
if (BMA4_OK == rslt) {
|
|
||||||
/* Enters only if the obtained interrupt is single-tap */
|
|
||||||
if (tap_out.single_tap) {
|
|
||||||
ESP_LOGI("INTERRUPT", "Single Tap interrupt occurred\n");
|
|
||||||
}
|
|
||||||
/* Enters only if the obtained interrupt is double-tap */
|
|
||||||
else if (tap_out.double_tap) {
|
|
||||||
ESP_LOGI("INTERRUPT", "Double Tap interrupt occurred\n");
|
|
||||||
}
|
|
||||||
/* Enters only if the obtained interrupt is triple-tap */
|
|
||||||
else if (tap_out.triple_tap) {
|
|
||||||
ESP_LOGI("INTERRUPT", "Triple Tap interrupt occurred\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESP_LOGI("i2c", "X:%d, Y%d, Z%d", sens_data.x, sens_data.y, sens_data.z);
|
vTaskDelay(pdMS_TO_TICKS(SENSOR_POLL_MS));
|
||||||
vTaskDelay(pdMS_TO_TICKS(100));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle) {
|
static esp_err_t configure_tap_interrupt(void) {
|
||||||
int8_t ret;
|
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
int8_t ret;
|
||||||
s_bma456_ready = false;
|
const uint8_t int_line = BMA4_INTR2_MAP;
|
||||||
bma456_dev_handle = NULL;
|
|
||||||
|
|
||||||
if (bus_handle == NULL) {
|
|
||||||
return ESP_ERR_INVALID_ARG;
|
|
||||||
}
|
|
||||||
|
|
||||||
i2c_device_config_t dev_cfg_bma456 = {
|
|
||||||
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
|
||||||
.device_address = BMA456_ADDRESS,
|
|
||||||
.scl_speed_hz = 100000,
|
|
||||||
};
|
|
||||||
|
|
||||||
err = i2c_master_bus_add_device(bus_handle, &dev_cfg_bma456, &bma456_dev_handle);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
bma456_struct.intf = BMA4_I2C_INTF;
|
|
||||||
bma456_struct.bus_read = bma4_i2c_read;
|
|
||||||
bma456_struct.bus_write = bma4_i2c_write;
|
|
||||||
bma456_struct.delay_us = bma4_delay_us;
|
|
||||||
bma456_struct.read_write_len = BMA4_READ_WRITE_LEN;
|
|
||||||
bma456_struct.intf_ptr = &bma456_dev_handle;
|
|
||||||
bma456_struct.chip_id = 0;
|
|
||||||
|
|
||||||
ret = bma456h_init(&bma456_struct);
|
|
||||||
if (check_bma4("bma456h_init", ret) != ESP_OK) {
|
|
||||||
remove_bma456_device();
|
|
||||||
return ESP_ERR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "chip id 0x%02x", bma456_struct.chip_id);
|
|
||||||
|
|
||||||
ret = bma4_soft_reset(&bma456_struct);
|
|
||||||
if (check_bma4("bma4_soft_reset", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(20));
|
|
||||||
|
|
||||||
ret = bma4_set_advance_power_save(BMA4_DISABLE, &bma456_struct);
|
|
||||||
if (check_bma4("bma4_set_advance_power_save", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(10));
|
|
||||||
|
|
||||||
ret = bma456h_write_config_file(&bma456_struct);
|
|
||||||
if (check_bma4("bma456h_write_config_file", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct bma4_accel_config accel_config;
|
|
||||||
bma4_get_accel_config(&accel_config, &bma456_struct);
|
|
||||||
accel_config.range = BMA4_ACCEL_RANGE_2G;
|
|
||||||
ret = bma4_set_accel_config(&accel_config, &bma456_struct);
|
|
||||||
if (check_bma4("bma4_set_accel_config", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
|
|
||||||
ret = bma4_set_accel_enable(BMA4_ENABLE, &bma456_struct);
|
|
||||||
if (check_bma4("bma4_set_accel_enable", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct bma456h_multitap_settings tap_settings = {0};
|
struct bma456h_multitap_settings tap_settings = {0};
|
||||||
ret = bma456h_tap_get_parameter(&tap_settings, &bma456_struct);
|
ret = bma456h_tap_get_parameter(&tap_settings, &s_bma456);
|
||||||
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
|
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
|
||||||
goto fail;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
tap_settings.tap_sens_thres = 0;
|
tap_settings.tap_sens_thres = 0;
|
||||||
ret = bma456h_tap_set_parameter(&tap_settings, &bma456_struct);
|
ret = bma456h_tap_set_parameter(&tap_settings, &s_bma456);
|
||||||
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
|
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
|
||||||
goto fail;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = bma456h_feature_enable(
|
ret = bma456h_feature_enable(
|
||||||
(BMA456H_SINGLE_TAP_EN | BMA456H_DOUBLE_TAP_EN | BMA456H_TRIPLE_TAP_EN),
|
(BMA456H_SINGLE_TAP_EN | BMA456H_DOUBLE_TAP_EN | BMA456H_TRIPLE_TAP_EN),
|
||||||
BMA4_ENABLE, &bma456_struct);
|
BMA4_ENABLE, &s_bma456);
|
||||||
if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) {
|
if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) {
|
||||||
goto fail;
|
return ESP_FAIL;
|
||||||
}
|
|
||||||
|
|
||||||
ret = bma456h_step_counter_set_watermark(1, &bma456_struct);
|
|
||||||
if (check_bma4("bma456h_step_counter_set_watermark", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
|
||||||
|
|
||||||
int_line = BMA4_INTR2_MAP;
|
|
||||||
|
|
||||||
ret = bma4_get_int_pin_config(&pin_config, int_line, &bma456_struct);
|
|
||||||
if (check_bma4("bma4_get_int_pin_config", ret) != ESP_OK) {
|
|
||||||
goto fail;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE,
|
ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE,
|
||||||
&bma456_struct);
|
&s_bma456);
|
||||||
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
|
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
|
||||||
goto fail;
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct bma4_int_pin_config pin_config = {0};
|
||||||
|
ret = bma4_get_int_pin_config(&pin_config, int_line, &s_bma456);
|
||||||
|
if (check_bma4("bma4_get_int_pin_config", ret) != ESP_OK) {
|
||||||
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
pin_config.edge_ctrl = BMA4_EDGE_TRIGGER;
|
pin_config.edge_ctrl = BMA4_EDGE_TRIGGER;
|
||||||
@ -349,36 +244,113 @@ esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle) {
|
|||||||
pin_config.od = BMA4_PUSH_PULL;
|
pin_config.od = BMA4_PUSH_PULL;
|
||||||
pin_config.input_en = BMA4_INPUT_DISABLE;
|
pin_config.input_en = BMA4_INPUT_DISABLE;
|
||||||
|
|
||||||
ret = bma4_set_int_pin_config(&pin_config, int_line, &bma456_struct);
|
ret = bma4_set_int_pin_config(&pin_config, int_line, &s_bma456);
|
||||||
if (check_bma4("bma4_set_int_pin_config", ret) != ESP_OK) {
|
if (check_bma4("bma4_set_int_pin_config", ret) != ESP_OK) {
|
||||||
goto fail;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
gpio_reset_pin(BMA456W_INT_PIN);
|
gpio_reset_pin(BMA456_INT_GPIO);
|
||||||
gpio_set_direction(BMA456W_INT_PIN, GPIO_MODE_INPUT);
|
gpio_set_direction(BMA456_INT_GPIO, GPIO_MODE_INPUT);
|
||||||
gpio_set_pull_mode(BMA456W_INT_PIN, GPIO_PULLDOWN_ENABLE);
|
gpio_set_pull_mode(BMA456_INT_GPIO, GPIO_PULLDOWN_ONLY);
|
||||||
gpio_set_intr_type(BMA456W_INT_PIN, GPIO_INTR_POSEDGE);
|
gpio_set_intr_type(BMA456_INT_GPIO, GPIO_INTR_POSEDGE);
|
||||||
gpio_intr_enable(BMA456W_INT_PIN);
|
|
||||||
|
|
||||||
err = gpio_install_isr_service(0);
|
err = gpio_install_isr_service(0);
|
||||||
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
goto fail;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = gpio_isr_handler_add(BMA456W_INT_PIN, interrupt_callback,
|
err = gpio_isr_handler_add(BMA456_INT_GPIO, bma456_int_isr, NULL);
|
||||||
(void *)BMA456W_INT_PIN);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle) {
|
||||||
|
int8_t ret;
|
||||||
|
esp_err_t err;
|
||||||
|
|
||||||
|
s_bma456_ready = false;
|
||||||
|
s_bma456_dev = NULL;
|
||||||
|
s_int_pending = false;
|
||||||
|
|
||||||
|
if (bus_handle == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
i2c_device_config_t dev_cfg = {
|
||||||
|
.dev_addr_length = I2C_ADDR_BIT_LEN_7,
|
||||||
|
.device_address = BMA456_I2C_ADDR,
|
||||||
|
.scl_speed_hz = 100000,
|
||||||
|
};
|
||||||
|
|
||||||
|
err = i2c_master_bus_add_device(bus_handle, &dev_cfg, &s_bma456_dev);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_bma456.intf = BMA4_I2C_INTF;
|
||||||
|
s_bma456.bus_read = bma4_i2c_read;
|
||||||
|
s_bma456.bus_write = bma4_i2c_write;
|
||||||
|
s_bma456.delay_us = bma4_delay_us;
|
||||||
|
s_bma456.read_write_len = BMA4_READ_WRITE_LEN;
|
||||||
|
s_bma456.intf_ptr = &s_bma456_dev;
|
||||||
|
s_bma456.chip_id = 0;
|
||||||
|
|
||||||
|
ret = bma456h_init(&s_bma456);
|
||||||
|
if (check_bma4("bma456h_init", ret) != ESP_OK) {
|
||||||
|
remove_bma456_device();
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "chip id 0x%02x", s_bma456.chip_id);
|
||||||
|
|
||||||
|
ret = bma4_soft_reset(&s_bma456);
|
||||||
|
if (check_bma4("bma4_soft_reset", ret) != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(20));
|
||||||
|
|
||||||
|
ret = bma4_set_advance_power_save(BMA4_DISABLE, &s_bma456);
|
||||||
|
if (check_bma4("bma4_set_advance_power_save", ret) != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
|
||||||
|
ret = bma456h_write_config_file(&s_bma456);
|
||||||
|
if (check_bma4("bma456h_write_config_file", ret) != ESP_OK) {
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xTaskCreate(read_sensor_task, "READ_SENSOR", 4096, NULL, 1, NULL) !=
|
struct bma4_accel_config accel_config = {0};
|
||||||
|
ret = bma4_get_accel_config(&accel_config, &s_bma456);
|
||||||
|
if (check_bma4("bma4_get_accel_config", ret) != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
accel_config.range = BMA4_ACCEL_RANGE_2G;
|
||||||
|
ret = bma4_set_accel_config(&accel_config, &s_bma456);
|
||||||
|
if (check_bma4("bma4_set_accel_config", ret) != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = bma4_set_accel_enable(BMA4_ENABLE, &s_bma456);
|
||||||
|
if (check_bma4("bma4_set_accel_enable", ret) != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (configure_tap_interrupt() != ESP_OK) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xTaskCreate(read_sensor_task, "bma456_poll", 4096, NULL, 1, NULL) !=
|
||||||
pdPASS) {
|
pdPASS) {
|
||||||
gpio_isr_handler_remove(BMA456W_INT_PIN);
|
|
||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
s_bma456_ready = true;
|
s_bma456_ready = true;
|
||||||
ESP_LOGI(TAG, "initialized");
|
ESP_LOGI(TAG, "ready (I2C 0x%02x, INT GPIO%d, poll %d ms)", BMA456_I2C_ADDR,
|
||||||
|
BMA456_INT_GPIO, SENSOR_POLL_MS);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
|
|
||||||
fail:
|
fail:
|
||||||
|
|||||||
@ -1,17 +1,30 @@
|
|||||||
#ifndef BOSCH456_H
|
#ifndef BOSCH456_H
|
||||||
#define BOSCH456_H
|
#define BOSCH456_H
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Powerpod driver for Bosch BMA456H (hearable variant) on the shared I2C bus.
|
||||||
|
*
|
||||||
|
* Vendor API: components/bma456 (bma4.c + bma456h.c only).
|
||||||
|
* Implementation: bosch456.c
|
||||||
|
*/
|
||||||
|
|
||||||
#include "driver/i2c_types.h"
|
#include "driver/i2c_types.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
#define TOUCH_1 9
|
/** 7-bit I2C address (SDO low). */
|
||||||
#define TOUCH_2 8
|
#define BMA456_I2C_ADDR 0x18
|
||||||
|
|
||||||
#define BMA456_ADDRESS 0x18
|
/** Sensor interrupt line → ESP32 GPIO (active high, rising edge). */
|
||||||
|
#define BMA456_INT_GPIO 10
|
||||||
|
|
||||||
|
/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */
|
||||||
#define BMA456_DEFAULT_ACCEL_DEADZONE 100u
|
#define BMA456_DEFAULT_ACCEL_DEADZONE 100u
|
||||||
|
|
||||||
/** Initialize BMA456 on the shared I2C bus. Returns ESP_OK or logs and skips sensor use. */
|
/**
|
||||||
|
* Probe and configure the sensor on bus_handle (100 kHz device).
|
||||||
|
* On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned;
|
||||||
|
* firmware continues without a sensor (see bma456_is_ready()).
|
||||||
|
*/
|
||||||
esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle);
|
esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle);
|
||||||
|
|
||||||
bool bma456_is_ready(void);
|
bool bma456_is_ready(void);
|
||||||
@ -19,7 +32,7 @@ bool bma456_is_ready(void);
|
|||||||
void bma456_set_accel_deadzone(uint32_t deadzone_lsb);
|
void bma456_set_accel_deadzone(uint32_t deadzone_lsb);
|
||||||
uint32_t bma456_get_accel_deadzone(void);
|
uint32_t bma456_get_accel_deadzone(void);
|
||||||
|
|
||||||
/** Log accel sample only when any axis changed more than deadzone since last report. */
|
/** Log accel when any axis moved more than deadzone since last reported sample. */
|
||||||
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z);
|
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user