simon b592401e78 Document firmware architecture, ESP-NOW, UART, and goTool in README.
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>
2026-05-18 22:17:53 +02:00
..
2026-05-05 17:36:29 +02:00
2026-05-05 17:36:29 +02:00
2026-05-05 17:36:29 +02:00

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 18 → ESP-NOW WiFi channel 18
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:

  1. Read DIP + IO expander → app_config
  2. esp_now_comm_init(&app_config) — WiFi + ESP-NOW
  3. led_ring_init()
  4. 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 113).

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 02):

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 1252
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 from message_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
1620 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 = VERSION
  • version_response.versionPOWERPOD_FW_VERSION
  • version_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

  1. Add or extend messages in uart_messages.proto and regenerate nanopb.
  2. Create cmd_*.c with a handler; register with msg_register_handler(MessageType_…, handler).
  3. Reply via uart_send_uart_message() where needed.
  4. Extend goTool or 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.