Compare commits

...
13 Commits
Author SHA1 Message Date
skrueckenandCursor f006f8b912 Add software LiPo UV protection with energy-save boot.
Detect under-voltage on GPIO1/11, persist UV state in NVS, enter a minimal
power mode without ESP-NOW, and recover after charge; expose LED mode 6
battery-low for host testing via goTool.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 18:22:52 +02:00
skruecken be18b758ed Added LIPO 2 Reading 2026-06-23 17:54:18 +02:00
skrueckenandCursor 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
skruecken ac223ada72 Added ECHO cmd and fixed some timing issues 2026-06-06 17:44:40 +02:00
skruecken 35ce1476d8 Missed for Previous Commit 2026-06-06 17:17:42 +02:00
skruecken f89ea3cbe3 Stabelized OTA Update with Retries 2026-06-06 17:15:55 +02:00
skruecken 99956e3362 Update InputClientSample struct to make TapKind optional and improve WebSocket API documentation. Adjust README to reflect changes in input push stream format and clarify connection flow. Enhance API_WEBSOCKET.md with detailed command and push message descriptions. 2026-06-06 12:26:31 +02:00
skrueckenandCursor ab1844ac32 Fix UART hang after master restart from the dashboard.
ReadFrame now returns on serial timeout instead of looping forever, and
serve closes and reopens the port after a master reboot so polling and
API commands can resume.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 16:45:57 +02:00
skrueckenandCursor e4ce18edd8 Close UART gracefully on SIGINT/SIGTERM in goTool.
Go exits on Ctrl+C without running defers, leaving the serial port locked; register shutdown hooks for serve and CLI commands.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 16:40:22 +02:00
skrueckenandCursor 0eea27a876 Fix web OTA upload and isolate OTA sessions across firmware and goTool.
Split ESP-NOW into core/master/slave modules, block non-OTA UART traffic during updates, and hold the host serial port exclusively so dashboard polling cannot interleave with firmware uploads.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 16:35:18 +02:00
skrueckenandCursor 0cbc4d0644 Add ESP firmware architecture and reference documentation.
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>
2026-05-31 15:55:32 +02:00
skrueckenandCursor 35b39fce46 Add configurable BMA456 tap detection profile in bosch456.h.
Centralize multitap tuning so sensitivity and timing can be adjusted without changing driver init logic.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 14:03:46 +02:00
skrueckenandCursor 41a66d4417 Unify external WebSocket push into a single input stream.
Replace separate accel/tap commands and messages with set_input_stream and input pushes that combine accel and tap per client, including pre_fetch timing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-31 13:47:39 +02:00
77 changed files with 5721 additions and 2210 deletions
+416
View File
@@ -0,0 +1,416 @@
# 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 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)
```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`, `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.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).
+564
View File
@@ -0,0 +1,564 @@
# Powerpod ESP-Firmware — Dokumentation
Vollständige Referenz für das ESP-IDF-Projekt unter `main/` und `components/`. Diese Doku beschreibt **nur die Firmware** — kein goTool, keine Host-Implementierung.
- **Architektur & Datenflüsse:** [`ARCHITECTURE.md`](ARCHITECTURE.md)
- **Neues Feature end-to-end:** [`adding-a-feature.md`](adding-a-feature.md)
---
## Inhaltsverzeichnis
1. [Projektziel](#1-projektziel)
2. [Hardware und Pins](#2-hardware-und-pins)
3. [Build und Flash](#3-build-und-flash)
4. [Boot-Konfiguration](#4-boot-konfiguration)
5. [UART-Protokoll](#5-uart-protokoll)
6. [ESP-NOW-Protokoll](#6-esp-now-protokoll)
7. [Befehlsreferenz (UART)](#7-befehlsreferenz-uart)
8. [OTA](#8-ota)
9. [BMA456 Beschleunigungssensor](#9-bma456-beschleunigungssensor)
10. [LED-Ring](#10-led-ring)
11. [Board-Input und Batterie](#11-board-input-und-batterie)
12. [Persistenz (NVS)](#12-persistenz-nvs)
13. [Protobuf und Code-Generierung](#13-protobuf-und-code-generierung)
14. [Modulreferenz](#14-modulreferenz)
15. [Logging-Tags](#15-logging-tags)
---
## 1. Projektziel
**Powerpod** ist Firmware für ESP32-S3-Knoten in einem verteilten System:
- Ein **Master** spricht per **UART** mit einem externen Host und verwaltet bis zu **16 Slaves** über **ESP-NOW**.
- **Slaves** haben keinen UART-Befehlspfad; sie joinen über periodisches **Discover**, senden Heartbeats und liefern Sensor-/Statusdaten.
- Master und Slave nutzen **identisches Firmware-Image**; Rolle und Funknetz werden beim Boot erkannt.
Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves und **cached** deren Telemetrie (`client_registry`) für schnelle UART-Abfragen.
---
## 2. Hardware und Pins
> Pinbelegung ist in `powerpod.h` als vorläufig markiert — mit Schaltplan abgleichen.
| Signal | GPIO | Datei / Modul |
|--------|------|----------------|
| DIP Master/Slave | 4 | `powerpod.h` — Low = Master |
| I2C SCL / SDA | 5 / 6 | IO-Expander `0x20`, BMA456 `0x18` |
| UART1 TX / RX | **2 / 3** | `uart.h` (Adapter: ESP-TX → Host-RX) |
| LED-Ring (WS2812) | 7 | `led_ring.c` |
| BMA456 Interrupt | 10 | `bosch456.c` |
| Taster | 12 | `board_input.c` |
| LiPo ADC 1 | 1 | `board_input.c` |
| LiPo ADC 2 | 11 | `board_input.c` |
**UART (Host-Protokoll):** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control.
**ESP-IDF-Log (Debug):** `esp_log_*` geht auf **UART0** (Standard-Konsole, typisch USB am Dev-Board, **115200** Baud, `CONFIG_ESP_CONSOLE_UART_NUM=0`). Das ist **getrennt** vom Host-UART1 — goTool liest keine `esp_log`-Ausgabe. Ohne angeschlossenes Debug-Kabel werden aktivierte Logs trotzdem formatiert und an UART0 gesendet (CPU-Overhead bleibt); nur `ESP_LOG_NONE` / Level-Filter vermeiden die Arbeit.
**I2C:** 100 kHz, interne Pull-ups, gemeinsamer Bus für Expander und BMA456H.
---
## 3. Build und Flash
**Ziel-Chip:** ESP32-S3 (ESP-IDF).
```bash
source ~/esp/esp-idf/export.sh # oder export.fish
cd /pfad/zu/powerpod
idf.py build
idf.py -p /dev/ttyUSB0 flash monitor
```
- **Git-Hash** wird beim Build in `POWERPOD_GIT_HASH` eingebettet (`main/CMakeLists.txt`).
- **Firmware-Version:** `POWERPOD_FW_VERSION` (Default `1` in `esp_now_comm.c` / `cmd_version.c`).
**Komponenten:**
| Pfad | Inhalt |
|------|--------|
| `main/` | Anwendungslogik |
| `components/bma456/` | Bosch BMA456H-Treiber (Vendor) |
| `libs/nanopb/` | Protobuf-Codec für Embedded |
---
## 4. Boot-Konfiguration
### 4.1 Initialisierungsreihenfolge (`powerpod.c`)
1. `pod_settings_init()` — NVS
2. `board_input_init_adc_only()` — LiPo-ADC
3. Bei `POWERPOD_BATTERY_UV_ENABLE`: `battery_uv_evaluate_boot()` → ggf. Energiesparmodus (nur LED + ADC, **kein** weiterer Init)
4. DIP → `app_config.master`
5. I2C + IO-Expander → `app_config.network` (18)
6. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
7. `pod_settings_apply_accel_deadzone()` wenn Sensor da
8. OTA-Partition → `app_config.running_partition`
9. `board_input_start_lipo_monitor()` + `board_input_init_button()`
10. `esp_now_comm_init(&app_config)`**normaler Pfad** (Master + Slave)
11. `led_ring_init()`
12. **Nur Master:** `cmd_queue``init_cmdHandler``init_uart` → alle `cmd_*_register()`
### 4.2 Master vs. Slave
| Funktion | Master | Slave |
|----------|--------|-------|
| UART Command-Handler | Ja | Nein |
| ESP-NOW Discover senden | Ja (Broadcast) | Nein |
| ESP-NOW auf Discover reagieren | Nein | Ja |
| `client_registry` für Fremdknoten | Ja | Nein (nur eigener Join-State) |
---
## 5. UART-Protokoll
*(Transport + Anwendung — Datenfluss-Diagramme in [`ARCHITECTURE.md`](ARCHITECTURE.md) §45.)*
### 5.1 Rahmen
| Byte | Inhalt |
|------|--------|
| 0 | `0xAA` Start |
| 1 | Länge N (1252) |
| 2…N+1 | Payload |
| N+2 | XOR-Checksum über Payload |
| N+3 | `0xCC` Stopp |
Implementierung: `uart.c``parse_uart_byte()`, `uart_send_framed()`.
### 5.2 Anwendungs-Payload
| Offset | Bedeutung |
|--------|-----------|
| 0 | `MessageType` (siehe Enum in `uart_messages.proto`) |
| 1… | nanopb `UartMessage` (ohne separates type-Feld im protobuf-Teil) |
**Antworten** nutzen dieselbe Struktur: Byte 0 = Response-Typ, Rest = kodierte `UartMessage`.
### 5.3 Handler-Pipeline
1. `uart_read_task` parst Frames → `uart_enqueue_packet`
2. `generic_msg_t``cmd_queue`
3. `vCmdDispatcherTask` → registrierter `msg_callback_t`
4. Handler: `uart_cmd_decode(data, len, &msg)``data` ist **nur** protobuf-Teil; bei `len > 0` strikt (Decode/`which_payload`-Fehler → Fehlerantwort)
5. `ota_session_uart_cmd_allowed()` — während OTA nur OTA-Befehle
6. Antwort: `uart_cmd_init_response()` + Felder setzen + `uart_cmd_send()`
Hilfsmakro für Request-Felder:
```c
UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadzone_request)
```
---
## 6. ESP-NOW-Protokoll
*(Join, Tasks, Registry — [`ARCHITECTURE.md`](ARCHITECTURE.md) §6.)*
### 6.1 Grundlagen
- WiFi **STA**, keine AP-Verbindung.
- **Kanal** = `app_config.network` (113).
- Payload = ein `EspNowMessage` (nanopb), max. `ESP_NOW_MAX_DATA_LEN`.
- Schema: `main/proto/esp_now_messages.proto`.
### 6.2 Nachrichtentypen
| Type | Richtung | Payload | Zweck |
|------|----------|---------|--------|
| `ESPNOW_DISCOVER` | M→Broadcast | `discover.network` | Slaves finden Master |
| `ESPNOW_SLAVE_INFO` | S→M | `slave_info` | Erste Registrierung |
| `ESPNOW_HEARTBEAT` | S→M | `heartbeat` | Keepalive 1 s |
| `ESPNOW_SET_ACCEL_DEADZONE` | M→S | `accel_deadzone` | Deadzone LSB |
| `ESPNOW_SET_ACCEL_STREAM` | M→S | `accel_stream` | Stream ~16 ms |
| `ESPNOW_ACCEL_SAMPLE` | S→M | `accel_sample` | x/y/z Roh-LSB |
| `ESPNOW_SET_TAP_NOTIFY` | M→S | `tap_notify` | Tap-Arten filtern |
| `ESPNOW_TAP_EVENT` | S→M | `tap_event` | kind 1/2/3 |
| `ESPNOW_BATTERY_QUERY` | M→S | `battery_query` | On-demand |
| `ESPNOW_BATTERY_REPORT` | S→M | `battery_report` | mV LiPo 1/2 |
| `ESPNOW_LED_RING` | M→S | `led_ring` | Wie UART LED_RING |
| `ESPNOW_FIND_ME` | M→S | `find_me` | LED-Locate |
| `ESPNOW_RESTART` | M→S | `restart` | Reboot Slave |
| `ESPNOW_UNICAST_TEST` | M→S | `unicast_test` | Link-Test |
| `ESPNOW_ECHO_PING` | M→S | `echo_ping` | Latenztest (Host-Timestamp + `master_time_us`) |
| `ESPNOW_ECHO_PONG` | S→M | `echo_pong` | Echo unverändert zurück zum Master |
| `ESPNOW_OTA_*` | M↔S | `ota_*` | Firmware-Verteilung |
### 6.3 Zeitkonstanten (`esp_now_comm.c`)
| Konstante | Wert |
|-----------|------|
| Discover-Intervall | 500 ms |
| Heartbeat | 1000 ms |
| Client-Timeout (Master) | 3 × Heartbeat = 3 s → `available=false` |
| Master-Verlust (Slave) | 5 s ohne Discover |
| Accel-Stream | 16 ms |
| Batterie-Report | 30 s (+ einmal 150 ms nach Join) |
| Echo-Ping Timeout (Master) | 500 ms (`ESPNOW_ECHO_PING_TIMEOUT_MS`) |
### 6.4 `EspNowSlavePresence`
Felder: `network`, `mac` (6 B), `version`, `slave_id`, `available`, `used`.
- `slave_id` = letztes Oktett der STA-MAC.
- Registry auf dem Master indexiert über **Sender-MAC** der ESP-NOW-Callback.
---
## 7. Befehlsreferenz (UART)
Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uart_messages.proto`.
| ID | Name | Modul | Kurzbeschreibung |
|----|------|-------|------------------|
| 1 | ACK | — | Reserviert |
| 2 | ECHO | — | Reserviert |
| 3 | VERSION | `cmd_version.c` | FW-Version, Git-Hash, OTA-Partition |
| 4 | CLIENT_INFO | `cmd_client_info.c` | Liste `client_registry` |
| 5 | CLIENT_INPUT | — | Geplant |
| 6 | ACCEL_DEADZONE | `cmd_accel_deadzone.c` | Get/Set Deadzone lokal + ESP-NOW |
| 7 | ESPNOW_UNICAST_TEST | `cmd_espnow_unicast_test.c` | Link-Test zu Slave |
| 8 | LED_RING | `cmd_led_ring.c` | Ring lokal / ESP-NOW |
| 16 | OTA_START | `cmd_ota.c` | UART-OTA beginnen |
| 17 | OTA_PAYLOAD | `cmd_ota.c` | Chunk ≤200 B |
| 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung |
| 19 | OTA_STATUS | `cmd_ota.c` | Gerät → Host Status |
| 20 | OTA_START_ESPNOW | `cmd_ota.c` | Nur ESP-NOW aus Staging |
| 21 | OTA_SLAVE_PROGRESS | `cmd_ota_slave_progress.c` | Fortschritt pro Slave |
| 22 | FIND_ME | `cmd_espnow_find_me.c` | LED Locate |
| 23 | RESTART | `cmd_restart.c` | Master oder Slave reboot |
| 25 | ACCEL_STREAM | `cmd_accel_stream.c` | ESP-NOW Accel-Stream an/aus |
| 26 | BATTERY_STATUS | `cmd_battery.c` | Cache LiPo Master + Slaves |
| 27 | TAP_NOTIFY | `cmd_tap_notify.c` | Tap-Weiterleitung konfigurieren |
| 29 | CACHE_STATUS | `cmd_cache_status.c` | Accel + Tap Cache (ein Round-Trip) |
| 30 | ESPNOW_ECHO_PING | `cmd_espnow_echo_ping.c` | Timestamp-Echo über ESP-NOW (Latenztest) |
| 31 | SET_LOG_LEVEL | `cmd_set_log_level.c` | ESP-IDF-Log-Level global (`"*"`) lesen/setzen |
### 7.1 VERSION (3)
- **Request:** nur Typ-Byte `0x03` oder leerer protobuf-Body.
- **Response:** `version`, `git_hash`, `running_partition`.
### 7.2 CLIENT_INFO (4)
- **Response:** wiederholtes `ClientInfo` pro Registry-Eintrag: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping`, Tap-/Stream-Flags.
### 7.3 ACCEL_DEADZONE (6)
- **Request:** `write`, `deadzone`, `client_id` (0=lokal), `all_clients`.
- **Write Master:** Registry + `esp_now_comm_send_accel_deadzone` pro Slave; lokal `bma456` + NVS.
- **Slave-Empfang:** `handle_slave_accel_deadzone` in `esp_now_comm.c`.
### 7.4 ACCEL_STREAM (25)
- Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`.
- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms.
### 7.5 TAP_NOTIFY (27)
- Konfiguriert auf Slave welche Tap-Arten `ESPNOW_TAP_EVENT` auslösen.
- Tap-Quelle: BMA456-Interrupt → `on_bma456_tap` in `esp_now_comm.c`.
### 7.6 CACHE_STATUS (29)
- **Request:** leer.
- **Response:** pro Slave mit Stream und/oder Tap-Notify: gecachte Accel-Werte (`age_ms`) und konsumierte Tap-Events (`client_registry_take_tap`).
### 7.7 BATTERY_STATUS (26)
- Liest **nur Cache** — keine ESP-NOW-Roundtrip-Pflicht pro Abfrage.
- Master-Batterie: `client_registry_set_master_battery` (Monitor-Task).
### 7.8 LED_RING (8)
| mode | Bedeutung |
|------|-----------|
| 0 | Clear |
| 1 | Progress 0100 % |
| 2 | Digit 010 |
| 3 | Blink |
| 4 | Find-me (RGB-Sequenz) |
| 5 | Solid color |
`client_id=0` nur Master-Ring; `>0` ESP-NOW; `all_clients` Broadcast über Registry.
### 7.9 FIND_ME (22) / RESTART (23)
- `client_id=0`: lokaler Master (`led_ring_find_me` / `pod_schedule_restart`).
- `client_id>0`: ESP-NOW Unicast zum Slave.
### 7.10 ESPNOW_UNICAST_TEST (7)
Minimaler Master→Slave-Ping; Slave loggt `UNICAST TEST OK`.
### 7.11 ESPNOW_ECHO_PING (30)
Round-Trip-Latenztest zu einem **Slave** (`client_id` > 0, muss in der Registry sein).
**Ablauf:**
1. Host (goTool) setzt `timestamp_us` (Unix-µs) und sendet `EspNowEchoPingRequest` per UART.
2. Master (`cmd_espnow_echo_ping.c`) löst MAC aus Registry auf, ruft `esp_now_comm_echo_ping()` auf.
3. Master sendet `ESPNOW_ECHO_PING` mit `host_timestamp_us` und `master_time_us` (`esp_timer_get_time()` kurz vor Send).
4. Slave (`handle_echo_ping`) antwortet mit `ESPNOW_ECHO_PONG` (Felder unverändert).
5. Master empfängt Pong, berechnet `esp_rtt_us = esp_timer_get_time() - master_time_us`, antwortet per UART.
**Request:** `client_id`, `timestamp_us` (vom Host gesetzt).
**Response:** `success`, `client_id`, `timestamp_us` (echoed), `esp_rtt_us` (nur bei Erfolg).
| Feld | Quelle | Bedeutung |
|------|--------|-----------|
| `timestamp_us` | Host → Slave → Host | Korrelations-ID; muss mit Request übereinstimmen |
| `esp_rtt_us` | Master | Rohe µs-Differenz von `esp_timer_get_time()` (Ping-Send → Pong-Empfang im `recv_cb`) |
**Implementierung:** `main/cmd/cmd_espnow_echo_ping.c`, `esp_now_comm_echo_ping()` in `esp_now_master.c`, Slave `handle_echo_ping` in `esp_now_slave.c`.
**Host-seitig (goTool):** `rtt_ms` = volle UART-Kette (Send bis Response-Empfang); unabhängig von `esp_rtt_us`.
### 7.12 SET_LOG_LEVEL (31)
Laufzeit-Steuerung des **globalen** ESP-IDF-Log-Levels auf dem Master (`esp_log_level_set("*", …)`). Kein ESP-NOW, kein NVS — nur Master.
**Request:** `write`, `level` (`esp_log_level_t`: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE).
| `write` | Verhalten |
|---------|-----------|
| `false` / leer | Aktuelles Level lesen (`esp_log_level_get("*")`) |
| `true` | Level setzen; ungültige Werte (>5) → `success=false` |
**Response:** `success`, `level` (aktuell bzw. gesetzt).
**Boot-Default:** `CONFIG_LOG_DEFAULT_LEVEL` in `sdkconfig` (aktuell **INFO** = 3). Kann per UART/Web-UI zur Laufzeit geändert werden; nach Reboot gilt wieder der sdkconfig-Wert.
**Ausgabe:** Logs erscheinen auf **UART0** (Debug-USB), nicht auf dem Host-UART1 (goTool).
```bash
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 3
```
---
## 8. OTA
### 8.1 UART-OTA (nur Master)
Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`.
| Phase | Host → Master | Master → Host |
|-------|---------------|---------------|
| Start | `OTA_START` + `total_size` | `OTA_STATUS` preparing → ready |
| Daten | `OTA_PAYLOAD` ≤200 B, `seq` | `block_ack` je 4096 B Flash |
| Ende | `OTA_END` | success/failed; startet ESP-NOW-Verteilung |
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
### 8.2 ESP-NOW-OTA (Master → Slaves)
`ota_espnow.c` — gleiche Status-Codes wie UART.
1. `ESPNOW_OTA_START` + `total_size` → Slave `ota_espnow_slave_on_start`
2. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave
3. `ESPNOW_OTA_END` → Boot-Partition setzen
4. Slave antwortet `ESPNOW_OTA_STATUS` (mit `send_message_ex` + Semaphore auf Master)
Nach Erfolg: **alle Knoten neu starten**.
### 8.3 OTA_SLAVE_PROGRESS (21)
Abfrage laufender oder letzter ESP-NOW-Verteilung: pro Slave `bytes_written`, `status`, `error`.
---
## 9. BMA456 Beschleunigungssensor
| Thema | Detail |
|-------|--------|
| Wrapper | `bosch456.c` / `bosch456.h` |
| Chip-Variante | BMA456**H** (`bma456h.c` im Component) |
| I2C | Adresse 0x18, 100 kHz |
| Polling | Task ~10 Hz |
| Interrupt GPIO 10 | Single/Double/Triple Tap |
| Deadzone | Software-Filter für Logs/Stream (Default 100 LSB) |
| API | `bma456_read_accel`, `bma456_set_accel_deadzone`, `bma456_set_tap_handler` |
Ohne Sensor: `bma456_is_ready() == false`, Firmware läuft weiter.
---
## 10. LED-Ring
- **95 LEDs**, WS2812 über RMT (`led_ring.c`).
- Segment-Karten für Ziffern 010 in `digit_lookup[]`.
- **Kein** lokaler Demo-Loop — Anzeige nur über UART (`cmd_led_ring.c`) oder ESP-NOW `ESPNOW_LED_RING`.
- Helligkeit: `intensity` 0 → Default ~5 % (`LED_RING_DEFAULT_INTENSITY`).
- Modi: `0` clear … `5` color, `6` **battery-low** (erste 4 LEDs rot ~10 %, 5 s).
---
## 11. Board-Input und Batterie
`board_input.c`:
- **Taster** GPIO 12 — Logging bei Druck.
- **LiPo-ADC** GPIO 1 und GPIO 11 (Pin-Spannung in mV).
- Master: `master_monitor_task` aktualisiert Master-Batterie alle 30 s.
- Slave: `slave_send_battery_report_to_master` nach Join, Heartbeat und Query.
Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART.
### Software-UV-Schutz (`battery_uv.c`)
Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override möglich).
| Zustand | ADC-Schwelle | Logik |
|---------|--------------|-------|
| UV | &lt; 2300 mV | Ein gültiger Kanal reicht |
| Geladen | ≥ 3000 mV | Alle gültigen Kanäle |
- NVS-Key `uv_latch` in `pod_settings.c`
- Boot: ADC früh, bei UV nur LED + ADC-Poll (kein ESP-NOW/WiFi/UART/I2C/Button)
- Runtime: `battery_uv_poll()` im LiPo-Monitor (2 Samples Debounce)
- LED-Anzeige: Mode `6` battery-low (`led_ring_show_battery_low`)
---
## 12. Persistenz (NVS)
`pod_settings.c` — Namespace `powerpod`:
| Key | Inhalt |
|-----|--------|
| `accel_dz` | Accel-Deadzone LSB |
| `uv_latch` | Software-UV-Zustand (`1` = Energiesparmodus) |
Gespeichert bei lokalem Set (UART `client_id=0`, ESP-NOW auf Slave). Geladen nach `init_bma456()`.
---
## 13. Protobuf und Code-Generierung
```bash
make proto_generate # beide Schemas
make proto_generate_uart # nur uart_messages.proto
make proto_generate_espnow # nur esp_now_messages.proto
```
**Ausgabe:** `main/proto/*.pb.c`, `*.pb.h`
**Optionen:** `main/proto/uart_messages.options` (nanopb max_size etc.)
Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `main/proto/...`).
**Paketname protobuf:** `alox` → C-Prefix `alox_`.
---
## 14. Modulreferenz
### 14.1 Kern
| Datei | Rolle |
|-------|--------|
| `powerpod.c` | `app_main`, Init, Master-Register |
| `app_config.h` | Laufzeit-Konfiguration |
| `cmd_handler.c` | Queue + Dispatcher |
| `uart.c` | Framing |
| `uart_proto.c` | UartMessage send |
| `uart_cmd.c` | Handler-Hilfen |
### 14.2 Kommunikation
| Datei | Rolle |
|-------|--------|
| `esp_now_comm.c` | Init, recv-Router |
| `esp_now_core.c` | WiFi, Peer, gemeinsamer Send |
| `esp_now_master.c` | Master-Tasks, Registry, Unicast send |
| `esp_now_slave.c` | Join, Heartbeat, Accel/Tap send |
| `esp_now_proto.c` | nanopb für EspNowMessage |
| `client_registry.c` | Slave-Tabelle + Telemetrie-Cache |
### 14.3 Befehle (`main/cmd/`)
| Datei | UART-Typ |
|-------|----------|
| `cmd_version.c` | VERSION |
| `cmd_client_info.c` | CLIENT_INFO |
| `cmd_accel_deadzone.c` | ACCEL_DEADZONE |
| `cmd_accel_stream.c` | ACCEL_STREAM |
| `cmd_tap_notify.c` | TAP_NOTIFY |
| `cmd_cache_status.c` | CACHE_STATUS |
| `cmd_battery.c` | BATTERY_STATUS |
| `cmd_led_ring.c` | LED_RING |
| `cmd_espnow_find_me.c` | FIND_ME |
| `cmd_restart.c` | RESTART |
| `cmd_espnow_unicast_test.c` | ESPNOW_UNICAST_TEST |
| `cmd_espnow_echo_ping.c` | ESPNOW_ECHO_PING |
| `cmd_set_log_level.c` | SET_LOG_LEVEL |
| `cmd_ota.c` | OTA_* |
| `cmd_ota_slave_progress.c` | OTA_SLAVE_PROGRESS |
### 14.4 Domäne
| Datei | Rolle |
|-------|--------|
| `bosch456.c` | Accelerometer + Tap |
| `led_ring.c` | LED-Anzeige |
| `board_input.c` | Taster, ADC |
| `battery_uv.c` | Software-UV-Schutz, Energiesparmodus |
| `pod_settings.c` | NVS |
| `pod_reboot.c` | Verzögerter Restart |
| `ota_uart.c` | Flash-Puffer UART-OTA |
| `ota_espnow.c` | OTA Master/Slave; Slave-Work-Queue |
| `ota_session.c` | OTA-Sperre für UART-Dispatcher |
### 14.5 Protobuf
| Datei | Rolle |
|-------|--------|
| `proto/uart_messages.proto` | UART-Vertrag |
| `proto/esp_now_messages.proto` | ESP-NOW-Vertrag |
| `proto/pb_encode.c`, `pb_decode.c`, `pb_common.c` | nanopb Runtime |
---
## 15. Logging-Tags
**Level zur Laufzeit:** UART `SET_LOG_LEVEL` (31) oder Dashboard „ESP Log-Level“ — steuert nur die Konsolen-Ausgabe auf UART0, nicht das Host-Protokoll.
| Tag | Modul |
|-----|--------|
| `[Main]` | powerpod.c |
| `[LOG_LVL]` | cmd_set_log_level.c |
| `[UART]` | uart.c |
| `[CMDH]` | cmd_handler.c |
| `[UART_CMD]` | uart_cmd.c |
| `[ESPNOW]` | esp_now_comm.c |
| `[CLIENTS]` | client_registry.c |
| `[BMA456]` | bosch456.c |
| `[VERSION]` etc. | jeweiliger cmd_* |
Typischer Ablauf bei UART-Befehl:
```
[UART] received message cmd=0x06 len=…
[CMDH] trigger command ACCEL_DEADZONE (0x06)
[ACCEL_DZ] …
```
---
## Anhang: Neues UART-Kommando hinzufügen
1. `uart_messages.proto` erweitern → `make proto_generate_uart`
2. `cmd/cmd_neu.c` mit `handle_*` + `uart_cmd_register()`
3. In `powerpod.c` `cmd_neu_register()` aufrufen (nur Master-Zweig)
4. Bei Slave-Bedarf: `esp_now_messages.proto` + `esp_now_comm_send_*` + Slave-Zweig in `espnow_recv_cb`
Siehe [`adding-a-feature.md`](adding-a-feature.md) für ein vollständiges Beispiel (Find Me).
+1 -1
View File
@@ -345,7 +345,7 @@ idf.py build
Ähnliche Features zum Abgucken: Ähnliche Features zum Abgucken:
- **Nur Master, kein ESP-NOW:** `main/cmd/cmd_version.c`, `main/cmd/cmd_led_ring.c` - **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` - **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` - **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` - **Großer ESP-NOW-Fluss mit Status:** `ota_espnow.c`, `main/cmd/cmd_ota.c`
+2
View File
@@ -0,0 +1,2 @@
win:
GOOS=windows GOARCH=386 go build .
+20 -2
View File
@@ -28,13 +28,15 @@ go run . -port /dev/ttyUSB0 clients
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) | | `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling | | `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) | | `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
| `echo-ping` | `0x1e` | ESP-NOW echo round-trip to one slave (`-client`); prints `rtt_ms` (host UART chain) and `esp_rtt_us` (master ESP-NOW, raw µs) |
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) | | `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) | | `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
| `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW | | `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) | | `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
| `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me`, `-client`, `-all` | | `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me\|battery-low`, `-client`, `-all` |
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) | | `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) | | `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
| `log-level` | `0x1f` | Get/set master ESP-IDF log level for tag `"*"` (`-set`, `-level` 05); output on UART0 debug, not host UART |
`clients` requires slaves to have responded to master discover broadcasts first. `clients` requires slaves to have responded to master discover broadcasts first.
@@ -86,7 +88,7 @@ If the UART device is unplugged or the port disappears, `serve` keeps running an
| Doc | Content | | Doc | Content |
|-----|---------| |-----|---------|
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands, **`accel` / `tap` push stream** format, dashboard `ws://…:8080/ws` | | **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands and **`input` push stream** (accel + tap) |
| **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) | | **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) |
CLI: CLI:
@@ -113,8 +115,24 @@ go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
On success the slave serial log should show `UNICAST TEST OK from master … seq=42`. On success the slave serial log should show `UNICAST TEST OK from master … seq=42`.
```bash
go run . -port /dev/ttyUSB0 echo-ping -client 16
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 3
```
`log-level` controls `esp_log_*` on the master (UART0 USB console). The host protocol UART (GPIO 2/3) is unchanged.
Measures latency to one slave. `rtt_ms` is the full host round-trip (UART + ESP-NOW + UART back). `esp_rtt_us` is the master-side ESP-NOW leg only (`esp_timer_get_time()` delta, raw microseconds from firmware).
Example output: Example output:
```
echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234
```
`clients` example:
``` ```
clients (2): clients (2):
[0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250 [0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250
+108 -1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
@@ -40,6 +41,19 @@ type unicastAPIResponse struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type echoPingAPIRequest struct {
ClientID uint32 `json:"client_id"`
}
type echoPingAPIResponse struct {
Success bool `json:"success"`
ClientID uint32 `json:"client_id,omitempty"`
TimestampUs uint64 `json:"timestamp_us,omitempty"`
RttMs float64 `json:"rtt_ms"`
EspRttUs uint32 `json:"esp_rtt_us"`
Error string `json:"error,omitempty"`
}
type findMeAPIRequest struct { type findMeAPIRequest struct {
ClientID uint32 `json:"client_id"` ClientID uint32 `json:"client_id"`
} }
@@ -60,6 +74,17 @@ type restartAPIResponse struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
type logLevelAPIResponse struct {
Success bool `json:"success"`
Level uint32 `json:"level"`
Error string `json:"error,omitempty"`
}
type logLevelAPIRequest struct {
Write bool `json:"write"`
Level uint32 `json:"level"`
}
type otaAPIResponse struct { type otaAPIResponse struct {
Success bool `json:"success"` Success bool `json:"success"`
BytesWritten uint32 `json:"bytes_written,omitempty"` BytesWritten uint32 `json:"bytes_written,omitempty"`
@@ -90,6 +115,13 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
} }
serveUnicastTest(w, r, link) serveUnicastTest(w, r, link)
}) })
mux.HandleFunc("/api/echo-ping", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveEchoPing(w, r, link)
})
mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -104,6 +136,16 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
} }
serveRestart(w, r, link) serveRestart(w, r, link)
}) })
mux.HandleFunc("/api/log-level", func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
serveLogLevelGet(w, r, link)
case http.MethodPost:
serveLogLevelPost(w, r, link)
default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
})
mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -146,7 +188,11 @@ func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial,
if hub != nil { if hub != nil {
hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()}) hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()})
} }
writeJSON(w, http.StatusServiceUnavailable, otaAPIResponse{Error: err.Error()}) status := http.StatusServiceUnavailable
if errors.Is(err, errOTAInProgress) {
status = http.StatusConflict
}
writeJSON(w, status, otaAPIResponse{Error: err.Error()})
return return
} }
writeJSON(w, http.StatusOK, otaAPIResponse{ writeJSON(w, http.StatusOK, otaAPIResponse{
@@ -255,6 +301,43 @@ func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error)
return updated, nil return updated, nil
} }
func serveLogLevelGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
resp, err := link.SetLogLevel(false, 0)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, logLevelAPIResponse{
Success: resp.GetSuccess(),
Level: resp.GetLevel(),
})
}
func serveLogLevelPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body logLevelAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "invalid JSON"})
return
}
if !body.Write {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "write must be true"})
return
}
if body.Level > 5 {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "level must be 05"})
return
}
resp, err := link.SetLogLevel(true, body.Level)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, logLevelAPIResponse{
Success: resp.GetSuccess(),
Level: resp.GetLevel(),
})
}
func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) { func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body restartAPIRequest var body restartAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil { if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
@@ -311,6 +394,30 @@ func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSeria
}) })
} }
func serveEchoPing(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body echoPingAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, echoPingAPIResponse{Error: "invalid JSON"})
return
}
if body.ClientID == 0 {
writeJSON(w, http.StatusBadRequest, echoPingAPIResponse{Error: "client_id required"})
return
}
result, err := link.EchoPing(body.ClientID)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, echoPingAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, echoPingAPIResponse{
Success: result.Success,
ClientID: result.ClientID,
TimestampUs: result.TimestampUs,
RttMs: result.RttMs,
EspRttUs: result.EspRttUs,
})
}
func parseUintQuery(r *http.Request, key string, def uint32) (uint32, error) { func parseUintQuery(r *http.Request, key string, def uint32) (uint32, error) {
s := r.URL.Query().Get(key) s := r.URL.Query().Get(key)
if s == "" { if s == "" {
+224 -292
View File
@@ -9,55 +9,61 @@ import (
"sync" "sync"
"time" "time"
"github.com/gorilla/websocket"
"powerpod/gotool/pb" "powerpod/gotool/pb"
"github.com/gorilla/websocket"
) )
const ( const (
defaultAccelStreamInterval = 16 * time.Millisecond defaultAccelStreamInterval = 16 * time.Millisecond
defaultPreFetchMs = 2
minAPIStreamInterval = 1 * time.Millisecond minAPIStreamInterval = 1 * time.Millisecond
maxAPIStreamInterval = 10 * time.Second maxAPIStreamInterval = 10 * time.Second
// How long tap events stay in API push/cache after first sight (matches dashboard). // How long tap events stay in API push/cache after first sight (matches dashboard).
apiTapDisplayMinMs = 2000 apiTapDisplayMinMs = 2000
) )
// AccelClientSample is one slave's cached accel on the master. // InputClientSample is one slave's cached accel + tap state on the master.
type AccelClientSample struct { type InputClientSample struct {
ClientID uint32 `json:"client_id"` ClientID uint32 `json:"client_id"`
Valid bool `json:"valid"` Valid bool `json:"valid"`
X int32 `json:"x,omitempty"` X int32 `json:"x,omitempty"`
Y int32 `json:"y,omitempty"` Y int32 `json:"y,omitempty"`
Z int32 `json:"z,omitempty"` Z int32 `json:"z,omitempty"`
AgeMs uint32 `json:"age_ms,omitempty"` AccelAgeMs uint32 `json:"accel_age_ms,omitempty"`
TapKind string `json:"tap_kind,omitempty"`
TapAgeMs uint32 `json:"tap_age_ms,omitempty"`
} }
// AccelStreamMessage is sent to external WebSocket clients (hello + accel samples). // InputStreamMessage is sent to external WebSocket clients (hello + input samples).
type AccelStreamMessage struct { type InputStreamMessage struct {
Type string `json:"type"` // "hello" | "accel" Type string `json:"type"` // "hello" | "input"
Serial string `json:"serial_port,omitempty"` Serial string `json:"serial_port,omitempty"`
IntervalMs int `json:"interval_ms,omitempty"` IntervalMs int `json:"interval_ms,omitempty"`
PreFetchMs int `json:"pre_fetch_ms,omitempty"`
TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"` TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"`
Commands []string `json:"commands,omitempty"` Commands []string `json:"commands,omitempty"`
Note string `json:"note,omitempty"` Note string `json:"note,omitempty"`
T int64 `json:"t,omitempty"` // Unix nanoseconds T int64 `json:"t,omitempty"` // Unix nanoseconds
Success bool `json:"success,omitempty"` Success bool `json:"success,omitempty"`
Clients []AccelClientSample `json:"clients,omitempty"` Clients []InputClientSample `json:"clients,omitempty"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// StreamStatusMessage is the reply to set_stream / get_stream (this connection). // StreamStatusMessage is the reply to set_stream / get_stream (this connection).
type StreamStatusMessage struct { type StreamStatusMessage struct {
Type string `json:"type"` // "stream_status" Type string `json:"type"` // "stream_status"
ReceiveAccel bool `json:"receive_accel"` ReceiveInput bool `json:"receive_input"`
IntervalMs int `json:"interval_ms"` IntervalMs int `json:"interval_ms"`
Success bool `json:"success"` PreFetch int `json:"pre_fetch"`
Error string `json:"error,omitempty"` Success bool `json:"success"`
Error string `json:"error,omitempty"`
} }
// AccelStreamStatusMessage is the reply to set_accel_stream / get_accel_stream (slave). // InputStreamStatusMessage is the reply to set_input_stream / get_input_stream (slave).
type AccelStreamStatusMessage struct { type InputStreamStatusMessage struct {
Type string `json:"type"` // "accel_stream_status" Type string `json:"type"` // "input_stream_status"
ClientID uint32 `json:"client_id"` ClientID uint32 `json:"client_id"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
Success bool `json:"success"` Success bool `json:"success"`
@@ -65,33 +71,6 @@ type AccelStreamStatusMessage struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// TapClientEvent is one tap visible to API clients (fresh or within tap_display_min_ms).
type TapClientEvent struct {
ClientID uint32 `json:"client_id"`
Valid bool `json:"valid"`
Kind string `json:"kind,omitempty"` // single | double | triple
AgeMs uint32 `json:"age_ms,omitempty"`
ShownAtMs int64 `json:"shown_at_ms,omitempty"` // Unix ms when API first saw this tap
}
// TapStreamMessage is pushed to external WebSocket clients when receive_tap is on.
type TapStreamMessage struct {
Type string `json:"type"` // "tap"
T int64 `json:"t,omitempty"`
Success bool `json:"success,omitempty"`
Events []TapClientEvent `json:"events,omitempty"`
Error string `json:"error,omitempty"`
}
// TapStreamStatusMessage is the reply to set_tap_stream / get_tap_stream (this connection).
type TapStreamStatusMessage struct {
Type string `json:"type"` // "tap_stream_status"
ReceiveTap bool `json:"receive_tap"`
IntervalMs int `json:"interval_ms"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// APIClientInfo is one registered slave (or slot) from CLIENT_INFO. // APIClientInfo is one registered slave (or slot) from CLIENT_INFO.
type APIClientInfo struct { type APIClientInfo struct {
ID uint32 `json:"id"` ID uint32 `json:"id"`
@@ -101,7 +80,7 @@ type APIClientInfo struct {
Used bool `json:"used"` Used bool `json:"used"`
LastPing uint32 `json:"last_ping"` LastPing uint32 `json:"last_ping"`
LastSuccessPing uint32 `json:"last_success_ping"` LastSuccessPing uint32 `json:"last_success_ping"`
AccelStream bool `json:"accel_stream"` InputStream bool `json:"input_stream"`
TapNotifySingle bool `json:"tap_notify_single"` TapNotifySingle bool `json:"tap_notify_single"`
TapNotifyDouble bool `json:"tap_notify_double"` TapNotifyDouble bool `json:"tap_notify_double"`
TapNotifyTriple bool `json:"tap_notify_triple"` TapNotifyTriple bool `json:"tap_notify_triple"`
@@ -132,6 +111,7 @@ type accelWSCommand struct {
ClientID uint32 `json:"client_id"` ClientID uint32 `json:"client_id"`
Enable *bool `json:"enable"` Enable *bool `json:"enable"`
IntervalMs *int `json:"interval_ms"` IntervalMs *int `json:"interval_ms"`
PreFetch *int `json:"pre_fetch"`
Single *bool `json:"single"` Single *bool `json:"single"`
DoubleTap *bool `json:"double_tap"` DoubleTap *bool `json:"double_tap"`
Triple *bool `json:"triple"` Triple *bool `json:"triple"`
@@ -144,6 +124,7 @@ type APIInfoResponse struct {
SerialPort string `json:"serial_port"` SerialPort string `json:"serial_port"`
WebSocket string `json:"websocket"` WebSocket string `json:"websocket"`
DefaultIntervalMs int `json:"default_interval_ms"` DefaultIntervalMs int `json:"default_interval_ms"`
DefaultPreFetchMs int `json:"default_pre_fetch_ms"`
MinIntervalMs int `json:"min_interval_ms"` MinIntervalMs int `json:"min_interval_ms"`
MaxIntervalMs int `json:"max_interval_ms"` MaxIntervalMs int `json:"max_interval_ms"`
TapDisplayMinMs int `json:"tap_display_min_ms"` TapDisplayMinMs int `json:"tap_display_min_ms"`
@@ -153,21 +134,28 @@ type APIInfoResponse struct {
type cachedTapEvent struct { type cachedTapEvent struct {
kind string kind string
shownAt time.Time shownAt time.Time
ageMs uint32
} }
type wsSubscriber struct { type wsSubscriber struct {
conn *websocket.Conn conn *websocket.Conn
receiveAccel bool receiveInput bool
receiveTap bool
interval time.Duration interval time.Duration
lastAccelSent time.Time preFetch time.Duration
lastTapSent time.Time lastInputSent time.Time
}
type pendingInputCache struct {
cache *pb.CacheStatusResponse
readAt time.Time
readErr error
} }
type accelStreamHub struct { type accelStreamHub struct {
mu sync.RWMutex mu sync.RWMutex
clients map[*websocket.Conn]*wsSubscriber clients map[*websocket.Conn]*wsSubscriber
defaultInterval time.Duration defaultInterval time.Duration
defaultPreFetch time.Duration
configChanged chan struct{} configChanged chan struct{}
recentTaps map[uint32]cachedTapEvent recentTaps map[uint32]cachedTapEvent
} }
@@ -176,6 +164,7 @@ func newAccelStreamHub(defaultInterval time.Duration) *accelStreamHub {
return &accelStreamHub{ return &accelStreamHub{
clients: make(map[*websocket.Conn]*wsSubscriber), clients: make(map[*websocket.Conn]*wsSubscriber),
defaultInterval: defaultInterval, defaultInterval: defaultInterval,
defaultPreFetch: defaultPreFetchMs * time.Millisecond,
configChanged: make(chan struct{}, 1), configChanged: make(chan struct{}, 1),
} }
} }
@@ -197,26 +186,39 @@ func clampAPIInterval(d time.Duration) time.Duration {
return d return d
} }
func clampPreFetch(d time.Duration) time.Duration {
if d < 0 {
return 0
}
if d > maxAPIStreamInterval {
return maxAPIStreamInterval
}
return d
}
func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubscriber { func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubscriber {
sub := &wsSubscriber{ sub := &wsSubscriber{
conn: conn, conn: conn,
receiveAccel: false, receiveInput: false,
interval: h.defaultInterval, interval: h.defaultInterval,
preFetch: h.defaultPreFetch,
} }
h.mu.Lock() h.mu.Lock()
h.clients[conn] = sub h.clients[conn] = sub
h.mu.Unlock() h.mu.Unlock()
hello := AccelStreamMessage{ hello := InputStreamMessage{
Type: "hello", Type: "hello",
Serial: portName, Serial: portName,
IntervalMs: int(h.defaultInterval / time.Millisecond), IntervalMs: int(h.defaultInterval / time.Millisecond),
PreFetchMs: int(h.defaultPreFetch / time.Millisecond),
TapDisplayMinMs: apiTapDisplayMinMs, TapDisplayMinMs: apiTapDisplayMinMs,
Note: "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push", Note: "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection",
Commands: []string{ Commands: []string{
"list_clients", "list_clients",
"set_stream", "get_stream", "set_accel_stream", "get_accel_stream", "set_stream", "get_stream",
"set_tap_stream", "get_tap_stream", "set_tap_notify", "get_tap_notify", "set_input_stream", "get_input_stream",
"set_tap_notify", "get_tap_notify",
"set_led_ring", "get_battery", "set_led_ring", "get_battery",
}, },
} }
@@ -229,36 +231,25 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
func (h *accelStreamHub) unregister(conn *websocket.Conn) { func (h *accelStreamHub) unregister(conn *websocket.Conn) {
h.mu.Lock() h.mu.Lock()
delete(h.clients, conn) delete(h.clients, conn)
anyTap := false anyInput := false
for _, sub := range h.clients { for _, sub := range h.clients {
if sub.receiveTap { if sub.receiveInput {
anyTap = true anyInput = true
break break
} }
} }
if !anyTap { if !anyInput {
h.recentTaps = nil h.recentTaps = nil
} }
h.mu.Unlock() h.mu.Unlock()
h.notifyConfigChanged() h.notifyConfigChanged()
} }
func (h *accelStreamHub) anyWantsAccel() bool { func (h *accelStreamHub) anyWantsInput() bool {
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
for _, sub := range h.clients { for _, sub := range h.clients {
if sub.receiveAccel { if sub.receiveInput {
return true
}
}
return false
}
func (h *accelStreamHub) anyWantsTap() bool {
h.mu.RLock()
defer h.mu.RUnlock()
for _, sub := range h.clients {
if sub.receiveTap {
return true return true
} }
} }
@@ -270,7 +261,7 @@ func (h *accelStreamHub) minWantedInterval() time.Duration {
defer h.mu.RUnlock() defer h.mu.RUnlock()
var min time.Duration var min time.Duration
for _, sub := range h.clients { for _, sub := range h.clients {
if !sub.receiveAccel && !sub.receiveTap { if !sub.receiveInput {
continue continue
} }
if min == 0 || sub.interval < min { if min == 0 || sub.interval < min {
@@ -283,20 +274,28 @@ func (h *accelStreamHub) minWantedInterval() time.Duration {
return min return min
} }
func (h *accelStreamHub) setStream(sub *wsSubscriber, enable bool, intervalMs *int) StreamStatusMessage { func (h *accelStreamHub) setStream(sub *wsSubscriber, enable bool, intervalMs, preFetchMs *int) StreamStatusMessage {
h.mu.Lock() h.mu.Lock()
sub.receiveAccel = enable sub.receiveInput = enable
if !enable {
h.recentTaps = nil
}
if intervalMs != nil { if intervalMs != nil {
sub.interval = clampAPIInterval(time.Duration(*intervalMs) * time.Millisecond) sub.interval = clampAPIInterval(time.Duration(*intervalMs) * time.Millisecond)
} }
if preFetchMs != nil {
sub.preFetch = clampPreFetch(time.Duration(*preFetchMs) * time.Millisecond)
}
ms := int(sub.interval / time.Millisecond) ms := int(sub.interval / time.Millisecond)
pf := int(sub.preFetch / time.Millisecond)
h.mu.Unlock() h.mu.Unlock()
h.notifyConfigChanged() h.notifyConfigChanged()
return StreamStatusMessage{ return StreamStatusMessage{
Type: "stream_status", Type: "stream_status",
ReceiveAccel: enable, ReceiveInput: enable,
IntervalMs: ms, IntervalMs: ms,
PreFetch: pf,
Success: true, Success: true,
} }
} }
@@ -306,45 +305,47 @@ func (h *accelStreamHub) getStream(sub *wsSubscriber) StreamStatusMessage {
defer h.mu.RUnlock() defer h.mu.RUnlock()
return StreamStatusMessage{ return StreamStatusMessage{
Type: "stream_status", Type: "stream_status",
ReceiveAccel: sub.receiveAccel, ReceiveInput: sub.receiveInput,
IntervalMs: int(sub.interval / time.Millisecond), IntervalMs: int(sub.interval / time.Millisecond),
PreFetch: int(sub.preFetch / time.Millisecond),
Success: true, Success: true,
} }
} }
func (h *accelStreamHub) setTapStream(sub *wsSubscriber, enable bool, intervalMs *int) TapStreamStatusMessage { func (h *accelStreamHub) streamTiming(now time.Time) (needRead, needDeliver bool, waitPreFetch time.Duration) {
h.mu.Lock()
sub.receiveTap = enable
if !enable {
h.recentTaps = nil
}
if intervalMs != nil {
sub.interval = clampAPIInterval(time.Duration(*intervalMs) * time.Millisecond)
}
ms := int(sub.interval / time.Millisecond)
h.mu.Unlock()
h.notifyConfigChanged()
return TapStreamStatusMessage{
Type: "tap_stream_status",
ReceiveTap: enable,
IntervalMs: ms,
Success: true,
}
}
func (h *accelStreamHub) getTapStream(sub *wsSubscriber) TapStreamStatusMessage {
h.mu.RLock() h.mu.RLock()
defer h.mu.RUnlock() defer h.mu.RUnlock()
return TapStreamStatusMessage{ for _, sub := range h.clients {
Type: "tap_stream_status", if !sub.receiveInput {
ReceiveTap: sub.receiveTap, continue
IntervalMs: int(sub.interval / time.Millisecond), }
Success: true, if sub.lastInputSent.IsZero() {
needRead = true
needDeliver = true
if sub.preFetch > waitPreFetch {
waitPreFetch = sub.preFetch
}
continue
}
nextPush := sub.lastInputSent.Add(sub.interval)
readAt := nextPush.Add(-sub.preFetch)
if !now.Before(readAt) {
needRead = true
}
if !now.Before(nextPush) {
needDeliver = true
if sub.preFetch > waitPreFetch {
waitPreFetch = sub.preFetch
}
}
} }
return needRead, needDeliver, waitPreFetch
} }
func (h *accelStreamHub) ingestTapEvents(incoming []TapClientEvent) []TapClientEvent { func (h *accelStreamHub) ingestTapFromCache(cache *pb.CacheStatusResponse) {
if cache == nil {
return
}
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
@@ -352,39 +353,66 @@ func (h *accelStreamHub) ingestTapEvents(incoming []TapClientEvent) []TapClientE
if h.recentTaps == nil { if h.recentTaps == nil {
h.recentTaps = make(map[uint32]cachedTapEvent) h.recentTaps = make(map[uint32]cachedTapEvent)
} }
for _, e := range incoming { for _, c := range cache.GetClients() {
if !e.Valid || e.Kind == "" { t := c.GetTap()
if t == nil {
continue continue
} }
h.recentTaps[e.ClientID] = cachedTapEvent{kind: e.Kind, shownAt: now} kind := tapKindLabelPB(t.GetKind())
if kind == "" {
continue
}
h.recentTaps[c.GetClientId()] = cachedTapEvent{
kind: kind,
shownAt: now,
ageMs: t.GetAgeMs(),
}
} }
return h.activeTapEventsLocked(now) h.pruneRecentTapsLocked(now)
} }
func (h *accelStreamHub) activeTapEventsLocked(now time.Time) []TapClientEvent { func (h *accelStreamHub) pruneRecentTapsLocked(now time.Time) {
if len(h.recentTaps) == 0 { if len(h.recentTaps) == 0 {
return nil return
} }
cutoff := now.Add(-apiTapDisplayMinMs * time.Millisecond) cutoff := now.Add(-apiTapDisplayMinMs * time.Millisecond)
out := make([]TapClientEvent, 0, len(h.recentTaps))
for id, ev := range h.recentTaps { for id, ev := range h.recentTaps {
if ev.shownAt.Before(cutoff) { if ev.shownAt.Before(cutoff) {
delete(h.recentTaps, id) delete(h.recentTaps, id)
continue
} }
shownAtMs := ev.shownAt.UnixMilli() }
out = append(out, TapClientEvent{ }
ClientID: id,
Valid: true, func (h *accelStreamHub) inputClientsFromCacheLocked(cache *pb.CacheStatusResponse, now time.Time) []InputClientSample {
Kind: ev.kind, h.pruneRecentTapsLocked(now)
AgeMs: uint32(now.Sub(ev.shownAt).Milliseconds()), out := make([]InputClientSample, 0, len(cache.GetClients()))
ShownAtMs: shownAtMs, for _, c := range cache.GetClients() {
}) sample := InputClientSample{
ClientID: c.GetClientId(),
}
if a := c.GetAccel(); a != nil {
sample.Valid = a.GetValid()
if a.GetValid() {
sample.X = a.GetX()
sample.Y = a.GetY()
sample.Z = a.GetZ()
sample.AccelAgeMs = a.GetAgeMs()
}
}
if ev, ok := h.recentTaps[c.GetClientId()]; ok {
sample.TapKind = ev.kind
if t := c.GetTap(); t != nil && tapKindLabelPB(t.GetKind()) == ev.kind {
sample.TapAgeMs = t.GetAgeMs()
} else {
sample.TapAgeMs = ev.ageMs + uint32(now.Sub(ev.shownAt).Milliseconds())
}
}
out = append(out, sample)
} }
return out return out
} }
func (h *accelStreamHub) deliver(msg AccelStreamMessage) { func (h *accelStreamHub) deliverInput(msg InputStreamMessage) {
data, err := json.Marshal(msg) data, err := json.Marshal(msg)
if err != nil { if err != nil {
return return
@@ -394,13 +422,13 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
for conn, sub := range h.clients { for conn, sub := range h.clients {
if !sub.receiveAccel { if !sub.receiveInput {
continue continue
} }
if !sub.lastAccelSent.IsZero() && now.Sub(sub.lastAccelSent) < sub.interval { if !sub.lastInputSent.IsZero() && now.Sub(sub.lastInputSent) < sub.interval {
continue continue
} }
sub.lastAccelSent = now sub.lastInputSent = now
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
delete(h.clients, conn) delete(h.clients, conn)
_ = conn.Close() _ = conn.Close()
@@ -408,155 +436,79 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
} }
} }
func (h *accelStreamHub) deliverTap(msg TapStreamMessage) { func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
data, err := json.Marshal(msg) ticker := time.NewTicker(minAPIStreamInterval)
if err != nil { defer ticker.Stop()
return
}
now := time.Now() var pending *pendingInputCache
h.mu.Lock()
defer h.mu.Unlock()
for conn, sub := range h.clients {
if !sub.receiveTap {
continue
}
if !sub.lastTapSent.IsZero() && now.Sub(sub.lastTapSent) < sub.interval {
continue
}
sub.lastTapSent = now
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
delete(h.clients, conn)
_ = conn.Close()
}
}
}
func runAccelStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
var ticker *time.Ticker
var tick <-chan time.Time
resetTicker := func() {
if ticker != nil {
ticker.Stop()
}
interval := hub.minWantedInterval()
ticker = time.NewTicker(interval)
tick = ticker.C
}
resetTicker()
defer func() {
if ticker != nil {
ticker.Stop()
}
}()
for { for {
select { select {
case <-stop: case <-stop:
return return
case <-hub.configChanged: case <-hub.configChanged:
resetTicker() pending = nil
case <-tick: case now := <-ticker.C:
wantAccel := hub.anyWantsAccel() && accelStreamPollingActive(dash, ctl) if !hub.anyWantsInput() || !inputPollingActive(dash, ctl, tapCtl) {
wantTap := hub.anyWantsTap() pending = nil
if !wantAccel && !wantTap {
continue continue
} }
now := time.Now().UnixNano() needRead, needDeliver, waitPreFetch := hub.streamTiming(now)
cache, err := link.readCacheStatusPoll()
if errors.Is(err, errUARTBusy) { if needRead && pending == nil {
if wantAccel { cache, err := link.readCacheStatusPoll()
hub.deliver(AccelStreamMessage{ if err != nil {
Type: "accel", pending = &pendingInputCache{readErr: err, readAt: now}
T: now, } else {
Success: false, hub.ingestTapFromCache(cache)
Error: "uart busy", pending = &pendingInputCache{cache: cache, readAt: now}
})
} }
if wantTap {
hub.deliverTap(TapStreamMessage{
Type: "tap",
T: now,
Success: false,
Error: "uart busy",
})
}
continue
} }
if err != nil {
if wantAccel { if !needDeliver || pending == nil {
hub.deliver(AccelStreamMessage{
Type: "accel",
T: now,
Success: false,
Error: err.Error(),
})
}
if wantTap {
hub.deliverTap(TapStreamMessage{
Type: "tap",
T: now,
Success: false,
Error: err.Error(),
})
}
continue continue
} }
if wantAccel { ts := now.UnixNano()
samples := accelSamplesFromCacheStatus(cache) if pending.readErr != nil {
clients := make([]AccelClientSample, 0, len(samples)) errMsg := pending.readErr.Error()
for _, s := range samples { if errors.Is(pending.readErr, errUARTBusy) {
clients = append(clients, AccelClientSample{ errMsg = "uart busy"
ClientID: s.GetClientId(),
Valid: s.GetValid(),
X: s.GetX(),
Y: s.GetY(),
Z: s.GetZ(),
AgeMs: s.GetAgeMs(),
})
} }
hub.deliver(AccelStreamMessage{ hub.deliverInput(InputStreamMessage{
Type: "accel", Type: "input",
T: now, T: ts,
Success: false,
Error: errMsg,
})
pending = nil
continue
}
if pending.cache != nil && now.Sub(pending.readAt) >= waitPreFetch {
hub.mu.RLock()
clients := hub.inputClientsFromCacheLocked(pending.cache, now)
hub.mu.RUnlock()
hub.deliverInput(InputStreamMessage{
Type: "input",
T: ts,
Success: true, Success: true,
Clients: clients, Clients: clients,
}) })
} pending = nil
if wantTap {
events := tapEventsFromCacheStatus(cache)
fresh := make([]TapClientEvent, 0, len(events))
for _, e := range events {
if !e.GetValid() {
continue
}
fresh = append(fresh, TapClientEvent{
ClientID: e.GetClientId(),
Valid: true,
Kind: tapKindLabelPB(e.GetKind()),
AgeMs: e.GetAgeMs(),
})
}
visible := hub.ingestTapEvents(fresh)
if len(visible) > 0 {
hub.deliverTap(TapStreamMessage{
Type: "tap",
T: now,
Success: true,
Events: visible,
})
}
} }
} }
} }
} }
func accelStreamPollingActive(dash *wsHub, ctl *accelStreamCtl) bool { func inputPollingActive(dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) bool {
if ctl != nil && ctl.Any() { if ctl != nil && ctl.Any() {
return true return true
} }
if tapCtl != nil && tapCtl.Any() {
return true
}
return dash != nil && dash.anyAccelStreamEnabled() return dash != nil && dash.anyAccelStreamEnabled()
} }
@@ -586,9 +538,9 @@ func writeLedRingStatus(conn *websocket.Conn, out ledRingAPIResponse) {
_ = conn.WriteMessage(websocket.TextMessage, data) _ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) { func writeInputStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
msg := AccelStreamStatusMessage{ msg := InputStreamStatusMessage{
Type: "accel_stream_status", Type: "input_stream_status",
ClientID: out.ClientID, ClientID: out.ClientID,
Enabled: out.Enabled, Enabled: out.Enabled,
Success: out.Success, Success: out.Success,
@@ -602,14 +554,6 @@ func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
_ = conn.WriteMessage(websocket.TextMessage, data) _ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeTapStreamStatus(conn *websocket.Conn, msg TapStreamStatusMessage) {
data, err := json.Marshal(msg)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
}
func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo { func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo {
return APIClientInfo{ return APIClientInfo{
ID: c.GetId(), ID: c.GetId(),
@@ -619,7 +563,7 @@ func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo {
Used: c.GetUsed(), Used: c.GetUsed(),
LastPing: c.GetLastPing(), LastPing: c.GetLastPing(),
LastSuccessPing: c.GetLastSuccessPing(), LastSuccessPing: c.GetLastSuccessPing(),
AccelStream: c.GetAccelStreamEnabled(), InputStream: c.GetAccelStreamEnabled(),
TapNotifySingle: c.GetTapNotifySingle(), TapNotifySingle: c.GetTapNotifySingle(),
TapNotifyDouble: c.GetTapNotifyDouble(), TapNotifyDouble: c.GetTapNotifyDouble(),
TapNotifyTriple: c.GetTapNotifyTriple(), TapNotifyTriple: c.GetTapNotifyTriple(),
@@ -717,28 +661,28 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
}) })
return return
} }
writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs)) writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs, cmd.PreFetch))
case "get_stream": case "get_stream":
writeStreamStatus(conn, hub.getStream(sub)) writeStreamStatus(conn, hub.getStream(sub))
case "set_accel_stream": case "set_input_stream":
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeAccelStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"}) writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
return return
} }
if cmd.Enable == nil { if cmd.Enable == nil {
writeAccelStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(conn, accelStreamAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: "enable required", Error: "enable required",
}) })
return return
} }
writeAccelStreamStatus(conn, applyAccelStreamClient(link, dash, ctl, cmd.ClientID, *cmd.Enable)) writeInputStreamStatus(conn, applyAccelStreamClient(link, dash, ctl, cmd.ClientID, *cmd.Enable))
case "get_accel_stream": case "get_input_stream":
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeAccelStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"}) writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
return return
} }
resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{ resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{
@@ -746,7 +690,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
ClientId: cmd.ClientID, ClientId: cmd.ClientID,
}) })
if err != nil { if err != nil {
writeAccelStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(conn, accelStreamAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: err.Error(), Error: err.Error(),
}) })
@@ -755,25 +699,12 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
if ctl != nil { if ctl != nil {
ctl.Set(cmd.ClientID, resp.GetEnabled()) ctl.Set(cmd.ClientID, resp.GetEnabled())
} }
writeAccelStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(conn, accelStreamAPIResponse{
Enabled: resp.GetEnabled(), Enabled: resp.GetEnabled(),
ClientID: resp.GetClientId(), ClientID: resp.GetClientId(),
Success: resp.GetSuccess(), Success: resp.GetSuccess(),
}) })
case "set_tap_stream":
if cmd.Enable == nil {
writeTapStreamStatus(conn, TapStreamStatusMessage{
Type: "tap_stream_status",
Error: "enable required",
})
return
}
writeTapStreamStatus(conn, hub.setTapStream(sub, *cmd.Enable, cmd.IntervalMs))
case "get_tap_stream":
writeTapStreamStatus(conn, hub.getTapStream(sub))
case "set_tap_notify": case "set_tap_notify":
if cmd.AllClients { if cmd.AllClients {
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil { if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
@@ -827,11 +758,11 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple()) tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple())
} }
writeTapNotifyStatus(conn, tapNotifyAPIResponse{ writeTapNotifyStatus(conn, tapNotifyAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Success: resp.GetSuccess(), Success: resp.GetSuccess(),
Single: resp.GetSingle(), Single: resp.GetSingle(),
DoubleTap: resp.GetDoubleTap(), DoubleTap: resp.GetDoubleTap(),
Triple: resp.GetTriple(), Triple: resp.GetTriple(),
}) })
case "set_led_ring": case "set_led_ring":
@@ -860,7 +791,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
default: default:
writeStreamStatus(conn, StreamStatusMessage{ writeStreamStatus(conn, StreamStatusMessage{
Type: "stream_status", Type: "stream_status",
Error: "unknown type (list_clients, set_stream, get_stream, set_accel_stream, get_accel_stream, set_tap_stream, get_tap_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)", Error: "unknown type (list_clients, set_stream, get_stream, set_input_stream, get_input_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)",
}) })
} }
} }
@@ -896,10 +827,11 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
SerialPort: portName, SerialPort: portName,
WebSocket: "/ws", WebSocket: "/ws",
DefaultIntervalMs: defMs, DefaultIntervalMs: defMs,
DefaultPreFetchMs: defaultPreFetchMs,
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond), MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond), MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
TapDisplayMinMs: apiTapDisplayMinMs, TapDisplayMinMs: apiTapDisplayMinMs,
Description: "WebSocket: set_accel_stream + set_stream for accel; set_tap_notify (slave S/D/T) then set_tap_stream for tap events (shown ≥2s)", Description: "WebSocket: set_input_stream + set_stream for input (accel + tap); set_tap_notify configures slave tap kinds",
}) })
}) })
@@ -915,7 +847,7 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
func runAPIServer(portName string, link *managedSerial, addr string, defaultInterval time.Duration, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) *http.Server { func runAPIServer(portName string, link *managedSerial, addr string, defaultInterval time.Duration, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) *http.Server {
hub := newAccelStreamHub(defaultInterval) hub := newAccelStreamHub(defaultInterval)
go runAccelStreamer(link, hub, dash, ctl, tapCtl, stop) go runInputStreamer(link, hub, dash, ctl, tapCtl, stop)
mux := http.NewServeMux() mux := http.NewServeMux()
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl, tapCtl) mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl, tapCtl)
@@ -924,7 +856,7 @@ func runAPIServer(portName string, link *managedSerial, addr string, defaultInte
srv := &http.Server{Addr: addr, Handler: mux} srv := &http.Server{Addr: addr, Handler: mux}
go func() { go func() {
log.Printf("external API http://localhost%s WebSocket ws://localhost%s/ws (default stream interval %s, per-client via set_stream / set_tap_stream)", log.Printf("external API http://localhost%s WebSocket ws://localhost%s/ws (default stream interval %s, per-client via set_stream)",
addr, addr, defaultInterval.String()) addr, addr, defaultInterval.String())
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("external API server: %v", err) log.Printf("external API server: %v", err)
+2
View File
@@ -347,6 +347,8 @@ func ledRingModeValue(mode string) (uint32, error) {
return 3, nil return 3, nil
case "find_me", "findme": case "find_me", "findme":
return 4, nil return 4, nil
case "battery_low", "batterylow":
return 6, nil
case "color", "solid", "fill": case "color", "solid", "fill":
return 5, nil return 5, nil
default: default:
+1
View File
@@ -12,6 +12,7 @@ func TestLedRingModeValue(t *testing.T) {
{"digit", 2}, {"digit", 2},
{"blink", 3}, {"blink", 3},
{"find-me", 4}, {"find-me", 4},
{"battery-low", 6},
} }
for _, tc := range tests { for _, tc := range tests {
got, err := ledRingModeValue(tc.mode) got, err := ledRingModeValue(tc.mode)
+93 -1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"time"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -410,16 +411,93 @@ func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
return r, nil return r, nil
} }
// EchoPingResult is the host-side round-trip for ESP-NOW echo ping.
type EchoPingResult struct {
Success bool `json:"success"`
ClientID uint32 `json:"client_id"`
TimestampUs uint64 `json:"timestamp_us"`
RttMs float64 `json:"rtt_ms"` // goTool: full UART round-trip
EspRttUs uint32 `json:"esp_rtt_us"` // master: µs delta ping send → pong recv
}
func (s *serialPort) echoPing(clientID uint32) (*EchoPingResult, error) {
t0 := time.Now()
timestampUs := uint64(t0.UnixMicro())
req := &pb.EspNowEchoPingRequest{
ClientId: clientID,
TimestampUs: timestampUs,
}
msg := &pb.UartMessage{
Type: pb.MessageType_ESPNOW_ECHO_PING,
Payload: &pb.UartMessage_EspnowEchoPingRequest{
EspnowEchoPingRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ESPNOW_ECHO_PING)}, body...)
respPayload, err := s.exchangePayload(payload, "ESPNOW_ECHO_PING")
if err != nil {
return nil, err
}
rttMs := float64(time.Since(t0).Microseconds()) / 1000.0
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetEspnowEchoPingResponse()
if r == nil {
return nil, fmt.Errorf("missing espnow_echo_ping_response")
}
if !r.GetSuccess() {
return &EchoPingResult{
Success: false,
ClientID: r.GetClientId(),
RttMs: rttMs,
}, nil
}
if r.GetTimestampUs() != timestampUs {
return nil, fmt.Errorf("timestamp mismatch: sent %d got %d", timestampUs, r.GetTimestampUs())
}
return &EchoPingResult{
Success: true,
ClientID: r.GetClientId(),
TimestampUs: r.GetTimestampUs(),
RttMs: rttMs,
EspRttUs: r.GetEspRttUs(),
}, nil
}
func (m *managedSerial) FindMe(clientID uint32) error { func (m *managedSerial) FindMe(clientID uint32) error {
return m.withPort(func(sp *serialPort) error { return m.withPort(func(sp *serialPort) error {
return runFindMeClient(sp, clientID) return runFindMeClient(sp, clientID)
}) })
} }
func (m *managedSerial) SetLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
var resp *pb.SetLogLevelResponse
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = runSetLogLevelClient(sp, write, level)
return e
})
return resp, err
}
func (m *managedSerial) Restart(clientID uint32) error { func (m *managedSerial) Restart(clientID uint32) error {
return m.withPort(func(sp *serialPort) error { err := m.withPort(func(sp *serialPort) error {
return runRestartClient(sp, clientID) return runRestartClient(sp, clientID)
}) })
if err != nil {
return err
}
if clientID == 0 {
m.recoverAfterMasterRestart()
}
return nil
} }
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) { func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
@@ -483,6 +561,20 @@ func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
return s.espnowUnicastTest(clientID, seq) return s.espnowUnicastTest(clientID, seq)
} }
func (s *serialPort) EchoPing(clientID uint32) (*EchoPingResult, error) {
return s.echoPing(clientID)
}
func (m *managedSerial) EchoPing(clientID uint32) (*EchoPingResult, error) {
var result *EchoPingResult
err := m.withPort(func(sp *serialPort) error {
var e error
result, e = sp.echoPing(clientID)
return e
})
return result, err
}
func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) { func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
return s.ledRingProgress(req) return s.ledRingProgress(req)
} }
+2
View File
@@ -62,6 +62,8 @@ func runTest(portOverride string, baudOverride int, args []string) error {
if err != nil { if err != nil {
return fmt.Errorf("open %s: %w", port, err) return fmt.Errorf("open %s: %w", port, err)
} }
registerShutdown(func() { _ = sp.Close() })
enableShutdownOnInterrupt()
defer sp.Close() defer sp.Close()
if !*verbose { if !*verbose {
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"flag"
"fmt"
)
func runEchoPing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("echo-ping", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id from `clients`")
if err := fs.Parse(args); err != nil {
return err
}
if *clientID == 0 {
return fmt.Errorf("client id required (see `gotool clients`)")
}
r, err := sp.echoPing(uint32(*clientID))
if err != nil {
return err
}
fmt.Printf("echo ping: success=%v client_id=%d rtt_ms=%.3f esp_rtt_us=%d\n",
r.Success, r.ClientID, r.RttMs, r.EspRttUs)
return nil
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func runLedRing(sp *serialPort, args []string) error { func runLedRing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("led-ring", flag.ExitOnError) fs := flag.NewFlagSet("led-ring", flag.ExitOnError)
mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, or find-me") mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, find-me, or battery-low")
clientID := fs.Uint("client", 0, "0=master ring, >0=slave via ESP-NOW") clientID := fs.Uint("client", 0, "0=master ring, >0=slave via ESP-NOW")
allClients := fs.Bool("all", false, "broadcast to all slaves") allClients := fs.Bool("all", false, "broadcast to all slaves")
slavesOnly := fs.Bool("slaves-only", false, "with -all: do not change master ring") slavesOnly := fs.Bool("slaves-only", false, "with -all: do not change master ring")
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"flag"
"fmt"
"google.golang.org/protobuf/proto"
"powerpod/gotool/pb"
)
func runLogLevel(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("log-level", flag.ExitOnError)
write := fs.Bool("set", false, "write log level (default: read)")
level := fs.Uint("level", 0, "esp_log_level_t 05 (with -set)")
if err := fs.Parse(args); err != nil {
return err
}
resp, err := sp.setLogLevel(*write, uint32(*level))
if err != nil {
return err
}
if !resp.GetSuccess() {
return fmt.Errorf("set_log_level rejected (level=%d)", resp.GetLevel())
}
fmt.Printf("log_level=%d success=%v\n", resp.GetLevel(), resp.GetSuccess())
return nil
}
func runSetLogLevelClient(sp *serialPort, write bool, level uint32) (*pb.SetLogLevelResponse, error) {
return sp.setLogLevel(write, level)
}
func (s *serialPort) setLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_SET_LOG_LEVEL,
Payload: &pb.UartMessage_SetLogLevelRequest{
SetLogLevelRequest: &pb.SetLogLevelRequest{
Write: write,
Level: level,
},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_SET_LOG_LEVEL)}, body...)
respPayload, err := s.exchangePayload(payload, "SET_LOG_LEVEL")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetSetLogLevelResponse()
if r == nil {
return nil, fmt.Errorf("missing set_log_level_response")
}
return r, nil
}
+1 -2
View File
@@ -16,8 +16,7 @@ func runOTA(sp *serialPort, args []string) error {
sp.mu.Lock() sp.mu.Lock()
defer sp.mu.Unlock() defer sp.mu.Unlock()
m := &managedSerial{quiet: false, sp: sp} return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) {
return runOTAOnPortUnlocked(m, data, func(p OTAProgress) {
switch p.Phase { switch p.Phase {
case "preparing", "ready": case "preparing", "ready":
fmt.Println(p.Message) fmt.Println(p.Message)
+23 -5
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"embed" "embed"
"errors"
"flag" "flag"
"fmt" "fmt"
"io/fs" "io/fs"
@@ -34,21 +35,29 @@ func runServe(portName string, baud int, args []string) error {
link := newManagedSerial(portName, baud) link := newManagedSerial(portName, baud)
link.quiet = true link.quiet = true
defer link.Close()
hub := newWSHub() hub := newWSHub()
streamCtl := newAccelStreamCtl() streamCtl := newAccelStreamCtl()
tapCtl := newTapNotifyCtl() tapCtl := newTapNotifyCtl()
stop := make(chan struct{}) stop := make(chan struct{})
defer close(stop)
var dashSrv *http.Server
var apiSrv *http.Server
registerShutdown(func() {
close(stop)
shutdownHTTPServer(dashSrv)
shutdownAPIServer(apiSrv)
if err := link.Close(); err != nil {
log.Printf("UART close: %v", err)
}
})
go runPoller(link, portName, hub, streamCtl, tapCtl, *interval, stop) go runPoller(link, portName, hub, streamCtl, tapCtl, *interval, stop)
go runBatteryPoller(link, hub, 5*time.Second, stop) go runBatteryPoller(link, hub, 5*time.Second, stop)
go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop) go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop)
var apiSrv *http.Server
if *apiAddr != "" { if *apiAddr != "" {
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop) apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop)
defer shutdownAPIServer(apiSrv)
} }
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -81,5 +90,14 @@ func runServe(portName string, baud int, args []string) error {
if *apiAddr == "" { if *apiAddr == "" {
log.Printf("external API disabled (-api-addr \"\")") log.Printf("external API disabled (-api-addr \"\")")
} }
return http.ListenAndServe(*addr, mux)
dashSrv = &http.Server{Addr: *addr, Handler: mux}
go func() {
if err := dashSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("dashboard server: %v", err)
}
}()
waitForShutdown()
return nil
} }
+38 -20
View File
@@ -101,7 +101,7 @@ func (h *wsHub) setState(st DashboardState) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -112,7 +112,7 @@ func (h *wsHub) register(c *websocket.Conn) {
h.mu.Unlock() h.mu.Unlock()
if data, err := json.Marshal(snap); err == nil { if data, err := json.Marshal(snap); err == nil {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -122,6 +122,30 @@ func (h *wsHub) unregister(c *websocket.Conn) {
h.mu.Unlock() h.mu.Unlock()
} }
// writeJSON sends to one client; removes it on error or panic (closed connection).
func (h *wsHub) writeJSON(c *websocket.Conn, data []byte) {
defer func() {
if recover() != nil {
h.unregister(c)
}
}()
if err := c.WriteMessage(websocket.TextMessage, data); err != nil {
h.unregister(c)
}
}
func (h *wsHub) broadcastJSON(data []byte) {
h.mu.RLock()
conns := make([]*websocket.Conn, 0, len(h.clients))
for c := range h.clients {
conns = append(conns, c)
}
h.mu.RUnlock()
for _, c := range conns {
h.writeJSON(c, data)
}
}
func applyAccelSamples(clients []ClientView, samples []*pb.AccelSample) []ClientView { func applyAccelSamples(clients []ClientView, samples []*pb.AccelSample) []ClientView {
if len(samples) == 0 { if len(samples) == 0 {
return clients return clients
@@ -338,7 +362,7 @@ func (h *wsHub) patchClientAccelStream(clientID uint32, enabled bool) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -397,7 +421,7 @@ func (h *wsHub) patchLiveStream(enabled bool) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -430,7 +454,7 @@ func (h *wsHub) patchClientTapNotify(clientID uint32, single, doubleTap, triple
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -455,7 +479,7 @@ func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -479,25 +503,16 @@ func (h *wsHub) mergeTap(events []*pb.TapEvent) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
func (h *wsHub) broadcastRaw(v any) { func (h *wsHub) broadcastRaw(v any) {
h.mu.RLock()
conns := make([]*websocket.Conn, 0, len(h.clients))
for c := range h.clients {
conns = append(conns, c)
}
h.mu.RUnlock()
data, err := json.Marshal(v) data, err := json.Marshal(v)
if err != nil { if err != nil {
return return
} }
for _, c := range conns { h.broadcastJSON(data)
_ = c.WriteMessage(websocket.TextMessage, data)
}
} }
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState { func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState {
@@ -575,6 +590,9 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
func applyBatteryToState(link *managedSerial, st *DashboardState) { func applyBatteryToState(link *managedSerial, st *DashboardState) {
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true}) bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
if errors.Is(err, errUARTBusy) {
return
}
if err != nil { if err != nil {
log.Printf("battery poll: %v", err) log.Printf("battery poll: %v", err)
return return
@@ -602,7 +620,7 @@ func (h *wsHub) mergeBattery(samples []batterySampleJSON) {
return return
} }
for _, c := range conns { for _, c := range conns {
_ = c.WriteMessage(websocket.TextMessage, data) h.writeJSON(c, data)
} }
} }
@@ -615,11 +633,11 @@ func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, s
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
if hub.clientCount() == 0 { if !link.IsConnected() {
continue continue
} }
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true}) bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
if err != nil { if errors.Is(err, errUARTBusy) || err != nil {
continue continue
} }
hub.mergeBattery(batterySamplesFromPB(bat.GetSamples())) hub.mergeBattery(batterySamplesFromPB(bat.GetSamples()))
+84
View File
@@ -96,6 +96,7 @@ Body:
| `digit` | `digit` 010 | | `digit` | `digit` 010 |
| `blink` | `blink_ms`, `blink_count` | | `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod | | `find-me` | Locate pod |
| `battery-low` | First 4 LEDs red at ~10 % for 5 s (UV indicator test) |
Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast. Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.
@@ -216,6 +217,54 @@ Content-Type: application/json
{"client_id": 16, "seq": 42} {"client_id": 16, "seq": 42}
``` ```
### Echo ping (ESP-NOW round-trip latency)
```http
POST /api/echo-ping
Content-Type: application/json
{"client_id": 16}
```
`client_id` must be a registered slave id (`> 0`). The host sends a microsecond timestamp; the master forwards it over ESP-NOW and the slave echoes it back unchanged.
**Flow:** Host → UART → `cmd_espnow_echo_ping``ESPNOW_ECHO_PING` (with `master_time_us` from `esp_timer_get_time()`) → Slave → `ESPNOW_ECHO_PONG` → Master `recv_cb` → UART response.
**Response fields:**
| Field | Unit | Meaning |
|-------|------|---------|
| `success` | — | `true` if pong received within 500 ms |
| `client_id` | — | Echo of request |
| `timestamp_us` | µs (Unix) | Echoed host timestamp; must match request on success |
| `rtt_ms` | ms | **Host-side** round-trip: goTool send → UART response (full chain incl. USB serial) |
| `esp_rtt_us` | µs | **Master-side** ESP-NOW only: `esp_timer_get_time()` delta from ping send to pong recv |
`esp_rtt_us` is the raw firmware value (microseconds, not converted). The web dashboard displays it converted to milliseconds for readability (`esp_rtt_us / 1000`, 3 decimal places). The success banner stays visible for at least 5 seconds.
```json
{
"success": true,
"client_id": 16,
"timestamp_us": 1717654321123456,
"rtt_ms": 49.729,
"esp_rtt_us": 18234
}
```
On failure (`success: false`), `esp_rtt_us` is omitted; `rtt_ms` still reflects the host round-trip. HTTP 503 if UART exchange fails (e.g. timeout, client not in registry).
**CLI:**
```bash
go run . -port /dev/ttyUSB0 echo-ping -client 16
```
Example output:
```
echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234
```
### Find me ### Find me
```http ```http
@@ -234,6 +283,39 @@ Content-Type: application/json
{"client_id": 16} {"client_id": 16}
``` ```
### Log level (master ESP-IDF console)
Runtime get/set of the **global** log filter on the master (`esp_log_level_set("*", …)`). Output goes to **UART0** (USB debug, 115200), not the host protocol UART (GPIO 2/3, 921600).
```http
GET /api/log-level
POST /api/log-level
Content-Type: application/json
{"write": true, "level": 3}
```
| `level` | Meaning |
|---------|---------|
| 0 | None (no log output) |
| 1 | Error |
| 2 | Warn |
| 3 | Info |
| 4 | Debug |
| 5 | Verbose |
```json
{"success": true, "level": 3}
```
**CLI:**
```bash
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 3
```
Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**.
### OTA (master UART upload) ### OTA (master UART upload)
```http ```http
@@ -280,5 +362,7 @@ Same as external API:
| Einzelner Slave | `client_id: <id>` | | Einzelner Slave | `client_id: <id>` |
| Alle Slaves deadzone | `all_clients` + `slaves_only` on POST | | Alle Slaves deadzone | `all_clients` + `slaves_only` on POST |
| Unicast test | `POST /api/unicast-test` | | Unicast test | `POST /api/unicast-test` |
| Echo ping | `POST /api/echo-ping` (per-slave latency; UI shows Host ms + ESP ms) |
| Master log level | `GET` / `POST /api/log-level` or CLI `log-level` |
| Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` | | Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` |
| Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API | | Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API |
+121 -216
View File
@@ -1,62 +1,63 @@
# WebSocket API # WebSocket API
`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes. External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string).
| URL | Port (default) | Role | Start with `go run . -port /dev/ttyUSB0 serve`.
|-----|----------------|------|
| `ws://localhost:8080/ws` | Dashboard (`-addr`) | Server → client only: full `DashboardState` JSON (~2 s poll + live-stream accel/tap) |
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **accel** / **tap** push streams |
Disable the external server with `-api-addr ""`.
CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints: [`API_REST.md`](API_REST.md).
--- ---
## External API (`:8081/ws`) ## Connection flow
### Connection flow 1. Connect → server sends `hello` (push off; defaults and command list).
2. Send JSON commands → one reply per message (`*_status` or `client_list`).
3. After `set_stream` with `enable: true`, server may push `input` messages without a prior command.
1. Connect → server sends **`hello`** (receive off; lists available commands). Commands and pushes share one socket — always branch on `type`.
2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command).
3. After `set_stream` / `set_tap_stream` with `enable: true`, the server may send **`accel`** and/or **`tap`** messages **without** a prior command (push stream).
Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error). On disconnect, `set_stream` state for that socket is dropped. Firmware settings (`set_input_stream`, `set_tap_notify`) stay on the master until changed.
### Two layers (accel and tap) ---
| Layer | Commands | Effect | ## Two layers (firmware vs host)
|-------|----------|--------|
| **Firmware (ESP-NOW)** | `set_accel_stream`, `set_tap_notify` | Per `client_id`: slave sends accel or tap kinds to the master |
| **This connection (host)** | `set_stream`, `set_tap_stream` | Whether **you** receive push JSON and at what rate (`interval_ms`, 1 ms … 10 s) |
- **Accel UART polling** runs only if at least one connection has `receive_accel: true` **and** at least one slave streams accel (`set_accel_stream` or dashboard).
- **Tap UART polling** runs only if at least one connection has `receive_tap: true` (`set_tap_stream`). `set_tap_notify` alone does **not** poll. | Layer | Commands | Effect |
| ------------------ | ------------------------------------ | ------------------------------------------------------------------ |
| Firmware (ESP-NOW) | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel and/or tap events to the master |
| This connection | `set_stream` | Whether you receive push JSON on this socket |
UART polling runs only when at least one connection has `receive_input: true` **and** at least one slave streams input or has tap notify enabled. `set_tap_notify` alone does not enable push — you still need `set_stream`.
### Push timing (per connection)
| Field | Where | Meaning |
| ------------- | -------------------------------------- | ------------------------------------------------------------ |
| `interval_ms` | `hello`, `set_stream`, `stream_status` | Minimum ms between `input` pushes on this socket (1 … 10000) |
| `pre_fetch` | `set_stream`, `stream_status` | Ms before each push when the host starts the UART cache read |
Global UART poll interval = minimum `interval_ms` among all connections with push enabled.
Typical sequence: Typical sequence:
1. `list_clients` → slave IDs 1. `list_clients` → slave IDs
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed 2. Per slave: `set_input_stream` and/or `set_tap_notify`
3. `set_stream` and/or `set_tap_stream` with `"enable": true` 3. `set_stream` with `"enable": true`
4. Read push messages in a loop 4. Read `input` messages; filter by `client_id` in your app (no per-slave filter on the wire)
There is **no per-slave filter** on push messages: each `accel` contains all cached slaves; each `tap` contains all visible events. Filter by `client_id` in your app.
--- ---
## Push stream messages ## Push: `input`
These are the samples you get after enabling receive. Interval is per WebSocket connection; the server UART poll uses the **minimum** `interval_ms` among all subscribers that want accel or tap. Combines latest accel cache and visible tap state for every slave slot on the master.
### `accel` (type `"accel"`) **Success:**
Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection.
**Success** — all slaves with a cache entry on the master (not only those with `valid: true`):
```json ```json
{ {
"type": "accel", "type": "input",
"t": 1716900123456789012, "t": 1716900123456789012,
"success": true, "success": true,
"clients": [ "clients": [
@@ -66,7 +67,9 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
"x": 12, "x": 12,
"y": -34, "y": -34,
"z": 16384, "z": 16384,
"age_ms": 8 "accel_age_ms": 8,
"tap_kind": "single",
"tap_age_ms": 3
}, },
{ {
"client_id": 42, "client_id": 42,
@@ -76,96 +79,49 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
} }
``` ```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
| `success` | `true` if `CACHE_STATUS` succeeded |
| `clients[]` | One entry per slave slot in the master cache |
| `client_id` | ESP-NOW client id (same as `list_clients`) |
| `valid` | `false` if no sample yet or stale; omit `x`/`y`/`z` when false |
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g scale on the pod) |
| `age_ms` | Milliseconds since the master received this sample |
**Failure** (e.g. UART busy): | Field | Meaning |
| -------------- | -------------------------------------------------------------------- |
| `t` | Unix timestamp in nanoseconds when the host read the cache |
| `success` | `true` if `CACHE_STATUS` succeeded |
| `clients[]` | One entry per slave slot (includes invalid/stale entries) |
| `client_id` | Same id as in `list_clients` |
| `valid` | `false` if no accel sample yet or stale; omit `x`/`y`/`z` when false |
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g) |
| `accel_age_ms` | Ms since the master received this accel sample |
| `tap_kind` | `"single"`, `"double"`, or `"triple"`; omit when no recent tap |
| `tap_age_ms` | Ms since tap in master cache; omit with `tap_kind` |
Tap events stay visible for `tap_display_min_ms` (2000, in `hello`) after the API first saw them.
**Failure** (no `clients` array):
```json ```json
{ {"type":"input","t":1716900123456789012,"success":false,"error":"uart busy"}
"type": "accel",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
```
No `clients` array on failure.
### `tap` (type `"tap"`)
Sent only when `set_tap_stream` has `enable: true` and there is at least one event to show.
Events appear when the master cache reports a new tap. Each event stays in push payloads for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw it, even if the hardware age grows.
**Success**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": true,
"events": [
{
"client_id": 16,
"valid": true,
"kind": "single",
"age_ms": 3,
"shown_at_ms": 1717000000123
}
]
}
```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** (poll time) |
| `events[]` | All taps currently “on screen” for the API |
| `client_id` | Slave that tapped |
| `kind` | `"single"`, `"double"`, or `"triple"` |
| `age_ms` | Age in the master cache when read |
| `shown_at_ms` | Unix **milliseconds** when this host first included the event |
If no events are visible, **no** `tap` message is sent on that tick (unlike accel, which can send empty `clients` only on success with cache data).
**Failure**:
```json
{
"type": "tap",
"t": 1716900123456789012,
"success": false,
"error": "uart busy"
}
``` ```
--- ---
## Commands (request → response) ## Commands
Send one JSON object per message. Field `type` selects the command. One JSON object per message; field `type` selects the command.
### `hello` (server → client, on connect) **Errors:** Replies use the matching response `type`. On failure: `success: false` (or omitted) and `"error": "…"`. Malformed JSON or unknown `type``stream_status` with `error`.
### `hello` (server → client)
```json ```json
{ {
"type": "hello", "type": "hello",
"serial_port": "/dev/ttyUSB0", "serial_port": "/dev/ttyUSB0",
"interval_ms": 16, "interval_ms": 16,
"pre_fetch_ms": 2,
"tap_display_min_ms": 2000, "tap_display_min_ms": 2000,
"note": "set_tap_notify configures slave S/D/T only; set_tap_stream enables tap polling/push",
"commands": [ "commands": [
"list_clients", "list_clients",
"set_stream", "get_stream", "set_stream", "get_stream",
"set_accel_stream", "get_accel_stream", "set_input_stream", "get_input_stream",
"set_tap_stream", "get_tap_stream",
"set_tap_notify", "get_tap_notify", "set_tap_notify", "get_tap_notify",
"set_led_ring", "get_battery" "set_led_ring", "get_battery"
] ]
@@ -186,12 +142,8 @@ Response `client_list`:
{ {
"id": 16, "id": 16,
"mac": "aa:bb:cc:dd:ee:10", "mac": "aa:bb:cc:dd:ee:10",
"version": 1,
"available": true, "available": true,
"used": true, "input_stream": false,
"last_ping": 1234,
"last_success_ping": 1200,
"accel_stream": false,
"tap_notify_single": false, "tap_notify_single": false,
"tap_notify_double": false, "tap_notify_double": false,
"tap_notify_triple": false "tap_notify_triple": false
@@ -200,149 +152,102 @@ Response `client_list`:
} }
``` ```
### `set_stream` / `get_stream` (receive accel on this connection) Also per client: `version`, `used`, `last_ping`, `last_success_ping`.
### `set_stream` / `get_stream`
```json ```json
{"type":"set_stream","enable":true,"interval_ms":32} {"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2}
{"type":"get_stream"} {"type":"get_stream"}
``` ```
Response `stream_status`: Response `stream_status`:
```json ```json
{"type":"stream_status","receive_accel":true,"interval_ms":32,"success":true} {"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true}
``` ```
### `set_accel_stream` / `get_accel_stream` (firmware, per slave) ### `set_input_stream` / `get_input_stream` (firmware)
`client_id` required (> 0). `client_id` required (> 0).
```json ```json
{"type":"set_accel_stream","client_id":16,"enable":true} {"type":"set_input_stream","client_id":16,"enable":true}
{"type":"get_accel_stream","client_id":16} {"type":"get_input_stream","client_id":16}
``` ```
Response `accel_stream_status`: Response `input_stream_status`:
```json ```json
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true} {"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
``` ```
### `set_tap_stream` / `get_tap_stream` (receive tap on this connection) ### `set_tap_notify` / `get_tap_notify` (firmware)
```json Set requires `single`, `double_tap`, `triple` per client, or `"all_clients": true` for broadcast.
{"type":"set_tap_stream","enable":true,"interval_ms":16}
{"type":"get_tap_stream"}
```
Response `tap_stream_status`:
```json
{"type":"tap_stream_status","receive_tap":true,"interval_ms":16,"success":true}
```
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
Per client: `single`, `double_tap`, `triple` required on set.
```json ```json
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false} {"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
{"type":"get_tap_notify","client_id":16}
``` ```
Broadcast: `"all_clients": true` with the three booleans.
Response `tap_notify_status`: Response `tap_notify_status`:
```json ```json
{ {"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false}
"type": "tap_notify_status",
"client_id": 16,
"success": true,
"single": true,
"double_tap": false,
"triple": false
}
``` ```
### `set_led_ring` ### `set_led_ring`
Same JSON body as [`POST /api/led-ring`](API_REST.md#led-ring) with `"type":"set_led_ring"` added. Reply: `led_ring_status`. ```json
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
{"type":"set_led_ring","mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
{"type":"set_led_ring","mode":"find-me","all_clients":true,"slaves_only":true}
```
| Request `mode` | Notes |
| -------------- | --------------------------- |
| `clear` | Turn off |
| `color` | Full ring RGB + `intensity` |
| `progress` | `progress` 0100 |
| `digit` | `digit` 010 |
| `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod |
| `battery-low` | UV indicator (4 LEDs, 5 s) |
Target: `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`).
Response `led_ring_status``mode` is numeric: 0=clear, 1=progress, 2=digit, 3=blink, 4=find-me, 5=color, 6=battery-low.
```json
{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1}
```
### `get_battery` ### `get_battery`
Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients. Slaves push battery every 30 s; this reads the master cache. Default: all clients.
Reply: `battery_status` with `samples[]` (see REST doc). ```json
{"type":"get_battery","all_clients":true}
--- {"type":"get_battery","client_id":16}
## Examples
### Accel stream
```python
import asyncio, json, websockets
async def main():
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws:
print(await ws.recv()) # hello
await ws.send(json.dumps({"type": "list_clients"}))
clients = json.loads(await ws.recv())["clients"]
for c in clients:
if not c.get("available"):
continue
await ws.send(json.dumps({
"type": "set_accel_stream", "client_id": c["id"], "enable": True
}))
await ws.recv() # accel_stream_status
await ws.send(json.dumps({"type": "set_stream", "enable": True, "interval_ms": 16}))
await ws.recv() # stream_status
while True:
msg = json.loads(await ws.recv())
if msg.get("type") != "accel":
continue
if not msg.get("success"):
print("error:", msg.get("error"))
continue
for c in msg.get("clients", []):
if c.get("valid"):
print(c["client_id"], c["x"], c["y"], c["z"], "age", c.get("age_ms"))
asyncio.run(main())
``` ```
### Tap stream Response `battery_status`:
```python ```json
import asyncio, json, websockets {
"type": "battery_status",
async def main(): "success": true,
async with websockets.connect("ws://127.0.0.1:8081/ws") as ws: "samples": [
print(await ws.recv()) # hello {
await ws.send(json.dumps({ "client_id": 16,
"type": "set_tap_notify", "client_id": 16, "lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
"single": True, "double_tap": False, "triple": False "lipo2": {"valid": false},
})) "age_ms": 1200
await ws.recv() # tap_notify_status }
await ws.send(json.dumps({"type": "set_tap_stream", "enable": True, "interval_ms": 16})) ]
await ws.recv() # tap_stream_status }
while True:
msg = json.loads(await ws.recv())
if msg.get("type") == "tap" and msg.get("events"):
for e in msg["events"]:
print(e["client_id"], e["kind"], "age", e.get("age_ms"))
asyncio.run(main())
``` ```
---
## Dashboard WebSocket (`:8080/ws`)
Read-only from the browsers perspective: the server pushes JSON whenever state changes. Clients do not send commands on this socket (messages are ignored).
Payload shape: `DashboardState``updated_at`, `serial_port`, `uart_connected`, `live_stream`, `master`, `clients[]` (id, mac, accel, tap notify flags, battery, etc.). Accel/tap samples appear here when **Live stream** is enabled in the UI (`PUT /api/live-stream`).
During OTA, additional messages with `"type":"ota_progress"` may appear on the same socket.
Configure slaves via REST on `:8080` ([`API_REST.md`](API_REST.md)), not via this WebSocket.
+4 -1
View File
@@ -14,6 +14,7 @@ const (
ledRingModeBlink = 3 ledRingModeBlink = 3
ledRingModeFindMe = 4 ledRingModeFindMe = 4
ledRingModeColor = 5 ledRingModeColor = 5
ledRingModeBatteryLow = 6
) )
type ledRingAPIRequest struct { type ledRingAPIRequest struct {
@@ -56,8 +57,10 @@ func ledRingModeFromString(s string) (uint32, error) {
return ledRingModeBlink, nil return ledRingModeBlink, nil
case "find-me", "find_me", "findme": case "find-me", "find_me", "findme":
return ledRingModeFindMe, nil return ledRingModeFindMe, nil
case "battery-low", "battery_low", "batterylow":
return ledRingModeBatteryLow, nil
default: default:
return 0, fmt.Errorf("unknown mode %q (clear, color, progress, digit, blink, find-me)", s) return 0, fmt.Errorf("unknown mode %q (clear, color, progress, digit, blink, find-me, battery-low)", s)
} }
} }
+10 -2
View File
@@ -19,13 +19,15 @@ func usage() {
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n") fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one UART round-trip)\n") fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one UART round-trip)\n")
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n") fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
fmt.Fprintf(os.Stderr, " echo-ping ESP-NOW timestamp echo round-trip to one slave\n")
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n") fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n") fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n") fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n") fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0100%%, rgb, intensity)\n") fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n") fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n\n") fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n")
fmt.Fprintf(os.Stderr, " log-level get/set master ESP-IDF log level (global)\n\n")
flag.PrintDefaults() flag.PrintDefaults()
} }
@@ -52,7 +54,7 @@ func main() {
os.Exit(2) os.Exit(2)
} }
runErr = runServe(*portName, *baud, flag.Args()[1:]) runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress": case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
if *portName == "" { if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd) fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage() usage()
@@ -62,6 +64,8 @@ func main() {
if err != nil { if err != nil {
log.Fatalf("open serial: %v", err) log.Fatalf("open serial: %v", err)
} }
registerShutdown(func() { _ = sp.Close() })
enableShutdownOnInterrupt()
defer sp.Close() defer sp.Close()
switch cmd { switch cmd {
case "version": case "version":
@@ -76,12 +80,16 @@ func main() {
runErr = runCacheStatus(sp) runErr = runCacheStatus(sp)
case "unicast-test", "unicast_test": case "unicast-test", "unicast_test":
runErr = runUnicastTest(sp, flag.Args()[1:]) runErr = runUnicastTest(sp, flag.Args()[1:])
case "echo-ping", "echo_ping":
runErr = runEchoPing(sp, flag.Args()[1:])
case "led-ring", "led_ring": case "led-ring", "led_ring":
runErr = runLedRing(sp, flag.Args()[1:]) runErr = runLedRing(sp, flag.Args()[1:])
case "find-me", "find_me": case "find-me", "find_me":
runErr = runFindMe(sp, flag.Args()[1:]) runErr = runFindMe(sp, flag.Args()[1:])
case "restart": case "restart":
runErr = runRestart(sp, flag.Args()[1:]) runErr = runRestart(sp, flag.Args()[1:])
case "log-level", "log_level":
runErr = runLogLevel(sp, flag.Args()[1:])
case "ota": case "ota":
runErr = runOTA(sp, flag.Args()[1:]) runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress": case "ota-progress", "ota_progress":
+152 -66
View File
@@ -2,7 +2,6 @@ package main
import ( import (
"fmt" "fmt"
"log"
"time" "time"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
@@ -21,6 +20,9 @@ const (
otaDistQueryInterval = 500 * time.Millisecond otaDistQueryInterval = 500 * time.Millisecond
otaDistQueryTimeout = 2 * time.Second otaDistQueryTimeout = 2 * time.Second
otaDistEmitMinInterval = 150 * time.Millisecond otaDistEmitMinInterval = 150 * time.Millisecond
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
otaHostChunkPace = 3 * time.Millisecond
otaBlockMaxRetries = 3
) )
const ( const (
@@ -69,16 +71,45 @@ const (
) )
func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
push := func(phase, msg string) {
if onProgress == nil {
return
}
onProgress(OTAProgress{
Type: "ota_progress", Phase: phase, Step: otaStepMaster,
Percent: 0, Message: msg, MasterMessage: msg,
})
}
push("preparing", "UART wird vorbereitet…")
// Block until the UART is free, then hold m.mu for the entire upload so
// dashboard/API polling cannot interleave on the serial port.
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() if m.otaActive {
err := runOTAOnPortUnlocked(m, firmware, onProgress) m.mu.Unlock()
return errOTAInProgress
}
m.otaActive = true
if m.sp == nil {
if err := m.openLocked(); err != nil {
m.otaActive = false
m.mu.Unlock()
push("error", err.Error())
return err
}
}
sp := m.sp
err := runOTAOnPortUnlocked(sp, firmware, onProgress)
if err != nil { if err != nil {
m.invalidateLocked(err) m.invalidateLocked(err)
} }
m.otaActive = false
m.mu.Unlock()
return err return err
} }
func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgressFn) error { func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
if len(firmware) == 0 { if len(firmware) == 0 {
return fmt.Errorf("empty firmware") return fmt.Errorf("empty firmware")
} }
@@ -120,32 +151,31 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
onProgress(p) onProgress(p)
} }
if m.sp == nil { if err := sp.port.SetReadTimeout(readTimeout); err != nil {
if err := m.openLocked(); err != nil {
notify("error", "", 0, err.Error())
return err
}
}
sp := m.sp
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
notify("error", "", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
defer sp.port.SetReadTimeout(readTimeout)
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize)) notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
flushSerialInput(sp)
if err := writeUartMessage(sp, &pb.UartMessage{ if err := writeUartMessage(sp, &pb.UartMessage{
Type: pb.MessageType_OTA_START, Type: pb.MessageType_OTA_START,
Payload: &pb.UartMessage_OtaStart{ Payload: &pb.UartMessage_OtaStart{
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)}, OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
}, },
}, false); err != nil { }); err != nil {
notify("error", "", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
notify("error", "", 0, err.Error())
return err
}
defer func() { _ = sp.port.SetReadTimeout(readTimeout) }()
ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) { ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) {
notify("preparing", otaStepMaster, 2, msg) notify("preparing", otaStepMaster, 2, msg)
}) })
@@ -162,52 +192,83 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
var seq uint32 var seq uint32
for offset := 0; offset < imageSize; { for offset := 0; offset < imageSize; {
bytesInBlock := 0 blockStart := offset
for bytesInBlock < otaFlashBlockSize && offset < imageSize { blockStartSeq := seq
n := otaHostChunkSize
room := otaFlashBlockSize - bytesInBlock
if n > room {
n = room
}
if offset+n > imageSize {
n = imageSize - offset
}
chunk := firmware[offset : offset+n]
if err := writeUartMessage(sp, &pb.UartMessage{ sendBlock := func() (fullBlock bool, err error) {
Type: pb.MessageType_OTA_PAYLOAD, bytesInBlock := 0
Payload: &pb.UartMessage_OtaPayload{ for bytesInBlock < otaFlashBlockSize && offset < imageSize {
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk}, n := otaHostChunkSize
}, room := otaFlashBlockSize - bytesInBlock
}, false); err != nil { if n > room {
notify("error", "", 0, err.Error()) n = room
return err }
} if offset+n > imageSize {
seq++ n = imageSize - offset
offset += n }
bytesInBlock += n chunk := firmware[offset : offset+n]
pct := offset * 100 / imageSize if err := writeUartMessage(sp, &pb.UartMessage{
if pct > 99 { Type: pb.MessageType_OTA_PAYLOAD,
pct = 99 Payload: &pb.UartMessage_OtaPayload{
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
},
}); err != nil {
return false, err
}
time.Sleep(otaHostChunkPace)
seq++
offset += n
bytesInBlock += n
pct := offset * 100 / imageSize
if pct > 99 {
pct = 99
}
notify("uploading", otaStepMaster, pct,
fmt.Sprintf("Master: %d / %d bytes", offset, imageSize))
} }
notify("uploading", otaStepMaster, pct, fmt.Sprintf("Master: %d / %d bytes", offset, imageSize)) return bytesInBlock == otaFlashBlockSize, nil
} }
if bytesInBlock == otaFlashBlockSize { fullBlock, err := sendBlock()
st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil) if err != nil {
if err != nil { notify("error", "", 0, err.Error())
notify("error", "", 0, err.Error()) return err
return err
}
pct := offset * 100 / imageSize
if pct > 99 {
pct = 99
}
notify("uploading", otaStepMaster, pct,
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
OTAProgress{Bytes: st.GetBytesWritten()})
} }
if !fullBlock {
continue
}
var st *pb.OtaStatusPayload
var ackErr error
for attempt := 0; attempt < otaBlockMaxRetries; attempt++ {
if attempt > 0 {
offset = blockStart
seq = blockStartSeq
if _, err := sendBlock(); err != nil {
notify("error", "", 0, err.Error())
return err
}
}
st, ackErr = waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
if ackErr == nil {
break
}
}
if ackErr != nil {
notify("error", "", 0, ackErr.Error())
return ackErr
}
pct := offset * 100 / imageSize
if pct > 99 {
pct = 99
}
notify("uploading", otaStepMaster, pct,
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
OTAProgress{Bytes: st.GetBytesWritten()})
} }
masterPct = 100 masterPct = 100
@@ -219,7 +280,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
Payload: &pb.UartMessage_OtaEnd{ Payload: &pb.UartMessage_OtaEnd{
OtaEnd: &pb.OtaEndPayload{}, OtaEnd: &pb.OtaEndPayload{},
}, },
}, false); err != nil { }); err != nil {
notify("error", "", 0, err.Error()) notify("error", "", 0, err.Error())
return err return err
} }
@@ -333,7 +394,7 @@ func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
}, },
}, },
} }
if err := writeUartMessage(sp, req, false); err != nil { if err := writeUartMessage(sp, req); err != nil {
return nil, err return nil, err
} }
if queryTimeout <= 0 { if queryTimeout <= 0 {
@@ -389,7 +450,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
if err := sp.port.SetReadTimeout(wait); err != nil { if err := sp.port.SetReadTimeout(wait); err != nil {
return nil, err return nil, err
} }
payload, err := uartframe.ReadFrame(sp.port, nil) payload, err := uartframe.ReadFrame(sp.port, nil, wait)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -487,14 +548,11 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
} }
} }
func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error { func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error {
frame, err := encodeUartMessage(msg) frame, err := encodeUartMessage(msg)
if err != nil { if err != nil {
return err return err
} }
if logFrame {
log.Printf("sending %s (%d frame bytes)", msg.Type, len(frame))
}
_, err = sp.port.Write(frame) _, err = sp.port.Write(frame)
return err return err
} }
@@ -505,12 +563,24 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
if time.Now().After(deadline) { if time.Now().After(deadline) {
return nil, fmt.Errorf("timeout waiting for OTA status %d", want) return nil, fmt.Errorf("timeout waiting for OTA status %d", want)
} }
if err := sp.port.SetReadTimeout(time.Until(deadline)); err != nil { readWait := time.Until(deadline)
if readWait > otaStatusPollTimeout {
readWait = otaStatusPollTimeout
}
if err := sp.port.SetReadTimeout(readWait); err != nil {
return nil, err return nil, err
} }
st, err := readOtaStatus(sp) payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
if err != nil { if err != nil {
return nil, err continue
}
msg, err := decodeUartPayload(payload)
if err != nil || msg.GetType() != pb.MessageType_OTA_STATUS {
continue
}
st := msg.GetOtaStatus()
if st == nil {
continue
} }
switch st.GetStatus() { switch st.GetStatus() {
case want: case want:
@@ -526,7 +596,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
} }
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) { func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
payload, err := uartframe.ReadFrame(sp.port, nil) payload, err := uartframe.ReadFrame(sp.port, nil, readTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("read response: %w", err) return nil, fmt.Errorf("read response: %w", err)
} }
@@ -553,6 +623,22 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
return uartframe.EncodeFrame(payload) return uartframe.EncodeFrame(payload)
} }
// flushSerialInput drops stale RX bytes (not full frames — avoids ReadFrame blocking).
func flushSerialInput(sp *serialPort) {
if sp == nil {
return
}
_ = sp.port.SetReadTimeout(10 * time.Millisecond)
buf := make([]byte, 256)
deadline := time.Now().Add(50 * time.Millisecond)
for time.Now().Before(deadline) {
n, err := sp.port.Read(buf)
if n == 0 || err != nil {
return
}
}
}
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) { func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
if len(payload) == 0 { if len(payload) == 0 {
return nil, fmt.Errorf("empty response") return nil, fmt.Errorf("empty response")
+424 -89
View File
@@ -46,6 +46,10 @@ const (
MessageType_TAP_NOTIFY MessageType = 27 MessageType_TAP_NOTIFY MessageType = 27
// * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). // * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence).
MessageType_CACHE_STATUS MessageType = 29 MessageType_CACHE_STATUS MessageType = 29
// * Host → master → slave → master: timestamp echo round-trip (latency test).
MessageType_ESPNOW_ECHO_PING MessageType = 30
// * Host → master: get/set ESP-IDF log level for tag "*" (global).
MessageType_SET_LOG_LEVEL MessageType = 31
) )
// Enum value maps for MessageType. // Enum value maps for MessageType.
@@ -72,6 +76,8 @@ var (
26: "BATTERY_STATUS", 26: "BATTERY_STATUS",
27: "TAP_NOTIFY", 27: "TAP_NOTIFY",
29: "CACHE_STATUS", 29: "CACHE_STATUS",
30: "ESPNOW_ECHO_PING",
31: "SET_LOG_LEVEL",
} }
MessageType_value = map[string]int32{ MessageType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
@@ -95,6 +101,8 @@ var (
"BATTERY_STATUS": 26, "BATTERY_STATUS": 26,
"TAP_NOTIFY": 27, "TAP_NOTIFY": 27,
"CACHE_STATUS": 29, "CACHE_STATUS": 29,
"ESPNOW_ECHO_PING": 30,
"SET_LOG_LEVEL": 31,
} }
) )
@@ -211,6 +219,10 @@ type UartMessage struct {
// *UartMessage_TapNotifyResponse // *UartMessage_TapNotifyResponse
// *UartMessage_CacheStatusRequest // *UartMessage_CacheStatusRequest
// *UartMessage_CacheStatusResponse // *UartMessage_CacheStatusResponse
// *UartMessage_EspnowEchoPingRequest
// *UartMessage_EspnowEchoPingResponse
// *UartMessage_SetLogLevelRequest
// *UartMessage_SetLogLevelResponse
Payload isUartMessage_Payload `protobuf_oneof:"payload"` Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -521,6 +533,42 @@ func (x *UartMessage) GetCacheStatusResponse() *CacheStatusResponse {
return nil return nil
} }
func (x *UartMessage) GetEspnowEchoPingRequest() *EspNowEchoPingRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowEchoPingRequest); ok {
return x.EspnowEchoPingRequest
}
}
return nil
}
func (x *UartMessage) GetEspnowEchoPingResponse() *EspNowEchoPingResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowEchoPingResponse); ok {
return x.EspnowEchoPingResponse
}
}
return nil
}
func (x *UartMessage) GetSetLogLevelRequest() *SetLogLevelRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_SetLogLevelRequest); ok {
return x.SetLogLevelRequest
}
}
return nil
}
func (x *UartMessage) GetSetLogLevelResponse() *SetLogLevelResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_SetLogLevelResponse); ok {
return x.SetLogLevelResponse
}
}
return nil
}
type isUartMessage_Payload interface { type isUartMessage_Payload interface {
isUartMessage_Payload() isUartMessage_Payload()
} }
@@ -641,6 +689,22 @@ type UartMessage_CacheStatusResponse struct {
CacheStatusResponse *CacheStatusResponse `protobuf:"bytes,34,opt,name=cache_status_response,json=cacheStatusResponse,proto3,oneof"` CacheStatusResponse *CacheStatusResponse `protobuf:"bytes,34,opt,name=cache_status_response,json=cacheStatusResponse,proto3,oneof"`
} }
type UartMessage_EspnowEchoPingRequest struct {
EspnowEchoPingRequest *EspNowEchoPingRequest `protobuf:"bytes,35,opt,name=espnow_echo_ping_request,json=espnowEchoPingRequest,proto3,oneof"`
}
type UartMessage_EspnowEchoPingResponse struct {
EspnowEchoPingResponse *EspNowEchoPingResponse `protobuf:"bytes,36,opt,name=espnow_echo_ping_response,json=espnowEchoPingResponse,proto3,oneof"`
}
type UartMessage_SetLogLevelRequest struct {
SetLogLevelRequest *SetLogLevelRequest `protobuf:"bytes,37,opt,name=set_log_level_request,json=setLogLevelRequest,proto3,oneof"`
}
type UartMessage_SetLogLevelResponse struct {
SetLogLevelResponse *SetLogLevelResponse `protobuf:"bytes,38,opt,name=set_log_level_response,json=setLogLevelResponse,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {} func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {} func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@@ -699,6 +763,14 @@ func (*UartMessage_CacheStatusRequest) isUartMessage_Payload() {}
func (*UartMessage_CacheStatusResponse) isUartMessage_Payload() {} func (*UartMessage_CacheStatusResponse) isUartMessage_Payload() {}
func (*UartMessage_EspnowEchoPingRequest) isUartMessage_Payload() {}
func (*UartMessage_EspnowEchoPingResponse) isUartMessage_Payload() {}
func (*UartMessage_SetLogLevelRequest) isUartMessage_Payload() {}
func (*UartMessage_SetLogLevelResponse) isUartMessage_Payload() {}
type Ack struct { type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -2329,8 +2401,132 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0 return 0
} }
// * Host → master: ESP-NOW echo ping to one slave (timestamp echoed back).
type EspNowEchoPingRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
// * Microseconds since Unix epoch (host clock).
TimestampUs uint64 `protobuf:"varint,2,opt,name=timestamp_us,json=timestampUs,proto3" json:"timestamp_us,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowEchoPingRequest) Reset() {
*x = EspNowEchoPingRequest{}
mi := &file_uart_messages_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowEchoPingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowEchoPingRequest) ProtoMessage() {}
func (x *EspNowEchoPingRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowEchoPingRequest.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{27}
}
func (x *EspNowEchoPingRequest) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *EspNowEchoPingRequest) GetTimestampUs() uint64 {
if x != nil {
return x.TimestampUs
}
return 0
}
type EspNowEchoPingResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ClientId uint32 `protobuf:"varint,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
// * Echoed host timestamp from goTool request.
TimestampUs uint64 `protobuf:"varint,3,opt,name=timestamp_us,json=timestampUs,proto3" json:"timestamp_us,omitempty"`
// * esp_timer_get_time() delta from ping send to pong recv (master→slave→master).
EspRttUs uint32 `protobuf:"varint,4,opt,name=esp_rtt_us,json=espRttUs,proto3" json:"esp_rtt_us,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowEchoPingResponse) Reset() {
*x = EspNowEchoPingResponse{}
mi := &file_uart_messages_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowEchoPingResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowEchoPingResponse) ProtoMessage() {}
func (x *EspNowEchoPingResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowEchoPingResponse.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{28}
}
func (x *EspNowEchoPingResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *EspNowEchoPingResponse) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *EspNowEchoPingResponse) GetTimestampUs() uint64 {
if x != nil {
return x.TimestampUs
}
return 0
}
func (x *EspNowEchoPingResponse) GetEspRttUs() uint32 {
if x != nil {
return x.EspRttUs
}
return 0
}
// Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW. // Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color. // mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low.
type LedRingProgressRequest struct { type LedRingProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"` Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"`
@@ -2359,7 +2555,7 @@ type LedRingProgressRequest struct {
func (x *LedRingProgressRequest) Reset() { func (x *LedRingProgressRequest) Reset() {
*x = LedRingProgressRequest{} *x = LedRingProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[27] mi := &file_uart_messages_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2371,7 +2567,7 @@ func (x *LedRingProgressRequest) String() string {
func (*LedRingProgressRequest) ProtoMessage() {} func (*LedRingProgressRequest) ProtoMessage() {}
func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message { func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[27] mi := &file_uart_messages_proto_msgTypes[29]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2384,7 +2580,7 @@ func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressRequest.ProtoReflect.Descriptor instead. // Deprecated: Use LedRingProgressRequest.ProtoReflect.Descriptor instead.
func (*LedRingProgressRequest) Descriptor() ([]byte, []int) { func (*LedRingProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{27} return file_uart_messages_proto_rawDescGZIP(), []int{29}
} }
func (x *LedRingProgressRequest) GetMode() uint32 { func (x *LedRingProgressRequest) GetMode() uint32 {
@@ -2485,7 +2681,7 @@ type LedRingProgressResponse struct {
func (x *LedRingProgressResponse) Reset() { func (x *LedRingProgressResponse) Reset() {
*x = LedRingProgressResponse{} *x = LedRingProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[28] mi := &file_uart_messages_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2497,7 +2693,7 @@ func (x *LedRingProgressResponse) String() string {
func (*LedRingProgressResponse) ProtoMessage() {} func (*LedRingProgressResponse) ProtoMessage() {}
func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message { func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[28] mi := &file_uart_messages_proto_msgTypes[30]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2510,7 +2706,7 @@ func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressResponse.ProtoReflect.Descriptor instead. // Deprecated: Use LedRingProgressResponse.ProtoReflect.Descriptor instead.
func (*LedRingProgressResponse) Descriptor() ([]byte, []int) { func (*LedRingProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{28} return file_uart_messages_proto_rawDescGZIP(), []int{30}
} }
func (x *LedRingProgressResponse) GetSuccess() bool { func (x *LedRingProgressResponse) GetSuccess() bool {
@@ -2565,7 +2761,7 @@ type EspNowFindMeRequest struct {
func (x *EspNowFindMeRequest) Reset() { func (x *EspNowFindMeRequest) Reset() {
*x = EspNowFindMeRequest{} *x = EspNowFindMeRequest{}
mi := &file_uart_messages_proto_msgTypes[29] mi := &file_uart_messages_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2577,7 +2773,7 @@ func (x *EspNowFindMeRequest) String() string {
func (*EspNowFindMeRequest) ProtoMessage() {} func (*EspNowFindMeRequest) ProtoMessage() {}
func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message { func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[29] mi := &file_uart_messages_proto_msgTypes[31]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2590,7 +2786,7 @@ func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead.
func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) { func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{29} return file_uart_messages_proto_rawDescGZIP(), []int{31}
} }
func (x *EspNowFindMeRequest) GetClientId() uint32 { func (x *EspNowFindMeRequest) GetClientId() uint32 {
@@ -2610,7 +2806,7 @@ type EspNowFindMeResponse struct {
func (x *EspNowFindMeResponse) Reset() { func (x *EspNowFindMeResponse) Reset() {
*x = EspNowFindMeResponse{} *x = EspNowFindMeResponse{}
mi := &file_uart_messages_proto_msgTypes[30] mi := &file_uart_messages_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2622,7 +2818,7 @@ func (x *EspNowFindMeResponse) String() string {
func (*EspNowFindMeResponse) ProtoMessage() {} func (*EspNowFindMeResponse) ProtoMessage() {}
func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message { func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[30] mi := &file_uart_messages_proto_msgTypes[32]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2635,7 +2831,7 @@ func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead.
func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) { func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{30} return file_uart_messages_proto_rawDescGZIP(), []int{32}
} }
func (x *EspNowFindMeResponse) GetSuccess() bool { func (x *EspNowFindMeResponse) GetSuccess() bool {
@@ -2662,7 +2858,7 @@ type RestartRequest struct {
func (x *RestartRequest) Reset() { func (x *RestartRequest) Reset() {
*x = RestartRequest{} *x = RestartRequest{}
mi := &file_uart_messages_proto_msgTypes[31] mi := &file_uart_messages_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2674,7 +2870,7 @@ func (x *RestartRequest) String() string {
func (*RestartRequest) ProtoMessage() {} func (*RestartRequest) ProtoMessage() {}
func (x *RestartRequest) ProtoReflect() protoreflect.Message { func (x *RestartRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[31] mi := &file_uart_messages_proto_msgTypes[33]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2687,7 +2883,7 @@ func (x *RestartRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RestartRequest.ProtoReflect.Descriptor instead.
func (*RestartRequest) Descriptor() ([]byte, []int) { func (*RestartRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{31} return file_uart_messages_proto_rawDescGZIP(), []int{33}
} }
func (x *RestartRequest) GetClientId() uint32 { func (x *RestartRequest) GetClientId() uint32 {
@@ -2707,7 +2903,7 @@ type RestartResponse struct {
func (x *RestartResponse) Reset() { func (x *RestartResponse) Reset() {
*x = RestartResponse{} *x = RestartResponse{}
mi := &file_uart_messages_proto_msgTypes[32] mi := &file_uart_messages_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2719,7 +2915,7 @@ func (x *RestartResponse) String() string {
func (*RestartResponse) ProtoMessage() {} func (*RestartResponse) ProtoMessage() {}
func (x *RestartResponse) ProtoReflect() protoreflect.Message { func (x *RestartResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[32] mi := &file_uart_messages_proto_msgTypes[34]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2732,7 +2928,7 @@ func (x *RestartResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RestartResponse.ProtoReflect.Descriptor instead.
func (*RestartResponse) Descriptor() ([]byte, []int) { func (*RestartResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{32} return file_uart_messages_proto_rawDescGZIP(), []int{34}
} }
func (x *RestartResponse) GetSuccess() bool { func (x *RestartResponse) GetSuccess() bool {
@@ -2749,6 +2945,112 @@ func (x *RestartResponse) GetClientId() uint32 {
return 0 return 0
} }
// * Host → master: read/write global log level (esp_log_level_set("*", …)).
type SetLogLevelRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Write bool `protobuf:"varint,1,opt,name=write,proto3" json:"write,omitempty"`
// * esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE
Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetLogLevelRequest) Reset() {
*x = SetLogLevelRequest{}
mi := &file_uart_messages_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetLogLevelRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetLogLevelRequest) ProtoMessage() {}
func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead.
func (*SetLogLevelRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{35}
}
func (x *SetLogLevelRequest) GetWrite() bool {
if x != nil {
return x.Write
}
return false
}
func (x *SetLogLevelRequest) GetLevel() uint32 {
if x != nil {
return x.Level
}
return 0
}
type SetLogLevelResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetLogLevelResponse) Reset() {
*x = SetLogLevelResponse{}
mi := &file_uart_messages_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetLogLevelResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetLogLevelResponse) ProtoMessage() {}
func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead.
func (*SetLogLevelResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36}
}
func (x *SetLogLevelResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *SetLogLevelResponse) GetLevel() uint32 {
if x != nil {
return x.Level
}
return 0
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). // Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
type OtaStartPayload struct { type OtaStartPayload struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
@@ -2759,7 +3061,7 @@ type OtaStartPayload struct {
func (x *OtaStartPayload) Reset() { func (x *OtaStartPayload) Reset() {
*x = OtaStartPayload{} *x = OtaStartPayload{}
mi := &file_uart_messages_proto_msgTypes[33] mi := &file_uart_messages_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2771,7 +3073,7 @@ func (x *OtaStartPayload) String() string {
func (*OtaStartPayload) ProtoMessage() {} func (*OtaStartPayload) ProtoMessage() {}
func (x *OtaStartPayload) ProtoReflect() protoreflect.Message { func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[33] mi := &file_uart_messages_proto_msgTypes[37]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2784,7 +3086,7 @@ func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead.
func (*OtaStartPayload) Descriptor() ([]byte, []int) { func (*OtaStartPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{33} return file_uart_messages_proto_rawDescGZIP(), []int{37}
} }
func (x *OtaStartPayload) GetTotalSize() uint32 { func (x *OtaStartPayload) GetTotalSize() uint32 {
@@ -2805,7 +3107,7 @@ type OtaPayload struct {
func (x *OtaPayload) Reset() { func (x *OtaPayload) Reset() {
*x = OtaPayload{} *x = OtaPayload{}
mi := &file_uart_messages_proto_msgTypes[34] mi := &file_uart_messages_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2817,7 +3119,7 @@ func (x *OtaPayload) String() string {
func (*OtaPayload) ProtoMessage() {} func (*OtaPayload) ProtoMessage() {}
func (x *OtaPayload) ProtoReflect() protoreflect.Message { func (x *OtaPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[34] mi := &file_uart_messages_proto_msgTypes[38]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2830,7 +3132,7 @@ func (x *OtaPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead.
func (*OtaPayload) Descriptor() ([]byte, []int) { func (*OtaPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{34} return file_uart_messages_proto_rawDescGZIP(), []int{38}
} }
func (x *OtaPayload) GetSeq() uint32 { func (x *OtaPayload) GetSeq() uint32 {
@@ -2856,7 +3158,7 @@ type OtaEndPayload struct {
func (x *OtaEndPayload) Reset() { func (x *OtaEndPayload) Reset() {
*x = OtaEndPayload{} *x = OtaEndPayload{}
mi := &file_uart_messages_proto_msgTypes[35] mi := &file_uart_messages_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2868,7 +3170,7 @@ func (x *OtaEndPayload) String() string {
func (*OtaEndPayload) ProtoMessage() {} func (*OtaEndPayload) ProtoMessage() {}
func (x *OtaEndPayload) ProtoReflect() protoreflect.Message { func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[35] mi := &file_uart_messages_proto_msgTypes[39]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2881,7 +3183,7 @@ func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead.
func (*OtaEndPayload) Descriptor() ([]byte, []int) { func (*OtaEndPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{35} return file_uart_messages_proto_rawDescGZIP(), []int{39}
} }
// Device → host status (also used as ACK after each 4 KiB written). // Device → host status (also used as ACK after each 4 KiB written).
@@ -2898,7 +3200,7 @@ type OtaStatusPayload struct {
func (x *OtaStatusPayload) Reset() { func (x *OtaStatusPayload) Reset() {
*x = OtaStatusPayload{} *x = OtaStatusPayload{}
mi := &file_uart_messages_proto_msgTypes[36] mi := &file_uart_messages_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2910,7 +3212,7 @@ func (x *OtaStatusPayload) String() string {
func (*OtaStatusPayload) ProtoMessage() {} func (*OtaStatusPayload) ProtoMessage() {}
func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message { func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36] mi := &file_uart_messages_proto_msgTypes[40]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2923,7 +3225,7 @@ func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead.
func (*OtaStatusPayload) Descriptor() ([]byte, []int) { func (*OtaStatusPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36} return file_uart_messages_proto_rawDescGZIP(), []int{40}
} }
func (x *OtaStatusPayload) GetStatus() uint32 { func (x *OtaStatusPayload) GetStatus() uint32 {
@@ -2964,7 +3266,7 @@ type OtaSlaveProgressRequest struct {
func (x *OtaSlaveProgressRequest) Reset() { func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{} *x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[37] mi := &file_uart_messages_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2976,7 +3278,7 @@ func (x *OtaSlaveProgressRequest) String() string {
func (*OtaSlaveProgressRequest) ProtoMessage() {} func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[37] mi := &file_uart_messages_proto_msgTypes[41]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2989,7 +3291,7 @@ func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{37} return file_uart_messages_proto_rawDescGZIP(), []int{41}
} }
func (x *OtaSlaveProgressRequest) GetClientId() uint32 { func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
@@ -3013,7 +3315,7 @@ type OtaSlaveProgressEntry struct {
func (x *OtaSlaveProgressEntry) Reset() { func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{} *x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[38] mi := &file_uart_messages_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3025,7 +3327,7 @@ func (x *OtaSlaveProgressEntry) String() string {
func (*OtaSlaveProgressEntry) ProtoMessage() {} func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[38] mi := &file_uart_messages_proto_msgTypes[42]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3038,7 +3340,7 @@ func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{38} return file_uart_messages_proto_rawDescGZIP(), []int{42}
} }
func (x *OtaSlaveProgressEntry) GetClientId() uint32 { func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
@@ -3089,7 +3391,7 @@ type OtaSlaveProgressResponse struct {
func (x *OtaSlaveProgressResponse) Reset() { func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{} *x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[39] mi := &file_uart_messages_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3101,7 +3403,7 @@ func (x *OtaSlaveProgressResponse) String() string {
func (*OtaSlaveProgressResponse) ProtoMessage() {} func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[39] mi := &file_uart_messages_proto_msgTypes[43]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3114,7 +3416,7 @@ func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{39} return file_uart_messages_proto_rawDescGZIP(), []int{43}
} }
func (x *OtaSlaveProgressResponse) GetActive() bool { func (x *OtaSlaveProgressResponse) GetActive() bool {
@@ -3156,7 +3458,7 @@ var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" + const file_uart_messages_proto_rawDesc = "" +
"\n" + "\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xec\x11\n" + "\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xc0\x14\n" +
"\vUartMessage\x12%\n" + "\vUartMessage\x12%\n" +
"\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" + "\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" +
"\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" + "\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" +
@@ -3191,7 +3493,11 @@ const file_uart_messages_proto_rawDesc = "" +
"\x12tap_notify_request\x18\x1d \x01(\v2\x16.alox.TapNotifyRequestH\x00R\x10tapNotifyRequest\x12I\n" + "\x12tap_notify_request\x18\x1d \x01(\v2\x16.alox.TapNotifyRequestH\x00R\x10tapNotifyRequest\x12I\n" +
"\x13tap_notify_response\x18\x1e \x01(\v2\x17.alox.TapNotifyResponseH\x00R\x11tapNotifyResponse\x12L\n" + "\x13tap_notify_response\x18\x1e \x01(\v2\x17.alox.TapNotifyResponseH\x00R\x11tapNotifyResponse\x12L\n" +
"\x14cache_status_request\x18! \x01(\v2\x18.alox.CacheStatusRequestH\x00R\x12cacheStatusRequest\x12O\n" + "\x14cache_status_request\x18! \x01(\v2\x18.alox.CacheStatusRequestH\x00R\x12cacheStatusRequest\x12O\n" +
"\x15cache_status_response\x18\" \x01(\v2\x19.alox.CacheStatusResponseH\x00R\x13cacheStatusResponseB\t\n" + "\x15cache_status_response\x18\" \x01(\v2\x19.alox.CacheStatusResponseH\x00R\x13cacheStatusResponse\x12V\n" +
"\x18espnow_echo_ping_request\x18# \x01(\v2\x1b.alox.EspNowEchoPingRequestH\x00R\x15espnowEchoPingRequest\x12Y\n" +
"\x19espnow_echo_ping_response\x18$ \x01(\v2\x1c.alox.EspNowEchoPingResponseH\x00R\x16espnowEchoPingResponse\x12M\n" +
"\x15set_log_level_request\x18% \x01(\v2\x18.alox.SetLogLevelRequestH\x00R\x12setLogLevelRequest\x12P\n" +
"\x16set_log_level_response\x18& \x01(\v2\x19.alox.SetLogLevelResponseH\x00R\x13setLogLevelResponseB\t\n" +
"\apayload\"\x05\n" + "\apayload\"\x05\n" +
"\x03Ack\"!\n" + "\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" + "\vEchoPayload\x12\x12\n" +
@@ -3311,7 +3617,16 @@ const file_uart_messages_proto_rawDesc = "" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" + "\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
"\x19EspNowUnicastTestResponse\x12\x18\n" + "\x19EspNowUnicastTestResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"\xc1\x02\n" + "\x03seq\x18\x02 \x01(\rR\x03seq\"W\n" +
"\x15EspNowEchoPingRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\x12!\n" +
"\ftimestamp_us\x18\x02 \x01(\x04R\vtimestampUs\"\x90\x01\n" +
"\x16EspNowEchoPingResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\x12!\n" +
"\ftimestamp_us\x18\x03 \x01(\x04R\vtimestampUs\x12\x1c\n" +
"\n" +
"esp_rtt_us\x18\x04 \x01(\rR\bespRttUs\"\xc1\x02\n" +
"\x16LedRingProgressRequest\x12\x12\n" + "\x16LedRingProgressRequest\x12\x12\n" +
"\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" + "\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" +
"\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\n" + "\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\n" +
@@ -3345,7 +3660,13 @@ const file_uart_messages_proto_rawDesc = "" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"H\n" + "\tclient_id\x18\x01 \x01(\rR\bclientId\"H\n" +
"\x0fRestartResponse\x12\x18\n" + "\x0fRestartResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" + "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"0\n" + "\tclient_id\x18\x02 \x01(\rR\bclientId\"@\n" +
"\x12SetLogLevelRequest\x12\x14\n" +
"\x05write\x18\x01 \x01(\bR\x05write\x12\x14\n" +
"\x05level\x18\x02 \x01(\rR\x05level\"E\n" +
"\x13SetLogLevelResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" +
"\x05level\x18\x02 \x01(\rR\x05level\"0\n" +
"\x0fOtaStartPayload\x12\x1d\n" + "\x0fOtaStartPayload\x12\x1d\n" +
"\n" + "\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\":\n" + "total_size\x18\x01 \x01(\rR\ttotalSize\":\n" +
@@ -3376,7 +3697,7 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" + "\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" +
"\vslave_count\x18\x04 \x01(\rR\n" + "\vslave_count\x18\x04 \x01(\rR\n" +
"slaveCount\x12:\n" + "slaveCount\x12:\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xf1\x02\n" + "\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\x9a\x03\n" +
"\vMessageType\x12\v\n" + "\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" + "\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" + "\x03ACK\x10\x01\x12\b\n" +
@@ -3400,7 +3721,9 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0eBATTERY_STATUS\x10\x1a\x12\x0e\n" + "\x0eBATTERY_STATUS\x10\x1a\x12\x0e\n" +
"\n" + "\n" +
"TAP_NOTIFY\x10\x1b\x12\x10\n" + "TAP_NOTIFY\x10\x1b\x12\x10\n" +
"\fCACHE_STATUS\x10\x1d\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" + "\fCACHE_STATUS\x10\x1d\x12\x14\n" +
"\x10ESPNOW_ECHO_PING\x10\x1e\x12\x11\n" +
"\rSET_LOG_LEVEL\x10\x1f\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\aTapKind\x12\f\n" + "\aTapKind\x12\f\n" +
"\bTAP_NONE\x10\x00\x12\x0e\n" + "\bTAP_NONE\x10\x00\x12\x0e\n" +
"\n" + "\n" +
@@ -3423,7 +3746,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
} }
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 40) var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 44)
var file_uart_messages_proto_goTypes = []any{ var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType (MessageType)(0), // 0: alox.MessageType
(TapKind)(0), // 1: alox.TapKind (TapKind)(0), // 1: alox.TapKind
@@ -3454,19 +3777,23 @@ var file_uart_messages_proto_goTypes = []any{
(*CacheStatusResponse)(nil), // 26: alox.CacheStatusResponse (*CacheStatusResponse)(nil), // 26: alox.CacheStatusResponse
(*EspNowUnicastTestRequest)(nil), // 27: alox.EspNowUnicastTestRequest (*EspNowUnicastTestRequest)(nil), // 27: alox.EspNowUnicastTestRequest
(*EspNowUnicastTestResponse)(nil), // 28: alox.EspNowUnicastTestResponse (*EspNowUnicastTestResponse)(nil), // 28: alox.EspNowUnicastTestResponse
(*LedRingProgressRequest)(nil), // 29: alox.LedRingProgressRequest (*EspNowEchoPingRequest)(nil), // 29: alox.EspNowEchoPingRequest
(*LedRingProgressResponse)(nil), // 30: alox.LedRingProgressResponse (*EspNowEchoPingResponse)(nil), // 30: alox.EspNowEchoPingResponse
(*EspNowFindMeRequest)(nil), // 31: alox.EspNowFindMeRequest (*LedRingProgressRequest)(nil), // 31: alox.LedRingProgressRequest
(*EspNowFindMeResponse)(nil), // 32: alox.EspNowFindMeResponse (*LedRingProgressResponse)(nil), // 32: alox.LedRingProgressResponse
(*RestartRequest)(nil), // 33: alox.RestartRequest (*EspNowFindMeRequest)(nil), // 33: alox.EspNowFindMeRequest
(*RestartResponse)(nil), // 34: alox.RestartResponse (*EspNowFindMeResponse)(nil), // 34: alox.EspNowFindMeResponse
(*OtaStartPayload)(nil), // 35: alox.OtaStartPayload (*RestartRequest)(nil), // 35: alox.RestartRequest
(*OtaPayload)(nil), // 36: alox.OtaPayload (*RestartResponse)(nil), // 36: alox.RestartResponse
(*OtaEndPayload)(nil), // 37: alox.OtaEndPayload (*SetLogLevelRequest)(nil), // 37: alox.SetLogLevelRequest
(*OtaStatusPayload)(nil), // 38: alox.OtaStatusPayload (*SetLogLevelResponse)(nil), // 38: alox.SetLogLevelResponse
(*OtaSlaveProgressRequest)(nil), // 39: alox.OtaSlaveProgressRequest (*OtaStartPayload)(nil), // 39: alox.OtaStartPayload
(*OtaSlaveProgressEntry)(nil), // 40: alox.OtaSlaveProgressEntry (*OtaPayload)(nil), // 40: alox.OtaPayload
(*OtaSlaveProgressResponse)(nil), // 41: alox.OtaSlaveProgressResponse (*OtaEndPayload)(nil), // 41: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 42: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 43: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 44: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 45: alox.OtaSlaveProgressResponse
} }
var file_uart_messages_proto_depIdxs = []int32{ var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType 0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@@ -3475,22 +3802,22 @@ var file_uart_messages_proto_depIdxs = []int32{
5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse 5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse
7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse 7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse
9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse 9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse
35, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload 39, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
36, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload 40, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
37, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload 41, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
38, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload 42, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest 10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest
11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse 11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
27, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest 27, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
28, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse 28, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
39, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest 43, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
41, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse 45, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
29, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest 31, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
30, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse 32, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
31, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest 33, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
32, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse 34, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse
33, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest 35, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest
34, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse 36, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse
12, // 22: alox.UartMessage.accel_stream_request:type_name -> alox.AccelStreamRequest 12, // 22: alox.UartMessage.accel_stream_request:type_name -> alox.AccelStreamRequest
13, // 23: alox.UartMessage.accel_stream_response:type_name -> alox.AccelStreamResponse 13, // 23: alox.UartMessage.accel_stream_response:type_name -> alox.AccelStreamResponse
14, // 24: alox.UartMessage.battery_status_request:type_name -> alox.BatteryStatusRequest 14, // 24: alox.UartMessage.battery_status_request:type_name -> alox.BatteryStatusRequest
@@ -3499,22 +3826,26 @@ var file_uart_messages_proto_depIdxs = []int32{
20, // 27: alox.UartMessage.tap_notify_response:type_name -> alox.TapNotifyResponse 20, // 27: alox.UartMessage.tap_notify_response:type_name -> alox.TapNotifyResponse
22, // 28: alox.UartMessage.cache_status_request:type_name -> alox.CacheStatusRequest 22, // 28: alox.UartMessage.cache_status_request:type_name -> alox.CacheStatusRequest
26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse 26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse
6, // 30: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo 29, // 30: alox.UartMessage.espnow_echo_ping_request:type_name -> alox.EspNowEchoPingRequest
8, // 31: alox.ClientInputResponse.clients:type_name -> alox.ClientInput 30, // 31: alox.UartMessage.espnow_echo_ping_response:type_name -> alox.EspNowEchoPingResponse
15, // 32: alox.BatterySample.lipo1:type_name -> alox.LipoReading 37, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest
15, // 33: alox.BatterySample.lipo2:type_name -> alox.LipoReading 38, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse
16, // 34: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample 6, // 34: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
1, // 35: alox.TapEvent.kind:type_name -> alox.TapKind 8, // 35: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
1, // 36: alox.CacheClientTap.kind:type_name -> alox.TapKind 15, // 36: alox.BatterySample.lipo1:type_name -> alox.LipoReading
23, // 37: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel 15, // 37: alox.BatterySample.lipo2:type_name -> alox.LipoReading
24, // 38: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap 16, // 38: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
25, // 39: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus 1, // 39: alox.TapEvent.kind:type_name -> alox.TapKind
40, // 40: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry 1, // 40: alox.CacheClientTap.kind:type_name -> alox.TapKind
41, // [41:41] is the sub-list for method output_type 23, // 41: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
41, // [41:41] is the sub-list for method input_type 24, // 42: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
41, // [41:41] is the sub-list for extension type_name 25, // 43: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
41, // [41:41] is the sub-list for extension extendee 44, // 44: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
0, // [0:41] is the sub-list for field type_name 45, // [45:45] is the sub-list for method output_type
45, // [45:45] is the sub-list for method input_type
45, // [45:45] is the sub-list for extension type_name
45, // [45:45] is the sub-list for extension extendee
0, // [0:45] is the sub-list for field type_name
} }
func init() { file_uart_messages_proto_init() } func init() { file_uart_messages_proto_init() }
@@ -3552,6 +3883,10 @@ func file_uart_messages_proto_init() {
(*UartMessage_TapNotifyResponse)(nil), (*UartMessage_TapNotifyResponse)(nil),
(*UartMessage_CacheStatusRequest)(nil), (*UartMessage_CacheStatusRequest)(nil),
(*UartMessage_CacheStatusResponse)(nil), (*UartMessage_CacheStatusResponse)(nil),
(*UartMessage_EspnowEchoPingRequest)(nil),
(*UartMessage_EspnowEchoPingResponse)(nil),
(*UartMessage_SetLogLevelRequest)(nil),
(*UartMessage_SetLogLevelResponse)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -3559,7 +3894,7 @@ func file_uart_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 40, NumMessages: 44,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+32 -11
View File
@@ -11,14 +11,18 @@ import (
// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug). // errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug).
var errUARTBusy = errors.New("uart busy (OTA in progress)") var errUARTBusy = errors.New("uart busy (OTA in progress)")
// errOTAInProgress is returned when a second OTA upload is attempted while one is running.
var errOTAInProgress = errors.New("OTA upload already in progress")
// managedSerial keeps the UART open and reconnects after I/O failures or unplug. // managedSerial keeps the UART open and reconnects after I/O failures or unplug.
type managedSerial struct { type managedSerial struct {
portName string portName string
baud int baud int
quiet bool quiet bool
mu sync.Mutex mu sync.Mutex
sp *serialPort sp *serialPort
otaActive bool // UART held for firmware upload; poll/API must not interleave
} }
func newManagedSerial(portName string, baud int) *managedSerial { func newManagedSerial(portName string, baud int) *managedSerial {
@@ -76,20 +80,17 @@ func (m *managedSerial) withPort(fn func(*serialPort) error) error {
return m.withPortLocked(false, fn) return m.withPortLocked(false, fn)
} }
// withPortPoll is like withPort but returns errUARTBusy instead of blocking during OTA. // withPortPoll is like withPort but returns errUARTBusy during OTA (no TryLock race).
func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error { func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error {
return m.withPortLocked(true, fn) return m.withPortLocked(true, fn)
} }
func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) error { func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error {
if try { m.mu.Lock()
if !m.mu.TryLock() {
return errUARTBusy
}
} else {
m.mu.Lock()
}
defer m.mu.Unlock() defer m.mu.Unlock()
if m.otaActive {
return errUARTBusy
}
if m.sp == nil { if m.sp == nil {
if err := m.openLocked(); err != nil { if err := m.openLocked(); err != nil {
@@ -104,6 +105,26 @@ func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) err
return err return err
} }
func (m *managedSerial) recoverAfterMasterRestart() {
const bootWait = 6 * time.Second
m.mu.Lock()
m.closeLocked()
m.mu.Unlock()
log.Printf("UART: master restart — waiting %s for boot", bootWait)
time.Sleep(bootWait)
m.mu.Lock()
defer m.mu.Unlock()
if err := m.openLocked(); err != nil {
log.Printf("UART reconnect after master restart: %v", err)
return
}
flushSerialInput(m.sp)
log.Printf("UART %s ready after master restart", m.portName)
}
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) { func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPort, payload, cmdName) return m.exchangePayloadVia(m.withPort, payload, cmdName)
} }
+2 -2
View File
@@ -80,7 +80,7 @@ func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string, timeo
} }
defer func() { _ = s.port.SetReadTimeout(readTimeout) }() defer func() { _ = s.port.SetReadTimeout(readTimeout) }()
respPayload, err := uartframe.ReadFrame(s.port, nil) respPayload, err := uartframe.ReadFrame(s.port, nil, timeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("read response: %w", err) return nil, fmt.Errorf("read response: %w", err)
} }
@@ -113,7 +113,7 @@ func (s *serialPort) exchangeLocked(cmdID byte, cmdName string) ([]byte, error)
return nil, fmt.Errorf("write: %w", err) return nil, fmt.Errorf("write: %w", err)
} }
payload, err := uartframe.ReadFrame(s.port, nil) payload, err := uartframe.ReadFrame(s.port, nil, readTimeout)
if err != nil { if err != nil {
return nil, fmt.Errorf("read response: %w", err) return nil, fmt.Errorf("read response: %w", err)
} }
+75
View File
@@ -0,0 +1,75 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
var (
shutdownMu sync.Mutex
shutdownFns []func()
hooksOnce sync.Once
bgHandlerOnce sync.Once
)
// registerShutdown runs fn on SIGINT/SIGTERM (LIFO order).
func registerShutdown(fn func()) {
shutdownMu.Lock()
shutdownFns = append(shutdownFns, fn)
shutdownMu.Unlock()
}
func runShutdownHooks() {
hooksOnce.Do(func() {
shutdownMu.Lock()
fns := shutdownFns
shutdownMu.Unlock()
for i := len(fns) - 1; i >= 0; i-- {
fns[i]()
}
})
}
// enableShutdownOnInterrupt listens for SIGINT/SIGTERM in the background and exits
// after running shutdown hooks. Use for one-shot CLI commands (OTA, etc.).
func enableShutdownOnInterrupt() {
bgHandlerOnce.Do(func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
go func() {
sig := <-ch
signal.Stop(ch)
log.Printf("received %v, shutting down…", sig)
runShutdownHooks()
os.Exit(0)
}()
})
}
// waitForShutdown blocks until SIGINT/SIGTERM, runs hooks, and returns.
// Use for long-running servers (serve/dashboard).
func waitForShutdown() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
sig := <-ch
signal.Stop(ch)
log.Printf("received %v, shutting down…", sig)
runShutdownHooks()
}
func shutdownHTTPServer(srv *http.Server) {
if srv == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP shutdown: %v", err)
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ Ordered steps: UART commands, delays, or esptool reset.
**unicast_test** — `input`: `slave` or `client_id`, `seq` **unicast_test** — `input`: `slave` or `client_id`, `seq`
`expect`: `success`, `seq` `expect`: `success`, `seq`
**led_ring** — `input`: `mode` (`clear`, `progress`, `digit`, `blink`, `find_me`), `progress`, `digit`, `r`/`g`/`b`, `intensity`, `blink_ms`, `blink_count` **led_ring** — `input`: `mode` (`clear`, `progress`, `digit`, `blink`, `find_me`, `battery_low`), `progress`, `digit`, `r`/`g`/`b`, `intensity`, `blink_ms`, `blink_count`
`expect`: `success`, `mode`, `progress`, `digit` `expect`: `success`, `mode`, `progress`, `digit`
**find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring) **find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring)
+13 -2
View File
@@ -4,12 +4,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"time"
) )
const ( const (
StartMarker = 0xAA StartMarker = 0xAA
StopMarker = 0xCC StopMarker = 0xCC
MaxPayload = 252 // Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4).
MaxPayload = 248
) )
var ( var (
@@ -97,13 +99,22 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
} }
// ReadFrame reads bytes from r until one full frame is parsed or an error occurs. // ReadFrame reads bytes from r until one full frame is parsed or an error occurs.
func ReadFrame(r io.Reader, buf []byte) ([]byte, error) { // maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
if buf == nil { if buf == nil {
buf = make([]byte, 256) buf = make([]byte, 256)
} }
parser := NewParser() parser := NewParser()
var deadline time.Time
if maxWait > 0 {
deadline = time.Now().Add(maxWait)
}
for { for {
if !deadline.IsZero() && !time.Now().Before(deadline) {
return nil, ErrTimeout
}
n, err := r.Read(buf) n, err := r.Read(buf)
if n > 0 { if n > 0 {
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
+161 -14
View File
@@ -219,6 +219,8 @@
<dd class="col-7" x-text="state.master.running_partition || '—'"></dd> <dd class="col-7" x-text="state.master.running_partition || '—'"></dd>
<dt class="col-5 text-muted">Deadzone</dt> <dt class="col-5 text-muted">Deadzone</dt>
<dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd> <dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd>
<dt class="col-5 text-muted">Log-Level</dt>
<dd class="col-7" x-text="formatLogLevel(masterLogLevel)"></dd>
<dt class="col-5 text-muted">LiPo 1</dt> <dt class="col-5 text-muted">LiPo 1</dt>
<dd class="col-7" x-text="formatLipo(state.master?.lipo1)"></dd> <dd class="col-7" x-text="formatLipo(state.master?.lipo1)"></dd>
<dt class="col-5 text-muted">LiPo 2</dt> <dt class="col-5 text-muted">LiPo 2</dt>
@@ -264,6 +266,31 @@
Restart Restart
</button> </button>
</div> </div>
<label class="form-label text-muted small mb-1 mt-3" for="master-log-level">
ESP Log-Level (global)
</label>
<div class="d-flex flex-wrap gap-2 align-items-end">
<select id="master-log-level" class="form-select form-select-sm config-input"
x-model.number="masterLogLevel"
:disabled="busy || !state.uart_connected">
<option value="0">None (aus)</option>
<option value="1">Error</option>
<option value="2">Warn</option>
<option value="3">Info</option>
<option value="4">Debug</option>
<option value="5">Verbose</option>
</select>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="readMasterLogLevel()"
:disabled="busy || !state.uart_connected">
Lesen
</button>
<button type="button" class="btn btn-primary btn-sm"
@click="setMasterLogLevel()"
:disabled="busy || !state.uart_connected">
Setzen
</button>
</div>
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected"> <p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected">
UART nicht verbunden — Eingabe gesperrt. UART nicht verbunden — Eingabe gesperrt.
</p> </p>
@@ -403,6 +430,12 @@
title="ESP-NOW Unicast-Test"> title="ESP-NOW Unicast-Test">
Test Test
</button> </button>
<button type="button" class="btn btn-outline-info btn-sm"
@click="echoPing(c.id)"
:disabled="busy || !state.uart_connected || !c.available"
title="ESP-NOW Timestamp-Echo (Round-Trip-Latenz)">
Ping
</button>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-info btn-sm"
@click="ledRing({ clientId: c.id })" @click="ledRing({ clientId: c.id })"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !state.uart_connected || !c.available"
@@ -438,7 +471,7 @@
<div class="card-body"> <div class="card-body">
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
Modi: <code>clear</code>, <code>color</code> (ganzer Ring), <code>progress</code> (0100&nbsp;%), Modi: <code>clear</code>, <code>color</code> (ganzer Ring), <code>progress</code> (0100&nbsp;%),
<code>digit</code> (010), <code>blink</code>, <code>find-me</code>. <code>digit</code> (010), <code>blink</code>, <code>find-me</code>, <code>battery-low</code>.
Ziel: Master (<code>client_id=0</code>), ein Slave oder alle Slaves (Broadcast). Ziel: Master (<code>client_id=0</code>), ein Slave oder alle Slaves (Broadcast).
</p> </p>
<div class="row g-3 align-items-end"> <div class="row g-3 align-items-end">
@@ -451,6 +484,7 @@
<option value="digit">Ziffer/Symbol</option> <option value="digit">Ziffer/Symbol</option>
<option value="blink">Blink</option> <option value="blink">Blink</option>
<option value="find-me">Find me</option> <option value="find-me">Find me</option>
<option value="battery-low">Battery low</option>
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
@@ -621,6 +655,7 @@
ws: null, ws: null,
wsConnected: false, wsConnected: false,
masterDz: 100, masterDz: 100,
masterLogLevel: 0,
allDz: 100, allDz: 100,
allTapSingle: false, allTapSingle: false,
allTapDouble: false, allTapDouble: false,
@@ -640,6 +675,7 @@
busy: false, busy: false,
configMsg: '', configMsg: '',
configMsgOk: false, configMsgOk: false,
_flashTimer: null,
led: { led: {
mode: 'color', mode: 'color',
r: 0, r: 0,
@@ -734,21 +770,35 @@
}, },
applyBatterySamples(samples) { applyBatterySamples(samples) {
if (!samples?.length) return; if (!samples?.length) return;
const master = { ...(this.state.master || {}) };
const clients = [...(this.state.clients || [])];
let masterChanged = false;
let clientsChanged = false;
for (const s of samples) { for (const s of samples) {
if (s.client_id === 0) { if (s.client_id === 0) {
if (!this.state.master) this.state.master = {}; master.lipo1 = s.lipo1;
this.state.master.lipo1 = s.lipo1; master.lipo2 = s.lipo2;
this.state.master.lipo2 = s.lipo2; master.battery_age_ms = s.age_ms;
this.state.master.battery_age_ms = s.age_ms; masterChanged = true;
continue; continue;
} }
const c = (this.state.clients || []).find((x) => x.id === s.client_id); const idx = clients.findIndex((x) => x.id === s.client_id);
if (c) { if (idx >= 0) {
c.lipo1 = s.lipo1; clients[idx] = {
c.lipo2 = s.lipo2; ...clients[idx],
c.battery_age_ms = s.age_ms; lipo1: s.lipo1,
lipo2: s.lipo2,
battery_age_ms: s.age_ms,
};
clientsChanged = true;
} }
} }
if (!masterChanged && !clientsChanged) return;
this.state = {
...this.state,
...(masterChanged ? { master } : {}),
...(clientsChanged ? { clients } : {}),
};
}, },
async refreshBattery() { async refreshBattery() {
if (!this.state?.uart_connected) return; if (!this.state?.uart_connected) return;
@@ -759,6 +809,11 @@
if (data.samples?.length) this.applyBatterySamples(data.samples); if (data.samples?.length) this.applyBatterySamples(data.samples);
} catch (_) {} } catch (_) {}
}, },
formatLogLevel(level) {
const labels = ['None', 'Error', 'Warn', 'Info', 'Debug', 'Verbose'];
if (level == null || level < 0 || level > 5) return '—';
return `${labels[level]} (${level})`;
},
formatMac(hex) { formatMac(hex) {
if (!hex || hex.length !== 12) return hex || ''; if (!hex || hex.length !== 12) return hex || '';
return hex.match(/.{2}/g).join(':'); return hex.match(/.{2}/g).join(':');
@@ -938,8 +993,21 @@
return rows; return rows;
}, },
applyOTAProgress(p) { applyOTAProgress(p) {
this.ota.phase = p.phase || ''; const prevPhase = this.ota.phase;
this.ota.step = p.step || this.ota.step || ''; const prevStep = this.ota.step;
if (p.phase) {
// Ignore out-of-order master upload updates after distribution started.
if (!(p.phase === 'uploading' && prevPhase === 'distributing')) {
this.ota.phase = p.phase;
}
}
if (p.step) {
if (!(p.step === 'master' && (prevStep === 'slaves' || prevPhase === 'distributing'))) {
this.ota.step = p.step;
}
} else if (!this.ota.step) {
this.ota.step = '';
}
this.ota.percent = p.percent ?? this.ota.percent; this.ota.percent = p.percent ?? this.ota.percent;
this.ota.message = p.message || ''; this.ota.message = p.message || '';
if (p.image_size) this.ota.imageSize = p.image_size; if (p.image_size) this.ota.imageSize = p.image_size;
@@ -1018,10 +1086,14 @@
this.busy = false; this.busy = false;
} }
}, },
flash(msg, ok) { flash(msg, ok, durationMs = 5000) {
this.configMsg = msg; this.configMsg = msg;
this.configMsgOk = ok; this.configMsgOk = ok;
setTimeout(() => { this.configMsg = ''; }, 5000); if (this._flashTimer) clearTimeout(this._flashTimer);
this._flashTimer = setTimeout(() => {
this.configMsg = '';
this._flashTimer = null;
}, durationMs);
}, },
async setDeadzone(clientId, deadzone, opts = {}) { async setDeadzone(clientId, deadzone, opts = {}) {
if (deadzone == null || deadzone < 0) { if (deadzone == null || deadzone < 0) {
@@ -1080,6 +1152,44 @@
async setMasterDeadzone() { async setMasterDeadzone() {
await this.setDeadzone(0, this.masterDz); await this.setDeadzone(0, this.masterDz);
}, },
async readMasterLogLevel() {
this.busy = true;
try {
const r = await fetch('/api/log-level');
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'Log-Level lesen fehlgeschlagen', false);
return;
}
this.masterLogLevel = data.level;
this.flash(`Master: Log-Level ${this.formatLogLevel(data.level)}`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async setMasterLogLevel() {
this.busy = true;
try {
const r = await fetch('/api/log-level', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ write: true, level: this.masterLogLevel })
});
const data = await r.json();
if (!r.ok || !data.success) {
this.flash(data.error || 'Log-Level setzen fehlgeschlagen', false);
return;
}
this.masterLogLevel = data.level;
this.flash(`Master: Log-Level ${this.formatLogLevel(data.level)} gesetzt`, true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
patchLiveStream(enabled) { patchLiveStream(enabled) {
let clients = this.state.clients || []; let clients = this.state.clients || [];
if (!enabled) { if (!enabled) {
@@ -1245,6 +1355,43 @@
this.busy = false; this.busy = false;
} }
}, },
formatMs(v) {
return v != null && Number.isFinite(Number(v)) ? Number(v).toFixed(3) : '—';
},
formatUsAsMs(us) {
return us != null && Number.isFinite(Number(us))
? (Number(us) / 1000).toFixed(3)
: '—';
},
async echoPing(clientId) {
this.busy = true;
try {
const r = await fetch('/api/echo-ping', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_id: clientId })
});
const data = await r.json();
if (!r.ok) {
this.flash(data.error || `Echo-Ping Slave ${clientId} fehlgeschlagen`, false);
return;
}
if (!data.success) {
this.flash(`Echo-Ping Slave ${clientId} fehlgeschlagen (${this.formatMs(data.rtt_ms)} ms)`, false);
return;
}
this.flash(
`Echo-Ping Slave ${clientId}: Host ${this.formatMs(data.rtt_ms)} ms, ` +
`ESP ${this.formatUsAsMs(data.esp_rtt_us)} ms`,
true,
5000
);
} catch (e) {
this.flash(String(e), false);
} finally {
this.busy = false;
}
},
async restart(clientId = 0) { async restart(clientId = 0) {
this.busy = true; this.busy = true;
try { try {
+10
View File
@@ -22,20 +22,27 @@ idf_component_register(
"cmd/cmd_tap_notify.c" "cmd/cmd_tap_notify.c"
"cmd/cmd_cache_status.c" "cmd/cmd_cache_status.c"
"cmd/cmd_espnow_unicast_test.c" "cmd/cmd_espnow_unicast_test.c"
"cmd/cmd_espnow_echo_ping.c"
"cmd/cmd_espnow_find_me.c" "cmd/cmd_espnow_find_me.c"
"cmd/cmd_restart.c" "cmd/cmd_restart.c"
"pod_reboot.c" "pod_reboot.c"
"cmd/cmd_led_ring.c" "cmd/cmd_led_ring.c"
"cmd/cmd_battery.c" "cmd/cmd_battery.c"
"cmd/cmd_set_log_level.c"
"cmd/cmd_ota.c" "cmd/cmd_ota.c"
"cmd/cmd_ota_slave_progress.c" "cmd/cmd_ota_slave_progress.c"
"ota_uart.c" "ota_uart.c"
"ota_espnow.c" "ota_espnow.c"
"ota_session.c"
"client_registry.c" "client_registry.c"
"esp_now_comm.c" "esp_now_comm.c"
"esp_now_core.c"
"esp_now_master.c"
"esp_now_slave.c"
"esp_now_proto.c" "esp_now_proto.c"
"bosch456.c" "bosch456.c"
"board_input.c" "board_input.c"
"battery_uv.c"
"pod_settings.c" "pod_settings.c"
"proto/uart_messages.pb.c" "proto/uart_messages.pb.c"
"proto/esp_now_messages.pb.c" "proto/esp_now_messages.pb.c"
@@ -57,7 +64,10 @@ idf_component_register(
esp_driver_i2c esp_driver_i2c
esp_adc esp_adc
app_update app_update
esp_timer
bma456) bma456)
target_compile_definitions(${COMPONENT_LIB} target_compile_definitions(${COMPONENT_LIB}
PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"") PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"")
# Optional: disable software UV protection
# target_compile_definitions(${COMPONENT_LIB} PRIVATE POWERPOD_BATTERY_UV_ENABLE=0)
+64 -14
View File
@@ -2,6 +2,8 @@
ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the **same binary**; role and ESP-NOW network are selected at boot via DIP switches and an I2C IO expander. ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the **same binary**; role and ESP-NOW network are selected at boot via DIP switches and an I2C IO expander.
**Architektur & ESP-only Doku (ohne goTool):** [`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) (Datenflüsse UART/Commands/ESP-NOW), [`../docs/DOCUMENTATION.md`](../docs/DOCUMENTATION.md) (vollständige Referenz).
## System overview ## System overview
``` ```
@@ -53,18 +55,20 @@ Pins (`powerpod.h`):
| BMA456 INT | 10 | | BMA456 INT | 10 |
| Button (Taster) | 12 | | Button (Taster) | 12 |
| LiPo sense 1 (ADC) | 1 | | LiPo sense 1 (ADC) | 1 |
| LiPo sense 2 (ADC) | 12 (skipped if same as button) | | LiPo sense 2 (ADC) | 11 |
> **TODO:** GPIO assignments above are provisional; confirm pinning against the real board before release. > **TODO:** GPIO assignments above are provisional; confirm pinning against the real board before release.
Startup order: Startup order (normal path; UV energy-save skips I2C/BMA456/ESP-NOW/UART/button — see **Software UV protection**):
1. Read DIP + IO expander → `app_config` 1. `pod_settings_init()` — NVS
2. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus) 2. LiPo ADC init + optional UV boot check (`battery_uv.c`)
3. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW 3. Read DIP + IO expander → `app_config`
4. `led_ring_init()` 4. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
5. `board_input_init()` — button press logs, LiPo ADC logs every **10 s** 5. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
6. **Master only:** command queue, UART, registered commands (e.g. VERSION) 6. `led_ring_init()`
7. LiPo monitor task + button (`board_input.c`)
8. **Master only:** command queue, UART, registered commands (e.g. VERSION)
## BMA456 accelerometer (`bosch456.c`) ## BMA456 accelerometer (`bosch456.c`)
@@ -176,7 +180,7 @@ Logging:
## Command handler ## Command handler
Generic dispatch for host commands (UART today; `msg_post()` for in-firmware sources later). Generic dispatch for host commands over UART only.
``` ```
UART → generic_msg_t queue → vCmdDispatcherTask → registered handler UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
@@ -186,7 +190,8 @@ UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
|-----|-------------| |-----|-------------|
| `init_cmdHandler(queue)` | Start dispatcher task (priority 5) | | `init_cmdHandler(queue)` | Start dispatcher task (priority 5) |
| `msg_register_handler(id, cb)` | Register callback; max 32 handlers | | `msg_register_handler(id, cb)` | Register callback; max 32 handlers |
| `msg_post(id, data, len)` | Enqueue from firmware (e.g. future ESP-NOW → PC path) |
During an OTA session (`ota_session_busy()`), the dispatcher rejects all UART commands except OTA_* and `OTA_SLAVE_PROGRESS` (see `ota_session.c`).
```c ```c
typedef void (*msg_callback_t)(const uint8_t *data, size_t len); typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
@@ -225,6 +230,8 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
| 25 | `ACCEL_STREAM` | Implemented — enable/disable slave ESP-NOW accel stream to master | | 25 | `ACCEL_STREAM` | Implemented — enable/disable slave ESP-NOW accel stream to master |
| 27 | `TAP_NOTIFY` | Implemented (`cmd/cmd_tap_notify.c`) — get/set which tap kinds notify via ESP-NOW | | 27 | `TAP_NOTIFY` | Implemented (`cmd/cmd_tap_notify.c`) — get/set which tap kinds notify via ESP-NOW |
| 29 | `CACHE_STATUS` | Implemented (`cmd/cmd_cache_status.c`) — subscribed accel + tap cache (one UART round-trip) | | 29 | `CACHE_STATUS` | Implemented (`cmd/cmd_cache_status.c`) — subscribed accel + tap cache (one UART round-trip) |
| 30 | `ESPNOW_ECHO_PING` | Implemented (`cmd/cmd_espnow_echo_ping.c`) — ESP-NOW timestamp echo (latency test) |
| 31 | `SET_LOG_LEVEL` | Implemented (`cmd/cmd_set_log_level.c`) — get/set global `esp_log_level_set("*", …)` on master |
Regenerate C code: Regenerate C code:
@@ -387,6 +394,30 @@ go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16 go run . -port /dev/ttyUSB0 find-me -client 16
``` ```
### SET_LOG_LEVEL command
Read or set the **global** ESP-IDF log filter on the master (`esp_log_level_set("*", level)`). Does not affect the host UART protocol (UART1); `esp_log_*` output goes to the **debug console UART0** (USB, 115200).
**Request:** framed `31` (`0x1f`) + `set_log_level_request` (`write`, `level` 05).
**Response:** `set_log_level_response` (`success`, `level`).
| `level` | `esp_log_level_t` |
|---------|-------------------|
| 0 | NONE |
| 1 | ERROR |
| 2 | WARN |
| 3 | INFO |
| 4 | DEBUG |
| 5 | VERBOSE |
Boot default follows `CONFIG_LOG_DEFAULT_LEVEL` in `sdkconfig` (not persisted across reboot).
```bash
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 0
```
### RESTART command ### RESTART command
Reboot the master (`client_id=0`) or one slave via ESP-NOW (`client_id` = registry id). The device sends the UART response, then restarts after ~150 ms. Reboot the master (`client_id=0`) or one slave via ESP-NOW (`client_id` = registry id). The device sends the UART response, then restarts after ~150 ms.
@@ -411,6 +442,19 @@ Read **cached** LiPo ADC values on the **master** (master local + one entry per
# Host / goTool: all_clients returns master (id 0) + slaves from cache # Host / goTool: all_clients returns master (id 0) + slaves from cache
``` ```
### Software UV protection (LiPo)
When `POWERPOD_BATTERY_UV_ENABLE` is `1` (default in `powerpod.h`, disable via CMake), the firmware monitors ADC pin voltages on GPIO 1 and 11:
| Condition | ADC threshold | Action |
|-----------|---------------|--------|
| Under-voltage | any valid channel &lt; **2300 mV** (3.0 V pack) | NVS latch `uv_latch`, restart into energy-save mode |
| Charged | all valid channels ≥ **3000 mV** (4.0 V pack) | Clear latch, restart into normal boot |
**Energy-save boot:** LED mode `6` (first 4 LEDs red ~10 % for 5 s), then no WiFi/ESP-NOW, UART, I2C, BMA456, or button — only LiPo ADC polling every 30 s until charged.
**Test LED pattern from host:** `led-ring -mode battery-low` (works even when UV feature is disabled).
### LED_RING command ### LED_RING command
Control the 95-LED ring from the host. The firmware **does not** animate digits locally; only UART updates the display. Control the 95-LED ring from the host. The firmware **does not** animate digits locally; only UART updates the display.
@@ -419,7 +463,7 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits
| Field | Meaning | | Field | Meaning |
|-------|---------| |-------|---------|
| `mode` | `0` = clear, `1` = progress, `2` = digit (010), `3` = blink, `4` = find-me, `5` = solid color (all LEDs) | | `mode` | `0` = clear, `1` = progress, `2` = digit (010), `3` = blink, `4` = find-me, `5` = solid color (all LEDs), `6` = battery-low (first 4 LEDs red ~10 % for 5 s) |
| `progress` | 0100 (% of ring lit, mode `1`) | | `progress` | 0100 (% of ring lit, mode `1`) |
| `digit` | 010 (mode `2`, segment maps in `led_ring.c`) | | `digit` | 010 (mode `2`, segment maps in `led_ring.c`) |
| `r`, `g`, `b` | Color 0255 | | `r`, `g`, `b` | Color 0255 |
@@ -441,6 +485,7 @@ go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2
go run . -port /dev/ttyUSB0 find-me go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16 go run . -port /dev/ttyUSB0 find-me -client 16
go run . -port /dev/ttyUSB0 led-ring -mode find-me go run . -port /dev/ttyUSB0 led-ring -mode find-me
go run . -port /dev/ttyUSB0 led-ring -mode battery-low -client 0
go run . -port /dev/ttyUSB0 led-ring -mode color -r 255 -g 0 -b 0 -client 16 go run . -port /dev/ttyUSB0 led-ring -mode color -r 255 -g 0 -b 0 -client 16
go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all
``` ```
@@ -517,7 +562,10 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
| `powerpod.c` | `app_main`, DIP/network config, init order | | `powerpod.c` | `app_main`, DIP/network config, init order |
| `powerpod.h` | Pin defines | | `powerpod.h` | Pin defines |
| `app_config.h` | `app_config_t` | | `app_config.h` | `app_config_t` |
| `esp_now_comm.c/h` | WiFi, ESP-NOW, discover / slave info / OTA send | | `esp_now_comm.c/h` | ESP-NOW init and recv router |
| `esp_now_core.c/h` | Shared WiFi, peer, send |
| `esp_now_master.c/h` | Master discover, monitor, unicast |
| `esp_now_slave.c/h` | Slave join, heartbeat, telemetry |
| `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) | | `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) |
| `ota_espnow.c/h` | Master: distribute staged image to slaves | | `ota_espnow.c/h` | Master: distribute staged image to slaves |
| `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) | | `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) |
@@ -531,10 +579,12 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
| `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter | | `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter |
| `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config | | `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config |
| `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — subscribed accel + tap cache poll | | `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — subscribed accel + tap cache poll |
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 | | `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO11 |
| `pod_settings.c/h` | NVS persistence (accel deadzone, …) | | `battery_uv.c/h` | Software LiPo UV latch, energy-save boot (`POWERPOD_BATTERY_UV_ENABLE`) |
| `pod_settings.c/h` | NVS persistence (accel deadzone, UV latch, …) |
| `led_ring.c/h` | LED ring (digit display, progress bar) | | `led_ring.c/h` | LED ring (digit display, progress bar) |
| `cmd/cmd_led_ring.c` | UART `LED_RING` progress command | | `cmd/cmd_led_ring.c` | UART `LED_RING` progress command |
| `cmd/cmd_set_log_level.c` | UART `SET_LOG_LEVEL` — runtime ESP-IDF log level |
| `proto/uart_messages.proto` | UART protocol schema | | `proto/uart_messages.proto` | UART protocol schema |
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema | | `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
| `esp_now_proto.c/h` | Encode/decode `EspNowMessage` | | `esp_now_proto.c/h` | Encode/decode `EspNowMessage` |
+130
View File
@@ -0,0 +1,130 @@
#include "battery_uv.h"
#if POWERPOD_BATTERY_UV_ENABLE
#include "led_ring.h"
#include "pod_reboot.h"
#include "pod_settings.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "[BATTERY_UV]";
#define UV_DEBOUNCE_SAMPLES 2u
#define UV_POLL_INTERVAL_MS 30000u
static uint8_t s_uv_streak;
static uint8_t s_charge_streak;
bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); }
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
if (reading->lipo1_valid && reading->lipo1_mv < BATTERY_UV_ADC_MV) {
return true;
}
if (reading->lipo2_valid && reading->lipo2_mv < BATTERY_UV_ADC_MV) {
return true;
}
return false;
}
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
bool any_valid = false;
if (reading->lipo1_valid) {
any_valid = true;
if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
if (reading->lipo2_valid) {
any_valid = true;
if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
return any_valid;
}
bool battery_uv_evaluate_boot(void) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) {
ESP_LOGI(TAG, "charged — clearing UV latch");
pod_settings_clear_uv_latched();
return false;
}
if (pod_settings_is_uv_latched() || battery_uv_reading_is_under(&reading)) {
if (!pod_settings_is_uv_latched()) {
ESP_LOGW(TAG, "under-voltage at boot — latching UV");
} else {
ESP_LOGW(TAG, "UV latched — energy-save boot");
}
pod_settings_set_uv_latched(true);
return true;
}
return false;
}
void battery_uv_poll(const board_lipo_reading_t *reading) {
if (reading == NULL || pod_settings_is_uv_latched()) {
return;
}
if (battery_uv_reading_is_under(reading)) {
s_uv_streak++;
ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGW(TAG, "UV confirmed — latching and restarting");
pod_settings_set_uv_latched(true);
pod_schedule_restart();
}
return;
}
s_uv_streak = 0;
}
void battery_uv_run_mode(void) {
s_charge_streak = 0;
s_uv_streak = 0;
led_ring_init();
led_ring_show_battery_low();
ESP_LOGW(TAG, "energy-save mode — ADC poll every %u ms", UV_POLL_INTERVAL_MS);
while (1) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
if (battery_uv_reading_is_charged(&reading)) {
s_charge_streak++;
ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_charge_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGI(TAG, "charged — clearing UV latch and restarting");
pod_settings_clear_uv_latched();
pod_schedule_restart();
}
} else {
s_charge_streak = 0;
}
vTaskDelay(pdMS_TO_TICKS(UV_POLL_INTERVAL_MS));
}
}
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef BATTERY_UV_H
#define BATTERY_UV_H
#include "board_input.h"
#include "powerpod.h"
#include <stdbool.h>
#if POWERPOD_BATTERY_UV_ENABLE
/** ADC pin voltage (mV) below which UV is triggered (3.0 V pack via 33k/10k divider). */
#define BATTERY_UV_ADC_MV 2300u
/** ADC pin voltage (mV) at which charging is detected (4.0 V pack, rounded). */
#define BATTERY_CHARGE_ADC_MV 3000u
bool battery_uv_is_latched(void);
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading);
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading);
/** true → enter UV energy-save path (does not return). */
bool battery_uv_evaluate_boot(void);
void battery_uv_poll(const board_lipo_reading_t *reading);
/** Blocks: LED indicator, then minimal ADC polling until charged. */
void battery_uv_run_mode(void);
#else
static inline bool battery_uv_is_latched(void) { return false; }
static inline bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
(void)reading;
return false;
}
static inline bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
(void)reading;
return false;
}
static inline bool battery_uv_evaluate_boot(void) { return false; }
static inline void battery_uv_poll(const board_lipo_reading_t *reading) {
(void)reading;
}
static inline void battery_uv_run_mode(void) {}
#endif
#endif
+74 -40
View File
@@ -1,6 +1,10 @@
#include "board_input.h" #include "board_input.h"
#include "powerpod.h" #include "powerpod.h"
#include "client_registry.h"
#include "driver/gpio.h" #include "driver/gpio.h"
#if POWERPOD_BATTERY_UV_ENABLE
#include "battery_uv.h"
#endif
#include "esp_adc/adc_oneshot.h" #include "esp_adc/adc_oneshot.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@@ -18,34 +22,51 @@ static const char *TAG_LIPO = "[LIPO]";
#define LIPO_ADC_MAX_RAW 4095 #define LIPO_ADC_MAX_RAW 4095
static QueueHandle_t s_button_queue; static QueueHandle_t s_button_queue;
static adc_oneshot_unit_handle_t s_adc;
static bool s_lipo1_ok;
static bool s_lipo2_ok;
static adc_channel_t s_lipo1_ch;
static adc_channel_t s_lipo2_ch;
static esp_err_t adc_init_channel(int gpio, adc_channel_t *out_ch, bool *out_ok) { typedef struct {
adc_unit_t unit; adc_oneshot_unit_handle_t unit;
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit, out_ch); adc_channel_t ch;
bool ok;
} lipo_adc_t;
static lipo_adc_t s_lipo1;
static lipo_adc_t s_lipo2;
static esp_err_t adc_init_gpio(int gpio, lipo_adc_t *out) {
out->unit = NULL;
out->ok = false;
adc_unit_t unit_id;
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit_id, &out->ch);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err)); ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err));
*out_ok = false;
return err; return err;
} }
if (unit != ADC_UNIT_1) {
ESP_LOGW(TAG_LIPO, "GPIO%d on ADC unit %d (expected ADC1)", gpio, (int)unit); adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = unit_id,
};
err = adc_oneshot_new_unit(&init_cfg, &out->unit);
if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "ADC unit %d init GPIO%d failed: %s", (int)unit_id, gpio,
esp_err_to_name(err));
return err;
} }
adc_oneshot_chan_cfg_t chan_cfg = { adc_oneshot_chan_cfg_t chan_cfg = {
.atten = ADC_ATTEN_DB_12, .atten = ADC_ATTEN_DB_12,
.bitwidth = ADC_BITWIDTH_DEFAULT, .bitwidth = ADC_BITWIDTH_DEFAULT,
}; };
err = adc_oneshot_config_channel(s_adc, *out_ch, &chan_cfg); err = adc_oneshot_config_channel(out->unit, out->ch, &chan_cfg);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "ADC config GPIO%d failed: %s", gpio, esp_err_to_name(err)); ESP_LOGW(TAG_LIPO, "ADC config GPIO%d failed: %s", gpio, esp_err_to_name(err));
*out_ok = false; adc_oneshot_del_unit(out->unit);
out->unit = NULL;
return err; return err;
} }
*out_ok = true;
out->ok = true;
ESP_LOGI(TAG_LIPO, "GPIO%d ready (ADC unit %d)", gpio, (int)unit_id);
return ESP_OK; return ESP_OK;
} }
@@ -56,15 +77,15 @@ static uint32_t raw_to_mv(int raw) {
return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW); return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW);
} }
static void sample_one_channel(adc_channel_t ch, bool ok, uint32_t *mv_out, static void sample_one_channel(const lipo_adc_t *adc, uint32_t *mv_out,
bool *valid_out) { bool *valid_out) {
*valid_out = false; *valid_out = false;
*mv_out = 0; *mv_out = 0;
if (!ok || s_adc == NULL) { if (adc == NULL || !adc->ok || adc->unit == NULL) {
return; return;
} }
int raw = 0; int raw = 0;
if (adc_oneshot_read(s_adc, ch, &raw) == ESP_OK) { if (adc_oneshot_read(adc->unit, adc->ch, &raw) == ESP_OK) {
*valid_out = true; *valid_out = true;
*mv_out = raw_to_mv(raw); *mv_out = raw_to_mv(raw);
} }
@@ -75,8 +96,8 @@ void board_input_read_lipo(board_lipo_reading_t *out) {
return; return;
} }
memset(out, 0, sizeof(*out)); memset(out, 0, sizeof(*out));
sample_one_channel(s_lipo1_ch, s_lipo1_ok, &out->lipo1_mv, &out->lipo1_valid); sample_one_channel(&s_lipo1, &out->lipo1_mv, &out->lipo1_valid);
sample_one_channel(s_lipo2_ch, s_lipo2_ok, &out->lipo2_mv, &out->lipo2_valid); sample_one_channel(&s_lipo2, &out->lipo2_mv, &out->lipo2_valid);
} }
static void lipo_monitor_task(void *param) { static void lipo_monitor_task(void *param) {
@@ -87,6 +108,7 @@ static void lipo_monitor_task(void *param) {
while (1) { while (1) {
board_lipo_reading_t reading; board_lipo_reading_t reading;
board_input_read_lipo(&reading); board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
ESP_LOGI(TAG_LIPO, ESP_LOGI(TAG_LIPO,
"LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV", "LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV",
@@ -95,6 +117,10 @@ static void lipo_monitor_task(void *param) {
reading.lipo2_valid ? "ok" : "n/a", reading.lipo2_valid ? "ok" : "n/a",
(unsigned long)reading.lipo2_mv); (unsigned long)reading.lipo2_mv);
#if POWERPOD_BATTERY_UV_ENABLE
battery_uv_poll(&reading);
#endif
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS)); vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
} }
} }
@@ -165,29 +191,30 @@ static esp_err_t init_button(void) {
return ESP_OK; return ESP_OK;
} }
static esp_err_t init_lipo_adc(void) { static esp_err_t init_lipo_adc_hw(void) {
adc_oneshot_unit_init_cfg_t init_cfg = { memset(&s_lipo1, 0, sizeof(s_lipo1));
.unit_id = ADC_UNIT_1, memset(&s_lipo2, 0, sizeof(s_lipo2));
};
esp_err_t err = adc_oneshot_new_unit(&init_cfg, &s_adc);
if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "ADC init failed: %s", esp_err_to_name(err));
return err;
}
adc_init_channel(V_LIPO_1_GPIO, &s_lipo1_ch, &s_lipo1_ok); adc_init_gpio(V_LIPO_1_GPIO, &s_lipo1);
if (V_LIPO_2_GPIO == TASTER_GPIO) { if (V_LIPO_2_GPIO == TASTER_GPIO) {
ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)", ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)",
V_LIPO_2_GPIO); V_LIPO_2_GPIO);
s_lipo2_ok = false;
} else { } else {
adc_init_channel(V_LIPO_2_GPIO, &s_lipo2_ch, &s_lipo2_ok); adc_init_gpio(V_LIPO_2_GPIO, &s_lipo2);
} }
if (!s_lipo1_ok && !s_lipo2_ok) { if (!s_lipo1.ok && !s_lipo2.ok) {
adc_oneshot_del_unit(s_adc); return ESP_FAIL;
s_adc = NULL; }
return ESP_OK;
}
esp_err_t board_input_init_adc_only(void) { return init_lipo_adc_hw(); }
esp_err_t board_input_start_lipo_monitor(void) {
if (!s_lipo1.ok && !s_lipo2.ok) {
return ESP_FAIL; return ESP_FAIL;
} }
@@ -198,16 +225,23 @@ static esp_err_t init_lipo_adc(void) {
return ESP_OK; return ESP_OK;
} }
esp_err_t board_input_init_button(void) { return init_button(); }
esp_err_t board_input_init(void) { esp_err_t board_input_init(void) {
esp_err_t err = init_button(); esp_err_t err = board_input_init_adc_only();
if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "ADC init failed: %s", esp_err_to_name(err));
} else {
err = board_input_start_lipo_monitor();
if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "monitor task failed: %s", esp_err_to_name(err));
}
}
err = board_input_init_button();
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG_BTN, "init failed: %s", esp_err_to_name(err)); ESP_LOGW(TAG_BTN, "init failed: %s", esp_err_to_name(err));
} }
err = init_lipo_adc();
if (err != ESP_OK) {
ESP_LOGW(TAG_LIPO, "init failed: %s", esp_err_to_name(err));
}
return ESP_OK; return ESP_OK;
} }
+9
View File
@@ -19,6 +19,15 @@ typedef struct {
*/ */
esp_err_t board_input_init(void); esp_err_t board_input_init(void);
/** LiPo ADC channels only (no button, no background task). */
esp_err_t board_input_init_adc_only(void);
/** Start 10 s LiPo monitor task (normal boot only). */
esp_err_t board_input_start_lipo_monitor(void);
/** Front-panel button debounce task. */
esp_err_t board_input_init_button(void);
/** On-demand ADC read of both LiPo sense inputs (if configured). */ /** On-demand ADC read of both LiPo sense inputs (if configured). */
void board_input_read_lipo(board_lipo_reading_t *out); void board_input_read_lipo(board_lipo_reading_t *out);
+34 -4
View File
@@ -38,6 +38,7 @@ static volatile bool s_int_pending;
static SemaphoreHandle_t s_accel_mutex; static SemaphoreHandle_t s_accel_mutex;
static bma456_tap_handler_t s_tap_handler; static bma456_tap_handler_t s_tap_handler;
static void *s_tap_handler_ctx; static void *s_tap_handler_ctx;
static const bma456_tap_config_t s_tap_config = BMA456_TAP_CONFIG_DEFAULT;
static esp_err_t check_bma4(const char *api_name, int8_t rslt); static esp_err_t check_bma4(const char *api_name, int8_t rslt);
@@ -263,19 +264,48 @@ static esp_err_t configure_tap_interrupt(void) {
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) { if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
return ESP_FAIL; return ESP_FAIL;
} }
tap_settings.tap_sens_thres = 0; tap_settings.tap_sens_thres = s_tap_config.tap_sens_thres;
tap_settings.max_gest_dur = s_tap_config.max_gest_dur;
tap_settings.tap_shock_dur = s_tap_config.tap_shock_dur;
tap_settings.quite_time_after_gest = s_tap_config.quite_time_after_gest;
tap_settings.wait_for_timeout = s_tap_config.wait_for_timeout;
tap_settings.axis_sel = s_tap_config.axis_sel;
ret = bma456h_tap_set_parameter(&tap_settings, &s_bma456); ret = bma456h_tap_set_parameter(&tap_settings, &s_bma456);
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) { if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
return ESP_FAIL; return ESP_FAIL;
} }
ret = bma456h_feature_enable( uint16_t tap_features = 0;
(BMA456H_SINGLE_TAP_EN | BMA456H_DOUBLE_TAP_EN | BMA456H_TRIPLE_TAP_EN), if (s_tap_config.enable_single) {
BMA4_ENABLE, &s_bma456); tap_features |= BMA456H_SINGLE_TAP_EN;
}
if (s_tap_config.enable_double) {
tap_features |= BMA456H_DOUBLE_TAP_EN;
}
if (s_tap_config.enable_triple) {
tap_features |= BMA456H_TRIPLE_TAP_EN;
}
if (tap_features == 0) {
ESP_LOGW(TAG, "tap config: no tap kinds enabled");
return ESP_ERR_INVALID_ARG;
}
ret = bma456h_feature_enable(tap_features, BMA4_ENABLE, &s_bma456);
if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) { if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) {
return ESP_FAIL; return ESP_FAIL;
} }
ESP_LOGI(TAG,
"tap config sens=%u max_gest=%u shock=%u quiet=%u wait=%u axis=%u "
"(single=%d double=%d triple=%d)",
(unsigned)s_tap_config.tap_sens_thres,
(unsigned)s_tap_config.max_gest_dur,
(unsigned)s_tap_config.tap_shock_dur,
(unsigned)s_tap_config.quite_time_after_gest,
(unsigned)s_tap_config.wait_for_timeout,
(unsigned)s_tap_config.axis_sel, s_tap_config.enable_single,
s_tap_config.enable_double, s_tap_config.enable_triple);
ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE, ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE,
&s_bma456); &s_bma456);
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) { if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
+36
View File
@@ -10,6 +10,8 @@
#include "driver/i2c_types.h" #include "driver/i2c_types.h"
#include "esp_err.h" #include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/** 7-bit I2C address (SDO low). */ /** 7-bit I2C address (SDO low). */
#define BMA456_I2C_ADDR 0x18 #define BMA456_I2C_ADDR 0x18
@@ -20,6 +22,40 @@
/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */ /** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */
#define BMA456_DEFAULT_ACCEL_DEADZONE 100u #define BMA456_DEFAULT_ACCEL_DEADZONE 100u
/**
* BMA456H multitap tuning (see BST-BMA456-AN000).
*
* Time fields use register units: value × 5 ms (e.g. 100 500 ms).
* tap_sens_thres: 0 = most sensitive 15 = least (~78 mg per LSB).
* axis_sel: 0 = X, 1 = Y, 2 = Z.
* wait_for_timeout: 0 = report immediately, 1 = wait max_gest_dur for classification.
*/
typedef struct {
uint16_t tap_sens_thres;
uint16_t max_gest_dur;
uint16_t tap_shock_dur;
uint16_t quite_time_after_gest;
uint16_t wait_for_timeout;
uint16_t axis_sel;
bool enable_single;
bool enable_double;
bool enable_triple;
} bma456_tap_config_t;
/** Edit these values to tune tap detection, then rebuild. */
#define BMA456_TAP_CONFIG_DEFAULT \
{ \
.tap_sens_thres = 5, /* sensitive; 0=max, Bosch default=9 */ \
.max_gest_dur = 100, /* 500 ms window for double/triple */ \
.tap_shock_dur = 6, /* 30 ms debounce after each impulse */ \
.quite_time_after_gest = 60, /* 300 ms min gap between gestures */ \
.wait_for_timeout = 0, /* 0 = faster single-tap response */ \
.axis_sel = 2, /* Z axis */ \
.enable_single = true, \
.enable_double = true, \
.enable_triple = true, \
}
/** /**
* Probe and configure the sensor on bus_handle (100 kHz device). * Probe and configure the sensor on bus_handle (100 kHz device).
* On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned; * On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned;
+11 -3
View File
@@ -33,12 +33,20 @@ static void handle_accel_stream(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg; alox_UartMessage uart_msg;
alox_AccelStreamRequest req = alox_AccelStreamRequest_init_zero; alox_AccelStreamRequest req = alox_AccelStreamRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { if (len > 0) {
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(false, 0, false, 0);
return;
}
const alox_AccelStreamRequest *req_ptr = UART_CMD_REQ( const alox_AccelStreamRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_accel_stream_request_tag, accel_stream_request); &uart_msg, alox_UartMessage_accel_stream_request_tag, accel_stream_request);
if (req_ptr != NULL) { if (req_ptr == NULL) {
req = *req_ptr; ESP_LOGW(TAG, "missing accel_stream_request");
reply(false, 0, false, 0);
return;
} }
req = *req_ptr;
} }
if (req.write) { if (req.write) {
+27 -17
View File
@@ -33,18 +33,14 @@ static bool append_battery_sample(alox_BatteryStatusResponse *resp,
return lipo1_valid || lipo2_valid; return lipo1_valid || lipo2_valid;
} }
static bool append_master_cached(alox_BatteryStatusResponse *resp) { static bool append_master_sample(alox_BatteryStatusResponse *resp) {
board_lipo_reading_t reading; board_lipo_reading_t reading;
uint32_t age_ms = 0;
if (!client_registry_get_master_battery(&reading, &age_ms)) { board_input_read_lipo(&reading);
board_input_read_lipo(&reading); client_registry_set_master_battery(&reading);
client_registry_set_master_battery(&reading);
age_ms = 0;
}
return append_battery_sample(resp, 0, reading.lipo1_valid, reading.lipo1_mv, return append_battery_sample(resp, 0, reading.lipo1_valid, reading.lipo1_mv,
reading.lipo2_valid, reading.lipo2_mv, age_ms); reading.lipo2_valid, reading.lipo2_mv, 0);
} }
static bool append_slave_cached(alox_BatteryStatusResponse *resp, static bool append_slave_cached(alox_BatteryStatusResponse *resp,
@@ -67,14 +63,28 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
if (len > 0) { if (len > 0) {
alox_UartMessage uart_msg; alox_UartMessage uart_msg;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
const alox_BatteryStatusRequest *req_ptr = UART_CMD_REQ( ESP_LOGW(TAG, "decode failed");
&uart_msg, alox_UartMessage_battery_status_request_tag, alox_UartMessage response;
battery_status_request); uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS,
if (req_ptr != NULL) { alox_UartMessage_battery_status_response_tag);
req = *req_ptr; response.payload.battery_status_response.success = false;
} uart_cmd_send(&response, TAG);
return;
} }
const alox_BatteryStatusRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_battery_status_request_tag,
battery_status_request);
if (req_ptr == NULL) {
ESP_LOGW(TAG, "missing battery_status_request");
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS,
alox_UartMessage_battery_status_response_tag);
response.payload.battery_status_response.success = false;
uart_cmd_send(&response, TAG);
return;
}
req = *req_ptr;
} }
alox_UartMessage response; alox_UartMessage response;
@@ -88,7 +98,7 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
bool any = false; bool any = false;
if (req.all_clients) { if (req.all_clients) {
any |= append_master_cached(resp); any |= append_master_sample(resp);
for (size_t i = 0; i < client_registry_count(); i++) { for (size_t i = 0; i < client_registry_count(); i++) {
const client_info_t *client = client_registry_at(i); const client_info_t *client = client_registry_at(i);
if (client == NULL) { if (client == NULL) {
@@ -99,7 +109,7 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
ESP_LOGI(TAG, "battery cache all_clients → %u samples", ESP_LOGI(TAG, "battery cache all_clients → %u samples",
(unsigned)resp->samples_count); (unsigned)resp->samples_count);
} else if (req.client_id == 0) { } else if (req.client_id == 0) {
any = append_master_cached(resp); any = append_master_sample(resp);
ESP_LOGI(TAG, "battery cache master"); ESP_LOGI(TAG, "battery cache master");
} else { } else {
const client_info_t *client = client_registry_find_by_id(req.client_id); const client_info_t *client = client_registry_find_by_id(req.client_id);
+70
View File
@@ -0,0 +1,70 @@
#include "client_registry.h"
#include "cmd_espnow_echo_ping.h"
#include "esp_log.h"
#include "esp_now_comm.h"
#include "uart_cmd.h"
static const char *TAG = "[ECHO_PING]";
static void reply(bool success, uint32_t client_id, uint64_t timestamp_us,
uint32_t esp_rtt_us) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_ESPNOW_ECHO_PING,
alox_UartMessage_espnow_echo_ping_response_tag);
response.payload.espnow_echo_ping_response.success = success;
response.payload.espnow_echo_ping_response.client_id = client_id;
response.payload.espnow_echo_ping_response.timestamp_us = timestamp_us;
response.payload.espnow_echo_ping_response.esp_rtt_us = esp_rtt_us;
uart_cmd_send(&response, TAG);
}
static void handle_espnow_echo_ping(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(false, 0, 0, 0);
return;
}
const alox_EspNowEchoPingRequest *req = UART_CMD_REQ(
&uart_msg, alox_UartMessage_espnow_echo_ping_request_tag,
espnow_echo_ping_request);
if (req == NULL || req->client_id == 0) {
ESP_LOGW(TAG, "need client_id in request");
reply(false, 0, 0, 0);
return;
}
const client_info_t *client = client_registry_find_by_id(req->client_id);
if (client == NULL) {
ESP_LOGW(TAG, "client id %lu not in registry",
(unsigned long)req->client_id);
reply(false, req->client_id, 0, 0);
return;
}
ESP_LOGI(TAG, "UART request client_id=%lu host_ts=%llu",
(unsigned long)req->client_id,
(unsigned long long)req->timestamp_us);
esp_now_echo_ping_result_t ping_result = {0};
esp_err_t err =
esp_now_comm_echo_ping(client->mac, req->timestamp_us, &ping_result);
if (err != ESP_OK) {
ESP_LOGW(TAG, "echo ping to id=%lu failed: %s", (unsigned long)req->client_id,
esp_err_to_name(err));
reply(false, req->client_id, 0, 0);
return;
}
ESP_LOGI(TAG, "UART reply client_id=%lu host_ts=%llu esp_rtt_us=%lu",
(unsigned long)req->client_id,
(unsigned long long)ping_result.echoed_us,
(unsigned long)ping_result.esp_rtt_us);
reply(true, req->client_id, ping_result.echoed_us, ping_result.esp_rtt_us);
}
void cmd_espnow_echo_ping_register(void) {
uart_cmd_register(alox_MessageType_ESPNOW_ECHO_PING, handle_espnow_echo_ping);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CMD_ESPNOW_ECHO_PING_H
#define CMD_ESPNOW_ECHO_PING_H
void cmd_espnow_echo_ping_register(void);
#endif
+12 -21
View File
@@ -1,4 +1,5 @@
#include "cmd_handler.h" #include "cmd_handler.h"
#include "ota_session.h"
#include "esp_err.h" #include "esp_err.h"
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
@@ -56,6 +57,10 @@ static const char *message_type_name(uint16_t id) {
return "TAP_NOTIFY"; return "TAP_NOTIFY";
case alox_MessageType_CACHE_STATUS: case alox_MessageType_CACHE_STATUS:
return "CACHE_STATUS"; return "CACHE_STATUS";
case alox_MessageType_ESPNOW_ECHO_PING:
return "ESPNOW_ECHO_PING";
case alox_MessageType_SET_LOG_LEVEL:
return "SET_LOG_LEVEL";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -88,33 +93,19 @@ esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb) {
return ESP_OK; return ESP_OK;
} }
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len) {
if (cmd_queue == NULL) {
return ESP_ERR_INVALID_STATE;
}
generic_msg_t msg = {.msg_id = id, .len = len, .payload = NULL};
if (len > 0) {
msg.payload = malloc(len);
if (msg.payload == NULL) {
return ESP_ERR_NO_MEM;
}
memcpy(msg.payload, data, len);
}
if (xQueueSend(cmd_queue, &msg, pdMS_TO_TICKS(100)) != pdPASS) {
free(msg.payload);
return ESP_ERR_TIMEOUT;
}
return ESP_OK;
}
void vCmdDispatcherTask(void *param) { void vCmdDispatcherTask(void *param) {
(void)param; (void)param;
generic_msg_t msg; generic_msg_t msg;
while (1) { while (1) {
if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) { if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) {
if (!ota_session_uart_cmd_allowed(msg.msg_id)) {
ESP_LOGW(TAG, "reject %s (0x%02x) during OTA session",
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
free(msg.payload);
continue;
}
bool handled = false; bool handled = false;
for (int i = 0; i < handler_count; i++) { for (int i = 0; i < handler_count; i++) {
if (handlers[i].msg_id == msg.msg_id) { if (handlers[i].msg_id == msg.msg_id) {
-1
View File
@@ -21,6 +21,5 @@ void init_cmdHandler(QueueHandle_t queue);
void vCmdDispatcherTask(void *param); void vCmdDispatcherTask(void *param);
esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb); esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb);
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len);
#endif #endif
+5
View File
@@ -13,6 +13,7 @@ static const char *TAG = "[LED_RING_CMD]";
#define LED_RING_MODE_BLINK 3 #define LED_RING_MODE_BLINK 3
#define LED_RING_MODE_FIND_ME 4 #define LED_RING_MODE_FIND_ME 4
#define LED_RING_MODE_COLOR 5 #define LED_RING_MODE_COLOR 5
#define LED_RING_MODE_BATTERY_LOW 6
static uint8_t clamp_u8(uint32_t v) { static uint8_t clamp_u8(uint32_t v) {
if (v > 255) { if (v > 255) {
@@ -90,6 +91,10 @@ bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req) {
led_ring_find_me(); led_ring_find_me();
return true; return true;
case LED_RING_MODE_BATTERY_LOW:
led_ring_show_battery_low();
return true;
case LED_RING_MODE_BLINK: case LED_RING_MODE_BLINK:
cmd.mode = LED_CMD_BLINK; cmd.mode = LED_CMD_BLINK;
cmd.r = r; cmd.r = r;
+55 -30
View File
@@ -81,8 +81,6 @@ static const ota_espnow_progress_cbs_t s_dist_progress = {
static void ota_prepare_task(void *param) { static void ota_prepare_task(void *param) {
uint32_t total_size = (uint32_t)(uintptr_t)param; uint32_t total_size = (uint32_t)(uintptr_t)param;
send_ota_status(OTA_UART_ST_PREPARING, 0);
int slot = ota_uart_prepare(total_size); int slot = ota_uart_prepare(total_size);
if (slot < 0) { if (slot < 0) {
send_ota_failed(1); send_ota_failed(1);
@@ -116,27 +114,32 @@ static void handle_ota_start(const uint8_t *data, size_t len) {
const alox_OtaStartPayload *req_ptr = const alox_OtaStartPayload *req_ptr =
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start); UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start);
if (req_ptr != NULL) { if (req_ptr == NULL) {
req = *req_ptr; ESP_LOGW(TAG, "OTA_START: missing ota_start payload");
send_ota_failed(3);
return;
} }
req = *req_ptr;
if (req.total_size == 0) { if (req.total_size == 0) {
ESP_LOGW(TAG, "OTA_START: total_size required"); ESP_LOGW(TAG, "OTA_START: total_size required");
send_ota_failed( 3); send_ota_failed(3);
return; return;
} }
if (ota_uart_is_active()) { if (ota_uart_is_active()) {
ESP_LOGW(TAG, "OTA_START while session active"); ESP_LOGW(TAG, "OTA_START while session active");
send_ota_failed( 4); send_ota_failed(4);
return; return;
} }
send_ota_status(OTA_UART_ST_PREPARING, 0);
if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK, if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK,
(void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO, (void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
NULL) != pdPASS) { NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create ota_prepare task"); ESP_LOGE(TAG, "failed to create ota_prepare task");
send_ota_failed( 5); send_ota_failed(5);
} }
} }
@@ -172,8 +175,18 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
return; return;
} }
ota_feed_result_t r = ota_feed_result_t r = ota_uart_feed_chunk(req_ptr->seq, req_ptr->data.bytes,
ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size); req_ptr->data.size);
if (r == OTA_FEED_SEQ_GAP) {
send_ota_failed(16);
return;
}
if (r == OTA_FEED_SEQ_DUP) {
if (ota_uart_block_ready_for_reack()) {
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
}
return;
}
if (r == OTA_FEED_ERROR) { if (r == OTA_FEED_ERROR) {
send_ota_failed( 13); send_ota_failed( 13);
return; return;
@@ -186,15 +199,6 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
led_ring_show_ota_progress(done, total, OTA_LED_UART_R, OTA_LED_UART_G, led_ring_show_ota_progress(done, total, OTA_LED_UART_R, OTA_LED_UART_G,
OTA_LED_UART_B); OTA_LED_UART_B);
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0); send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
return;
}
if (r == OTA_FEED_OK) {
uint32_t total = ota_uart_total_size();
if (total > 0) {
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
OTA_LED_UART_R, OTA_LED_UART_G, OTA_LED_UART_B);
}
} }
} }
@@ -293,31 +297,28 @@ static void handle_ota_end(const uint8_t *data, size_t len) {
} }
} }
static void handle_ota_start_espnow(const uint8_t *data, size_t len) { static void ota_start_espnow_task(void *param) {
(void)data; (void)param;
(void)len;
if (ota_uart_is_active()) {
send_ota_failed( 40);
return;
}
const esp_partition_t *part = NULL; const esp_partition_t *part = NULL;
uint32_t image_size = 0; uint32_t image_size = 0;
if (!ota_uart_get_staged_image(&part, &image_size)) { if (!ota_uart_get_staged_image(&part, &image_size)) {
send_ota_failed( 41); send_ota_failed(41);
vTaskDelete(NULL);
return; return;
} }
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress); esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_failed( 42); send_ota_failed(42);
vTaskDelete(NULL);
return; return;
} }
err = ota_uart_apply_boot(); err = ota_uart_apply_boot();
if (err != ESP_OK) { if (err != ESP_OK) {
send_ota_failed( (uint32_t)err); send_ota_failed((uint32_t)err);
vTaskDelete(NULL);
return; return;
} }
@@ -329,6 +330,30 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
response.payload.ota_status.error = 0; response.payload.ota_status.error = 0;
uart_cmd_send(&response, TAG); uart_cmd_send(&response, TAG);
led_ring_ota_success(); led_ring_ota_success();
vTaskDelete(NULL);
}
static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
(void)data;
(void)len;
if (ota_uart_is_active()) {
send_ota_failed(40);
return;
}
const esp_partition_t *part = NULL;
uint32_t image_size = 0;
if (!ota_uart_get_staged_image(&part, &image_size)) {
send_ota_failed(41);
return;
}
if (xTaskCreate(ota_start_espnow_task, "ota_espnow", OTA_DIST_STACK, NULL,
OTA_DIST_PRIO, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create ota_start_espnow task");
send_ota_failed(43);
}
} }
void cmd_ota_register(void) { void cmd_ota_register(void) {
+19 -3
View File
@@ -9,13 +9,29 @@ static void handle_ota_slave_progress(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg; alox_UartMessage uart_msg;
uint32_t filter = 0; uint32_t filter = 0;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { if (len > 0) {
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
alox_UartMessage response;
uart_cmd_init_response(
&response, alox_MessageType_OTA_SLAVE_PROGRESS,
alox_UartMessage_ota_slave_progress_response_tag);
uart_cmd_send(&response, TAG);
return;
}
const alox_OtaSlaveProgressRequest *req = const alox_OtaSlaveProgressRequest *req =
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag, UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag,
ota_slave_progress_request); ota_slave_progress_request);
if (req != NULL) { if (req == NULL) {
filter = req->client_id; ESP_LOGW(TAG, "missing ota_slave_progress_request");
alox_UartMessage response;
uart_cmd_init_response(
&response, alox_MessageType_OTA_SLAVE_PROGRESS,
alox_UartMessage_ota_slave_progress_response_tag);
uart_cmd_send(&response, TAG);
return;
} }
filter = req->client_id;
} }
alox_UartMessage response; alox_UartMessage response;
+51
View File
@@ -0,0 +1,51 @@
#include "cmd_set_log_level.h"
#include "esp_log.h"
#include "uart_cmd.h"
static const char *TAG = "[LOG_LVL]";
static bool valid_level(uint32_t level) { return level <= ESP_LOG_VERBOSE; }
static void reply(uint32_t level, bool success) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_SET_LOG_LEVEL,
alox_UartMessage_set_log_level_response_tag);
response.payload.set_log_level_response.success = success;
response.payload.set_log_level_response.level = level;
uart_cmd_send(&response, TAG);
}
static void handle_set_log_level(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_SetLogLevelRequest req = alox_SetLogLevelRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply((uint32_t)esp_log_level_get("*"), false);
return;
}
const alox_SetLogLevelRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_set_log_level_request_tag, set_log_level_request);
if (req_ptr != NULL) {
req = *req_ptr;
}
if (req.write) {
if (!valid_level(req.level)) {
ESP_LOGW(TAG, "invalid level %lu", (unsigned long)req.level);
reply((uint32_t)esp_log_level_get("*"), false);
return;
}
esp_log_level_set("*", (esp_log_level_t)req.level);
ESP_LOGI(TAG, "global log level set to %lu", (unsigned long)req.level);
reply(req.level, true);
return;
}
reply((uint32_t)esp_log_level_get("*"), true);
}
void cmd_set_log_level_register(void) {
uart_cmd_register(alox_MessageType_SET_LOG_LEVEL, handle_set_log_level);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CMD_SET_LOG_LEVEL_H
#define CMD_SET_LOG_LEVEL_H
void cmd_set_log_level_register(void);
#endif
+11 -3
View File
@@ -39,12 +39,20 @@ static void handle_tap_notify(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg; alox_UartMessage uart_msg;
alox_TapNotifyRequest req = alox_TapNotifyRequest_init_zero; alox_TapNotifyRequest req = alox_TapNotifyRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) { if (len > 0) {
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(0, false, 0, false, false, false);
return;
}
const alox_TapNotifyRequest *req_ptr = UART_CMD_REQ( const alox_TapNotifyRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_tap_notify_request_tag, tap_notify_request); &uart_msg, alox_UartMessage_tap_notify_request_tag, tap_notify_request);
if (req_ptr != NULL) { if (req_ptr == NULL) {
req = *req_ptr; ESP_LOGW(TAG, "missing tap_notify_request");
reply(0, false, 0, false, false, false);
return;
} }
req = *req_ptr;
} }
if (req.write) { if (req.write) {
+19 -1238
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -26,6 +26,17 @@ esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t seq); uint32_t seq);
/** Result of a master-side ESP-NOW echo ping round-trip. */
typedef struct {
uint64_t echoed_us;
uint32_t esp_rtt_us;
} esp_now_echo_ping_result_t;
/** Master: ESP-NOW echo ping round-trip; fills result on success. */
esp_err_t esp_now_comm_echo_ping(const uint8_t mac[CLIENT_MAC_LEN],
uint64_t timestamp_us,
esp_now_echo_ping_result_t *result);
/** Master: trigger find-me LED sequence on one slave. */ /** Master: trigger find-me LED sequence on one slave. */
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN], esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id); uint32_t client_id);
+282
View File
@@ -0,0 +1,282 @@
#include "esp_now_core.h"
#include "esp_now_proto.h"
#include "esp_err.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "esp_netif.h"
#include "esp_now.h"
#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include <stdio.h>
#include <string.h>
static const char *TAG = "[ESPNOW_CORE]";
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
0xff, 0xff, 0xff};
static app_config_t s_config;
static uint8_t s_wifi_channel;
static uint8_t s_own_mac[ESP_NOW_ETH_ALEN];
static SemaphoreHandle_t s_send_done;
static SemaphoreHandle_t s_send_lock;
static bool s_send_cb_ready;
static volatile esp_now_send_status_t s_last_send_status;
static volatile bool s_last_send_ok;
#define ESPNOW_SEND_DONE_TIMEOUT_MS 500u
#define ESPNOW_SEND_MAX_ATTEMPTS 8u
#define ESPNOW_SEND_RETRY_DELAY_MS 10u
#define ESPNOW_NOMEM_RETRY_DELAY_MS 50u
#define ESPNOW_RELIABLE_MAX_ATTEMPTS 24u
#define ESPNOW_RELIABLE_NOMEM_DELAY_MS 50u
static uint8_t network_to_channel(uint8_t network) {
if (network < 1 || network > 13) {
return 1;
}
return network;
}
static void espnow_send_done_cb(const esp_now_send_info_t *tx_info,
esp_now_send_status_t status) {
(void)tx_info;
s_last_send_status = status;
s_last_send_ok = (status == ESP_NOW_SEND_SUCCESS);
if (s_send_done != NULL) {
xSemaphoreGive(s_send_done);
}
}
void esp_now_core_store_config(const app_config_t *config) {
if (config == NULL) {
return;
}
memset(&s_config, 0, sizeof(s_config));
memcpy(&s_config, config, sizeof(s_config));
s_wifi_channel = network_to_channel(config->network);
}
const app_config_t *esp_now_core_get_config(void) { return &s_config; }
bool esp_now_core_is_master(void) { return s_config.master; }
uint8_t esp_now_core_network(void) { return s_config.network; }
uint8_t esp_now_core_wifi_channel(void) { return s_wifi_channel; }
const uint8_t *esp_now_core_own_mac(void) { return s_own_mac; }
uint32_t esp_now_core_now_ms(void) {
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
}
bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b) {
return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0;
}
void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len) {
snprintf(out, out_len, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1],
mac[2], mac[3], mac[4], mac[5]);
}
esp_err_t esp_now_core_ensure_peer(const uint8_t *mac) {
if (esp_now_is_peer_exist(mac)) {
return ESP_OK;
}
esp_now_peer_info_t peer = {0};
memcpy(peer.peer_addr, mac, ESP_NOW_ETH_ALEN);
peer.channel = s_wifi_channel;
peer.ifidx = WIFI_IF_STA;
peer.encrypt = false;
esp_err_t err = esp_now_add_peer(&peer);
if (err != ESP_OK) {
ESP_LOGW(TAG, "add peer failed: %s", esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_core_ensure_broadcast_peer(void) {
return esp_now_core_ensure_peer(ESPNOW_BCAST);
}
static esp_err_t send_with_backpressure(const uint8_t *dest_mac,
const alox_EspNowMessage *msg,
uint32_t max_attempts,
uint32_t nomem_delay_ms,
uint32_t retry_delay_ms,
bool quiet_retries) {
if (s_send_lock == NULL ||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
uint8_t buf[ESPNOW_PB_MAX_SIZE];
size_t len = 0;
esp_err_t result = ESP_FAIL;
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "encode failed");
result = err;
goto out;
}
if (len > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
(unsigned)ESP_NOW_MAX_DATA_LEN);
result = ESP_ERR_INVALID_SIZE;
goto out;
}
if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) {
result = ESP_FAIL;
goto out;
}
for (uint32_t attempt = 0; attempt < max_attempts; attempt++) {
if (s_send_cb_ready && s_send_done != NULL) {
xSemaphoreTake(s_send_done, 0);
}
s_last_send_ok = false;
err = esp_now_send(dest_mac, buf, len);
if (err != ESP_OK) {
const bool last = (attempt + 1 >= max_attempts);
if (!quiet_retries || last) {
ESP_LOGW(TAG, "send type=%u failed (attempt %lu/%lu): %s",
(unsigned)msg->type, (unsigned long)(attempt + 1),
(unsigned long)max_attempts, esp_err_to_name(err));
}
vTaskDelay(pdMS_TO_TICKS(err == ESP_ERR_ESPNOW_NO_MEM ? nomem_delay_ms
: retry_delay_ms));
continue;
}
if (s_send_cb_ready && s_send_done != NULL) {
if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(ESPNOW_SEND_DONE_TIMEOUT_MS)) !=
pdTRUE) {
const bool last = (attempt + 1 >= max_attempts);
if (!quiet_retries || last) {
ESP_LOGW(TAG, "send type=%u done timeout (attempt %lu/%lu)",
(unsigned)msg->type, (unsigned long)(attempt + 1),
(unsigned long)max_attempts);
}
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
continue;
}
if (!s_last_send_ok) {
const bool last = (attempt + 1 >= max_attempts);
if (!quiet_retries || last) {
ESP_LOGW(TAG, "send type=%u peer status=%d (attempt %lu/%lu)",
(unsigned)msg->type, (int)s_last_send_status,
(unsigned long)(attempt + 1), (unsigned long)max_attempts);
}
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
continue;
}
}
result = ESP_OK;
goto out;
}
out:
xSemaphoreGive(s_send_lock);
return result;
}
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
return send_with_backpressure(dest_mac, msg, ESPNOW_SEND_MAX_ATTEMPTS,
ESPNOW_NOMEM_RETRY_DELAY_MS,
ESPNOW_SEND_RETRY_DELAY_MS, false);
}
esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
return send_with_backpressure(dest_mac, msg, ESPNOW_RELIABLE_MAX_ATTEMPTS,
ESPNOW_RELIABLE_NOMEM_DELAY_MS,
ESPNOW_SEND_RETRY_DELAY_MS, true);
}
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
if (s_send_lock == NULL ||
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
return ESP_ERR_TIMEOUT;
}
uint8_t buf[ESPNOW_PB_MAX_SIZE];
size_t len = 0;
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "encode failed");
xSemaphoreGive(s_send_lock);
return err;
}
if (len > ESP_NOW_MAX_DATA_LEN) {
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
(unsigned)ESP_NOW_MAX_DATA_LEN);
xSemaphoreGive(s_send_lock);
return ESP_ERR_INVALID_SIZE;
}
if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) {
xSemaphoreGive(s_send_lock);
return ESP_FAIL;
}
err = esp_now_send(dest_mac, buf, len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
esp_err_to_name(err));
}
xSemaphoreGive(s_send_lock);
return err;
}
esp_err_t esp_now_core_send(const uint8_t *dest_mac,
const alox_EspNowMessage *msg) {
return esp_now_core_send_wait(dest_mac, msg);
}
esp_err_t esp_now_core_init_radio(uint8_t channel) {
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
wifi_config_t wifi_config = {0};
wifi_config.sta.channel = channel;
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE));
ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA));
s_wifi_channel = channel;
return ESP_OK;
}
void esp_now_core_init_send_done(void) {
s_send_done = xSemaphoreCreateBinary();
s_send_lock = xSemaphoreCreateMutex();
if (s_send_done != NULL && s_send_lock != NULL &&
esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) {
s_send_cb_ready = true;
} else {
ESP_LOGW(TAG, "send-done callback unavailable (OTA may drop packets)");
}
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef ESP_NOW_CORE_H
#define ESP_NOW_CORE_H
#include "app_config.h"
#include "esp_err.h"
#include "esp_now_messages.pb.h"
#include <stdbool.h>
#include <stdint.h>
void esp_now_core_store_config(const app_config_t *config);
const app_config_t *esp_now_core_get_config(void);
bool esp_now_core_is_master(void);
uint8_t esp_now_core_network(void);
uint8_t esp_now_core_wifi_channel(void);
const uint8_t *esp_now_core_own_mac(void);
uint32_t esp_now_core_now_ms(void);
bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b);
void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len);
esp_err_t esp_now_core_ensure_peer(const uint8_t *mac);
esp_err_t esp_now_core_ensure_broadcast_peer(void);
esp_err_t esp_now_core_send(const uint8_t *dest_mac,
const alox_EspNowMessage *msg);
/** Like send but does not wait for esp_now send-done (lower latency). */
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
const alox_EspNowMessage *msg);
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
const alox_EspNowMessage *msg);
/** OTA payloads: wait for send-done with extra NO_MEM backoff (queue drain). */
esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
const alox_EspNowMessage *msg);
esp_err_t esp_now_core_init_radio(uint8_t channel);
void esp_now_core_init_send_done(void);
#endif
+597
View File
@@ -0,0 +1,597 @@
#include "esp_now_master.h"
#include "client_registry.h"
#include "esp_now_comm.h"
#include "esp_now_core.h"
#include "esp_now_proto.h"
#include "board_input.h"
#include "ota_espnow.h"
#include "ota_session.h"
#include "ota_uart.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include <string.h>
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
0xff, 0xff, 0xff};
#define ESPNOW_DISCOVER_INTERVAL_MS 500
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
#define ESPNOW_HEARTBEAT_MISS_COUNT 3
#define ESPNOW_CLIENT_TIMEOUT_MS \
(ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT)
#define ESPNOW_BATTERY_INTERVAL_MS 30000
static const char *TAG = "[ESPNOW_M]";
static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id,
bool enable) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM;
msg.which_payload = alox_EspNowMessage_accel_stream_tag;
msg.payload.accel_stream.enable = enable;
msg.payload.accel_stream.client_id = client_id;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id,
uint32_t deadzone) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE;
msg.which_payload = alox_EspNowMessage_accel_deadzone_tag;
msg.payload.accel_deadzone.deadzone = deadzone;
msg.payload.accel_deadzone.client_id = client_id;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST;
msg.which_payload = alox_EspNowMessage_unicast_test_tag;
msg.payload.unicast_test.seq = seq;
return esp_now_core_send(dest_mac, &msg);
}
#define ESPNOW_ECHO_PING_TIMEOUT_MS 500
#define ECHO_PING_TAG "[ECHO_PING]"
static SemaphoreHandle_t s_echo_pong_sem;
static bool s_echo_waiting;
static uint64_t s_echo_expect_ts;
static uint32_t s_echo_esp_rtt_us;
static uint8_t s_echo_expect_mac[ESP_NOW_ETH_ALEN];
static esp_err_t echo_ping_init(void) {
if (s_echo_pong_sem != NULL) {
return ESP_OK;
}
s_echo_pong_sem = xSemaphoreCreateBinary();
if (s_echo_pong_sem == NULL) {
return ESP_ERR_NO_MEM;
}
return ESP_OK;
}
static esp_err_t send_echo_ping(const uint8_t *dest_mac, uint64_t host_timestamp_us,
uint64_t master_time_us) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_ECHO_PING;
msg.which_payload = alox_EspNowMessage_echo_ping_tag;
msg.payload.echo_ping.host_timestamp_us = host_timestamp_us;
msg.payload.echo_ping.master_time_us = master_time_us;
char mac_str[18];
esp_now_core_mac_to_str(dest_mac, mac_str, sizeof(mac_str));
ESP_LOGI(ECHO_PING_TAG, "ESP-NOW PING send to %s host_ts=%llu master_time_us=%llu",
mac_str, (unsigned long long)host_timestamp_us,
(unsigned long long)master_time_us);
return esp_now_core_send_fast(dest_mac, &msg);
}
static void echo_ping_on_pong(const uint8_t mac[ESP_NOW_ETH_ALEN],
const alox_EspNowEchoPong *pong) {
if (pong == NULL || !s_echo_waiting) {
return;
}
if (!esp_now_core_mac_equal(mac, s_echo_expect_mac)) {
return;
}
if (pong->host_timestamp_us != s_echo_expect_ts) {
return;
}
int64_t now_us = esp_timer_get_time();
s_echo_esp_rtt_us =
(uint32_t)(now_us - (int64_t)pong->master_time_us);
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
ESP_LOGI(ECHO_PING_TAG,
"ESP-NOW PONG recv from %s host_ts=%llu master_time_us=%llu "
"recv_time_us=%lld esp_rtt_us=%lu",
mac_str, (unsigned long long)pong->host_timestamp_us,
(unsigned long long)pong->master_time_us, (long long)now_us,
(unsigned long)s_echo_esp_rtt_us);
xSemaphoreGive(s_echo_pong_sem);
}
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 esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id,
const alox_LedRingProgressRequest *req) {
if (req == NULL) {
return ESP_ERR_INVALID_ARG;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_LED_RING;
msg.which_payload = alox_EspNowMessage_led_ring_tag;
msg.payload.led_ring.client_id = client_id;
msg.payload.led_ring.mode = req->mode;
msg.payload.led_ring.progress = req->progress;
msg.payload.led_ring.digit = req->digit;
msg.payload.led_ring.r = req->r;
msg.payload.led_ring.g = req->g;
msg.payload.led_ring.b = req->b;
msg.payload.led_ring.intensity = req->intensity;
msg.payload.led_ring.blink_ms = req->blink_ms;
msg.payload.led_ring.blink_count = req->blink_count;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_RESTART;
msg.which_payload = alox_EspNowMessage_restart_tag;
msg.payload.restart.client_id = client_id;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id,
bool single, bool double_tap, bool triple) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY;
msg.which_payload = alox_EspNowMessage_tap_notify_tag;
msg.payload.tap_notify.client_id = client_id;
msg.payload.tap_notify.single = single;
msg.payload.tap_notify.double_tap = double_tap;
msg.payload.tap_notify.triple = triple;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_OTA_START;
msg.which_payload = alox_EspNowMessage_ota_start_tag;
msg.payload.ota_start.total_size = total_size;
return esp_now_core_send_wait(dest_mac, &msg);
}
static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq,
const uint8_t *data, size_t len) {
if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) {
return ESP_ERR_INVALID_ARG;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD;
msg.which_payload = alox_EspNowMessage_ota_payload_tag;
msg.payload.ota_payload.seq = seq;
msg.payload.ota_payload.data.size = len;
memcpy(msg.payload.ota_payload.data.bytes, data, len);
return esp_now_core_send_reliable(dest_mac, &msg);
}
static esp_err_t send_ota_end(const uint8_t *dest_mac) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_OTA_END;
msg.which_payload = alox_EspNowMessage_ota_end_tag;
return esp_now_core_send_wait(dest_mac, &msg);
}
esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t total_size) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
return send_ota_start(mac, total_size);
}
esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t seq, const uint8_t *data,
size_t len) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
return send_ota_payload(mac, seq, data, len);
}
esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
return send_ota_end(mac);
}
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_restart(mac, client_id);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast RESTART to %s client_id=%lu", mac_str,
(unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast RESTART to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_find_me(mac, client_id);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast FIND_ME to %s client_id=%lu", mac_str,
(unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast FIND_ME to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id,
const alox_LedRingProgressRequest *req) {
if (mac == NULL || !esp_now_core_is_master() || req == NULL) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_led_ring(mac, client_id, req);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu",
(unsigned long)req->mode, mac_str, (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t seq) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_unicast_test(mac, seq);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast TEST to %s seq=%lu", mac_str, (unsigned long)seq);
} else {
ESP_LOGW(TAG, "unicast TEST to %s failed: %s", mac_str, esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_echo_ping(const uint8_t mac[CLIENT_MAC_LEN],
uint64_t timestamp_us,
esp_now_echo_ping_result_t *result) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
if (echo_ping_init() != ESP_OK) {
return ESP_ERR_NO_MEM;
}
xSemaphoreTake(s_echo_pong_sem, 0);
s_echo_waiting = true;
s_echo_expect_ts = timestamp_us;
s_echo_esp_rtt_us = 0;
memcpy(s_echo_expect_mac, mac, ESP_NOW_ETH_ALEN);
uint64_t master_time_us = (uint64_t)esp_timer_get_time();
esp_err_t err = send_echo_ping(mac, timestamp_us, master_time_us);
if (err != ESP_OK) {
s_echo_waiting = false;
return err;
}
if (xSemaphoreTake(s_echo_pong_sem,
pdMS_TO_TICKS(ESPNOW_ECHO_PING_TIMEOUT_MS)) != pdTRUE) {
s_echo_waiting = false;
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
ESP_LOGW(ECHO_PING_TAG,
"ESP-NOW PONG timeout to %s host_ts=%llu",
mac_str, (unsigned long long)timestamp_us);
return ESP_ERR_TIMEOUT;
}
s_echo_waiting = false;
if (result != NULL) {
result->echoed_us = timestamp_us;
result->esp_rtt_us = s_echo_esp_rtt_us;
}
return ESP_OK;
}
esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, bool enable) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_accel_stream(mac, client_id, enable);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str,
enable ? "on" : "off", (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id, bool single,
bool double_tap, bool triple) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err =
send_tap_notify(mac, client_id, single, double_tap, triple);
if (err == ESP_OK) {
ESP_LOGI(TAG,
"unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d "
"client_id=%lu",
mac_str, single, double_tap, triple, (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id,
uint32_t deadzone) {
if (mac == NULL || !esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
esp_err_t err = send_accel_deadzone(mac, client_id, deadzone);
if (err == ESP_OK) {
ESP_LOGI(TAG, "unicast SET_ACCEL_DEADZONE to %s: deadzone=%lu client_id=%lu",
mac_str, (unsigned long)deadzone, (unsigned long)client_id);
} else {
ESP_LOGW(TAG, "unicast SET_ACCEL_DEADZONE to %s failed: %s", mac_str,
esp_err_to_name(err));
}
return err;
}
static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
const alox_EspNowAccelSample *sample) {
if (sample == NULL) {
return;
}
esp_err_t err = client_registry_update_accel(
mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y,
(int16_t)sample->z);
if (err == ESP_ERR_NOT_FOUND) {
return;
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]);
}
}
static void handle_tap_event(const uint8_t mac[CLIENT_MAC_LEN],
const alox_EspNowTapEvent *event) {
if (event == NULL) {
return;
}
esp_err_t err =
client_registry_update_tap(mac, event->slave_id, event->kind);
if (err == ESP_ERR_NOT_FOUND) {
return;
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x",
(unsigned long)event->slave_id, (unsigned long)event->kind, mac[0],
mac[5]);
}
}
static void handle_battery_report(const uint8_t mac[CLIENT_MAC_LEN],
const alox_EspNowBatteryReport *report) {
if (report == NULL) {
return;
}
esp_err_t err = client_registry_update_battery(
mac, report->client_id, report->lipo1_valid, report->lipo1_mv,
report->lipo2_valid, report->lipo2_mv);
if (err == ESP_ERR_NOT_FOUND) {
ESP_LOGW(TAG, "battery report from unregistered slave id=%lu",
(unsigned long)report->client_id);
return;
}
if (err != ESP_OK) {
ESP_LOGW(TAG, "battery report id=%lu rejected: %s",
(unsigned long)report->client_id, esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "battery cached id=%lu L1=%s %lu mV L2=%s %lu mV",
(unsigned long)report->client_id,
report->lipo1_valid ? "ok" : "n/a",
(unsigned long)report->lipo1_mv, report->lipo2_valid ? "ok" : "n/a",
(unsigned long)report->lipo2_mv);
}
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
const uint8_t mac[CLIENT_MAC_LEN]) {
if (presence->network != esp_now_core_network()) {
return;
}
esp_now_core_ensure_peer(mac);
bool is_new = false;
bool reactivated = false;
esp_err_t err = client_registry_heartbeat(
mac, presence->slave_id, presence->version, presence->used, &is_new,
&reactivated);
if (err != ESP_OK) {
ESP_LOGW(TAG, "client registry full");
return;
}
char mac_str[18];
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
if (is_new) {
ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu",
(unsigned long)presence->slave_id, mac_str,
(unsigned long)presence->version);
} else if (reactivated) {
ESP_LOGI(TAG, "client reconnected id=%lu mac=%s",
(unsigned long)presence->slave_id, mac_str);
}
}
void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
int len) {
if (info == NULL || data == NULL || len <= 0) {
return;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed (%d bytes)", len);
return;
}
if (ota_espnow_distribution_active()) {
if (msg.which_payload == alox_EspNowMessage_ota_status_tag) {
esp_now_core_ensure_peer(info->src_addr);
ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status);
}
return;
}
if (msg.which_payload == alox_EspNowMessage_ota_status_tag) {
esp_now_core_ensure_peer(info->src_addr);
ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status);
return;
}
if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) {
esp_now_core_ensure_peer(info->src_addr);
handle_accel_sample(info->src_addr, &msg.payload.accel_sample);
return;
}
if (msg.which_payload == alox_EspNowMessage_tap_event_tag) {
esp_now_core_ensure_peer(info->src_addr);
handle_tap_event(info->src_addr, &msg.payload.tap_event);
return;
}
if (msg.which_payload == alox_EspNowMessage_battery_report_tag) {
esp_now_core_ensure_peer(info->src_addr);
handle_battery_report(info->src_addr, &msg.payload.battery_report);
return;
}
if (msg.which_payload == alox_EspNowMessage_echo_pong_tag) {
esp_now_core_ensure_peer(info->src_addr);
echo_ping_on_pong(info->src_addr, &msg.payload.echo_pong);
return;
}
if (msg.type == alox_EspNowMessageType_ESPNOW_BATTERY_REPORT &&
msg.which_payload != alox_EspNowMessage_battery_report_tag) {
ESP_LOGW(TAG, "BATTERY_REPORT type but which=%u", msg.which_payload);
}
const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg);
if (presence != NULL) {
esp_now_core_ensure_peer(info->src_addr);
handle_client_presence(presence, info->src_addr);
}
}
static void discover_task(void *param) {
(void)param;
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER;
msg.which_payload = alox_EspNowMessage_discover_tag;
msg.payload.discover.network = esp_now_core_network();
ESP_LOGI(TAG, "discover on network %u ch %u", (unsigned)esp_now_core_network(),
(unsigned)esp_now_core_wifi_channel());
while (1) {
msg.payload.discover.master_ota_pending = ota_session_busy();
if (!ota_espnow_distribution_active()) {
esp_now_core_send_fast(ESPNOW_BCAST, &msg);
}
vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS));
}
}
static void monitor_task(void *param) {
(void)param;
uint32_t last_local_battery_ms = 0;
ESP_LOGI(TAG, "monitor (client timeout %u ms)",
(unsigned)ESPNOW_CLIENT_TIMEOUT_MS);
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
last_local_battery_ms = esp_now_core_now_ms();
while (1) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS);
uint32_t t = esp_now_core_now_ms();
if (t - last_local_battery_ms >= ESPNOW_BATTERY_INTERVAL_MS) {
board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
last_local_battery_ms = t;
}
}
}
esp_err_t esp_now_master_start(void) {
ESP_ERROR_CHECK(esp_now_core_ensure_broadcast_peer());
if (xTaskCreate(discover_task, "espnow_disc", 4096, NULL, 4, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create discover task");
return ESP_FAIL;
}
if (xTaskCreate(monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create monitor task");
return ESP_FAIL;
}
return ESP_OK;
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef ESP_NOW_MASTER_H
#define ESP_NOW_MASTER_H
#include "esp_err.h"
#include "esp_now.h"
esp_err_t esp_now_master_start(void);
void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
int len);
#endif
+646
View File
@@ -0,0 +1,646 @@
#include "esp_now_slave.h"
#include "bosch456.h"
#include "cmd_led_ring.h"
#include "esp_now_comm.h"
#include "esp_now_core.h"
#include "esp_now_proto.h"
#include "board_input.h"
#include "led_ring.h"
#include "ota_espnow.h"
#include "ota_uart.h"
#include "pod_reboot.h"
#include "pod_settings.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/idf_additions.h"
#include <string.h>
#ifndef POWERPOD_FW_VERSION
#define POWERPOD_FW_VERSION 1u
#endif
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
/** While master or slave OTA is in progress (discover may be sparse). */
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
#define ESPNOW_ACCEL_INTERVAL_MS 16
#define ESPNOW_BATTERY_INTERVAL_MS 30000
#define SLAVE_BATTERY_AFTER_JOIN_MS 150
static const char *TAG = "[ESPNOW_S]";
static bool s_joined;
static bool s_master_ota_grace;
static bool s_accel_stream_enabled;
static bool s_tap_notify_single;
static bool s_tap_notify_double;
static bool s_tap_notify_triple;
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
static uint32_t s_last_discover_ms;
typedef enum {
SLAVE_TX_SLAVE_INFO = 1,
SLAVE_TX_BATTERY,
} slave_tx_op_t;
static QueueHandle_t s_tx_queue;
static bool from_joined_master(const uint8_t *master_mac) {
return s_joined && esp_now_core_mac_equal(master_mac, s_master_mac);
}
static void fill_presence(alox_EspNowSlavePresence *presence) {
const uint8_t *own = esp_now_core_own_mac();
presence->network = esp_now_core_network();
presence->version = POWERPOD_FW_VERSION;
presence->slave_id = own[5];
presence->available = true;
presence->used = false;
esp_now_proto_setup_presence_encode(presence, own);
}
static void send_presence(const uint8_t *dest_mac, alox_EspNowMessageType type) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
alox_EspNowSlavePresence *presence = NULL;
msg.type = type;
if (type == alox_EspNowMessageType_ESPNOW_SLAVE_INFO) {
msg.which_payload = alox_EspNowMessage_slave_info_tag;
presence = &msg.payload.slave_info;
} else {
msg.which_payload = alox_EspNowMessage_heartbeat_tag;
presence = &msg.payload.heartbeat;
}
fill_presence(presence);
esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id,
int16_t x, int16_t y, int16_t z) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE;
msg.which_payload = alox_EspNowMessage_accel_sample_tag;
msg.payload.accel_sample.slave_id = slave_id;
msg.payload.accel_sample.x = x;
msg.payload.accel_sample.y = y;
msg.payload.accel_sample.z = z;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
uint32_t kind) {
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT;
msg.which_payload = alox_EspNowMessage_tap_event_tag;
msg.payload.tap_event.slave_id = slave_id;
msg.payload.tap_event.kind = kind;
return esp_now_core_send(dest_mac, &msg);
}
static esp_err_t send_battery_report(const uint8_t *dest_mac,
const alox_EspNowBatteryReport *report) {
if (report == NULL) {
return ESP_ERR_INVALID_ARG;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_BATTERY_REPORT;
msg.which_payload = alox_EspNowMessage_battery_report_tag;
msg.payload.battery_report = *report;
return esp_now_core_send(dest_mac, &msg);
}
static uint32_t slave_master_lost_ms(void) {
if (ota_uart_is_active() || s_master_ota_grace) {
return SLAVE_MASTER_OTA_GRACE_MS;
}
return SLAVE_MASTER_LOST_MS;
}
static void touch_master_presence(uint32_t now) { s_last_discover_ms = now; }
static void reset_join(void) {
s_joined = false;
s_master_ota_grace = false;
s_accel_stream_enabled = false;
memset(s_master_mac, 0, sizeof(s_master_mac));
s_last_discover_ms = 0;
if (s_tx_queue != NULL) {
xQueueReset(s_tx_queue);
}
}
static void queue_tx(slave_tx_op_t op) {
if (s_tx_queue == NULL) {
return;
}
if (xQueueSend(s_tx_queue, &op, 0) != pdTRUE) {
ESP_LOGW(TAG, "tx queue full (op=%d)", (int)op);
}
}
static void send_battery_to_master(void) {
if (!s_joined) {
return;
}
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero;
report.client_id = esp_now_core_own_mac()[5];
report.lipo1_valid = reading.lipo1_valid;
report.lipo2_valid = reading.lipo2_valid;
report.lipo1_mv = reading.lipo1_mv;
report.lipo2_mv = reading.lipo2_mv;
esp_err_t err = send_battery_report(s_master_mac, &report);
if (err != ESP_OK) {
ESP_LOGW(TAG, "battery report send failed id=%lu: %s",
(unsigned long)report.client_id, esp_err_to_name(err));
} else {
ESP_LOGI(TAG, "battery report sent id=%lu L1=%s %lu mV L2=%s %lu mV",
(unsigned long)report.client_id,
report.lipo1_valid ? "ok" : "n/a",
(unsigned long)report.lipo1_mv,
report.lipo2_valid ? "ok" : "n/a",
(unsigned long)report.lipo2_mv);
}
}
esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN],
uint32_t status, uint32_t bytes_written,
uint32_t error) {
if (master_mac == NULL || esp_now_core_is_master()) {
return ESP_ERR_INVALID_STATE;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS;
msg.which_payload = alox_EspNowMessage_ota_status_tag;
msg.payload.ota_status.status = status;
msg.payload.ota_status.bytes_written = bytes_written;
msg.payload.ota_status.error = error;
return esp_now_core_send_wait(master_mac, &msg);
}
bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
return esp_now_slave_get_master_mac(mac_out);
}
bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
if (mac_out == NULL || !s_joined) {
return false;
}
memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN);
return true;
}
static void tx_task(void *param) {
(void)param;
slave_tx_op_t op;
ESP_LOGI(TAG, "deferred tx task ready");
while (1) {
if (xQueueReceive(s_tx_queue, &op, portMAX_DELAY) != pdTRUE) {
continue;
}
if (!s_joined) {
continue;
}
switch (op) {
case SLAVE_TX_SLAVE_INFO:
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
break;
case SLAVE_TX_BATTERY:
vTaskDelay(pdMS_TO_TICKS(SLAVE_BATTERY_AFTER_JOIN_MS));
send_battery_to_master();
break;
default:
break;
}
}
}
static void handle_unicast_test(const uint8_t *master_mac,
const alox_EspNowUnicastTest *test) {
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "UNICAST TEST OK from master %s seq=%lu (joined=%d)", mac_str,
(unsigned long)test->seq, (int)s_joined);
}
static void handle_echo_ping(const uint8_t *master_mac,
const alox_EspNowEchoPing *ping) {
if (ping == NULL || !s_joined ||
!esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "ESP-NOW PING recv from %s host_ts=%llu master_time_us=%llu",
mac_str, (unsigned long long)ping->host_timestamp_us,
(unsigned long long)ping->master_time_us);
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
msg.type = alox_EspNowMessageType_ESPNOW_ECHO_PONG;
msg.which_payload = alox_EspNowMessage_echo_pong_tag;
msg.payload.echo_pong.host_timestamp_us = ping->host_timestamp_us;
msg.payload.echo_pong.master_time_us = ping->master_time_us;
esp_err_t err = esp_now_core_send_fast(s_master_mac, &msg);
if (err != ESP_OK) {
ESP_LOGW(TAG, "ECHO PONG send failed: %s", esp_err_to_name(err));
return;
}
ESP_LOGI(TAG, "ESP-NOW PONG send to %s host_ts=%llu master_time_us=%llu",
mac_str, (unsigned long long)ping->host_timestamp_us,
(unsigned long long)ping->master_time_us);
}
static void handle_restart(const uint8_t *master_mac,
const alox_EspNowRestart *req) {
const uint8_t *own = esp_now_core_own_mac();
uint32_t my_id = own[5];
if (req->client_id != 0 && req->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id);
pod_schedule_restart();
}
static void handle_battery_query(const uint8_t *master_mac,
const alox_EspNowBatteryQuery *query) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (query->client_id != 0 && query->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
send_battery_to_master();
}
static void handle_led_ring(const uint8_t *master_mac,
const alox_EspNowLedRing *msg) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (msg->client_id != 0 && msg->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
req.mode = msg->mode;
req.progress = msg->progress;
req.digit = msg->digit;
req.r = msg->r;
req.g = msg->g;
req.b = msg->b;
req.intensity = msg->intensity;
req.blink_ms = msg->blink_ms;
req.blink_count = msg->blink_count;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)",
(unsigned long)req.mode, mac_str, (unsigned long)my_id);
cmd_led_ring_apply(&req);
}
static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (req->client_id != 0 && req->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, (unsigned long)my_id);
led_ring_find_me();
}
static void handle_accel_stream(const uint8_t *master_mac,
const alox_EspNowAccelStream *cfg) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (cfg->client_id != 0 && cfg->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
s_accel_stream_enabled = cfg->enable;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "accel stream %s from master %s (id=%lu)",
cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id);
}
static void handle_tap_notify(const uint8_t *master_mac,
const alox_EspNowTapNotify *cfg) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (cfg->client_id != 0 && cfg->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
s_tap_notify_single = cfg->single;
s_tap_notify_double = cfg->double_tap;
s_tap_notify_triple = cfg->triple;
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG,
"tap notify single=%d double=%d triple=%d from master %s (id=%lu)",
cfg->single, cfg->double_tap, cfg->triple, mac_str,
(unsigned long)my_id);
}
static void handle_accel_deadzone(const uint8_t *master_mac,
const alox_EspNowAccelDeadzone *cfg) {
uint32_t my_id = esp_now_core_own_mac()[5];
if (cfg->client_id != 0 && cfg->client_id != my_id) {
return;
}
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
return;
}
char mac_str[18];
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG,
"accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", mac_str,
(unsigned long)cfg->deadzone, (unsigned long)my_id,
bma456_is_ready() ? "ok" : "not installed");
bma456_set_accel_deadzone(cfg->deadzone);
if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) {
ESP_LOGW(TAG, "deadzone %lu applied but not saved to NVS",
(unsigned long)cfg->deadzone);
}
}
static void handle_discover(const uint8_t *sender_mac,
const alox_EspNowDiscover *discover) {
if (discover->network != esp_now_core_network()) {
return;
}
uint32_t now = esp_now_core_now_ms();
if (s_joined) {
if (!esp_now_core_mac_equal(sender_mac, s_master_mac)) {
return;
}
s_master_ota_grace = discover->master_ota_pending;
if ((now - s_last_discover_ms) <= slave_master_lost_ms()) {
touch_master_presence(now);
return;
}
if (ota_uart_is_active()) {
touch_master_presence(now);
return;
}
ESP_LOGW(TAG, "master lost, rejoining");
reset_join();
}
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
s_joined = true;
s_master_ota_grace = discover->master_ota_pending;
touch_master_presence(now);
esp_now_core_ensure_peer(sender_mac);
char mac_str[18];
esp_now_core_mac_to_str(sender_mac, mac_str, sizeof(mac_str));
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network,
mac_str);
queue_tx(SLAVE_TX_SLAVE_INFO);
queue_tx(SLAVE_TX_BATTERY);
}
static void check_master_timeout(void) {
if (!s_joined || s_last_discover_ms == 0) {
return;
}
if (ota_uart_is_active()) {
return;
}
uint32_t now = esp_now_core_now_ms();
uint32_t limit = slave_master_lost_ms();
if ((now - s_last_discover_ms) > limit) {
ESP_LOGW(TAG, "no master discover for %u ms (limit %u), reconnecting",
(unsigned)(now - s_last_discover_ms), (unsigned)limit);
reset_join();
}
}
static void accel_stream_task(void *param) {
(void)param;
const uint8_t *own = esp_now_core_own_mac();
ESP_LOGI(TAG, "accel stream task (interval %u ms)",
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
while (1) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS));
if (!s_joined || !s_accel_stream_enabled || !bma456_is_ready()) {
continue;
}
int16_t x = 0;
int16_t y = 0;
int16_t z = 0;
if (bma456_read_accel(&x, &y, &z) != ESP_OK) {
continue;
}
(void)send_accel_sample(s_master_mac, own[5], x, y, z);
}
}
static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) {
(void)ctx;
if (!s_joined) {
return;
}
bool enabled = false;
switch (kind) {
case BMA456_TAP_SINGLE:
enabled = s_tap_notify_single;
break;
case BMA456_TAP_DOUBLE:
enabled = s_tap_notify_double;
break;
case BMA456_TAP_TRIPLE:
enabled = s_tap_notify_triple;
break;
default:
return;
}
if (!enabled) {
return;
}
(void)send_tap_event(s_master_mac, esp_now_core_own_mac()[5], (uint32_t)kind);
}
static void heartbeat_task(void *param) {
(void)param;
uint32_t last_battery_ms = 0;
ESP_LOGI(TAG, "heartbeat task (interval %u ms)",
(unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS);
while (1) {
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
check_master_timeout();
if (!s_joined) {
last_battery_ms = 0;
continue;
}
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
uint32_t now = esp_now_core_now_ms();
if (last_battery_ms == 0 ||
(now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) {
send_battery_to_master();
last_battery_ms = now;
}
}
}
void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
int len) {
if (info == NULL || data == NULL || len <= 0) {
return;
}
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed (%d bytes)", len);
return;
}
if (from_joined_master(info->src_addr)) {
esp_now_core_ensure_peer(info->src_addr);
}
if (ota_uart_is_active()) {
switch (msg.which_payload) {
case alox_EspNowMessage_discover_tag:
handle_discover(info->src_addr, &msg.payload.discover);
break;
case alox_EspNowMessage_ota_start_tag:
case alox_EspNowMessage_ota_payload_tag:
case alox_EspNowMessage_ota_end_tag:
if (!from_joined_master(info->src_addr)) {
break;
}
touch_master_presence(esp_now_core_now_ms());
if (msg.which_payload == alox_EspNowMessage_ota_start_tag) {
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
} else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) {
ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload);
} else {
ota_espnow_slave_on_end(info->src_addr);
}
break;
default:
break;
}
return;
}
switch (msg.which_payload) {
case alox_EspNowMessage_discover_tag:
handle_discover(info->src_addr, &msg.payload.discover);
break;
case alox_EspNowMessage_unicast_test_tag:
if (from_joined_master(info->src_addr)) {
handle_unicast_test(info->src_addr, &msg.payload.unicast_test);
}
break;
case alox_EspNowMessage_echo_ping_tag:
if (from_joined_master(info->src_addr)) {
handle_echo_ping(info->src_addr, &msg.payload.echo_ping);
}
break;
case alox_EspNowMessage_accel_deadzone_tag:
if (from_joined_master(info->src_addr)) {
handle_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone);
}
break;
case alox_EspNowMessage_accel_stream_tag:
if (from_joined_master(info->src_addr)) {
handle_accel_stream(info->src_addr, &msg.payload.accel_stream);
}
break;
case alox_EspNowMessage_tap_notify_tag:
if (from_joined_master(info->src_addr)) {
handle_tap_notify(info->src_addr, &msg.payload.tap_notify);
}
break;
case alox_EspNowMessage_battery_query_tag:
if (from_joined_master(info->src_addr)) {
handle_battery_query(info->src_addr, &msg.payload.battery_query);
}
break;
case alox_EspNowMessage_led_ring_tag:
if (from_joined_master(info->src_addr)) {
handle_led_ring(info->src_addr, &msg.payload.led_ring);
}
break;
case alox_EspNowMessage_find_me_tag:
if (from_joined_master(info->src_addr)) {
handle_find_me(info->src_addr, &msg.payload.find_me);
}
break;
case alox_EspNowMessage_restart_tag:
if (from_joined_master(info->src_addr)) {
handle_restart(info->src_addr, &msg.payload.restart);
}
break;
case alox_EspNowMessage_ota_start_tag:
if (from_joined_master(info->src_addr)) {
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
}
break;
default:
ESP_LOGW(TAG, "unhandled which=%u type=%u", msg.which_payload,
(unsigned)msg.type);
break;
}
}
esp_err_t esp_now_slave_start(void) {
reset_join();
s_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t));
if (s_tx_queue == NULL) {
ESP_LOGE(TAG, "failed to create tx queue");
return ESP_ERR_NO_MEM;
}
if (xTaskCreate(tx_task, "espnow_stx", 4096, NULL, 5, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create tx task");
return ESP_FAIL;
}
if (xTaskCreate(heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create heartbeat task");
return ESP_FAIL;
}
if (xTaskCreate(accel_stream_task, "espnow_accel", 4096, NULL, 5, NULL) !=
pdPASS) {
ESP_LOGE(TAG, "failed to create accel stream task");
return ESP_FAIL;
}
ota_espnow_slave_init();
bma456_set_tap_handler(on_bma456_tap, NULL);
return ESP_OK;
}
+14
View File
@@ -0,0 +1,14 @@
#ifndef ESP_NOW_SLAVE_H
#define ESP_NOW_SLAVE_H
#include "client_registry.h"
#include "esp_err.h"
#include "esp_now.h"
esp_err_t esp_now_slave_start(void);
void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
int len);
bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]);
#endif
+33 -1
View File
@@ -5,6 +5,7 @@
#include "esp_log.h" #include "esp_log.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "freertos/semphr.h"
#include "led_strip.h" #include "led_strip.h"
#include <stdint.h> #include <stdint.h>
@@ -23,8 +24,12 @@ static led_strip_handle_t led_ring;
#define LED_RING_FIND_ME_ON_MS 300 #define LED_RING_FIND_ME_ON_MS 300
#define LED_RING_FIND_ME_OFF_MS 150 #define LED_RING_FIND_ME_OFF_MS 150
#define LED_RING_FIND_ME_BLINKS_PER_COLOR 3 #define LED_RING_FIND_ME_BLINKS_PER_COLOR 3
#define LED_RING_BATTERY_LOW_R 255
#define LED_RING_BATTERY_LOW_G 0
#define LED_RING_BATTERY_LOW_B 0
static QueueHandle_t led_queue; static QueueHandle_t led_queue;
static SemaphoreHandle_t s_battery_low_done;
// Led Matrix Maps // Led Matrix Maps
const uint8_t d0[] = {46, 47, 60, 61, 62, 75, 78, 79, const uint8_t d0[] = {46, 47, 60, 61, 62, 75, 78, 79,
@@ -145,6 +150,22 @@ void vTaskLedRing(void *pvParameters) {
} }
} }
continue; continue;
} else if (cmd.mode == LED_CMD_BATTERY_LOW) {
uint8_t r = LED_RING_BATTERY_LOW_R;
uint8_t g = LED_RING_BATTERY_LOW_G;
uint8_t b = LED_RING_BATTERY_LOW_B;
led_ring_scale_rgb(&r, &g, &b, LED_RING_BATTERY_LOW_INTENSITY);
for (uint32_t i = 0; i < LED_RING_BATTERY_LOW_LEDS; i++) {
led_strip_set_pixel(led_ring, i, r, g, b);
}
led_strip_refresh(led_ring);
vTaskDelay(pdMS_TO_TICKS(LED_RING_BATTERY_LOW_HOLD_MS));
led_strip_clear(led_ring);
led_strip_refresh(led_ring);
if (s_battery_low_done != NULL) {
xSemaphoreGive(s_battery_low_done);
}
continue;
} }
led_strip_refresh(led_ring); led_strip_refresh(led_ring);
} }
@@ -153,12 +174,13 @@ void vTaskLedRing(void *pvParameters) {
void led_ring_init(void) { void led_ring_init(void) {
led_queue = xQueueCreate(10, sizeof(led_command_t)); led_queue = xQueueCreate(10, sizeof(led_command_t));
s_battery_low_done = xSemaphoreCreateBinary();
xTaskCreate(vTaskLedRing, "led_task", 4096, NULL, 5, NULL); xTaskCreate(vTaskLedRing, "led_task", 4096, NULL, 5, NULL);
} }
void led_ring_send_command(led_command_t *cmd) { void led_ring_send_command(led_command_t *cmd) {
if (led_queue != NULL) { if (led_queue != NULL) {
xQueueSend(led_queue, cmd, portMAX_DELAY); (void)xQueueSend(led_queue, cmd, 0);
} }
} }
@@ -224,3 +246,13 @@ void led_ring_find_me(void) {
led_command_t cmd = {.mode = LED_CMD_FIND_ME}; led_command_t cmd = {.mode = LED_CMD_FIND_ME};
led_ring_send_command(&cmd); led_ring_send_command(&cmd);
} }
void led_ring_show_battery_low(void) {
if (led_queue == NULL || s_battery_low_done == NULL) {
return;
}
(void)xSemaphoreTake(s_battery_low_done, 0);
led_command_t cmd = {.mode = LED_CMD_BATTERY_LOW};
led_ring_send_command(&cmd);
(void)xSemaphoreTake(s_battery_low_done, portMAX_DELAY);
}
+9 -1
View File
@@ -7,6 +7,10 @@
#define LED_RING_DEFAULT_INTENSITY 13 #define LED_RING_DEFAULT_INTENSITY 13
/** Full brightness for find-me and similar alerts. */ /** Full brightness for find-me and similar alerts. */
#define LED_RING_FULL_INTENSITY 255 #define LED_RING_FULL_INTENSITY 255
/** ~10 % brightness for battery-low indicator. */
#define LED_RING_BATTERY_LOW_INTENSITY 26
#define LED_RING_BATTERY_LOW_LEDS 4
#define LED_RING_BATTERY_LOW_HOLD_MS 5000
typedef enum { typedef enum {
LED_CMD_CLEAR, LED_CMD_CLEAR,
@@ -14,7 +18,8 @@ typedef enum {
LED_CMD_SET_COLOR, LED_CMD_SET_COLOR,
LED_CMD_PROGRESS, LED_CMD_PROGRESS,
LED_CMD_BLINK, LED_CMD_BLINK,
LED_CMD_FIND_ME LED_CMD_FIND_ME,
LED_CMD_BATTERY_LOW
} led_mode_t; } led_mode_t;
typedef struct { typedef struct {
@@ -45,4 +50,7 @@ void led_ring_ota_failed(void);
/** Red / green / blue: 3 blinks each at full intensity. */ /** Red / green / blue: 3 blinks each at full intensity. */
void led_ring_find_me(void); void led_ring_find_me(void);
/** First 4 LEDs red at ~10 % for 5 s, then off (blocking in LED task). */
void led_ring_show_battery_low(void);
#endif #endif
+298 -82
View File
@@ -18,9 +18,9 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_ESPNOW_PREPARE_PRIO 5 #define OTA_ESPNOW_PREPARE_PRIO 5
#define OTA_PREPARE_TIMEOUT_MS 120000u #define OTA_PREPARE_TIMEOUT_MS 120000u
#define OTA_BLOCK_TIMEOUT_MS 30000u #define OTA_BLOCK_TIMEOUT_PER_SLAVE_MS 2000u
#define OTA_BLOCK_MAX_RETRIES 2u
#define OTA_END_TIMEOUT_MS 60000u #define OTA_END_TIMEOUT_MS 60000u
#define OTA_PAYLOAD_DELAY_MS 3
#define OTA_ST_PREPARING 1u #define OTA_ST_PREPARING 1u
#define OTA_ST_READY 2u #define OTA_ST_READY 2u
@@ -35,7 +35,29 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX #define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
/** ~21 payloads per 4 KiB block; headroom for bursts + status/end. */
#define OTA_SLAVE_WORK_QUEUE_LEN 32
#define OTA_SLAVE_WORK_STACK 8192
#define OTA_SLAVE_WORK_PRIO 5
typedef enum {
OTA_SLAVE_WORK_STATUS = 1,
OTA_SLAVE_WORK_PAYLOAD,
OTA_SLAVE_WORK_END,
} ota_slave_work_op_t;
typedef struct {
ota_slave_work_op_t op;
uint8_t master_mac[6];
uint32_t status;
uint32_t bytes_written;
uint32_t error;
alox_EspNowOtaPayload payload;
} ota_slave_work_t;
static EventGroupHandle_t s_eg; static EventGroupHandle_t s_eg;
static QueueHandle_t s_slave_work_queue;
static bool s_distribution_active;
typedef struct { typedef struct {
uint8_t count; uint8_t count;
@@ -152,11 +174,204 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
return (got & want_bits) == want_bits; return (got & want_bits) == want_bits;
} }
static uint32_t block_ack_timeout_ms(void) {
if (s_dist.count == 0) {
return OTA_BLOCK_TIMEOUT_PER_SLAVE_MS;
}
return (uint32_t)s_dist.count * OTA_BLOCK_TIMEOUT_PER_SLAVE_MS;
}
static void log_missing_block_acks(uint32_t expected_bytes) {
if (s_eg == NULL || s_dist.count == 0) {
return;
}
EventBits_t bits = xEventGroupGetBits(s_eg);
for (uint8_t i = 0; i < s_dist.count; i++) {
uint32_t bit = (1u << (unsigned)i);
if (bits & bit) {
continue;
}
const ota_prog_entry_t *e = &s_prog.entries[i];
ESP_LOGE(TAG,
"slave %lu missing block ack @%lu (last status=%lu bytes=%lu err=%lu)",
(unsigned long)s_dist.id[i], (unsigned long)expected_bytes,
(unsigned long)e->status, (unsigned long)e->bytes_written,
(unsigned long)e->error);
}
}
static esp_err_t send_block_payloads(const uint8_t *block_buf, uint32_t block_len,
uint32_t *seq_io) {
uint32_t sent = 0;
while (sent < block_len) {
uint32_t chunk = block_len - sent;
if (chunk > OTA_UART_HOST_CHUNK_SIZE) {
chunk = OTA_UART_HOST_CHUNK_SIZE;
}
for (uint8_t i = 0; i < s_dist.count; i++) {
esp_err_t err = esp_now_comm_send_ota_payload(s_dist.mac[i], *seq_io,
block_buf + sent, chunk);
if (err != ESP_OK) {
return err;
}
}
(*seq_io)++;
sent += chunk;
}
return ESP_OK;
}
bool ota_espnow_distribution_active(void) { return s_distribution_active; }
static void send_slave_status(const uint8_t master_mac[6], uint32_t status, static void send_slave_status(const uint8_t master_mac[6], uint32_t status,
uint32_t bytes_written, uint32_t error) { uint32_t bytes_written, uint32_t error) {
esp_now_comm_send_ota_status(master_mac, status, bytes_written, error); esp_now_comm_send_ota_status(master_mac, status, bytes_written, error);
} }
static bool queue_slave_work(const ota_slave_work_t *work) {
if (work == NULL || s_slave_work_queue == NULL) {
return false;
}
if (xQueueSend(s_slave_work_queue, work, 0) != pdTRUE) {
ESP_LOGW(TAG, "slave OTA work queue full (op=%d)", (int)work->op);
return false;
}
return true;
}
static void queue_slave_status(const uint8_t master_mac[6], uint32_t status,
uint32_t bytes_written, uint32_t error) {
ota_slave_work_t work = {
.op = OTA_SLAVE_WORK_STATUS,
.status = status,
.bytes_written = bytes_written,
.error = error,
};
memcpy(work.master_mac, master_mac, 6);
(void)queue_slave_work(&work);
}
static void process_slave_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload) {
if (payload == NULL || payload->data.size == 0) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
return;
}
if (!ota_uart_is_active()) {
ESP_LOGW(TAG, "OTA_PAYLOAD seq=%lu but no active session",
(unsigned long)payload->seq);
send_slave_status(master_mac, OTA_ST_FAILED, 0, 12);
return;
}
if (payload->seq == 0) {
ESP_LOGI(TAG, "ESP-NOW OTA payloads started");
}
ota_feed_result_t r = ota_uart_feed_chunk(payload->seq, payload->data.bytes,
payload->data.size);
if (r == OTA_FEED_SEQ_GAP) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 16);
return;
}
if (r == OTA_FEED_SEQ_DUP) {
if (ota_uart_block_ready_for_reack()) {
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, ota_uart_bytes_written(),
0);
}
return;
}
if (r == OTA_FEED_ERROR) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13);
return;
}
if (r == OTA_FEED_BLOCK_WRITTEN) {
uint32_t written = ota_uart_bytes_written();
uint32_t total = ota_uart_total_size();
ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written);
led_ring_show_ota_progress(written, total, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0);
return;
}
if (r == OTA_FEED_OK) {
uint32_t total = ota_uart_total_size();
if (total > 0) {
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
}
}
}
static void process_slave_end(const uint8_t master_mac[6]) {
ESP_LOGI(TAG, "ESP-NOW OTA_END");
if (!ota_uart_is_active()) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 20);
return;
}
uint32_t written = ota_uart_bytes_written();
bool success = false;
esp_err_t err = ota_uart_finish(true, &success);
if (err != ESP_OK || !success) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err);
return;
}
send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0);
led_ring_ota_success();
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
(unsigned long)written);
}
static void ota_slave_work_task(void *param) {
(void)param;
ota_slave_work_t work;
while (1) {
if (xQueueReceive(s_slave_work_queue, &work, portMAX_DELAY) != pdTRUE) {
continue;
}
switch (work.op) {
case OTA_SLAVE_WORK_STATUS:
send_slave_status(work.master_mac, work.status, work.bytes_written,
work.error);
break;
case OTA_SLAVE_WORK_PAYLOAD:
process_slave_payload(work.master_mac, &work.payload);
break;
case OTA_SLAVE_WORK_END:
process_slave_end(work.master_mac);
break;
default:
break;
}
}
}
void ota_espnow_slave_init(void) {
if (s_slave_work_queue != NULL) {
return;
}
s_slave_work_queue = xQueueCreate(OTA_SLAVE_WORK_QUEUE_LEN, sizeof(ota_slave_work_t));
if (s_slave_work_queue == NULL) {
ESP_LOGE(TAG, "failed to create slave OTA work queue");
return;
}
if (xTaskCreate(ota_slave_work_task, "ota_slave_wrk", OTA_SLAVE_WORK_STACK, NULL,
OTA_SLAVE_WORK_PRIO, NULL) != pdPASS) {
ESP_LOGE(TAG, "failed to create slave OTA work task");
}
}
static void ota_slave_prepare_task(void *param) { static void ota_slave_prepare_task(void *param) {
uint32_t total_size = (uint32_t)(uintptr_t)param; uint32_t total_size = (uint32_t)(uintptr_t)param;
uint8_t master_mac[6]; uint8_t master_mac[6];
@@ -190,82 +405,37 @@ void ota_espnow_slave_on_start(const uint8_t master_mac[6],
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size); ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
if (ota_uart_is_active()) { if (ota_uart_is_active()) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 4); queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
return; return;
} }
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK, if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO, (void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
NULL) != pdPASS) { NULL) != pdPASS) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 5); queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
} }
} }
void ota_espnow_slave_on_payload(const uint8_t master_mac[6], void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
const alox_EspNowOtaPayload *payload) { const alox_EspNowOtaPayload *payload) {
if (payload == NULL || payload->data.size == 0) { if (payload == NULL) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 11); queue_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
return; return;
} }
if (!ota_uart_is_active()) { ota_slave_work_t work = {.op = OTA_SLAVE_WORK_PAYLOAD, .payload = *payload};
ESP_LOGW(TAG, "OTA_PAYLOAD seq=%lu but no active session", memcpy(work.master_mac, master_mac, 6);
(unsigned long)payload->seq); if (!queue_slave_work(&work)) {
send_slave_status(master_mac, OTA_ST_FAILED, 0, 12); queue_slave_status(master_mac, OTA_ST_FAILED, 0, 14);
return;
}
if (payload->seq == 0) {
ESP_LOGI(TAG, "ESP-NOW OTA payloads started");
}
ota_feed_result_t r =
ota_uart_feed(payload->data.bytes, payload->data.size);
if (r == OTA_FEED_ERROR) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13);
return;
}
if (r == OTA_FEED_BLOCK_WRITTEN) {
uint32_t written = ota_uart_bytes_written();
uint32_t total = ota_uart_total_size();
ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written);
led_ring_show_ota_progress(written, total, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0);
return;
}
if (r == OTA_FEED_OK) {
uint32_t total = ota_uart_total_size();
if (total > 0) {
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
OTA_LED_ESPNOW_RX_B);
}
} }
} }
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) { void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
ESP_LOGI(TAG, "ESP-NOW OTA_END"); ota_slave_work_t work = {.op = OTA_SLAVE_WORK_END};
if (!ota_uart_is_active()) { memcpy(work.master_mac, master_mac, 6);
send_slave_status(master_mac, OTA_ST_FAILED, 0, 20); if (!queue_slave_work(&work)) {
return; queue_slave_status(master_mac, OTA_ST_FAILED, 0, 15);
} }
uint32_t written = ota_uart_bytes_written();
bool success = false;
esp_err_t err = ota_uart_finish(true, &success);
if (err != ESP_OK || !success) {
led_ring_ota_failed();
send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err);
return;
}
send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0);
led_ring_ota_success();
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
(unsigned long)written);
} }
void ota_espnow_master_on_status(const uint8_t slave_mac[6], void ota_espnow_master_on_status(const uint8_t slave_mac[6],
@@ -342,6 +512,8 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
} }
} }
s_distribution_active = true;
memset(&s_dist.progress, 0, sizeof(s_dist.progress)); memset(&s_dist.progress, 0, sizeof(s_dist.progress));
if (progress != NULL) { if (progress != NULL) {
s_dist.progress = *progress; s_dist.progress = *progress;
@@ -362,6 +534,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
ESP_LOGW(TAG, "OTA_START to slave %lu failed", ESP_LOGW(TAG, "OTA_START to slave %lu failed",
(unsigned long)s_dist.id[i]); (unsigned long)s_dist.id[i]);
prog_end(); prog_end();
s_distribution_active = false;
return err; return err;
} }
} }
@@ -369,6 +542,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) { if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA ready"); ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
prog_end(); prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
@@ -392,38 +566,77 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset, ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
esp_err_to_name(err)); esp_err_to_name(err));
prog_end(); prog_end();
s_distribution_active = false;
return err; return err;
} }
uint32_t sent = 0;
while (sent < block_len) {
uint32_t chunk = block_len - sent;
if (chunk > OTA_UART_HOST_CHUNK_SIZE) {
chunk = OTA_UART_HOST_CHUNK_SIZE;
}
for (uint8_t i = 0; i < s_dist.count; i++) {
err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq,
block_buf + sent, chunk);
if (err != ESP_OK) {
prog_end();
return err;
}
}
seq++;
sent += chunk;
vTaskDelay(pdMS_TO_TICKS(OTA_PAYLOAD_DELAY_MS));
}
const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE); const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE);
s_dist.expected_bytes = offset + block_len; s_dist.expected_bytes = offset + block_len;
const uint32_t block_start_seq = seq;
if (full_block) { if (full_block) {
xEventGroupClearBits(s_eg, target_mask); xEventGroupClearBits(s_eg, target_mask);
if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) { }
ESP_LOGE(TAG, "timeout block ack @%lu bytes",
(unsigned long)s_dist.expected_bytes); bool block_sent = false;
for (uint32_t send_attempt = 0; send_attempt <= OTA_BLOCK_MAX_RETRIES;
send_attempt++) {
if (send_attempt > 0) {
seq = block_start_seq;
if (full_block) {
xEventGroupClearBits(s_eg, target_mask);
}
ESP_LOGW(TAG, "block send failed @%lu — resend %lu/%lu",
(unsigned long)s_dist.expected_bytes,
(unsigned long)send_attempt,
(unsigned long)OTA_BLOCK_MAX_RETRIES);
}
err = send_block_payloads(block_buf, block_len, &seq);
if (err == ESP_OK) {
block_sent = true;
break;
}
}
if (!block_sent) {
ESP_LOGE(TAG, "block send failed @%lu after %lu retries",
(unsigned long)s_dist.expected_bytes,
(unsigned long)OTA_BLOCK_MAX_RETRIES);
prog_end();
s_distribution_active = false;
return err;
}
if (full_block) {
const uint32_t ack_timeout = block_ack_timeout_ms();
bool acked = false;
for (uint32_t attempt = 0; attempt <= OTA_BLOCK_MAX_RETRIES; attempt++) {
if (wait_target_bits(target_mask, ack_timeout)) {
acked = true;
break;
}
log_missing_block_acks(s_dist.expected_bytes);
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
break;
}
ESP_LOGW(TAG, "block ack timeout @%lu — resend %lu/%lu",
(unsigned long)s_dist.expected_bytes,
(unsigned long)(attempt + 1),
(unsigned long)OTA_BLOCK_MAX_RETRIES);
xEventGroupClearBits(s_eg, target_mask);
seq = block_start_seq;
err = send_block_payloads(block_buf, block_len, &seq);
if (err != ESP_OK) {
prog_end();
s_distribution_active = false;
return err;
}
}
if (!acked) {
ESP_LOGE(TAG, "timeout block ack @%lu bytes after %lu retries",
(unsigned long)s_dist.expected_bytes,
(unsigned long)OTA_BLOCK_MAX_RETRIES);
prog_end(); prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)", ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
@@ -445,6 +658,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
err = esp_now_comm_send_ota_end(s_dist.mac[i]); err = esp_now_comm_send_ota_end(s_dist.mac[i]);
if (err != ESP_OK) { if (err != ESP_OK) {
prog_end(); prog_end();
s_distribution_active = false;
return err; return err;
} }
} }
@@ -452,11 +666,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) { if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
ESP_LOGE(TAG, "timeout waiting for slave OTA success"); ESP_LOGE(TAG, "timeout waiting for slave OTA success");
prog_end(); prog_end();
s_distribution_active = false;
return ESP_ERR_TIMEOUT; return ESP_ERR_TIMEOUT;
} }
prog_set_aggregate(size); prog_set_aggregate(size);
prog_end(); prog_end();
s_distribution_active = false;
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count); ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
return ESP_OK; return ESP_OK;
} }
+6
View File
@@ -37,4 +37,10 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]);
void ota_espnow_progress_query(uint32_t filter_client_id, void ota_espnow_progress_query(uint32_t filter_client_id,
alox_OtaSlaveProgressResponse *out); alox_OtaSlaveProgressResponse *out);
/** True while master is pushing a staged image to slaves. */
bool ota_espnow_distribution_active(void);
/** Slave: work queue for OTA (no esp_now_send from recv callback). */
void ota_espnow_slave_init(void);
#endif #endif
+25
View File
@@ -0,0 +1,25 @@
#include "ota_session.h"
#include "ota_espnow.h"
#include "ota_uart.h"
#include "uart_messages.pb.h"
bool ota_session_busy(void) {
return ota_uart_is_active() || ota_espnow_distribution_active();
}
bool ota_session_uart_cmd_allowed(uint16_t msg_id) {
if (!ota_session_busy()) {
return true;
}
switch ((alox_MessageType)msg_id) {
case alox_MessageType_OTA_START:
case alox_MessageType_OTA_PAYLOAD:
case alox_MessageType_OTA_END:
case alox_MessageType_OTA_START_ESPNOW:
case alox_MessageType_OTA_SLAVE_PROGRESS:
return true;
default:
return false;
}
}
+13
View File
@@ -0,0 +1,13 @@
#ifndef OTA_SESSION_H
#define OTA_SESSION_H
#include <stdbool.h>
#include <stdint.h>
/** UART upload or ESP-NOW slave distribution in progress. */
bool ota_session_busy(void);
/** During OTA only UART OTA-related commands are accepted on the master. */
bool ota_session_uart_cmd_allowed(uint16_t msg_id);
#endif
+29 -1
View File
@@ -12,6 +12,7 @@ typedef struct {
uint32_t total_size; uint32_t total_size;
uint32_t received; uint32_t received;
uint32_t written; uint32_t written;
uint32_t expected_seq;
int target_slot; int target_slot;
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE]; uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
size_t block_len; size_t block_len;
@@ -112,10 +113,30 @@ int ota_uart_prepare(uint32_t total_size) {
return s_ota.target_slot; return s_ota.target_slot;
} }
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len) { bool ota_uart_block_ready_for_reack(void) {
if (!s_ota.active) {
return false;
}
return s_ota.written > 0 &&
(s_ota.written % OTA_UART_FLASH_BLOCK_SIZE) == 0 &&
s_ota.block_len == 0;
}
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data,
size_t len) {
if (!s_ota.active || data == NULL || len == 0) { if (!s_ota.active || data == NULL || len == 0) {
return OTA_FEED_ERROR; return OTA_FEED_ERROR;
} }
if (seq < s_ota.expected_seq) {
return OTA_FEED_SEQ_DUP;
}
if (seq > s_ota.expected_seq) {
ESP_LOGW(TAG, "seq gap: got %lu expected %lu", (unsigned long)seq,
(unsigned long)s_ota.expected_seq);
return OTA_FEED_SEQ_GAP;
}
s_ota.expected_seq++;
if (len > OTA_UART_HOST_CHUNK_SIZE) { if (len > OTA_UART_HOST_CHUNK_SIZE) {
ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len, ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len,
OTA_UART_HOST_CHUNK_SIZE); OTA_UART_HOST_CHUNK_SIZE);
@@ -200,6 +221,13 @@ esp_err_t ota_uart_finish(bool set_boot, bool *success_out) {
return err; return err;
} }
if (s_ota.total_size > 0 && s_ota.received != s_ota.total_size) {
ESP_LOGE(TAG, "size mismatch: received=%lu expected=%lu",
(unsigned long)s_ota.received, (unsigned long)s_ota.total_size);
ota_uart_abort();
return ESP_ERR_INVALID_SIZE;
}
err = esp_ota_end(s_ota.handle); err = esp_ota_end(s_ota.handle);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
+10 -2
View File
@@ -28,6 +28,8 @@ typedef enum {
typedef enum { typedef enum {
OTA_FEED_OK = 0, OTA_FEED_OK = 0,
OTA_FEED_BLOCK_WRITTEN, OTA_FEED_BLOCK_WRITTEN,
OTA_FEED_SEQ_DUP,
OTA_FEED_SEQ_GAP,
OTA_FEED_ERROR, OTA_FEED_ERROR,
} ota_feed_result_t; } ota_feed_result_t;
@@ -41,8 +43,14 @@ int ota_uart_prepare(uint32_t total_size);
void ota_uart_abort(void); void ota_uart_abort(void);
/** Append up to 200 bytes; flushes 4 KiB blocks to flash when full. */ /**
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len); * Append up to 200 bytes with strict seq checking (0, 1, 2, ).
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP; gaps return OTA_FEED_SEQ_GAP.
*/
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data, size_t len);
/** True when a full 4 KiB block is in flash (used to re-ACK host block retries). */
bool ota_uart_block_ready_for_reack(void);
uint32_t ota_uart_bytes_written(void); uint32_t ota_uart_bytes_written(void);
+51
View File
@@ -7,6 +7,7 @@
static const char *TAG = "[SETTINGS]"; static const char *TAG = "[SETTINGS]";
static const char *NS = "powerpod"; static const char *NS = "powerpod";
static const char *KEY_ACCEL_DZ = "accel_dz"; static const char *KEY_ACCEL_DZ = "accel_dz";
static const char *KEY_UV_LATCH = "uv_latch";
#define ACCEL_DEADZONE_MAX 4095u #define ACCEL_DEADZONE_MAX 4095u
@@ -108,3 +109,53 @@ void pod_settings_apply_accel_deadzone(void) {
bma456_set_accel_deadzone(deadzone); bma456_set_accel_deadzone(deadzone);
} }
} }
bool pod_settings_is_uv_latched(void) {
if (!s_nvs_ready) {
return false;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(NS, NVS_READONLY, &handle);
if (err != ESP_OK) {
return false;
}
uint8_t value = 0;
err = nvs_get_u8(handle, KEY_UV_LATCH, &value);
nvs_close(handle);
return err == ESP_OK && value != 0;
}
esp_err_t pod_settings_set_uv_latched(bool latched) {
esp_err_t err = ensure_nvs();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(NS, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_u8(handle, KEY_UV_LATCH, latched ? 1u : 0u);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "save uv_latch failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "UV latch %s", latched ? "set" : "cleared");
return ESP_OK;
}
esp_err_t pod_settings_clear_uv_latched(void) {
return pod_settings_set_uv_latched(false);
}
+6
View File
@@ -2,6 +2,7 @@
#define POD_SETTINGS_H #define POD_SETTINGS_H
#include "esp_err.h" #include "esp_err.h"
#include <stdbool.h>
#include <stdint.h> #include <stdint.h>
/** Initialize NVS (idempotent) and log stored settings. Call once early in app_main. */ /** Initialize NVS (idempotent) and log stored settings. Call once early in app_main. */
@@ -16,4 +17,9 @@ uint32_t pod_settings_load_accel_deadzone(void);
/** Apply NVS deadzone to BMA456 when the sensor is present. */ /** Apply NVS deadzone to BMA456 when the sensor is present. */
void pod_settings_apply_accel_deadzone(void); void pod_settings_apply_accel_deadzone(void);
/** LiPo under-voltage latch persisted across reboot (software UV protection). */
bool pod_settings_is_uv_latched(void);
esp_err_t pod_settings_set_uv_latched(bool latched);
esp_err_t pod_settings_clear_uv_latched(void);
#endif #endif
+17 -1
View File
@@ -5,6 +5,7 @@
#include "cmd_tap_notify.h" #include "cmd_tap_notify.h"
#include "cmd_cache_status.h" #include "cmd_cache_status.h"
#include "cmd_espnow_unicast_test.h" #include "cmd_espnow_unicast_test.h"
#include "cmd_espnow_echo_ping.h"
#include "cmd_espnow_find_me.h" #include "cmd_espnow_find_me.h"
#include "cmd_restart.h" #include "cmd_restart.h"
#include "cmd_client_info.h" #include "cmd_client_info.h"
@@ -13,6 +14,7 @@
#include "cmd_ota_slave_progress.h" #include "cmd_ota_slave_progress.h"
#include "cmd_led_ring.h" #include "cmd_led_ring.h"
#include "cmd_battery.h" #include "cmd_battery.h"
#include "cmd_set_log_level.h"
#include "esp_now_comm.h" #include "esp_now_comm.h"
#include "powerpod.h" #include "powerpod.h"
#include "driver/gpio.h" #include "driver/gpio.h"
@@ -29,6 +31,7 @@
#include "led_ring.h" #include "led_ring.h"
#include "pod_settings.h" #include "pod_settings.h"
#include "uart.h" #include "uart.h"
#include "battery_uv.h"
#include <stdint.h> #include <stdint.h>
enum MASTER_STATES { enum MASTER_STATES {
@@ -76,6 +79,16 @@ void app_main(void) {
ESP_LOGW(TAG, "settings NVS init failed; using defaults"); ESP_LOGW(TAG, "settings NVS init failed; using defaults");
} }
if (board_input_init_adc_only() != ESP_OK) {
ESP_LOGW(TAG, "LiPo ADC init failed");
}
#if POWERPOD_BATTERY_UV_ENABLE
if (battery_uv_evaluate_boot()) {
battery_uv_run_mode();
}
#endif
// Get Master Mode Pin // Get Master Mode Pin
gpio_reset_pin(DIP_MASTER); gpio_reset_pin(DIP_MASTER);
gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT); gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT);
@@ -165,7 +178,8 @@ void app_main(void) {
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)", ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
app_config.running_partition, ota_slot); app_config.running_partition, ota_slot);
board_input_init(); board_input_start_lipo_monitor();
board_input_init_button();
err = esp_now_comm_init(&app_config); err = esp_now_comm_init(&app_config);
if (err != ESP_OK) { if (err != ESP_OK) {
@@ -185,12 +199,14 @@ void app_main(void) {
cmd_tap_notify_register(); cmd_tap_notify_register();
cmd_cache_status_register(); cmd_cache_status_register();
cmd_espnow_unicast_test_register(); cmd_espnow_unicast_test_register();
cmd_espnow_echo_ping_register();
cmd_espnow_find_me_register(); cmd_espnow_find_me_register();
cmd_restart_register(); cmd_restart_register();
cmd_led_ring_register(); cmd_led_ring_register();
cmd_battery_register(); cmd_battery_register();
cmd_ota_register(); cmd_ota_register();
cmd_ota_slave_progress_register(); cmd_ota_slave_progress_register();
cmd_set_log_level_register();
} }
ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)"); ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)");
+7 -3
View File
@@ -1,6 +1,11 @@
#ifndef POWERPOD_H #ifndef POWERPOD_H
#define POWERPOD_H #define POWERPOD_H
/** 1 = software LiPo UV latch + energy-save mode; 0 = disabled (LED mode 6 still works). */
#ifndef POWERPOD_BATTERY_UV_ENABLE
#define POWERPOD_BATTERY_UV_ENABLE 0
#endif
#define DIP_MASTER 4 #define DIP_MASTER 4
#define I2C_SCL 5 #define I2C_SCL 5
#define I2C_SDA 6 #define I2C_SDA 6
@@ -11,9 +16,8 @@
/** Front-panel button (active low, internal pull-up). */ /** Front-panel button (active low, internal pull-up). */
#define TASTER_GPIO 12 #define TASTER_GPIO 12
/** LiPo voltage sense inputs (ADC1-capable GPIOs). */ /** LiPo voltage sense inputs (ADC-capable GPIOs; GPIO11 = ADC2 on ESP32-S3). */
#define V_LIPO_1_GPIO 1 #define V_LIPO_1_GPIO 1
/** Shares GPIO with TASTER on current bench wiring; second ADC is skipped in board_input.c. */ #define V_LIPO_2_GPIO 11
#define V_LIPO_2_GPIO 12
#endif #endif
+6
View File
@@ -9,6 +9,12 @@
PB_BIND(alox_EspNowUnicastTest, alox_EspNowUnicastTest, AUTO) PB_BIND(alox_EspNowUnicastTest, alox_EspNowUnicastTest, AUTO)
PB_BIND(alox_EspNowEchoPing, alox_EspNowEchoPing, AUTO)
PB_BIND(alox_EspNowEchoPong, alox_EspNowEchoPong, AUTO)
PB_BIND(alox_EspNowFindMe, alox_EspNowFindMe, AUTO) PB_BIND(alox_EspNowFindMe, alox_EspNowFindMe, AUTO)
+63 -8
View File
@@ -29,7 +29,9 @@ typedef enum _alox_EspNowMessageType {
alox_EspNowMessageType_ESPNOW_BATTERY_QUERY = 15, alox_EspNowMessageType_ESPNOW_BATTERY_QUERY = 15,
alox_EspNowMessageType_ESPNOW_BATTERY_REPORT = 16, alox_EspNowMessageType_ESPNOW_BATTERY_REPORT = 16,
alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY = 17, alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY = 17,
alox_EspNowMessageType_ESPNOW_TAP_EVENT = 18 alox_EspNowMessageType_ESPNOW_TAP_EVENT = 18,
alox_EspNowMessageType_ESPNOW_ECHO_PING = 19,
alox_EspNowMessageType_ESPNOW_ECHO_PONG = 20
} alox_EspNowMessageType; } alox_EspNowMessageType;
/* Struct definitions */ /* Struct definitions */
@@ -37,6 +39,19 @@ typedef struct _alox_EspNowUnicastTest {
uint32_t seq; uint32_t seq;
} alox_EspNowUnicastTest; } alox_EspNowUnicastTest;
/* * Master → slave: echo ping (host ts + master monotonic time for RTT). */
typedef struct _alox_EspNowEchoPing {
uint64_t host_timestamp_us;
/* * esp_timer_get_time() (µs since boot) when master forwarded this message. */
uint64_t master_time_us;
} alox_EspNowEchoPing;
/* * Slave → master: echo ping payload unchanged. */
typedef struct _alox_EspNowEchoPong {
uint64_t host_timestamp_us;
uint64_t master_time_us;
} alox_EspNowEchoPong;
/* * Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */ /* * Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
typedef struct _alox_EspNowFindMe { typedef struct _alox_EspNowFindMe {
/* * 0 = any slave; otherwise only slave_id must match */ /* * 0 = any slave; otherwise only slave_id must match */
@@ -50,6 +65,8 @@ typedef struct _alox_EspNowRestart {
typedef struct _alox_EspNowDiscover { typedef struct _alox_EspNowDiscover {
uint32_t network; uint32_t network;
/* * Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending;
} alox_EspNowDiscover; } alox_EspNowDiscover;
typedef struct _alox_EspNowSlavePresence { typedef struct _alox_EspNowSlavePresence {
@@ -169,6 +186,8 @@ typedef struct _alox_EspNowMessage {
alox_EspNowBatteryReport battery_report; alox_EspNowBatteryReport battery_report;
alox_EspNowTapNotify tap_notify; alox_EspNowTapNotify tap_notify;
alox_EspNowTapEvent tap_event; alox_EspNowTapEvent tap_event;
alox_EspNowEchoPing echo_ping;
alox_EspNowEchoPong echo_pong;
} payload; } payload;
} alox_EspNowMessage; } alox_EspNowMessage;
@@ -179,8 +198,10 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN #define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_TAP_EVENT #define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_ECHO_PONG
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_TAP_EVENT+1)) #define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_ECHO_PONG+1))
@@ -204,9 +225,11 @@ extern "C" {
/* Initializer values for message structs */ /* Initializer values for message structs */
#define alox_EspNowUnicastTest_init_default {0} #define alox_EspNowUnicastTest_init_default {0}
#define alox_EspNowEchoPing_init_default {0, 0}
#define alox_EspNowEchoPong_init_default {0, 0}
#define alox_EspNowFindMe_init_default {0} #define alox_EspNowFindMe_init_default {0}
#define alox_EspNowRestart_init_default {0} #define alox_EspNowRestart_init_default {0}
#define alox_EspNowDiscover_init_default {0} #define alox_EspNowDiscover_init_default {0, 0}
#define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0} #define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0}
#define alox_EspNowAccelDeadzone_init_default {0, 0} #define alox_EspNowAccelDeadzone_init_default {0, 0}
#define alox_EspNowAccelStream_init_default {0, 0} #define alox_EspNowAccelStream_init_default {0, 0}
@@ -222,9 +245,11 @@ extern "C" {
#define alox_EspNowOtaStatus_init_default {0, 0, 0} #define alox_EspNowOtaStatus_init_default {0, 0, 0}
#define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}} #define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}}
#define alox_EspNowUnicastTest_init_zero {0} #define alox_EspNowUnicastTest_init_zero {0}
#define alox_EspNowEchoPing_init_zero {0, 0}
#define alox_EspNowEchoPong_init_zero {0, 0}
#define alox_EspNowFindMe_init_zero {0} #define alox_EspNowFindMe_init_zero {0}
#define alox_EspNowRestart_init_zero {0} #define alox_EspNowRestart_init_zero {0}
#define alox_EspNowDiscover_init_zero {0} #define alox_EspNowDiscover_init_zero {0, 0}
#define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0} #define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0}
#define alox_EspNowAccelDeadzone_init_zero {0, 0} #define alox_EspNowAccelDeadzone_init_zero {0, 0}
#define alox_EspNowAccelStream_init_zero {0, 0} #define alox_EspNowAccelStream_init_zero {0, 0}
@@ -242,9 +267,14 @@ extern "C" {
/* Field tags (for use in manual encoding/decoding) */ /* Field tags (for use in manual encoding/decoding) */
#define alox_EspNowUnicastTest_seq_tag 1 #define alox_EspNowUnicastTest_seq_tag 1
#define alox_EspNowEchoPing_host_timestamp_us_tag 1
#define alox_EspNowEchoPing_master_time_us_tag 2
#define alox_EspNowEchoPong_host_timestamp_us_tag 1
#define alox_EspNowEchoPong_master_time_us_tag 2
#define alox_EspNowFindMe_client_id_tag 1 #define alox_EspNowFindMe_client_id_tag 1
#define alox_EspNowRestart_client_id_tag 1 #define alox_EspNowRestart_client_id_tag 1
#define alox_EspNowDiscover_network_tag 1 #define alox_EspNowDiscover_network_tag 1
#define alox_EspNowDiscover_master_ota_pending_tag 2
#define alox_EspNowSlavePresence_network_tag 1 #define alox_EspNowSlavePresence_network_tag 1
#define alox_EspNowSlavePresence_mac_tag 2 #define alox_EspNowSlavePresence_mac_tag 2
#define alox_EspNowSlavePresence_version_tag 3 #define alox_EspNowSlavePresence_version_tag 3
@@ -306,6 +336,8 @@ extern "C" {
#define alox_EspNowMessage_battery_report_tag 17 #define alox_EspNowMessage_battery_report_tag 17
#define alox_EspNowMessage_tap_notify_tag 18 #define alox_EspNowMessage_tap_notify_tag 18
#define alox_EspNowMessage_tap_event_tag 19 #define alox_EspNowMessage_tap_event_tag 19
#define alox_EspNowMessage_echo_ping_tag 20
#define alox_EspNowMessage_echo_pong_tag 21
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \ #define alox_EspNowUnicastTest_FIELDLIST(X, a) \
@@ -313,6 +345,18 @@ X(a, STATIC, SINGULAR, UINT32, seq, 1)
#define alox_EspNowUnicastTest_CALLBACK NULL #define alox_EspNowUnicastTest_CALLBACK NULL
#define alox_EspNowUnicastTest_DEFAULT NULL #define alox_EspNowUnicastTest_DEFAULT NULL
#define alox_EspNowEchoPing_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT64, host_timestamp_us, 1) \
X(a, STATIC, SINGULAR, UINT64, master_time_us, 2)
#define alox_EspNowEchoPing_CALLBACK NULL
#define alox_EspNowEchoPing_DEFAULT NULL
#define alox_EspNowEchoPong_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT64, host_timestamp_us, 1) \
X(a, STATIC, SINGULAR, UINT64, master_time_us, 2)
#define alox_EspNowEchoPong_CALLBACK NULL
#define alox_EspNowEchoPong_DEFAULT NULL
#define alox_EspNowFindMe_FIELDLIST(X, a) \ #define alox_EspNowFindMe_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_EspNowFindMe_CALLBACK NULL #define alox_EspNowFindMe_CALLBACK NULL
@@ -324,7 +368,8 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_EspNowRestart_DEFAULT NULL #define alox_EspNowRestart_DEFAULT NULL
#define alox_EspNowDiscover_FIELDLIST(X, a) \ #define alox_EspNowDiscover_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, network, 1) X(a, STATIC, SINGULAR, UINT32, network, 1) \
X(a, STATIC, SINGULAR, BOOL, master_ota_pending, 2)
#define alox_EspNowDiscover_CALLBACK NULL #define alox_EspNowDiscover_CALLBACK NULL
#define alox_EspNowDiscover_DEFAULT NULL #define alox_EspNowDiscover_DEFAULT NULL
@@ -442,7 +487,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_query,payload.battery_query), 16) \ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_query,payload.battery_query), 16) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_report), 17) \ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_report), 17) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify,payload.tap_notify), 18) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify,payload.tap_notify), 18) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19) X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19) \
X(a, STATIC, ONEOF, MESSAGE, (payload,echo_ping,payload.echo_ping), 20) \
X(a, STATIC, ONEOF, MESSAGE, (payload,echo_pong,payload.echo_pong), 21)
#define alox_EspNowMessage_CALLBACK NULL #define alox_EspNowMessage_CALLBACK NULL
#define alox_EspNowMessage_DEFAULT NULL #define alox_EspNowMessage_DEFAULT NULL
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover #define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
@@ -463,8 +510,12 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19)
#define alox_EspNowMessage_payload_battery_report_MSGTYPE alox_EspNowBatteryReport #define alox_EspNowMessage_payload_battery_report_MSGTYPE alox_EspNowBatteryReport
#define alox_EspNowMessage_payload_tap_notify_MSGTYPE alox_EspNowTapNotify #define alox_EspNowMessage_payload_tap_notify_MSGTYPE alox_EspNowTapNotify
#define alox_EspNowMessage_payload_tap_event_MSGTYPE alox_EspNowTapEvent #define alox_EspNowMessage_payload_tap_event_MSGTYPE alox_EspNowTapEvent
#define alox_EspNowMessage_payload_echo_ping_MSGTYPE alox_EspNowEchoPing
#define alox_EspNowMessage_payload_echo_pong_MSGTYPE alox_EspNowEchoPong
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg; extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
extern const pb_msgdesc_t alox_EspNowEchoPing_msg;
extern const pb_msgdesc_t alox_EspNowEchoPong_msg;
extern const pb_msgdesc_t alox_EspNowFindMe_msg; extern const pb_msgdesc_t alox_EspNowFindMe_msg;
extern const pb_msgdesc_t alox_EspNowRestart_msg; extern const pb_msgdesc_t alox_EspNowRestart_msg;
extern const pb_msgdesc_t alox_EspNowDiscover_msg; extern const pb_msgdesc_t alox_EspNowDiscover_msg;
@@ -485,6 +536,8 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */ /* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#define alox_EspNowUnicastTest_fields &alox_EspNowUnicastTest_msg #define alox_EspNowUnicastTest_fields &alox_EspNowUnicastTest_msg
#define alox_EspNowEchoPing_fields &alox_EspNowEchoPing_msg
#define alox_EspNowEchoPong_fields &alox_EspNowEchoPong_msg
#define alox_EspNowFindMe_fields &alox_EspNowFindMe_msg #define alox_EspNowFindMe_fields &alox_EspNowFindMe_msg
#define alox_EspNowRestart_fields &alox_EspNowRestart_msg #define alox_EspNowRestart_fields &alox_EspNowRestart_msg
#define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg #define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg
@@ -512,7 +565,9 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelStream_size 8 #define alox_EspNowAccelStream_size 8
#define alox_EspNowBatteryQuery_size 6 #define alox_EspNowBatteryQuery_size 6
#define alox_EspNowBatteryReport_size 22 #define alox_EspNowBatteryReport_size 22
#define alox_EspNowDiscover_size 6 #define alox_EspNowDiscover_size 8
#define alox_EspNowEchoPing_size 22
#define alox_EspNowEchoPong_size 22
#define alox_EspNowFindMe_size 6 #define alox_EspNowFindMe_size 6
#define alox_EspNowLedRing_size 60 #define alox_EspNowLedRing_size 60
#define alox_EspNowOtaEnd_size 0 #define alox_EspNowOtaEnd_size 0
+19
View File
@@ -24,12 +24,27 @@ enum EspNowMessageType {
ESPNOW_BATTERY_REPORT = 16; ESPNOW_BATTERY_REPORT = 16;
ESPNOW_SET_TAP_NOTIFY = 17; ESPNOW_SET_TAP_NOTIFY = 17;
ESPNOW_TAP_EVENT = 18; ESPNOW_TAP_EVENT = 18;
ESPNOW_ECHO_PING = 19;
ESPNOW_ECHO_PONG = 20;
} }
message EspNowUnicastTest { message EspNowUnicastTest {
uint32 seq = 1; uint32 seq = 1;
} }
/** Master → slave: echo ping (host ts + master monotonic time for RTT). */
message EspNowEchoPing {
uint64 host_timestamp_us = 1;
/** esp_timer_get_time() (µs since boot) when master forwarded this message. */
uint64 master_time_us = 2;
}
/** Slave → master: echo ping payload unchanged. */
message EspNowEchoPong {
uint64 host_timestamp_us = 1;
uint64 master_time_us = 2;
}
/** Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */ /** Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
message EspNowFindMe { message EspNowFindMe {
/** 0 = any slave; otherwise only slave_id must match */ /** 0 = any slave; otherwise only slave_id must match */
@@ -43,6 +58,8 @@ message EspNowRestart {
message EspNowDiscover { message EspNowDiscover {
uint32 network = 1; uint32 network = 1;
/** Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending = 2;
} }
message EspNowSlavePresence { message EspNowSlavePresence {
@@ -158,5 +175,7 @@ message EspNowMessage {
EspNowBatteryReport battery_report = 17; EspNowBatteryReport battery_report = 17;
EspNowTapNotify tap_notify = 18; EspNowTapNotify tap_notify = 18;
EspNowTapEvent tap_event = 19; EspNowTapEvent tap_event = 19;
EspNowEchoPing echo_ping = 20;
EspNowEchoPong echo_pong = 21;
} }
} }
+12
View File
@@ -87,6 +87,12 @@ PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
PB_BIND(alox_EspNowUnicastTestResponse, alox_EspNowUnicastTestResponse, AUTO) PB_BIND(alox_EspNowUnicastTestResponse, alox_EspNowUnicastTestResponse, AUTO)
PB_BIND(alox_EspNowEchoPingRequest, alox_EspNowEchoPingRequest, AUTO)
PB_BIND(alox_EspNowEchoPingResponse, alox_EspNowEchoPingResponse, AUTO)
PB_BIND(alox_LedRingProgressRequest, alox_LedRingProgressRequest, AUTO) PB_BIND(alox_LedRingProgressRequest, alox_LedRingProgressRequest, AUTO)
@@ -105,6 +111,12 @@ PB_BIND(alox_RestartRequest, alox_RestartRequest, AUTO)
PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO) PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO)
PB_BIND(alox_SetLogLevelRequest, alox_SetLogLevelRequest, AUTO)
PB_BIND(alox_SetLogLevelResponse, alox_SetLogLevelResponse, AUTO)
PB_BIND(alox_OtaStartPayload, alox_OtaStartPayload, AUTO) PB_BIND(alox_OtaStartPayload, alox_OtaStartPayload, AUTO)
+113 -5
View File
@@ -32,7 +32,11 @@ typedef enum _alox_MessageType {
alox_MessageType_BATTERY_STATUS = 26, alox_MessageType_BATTERY_STATUS = 26,
alox_MessageType_TAP_NOTIFY = 27, alox_MessageType_TAP_NOTIFY = 27,
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */ /* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
alox_MessageType_CACHE_STATUS = 29 alox_MessageType_CACHE_STATUS = 29,
/* * Host → master → slave → master: timestamp echo round-trip (latency test). */
alox_MessageType_ESPNOW_ECHO_PING = 30,
/* * Host → master: get/set ESP-IDF log level for tag "*" (global). */
alox_MessageType_SET_LOG_LEVEL = 31
} alox_MessageType; } alox_MessageType;
typedef enum _alox_TapKind { typedef enum _alox_TapKind {
@@ -235,8 +239,24 @@ typedef struct _alox_EspNowUnicastTestResponse {
uint32_t seq; uint32_t seq;
} alox_EspNowUnicastTestResponse; } alox_EspNowUnicastTestResponse;
/* * Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
typedef struct _alox_EspNowEchoPingRequest {
uint32_t client_id;
/* * Microseconds since Unix epoch (host clock). */
uint64_t timestamp_us;
} alox_EspNowEchoPingRequest;
typedef struct _alox_EspNowEchoPingResponse {
bool success;
uint32_t client_id;
/* * Echoed host timestamp from goTool request. */
uint64_t timestamp_us;
/* * esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
uint32_t esp_rtt_us;
} alox_EspNowEchoPingResponse;
/* Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW. /* Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color. */ mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low. */
typedef struct _alox_LedRingProgressRequest { typedef struct _alox_LedRingProgressRequest {
uint32_t mode; uint32_t mode;
/* * 0100: fraction of ring LEDs to light (mode=progress) */ /* * 0100: fraction of ring LEDs to light (mode=progress) */
@@ -289,6 +309,18 @@ typedef struct _alox_RestartResponse {
uint32_t client_id; uint32_t client_id;
} alox_RestartResponse; } alox_RestartResponse;
/* * Host → master: read/write global log level (esp_log_level_set("*", …)). */
typedef struct _alox_SetLogLevelRequest {
bool write;
/* * esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE */
uint32_t level;
} alox_SetLogLevelRequest;
typedef struct _alox_SetLogLevelResponse {
bool success;
uint32_t level;
} alox_SetLogLevelResponse;
/* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */ /* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */
typedef struct _alox_OtaStartPayload { typedef struct _alox_OtaStartPayload {
uint32_t total_size; uint32_t total_size;
@@ -371,6 +403,10 @@ typedef struct _alox_UartMessage {
alox_TapNotifyResponse tap_notify_response; alox_TapNotifyResponse tap_notify_response;
alox_CacheStatusRequest cache_status_request; alox_CacheStatusRequest cache_status_request;
alox_CacheStatusResponse cache_status_response; alox_CacheStatusResponse cache_status_response;
alox_EspNowEchoPingRequest espnow_echo_ping_request;
alox_EspNowEchoPingResponse espnow_echo_ping_response;
alox_SetLogLevelRequest set_log_level_request;
alox_SetLogLevelResponse set_log_level_response;
} payload; } payload;
} alox_UartMessage; } alox_UartMessage;
@@ -381,8 +417,8 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN #define _alox_MessageType_MIN alox_MessageType_UNKNOWN
#define _alox_MessageType_MAX alox_MessageType_CACHE_STATUS #define _alox_MessageType_MAX alox_MessageType_SET_LOG_LEVEL
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_CACHE_STATUS+1)) #define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_SET_LOG_LEVEL+1))
#define _alox_TapKind_MIN alox_TapKind_TAP_NONE #define _alox_TapKind_MIN alox_TapKind_TAP_NONE
#define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE #define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE
@@ -429,6 +465,10 @@ extern "C" {
@@ -460,12 +500,16 @@ extern "C" {
#define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}} #define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}}
#define alox_EspNowUnicastTestRequest_init_default {0, 0} #define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0} #define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_EspNowEchoPingRequest_init_default {0, 0}
#define alox_EspNowEchoPingResponse_init_default {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0, 0, 0} #define alox_LedRingProgressResponse_init_default {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_default {0} #define alox_EspNowFindMeRequest_init_default {0}
#define alox_EspNowFindMeResponse_init_default {0, 0} #define alox_EspNowFindMeResponse_init_default {0, 0}
#define alox_RestartRequest_init_default {0} #define alox_RestartRequest_init_default {0}
#define alox_RestartResponse_init_default {0, 0} #define alox_RestartResponse_init_default {0, 0}
#define alox_SetLogLevelRequest_init_default {0, 0}
#define alox_SetLogLevelResponse_init_default {0, 0}
#define alox_OtaStartPayload_init_default {0} #define alox_OtaStartPayload_init_default {0}
#define alox_OtaPayload_init_default {0, {0, {0}}} #define alox_OtaPayload_init_default {0, {0, {0}}}
#define alox_OtaEndPayload_init_default {0} #define alox_OtaEndPayload_init_default {0}
@@ -500,12 +544,16 @@ extern "C" {
#define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}} #define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0} #define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0} #define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_EspNowEchoPingRequest_init_zero {0, 0}
#define alox_EspNowEchoPingResponse_init_zero {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} #define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0, 0, 0} #define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_zero {0} #define alox_EspNowFindMeRequest_init_zero {0}
#define alox_EspNowFindMeResponse_init_zero {0, 0} #define alox_EspNowFindMeResponse_init_zero {0, 0}
#define alox_RestartRequest_init_zero {0} #define alox_RestartRequest_init_zero {0}
#define alox_RestartResponse_init_zero {0, 0} #define alox_RestartResponse_init_zero {0, 0}
#define alox_SetLogLevelRequest_init_zero {0, 0}
#define alox_SetLogLevelResponse_init_zero {0, 0}
#define alox_OtaStartPayload_init_zero {0} #define alox_OtaStartPayload_init_zero {0}
#define alox_OtaPayload_init_zero {0, {0, {0}}} #define alox_OtaPayload_init_zero {0, {0, {0}}}
#define alox_OtaEndPayload_init_zero {0} #define alox_OtaEndPayload_init_zero {0}
@@ -599,6 +647,12 @@ extern "C" {
#define alox_EspNowUnicastTestRequest_seq_tag 2 #define alox_EspNowUnicastTestRequest_seq_tag 2
#define alox_EspNowUnicastTestResponse_success_tag 1 #define alox_EspNowUnicastTestResponse_success_tag 1
#define alox_EspNowUnicastTestResponse_seq_tag 2 #define alox_EspNowUnicastTestResponse_seq_tag 2
#define alox_EspNowEchoPingRequest_client_id_tag 1
#define alox_EspNowEchoPingRequest_timestamp_us_tag 2
#define alox_EspNowEchoPingResponse_success_tag 1
#define alox_EspNowEchoPingResponse_client_id_tag 2
#define alox_EspNowEchoPingResponse_timestamp_us_tag 3
#define alox_EspNowEchoPingResponse_esp_rtt_us_tag 4
#define alox_LedRingProgressRequest_mode_tag 1 #define alox_LedRingProgressRequest_mode_tag 1
#define alox_LedRingProgressRequest_progress_tag 2 #define alox_LedRingProgressRequest_progress_tag 2
#define alox_LedRingProgressRequest_digit_tag 3 #define alox_LedRingProgressRequest_digit_tag 3
@@ -623,6 +677,10 @@ extern "C" {
#define alox_RestartRequest_client_id_tag 1 #define alox_RestartRequest_client_id_tag 1
#define alox_RestartResponse_success_tag 1 #define alox_RestartResponse_success_tag 1
#define alox_RestartResponse_client_id_tag 2 #define alox_RestartResponse_client_id_tag 2
#define alox_SetLogLevelRequest_write_tag 1
#define alox_SetLogLevelRequest_level_tag 2
#define alox_SetLogLevelResponse_success_tag 1
#define alox_SetLogLevelResponse_level_tag 2
#define alox_OtaStartPayload_total_size_tag 1 #define alox_OtaStartPayload_total_size_tag 1
#define alox_OtaPayload_seq_tag 1 #define alox_OtaPayload_seq_tag 1
#define alox_OtaPayload_data_tag 2 #define alox_OtaPayload_data_tag 2
@@ -671,6 +729,10 @@ extern "C" {
#define alox_UartMessage_tap_notify_response_tag 30 #define alox_UartMessage_tap_notify_response_tag 30
#define alox_UartMessage_cache_status_request_tag 33 #define alox_UartMessage_cache_status_request_tag 33
#define alox_UartMessage_cache_status_response_tag 34 #define alox_UartMessage_cache_status_response_tag 34
#define alox_UartMessage_espnow_echo_ping_request_tag 35
#define alox_UartMessage_espnow_echo_ping_response_tag 36
#define alox_UartMessage_set_log_level_request_tag 37
#define alox_UartMessage_set_log_level_response_tag 38
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_UartMessage_FIELDLIST(X, a) \ #define alox_UartMessage_FIELDLIST(X, a) \
@@ -703,7 +765,11 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.batt
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_response,payload.tap_notify_response), 30) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_response,payload.tap_notify_response), 30) \
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_request,payload.cache_status_request), 33) \ X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_request,payload.cache_status_request), 33) \
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_status_response), 34) X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_status_response), 34) \
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_echo_ping_request,payload.espnow_echo_ping_request), 35) \
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_echo_ping_response,payload.espnow_echo_ping_response), 36) \
X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_request,payload.set_log_level_request), 37) \
X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_response,payload.set_log_level_response), 38)
#define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_CALLBACK NULL
#define alox_UartMessage_DEFAULT NULL #define alox_UartMessage_DEFAULT NULL
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack #define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
@@ -735,6 +801,10 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_
#define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse #define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse
#define alox_UartMessage_payload_cache_status_request_MSGTYPE alox_CacheStatusRequest #define alox_UartMessage_payload_cache_status_request_MSGTYPE alox_CacheStatusRequest
#define alox_UartMessage_payload_cache_status_response_MSGTYPE alox_CacheStatusResponse #define alox_UartMessage_payload_cache_status_response_MSGTYPE alox_CacheStatusResponse
#define alox_UartMessage_payload_espnow_echo_ping_request_MSGTYPE alox_EspNowEchoPingRequest
#define alox_UartMessage_payload_espnow_echo_ping_response_MSGTYPE alox_EspNowEchoPingResponse
#define alox_UartMessage_payload_set_log_level_request_MSGTYPE alox_SetLogLevelRequest
#define alox_UartMessage_payload_set_log_level_response_MSGTYPE alox_SetLogLevelResponse
#define alox_Ack_FIELDLIST(X, a) \ #define alox_Ack_FIELDLIST(X, a) \
@@ -934,6 +1004,20 @@ X(a, STATIC, SINGULAR, UINT32, seq, 2)
#define alox_EspNowUnicastTestResponse_CALLBACK NULL #define alox_EspNowUnicastTestResponse_CALLBACK NULL
#define alox_EspNowUnicastTestResponse_DEFAULT NULL #define alox_EspNowUnicastTestResponse_DEFAULT NULL
#define alox_EspNowEchoPingRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, UINT64, timestamp_us, 2)
#define alox_EspNowEchoPingRequest_CALLBACK NULL
#define alox_EspNowEchoPingRequest_DEFAULT NULL
#define alox_EspNowEchoPingResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, success, 1) \
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
X(a, STATIC, SINGULAR, UINT64, timestamp_us, 3) \
X(a, STATIC, SINGULAR, UINT32, esp_rtt_us, 4)
#define alox_EspNowEchoPingResponse_CALLBACK NULL
#define alox_EspNowEchoPingResponse_DEFAULT NULL
#define alox_LedRingProgressRequest_FIELDLIST(X, a) \ #define alox_LedRingProgressRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, mode, 1) \ X(a, STATIC, SINGULAR, UINT32, mode, 1) \
X(a, STATIC, SINGULAR, UINT32, progress, 2) \ X(a, STATIC, SINGULAR, UINT32, progress, 2) \
@@ -982,6 +1066,18 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2)
#define alox_RestartResponse_CALLBACK NULL #define alox_RestartResponse_CALLBACK NULL
#define alox_RestartResponse_DEFAULT NULL #define alox_RestartResponse_DEFAULT NULL
#define alox_SetLogLevelRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, write, 1) \
X(a, STATIC, SINGULAR, UINT32, level, 2)
#define alox_SetLogLevelRequest_CALLBACK NULL
#define alox_SetLogLevelRequest_DEFAULT NULL
#define alox_SetLogLevelResponse_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, BOOL, success, 1) \
X(a, STATIC, SINGULAR, UINT32, level, 2)
#define alox_SetLogLevelResponse_CALLBACK NULL
#define alox_SetLogLevelResponse_DEFAULT NULL
#define alox_OtaStartPayload_FIELDLIST(X, a) \ #define alox_OtaStartPayload_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, total_size, 1) X(a, STATIC, SINGULAR, UINT32, total_size, 1)
#define alox_OtaStartPayload_CALLBACK NULL #define alox_OtaStartPayload_CALLBACK NULL
@@ -1057,12 +1153,16 @@ extern const pb_msgdesc_t alox_CacheClientStatus_msg;
extern const pb_msgdesc_t alox_CacheStatusResponse_msg; extern const pb_msgdesc_t alox_CacheStatusResponse_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
extern const pb_msgdesc_t alox_EspNowEchoPingRequest_msg;
extern const pb_msgdesc_t alox_EspNowEchoPingResponse_msg;
extern const pb_msgdesc_t alox_LedRingProgressRequest_msg; extern const pb_msgdesc_t alox_LedRingProgressRequest_msg;
extern const pb_msgdesc_t alox_LedRingProgressResponse_msg; extern const pb_msgdesc_t alox_LedRingProgressResponse_msg;
extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg; extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg;
extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg; extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg;
extern const pb_msgdesc_t alox_RestartRequest_msg; extern const pb_msgdesc_t alox_RestartRequest_msg;
extern const pb_msgdesc_t alox_RestartResponse_msg; extern const pb_msgdesc_t alox_RestartResponse_msg;
extern const pb_msgdesc_t alox_SetLogLevelRequest_msg;
extern const pb_msgdesc_t alox_SetLogLevelResponse_msg;
extern const pb_msgdesc_t alox_OtaStartPayload_msg; extern const pb_msgdesc_t alox_OtaStartPayload_msg;
extern const pb_msgdesc_t alox_OtaPayload_msg; extern const pb_msgdesc_t alox_OtaPayload_msg;
extern const pb_msgdesc_t alox_OtaEndPayload_msg; extern const pb_msgdesc_t alox_OtaEndPayload_msg;
@@ -1099,12 +1199,16 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg #define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg #define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg #define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
#define alox_EspNowEchoPingRequest_fields &alox_EspNowEchoPingRequest_msg
#define alox_EspNowEchoPingResponse_fields &alox_EspNowEchoPingResponse_msg
#define alox_LedRingProgressRequest_fields &alox_LedRingProgressRequest_msg #define alox_LedRingProgressRequest_fields &alox_LedRingProgressRequest_msg
#define alox_LedRingProgressResponse_fields &alox_LedRingProgressResponse_msg #define alox_LedRingProgressResponse_fields &alox_LedRingProgressResponse_msg
#define alox_EspNowFindMeRequest_fields &alox_EspNowFindMeRequest_msg #define alox_EspNowFindMeRequest_fields &alox_EspNowFindMeRequest_msg
#define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg #define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg
#define alox_RestartRequest_fields &alox_RestartRequest_msg #define alox_RestartRequest_fields &alox_RestartRequest_msg
#define alox_RestartResponse_fields &alox_RestartResponse_msg #define alox_RestartResponse_fields &alox_RestartResponse_msg
#define alox_SetLogLevelRequest_fields &alox_SetLogLevelRequest_msg
#define alox_SetLogLevelResponse_fields &alox_SetLogLevelResponse_msg
#define alox_OtaStartPayload_fields &alox_OtaStartPayload_msg #define alox_OtaStartPayload_fields &alox_OtaStartPayload_msg
#define alox_OtaPayload_fields &alox_OtaPayload_msg #define alox_OtaPayload_fields &alox_OtaPayload_msg
#define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg #define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg
@@ -1136,6 +1240,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_CacheStatusRequest_size 0 #define alox_CacheStatusRequest_size 0
#define alox_CacheStatusResponse_size 736 #define alox_CacheStatusResponse_size 736
#define alox_ClientInput_size 22 #define alox_ClientInput_size 22
#define alox_EspNowEchoPingRequest_size 17
#define alox_EspNowEchoPingResponse_size 25
#define alox_EspNowFindMeRequest_size 6 #define alox_EspNowFindMeRequest_size 6
#define alox_EspNowFindMeResponse_size 8 #define alox_EspNowFindMeResponse_size 8
#define alox_EspNowUnicastTestRequest_size 12 #define alox_EspNowUnicastTestRequest_size 12
@@ -1152,6 +1258,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_OtaStatusPayload_size 24 #define alox_OtaStatusPayload_size 24
#define alox_RestartRequest_size 6 #define alox_RestartRequest_size 6
#define alox_RestartResponse_size 8 #define alox_RestartResponse_size 8
#define alox_SetLogLevelRequest_size 8
#define alox_SetLogLevelResponse_size 8
#define alox_TapEvent_size 16 #define alox_TapEvent_size 16
#define alox_TapNotifyRequest_size 16 #define alox_TapNotifyRequest_size 16
#define alox_TapNotifyResponse_size 20 #define alox_TapNotifyResponse_size 20
+37 -1
View File
@@ -29,6 +29,10 @@ enum MessageType {
reserved 28; reserved 28;
/** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */ /** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
CACHE_STATUS = 29; CACHE_STATUS = 29;
/** Host → master → slave → master: timestamp echo round-trip (latency test). */
ESPNOW_ECHO_PING = 30;
/** Host → master: get/set ESP-IDF log level for tag "*" (global). */
SET_LOG_LEVEL = 31;
} }
message UartMessage { message UartMessage {
@@ -63,6 +67,10 @@ message UartMessage {
TapNotifyResponse tap_notify_response = 30; TapNotifyResponse tap_notify_response = 30;
CacheStatusRequest cache_status_request = 33; CacheStatusRequest cache_status_request = 33;
CacheStatusResponse cache_status_response = 34; CacheStatusResponse cache_status_response = 34;
EspNowEchoPingRequest espnow_echo_ping_request = 35;
EspNowEchoPingResponse espnow_echo_ping_response = 36;
SetLogLevelRequest set_log_level_request = 37;
SetLogLevelResponse set_log_level_response = 38;
} }
} }
@@ -255,8 +263,24 @@ message EspNowUnicastTestResponse {
uint32 seq = 2; uint32 seq = 2;
} }
/** Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
message EspNowEchoPingRequest {
uint32 client_id = 1;
/** Microseconds since Unix epoch (host clock). */
uint64 timestamp_us = 2;
}
message EspNowEchoPingResponse {
bool success = 1;
uint32 client_id = 2;
/** Echoed host timestamp from goTool request. */
uint64 timestamp_us = 3;
/** esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
uint32 esp_rtt_us = 4;
}
// Host master: LED ring on master (client_id=0) and/or slaves via ESP-NOW. // Host master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color. // mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low.
message LedRingProgressRequest { message LedRingProgressRequest {
uint32 mode = 1; uint32 mode = 1;
/** 0100: fraction of ring LEDs to light (mode=progress) */ /** 0100: fraction of ring LEDs to light (mode=progress) */
@@ -309,6 +333,18 @@ message RestartResponse {
uint32 client_id = 2; uint32 client_id = 2;
} }
/** Host → master: read/write global log level (esp_log_level_set("*", …)). */
message SetLogLevelRequest {
bool write = 1;
/** esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE */
uint32 level = 2;
}
message SetLogLevelResponse {
bool success = 1;
uint32 level = 2;
}
// Host device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). // Host device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
message OtaStartPayload { message OtaStartPayload {
uint32 total_size = 1; uint32 total_size = 1;
+2 -1
View File
@@ -52,7 +52,8 @@ void init_uart(QueueHandle_t cmd_queue) {
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, .flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
}; };
err = uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, UART_BUF_SIZE, 0, NULL, 0); err = uart_driver_install(UART_NUM, UART_DRIVER_RX_BUF_SIZE,
UART_DRIVER_TX_BUF_SIZE, 0, NULL, 0);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err)); ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return; return;
+4 -1
View File
@@ -16,7 +16,10 @@
#define UART_RXD_PIN 3 #define UART_RXD_PIN 3
#define UART_BUF_SIZE 2048 #define UART_BUF_SIZE 4096
/** Driver RX ring — must hold a full OTA block burst (~20 × ~215 B frames). */
#define UART_DRIVER_RX_BUF_SIZE 16384
#define UART_DRIVER_TX_BUF_SIZE 4096
#define START_MARKER 0xAA #define START_MARKER 0xAA
#define STOP_MARKER 0xCC #define STOP_MARKER 0xCC
#define MAX_BUF_SIZE 252 #define MAX_BUF_SIZE 252