powerpods/main/README.md
simon 92e146e2ed Add client registry and CLIENT_INFO UART command on master.
Track ESP-NOW slaves in a shared registry and respond to CLIENT_INFO
with protobuf ClientInfoResponse; ESP-NOW path upserts registry entries.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-18 22:26:42 +02:00

9.1 KiB
Raw Blame History

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 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 first matching DISCOVER, logs joined network N, master …, sends SLAVE_INFO once, then ignores further discovers from that master (no repeat log or reply).

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 Implemented (cmd_client_info.c) — slave list from registry
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.

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 boot, updated on each SLAVE_INFO).

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_count() / client_registry_at(i) Iterate for UART encoding

Slaves register when the master receives SLAVE_INFO on the matching network.

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 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.