powerpods/docs/ARCHITECTURE.md
simon 490e0ee61f Add UART SET_LOG_LEVEL for runtime master ESP-IDF logging.
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>
2026-06-06 18:03:34 +02:00

16 KiB
Raw Permalink Blame History

Powerpod — Architektur-Spezifikation

Dieses Dokument beschreibt die Firmware-Architektur des ESP32-S3-Projekts: Rollen, Schichten, Datenflüsse und die wichtigsten Implementierungsstellen. goTool (Host-CLI) ist bewusst ausgeschlossen — es nutzt nur das UART-Protokoll des Masters.


1. Systemüberblick

Master und Slave laufen mit demselben Binary. Die Rolle wird beim Boot per DIP-Schalter und I2C-IO-Expander festgelegt; danach verzweigt die Initialisierung.

flowchart TB
  subgraph host["Externer Host (UART)"]
    PC[PC / beliebiger UART-Client]
  end

  subgraph master["Master ESP32-S3"]
    UART_RX[uart_read_task]
    Q[cmd_queue]
    DISP[vCmdDispatcherTask]
    HAND[cmd/*.c Handler]
    REG[client_registry]
    ENOW_M[esp_now_comm Master]
  end

  subgraph slaves["Slave ESP32-S3 × N"]
    ENOW_S[esp_now_comm Slave]
    BMA[BMA456 + LED Ring]
  end

  PC <-->|UART1 921600 framed + protobuf| UART_RX
  UART_RX --> Q --> DISP --> HAND
  HAND --> REG
  HAND --> ENOW_M
  ENOW_M <-->|ESP-NOW nanopb| ENOW_S
  ENOW_S --> BMA
  ENOW_S -->|Accel/Tap/Battery| ENOW_M
  ENOW_M --> REG
Rolle UART ESP-NOW Zentrale Datenhaltung
Master Ja — einziger Befehlseingang von außen Discover (Broadcast), Unicast zu Slaves client_registry.c
Slave Nein Antwort auf Discover, Heartbeat, Events zum Master esp_now_slave.c

Einstieg: main/powerpod.capp_main().


2. Boot und Konfiguration

2.1 Ablauf

sequenceDiagram
  participant AM as app_main
  participant NVS as pod_settings
  participant I2C as I2C / IO-Expander
  participant BMA as bosch456
  participant EN as esp_now_comm
  participant LR as led_ring
  participant BI as board_input
  participant CMD as cmd_handler + uart

  AM->>NVS: pod_settings_init()
  AM->>AM: GPIO DIP_MASTER → master/slave
  AM->>I2C: Bus + Expander 0x20 → network 18
  AM->>BMA: init_bma456() (Master + Slave)
  AM->>AM: app_config_t füllen
  AM->>BI: board_input_init()
  AM->>EN: esp_now_comm_init(&app_config)
  AM->>LR: led_ring_init()
  alt master == true
    AM->>CMD: Queue, Dispatcher, UART, Handler registrieren
  end

2.2 app_config_t

Feld Quelle Bedeutung
master DIP_MASTER (GPIO 4): Low = Master Steuert UART + Registry-Nutzung
network IO-Expander, Bits 58 (nibble reversed) 18 → WiFi/ESP-NOW-Kanal
running_partition esp_ota_get_running_partition() Aktives OTA-Label (ota_0 / ota_1)

Dateien: main/app_config.h, main/powerpod.c, main/powerpod.h (Pins).


3. Schichtenmodell

┌─────────────────────────────────────────────────────────────┐
│  Befehlshandler (main/cmd/cmd_*.c)                          │
│  Decode/Encode: uart_cmd.c, Antwort: uart_proto.c           │
├─────────────────────────────────────────────────────────────┤
│  Dispatch: cmd_handler.c (Queue + vCmdDispatcherTask)       │
├─────────────────────────────────────────────────────────────┤
│  Transport UART: uart.c (Framing)  │  ESP-NOW: esp_now_comm │
├─────────────────────────────────────────────────────────────┤
│  Protokoll: uart_messages.proto    │  esp_now_messages.proto │
│  Codec: nanopb (proto/*.pb.c)      │  esp_now_proto.c        │
├─────────────────────────────────────────────────────────────┤
│  Domäne: client_registry, bosch456, led_ring, ota_*, board  │
├─────────────────────────────────────────────────────────────┤
│  ESP-IDF: UART, WiFi, ESP-NOW, I2C, ADC, OTA, NVS, FreeRTOS │
└─────────────────────────────────────────────────────────────┘

4. Datenfluss: Commands (Befehle)

Commands sind asynchron über eine FreeRTOS-Queue entkoppelt; der UART-Reader blockiert nicht auf Handler-Logik.

4.1 Eingang (UART → Handler)

flowchart LR
  A[Bytes UART1] --> B[parse_uart_byte]
  B --> C{Frame vollständig?}
  C -->|ja| D[uart_enqueue_packet]
  D --> E["generic_msg_t<br/>msg_id = payload[0]<br/>payload = Rest"]
  E --> F[cmd_queue xQueueSend]
  F --> G[vCmdDispatcherTask]
  G --> H{msg_register_handler}
  H --> I[cmd_*.c callback]
  I --> J[free payload]

Wichtige Stellen:

Schritt Datei Funktion
Byte-Parser + Timeout 50 ms main/uart.c parse_uart_byte(), uart_read_task()
Queue-Eintrag main/uart.c uart_enqueue_packet()
Dispatcher main/cmd/cmd_handler.c vCmdDispatcherTask(), msg_register_handler()
Registrierung Master main/powerpod.c cmd_*_register() nach init_uart()

Nachrichten-ID: Byte 0 des UART-Payloads = alox_MessageType (siehe main/proto/uart_messages.proto). Bytes 1… = nanopb-kodiertes UartMessage ohne wiederholtes Type-Feld — Handler erhalten nur den protobuf-Teil (msg.payload ab Offset 1).

4.2 Ausgang (Handler → UART)

flowchart LR
  H[Handler baut alox_UartMessage] --> U[uart_cmd_send]
  U --> P[uart_send_uart_message]
  P --> E[pb_encode + Byte0 = type]
  E --> F[uart_send_framed]
  F --> W[uart_write_bytes]

Wichtige Stellen:

Schritt Datei Funktion
Handler-Hilfen main/uart_cmd.c uart_cmd_decode(), uart_cmd_init_response(), uart_cmd_send()
Protobuf + Framing main/uart_proto.c uart_send_uart_message()
XOR-Rahmen main/uart.c uart_send_framed()

4.3 OTA-Sperre (keine parallelen Befehle)

Während einer OTA-Session (ota_uart_is_active() oder ota_espnow_distribution_active()) lehnt der Dispatcher alle UART-Befehle ab außer:

  • OTA_START, OTA_PAYLOAD, OTA_END, OTA_START_ESPNOW, OTA_SLAVE_PROGRESS

Implementierung: ota_session.c, Prüfung in vCmdDispatcherTask vor Handler-Aufruf.

Auf dem Slave verarbeitet espnow_recv_cb während ota_uart_is_active() nur noch OTA-Nachrichten vom joined Master (kein Discover, Stream, LED, …).

Auf dem Master während ESP-NOW-Verteilung nur noch ESPNOW_OTA_STATUS aus dem recv_cb.

4.4 UART-Request-Decode (strikt)

Regel Beispiele
Leerer protobuf-Body (len == 0) erlaubt VERSION, CLIENT_INFO, CACHE_STATUS, BATTERY_STATUS (Defaults)
len > 0 → Decode muss gelingen, which_payload muss passen ACCEL_STREAM, TAP_NOTIFY, RESTART, OTA, …

4.5 Bridge-Muster: UART-Befehl → ESP-NOW

Viele Master-Handler folgen demselben Muster:

  1. uart_cmd_decode() → Request aus UartMessage
  2. Lokale Aktion und/oder client_registry_* aktualisieren
  3. esp_now_comm_send_*() mit MAC aus Registry
  4. uart_cmd_send() mit Response
sequenceDiagram
  participant Host
  participant UART as uart.c
  participant CMD as cmd_accel_deadzone.c
  participant REG as client_registry
  participant EN as esp_now_comm.c
  participant SL as Slave

  Host->>UART: Frame ACCEL_DEADZONE
  UART->>CMD: generic_msg_t
  CMD->>REG: set_accel_deadzone / lookup MAC
  CMD->>EN: esp_now_comm_send_accel_deadzone
  EN->>SL: ESPNOW_SET_ACCEL_DEADZONE
  SL->>SL: bma456 + NVS
  CMD->>Host: uart_cmd_send Response

Beispiel-Implementierung: main/cmd/cmd_accel_deadzone.c
Weitere Bridge-Handler: cmd_accel_stream.c, cmd_tap_notify.c, cmd_led_ring.c, cmd_espnow_find_me.c, cmd_restart.c, cmd_espnow_unicast_test.c, cmd_espnow_echo_ping.c, cmd/cmd_ota.c (OTA + ota_espnow.c).

Nur Master / nur Cache (kein Slave-Roundtrip): cmd_client_info.c, cmd_battery.c, cmd_cache_status.c, cmd_version.c, cmd_set_log_level.c.


5. Datenfluss: UART

5.1 Rahmenformat (Transport)

Unabhängig von Protobuf — reines Bytestream-Framing:

Feld Wert
Start 0xAA
Länge 1 Byte (1252)
Payload length Bytes
Prüfsumme XOR aller Payload-Bytes
Stopp 0xCC

Parameter: main/uart.hUART_NUM_1, 921600 Baud, TX GPIO 2, RX GPIO 3, MAX_PAYLOAD_SIZE 248.

5.2 Nutzlast (Anwendung)

Payload[0]     = MessageType (enum, 1 Byte)
Payload[1…]    = nanopb UartMessage (Felder ab type/payload oneof)

Schema: main/proto/uart_messages.proto
Generiert: main/proto/uart_messages.pb.c/h (make proto_generate_uart)

5.3 Tasks und Prioritäten

Task Stack Priorität Datei
uart_rx 4096 5 uart.c
cmd_dispatch 8192 5 cmd_handler.c

Queue-Größe Master: 64 Einträge (powerpod.c); volle Queue → Warnung, Payload wird freigegeben.


6. Datenfluss: ESP-NOW

6.1 Stack-Initialisierung

esp_now_comm_init() (esp_now_comm.c) → esp_now_core (WiFi/Radio/Send) + Rolle:

  1. client_registry_init() (Master)
  2. esp_now_init(), recv_cb leitet an esp_now_master_on_recv / esp_now_slave_on_recv
  3. Master (esp_now_master.c): espnow_disc, espnow_mon
  4. Slave (esp_now_slave.c): espnow_stx, espnow_hb, espnow_accel, ota_slave_work

Codec: Rohes Paket = ein nanopb EspNowMessagekein zusätzliches Framing (main/esp_now_proto.c).

6.2 Discovery und Join (Slave)

sequenceDiagram
  participant M as Master espnow_disc
  participant S as Slave recv_cb
  participant TX as slave_tx_task

  loop alle 500 ms
    M->>M: ESPNOW_DISCOVER → FF:FF:…:FF
  end
  S->>S: handle_discover (network match)
  S->>S: s_slave_joined, s_master_mac
  Note over S,TX: Kein Send aus recv_cb!
  S->>TX: SLAVE_TX_SLAVE_INFO
  TX->>M: ESPNOW_SLAVE_INFO
  S->>TX: SLAVE_TX_BATTERY
  TX->>M: ESPNOW_BATTERY_REPORT
  loop alle 1 s
    S->>M: ESPNOW_HEARTBEAT
  end

Registry-Schlüssel: immer recv_info.src_addr (WiFi-MAC des Senders), nicht optionales mac-Feld in der Protobuf-Nachricht.

Slave-ID: mac[5] (letztes Oktett) — kann kollidieren; eindeutig ist die volle MAC in der Registry.

Master-Verlust: Slave setzt Join zurück, wenn 5 s kein Discover vom gleichen Master (SLAVE_MASTER_LOST_MS).

Join-Policy: Master→Slave-Steuerung (Stream, Tap, LED, OTA, UNICAST_TEST, SET_ACCEL_DEADZONE, …) nur bei s_slave_joined und src_addr == s_master_mac. Während ota_uart_is_active() auf dem Slave verarbeitet der recv_cb nur OTA vom joined Master.

Slave OTA: Payload/End/Status-Sends laufen über ota_slave_work_task (Queue), nicht im ESP-NOW-recv_cb.

6.3 Master → Slave (Unicast)

Alle Master-Sends laufen über send_message() / send_message_ex():

  1. esp_now_proto_encode()
  2. ensure_peer(dest_mac)
  3. esp_now_send()

Öffentliche API: main/esp_now_comm.h (esp_now_comm_send_*).

Empfang Slave: espnow_recv_cb() → Switch auf which_payload → Handler (handle_slave_*).

6.4 Slave → Master (Events)

Nachricht Task / Trigger Master-Ziel
ESPNOW_ACCEL_SAMPLE slave_accel_stream_task 16 ms client_registry_update_accel()
ESPNOW_TAP_EVENT BMA456-IRQ → on_bma456_tap client_registry_update_tap()
ESPNOW_BATTERY_REPORT Heartbeat + nach Join client_registry_update_battery()
ESPNOW_SLAVE_INFO / HEARTBEAT slave_tx_task / Heartbeat-Task client_registry_heartbeat()
ESPNOW_OTA_STATUS ota_espnow_slave_* ota_espnow_master_on_status()

Master recv: espnow_recv_cb() — Presence, Accel, Tap, Battery, OTA-Status.

6.5 OTA über ESP-NOW

Nach erfolgreichem UART-OTA auf dem Master (oder OTA_START_ESPNOW): ota_espnow.c liest gestagte Partition, sendet OTA_START / PAYLOAD (≤200 B, send_message_ex mit ACK-Semaphore) / OTA_END an alle available Slaves.

Gemeinsamer Flash-Puffer: ota_uart.c (4 KiB Blockgröße).


7. Client Registry (Master-Datenhub)

Die Registry ist die zentrale Brücke zwischen ESP-NOW-Echtzeitdaten und UART-Abfragen.

flowchart TB
  EN_RX[espnow_recv_cb] --> REG[client_registry]
  CMD_R[cmd_client_info / battery / cache_status] --> REG
  CMD_W[cmd_* write paths] --> REG
  REG --> CMD_R
  EN_TX[esp_now_comm_send_*] --> MAC[MAC aus Registry]
Daten Aktualisiert durch Gelesen durch UART
Presence, Version HEARTBEAT / SLAVE_INFO CLIENT_INFO
Accel-Stream-Samples ESPNOW_ACCEL_SAMPLE CACHE_STATUS
Tap-Events (16 ms Cache) ESPNOW_TAP_EVENT CACHE_STATUS
Batterie ESPNOW_BATTERY_REPORT + Master-ADC BATTERY_STATUS
Konfig-Flags Handler + ESP-NOW CLIENT_INFO, diverse GET

Datei: main/client_registry.c, main/client_registry.h (max. 16 Clients, Timeout 3 s ohne Heartbeat).


8. FreeRTOS-Tasks (Übersicht)

Task Rolle Intervall / Trigger
uart_rx Master UART read 20 ms timeout
cmd_dispatch Master Queue blocking
espnow_disc Master 500 ms Discover
espnow_mon Master 1 s Timeout + Master-Battery
espnow_stx Slave Queue: SLAVE_INFO, Battery nach Join
espnow_hb Slave 1 s Heartbeat + 30 s Battery
espnow_accel Slave 16 ms Accel wenn Stream an
bma456_poll Beide 10 Hz (bosch456.c)
vTaskLedRing Beide LED-Ring-Befehle

app_main endet in while(1) vTaskDelay(portMAX_DELAY) — alle Arbeit läuft in Tasks.


9. Implementierungskarte (wichtige Dateien)

Bereich Pfad Verantwortung
Einstieg / Init main/powerpod.c Boot-Reihenfolge, Master-Handler-Register
UART Transport main/uart.c, main/uart.h Framing RX/TX
UART Protobuf TX main/uart_proto.c uart_send_uart_message
UART Handler-Boilerplate main/uart_cmd.c, main/uart_cmd.h Decode, Register, Send
Command Dispatch main/cmd/cmd_handler.c Queue, Dispatcher
UART Commands main/cmd/cmd_*.c Pro Befehl ein Modul
ESP-NOW esp_now_comm.c Init/Router; esp_now_core.c Send/Peer; esp_now_master.c / esp_now_slave.c Rollenlogik
ESP-NOW Codec main/esp_now_proto.c nanopb encode/decode
Registry main/client_registry.c Slave-Tabelle + Caches
BMA456 main/bosch456.c Sensor, Tap, Deadzone-Filter
LED main/led_ring.c 95 LEDs, Digit-Maps
OTA UART main/ota_uart.c, cmd/cmd_ota.c A/B-Partition Upload
OTA ESP-NOW main/ota_espnow.c Verteilung an Slaves
Einstellungen NVS main/pod_settings.c Accel-Deadzone persistent
Board main/board_input.c Taster, LiPo-ADC
Protobuf Schemas main/proto/*.proto Vertrag UART / ESP-NOW
Vendor BMA456 components/bma456/ Nur bma4.c + bma456h.c gelinkt

10. Protokoll-Verträge (Kurzreferenz)

  • UART: main/proto/uart_messages.proto — Host ↔ Master
  • ESP-NOW: main/proto/esp_now_messages.proto — Master ↔ Slave

Regenerierung: make proto_generate (siehe DOCUMENTATION.md).


11. Design-Entscheidungen

  1. Eine Queue für UART-Commands — einziger Eingang vom Host; während OTA nur OTA-Befehle.
  2. Kein ESP-NOW-Send im recv_cb — Slave antwortet auf Discover über slave_tx_task (Deadlock-/Stack-Risiko vermeiden).
  3. Registry-MAC = ESP-NOW-Quelladresse — Protobuf-MAC ist optional/informativ.
  4. Gleiches Binary — Konfiguration nur Hardware (DIP + Expander); reduziert Release-Komplexität.
  5. Nanopb statt vollem protobuf-c — passend für ESP-RAM/Flash.
  6. Master aggregiert Sensor-Daten — Slaves streamen; Host pollt Cache per UART (CACHE_STATUS), kein Durchreichen jedes Accel-Samples über UART.

Weitere Details (Befehlsliste, GPIO, Build, BMA456): DOCUMENTATION.md.