# 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 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 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 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 ``` | 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 | | 16–20 | 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.