Expand main/README with master/slave overview, boot config, protocols, and build notes; point goTool README at the full system doc. 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 |
Planned: master forwards aggregated ClientInfo to the PC over UART (CLIENT_INFO in uart_messages.proto).
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 (compact binary, not protobuf)
| Byte | Field |
|---|---|
| 0 | Magic 0xA1 |
| 1 | Message type |
| 2 | Network ID (must match local config) |
| 3+ | Type-specific |
| Type | Value | Direction | Purpose |
|---|---|---|---|
DISCOVER |
1 | Master → broadcast FF:FF:FF:FF:FF:FF |
Master is searching for slaves |
SLAVE_INFO |
2 | Slave → master | Slave registration |
SLAVE_INFO payload (after header bytes 0–2):
| Field | Type | Description |
|---|---|---|
mac |
6 bytes | Slave WiFi MAC |
version |
uint32 | POWERPOD_FW_VERSION (default 1) |
slave_id |
uint32 | Currently last byte of MAC |
available |
uint8 | 1 = available |
used |
uint8 | 0 = unused |
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).
Slave: on matching DISCOVER, unicast SLAVE_INFO back to the master source MAC.
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 |
Planned — slave list to PC |
| 5 | CLIENT_INPUT |
Planned |
| 16–20 | OTA / ESP-NOW OTA | Planned |
Regenerate C code:
make proto_generate_uart
# or:
python libs/nanopb/generator/nanopb_generator.py main/proto/uart_messages.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.
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
| Flag | Default | Description |
|---|---|---|
-port |
(required) | Serial device, e.g. /dev/ttyUSB0 |
-baud |
921600 |
Must match UART_BAUD_RATE |
Sends VERSION, reads framed response, prints version and git_hash.
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 |
led_ring.c/h |
LED digit display |
proto/uart_messages.proto |
Protocol schema |
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.