Document UART command flow, ESP-NOW data paths, and module map for developers without covering goTool; link from main/README. Co-authored-by: Cursor <cursoragent@cursor.com>
15 KiB
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 | Lokaler Zustand in esp_now_comm.c |
Einstieg: main/powerpod.c → app_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 1–8
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 5–8 (nibble reversed) | 1–8 → 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 Interner Befehlspost (ohne UART)
msg_post(id, data, len) in cmd_handler.c legt dieselbe generic_msg_t-Struktur in cmd_queue — für zukünftige Firmware-interne Quellen (z. B. ESP-NOW → PC-Pfad). Aktuell ist UART der einzige Produktiv-Eingang.
4.4 Bridge-Muster: UART-Befehl → ESP-NOW
Viele Master-Handler folgen demselben Muster:
uart_cmd_decode()→ Request ausUartMessage- Lokale Aktion und/oder
client_registry_*aktualisieren esp_now_comm_send_*()mit MAC aus Registryuart_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/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.
5. Datenfluss: UART
5.1 Rahmenformat (Transport)
Unabhängig von Protobuf — reines Bytestream-Framing:
| Feld | Wert |
|---|---|
| Start | 0xAA |
| Länge | 1 Byte (1–252) |
| Payload | length Bytes |
| Prüfsumme | XOR aller Payload-Bytes |
| Stopp | 0xCC |
Parameter: main/uart.h — UART_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() (main/esp_now_comm.c):
client_registry_init()- WiFi STA, Kanal =
network(1–13) esp_now_init(),esp_now_register_recv_cb(espnow_recv_cb)- Master: Broadcast-Peer, Tasks
espnow_disc(500 ms),espnow_mon(1 s) - Slave:
slave_tx_task,slave_heartbeat_task,slave_accel_stream_task(16 ms)
Codec: Rohes Paket = ein nanopb EspNowMessage — kein 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).
6.3 Master → Slave (Unicast)
Alle Master-Sends laufen über send_message() / send_message_ex():
esp_now_proto_encode()ensure_peer(dest_mac)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 Kern | main/esp_now_comm.c, .h |
WiFi, Tasks, recv/send, Slave-Join |
| 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
- Eine Queue für alle Commands — einheitliches Modell für UART und künftige interne Quellen.
- Kein ESP-NOW-Send im
recv_cb— Slave antwortet auf Discover überslave_tx_task(Deadlock-/Stack-Risiko vermeiden). - Registry-MAC = ESP-NOW-Quelladresse — Protobuf-MAC ist optional/informativ.
- Gleiches Binary — Konfiguration nur Hardware (DIP + Expander); reduziert Release-Komplexität.
- Nanopb statt vollem protobuf-c — passend für ESP-RAM/Flash.
- 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.