417 lines
16 KiB
Markdown
417 lines
16 KiB
Markdown
# 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.
|
||
|
||
```mermaid
|
||
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.c` → `app_main()`.
|
||
|
||
---
|
||
|
||
## 2. Boot und Konfiguration
|
||
|
||
### 2.1 Ablauf
|
||
|
||
```mermaid
|
||
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)
|
||
|
||
```mermaid
|
||
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)
|
||
|
||
```mermaid
|
||
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
|
||
|
||
```mermaid
|
||
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`.
|
||
|
||
---
|
||
|
||
## 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()` (`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 `EspNowMessage` — **kein** zusätzliches Framing (`main/esp_now_proto.c`).
|
||
|
||
### 6.2 Discovery und Join (Slave)
|
||
|
||
```mermaid
|
||
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.
|
||
|
||
```mermaid
|
||
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`](DOCUMENTATION.md).
|