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

272 lines
9.1 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 |
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) |
```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` | Implemented (`cmd_client_info.c`) — slave list from registry |
| 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`.
### 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.
```bash
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:
```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 |
| `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.