powerpods/docs/adding-a-feature.md
simon 16c521f71c Move UART command handlers into main/cmd/ for clearer layout.
Add cmd/ to CMake include paths and update documentation paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-19 22:49:11 +02:00

352 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Feature hinzufügen — von UART bis ESP-NOW
Dieser Guide beschreibt die **komplette Kette** am Beispiel **Find me** (Commit `efd6260`): Host sendet ein UART-Kommando an den Master, der die Aktion lokal ausführt oder per ESP-NOW an einen Slave weiterleitet.
## Architektur (Überblick)
```mermaid
sequenceDiagram
participant Host as Host (goTool / Dashboard)
participant Master as Master (UART + ESP-NOW)
participant Slave as Slave (ESP-NOW)
Host->>Master: UART-Frame [cmd_id + UartMessage protobuf]
Master->>Master: cmd_* Handler (Queue)
alt client_id == 0
Master->>Master: z.B. led_ring_find_me()
else client_id > 0
Master->>Slave: ESP-NOW EspNowMessage (unicast)
Slave->>Slave: esp_now_recv_cb → Handler
end
Master->>Host: UART-Response (UartMessage)
```
| Schicht | Dateien | Rolle |
|---------|---------|--------|
| Schema UART | `main/proto/uart_messages.proto` | `MessageType`, Request/Response für Host ↔ Master |
| Schema ESP-NOW | `main/proto/esp_now_messages.proto` | Master ↔ Slave (ohne UART) |
| Transport UART | `main/uart.c`, `goTool/uart/` | Rahmen `0xAA … 0xCC`, Byte 0 = Command-ID |
| Dispatch | `main/cmd/cmd_handler.c`, `main/uart_cmd.c` | Queue + `uart_cmd_register()` |
| Master-Logik | `main/cmd/cmd_*.c` | Decode, Registry, ESP-NOW senden |
| ESP-NOW | `main/esp_now_comm.c` | Encode/Decode, Send, Slave-`recv_cb` |
| Geräte-Funktion | z.B. `main/led_ring.c` | Wiederverwendbare Aktion (Master + Slave) |
| Host | `goTool/cmd_*.go`, `api_serve.go`, `webui/` | CLI, HTTP, UI |
**Wichtig:** UART-Befehle laufen auf dem **Master** (nur wenn `app_config.master`). Slaves haben keinen UART-Command-Handler; sie reagieren auf **ESP-NOW**. Die eigentliche Wirkung (LED, Sensor, …) liegt in gemeinsamen Modulen (`led_ring`, `bosch456`, …).
---
## Wann braucht man was?
| Ziel | UART (`uart_messages.proto`) | ESP-NOW (`esp_now_messages.proto`) |
|------|------------------------------|-------------------------------------|
| Nur Master (angeschlossener Pod) | Ja | Nein |
| Einen registrierten Slave ansteuern | Ja (Master leitet weiter) | Ja |
| Slave → Master Rückmeldung | Optional (UART-Status) | Ja, wenn Slave antworten soll |
**Find me:** UART `FIND_ME` mit `client_id`; `0` = Master-Ring, `>0` = Unicast `ESPNOW_FIND_ME` an die Slave-MAC aus der Registry.
Referenz für **nur ESP-NOW-Weiterleitung ohne lokale Wirkung:** `main/cmd/cmd_espnow_unicast_test.c`
Referenz für **Master lokal + Slave + `client_id`:** `main/cmd/cmd_espnow_find_me.c`, `main/cmd/cmd_accel_deadzone.c`
---
## Schritt 1 — Protobuf (UART)
Datei: `main/proto/uart_messages.proto`
1. **Neue ID** in `enum MessageType` (freie Nummer wählen, z.B. `22`):
```protobuf
FIND_ME = 22;
```
2. **Request/Response** definieren und im `UartMessage`-`oneof` eintragen (neue Feldnummern, nicht wiederverwenden):
```protobuf
message EspNowFindMeRequest {
uint32 client_id = 1;
}
message EspNowFindMeResponse {
bool success = 1;
uint32 client_id = 2;
}
// in message UartMessage { oneof payload { …
EspNowFindMeRequest espnow_find_me_request = 19;
EspNowFindMeResponse espnow_find_me_response = 20;
}
```
3. **Generieren:**
```bash
make proto_generate_uart
make gotool-proto
```
Erzeugt u.a. `main/proto/uart_messages.pb.h`, `.pb.c` und `goTool/pb/uart_messages.pb.go`.
---
## Schritt 2 — Protobuf (ESP-NOW), falls Slaves betroffen
Datei: `main/proto/esp_now_messages.proto`
1. Neuer Wert in `enum EspNowMessageType`:
```protobuf
ESPNOW_FIND_ME = 10;
```
2. Payload-Nachricht + Eintrag im `EspNowMessage`-`oneof`:
```protobuf
message EspNowFindMe {
uint32 client_id = 1; // 0 = alle; sonst nur passende slave_id
}
// EspNowMessage.oneof:
EspNowFindMe find_me = 11;
```
3. **Generieren:**
```bash
make proto_generate_espnow
# oder: make proto_generate
```
**Hinweis:** Master und alle Slaves müssen dieselbe ESP-NOW-Proto-Version flashen, sobald sich `esp_now_messages.proto` ändert.
---
## Schritt 3 — Geräte-Logik (gemeinsam)
Funktion, die auf **Master und Slave** gleich wirken soll, gehört **nicht** in den UART-Handler, sondern in ein Modul (z.B. `led_ring`).
Find me:
- `led_ring_find_me()` in `led_ring.c` — sequenz `LED_CMD_FIND_ME` (3× rot/grün/blau, volle Helligkeit)
- Optional separater UART-Pfad nur für Ring-Steuerung: `LED_RING` mode `4` in `main/cmd/cmd_led_ring.c` (ohne ESP-NOW)
---
## Schritt 4 — UART-Command-Handler (nur Master)
Neue Dateien: `main/cmd/cmd_espnow_find_me.c`, `main/cmd/cmd_espnow_find_me.h`
Muster (gekürzt):
```c
static void reply(bool success, uint32_t client_id) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_FIND_ME,
alox_UartMessage_espnow_find_me_response_tag);
response.payload.espnow_find_me_response.success = success;
response.payload.espnow_find_me_response.client_id = client_id;
uart_cmd_send(&response, TAG);
}
static void handle_find_me(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { }
const alox_EspNowFindMeRequest *req = UART_CMD_REQ(
&uart_msg, alox_UartMessage_espnow_find_me_request_tag,
espnow_find_me_request);
if (req == NULL) { }
if (req->client_id == 0) {
led_ring_find_me(); // lokal
reply(true, 0);
return;
}
const client_info_t *client = client_registry_find_by_id(req->client_id);
if (client == NULL) { reply(false, req->client_id); return; }
esp_err_t err = esp_now_comm_send_find_me(client->mac, req->client_id);
reply(err == ESP_OK, req->client_id);
}
void cmd_espnow_find_me_register(void) {
uart_cmd_register(alox_MessageType_FIND_ME, handle_find_me);
}
```
Hilfs-APIs (`uart_cmd.h`):
| API | Zweck |
|-----|--------|
| `uart_cmd_decode()` | Protobuf-Body (ohne führendes Command-Byte) dekodieren |
| `UART_CMD_REQ()` | Sicheres Lesen des `oneof`-Zweigs |
| `uart_cmd_init_response()` | Response-Typ + `which_payload` setzen |
| `uart_cmd_send()` | Antwort UART raus |
**Registrierung** in `main/powerpod.c` (nur im `if (app_config.master)`-Block):
```c
cmd_espnow_find_me_register();
```
**Build:** `main/CMakeLists.txt``"cmd/cmd_espnow_find_me.c"`
**Logging-Namen:** `main/cmd/cmd_handler.c``case alox_MessageType_FIND_ME: return "FIND_ME";`
### Ablauf UART intern
1. `uart_read_task` liest Frame, legt `msg_id = payload[0]` und Rest in Queue.
2. `vCmdDispatcherTask` ruft den registrierten Handler mit `data` = Bytes **nach** der ID auf.
3. Handler antwortet synchron über `uart_cmd_send` (die LED-Animation läuft danach im `led_task` weiter).
---
## Schritt 5 — ESP-NOW senden (Master)
In `main/esp_now_comm.c`:
1. **Statische Sendefunktion** (wie `send_unicast_test`):
```c
static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_FIND_ME;
msg.which_payload = alox_EspNowMessage_find_me_tag;
msg.payload.find_me.client_id = client_id;
return send_message(dest_mac, &msg);
}
```
2. **Öffentliche API** in `esp_now_comm.h` / `.c`:
```c
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id);
```
Prüfungen: `mac != NULL`, `s_config.master`, Peer per `ensure_peer()` (passiert in `send_message`).
---
## Schritt 6 — ESP-NOW empfangen (Slave)
Im Slave-Zweig von `espnow_recv_cb` (`!s_config.master`):
1. Handler-Funktion, typisch mit **Master-MAC-Check** und **`client_id`-Filter** (wie Deadzone):
```c
static void handle_slave_find_me(const uint8_t *master_mac,
const alox_EspNowFindMe *req) {
uint32_t my_id = s_own_mac[5];
if (req->client_id != 0 && req->client_id != my_id) return;
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) return;
led_ring_find_me();
}
```
2. Im `switch (msg.which_payload)`:
```c
case alox_EspNowMessage_find_me_tag:
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) break;
handle_slave_find_me(info->src_addr, &msg.payload.find_me);
break;
```
Slaves führen `led_ring_init()` in `powerpod.c` ebenfalls aus — Ring-Hardware ist auf beiden Rollen vorhanden.
---
## Schritt 7 — Host (goTool)
### CLI
`goTool/cmd_find_me.go`:
- `UartMessage` mit `Type: MessageType_FIND_ME` und `EspnowFindMeRequest`
- Payload = `[byte(MessageType_FIND_ME)] + proto.Marshal(msg)`
- `exchangePayload` → Response dekodieren
`main.go`: Command `find-me` registrieren, `-port` Pflicht.
```bash
go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
```
### Dashboard / HTTP
1. `managedSerial.FindMe(clientID)` in `client_api.go` (UART-Serial mutex wie andere Befehle).
2. `POST /api/find-me` mit `{"client_id": 0}` in `api_serve.go`.
3. Button in `goTool/webui/index.html` (Master + pro Slave-Zeile).
### Serial-Format (Host)
Entspricht `main/README.md`:
| Byte | Inhalt |
|------|--------|
| Frame | `0xAA`, Länge, Payload, XOR-Checksum, `0xCC` |
| Payload[0] | `MessageType` (z.B. `22` = FIND_ME) |
| Payload[1…] | Nanopb-`UartMessage` |
---
## Schritt 8 — Dokumentation & Test
1. **`main/README.md`:** Zeile in UART-Tabelle, ggf. ESP-NOW-Tabelle, eigener Abschnitt mit Beispiel-`go run`.
2. **`goTool/README.md`:** Command-Tabelle + HTTP-API.
3. **Build:**
```bash
make proto_generate
cd goTool && go build .
idf.py build
```
4. **Manuell testen:**
| Test | Erwartung |
|------|-----------|
| `find-me` (client 0) | Master-Ring blinkt RGB |
| `find-me -client <id>` | Nur dieser Slave blinkt; Log Master: `unicast FIND_ME` |
| Slave offline / unbekannte ID | `success=false` in UART-Response |
| Dashboard-Buttons | Gleiches Verhalten wie CLI |
---
## Checkliste (Kurz)
- [ ] `uart_messages.proto`: `MessageType`, Request, Response, `oneof`
- [ ] `esp_now_messages.proto` (falls Slave): `EspNowMessageType`, Message, `oneof`
- [ ] `make proto_generate` + `make gotool-proto`
- [ ] Geräte-Modul (z.B. `led_ring_*`)
- [ ] `cmd_*.c` + `uart_cmd_register`
- [ ] `esp_now_comm`: `send_*` + Slave-`recv` case
- [ ] `CMakeLists.txt`, `powerpod.c`, `cmd/cmd_handler.c` Name
- [ ] goTool: CLI, `client_api`, optional `api_serve` + WebUI
- [ ] README aktualisieren
- [ ] Master + Slave flashen bei ESP-NOW-Proto-Änderung
---
## Referenz-Dateien (Find me)
| Bereich | Datei |
|---------|--------|
| UART Proto | `main/proto/uart_messages.proto` |
| ESP-NOW Proto | `main/proto/esp_now_messages.proto` |
| UART Handler | `main/cmd/cmd_espnow_find_me.c` |
| ESP-NOW | `main/esp_now_comm.c`, `main/esp_now_comm.h` |
| LED | `main/led_ring.c`, `main/led_ring.h` |
| Host CLI | `goTool/cmd_find_me.go` |
| HTTP/UI | `goTool/api_serve.go`, `goTool/webui/index.html` |
Ähnliche Features zum Abgucken:
- **Nur Master, kein ESP-NOW:** `main/cmd/cmd_version.c`, `main/cmd/cmd_led_ring.c`
- **Nur Slave per ESP-NOW (Master leitet nur durch):** `main/cmd/cmd_espnow_unicast_test.c`
- **Master + alle Slaves / Filter:** `main/cmd/cmd_accel_deadzone.c`
- **Großer ESP-NOW-Fluss mit Status:** `ota_espnow.c`, `main/cmd/cmd_ota.c`