powerpods/main/README.md
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

248 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) |
```c
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
```
Init order on master:
```c
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:
```bash
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.version``POWERPOD_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.
```bash
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:
```bash
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
```bash
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.