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>
This commit is contained in:
simon 2026-05-18 22:17:53 +02:00
parent 54f2a7de5b
commit b592401e78
2 changed files with 223 additions and 88 deletions

View File

@ -1,10 +1,10 @@
# goTool # 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 ```bash
cd goTool cd goTool
@ -12,10 +12,12 @@ go mod tidy
go run . -port /dev/ttyUSB0 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) Implements the VERSION command (`MessageType` = 3): sends framed `03`, decodes protobuf `UartMessage.version_response`.
- `-baud` — default `921600` (must match firmware)
## Regenerate protobuf ## Regenerate protobuf

View File

@ -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 | Role | UART to PC | ESP-NOW |
- **`uart`** — framed serial input, converts packets to `generic_msg_t` |--------|------------|---------|
- **`powerpod.c`** — creates the queue, calls `init_cmdHandler()` then `init_uart()` | 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 ## Boot configuration
cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
init_cmdHandler(cmd_queue); Read in `app_main()` before subsystems start. Stored in `app_config_t` (`app_config.h`).
init_uart(cmd_queue);
| 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
``` ```
## UART frame format | API | Description |
|-----|-------------|
Packets on **UART1** (921600 baud, **TX=GPIO3**, **RX=GPIO2** — USB adapter on `/dev/ttyUSB0`): | `init_cmdHandler(queue)` | Start dispatcher task (priority 5) |
| `msg_register_handler(id, cb)` | Register callback; max 32 handlers |
| Field | Value | | `msg_post(id, data, len)` | Enqueue from firmware (e.g. future ESP-NOW → PC path) |
|-----------|--------------------------------------------|
| Start | `0xAA` |
| Length | 1 byte, payload size (1252), 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:
```c ```c
typedef void (*msg_callback_t)(const uint8_t *data, size_t len); typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
``` ```
### `msg_post(uint16_t id, const uint8_t *data, size_t len)` Init order on master:
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.
```c ```c
uint8_t args[] = {0x02, 0x03}; cmd_queue = xQueueCreate(10, sizeof(generic_msg_t));
msg_post(0x01, args, sizeof(args)); 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). | ID | Name | Status |
2. Implement a handler in `powerpod.c` (or a dedicated module). |----|------|--------|
3. Call `msg_register_handler()` after `init_cmdHandler()`. | 3 | `VERSION` | Implemented (`cmd_version.c`) |
4. From a host tool, send a framed UART packet with that ID in byte 0. | 4 | `CLIENT_INFO` | Planned — slave list to PC |
| 5 | `CLIENT_INPUT` | Planned |
| 1620 | 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… | ### VERSION command
|--------|----------|
| `0x03` | nanopb-encoded `UartMessage` with `type = VERSION` and `version_response` set |
`VersionResponse` fields: **Request:** framed payload `03` only.
- `version``POWERPOD_FW_VERSION` (default `1`, override at compile time) **Response:** payload `03` + nanopb `UartMessage`:
- `git_hash` — short git hash from build (`POWERPOD_GIT_HASH`, from `git rev-parse`)
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 | ```bash
|-----------------|-------------------------------------------| cd goTool
| `cmd_handler.h` | Types and public API | go mod tidy
| `cmd_handler.c` | Queue dispatch, registration, `msg_post` | go run . -port /dev/ttyUSB0
| `uart.c` | Framed UART parser → queue | ```
| `powerpod.c` | Queue creation and init order |
| `cmd_version.c` | VERSION command handler | | Flag | Default | Description |
| `uart_proto.c` | Encode/send `UartMessage` over UART | |------|---------|-------------|
| `-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.