powerpods/docs/adding-a-feature.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

11 KiB
Raw Permalink Blame History

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)

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):
FIND_ME = 22;
  1. Request/Response definieren und im UartMessage-oneof eintragen (neue Feldnummern, nicht wiederverwenden):
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;
}
  1. Generieren:
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:
ESPNOW_FIND_ME = 10;
  1. Payload-Nachricht + Eintrag im EspNowMessage-oneof:
message EspNowFindMe {
  uint32 client_id = 1;  // 0 = alle; sonst nur passende slave_id
}

// EspNowMessage.oneof:
  EspNowFindMe find_me = 11;
  1. Generieren:
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):

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):

cmd_espnow_find_me_register();

Build: main/CMakeLists.txt"cmd/cmd_espnow_find_me.c"
Logging-Namen: main/cmd/cmd_handler.ccase 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):
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);
}
  1. Öffentliche API in esp_now_comm.h / .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):
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();
}
  1. Im switch (msg.which_payload):
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.

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:
make proto_generate
cd goTool && go build .
idf.py build
  1. 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, main/cmd/cmd_set_log_level.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