# Powerpod ESP-Firmware — Dokumentation Vollständige Referenz für das ESP-IDF-Projekt unter `main/` und `components/`. Diese Doku beschreibt **nur die Firmware** — kein goTool, keine Host-Implementierung. - **Architektur & Datenflüsse:** [`ARCHITECTURE.md`](ARCHITECTURE.md) - **Neues Feature end-to-end:** [`adding-a-feature.md`](adding-a-feature.md) --- ## Inhaltsverzeichnis 1. [Projektziel](#1-projektziel) 2. [Hardware und Pins](#2-hardware-und-pins) 3. [Build und Flash](#3-build-und-flash) 4. [Boot-Konfiguration](#4-boot-konfiguration) 5. [UART-Protokoll](#5-uart-protokoll) 6. [ESP-NOW-Protokoll](#6-esp-now-protokoll) 7. [Befehlsreferenz (UART)](#7-befehlsreferenz-uart) 8. [OTA](#8-ota) 9. [BMA456 Beschleunigungssensor](#9-bma456-beschleunigungssensor) 10. [LED-Ring](#10-led-ring) 11. [Board-Input und Batterie](#11-board-input-und-batterie) 12. [Persistenz (NVS)](#12-persistenz-nvs) 13. [Protobuf und Code-Generierung](#13-protobuf-und-code-generierung) 14. [Modulreferenz](#14-modulreferenz) 15. [Logging-Tags](#15-logging-tags) --- ## 1. Projektziel **Powerpod** ist Firmware für ESP32-S3-Knoten in einem verteilten System: - Ein **Master** spricht per **UART** mit einem externen Host und verwaltet bis zu **16 Slaves** über **ESP-NOW**. - **Slaves** haben keinen UART-Befehlspfad; sie joinen über periodisches **Discover**, senden Heartbeats und liefern Sensor-/Statusdaten. - Master und Slave nutzen **identisches Firmware-Image**; Rolle und Funknetz werden beim Boot erkannt. Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves und **cached** deren Telemetrie (`client_registry`) für schnelle UART-Abfragen. --- ## 2. Hardware und Pins > Pinbelegung ist in `powerpod.h` als vorläufig markiert — mit Schaltplan abgleichen. | Signal | GPIO | Datei / Modul | |--------|------|----------------| | DIP Master/Slave | 4 | `powerpod.h` — Low = Master | | I2C SCL / SDA | 5 / 6 | IO-Expander `0x20`, BMA456 `0x18` | | UART1 TX / RX | **2 / 3** | `uart.h` (Adapter: ESP-TX → Host-RX) | | LED-Ring (WS2812) | 7 | `led_ring.c` | | BMA456 Interrupt | 10 | `bosch456.c` | | Taster | 12 | `board_input.c` | | LiPo ADC 1 | 1 | `board_input.c` | | LiPo ADC 2 | 12 | Entfällt wenn = Taster-GPIO | **UART:** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control. **I2C:** 100 kHz, interne Pull-ups, gemeinsamer Bus für Expander und BMA456H. --- ## 3. Build und Flash **Ziel-Chip:** ESP32-S3 (ESP-IDF). ```bash source ~/esp/esp-idf/export.sh # oder export.fish cd /pfad/zu/powerpod idf.py build idf.py -p /dev/ttyUSB0 flash monitor ``` - **Git-Hash** wird beim Build in `POWERPOD_GIT_HASH` eingebettet (`main/CMakeLists.txt`). - **Firmware-Version:** `POWERPOD_FW_VERSION` (Default `1` in `esp_now_comm.c` / `cmd_version.c`). **Komponenten:** | Pfad | Inhalt | |------|--------| | `main/` | Anwendungslogik | | `components/bma456/` | Bosch BMA456H-Treiber (Vendor) | | `libs/nanopb/` | Protobuf-Codec für Embedded | --- ## 4. Boot-Konfiguration ### 4.1 Initialisierungsreihenfolge (`powerpod.c`) 1. `pod_settings_init()` — NVS 2. DIP → `app_config.master` 3. I2C + IO-Expander → `app_config.network` (1–8) 4. `init_bma456()` — optional, bei Fehler weiter ohne Sensor 5. `pod_settings_apply_accel_deadzone()` wenn Sensor da 6. OTA-Partition → `app_config.running_partition` 7. `board_input_init()` 8. `esp_now_comm_init(&app_config)` — **immer** (Master + Slave) 9. `led_ring_init()` 10. **Nur Master:** `cmd_queue` → `init_cmdHandler` → `init_uart` → alle `cmd_*_register()` ### 4.2 Master vs. Slave | Funktion | Master | Slave | |----------|--------|-------| | UART Command-Handler | Ja | Nein | | ESP-NOW Discover senden | Ja (Broadcast) | Nein | | ESP-NOW auf Discover reagieren | Nein | Ja | | `client_registry` für Fremdknoten | Ja | Nein (nur eigener Join-State) | --- ## 5. UART-Protokoll *(Transport + Anwendung — Datenfluss-Diagramme in [`ARCHITECTURE.md`](ARCHITECTURE.md) §4–5.)* ### 5.1 Rahmen | Byte | Inhalt | |------|--------| | 0 | `0xAA` Start | | 1 | Länge N (1–252) | | 2…N+1 | Payload | | N+2 | XOR-Checksum über Payload | | N+3 | `0xCC` Stopp | Implementierung: `uart.c` — `parse_uart_byte()`, `uart_send_framed()`. ### 5.2 Anwendungs-Payload | Offset | Bedeutung | |--------|-----------| | 0 | `MessageType` (siehe Enum in `uart_messages.proto`) | | 1… | nanopb `UartMessage` (ohne separates type-Feld im protobuf-Teil) | **Antworten** nutzen dieselbe Struktur: Byte 0 = Response-Typ, Rest = kodierte `UartMessage`. ### 5.3 Handler-Pipeline 1. `uart_read_task` parst Frames → `uart_enqueue_packet` 2. `generic_msg_t` → `cmd_queue` 3. `vCmdDispatcherTask` → registrierter `msg_callback_t` 4. Handler: `uart_cmd_decode(data, len, &msg)` — `data` ist **nur** protobuf-Teil; bei `len > 0` strikt (Decode/`which_payload`-Fehler → Fehlerantwort) 5. `ota_session_uart_cmd_allowed()` — während OTA nur OTA-Befehle 6. Antwort: `uart_cmd_init_response()` + Felder setzen + `uart_cmd_send()` Hilfsmakro für Request-Felder: ```c UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadzone_request) ``` --- ## 6. ESP-NOW-Protokoll *(Join, Tasks, Registry — [`ARCHITECTURE.md`](ARCHITECTURE.md) §6.)* ### 6.1 Grundlagen - WiFi **STA**, keine AP-Verbindung. - **Kanal** = `app_config.network` (1–13). - Payload = ein `EspNowMessage` (nanopb), max. `ESP_NOW_MAX_DATA_LEN`. - Schema: `main/proto/esp_now_messages.proto`. ### 6.2 Nachrichtentypen | Type | Richtung | Payload | Zweck | |------|----------|---------|--------| | `ESPNOW_DISCOVER` | M→Broadcast | `discover.network` | Slaves finden Master | | `ESPNOW_SLAVE_INFO` | S→M | `slave_info` | Erste Registrierung | | `ESPNOW_HEARTBEAT` | S→M | `heartbeat` | Keepalive 1 s | | `ESPNOW_SET_ACCEL_DEADZONE` | M→S | `accel_deadzone` | Deadzone LSB | | `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream ~16 ms | | `ESPNOW_ACCEL_SAMPLE` | S→M | `accel_sample` | x/y/z Roh-LSB | | `ESPNOW_SET_TAP_NOTIFY` | M→S | `tap_notify` | Tap-Arten filtern | | `ESPNOW_TAP_EVENT` | S→M | `tap_event` | kind 1/2/3 | | `ESPNOW_BATTERY_QUERY` | M→S | `battery_query` | On-demand | | `ESPNOW_BATTERY_REPORT` | S→M | `battery_report` | mV LiPo 1/2 | | `ESPNOW_LED_RING` | M→S | `led_ring` | Wie UART LED_RING | | `ESPNOW_FIND_ME` | M→S | `find_me` | LED-Locate | | `ESPNOW_RESTART` | M→S | `restart` | Reboot Slave | | `ESPNOW_UNICAST_TEST` | M→S | `unicast_test` | Link-Test | | `ESPNOW_OTA_*` | M↔S | `ota_*` | Firmware-Verteilung | ### 6.3 Zeitkonstanten (`esp_now_comm.c`) | Konstante | Wert | |-----------|------| | Discover-Intervall | 500 ms | | Heartbeat | 1000 ms | | Client-Timeout (Master) | 3 × Heartbeat = 3 s → `available=false` | | Master-Verlust (Slave) | 5 s ohne Discover | | Accel-Stream | 16 ms | | Batterie-Report | 30 s (+ einmal 150 ms nach Join) | ### 6.4 `EspNowSlavePresence` Felder: `network`, `mac` (6 B), `version`, `slave_id`, `available`, `used`. - `slave_id` = letztes Oktett der STA-MAC. - Registry auf dem Master indexiert über **Sender-MAC** der ESP-NOW-Callback. --- ## 7. Befehlsreferenz (UART) Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uart_messages.proto`. | ID | Name | Modul | Kurzbeschreibung | |----|------|-------|------------------| | 1 | ACK | — | Reserviert | | 2 | ECHO | — | Reserviert | | 3 | VERSION | `cmd_version.c` | FW-Version, Git-Hash, OTA-Partition | | 4 | CLIENT_INFO | `cmd_client_info.c` | Liste `client_registry` | | 5 | CLIENT_INPUT | — | Geplant | | 6 | ACCEL_DEADZONE | `cmd_accel_deadzone.c` | Get/Set Deadzone lokal + ESP-NOW | | 7 | ESPNOW_UNICAST_TEST | `cmd_espnow_unicast_test.c` | Link-Test zu Slave | | 8 | LED_RING | `cmd_led_ring.c` | Ring lokal / ESP-NOW | | 16 | OTA_START | `cmd_ota.c` | UART-OTA beginnen | | 17 | OTA_PAYLOAD | `cmd_ota.c` | Chunk ≤200 B | | 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung | | 19 | OTA_STATUS | `cmd_ota.c` | Gerät → Host Status | | 20 | OTA_START_ESPNOW | `cmd_ota.c` | Nur ESP-NOW aus Staging | | 21 | OTA_SLAVE_PROGRESS | `cmd_ota_slave_progress.c` | Fortschritt pro Slave | | 22 | FIND_ME | `cmd_espnow_find_me.c` | LED Locate | | 23 | RESTART | `cmd_restart.c` | Master oder Slave reboot | | 25 | ACCEL_STREAM | `cmd_accel_stream.c` | ESP-NOW Accel-Stream an/aus | | 26 | BATTERY_STATUS | `cmd_battery.c` | Cache LiPo Master + Slaves | | 27 | TAP_NOTIFY | `cmd_tap_notify.c` | Tap-Weiterleitung konfigurieren | | 29 | CACHE_STATUS | `cmd_cache_status.c` | Accel + Tap Cache (ein Round-Trip) | ### 7.1 VERSION (3) - **Request:** nur Typ-Byte `0x03` oder leerer protobuf-Body. - **Response:** `version`, `git_hash`, `running_partition`. ### 7.2 CLIENT_INFO (4) - **Response:** wiederholtes `ClientInfo` pro Registry-Eintrag: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping`, Tap-/Stream-Flags. ### 7.3 ACCEL_DEADZONE (6) - **Request:** `write`, `deadzone`, `client_id` (0=lokal), `all_clients`. - **Write Master:** Registry + `esp_now_comm_send_accel_deadzone` pro Slave; lokal `bma456` + NVS. - **Slave-Empfang:** `handle_slave_accel_deadzone` in `esp_now_comm.c`. ### 7.4 ACCEL_STREAM (25) - Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`. - Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms. ### 7.5 TAP_NOTIFY (27) - Konfiguriert auf Slave welche Tap-Arten `ESPNOW_TAP_EVENT` auslösen. - Tap-Quelle: BMA456-Interrupt → `on_bma456_tap` in `esp_now_comm.c`. ### 7.6 CACHE_STATUS (29) - **Request:** leer. - **Response:** pro Slave mit Stream und/oder Tap-Notify: gecachte Accel-Werte (`age_ms`) und konsumierte Tap-Events (`client_registry_take_tap`). ### 7.7 BATTERY_STATUS (26) - Liest **nur Cache** — keine ESP-NOW-Roundtrip-Pflicht pro Abfrage. - Master-Batterie: `client_registry_set_master_battery` (Monitor-Task). ### 7.8 LED_RING (8) | mode | Bedeutung | |------|-----------| | 0 | Clear | | 1 | Progress 0–100 % | | 2 | Digit 0–10 | | 3 | Blink | | 4 | Find-me (RGB-Sequenz) | | 5 | Solid color | `client_id=0` nur Master-Ring; `>0` ESP-NOW; `all_clients` Broadcast über Registry. ### 7.9 FIND_ME (22) / RESTART (23) - `client_id=0`: lokaler Master (`led_ring_find_me` / `pod_schedule_restart`). - `client_id>0`: ESP-NOW Unicast zum Slave. ### 7.10 ESPNOW_UNICAST_TEST (7) Minimaler Master→Slave-Ping; Slave loggt `UNICAST TEST OK`. --- ## 8. OTA ### 8.1 UART-OTA (nur Master) Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. | Phase | Host → Master | Master → Host | |-------|---------------|---------------| | Start | `OTA_START` + `total_size` | `OTA_STATUS` preparing → ready | | Daten | `OTA_PAYLOAD` ≤200 B, `seq` | `block_ack` je 4096 B Flash | | Ende | `OTA_END` | success/failed; startet ESP-NOW-Verteilung | - Inaktive Partition: `esp_ota_get_next_update_partition()`. - LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung). ### 8.2 ESP-NOW-OTA (Master → Slaves) `ota_espnow.c` — gleiche Status-Codes wie UART. 1. `ESPNOW_OTA_START` + `total_size` → Slave `ota_espnow_slave_on_start` 2. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave 3. `ESPNOW_OTA_END` → Boot-Partition setzen 4. Slave antwortet `ESPNOW_OTA_STATUS` (mit `send_message_ex` + Semaphore auf Master) Nach Erfolg: **alle Knoten neu starten**. ### 8.3 OTA_SLAVE_PROGRESS (21) Abfrage laufender oder letzter ESP-NOW-Verteilung: pro Slave `bytes_written`, `status`, `error`. --- ## 9. BMA456 Beschleunigungssensor | Thema | Detail | |-------|--------| | Wrapper | `bosch456.c` / `bosch456.h` | | Chip-Variante | BMA456**H** (`bma456h.c` im Component) | | I2C | Adresse 0x18, 100 kHz | | Polling | Task ~10 Hz | | Interrupt GPIO 10 | Single/Double/Triple Tap | | Deadzone | Software-Filter für Logs/Stream (Default 100 LSB) | | API | `bma456_read_accel`, `bma456_set_accel_deadzone`, `bma456_set_tap_handler` | Ohne Sensor: `bma456_is_ready() == false`, Firmware läuft weiter. --- ## 10. LED-Ring - **95 LEDs**, WS2812 über RMT (`led_ring.c`). - Segment-Karten für Ziffern 0–10 in `digit_lookup[]`. - **Kein** lokaler Demo-Loop — Anzeige nur über UART (`cmd_led_ring.c`) oder ESP-NOW `ESPNOW_LED_RING`. - Helligkeit: `intensity` 0 → Default ~5 % (`LED_RING_DEFAULT_INTENSITY`). --- ## 11. Board-Input und Batterie `board_input.c`: - **Taster** GPIO 12 — Logging bei Druck. - **LiPo-ADC** GPIO 1 (und optional 2, wenn nicht Taster). - Master: `master_monitor_task` aktualisiert Master-Batterie alle 30 s. - Slave: `slave_send_battery_report_to_master` nach Join, Heartbeat und Query. Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART. --- ## 12. Persistenz (NVS) `pod_settings.c` — Namespace `powerpod`: | Key | Inhalt | |-----|--------| | `accel_dz` | Accel-Deadzone LSB | Gespeichert bei lokalem Set (UART `client_id=0`, ESP-NOW auf Slave). Geladen nach `init_bma456()`. --- ## 13. Protobuf und Code-Generierung ```bash make proto_generate # beide Schemas make proto_generate_uart # nur uart_messages.proto make proto_generate_espnow # nur esp_now_messages.proto ``` **Ausgabe:** `main/proto/*.pb.c`, `*.pb.h` **Optionen:** `main/proto/uart_messages.options` (nanopb max_size etc.) Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `main/proto/...`). **Paketname protobuf:** `alox` → C-Prefix `alox_`. --- ## 14. Modulreferenz ### 14.1 Kern | Datei | Rolle | |-------|--------| | `powerpod.c` | `app_main`, Init, Master-Register | | `app_config.h` | Laufzeit-Konfiguration | | `cmd_handler.c` | Queue + Dispatcher | | `uart.c` | Framing | | `uart_proto.c` | UartMessage send | | `uart_cmd.c` | Handler-Hilfen | ### 14.2 Kommunikation | Datei | Rolle | |-------|--------| | `esp_now_comm.c` | Init, recv-Router | | `esp_now_core.c` | WiFi, Peer, gemeinsamer Send | | `esp_now_master.c` | Master-Tasks, Registry, Unicast send | | `esp_now_slave.c` | Join, Heartbeat, Accel/Tap send | | `esp_now_proto.c` | nanopb für EspNowMessage | | `client_registry.c` | Slave-Tabelle + Telemetrie-Cache | ### 14.3 Befehle (`main/cmd/`) | Datei | UART-Typ | |-------|----------| | `cmd_version.c` | VERSION | | `cmd_client_info.c` | CLIENT_INFO | | `cmd_accel_deadzone.c` | ACCEL_DEADZONE | | `cmd_accel_stream.c` | ACCEL_STREAM | | `cmd_tap_notify.c` | TAP_NOTIFY | | `cmd_cache_status.c` | CACHE_STATUS | | `cmd_battery.c` | BATTERY_STATUS | | `cmd_led_ring.c` | LED_RING | | `cmd_espnow_find_me.c` | FIND_ME | | `cmd_restart.c` | RESTART | | `cmd_espnow_unicast_test.c` | ESPNOW_UNICAST_TEST | | `cmd_ota.c` | OTA_* | | `cmd_ota_slave_progress.c` | OTA_SLAVE_PROGRESS | ### 14.4 Domäne | Datei | Rolle | |-------|--------| | `bosch456.c` | Accelerometer + Tap | | `led_ring.c` | LED-Anzeige | | `board_input.c` | Taster, ADC | | `pod_settings.c` | NVS | | `pod_reboot.c` | Verzögerter Restart | | `ota_uart.c` | Flash-Puffer UART-OTA | | `ota_espnow.c` | OTA Master/Slave; Slave-Work-Queue | | `ota_session.c` | OTA-Sperre für UART-Dispatcher | ### 14.5 Protobuf | Datei | Rolle | |-------|--------| | `proto/uart_messages.proto` | UART-Vertrag | | `proto/esp_now_messages.proto` | ESP-NOW-Vertrag | | `proto/pb_encode.c`, `pb_decode.c`, `pb_common.c` | nanopb Runtime | --- ## 15. Logging-Tags | Tag | Modul | |-----|--------| | `[Main]` | powerpod.c | | `[UART]` | uart.c | | `[CMDH]` | cmd_handler.c | | `[UART_CMD]` | uart_cmd.c | | `[ESPNOW]` | esp_now_comm.c | | `[CLIENTS]` | client_registry.c | | `[BMA456]` | bosch456.c | | `[VERSION]` etc. | jeweiliger cmd_* | Typischer Ablauf bei UART-Befehl: ``` [UART] received message cmd=0x06 len=… [CMDH] trigger command ACCEL_DEADZONE (0x06) [ACCEL_DZ] … ``` --- ## Anhang: Neues UART-Kommando hinzufügen 1. `uart_messages.proto` erweitern → `make proto_generate_uart` 2. `cmd/cmd_neu.c` mit `handle_*` + `uart_cmd_register()` 3. In `powerpod.c` `cmd_neu_register()` aufrufen (nur Master-Zweig) 4. Bei Slave-Bedarf: `esp_now_messages.proto` + `esp_now_comm_send_*` + Slave-Zweig in `espnow_recv_cb` Siehe [`adding-a-feature.md`](adding-a-feature.md) für ein vollständiges Beispiel (Find Me).