Expose the command via goTool CLI/REST and dashboard controls so log verbosity can be tuned without reflashing. Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Architektur & ESP-only Doku (ohne goTool): ../docs/ARCHITECTURE.md (Datenflüsse UART/Commands/ESP-NOW), ../docs/DOCUMENTATION.md (vollständige Referenz).
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 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 |
| BMA456 INT | 10 |
| Button (Taster) | 12 |
| LiPo sense 1 (ADC) | 1 |
| LiPo sense 2 (ADC) | 12 (skipped if same as button) |
TODO: GPIO assignments above are provisional; confirm pinning against the real board before release.
Startup order:
- Read DIP + IO expander →
app_config - I2C bus — IO expander
0x20; optional BMA456H (init_bma456, same bus) esp_now_comm_init(&app_config)— WiFi + ESP-NOWled_ring_init()board_input_init()— button press logs, LiPo ADC logs every 10 s- Master only: command queue, UART, registered commands (e.g. VERSION)
BMA456 accelerometer (bosch456.c)
Powerpod uses the Bosch BMA456H (hearable) variant, not the generic bma456w examples in the vendor tree.
| Item | Value |
|---|---|
| Project wrapper | main/bosch456.c, main/bosch456.h |
| Vendor component | components/bma456 — only bma4.c + bma456h.c are linked |
| I2C | Shared bus with IO expander (SCL/SDA GPIO 5/6), address 0x18, 100 kHz |
| Interrupt | GPIO 10, active high, tap events (single / double / triple) |
| Polling | FreeRTOS task bma456_poll, 10 Hz accel read |
Boot: init_bma456(bus_handle) runs on master and slave after the IO expander. If the sensor is missing or init fails, firmware logs BMA456 init skipped and continues (bma456_is_ready() == false).
Accel logging: Samples are printed only when any axis changes by more than the deadzone (raw LSB) since the last logged sample (default 100). This is a software filter on top of the sensor; it does not change BMA456 hardware thresholds.
Persistence: The local deadzone is stored in the nvs partition (namespace powerpod, key accel_dz) via pod_settings.c. Each node (master or slave) keeps its own value across reboot. Loaded at boot after init_bma456(); saved when set locally (UART client_id = 0, all_clients on master, or ESP-NOW deadzone on a slave).
Configuration paths:
| Path | Effect |
|---|---|
UART ACCEL_DEADZONE with client_id = 0 |
Set + save local deadzone |
ESP-NOW SET_ACCEL_DEADZONE |
Set + save on the receiving slave |
make gotool-deadzone-set DEADZONE=… CLIENT=0 |
Host shortcut for local deadzone |
Logs: [BMA456] ACC X=… Y=… Z=… when deadzone exceeded; [BMA456] tap: single|double|triple on interrupt.
Regenerate nanopb only when changing protos; sensor code has no code generation step.
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 (nanopb)
Schema: proto/esp_now_messages.proto. Encode/decode: esp_now_proto.c. The ESP-NOW payload is a single encoded EspNowMessage (no extra framing).
EspNowMessageType |
Direction | oneof payload |
|---|---|---|
ESPNOW_DISCOVER |
Master → broadcast FF:FF:FF:FF:FF:FF |
EspNowDiscover (network) |
ESPNOW_SLAVE_INFO |
Slave → master | EspNowSlavePresence |
ESPNOW_HEARTBEAT |
Slave → master | EspNowSlavePresence (same fields) |
ESPNOW_SET_ACCEL_DEADZONE |
Master → slave | EspNowAccelDeadzone (deadzone LSB) |
ESPNOW_UNICAST_TEST |
Master → slave | EspNowUnicastTest (seq) |
ESPNOW_FIND_ME |
Master → slave | EspNowFindMe (client_id filter) — LED locate sequence |
ESPNOW_RESTART |
Master → slave | EspNowRestart (client_id filter) — reboot slave |
ESPNOW_ACCEL_SAMPLE |
Slave → master | EspNowAccelSample (slave_id, x, y, z raw LSB) — ~every 16 ms |
ESPNOW_SET_TAP_NOTIFY |
Master → slave | EspNowTapNotify (client_id, single, double_tap, triple) — which tap kinds to forward |
ESPNOW_TAP_EVENT |
Slave → master | EspNowTapEvent (client_id, kind) — on BMA456 tap interrupt if notify enabled |
ESPNOW_BATTERY_REPORT |
Slave → master | EspNowBatteryReport (client_id, lipo1/2 mV) — ~every 30 s; cached in client_registry |
ESPNOW_OTA_START |
Master → slave (unicast) | EspNowOtaStart (total_size) |
ESPNOW_OTA_PAYLOAD |
Master → slave | EspNowOtaPayload (seq, up to 200 B data) |
ESPNOW_OTA_END |
Master → slave | EspNowOtaEnd |
ESPNOW_OTA_STATUS |
Slave → master | EspNowOtaStatus (same status codes as UART OTA) |
ESP-NOW OTA (master → slaves)
Triggered automatically after a successful UART OTA_END on the master (or manually via UART OTA_START_ESPNOW if an image is already staged). Implementation: ota_espnow.c.
| Step | Master → slave | Slave → master |
|---|---|---|
| 1 | ESPNOW_OTA_START + total_size |
ESPNOW_OTA_STATUS preparing, then ready |
| 2 | ESPNOW_OTA_PAYLOAD (≤200 B, shared seq) |
block_ack after each 4096 B written to flash |
| 3 | ESPNOW_OTA_END |
success or failed (+ bytes_written) |
Master reads the staged partition with esp_partition_read (same image just written via UART). Only available registry slaves are updated. The last transfer block may be under 4096 B — no block_ack is waited for that block; slaves flush the remainder on ESPNOW_OTA_END.
Status codes match UART OtaStatusPayload (1…5). After success, master and slaves have the boot partition set — reboot all nodes to run the new firmware.
EspNowSlavePresence: network, mac (6 bytes), version, slave_id, available, used.
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). Task espnow_mon runs every 1 s and marks a client inactive (available = false) if no SLAVE_INFO or HEARTBEAT was received for 3 s (three missed 1 s heartbeats). A later heartbeat sets available true again and logs reactivation.
Slave: on first matching DISCOVER, logs joined network N, master …, sends SLAVE_INFO once, then sends HEARTBEAT to the master every 1 s. While joined, periodic discovers from the same master refresh a “master alive” timer; if no discover arrives for 5 s, the slave treats the master as lost, clears join state, and will register again on the next discover (reconnect). Discover from a different master is ignored while already joined.
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 frommessage_type_name())
Command handler
Generic dispatch for host commands over UART only.
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 |
During an OTA session (ota_session_busy()), the dispatcher rejects all UART commands except OTA_* and OTA_SLAVE_PROGRESS (see ota_session.c).
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
Init order on master:
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/cmd_version.c) |
| 4 | CLIENT_INFO |
Implemented (cmd/cmd_client_info.c) — slave list from registry |
| 5 | CLIENT_INPUT |
Planned |
| 6 | ACCEL_DEADZONE |
Implemented (cmd/cmd_accel_deadzone.c) — get/set accel filter LSB |
| 7 | ESPNOW_UNICAST_TEST |
Implemented (cmd/cmd_espnow_unicast_test.c) |
| 8 | LED_RING |
Implemented (cmd/cmd_led_ring.c) — ring progress bar (0–100 %, RGB, intensity) |
| 26 | BATTERY_STATUS |
Implemented (cmd/cmd_battery.c) — cached LiPo 1/2 per pod from client_registry (UART read, no slave round-trip) |
| 16 | OTA_START |
Implemented (cmd/cmd_ota.c) — begin UART OTA on inactive slot |
| 17 | OTA_PAYLOAD |
Implemented — up to 200 B per frame; device buffers 4 KiB |
| 18 | OTA_END |
Implemented — flush, esp_ota_end, push image to slaves via ESP-NOW, set boot |
| 19 | OTA_STATUS |
Device → host (prepare/ready/block ACK/success/failed) |
| 20 | OTA_START_ESPNOW |
Implemented — re-distribute staged image to slaves only |
| 21 | OTA_SLAVE_PROGRESS |
Implemented (cmd/cmd_ota_slave_progress.c) — query per-slave ESP-NOW OTA progress |
| 22 | FIND_ME |
Implemented (cmd/cmd_espnow_find_me.c) — client_id=0 local ring, >0 ESP-NOW to slave |
| 23 | RESTART |
Implemented (cmd/cmd_restart.c) — client_id=0 reboot master, >0 ESP-NOW reboot slave |
| 25 | ACCEL_STREAM |
Implemented — enable/disable slave ESP-NOW accel stream to master |
| 27 | TAP_NOTIFY |
Implemented (cmd/cmd_tap_notify.c) — get/set which tap kinds notify via ESP-NOW |
| 29 | CACHE_STATUS |
Implemented (cmd/cmd_cache_status.c) — subscribed accel + tap cache (one UART round-trip) |
| 30 | ESPNOW_ECHO_PING |
Implemented (cmd/cmd_espnow_echo_ping.c) — ESP-NOW timestamp echo (latency test) |
| 31 | SET_LOG_LEVEL |
Implemented (cmd/cmd_set_log_level.c) — get/set global esp_log_level_set("*", …) on master |
Regenerate C code:
make proto_generate
# or individually:
make proto_generate_uart
make proto_generate_espnow
After generation, ensure main/proto/*.pb.c includes use #include "…pb.h" (not main/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 = VERSIONversion_response.version—POWERPOD_FW_VERSIONversion_response.git_hash— build git hash stringversion_response.running_partition— active OTA label (ota_0/ota_1)
Encoding: uart_send_uart_message() in uart_proto.c.
At boot, firmware logs the running partition and OTA slot index (A/B).
UART OTA (A/B)
UART upload is master-only. Slaves receive the same image afterwards via ESP-NOW OTA.
Inactive app partition is selected with esp_ota_get_next_update_partition(); esp_ota_begin erases it (can take ~30 s — host should wait).
| Step | Host → master | Master → host |
|---|---|---|
| 1 | OTA_START + total_size |
OTA_STATUS preparing, then ready (+ target_slot 0/1) |
| 2 | OTA_PAYLOAD chunks (≤200 B, seq optional) |
OTA_STATUS block_ack only after each 4096 B written to flash |
| 3 | OTA_END |
Stages image, runs ESP-NOW OTA to all available slaves, sets boot partition, then OTA_STATUS success or failed |
OTA_END can take a long time on the wire (slave flash + ESP-NOW); the host should use a generous read timeout.
During OTA the LED ring shows progress at ~5 % brightness: blue while the image is written (UART on master, ESP-NOW on slaves), green on the master while it forwards the image to slaves over ESP-NOW. On success the ring gives one short green blink; on failure one red blink and ESP-NOW distribution is not started (failed UART upload / OTA_END validation).
OTA_START_ESPNOW (type 20): re-run ESP-NOW distribution from the last staged image without a new UART upload (no-op if nothing staged).
Implementation: ota_uart.c (4 KiB buffer, esp_ota_write), ota_espnow.c, cmd/cmd_ota.c.
Host upload:
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
OtaStatusPayload.status: 1 preparing, 2 ready, 3 block_ack, 4 success, 5 failed, 6 distributing (bytes_written = progress, target_slot = slave count).
OTA_SLAVE_PROGRESS command
Request: framed 15 (0x15) + optional ota_slave_progress_request (client_id; 0 = all slaves in the current/last distribution session).
Response: ota_slave_progress_response:
| Field | Meaning |
|---|---|
active |
ESP-NOW distribution running |
total_bytes |
Image size |
aggregate_bytes |
Overall bytes sent to all slaves |
slave_count |
Number of slaves in session |
slaves[] |
Per slave: client_id, bytes_written, total_bytes, status, error |
Per-slave status: 0 idle, 1 preparing, 2 ready, 3 block_ack/distributing, 4 success, 5 failed.
go run . -port /dev/ttyUSB0 ota-progress
go run . -port /dev/ttyUSB0 ota-progress -client 16
ACCEL_DEADZONE command
Sets the software deadzone used by bosch456.c when logging accel (see BMA456 accelerometer). Default 100 LSB.
Request: framed 06 + nanopb UartMessage with accel_deadzone_request:
| Field | Meaning |
|---|---|
write |
false = read, true = write |
deadzone |
Threshold in LSB (write) |
client_id |
0 = local sensor on this node; >0 = slave id (master) |
all_clients |
Master: ESP-NOW unicast to every registered slave |
Response: accel_deadzone_response with applied deadzone, success, and slaves_updated (ESP-NOW count).
TAP_NOTIFY command
Configure which BMA456 tap kinds a slave forwards to the master over ESP-NOW. The slave only sends ESPNOW_TAP_EVENT when the matching notify flag is enabled (set locally on the slave via ESP-NOW).
Request: framed 1b (0x1b) + tap_notify_request:
| Field | Meaning |
|---|---|
write |
false = read, true = write |
single, double_tap, triple |
Which tap kinds to notify (write) |
client_id |
Slave id (read/write one slave) |
all_clients |
Master: ESP-NOW unicast to every registered slave |
Response: tap_notify_response (client_id, success, slaves_updated, single, double_tap, triple).
Notify flags are mirrored in ClientInfo (tap_notify_single/double/triple) for the dashboard.
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
go run . -port /dev/ttyUSB0 tap-notify -client 16
CACHE_STATUS command
Read cached accel and/or tap data on the master in one UART round-trip. Slaves send ESPNOW_ACCEL_SAMPLE every 16 ms when streaming; tap events arrive via ESPNOW_TAP_EVENT and are held up to 16 ms (CLIENT_REGISTRY_TAP_MAX_AGE_MS). Pending taps are consumed on read (like the former TAP_SNAPSHOT).
Request: framed 1d (0x1d) only — no body (CacheStatusRequest empty).
Response: cache_status_response.clients[] — one entry per slave with accel_stream_enabled and/or any tap-notify flag:
| Field | When present |
|---|---|
client_id |
Always (for listed slaves) |
accel |
Slave has accel stream on (valid, x/y/z, age_ms when sample fresh) |
tap |
Tap notify on and a pending tap was consumed (kind, age_ms) |
Unsubscribed submessages are omitted on the wire (proto3 defaults). The master walks client_registry once per request (cmd/cmd_cache_status.c).
Host tools poll this at 16 ms when live-stream / WebSocket receive is enabled. Tap events stay visible for 2 s in the UI/API after first sight.
go run . -port /dev/ttyUSB0 cache-status
External API (serve -api-addr :8081) uses the same command for WebSocket accel / tap push.
ESPNOW_UNICAST_TEST command
Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging ACCEL_DEADZONE unicast.
Request: framed 07 + espnow_unicast_test_request (client_id, seq).
Response: espnow_unicast_test_response (success, seq).
Firmware logs: master unicast TEST to … seq=N; slave UNICAST TEST OK from master … seq=N.
FIND_ME command
Locate a pod: the LED ring blinks 3× red, 3× green, 3× blue at full brightness.
Request: framed 22 + espnow_find_me_request (client_id: 0 = master only, >0 = ESP-NOW unicast to that slave).
Response: espnow_find_me_response (success, client_id).
go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
SET_LOG_LEVEL command
Read or set the global ESP-IDF log filter on the master (esp_log_level_set("*", level)). Does not affect the host UART protocol (UART1); esp_log_* output goes to the debug console UART0 (USB, 115200).
Request: framed 31 (0x1f) + set_log_level_request (write, level 0–5).
Response: set_log_level_response (success, level).
level |
esp_log_level_t |
|---|---|
| 0 | NONE |
| 1 | ERROR |
| 2 | WARN |
| 3 | INFO |
| 4 | DEBUG |
| 5 | VERBOSE |
Boot default follows CONFIG_LOG_DEFAULT_LEVEL in sdkconfig (not persisted across reboot).
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 0
RESTART command
Reboot the master (client_id=0) or one slave via ESP-NOW (client_id = registry id). The device sends the UART response, then restarts after ~150 ms.
Request: framed 23 + restart_request
Response: restart_response (success, client_id)
go run . -port /dev/ttyUSB0 restart
go run . -port /dev/ttyUSB0 restart -client 16
BATTERY_STATUS command
Read cached LiPo ADC values on the master (master local + one entry per registered slave). Slaves push ESPNOW_BATTERY_REPORT every 30 s; the master stores them in client_registry (lipo1/2_valid, lipo1/2_mv, battery_updated_at). The master refreshes its own pack on the same interval in master_monitor_task.
Request: framed 26 + optional battery_status_request (client_id, all_clients).
Response: battery_status_response with samples[] (client_id, lipo1, lipo2, age_ms).
# Host / goTool: all_clients returns master (id 0) + slaves from cache
LED_RING command
Control the 95-LED ring from the host. The firmware does not animate digits locally; only UART updates the display.
Request: framed 08 + led_ring_progress_request:
| Field | Meaning |
|---|---|
mode |
0 = clear, 1 = progress, 2 = digit (0–10), 3 = blink, 4 = find-me, 5 = solid color (all LEDs) |
progress |
0–100 (% of ring lit, mode 1) |
digit |
0–10 (mode 2, segment maps in led_ring.c) |
r, g, b |
Color 0–255 |
intensity |
Brightness 0–255 (scaled into RGB; 0 → firmware default ~5 %) |
blink_ms, blink_count |
Pulse length and count (mode 3; defaults 350 ms, 1) |
client_id |
0 = master ring only; >0 = ESP-NOW unicast to one slave |
all_clients |
Broadcast to all registered slaves |
slaves_only |
With all_clients: do not change master ring |
Response: led_ring_progress_response (success, mode, progress, digit, client_id, slaves_updated).
Slaves receive the same command via ESP-NOW ESPNOW_LED_RING and run it locally.
go run . -port /dev/ttyUSB0 led-ring -mode progress -progress 75 -g 80 -b 255
go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 7 -r 255 -g 200
go run . -port /dev/ttyUSB0 led-ring -mode clear
go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2
go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
go run . -port /dev/ttyUSB0 led-ring -mode find-me
go run . -port /dev/ttyUSB0 led-ring -mode color -r 255 -g 0 -b 0 -client 16
go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all
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, tap_notify_single, tap_notify_double, tap_notify_triple — milliseconds since the last packet / last successful heartbeat (computed when CLIENT_INFO is answered; typically 0–1000 while the slave is heartbeating every 1 s).
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_heartbeat(mac, id, version, …) |
Same as upsert for heartbeats; reactivates inactive clients |
client_registry_check_timeouts(timeout_ms) |
Mark stale clients inactive (master monitor task) |
client_registry_count() / client_registry_at(i) |
Iterate for UART encoding |
client_registry_set_tap_notify() / client_registry_take_tap() |
Tap notify flags + short-lived tap cache (16 ms) |
Slaves register when the master receives SLAVE_INFO on the matching network; HEARTBEAT keeps them marked available. The registry MAC is always the ESP-NOW source address (recv_info.src_addr), not the optional mac bytes in the protobuf (used only on the wire for debugging).
slave_id is the sender’s WiFi STA address last octet (mac[5]); it can collide across devices — use gotool clients and match the full MAC.
Host tool (goTool/)
Go CLI to test UART from a PC connected to the master only.
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:
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
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 |
ESP-NOW init and recv router |
esp_now_core.c/h |
Shared WiFi, peer, send |
esp_now_master.c/h |
Master discover, monitor, unicast |
esp_now_slave.c/h |
Slave join, heartbeat, telemetry |
ota_uart.c/h |
Shared 4 KiB OTA flash buffer (UART + ESP-NOW) |
ota_espnow.c/h |
Master: distribute staged image to slaves |
cmd/cmd_ota.c/h |
UART OTA command handlers (master only) |
uart.c/h |
Framed UART RX/TX |
uart_proto.c/h |
Encode/send UartMessage |
cmd/cmd_handler.c/h |
Command queue and dispatch |
uart_cmd.c/h |
Shared UART decode/send helpers for handlers |
cmd/cmd_version.c/h |
VERSION handler |
cmd/cmd_client_info.c/h |
CLIENT_INFO handler |
client_registry.c/h |
Registered slave table |
bosch456.c/h |
BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter |
cmd/cmd_tap_notify.c |
UART TAP_NOTIFY — ESP-NOW tap notify config |
cmd/cmd_cache_status.c |
UART CACHE_STATUS — subscribed accel + tap cache poll |
board_input.c/h |
Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 |
pod_settings.c/h |
NVS persistence (accel deadzone, …) |
led_ring.c/h |
LED ring (digit display, progress bar) |
cmd/cmd_led_ring.c |
UART LED_RING progress command |
cmd/cmd_set_log_level.c |
UART SET_LOG_LEVEL — runtime ESP-IDF log level |
proto/uart_messages.proto |
UART protocol schema |
proto/esp_now_messages.proto |
ESP-NOW protocol schema |
esp_now_proto.c/h |
Encode/decode EspNowMessage |
proto/*.pb.c/h |
Generated nanopb |
CMakeLists.txt |
Sources, esp_wifi, drivers, git hash |
Adding a new feature (UART → ESP-NOW)
End-to-end walkthrough (protobuf, master handler, ESP-NOW unicast to slaves, goTool, dashboard) with Find me as the worked example:
Short checklist:
- Add or extend messages in
uart_messages.proto(andesp_now_messages.protoif slaves are involved); runmake proto_generateandmake gotool-proto. - Implement device logic in a shared module (e.g.
led_ring.c), not only in the UART handler. - Create
cmd/cmd_*.c, register withuart_cmd_register(); decode withuart_cmd_decode()/UART_CMD_REQ(); reply withuart_cmd_init_response()+uart_cmd_send(). - Master → slave:
esp_now_comm_send_*()+ slave branch inespnow_recv_cb. - Extend
goTool(CLI, optional/api/…and web UI).
For ESP-NOW-driven PC updates later: map slave state to ClientInfo and send CLIENT_INFO over UART from the master.