Probe and configure the sensor when present; log and continue boot if init fails so boards without BMA456 still run normally. Co-authored-by: Cursor <cursoragent@cursor.com>
Powerpod firmware
ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the same binary; role and ESP-NOW network are selected at boot via DIP switches and an I2C IO expander.
System overview
┌─────────────┐
│ PC │
│ (goTool) │
└──────┬──────┘
│ UART1 (921600)
│ framed commands + protobuf
┌──────▼──────┐
│ MASTER │
│ ESP32 │
└──────┬──────┘
│ ESP-NOW (WiFi channel = network ID)
┌────────────┼────────────┐
│ │ │
┌──────▼──────┐ ┌───▼────┐ ┌─────▼─────┐
│ SLAVE │ │ SLAVE │ │ SLAVE │
└─────────────┘ └────────┘ └───────────┘
| Role | UART to PC | ESP-NOW |
|---|---|---|
| Master | Yes — command handler, protobuf replies | Broadcasts discover; collects slave info |
| Slave | No | Responds to discover with slave info |
Master keeps a client registry (client_registry.c) of slaves seen via ESP-NOW. The PC can query it with the CLIENT_INFO UART command.
Boot configuration
Read in app_main() before subsystems start. Stored in app_config_t (app_config.h).
| Setting | Source | Notes |
|---|---|---|
master |
GPIO DIP_MASTER (pin 4) |
Low = master, high = slave |
network |
I2C IO expander 0x20, port bits (nibble reversed) |
Value 1–8 → ESP-NOW WiFi channel 1–8 |
running_partition |
OTA API | Active partition label |
Pins (powerpod.h):
| Signal | GPIO |
|---|---|
| DIP master | 4 |
| I2C SCL | 5 |
| I2C SDA | 6 |
| UART TX | 3 |
| UART RX | 2 |
| LED ring | 7 |
Startup order:
- Read DIP + IO expander →
app_config esp_now_comm_init(&app_config)— WiFi + ESP-NOWled_ring_init()- Master only: command queue, UART, registered commands (e.g. VERSION)
ESP-NOW discovery
Implementation: esp_now_comm.c / esp_now_comm.h.
WiFi is brought up in STA mode (no AP association). Channel = app_config.network (clamped to 1–13).
Air protocol (nanopb)
Schema: proto/esp_now_messages.proto. Encode/decode: esp_now_proto.c. The ESP-NOW payload is a single encoded EspNowMessage (no extra framing).
EspNowMessageType |
Direction | oneof payload |
|---|---|---|
ESPNOW_DISCOVER |
Master → broadcast FF:FF:FF:FF:FF:FF |
EspNowDiscover (network) |
ESPNOW_SLAVE_INFO |
Slave → master | EspNowSlavePresence |
ESPNOW_HEARTBEAT |
Slave → master | EspNowSlavePresence (same fields) |
EspNowSlavePresence: network, mac (6 bytes), version, slave_id, available, used.
Master: task espnow_disc sends DISCOVER every 500 ms on the configured network. Logs slave joined id=… mac=… ver=… when a new slave is seen (up to 16 entries). Task espnow_mon runs every 1 s and marks a client inactive (available = false) if no SLAVE_INFO or HEARTBEAT was received for 3 s (three missed 1 s heartbeats). A later heartbeat sets available true again and logs reactivation.
Slave: on first matching DISCOVER, logs joined network N, master …, sends SLAVE_INFO once, then sends HEARTBEAT to the master every 1 s. While joined, periodic discovers from the same master refresh a “master alive” timer; if no discover arrives for 5 s, the slave treats the master as lost, clears join state, and will register again on the next discover (reconnect). Discover from a different master is ignored while already joined.
Monitor via USB-JTAG (/dev/ttyACM0) while using a USB-serial adapter on GPIO2/3 (/dev/ttyUSB0) for UART — they are different interfaces.
UART (master only)
Hardware: UART1, 921600 baud, TX = GPIO3, RX = GPIO2 (adapter TX → ESP RX, adapter RX → ESP TX).
Frame format
| Field | Value |
|---|---|
| Start | 0xAA |
| Length | 1 byte, payload size 1–252 |
| Payload | length bytes |
| Checksum | XOR of all payload bytes |
| Stop | 0xCC |
Command handler payload
| Offset | Meaning |
|---|---|
| 0 | Command ID (MessageType / msg_id) |
| 1… | Arguments (handler receives bytes after ID only) |
Example VERSION request: single-byte payload 03 → frame AA 01 03 03 CC.
Logging:
[UART] received message cmd=0x… len=…[CMDH] trigger command VERSION (0x03)(or other name frommessage_type_name())
Command handler
Generic dispatch for host commands (UART today; msg_post() for in-firmware sources later).
UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
| API | Description |
|---|---|
init_cmdHandler(queue) |
Start dispatcher task (priority 5) |
msg_register_handler(id, cb) |
Register callback; max 32 handlers |
msg_post(id, data, len) |
Enqueue from firmware (e.g. future ESP-NOW → PC path) |
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
Init order on master:
cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
init_cmdHandler(cmd_queue);
init_uart(cmd_queue);
cmd_version_register();
Protobuf (proto/uart_messages.proto)
Host and master speak nanopb-encoded UartMessage inside UART frames (byte 0 = MessageType, bytes 1… = encoded message).
| ID | Name | Status |
|---|---|---|
| 3 | VERSION |
Implemented (cmd_version.c) |
| 4 | CLIENT_INFO |
Implemented (cmd_client_info.c) — slave list from registry |
| 5 | CLIENT_INPUT |
Planned |
| 16–20 | OTA / ESP-NOW OTA | Planned |
Regenerate C code:
make proto_generate
# or individually:
make proto_generate_uart
make proto_generate_espnow
After generation, ensure main/proto/*.pb.c includes use #include "…pb.h" (not main/proto/…).
Build embeds POWERPOD_GIT_HASH via git rev-parse in main/CMakeLists.txt.
VERSION command
Request: framed payload 03 only.
Response: payload 03 + nanopb UartMessage:
type = VERSIONversion_response.version—POWERPOD_FW_VERSIONversion_response.git_hash— build git hash string
Encoding: uart_send_uart_message() in uart_proto.c.
CLIENT_INFO command
Request: framed payload 04 only (MessageType.CLIENT_INFO).
Response: payload 04 + nanopb UartMessage with client_info_response.clients — one ClientInfo per registered slave (from ESP-NOW SLAVE_INFO).
Fields per client: id, mac, version, available, used, last_ping, last_success_ping — milliseconds since the last packet / last successful heartbeat (computed when CLIENT_INFO is answered; typically 0–1000 while the slave is heartbeating every 1 s).
Client registry
| API | Description |
|---|---|
client_registry_init() |
Clear all slots (called from esp_now_comm_init) |
client_registry_upsert(mac, id, version, …) |
Add or refresh client; updates ping timestamps |
client_registry_heartbeat(mac, id, version, …) |
Same as upsert for heartbeats; reactivates inactive clients |
client_registry_check_timeouts(timeout_ms) |
Mark stale clients inactive (master monitor task) |
client_registry_count() / client_registry_at(i) |
Iterate for UART encoding |
Slaves register when the master receives SLAVE_INFO on the matching network; HEARTBEAT keeps them marked available.
Host tool (goTool/)
Go CLI to test UART from a PC connected to the master only.
cd goTool
go mod tidy
go run . -port /dev/ttyUSB0 version
go run . -port /dev/ttyUSB0 clients
| Flag | Default | Description |
|---|---|---|
-port |
(required) | Serial device, e.g. /dev/ttyUSB0 |
-baud |
921600 |
Must match UART_BAUD_RATE |
| Command | Description |
|---|---|
version |
Firmware version and git hash |
clients |
Registered slaves from master client registry |
Regenerate Go protobuf:
protoc --go_out=./pb --go_opt=paths=source_relative \
--go_opt=Muart_messages.proto=powerpod/gotool/pb \
-I ../main/proto ../main/proto/uart_messages.proto
See goTool/README.md for tool-only notes.
Build and flash
source ~/esp/esp-idf/export.sh # or export.fish in fish
cd /path/to/powerpod
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor # USB-JTAG / console
Target: ESP32-S3. Close serial monitor on the UART adapter port before running goTool on the same device.
Source files
| File | Role |
|---|---|
powerpod.c |
app_main, DIP/network config, init order |
powerpod.h |
Pin defines |
app_config.h |
app_config_t |
esp_now_comm.c/h |
WiFi, ESP-NOW, discover / slave info |
uart.c/h |
Framed UART RX/TX |
uart_proto.c/h |
Encode/send UartMessage |
cmd_handler.c/h |
Command queue and dispatch |
cmd_version.c/h |
VERSION handler |
cmd_client_info.c/h |
CLIENT_INFO handler |
client_registry.c/h |
Registered slave table |
led_ring.c/h |
LED digit display |
proto/uart_messages.proto |
UART protocol schema |
proto/esp_now_messages.proto |
ESP-NOW protocol schema |
esp_now_proto.c/h |
Encode/decode EspNowMessage |
proto/*.pb.c/h |
Generated nanopb |
CMakeLists.txt |
Sources, esp_wifi, drivers, git hash |
Adding a new UART command
- Add or extend messages in
uart_messages.protoand regenerate nanopb. - Create
cmd_*.cwith a handler; register withmsg_register_handler(MessageType_…, handler). - Reply via
uart_send_uart_message()where needed. - Extend
goToolor another host client to send the matching frame.
For ESP-NOW-driven PC updates later: map slave state to ClientInfo and send CLIENT_INFO over UART from the master.