From b592401e78021db2712d86102f840d3db8def6aa Mon Sep 17 00:00:00 2001 From: simon Date: Mon, 18 May 2026 22:17:53 +0200 Subject: [PATCH] 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 --- goTool/README.md | 14 ++- main/README.md | 297 ++++++++++++++++++++++++++++++++++------------- 2 files changed, 223 insertions(+), 88 deletions(-) diff --git a/goTool/README.md b/goTool/README.md index a60fae7..2870417 100644 --- a/goTool/README.md +++ b/goTool/README.md @@ -1,10 +1,10 @@ # goTool -Host-side UART utilities for Powerpod. +Host-side UART client for the Powerpod **master** ESP. -## version command +Full system documentation (roles, ESP-NOW, framing, protobuf): [`../main/README.md`](../main/README.md). -Sends `MessageType.VERSION` (0x03) in a framed UART packet and prints the protobuf response. +## Usage ```bash cd goTool @@ -12,10 +12,12 @@ go mod tidy go run . -port /dev/ttyUSB0 ``` -Options: +| Flag | Default | Description | +|------|---------|-------------| +| `-port` | (required) | Serial port on master UART (GPIO2/3 adapter) | +| `-baud` | `921600` | Must match firmware `UART_BAUD_RATE` | -- `-port` — serial device (required) -- `-baud` — default `921600` (must match firmware) +Implements the VERSION command (`MessageType` = 3): sends framed `03`, decodes protobuf `UartMessage.version_response`. ## Regenerate protobuf diff --git a/main/README.md b/main/README.md index 83e83ff..8a7563a 100644 --- a/main/README.md +++ b/main/README.md @@ -1,114 +1,247 @@ -# Command handler +# Powerpod firmware -Generic command dispatch for Powerpod. Transports (UART today, ESP-NOW later) enqueue messages on a shared FreeRTOS queue; a dispatcher task invokes registered callbacks by message ID. +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. -## Architecture +## System overview ``` -UART / ESP-NOW → generic_msg_t queue → vCmdDispatcherTask → registered handler + ┌─────────────┐ + │ PC │ + │ (goTool) │ + └──────┬──────┘ + │ UART1 (921600) + │ framed commands + protobuf + ┌──────▼──────┐ + │ MASTER │ + │ ESP32 │ + └──────┬──────┘ + │ ESP-NOW (WiFi channel = network ID) + ┌────────────┼────────────┐ + │ │ │ + ┌──────▼──────┐ ┌───▼────┐ ┌─────▼─────┐ + │ SLAVE │ │ SLAVE │ │ SLAVE │ + └─────────────┘ └────────┘ └───────────┘ ``` -- **`cmd_handler`** — queue, registration, dispatcher task -- **`uart`** — framed serial input, converts packets to `generic_msg_t` -- **`powerpod.c`** — creates the queue, calls `init_cmdHandler()` then `init_uart()` +| 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 | -Initialize the command handler **before** UART so the dispatcher is running when packets arrive. +Planned: master forwards aggregated `ClientInfo` to the PC over UART (`CLIENT_INFO` in `uart_messages.proto`). -```c -cmd_queue = xQueueCreate(10, sizeof(generic_msg_t)); -init_cmdHandler(cmd_queue); -init_uart(cmd_queue); +## 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: + +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 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 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 ``` -## UART frame format - -Packets on **UART1** (921600 baud, **TX=GPIO3**, **RX=GPIO2** — USB adapter on `/dev/ttyUSB0`): - -| Field | Value | -|-----------|--------------------------------------------| -| Start | `0xAA` | -| Length | 1 byte, payload size (1–252), non-zero | -| Payload | `length` bytes | -| Checksum | XOR of all payload bytes | -| Stop | `0xCC` | - -**Payload layout for the command handler:** - -| Offset | Meaning | -|--------|----------------------------------| -| 0 | Command ID (`msg_id`, uint8/16) | -| 1… | Arguments (passed to handler) | - -Example: command `0x01` with arguments `0x02 0x03` → payload `01 02 03`, length = 3. - -The dispatcher strips the first byte; handlers receive only the argument bytes. - -## API - -### `msg_register_handler(uint16_t id, msg_callback_t cb)` - -Register a callback for a command ID. Up to 32 handlers. Re-registering the same ID updates the callback. - -```c -static void on_ping(const uint8_t *data, size_t len) { - ESP_LOGI("app", "ping, %u bytes", (unsigned)len); -} - -msg_register_handler(0x01, on_ping); -``` - -Callback signature: +| 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); ``` -### `msg_post(uint16_t id, const uint8_t *data, size_t len)` - -Enqueue a command from firmware (e.g. ESP-NOW receive path) without UART. Copies `data` into heap memory; the dispatcher frees it after the handler returns. +Init order on master: ```c -uint8_t args[] = {0x02, 0x03}; -msg_post(0x01, args, sizeof(args)); +cmd_queue = xQueueCreate(10, sizeof(generic_msg_t)); +init_cmdHandler(cmd_queue); +init_uart(cmd_queue); +cmd_version_register(); ``` -Returns `ESP_OK`, `ESP_ERR_NO_MEM`, `ESP_ERR_TIMEOUT` (queue full), or `ESP_ERR_INVALID_STATE`. +## Protobuf (`proto/uart_messages.proto`) -## Adding a new command +Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 = `MessageType`, bytes 1… = encoded message). -1. Pick a command ID (first byte of UART payload). -2. Implement a handler in `powerpod.c` (or a dedicated module). -3. Call `msg_register_handler()` after `init_cmdHandler()`. -4. From a host tool, send a framed UART packet with that ID in byte 0. +| 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 | -## VERSION command (`MessageType.VERSION` = 3) +Regenerate C code: -Implemented in `cmd_version.c`. Request is a UART frame with payload `03` (command byte only). +```bash +make proto_generate_uart +# or: +python libs/nanopb/generator/nanopb_generator.py main/proto/uart_messages.proto +``` -Response frame payload: +Build embeds `POWERPOD_GIT_HASH` via `git rev-parse` in `main/CMakeLists.txt`. -| Byte 0 | Bytes 1… | -|--------|----------| -| `0x03` | nanopb-encoded `UartMessage` with `type = VERSION` and `version_response` set | +### VERSION command -`VersionResponse` fields: +**Request:** framed payload `03` only. -- `version` — `POWERPOD_FW_VERSION` (default `1`, override at compile time) -- `git_hash` — short git hash from build (`POWERPOD_GIT_HASH`, from `git rev-parse`) +**Response:** payload `03` + nanopb `UartMessage`: -Register additional proto commands the same way: handler + `uart_send_uart_message()` for replies. +- `type = VERSION` +- `version_response.version` — `POWERPOD_FW_VERSION` +- `version_response.git_hash` — build git hash string -## ESP-NOW (planned) +Encoding: `uart_send_uart_message()` in `uart_proto.c`. -Parse incoming ESP-NOW data in the Wi-Fi layer and call `msg_post()` with the same ID + payload layout as UART (ID separate, arguments in `data`). No changes to `cmd_handler` required. +## Host tool (`goTool/`) -## Files +Go CLI to test UART from a PC connected to the **master** only. -| File | Role | -|-----------------|-------------------------------------------| -| `cmd_handler.h` | Types and public API | -| `cmd_handler.c` | Queue dispatch, registration, `msg_post` | -| `uart.c` | Framed UART parser → queue | -| `powerpod.c` | Queue creation and init order | -| `cmd_version.c` | VERSION command handler | -| `uart_proto.c` | Encode/send `UartMessage` over UART | +```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.