Compare commits
6 Commits
498b89d7ba
...
ab1844ac32
| Author | SHA1 | Date | |
|---|---|---|---|
| ab1844ac32 | |||
| e4ce18edd8 | |||
| 0eea27a876 | |||
| 0cbc4d0644 | |||
| 35b39fce46 | |||
| 41a66d4417 |
416
docs/ARCHITECTURE.md
Normal file
416
docs/ARCHITECTURE.md
Normal 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 1–8
|
||||
AM->>BMA: init_bma456() (Master + Slave)
|
||||
AM->>AM: app_config_t füllen
|
||||
AM->>BI: board_input_init()
|
||||
AM->>EN: esp_now_comm_init(&app_config)
|
||||
AM->>LR: led_ring_init()
|
||||
alt master == true
|
||||
AM->>CMD: Queue, Dispatcher, UART, Handler registrieren
|
||||
end
|
||||
```
|
||||
|
||||
### 2.2 `app_config_t`
|
||||
|
||||
| Feld | Quelle | Bedeutung |
|
||||
|------|--------|-----------|
|
||||
| `master` | `DIP_MASTER` (GPIO 4): Low = Master | Steuert UART + Registry-Nutzung |
|
||||
| `network` | IO-Expander, Bits 5–8 (nibble reversed) | 1–8 → WiFi/ESP-NOW-Kanal |
|
||||
| `running_partition` | `esp_ota_get_running_partition()` | Aktives OTA-Label (`ota_0` / `ota_1`) |
|
||||
|
||||
**Dateien:** `main/app_config.h`, `main/powerpod.c`, `main/powerpod.h` (Pins).
|
||||
|
||||
---
|
||||
|
||||
## 3. Schichtenmodell
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Befehlshandler (main/cmd/cmd_*.c) │
|
||||
│ Decode/Encode: uart_cmd.c, Antwort: uart_proto.c │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Dispatch: cmd_handler.c (Queue + vCmdDispatcherTask) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Transport UART: uart.c (Framing) │ ESP-NOW: esp_now_comm │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Protokoll: uart_messages.proto │ esp_now_messages.proto │
|
||||
│ Codec: nanopb (proto/*.pb.c) │ esp_now_proto.c │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Domäne: client_registry, bosch456, led_ring, ota_*, board │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ESP-IDF: UART, WiFi, ESP-NOW, I2C, ADC, OTA, NVS, FreeRTOS │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Datenfluss: Commands (Befehle)
|
||||
|
||||
Commands sind **asynchron über eine FreeRTOS-Queue** entkoppelt; der UART-Reader blockiert nicht auf Handler-Logik.
|
||||
|
||||
### 4.1 Eingang (UART → Handler)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Bytes UART1] --> B[parse_uart_byte]
|
||||
B --> C{Frame vollständig?}
|
||||
C -->|ja| D[uart_enqueue_packet]
|
||||
D --> E["generic_msg_t<br/>msg_id = payload[0]<br/>payload = Rest"]
|
||||
E --> F[cmd_queue xQueueSend]
|
||||
F --> G[vCmdDispatcherTask]
|
||||
G --> H{msg_register_handler}
|
||||
H --> I[cmd_*.c callback]
|
||||
I --> J[free payload]
|
||||
```
|
||||
|
||||
**Wichtige Stellen:**
|
||||
|
||||
| Schritt | Datei | Funktion |
|
||||
|---------|--------|----------|
|
||||
| Byte-Parser + Timeout 50 ms | `main/uart.c` | `parse_uart_byte()`, `uart_read_task()` |
|
||||
| Queue-Eintrag | `main/uart.c` | `uart_enqueue_packet()` |
|
||||
| Dispatcher | `main/cmd/cmd_handler.c` | `vCmdDispatcherTask()`, `msg_register_handler()` |
|
||||
| Registrierung Master | `main/powerpod.c` | `cmd_*_register()` nach `init_uart()` |
|
||||
|
||||
**Nachrichten-ID:** Byte 0 des UART-Payloads = `alox_MessageType` (siehe `main/proto/uart_messages.proto`). Bytes 1… = nanopb-kodiertes `UartMessage` **ohne** wiederholtes Type-Feld — Handler erhalten nur den protobuf-Teil (`msg.payload` ab Offset 1).
|
||||
|
||||
### 4.2 Ausgang (Handler → UART)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
H[Handler baut alox_UartMessage] --> U[uart_cmd_send]
|
||||
U --> P[uart_send_uart_message]
|
||||
P --> E[pb_encode + Byte0 = type]
|
||||
E --> F[uart_send_framed]
|
||||
F --> W[uart_write_bytes]
|
||||
```
|
||||
|
||||
**Wichtige Stellen:**
|
||||
|
||||
| Schritt | Datei | Funktion |
|
||||
|---------|--------|----------|
|
||||
| Handler-Hilfen | `main/uart_cmd.c` | `uart_cmd_decode()`, `uart_cmd_init_response()`, `uart_cmd_send()` |
|
||||
| Protobuf + Framing | `main/uart_proto.c` | `uart_send_uart_message()` |
|
||||
| XOR-Rahmen | `main/uart.c` | `uart_send_framed()` |
|
||||
|
||||
### 4.3 OTA-Sperre (keine parallelen Befehle)
|
||||
|
||||
Während einer OTA-Session (`ota_uart_is_active()` oder `ota_espnow_distribution_active()`) lehnt der Dispatcher alle UART-Befehle ab **außer**:
|
||||
|
||||
- `OTA_START`, `OTA_PAYLOAD`, `OTA_END`, `OTA_START_ESPNOW`, `OTA_SLAVE_PROGRESS`
|
||||
|
||||
Implementierung: `ota_session.c`, Prüfung in `vCmdDispatcherTask` vor Handler-Aufruf.
|
||||
|
||||
Auf dem **Slave** verarbeitet `espnow_recv_cb` während `ota_uart_is_active()` nur noch OTA-Nachrichten vom joined Master (kein Discover, Stream, LED, …).
|
||||
|
||||
Auf dem **Master** während ESP-NOW-Verteilung nur noch `ESPNOW_OTA_STATUS` aus dem recv_cb.
|
||||
|
||||
### 4.4 UART-Request-Decode (strikt)
|
||||
|
||||
| Regel | Beispiele |
|
||||
|-------|-----------|
|
||||
| Leerer protobuf-Body (`len == 0`) erlaubt | `VERSION`, `CLIENT_INFO`, `CACHE_STATUS`, `BATTERY_STATUS` (Defaults) |
|
||||
| `len > 0` → Decode muss gelingen, `which_payload` muss passen | `ACCEL_STREAM`, `TAP_NOTIFY`, `RESTART`, OTA, … |
|
||||
|
||||
### 4.5 Bridge-Muster: UART-Befehl → ESP-NOW
|
||||
|
||||
Viele Master-Handler folgen demselben Muster:
|
||||
|
||||
1. `uart_cmd_decode()` → Request aus `UartMessage`
|
||||
2. Lokale Aktion und/oder `client_registry_*` aktualisieren
|
||||
3. `esp_now_comm_send_*()` mit MAC aus Registry
|
||||
4. `uart_cmd_send()` mit Response
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Host
|
||||
participant UART as uart.c
|
||||
participant CMD as cmd_accel_deadzone.c
|
||||
participant REG as client_registry
|
||||
participant EN as esp_now_comm.c
|
||||
participant SL as Slave
|
||||
|
||||
Host->>UART: Frame ACCEL_DEADZONE
|
||||
UART->>CMD: generic_msg_t
|
||||
CMD->>REG: set_accel_deadzone / lookup MAC
|
||||
CMD->>EN: esp_now_comm_send_accel_deadzone
|
||||
EN->>SL: ESPNOW_SET_ACCEL_DEADZONE
|
||||
SL->>SL: bma456 + NVS
|
||||
CMD->>Host: uart_cmd_send Response
|
||||
```
|
||||
|
||||
**Beispiel-Implementierung:** `main/cmd/cmd_accel_deadzone.c`
|
||||
**Weitere Bridge-Handler:** `cmd_accel_stream.c`, `cmd_tap_notify.c`, `cmd_led_ring.c`, `cmd_espnow_find_me.c`, `cmd_restart.c`, `cmd_espnow_unicast_test.c`, `cmd/cmd_ota.c` (OTA + `ota_espnow.c`).
|
||||
|
||||
**Nur Master / nur Cache (kein Slave-Roundtrip):** `cmd_client_info.c`, `cmd_battery.c`, `cmd_cache_status.c`, `cmd_version.c`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Datenfluss: UART
|
||||
|
||||
### 5.1 Rahmenformat (Transport)
|
||||
|
||||
Unabhängig von Protobuf — reines Bytestream-Framing:
|
||||
|
||||
| Feld | Wert |
|
||||
|------|------|
|
||||
| Start | `0xAA` |
|
||||
| Länge | 1 Byte (1–252) |
|
||||
| Payload | `length` Bytes |
|
||||
| Prüfsumme | XOR aller Payload-Bytes |
|
||||
| Stopp | `0xCC` |
|
||||
|
||||
**Parameter:** `main/uart.h` — `UART_NUM_1`, `921600` Baud, TX GPIO **2**, RX GPIO **3**, `MAX_PAYLOAD_SIZE` 248.
|
||||
|
||||
### 5.2 Nutzlast (Anwendung)
|
||||
|
||||
```
|
||||
Payload[0] = MessageType (enum, 1 Byte)
|
||||
Payload[1…] = nanopb UartMessage (Felder ab type/payload oneof)
|
||||
```
|
||||
|
||||
**Schema:** `main/proto/uart_messages.proto`
|
||||
**Generiert:** `main/proto/uart_messages.pb.c/h` (`make proto_generate_uart`)
|
||||
|
||||
### 5.3 Tasks und Prioritäten
|
||||
|
||||
| Task | Stack | Priorität | Datei |
|
||||
|------|-------|-----------|--------|
|
||||
| `uart_rx` | 4096 | 5 | `uart.c` |
|
||||
| `cmd_dispatch` | 8192 | 5 | `cmd_handler.c` |
|
||||
|
||||
Queue-Größe Master: **64** Einträge (`powerpod.c`); volle Queue → Warnung, Payload wird freigegeben.
|
||||
|
||||
---
|
||||
|
||||
## 6. Datenfluss: ESP-NOW
|
||||
|
||||
### 6.1 Stack-Initialisierung
|
||||
|
||||
`esp_now_comm_init()` (`esp_now_comm.c`) → `esp_now_core` (WiFi/Radio/Send) + Rolle:
|
||||
|
||||
1. `client_registry_init()` (Master)
|
||||
2. `esp_now_init()`, recv_cb leitet an `esp_now_master_on_recv` / `esp_now_slave_on_recv`
|
||||
3. **Master** (`esp_now_master.c`): `espnow_disc`, `espnow_mon`
|
||||
4. **Slave** (`esp_now_slave.c`): `espnow_stx`, `espnow_hb`, `espnow_accel`, `ota_slave_work`
|
||||
|
||||
**Codec:** Rohes Paket = ein nanopb `EspNowMessage` — **kein** zusätzliches Framing (`main/esp_now_proto.c`).
|
||||
|
||||
### 6.2 Discovery und Join (Slave)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant M as Master espnow_disc
|
||||
participant S as Slave recv_cb
|
||||
participant TX as slave_tx_task
|
||||
|
||||
loop alle 500 ms
|
||||
M->>M: ESPNOW_DISCOVER → FF:FF:…:FF
|
||||
end
|
||||
S->>S: handle_discover (network match)
|
||||
S->>S: s_slave_joined, s_master_mac
|
||||
Note over S,TX: Kein Send aus recv_cb!
|
||||
S->>TX: SLAVE_TX_SLAVE_INFO
|
||||
TX->>M: ESPNOW_SLAVE_INFO
|
||||
S->>TX: SLAVE_TX_BATTERY
|
||||
TX->>M: ESPNOW_BATTERY_REPORT
|
||||
loop alle 1 s
|
||||
S->>M: ESPNOW_HEARTBEAT
|
||||
end
|
||||
```
|
||||
|
||||
**Registry-Schlüssel:** immer `recv_info.src_addr` (WiFi-MAC des Senders), **nicht** optionales `mac`-Feld in der Protobuf-Nachricht.
|
||||
|
||||
**Slave-ID:** `mac[5]` (letztes Oktett) — kann kollidieren; eindeutig ist die volle MAC in der Registry.
|
||||
|
||||
**Master-Verlust:** Slave setzt Join zurück, wenn **5 s** kein Discover vom gleichen Master (`SLAVE_MASTER_LOST_MS`).
|
||||
|
||||
**Join-Policy:** Master→Slave-Steuerung (Stream, Tap, LED, OTA, `UNICAST_TEST`, `SET_ACCEL_DEADZONE`, …) nur bei `s_slave_joined` und `src_addr == s_master_mac`. Während `ota_uart_is_active()` auf dem Slave verarbeitet der recv_cb nur OTA vom joined Master.
|
||||
|
||||
**Slave OTA:** Payload/End/Status-Sends laufen über `ota_slave_work_task` (Queue), nicht im ESP-NOW-recv_cb.
|
||||
|
||||
### 6.3 Master → Slave (Unicast)
|
||||
|
||||
Alle Master-Sends laufen über `send_message()` / `send_message_ex()`:
|
||||
|
||||
1. `esp_now_proto_encode()`
|
||||
2. `ensure_peer(dest_mac)`
|
||||
3. `esp_now_send()`
|
||||
|
||||
Öffentliche API: `main/esp_now_comm.h` (`esp_now_comm_send_*`).
|
||||
|
||||
**Empfang Slave:** `espnow_recv_cb()` → Switch auf `which_payload` → Handler (`handle_slave_*`).
|
||||
|
||||
### 6.4 Slave → Master (Events)
|
||||
|
||||
| Nachricht | Task / Trigger | Master-Ziel |
|
||||
|-----------|----------------|-------------|
|
||||
| `ESPNOW_ACCEL_SAMPLE` | `slave_accel_stream_task` 16 ms | `client_registry_update_accel()` |
|
||||
| `ESPNOW_TAP_EVENT` | BMA456-IRQ → `on_bma456_tap` | `client_registry_update_tap()` |
|
||||
| `ESPNOW_BATTERY_REPORT` | Heartbeat + nach Join | `client_registry_update_battery()` |
|
||||
| `ESPNOW_SLAVE_INFO` / `HEARTBEAT` | `slave_tx_task` / Heartbeat-Task | `client_registry_heartbeat()` |
|
||||
| `ESPNOW_OTA_STATUS` | `ota_espnow_slave_*` | `ota_espnow_master_on_status()` |
|
||||
|
||||
**Master recv:** `espnow_recv_cb()` — Presence, Accel, Tap, Battery, OTA-Status.
|
||||
|
||||
### 6.5 OTA über ESP-NOW
|
||||
|
||||
Nach erfolgreichem UART-OTA auf dem Master (oder `OTA_START_ESPNOW`): `ota_espnow.c` liest gestagte Partition, sendet `OTA_START` / `PAYLOAD` (≤200 B, `send_message_ex` mit ACK-Semaphore) / `OTA_END` an alle **available** Slaves.
|
||||
|
||||
Gemeinsamer Flash-Puffer: `ota_uart.c` (4 KiB Blockgröße).
|
||||
|
||||
---
|
||||
|
||||
## 7. Client Registry (Master-Datenhub)
|
||||
|
||||
Die Registry ist die **zentrale Brücke** zwischen ESP-NOW-Echtzeitdaten und UART-Abfragen.
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
EN_RX[espnow_recv_cb] --> REG[client_registry]
|
||||
CMD_R[cmd_client_info / battery / cache_status] --> REG
|
||||
CMD_W[cmd_* write paths] --> REG
|
||||
REG --> CMD_R
|
||||
EN_TX[esp_now_comm_send_*] --> MAC[MAC aus Registry]
|
||||
```
|
||||
|
||||
| Daten | Aktualisiert durch | Gelesen durch UART |
|
||||
|-------|-------------------|-------------------|
|
||||
| Presence, Version | HEARTBEAT / SLAVE_INFO | `CLIENT_INFO` |
|
||||
| Accel-Stream-Samples | `ESPNOW_ACCEL_SAMPLE` | `CACHE_STATUS` |
|
||||
| Tap-Events (16 ms Cache) | `ESPNOW_TAP_EVENT` | `CACHE_STATUS` |
|
||||
| Batterie | `ESPNOW_BATTERY_REPORT` + Master-ADC | `BATTERY_STATUS` |
|
||||
| Konfig-Flags | Handler + ESP-NOW | `CLIENT_INFO`, diverse GET |
|
||||
|
||||
**Datei:** `main/client_registry.c`, `main/client_registry.h` (max. 16 Clients, Timeout 3 s ohne Heartbeat).
|
||||
|
||||
---
|
||||
|
||||
## 8. FreeRTOS-Tasks (Übersicht)
|
||||
|
||||
| Task | Rolle | Intervall / Trigger |
|
||||
|------|-------|---------------------|
|
||||
| `uart_rx` | Master | UART read 20 ms timeout |
|
||||
| `cmd_dispatch` | Master | Queue blocking |
|
||||
| `espnow_disc` | Master | 500 ms Discover |
|
||||
| `espnow_mon` | Master | 1 s Timeout + Master-Battery |
|
||||
| `espnow_stx` | Slave | Queue: SLAVE_INFO, Battery nach Join |
|
||||
| `espnow_hb` | Slave | 1 s Heartbeat + 30 s Battery |
|
||||
| `espnow_accel` | Slave | 16 ms Accel wenn Stream an |
|
||||
| `bma456_poll` | Beide | 10 Hz (`bosch456.c`) |
|
||||
| `vTaskLedRing` | Beide | LED-Ring-Befehle |
|
||||
|
||||
`app_main` endet in `while(1) vTaskDelay(portMAX_DELAY)` — alle Arbeit läuft in Tasks.
|
||||
|
||||
---
|
||||
|
||||
## 9. Implementierungskarte (wichtige Dateien)
|
||||
|
||||
| Bereich | Pfad | Verantwortung |
|
||||
|---------|------|----------------|
|
||||
| Einstieg / Init | `main/powerpod.c` | Boot-Reihenfolge, Master-Handler-Register |
|
||||
| UART Transport | `main/uart.c`, `main/uart.h` | Framing RX/TX |
|
||||
| UART Protobuf TX | `main/uart_proto.c` | `uart_send_uart_message` |
|
||||
| UART Handler-Boilerplate | `main/uart_cmd.c`, `main/uart_cmd.h` | Decode, Register, Send |
|
||||
| Command Dispatch | `main/cmd/cmd_handler.c` | Queue, Dispatcher |
|
||||
| UART Commands | `main/cmd/cmd_*.c` | Pro Befehl ein Modul |
|
||||
| ESP-NOW | `esp_now_comm.c` Init/Router; `esp_now_core.c` Send/Peer; `esp_now_master.c` / `esp_now_slave.c` Rollenlogik |
|
||||
| ESP-NOW Codec | `main/esp_now_proto.c` | nanopb encode/decode |
|
||||
| Registry | `main/client_registry.c` | Slave-Tabelle + Caches |
|
||||
| BMA456 | `main/bosch456.c` | Sensor, Tap, Deadzone-Filter |
|
||||
| LED | `main/led_ring.c` | 95 LEDs, Digit-Maps |
|
||||
| OTA UART | `main/ota_uart.c`, `cmd/cmd_ota.c` | A/B-Partition Upload |
|
||||
| OTA ESP-NOW | `main/ota_espnow.c` | Verteilung an Slaves |
|
||||
| Einstellungen NVS | `main/pod_settings.c` | Accel-Deadzone persistent |
|
||||
| Board | `main/board_input.c` | Taster, LiPo-ADC |
|
||||
| Protobuf Schemas | `main/proto/*.proto` | Vertrag UART / ESP-NOW |
|
||||
| Vendor BMA456 | `components/bma456/` | Nur `bma4.c` + `bma456h.c` gelinkt |
|
||||
|
||||
---
|
||||
|
||||
## 10. Protokoll-Verträge (Kurzreferenz)
|
||||
|
||||
- **UART:** `main/proto/uart_messages.proto` — Host ↔ Master
|
||||
- **ESP-NOW:** `main/proto/esp_now_messages.proto` — Master ↔ Slave
|
||||
|
||||
Regenerierung: `make proto_generate` (siehe `DOCUMENTATION.md`).
|
||||
|
||||
---
|
||||
|
||||
## 11. Design-Entscheidungen
|
||||
|
||||
1. **Eine Queue für UART-Commands** — einziger Eingang vom Host; während OTA nur OTA-Befehle.
|
||||
2. **Kein ESP-NOW-Send im `recv_cb`** — Slave antwortet auf Discover über `slave_tx_task` (Deadlock-/Stack-Risiko vermeiden).
|
||||
3. **Registry-MAC = ESP-NOW-Quelladresse** — Protobuf-MAC ist optional/informativ.
|
||||
4. **Gleiches Binary** — Konfiguration nur Hardware (DIP + Expander); reduziert Release-Komplexität.
|
||||
5. **Nanopb statt vollem protobuf-c** — passend für ESP-RAM/Flash.
|
||||
6. **Master aggregiert Sensor-Daten** — Slaves streamen; Host pollt Cache per UART (`CACHE_STATUS`), kein Durchreichen jedes Accel-Samples über UART.
|
||||
|
||||
---
|
||||
|
||||
Weitere Details (Befehlsliste, GPIO, Build, BMA456): [`DOCUMENTATION.md`](DOCUMENTATION.md).
|
||||
486
docs/DOCUMENTATION.md
Normal file
486
docs/DOCUMENTATION.md
Normal file
@ -0,0 +1,486 @@
|
||||
# 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 | 12 | Entfällt wenn = Taster-GPIO |
|
||||
|
||||
**UART:** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control.
|
||||
|
||||
**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. DIP → `app_config.master`
|
||||
3. I2C + IO-Expander → `app_config.network` (1–8)
|
||||
4. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
|
||||
5. `pod_settings_apply_accel_deadzone()` wenn Sensor da
|
||||
6. OTA-Partition → `app_config.running_partition`
|
||||
7. `board_input_init()`
|
||||
8. `esp_now_comm_init(&app_config)` — **immer** (Master + Slave)
|
||||
9. `led_ring_init()`
|
||||
10. **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) §4–5.)*
|
||||
|
||||
### 5.1 Rahmen
|
||||
|
||||
| Byte | Inhalt |
|
||||
|------|--------|
|
||||
| 0 | `0xAA` Start |
|
||||
| 1 | Länge N (1–252) |
|
||||
| 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` (1–13).
|
||||
- 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_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) |
|
||||
|
||||
### 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) |
|
||||
|
||||
### 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 0–100 % |
|
||||
| 2 | Digit 0–10 |
|
||||
| 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`.
|
||||
|
||||
---
|
||||
|
||||
## 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 0–10 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`).
|
||||
|
||||
---
|
||||
|
||||
## 11. Board-Input und Batterie
|
||||
|
||||
`board_input.c`:
|
||||
|
||||
- **Taster** GPIO 12 — Logging bei Druck.
|
||||
- **LiPo-ADC** GPIO 1 (und optional 2, wenn nicht Taster).
|
||||
- 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.
|
||||
|
||||
---
|
||||
|
||||
## 12. Persistenz (NVS)
|
||||
|
||||
`pod_settings.c` — Namespace `powerpod`:
|
||||
|
||||
| Key | Inhalt |
|
||||
|-----|--------|
|
||||
| `accel_dz` | Accel-Deadzone LSB |
|
||||
|
||||
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_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 |
|
||||
| `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
|
||||
|
||||
| Tag | Modul |
|
||||
|-----|--------|
|
||||
| `[Main]` | powerpod.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).
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
@ -146,7 +147,11 @@ func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial,
|
||||
if hub != nil {
|
||||
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
|
||||
}
|
||||
writeJSON(w, http.StatusOK, otaAPIResponse{
|
||||
|
||||
@ -9,55 +9,61 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"powerpod/gotool/pb"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAccelStreamInterval = 16 * time.Millisecond
|
||||
defaultPreFetchMs = 2
|
||||
minAPIStreamInterval = 1 * time.Millisecond
|
||||
maxAPIStreamInterval = 10 * time.Second
|
||||
// How long tap events stay in API push/cache after first sight (matches dashboard).
|
||||
apiTapDisplayMinMs = 2000
|
||||
)
|
||||
|
||||
// AccelClientSample is one slave's cached accel on the master.
|
||||
type AccelClientSample struct {
|
||||
// InputClientSample is one slave's cached accel + tap state on the master.
|
||||
type InputClientSample struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Valid bool `json:"valid"`
|
||||
X int32 `json:"x,omitempty"`
|
||||
Y int32 `json:"y,omitempty"`
|
||||
Z int32 `json:"z,omitempty"`
|
||||
AgeMs uint32 `json:"age_ms,omitempty"`
|
||||
AccelAgeMs uint32 `json:"accel_age_ms,omitempty"`
|
||||
TapKind string `json:"tap_kind"`
|
||||
TapAgeMs uint32 `json:"tap_age_ms,omitempty"`
|
||||
}
|
||||
|
||||
// AccelStreamMessage is sent to external WebSocket clients (hello + accel samples).
|
||||
type AccelStreamMessage struct {
|
||||
Type string `json:"type"` // "hello" | "accel"
|
||||
// InputStreamMessage is sent to external WebSocket clients (hello + input samples).
|
||||
type InputStreamMessage struct {
|
||||
Type string `json:"type"` // "hello" | "input"
|
||||
Serial string `json:"serial_port,omitempty"`
|
||||
IntervalMs int `json:"interval_ms,omitempty"`
|
||||
PreFetchMs int `json:"pre_fetch_ms,omitempty"`
|
||||
TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"`
|
||||
Commands []string `json:"commands,omitempty"`
|
||||
Note string `json:"note,omitempty"`
|
||||
|
||||
T int64 `json:"t,omitempty"` // Unix nanoseconds
|
||||
Success bool `json:"success,omitempty"`
|
||||
Clients []AccelClientSample `json:"clients,omitempty"`
|
||||
Clients []InputClientSample `json:"clients,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// StreamStatusMessage is the reply to set_stream / get_stream (this connection).
|
||||
type StreamStatusMessage struct {
|
||||
Type string `json:"type"` // "stream_status"
|
||||
ReceiveAccel bool `json:"receive_accel"`
|
||||
ReceiveInput bool `json:"receive_input"`
|
||||
IntervalMs int `json:"interval_ms"`
|
||||
PreFetch int `json:"pre_fetch"`
|
||||
Success bool `json:"success"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// AccelStreamStatusMessage is the reply to set_accel_stream / get_accel_stream (slave).
|
||||
type AccelStreamStatusMessage struct {
|
||||
Type string `json:"type"` // "accel_stream_status"
|
||||
// InputStreamStatusMessage is the reply to set_input_stream / get_input_stream (slave).
|
||||
type InputStreamStatusMessage struct {
|
||||
Type string `json:"type"` // "input_stream_status"
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Success bool `json:"success"`
|
||||
@ -65,33 +71,6 @@ type AccelStreamStatusMessage struct {
|
||||
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.
|
||||
type APIClientInfo struct {
|
||||
ID uint32 `json:"id"`
|
||||
@ -101,7 +80,7 @@ type APIClientInfo struct {
|
||||
Used bool `json:"used"`
|
||||
LastPing uint32 `json:"last_ping"`
|
||||
LastSuccessPing uint32 `json:"last_success_ping"`
|
||||
AccelStream bool `json:"accel_stream"`
|
||||
InputStream bool `json:"input_stream"`
|
||||
TapNotifySingle bool `json:"tap_notify_single"`
|
||||
TapNotifyDouble bool `json:"tap_notify_double"`
|
||||
TapNotifyTriple bool `json:"tap_notify_triple"`
|
||||
@ -132,6 +111,7 @@ type accelWSCommand struct {
|
||||
ClientID uint32 `json:"client_id"`
|
||||
Enable *bool `json:"enable"`
|
||||
IntervalMs *int `json:"interval_ms"`
|
||||
PreFetch *int `json:"pre_fetch"`
|
||||
Single *bool `json:"single"`
|
||||
DoubleTap *bool `json:"double_tap"`
|
||||
Triple *bool `json:"triple"`
|
||||
@ -144,6 +124,7 @@ type APIInfoResponse struct {
|
||||
SerialPort string `json:"serial_port"`
|
||||
WebSocket string `json:"websocket"`
|
||||
DefaultIntervalMs int `json:"default_interval_ms"`
|
||||
DefaultPreFetchMs int `json:"default_pre_fetch_ms"`
|
||||
MinIntervalMs int `json:"min_interval_ms"`
|
||||
MaxIntervalMs int `json:"max_interval_ms"`
|
||||
TapDisplayMinMs int `json:"tap_display_min_ms"`
|
||||
@ -153,21 +134,28 @@ type APIInfoResponse struct {
|
||||
type cachedTapEvent struct {
|
||||
kind string
|
||||
shownAt time.Time
|
||||
ageMs uint32
|
||||
}
|
||||
|
||||
type wsSubscriber struct {
|
||||
conn *websocket.Conn
|
||||
receiveAccel bool
|
||||
receiveTap bool
|
||||
receiveInput bool
|
||||
interval time.Duration
|
||||
lastAccelSent time.Time
|
||||
lastTapSent time.Time
|
||||
preFetch time.Duration
|
||||
lastInputSent time.Time
|
||||
}
|
||||
|
||||
type pendingInputCache struct {
|
||||
cache *pb.CacheStatusResponse
|
||||
readAt time.Time
|
||||
readErr error
|
||||
}
|
||||
|
||||
type accelStreamHub struct {
|
||||
mu sync.RWMutex
|
||||
clients map[*websocket.Conn]*wsSubscriber
|
||||
defaultInterval time.Duration
|
||||
defaultPreFetch time.Duration
|
||||
configChanged chan struct{}
|
||||
recentTaps map[uint32]cachedTapEvent
|
||||
}
|
||||
@ -176,6 +164,7 @@ func newAccelStreamHub(defaultInterval time.Duration) *accelStreamHub {
|
||||
return &accelStreamHub{
|
||||
clients: make(map[*websocket.Conn]*wsSubscriber),
|
||||
defaultInterval: defaultInterval,
|
||||
defaultPreFetch: defaultPreFetchMs * time.Millisecond,
|
||||
configChanged: make(chan struct{}, 1),
|
||||
}
|
||||
}
|
||||
@ -197,26 +186,39 @@ func clampAPIInterval(d time.Duration) time.Duration {
|
||||
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 {
|
||||
sub := &wsSubscriber{
|
||||
conn: conn,
|
||||
receiveAccel: false,
|
||||
receiveInput: false,
|
||||
interval: h.defaultInterval,
|
||||
preFetch: h.defaultPreFetch,
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.clients[conn] = sub
|
||||
h.mu.Unlock()
|
||||
|
||||
hello := AccelStreamMessage{
|
||||
hello := InputStreamMessage{
|
||||
Type: "hello",
|
||||
Serial: portName,
|
||||
IntervalMs: int(h.defaultInterval / time.Millisecond),
|
||||
PreFetchMs: int(h.defaultPreFetch / time.Millisecond),
|
||||
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{
|
||||
"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_stream", "get_stream",
|
||||
"set_input_stream", "get_input_stream",
|
||||
"set_tap_notify", "get_tap_notify",
|
||||
"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) {
|
||||
h.mu.Lock()
|
||||
delete(h.clients, conn)
|
||||
anyTap := false
|
||||
anyInput := false
|
||||
for _, sub := range h.clients {
|
||||
if sub.receiveTap {
|
||||
anyTap = true
|
||||
if sub.receiveInput {
|
||||
anyInput = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !anyTap {
|
||||
if !anyInput {
|
||||
h.recentTaps = nil
|
||||
}
|
||||
h.mu.Unlock()
|
||||
h.notifyConfigChanged()
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) anyWantsAccel() bool {
|
||||
func (h *accelStreamHub) anyWantsInput() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, sub := range h.clients {
|
||||
if sub.receiveAccel {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) anyWantsTap() bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
for _, sub := range h.clients {
|
||||
if sub.receiveTap {
|
||||
if sub.receiveInput {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -270,7 +261,7 @@ func (h *accelStreamHub) minWantedInterval() time.Duration {
|
||||
defer h.mu.RUnlock()
|
||||
var min time.Duration
|
||||
for _, sub := range h.clients {
|
||||
if !sub.receiveAccel && !sub.receiveTap {
|
||||
if !sub.receiveInput {
|
||||
continue
|
||||
}
|
||||
if min == 0 || sub.interval < min {
|
||||
@ -283,20 +274,28 @@ func (h *accelStreamHub) minWantedInterval() time.Duration {
|
||||
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()
|
||||
sub.receiveAccel = enable
|
||||
sub.receiveInput = enable
|
||||
if !enable {
|
||||
h.recentTaps = nil
|
||||
}
|
||||
if intervalMs != nil {
|
||||
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)
|
||||
pf := int(sub.preFetch / time.Millisecond)
|
||||
h.mu.Unlock()
|
||||
h.notifyConfigChanged()
|
||||
|
||||
return StreamStatusMessage{
|
||||
Type: "stream_status",
|
||||
ReceiveAccel: enable,
|
||||
ReceiveInput: enable,
|
||||
IntervalMs: ms,
|
||||
PreFetch: pf,
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
@ -306,45 +305,47 @@ func (h *accelStreamHub) getStream(sub *wsSubscriber) StreamStatusMessage {
|
||||
defer h.mu.RUnlock()
|
||||
return StreamStatusMessage{
|
||||
Type: "stream_status",
|
||||
ReceiveAccel: sub.receiveAccel,
|
||||
ReceiveInput: sub.receiveInput,
|
||||
IntervalMs: int(sub.interval / time.Millisecond),
|
||||
PreFetch: int(sub.preFetch / time.Millisecond),
|
||||
Success: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) setTapStream(sub *wsSubscriber, enable bool, intervalMs *int) TapStreamStatusMessage {
|
||||
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 {
|
||||
func (h *accelStreamHub) streamTiming(now time.Time) (needRead, needDeliver bool, waitPreFetch time.Duration) {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
return TapStreamStatusMessage{
|
||||
Type: "tap_stream_status",
|
||||
ReceiveTap: sub.receiveTap,
|
||||
IntervalMs: int(sub.interval / time.Millisecond),
|
||||
Success: true,
|
||||
for _, sub := range h.clients {
|
||||
if !sub.receiveInput {
|
||||
continue
|
||||
}
|
||||
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()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
@ -352,39 +353,67 @@ func (h *accelStreamHub) ingestTapEvents(incoming []TapClientEvent) []TapClientE
|
||||
if h.recentTaps == nil {
|
||||
h.recentTaps = make(map[uint32]cachedTapEvent)
|
||||
}
|
||||
for _, e := range incoming {
|
||||
if !e.Valid || e.Kind == "" {
|
||||
for _, c := range cache.GetClients() {
|
||||
t := c.GetTap()
|
||||
if t == nil {
|
||||
continue
|
||||
}
|
||||
h.recentTaps[e.ClientID] = cachedTapEvent{kind: e.Kind, shownAt: now}
|
||||
kind := tapKindLabelPB(t.GetKind())
|
||||
if kind == "" {
|
||||
continue
|
||||
}
|
||||
return h.activeTapEventsLocked(now)
|
||||
h.recentTaps[c.GetClientId()] = cachedTapEvent{
|
||||
kind: kind,
|
||||
shownAt: now,
|
||||
ageMs: t.GetAgeMs(),
|
||||
}
|
||||
}
|
||||
h.pruneRecentTapsLocked(now)
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) activeTapEventsLocked(now time.Time) []TapClientEvent {
|
||||
func (h *accelStreamHub) pruneRecentTapsLocked(now time.Time) {
|
||||
if len(h.recentTaps) == 0 {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
cutoff := now.Add(-apiTapDisplayMinMs * time.Millisecond)
|
||||
out := make([]TapClientEvent, 0, len(h.recentTaps))
|
||||
for id, ev := range h.recentTaps {
|
||||
if ev.shownAt.Before(cutoff) {
|
||||
delete(h.recentTaps, id)
|
||||
continue
|
||||
}
|
||||
shownAtMs := ev.shownAt.UnixMilli()
|
||||
out = append(out, TapClientEvent{
|
||||
ClientID: id,
|
||||
Valid: true,
|
||||
Kind: ev.kind,
|
||||
AgeMs: uint32(now.Sub(ev.shownAt).Milliseconds()),
|
||||
ShownAtMs: shownAtMs,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) inputClientsFromCacheLocked(cache *pb.CacheStatusResponse, now time.Time) []InputClientSample {
|
||||
h.pruneRecentTapsLocked(now)
|
||||
out := make([]InputClientSample, 0, len(cache.GetClients()))
|
||||
for _, c := range cache.GetClients() {
|
||||
sample := InputClientSample{
|
||||
ClientID: c.GetClientId(),
|
||||
TapKind: "none",
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
func (h *accelStreamHub) deliverInput(msg InputStreamMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
@ -394,13 +423,13 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
for conn, sub := range h.clients {
|
||||
if !sub.receiveAccel {
|
||||
if !sub.receiveInput {
|
||||
continue
|
||||
}
|
||||
if !sub.lastAccelSent.IsZero() && now.Sub(sub.lastAccelSent) < sub.interval {
|
||||
if !sub.lastInputSent.IsZero() && now.Sub(sub.lastInputSent) < sub.interval {
|
||||
continue
|
||||
}
|
||||
sub.lastAccelSent = now
|
||||
sub.lastInputSent = now
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
delete(h.clients, conn)
|
||||
_ = conn.Close()
|
||||
@ -408,155 +437,79 @@ func (h *accelStreamHub) deliver(msg AccelStreamMessage) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *accelStreamHub) deliverTap(msg TapStreamMessage) {
|
||||
data, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
|
||||
ticker := time.NewTicker(minAPIStreamInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
now := time.Now()
|
||||
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()
|
||||
}
|
||||
}()
|
||||
var pending *pendingInputCache
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-stop:
|
||||
return
|
||||
case <-hub.configChanged:
|
||||
resetTicker()
|
||||
case <-tick:
|
||||
wantAccel := hub.anyWantsAccel() && accelStreamPollingActive(dash, ctl)
|
||||
wantTap := hub.anyWantsTap()
|
||||
if !wantAccel && !wantTap {
|
||||
pending = nil
|
||||
case now := <-ticker.C:
|
||||
if !hub.anyWantsInput() || !inputPollingActive(dash, ctl, tapCtl) {
|
||||
pending = nil
|
||||
continue
|
||||
}
|
||||
|
||||
now := time.Now().UnixNano()
|
||||
needRead, needDeliver, waitPreFetch := hub.streamTiming(now)
|
||||
|
||||
if needRead && pending == nil {
|
||||
cache, err := link.readCacheStatusPoll()
|
||||
if errors.Is(err, errUARTBusy) {
|
||||
if wantAccel {
|
||||
hub.deliver(AccelStreamMessage{
|
||||
Type: "accel",
|
||||
T: now,
|
||||
Success: false,
|
||||
Error: "uart busy",
|
||||
})
|
||||
}
|
||||
if wantTap {
|
||||
hub.deliverTap(TapStreamMessage{
|
||||
Type: "tap",
|
||||
T: now,
|
||||
Success: false,
|
||||
Error: "uart busy",
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
if wantAccel {
|
||||
hub.deliver(AccelStreamMessage{
|
||||
Type: "accel",
|
||||
T: now,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
pending = &pendingInputCache{readErr: err, readAt: now}
|
||||
} else {
|
||||
hub.ingestTapFromCache(cache)
|
||||
pending = &pendingInputCache{cache: cache, readAt: now}
|
||||
}
|
||||
if wantTap {
|
||||
hub.deliverTap(TapStreamMessage{
|
||||
Type: "tap",
|
||||
T: now,
|
||||
Success: false,
|
||||
Error: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
if !needDeliver || pending == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if wantAccel {
|
||||
samples := accelSamplesFromCacheStatus(cache)
|
||||
clients := make([]AccelClientSample, 0, len(samples))
|
||||
for _, s := range samples {
|
||||
clients = append(clients, AccelClientSample{
|
||||
ClientID: s.GetClientId(),
|
||||
Valid: s.GetValid(),
|
||||
X: s.GetX(),
|
||||
Y: s.GetY(),
|
||||
Z: s.GetZ(),
|
||||
AgeMs: s.GetAgeMs(),
|
||||
})
|
||||
ts := now.UnixNano()
|
||||
if pending.readErr != nil {
|
||||
errMsg := pending.readErr.Error()
|
||||
if errors.Is(pending.readErr, errUARTBusy) {
|
||||
errMsg = "uart busy"
|
||||
}
|
||||
hub.deliver(AccelStreamMessage{
|
||||
Type: "accel",
|
||||
T: now,
|
||||
hub.deliverInput(InputStreamMessage{
|
||||
Type: "input",
|
||||
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,
|
||||
Clients: clients,
|
||||
})
|
||||
}
|
||||
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,
|
||||
})
|
||||
}
|
||||
pending = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accelStreamPollingActive(dash *wsHub, ctl *accelStreamCtl) bool {
|
||||
func inputPollingActive(dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) bool {
|
||||
if ctl != nil && ctl.Any() {
|
||||
return true
|
||||
}
|
||||
if tapCtl != nil && tapCtl.Any() {
|
||||
return true
|
||||
}
|
||||
return dash != nil && dash.anyAccelStreamEnabled()
|
||||
}
|
||||
|
||||
@ -586,9 +539,9 @@ func writeLedRingStatus(conn *websocket.Conn, out ledRingAPIResponse) {
|
||||
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
|
||||
func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
|
||||
msg := AccelStreamStatusMessage{
|
||||
Type: "accel_stream_status",
|
||||
func writeInputStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
|
||||
msg := InputStreamStatusMessage{
|
||||
Type: "input_stream_status",
|
||||
ClientID: out.ClientID,
|
||||
Enabled: out.Enabled,
|
||||
Success: out.Success,
|
||||
@ -602,14 +555,6 @@ func writeAccelStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
|
||||
_ = 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 {
|
||||
return APIClientInfo{
|
||||
ID: c.GetId(),
|
||||
@ -619,7 +564,7 @@ func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo {
|
||||
Used: c.GetUsed(),
|
||||
LastPing: c.GetLastPing(),
|
||||
LastSuccessPing: c.GetLastSuccessPing(),
|
||||
AccelStream: c.GetAccelStreamEnabled(),
|
||||
InputStream: c.GetAccelStreamEnabled(),
|
||||
TapNotifySingle: c.GetTapNotifySingle(),
|
||||
TapNotifyDouble: c.GetTapNotifyDouble(),
|
||||
TapNotifyTriple: c.GetTapNotifyTriple(),
|
||||
@ -717,28 +662,28 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
})
|
||||
return
|
||||
}
|
||||
writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs))
|
||||
writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs, cmd.PreFetch))
|
||||
|
||||
case "get_stream":
|
||||
writeStreamStatus(conn, hub.getStream(sub))
|
||||
|
||||
case "set_accel_stream":
|
||||
case "set_input_stream":
|
||||
if cmd.ClientID == 0 {
|
||||
writeAccelStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
if cmd.Enable == nil {
|
||||
writeAccelStreamStatus(conn, accelStreamAPIResponse{
|
||||
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||
ClientID: cmd.ClientID,
|
||||
Error: "enable required",
|
||||
})
|
||||
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 {
|
||||
writeAccelStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||
return
|
||||
}
|
||||
resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{
|
||||
@ -746,7 +691,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
ClientId: cmd.ClientID,
|
||||
})
|
||||
if err != nil {
|
||||
writeAccelStreamStatus(conn, accelStreamAPIResponse{
|
||||
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||
ClientID: cmd.ClientID,
|
||||
Error: err.Error(),
|
||||
})
|
||||
@ -755,25 +700,12 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
if ctl != nil {
|
||||
ctl.Set(cmd.ClientID, resp.GetEnabled())
|
||||
}
|
||||
writeAccelStreamStatus(conn, accelStreamAPIResponse{
|
||||
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||
Enabled: resp.GetEnabled(),
|
||||
ClientID: resp.GetClientId(),
|
||||
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":
|
||||
if cmd.AllClients {
|
||||
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
|
||||
@ -860,7 +792,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
|
||||
default:
|
||||
writeStreamStatus(conn, StreamStatusMessage{
|
||||
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 +828,11 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
|
||||
SerialPort: portName,
|
||||
WebSocket: "/ws",
|
||||
DefaultIntervalMs: defMs,
|
||||
DefaultPreFetchMs: defaultPreFetchMs,
|
||||
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
|
||||
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
|
||||
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 +848,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 {
|
||||
hub := newAccelStreamHub(defaultInterval)
|
||||
go runAccelStreamer(link, hub, dash, ctl, tapCtl, stop)
|
||||
go runInputStreamer(link, hub, dash, ctl, tapCtl, stop)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl, tapCtl)
|
||||
@ -924,7 +857,7 @@ func runAPIServer(portName string, link *managedSerial, addr string, defaultInte
|
||||
|
||||
srv := &http.Server{Addr: addr, Handler: mux}
|
||||
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())
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Printf("external API server: %v", err)
|
||||
|
||||
@ -417,9 +417,16 @@ func (m *managedSerial) FindMe(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)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if clientID == 0 {
|
||||
m.recoverAfterMasterRestart()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
||||
|
||||
@ -62,6 +62,8 @@ func runTest(portOverride string, baudOverride int, args []string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s: %w", port, err)
|
||||
}
|
||||
registerShutdown(func() { _ = sp.Close() })
|
||||
enableShutdownOnInterrupt()
|
||||
defer sp.Close()
|
||||
|
||||
if !*verbose {
|
||||
|
||||
@ -16,8 +16,7 @@ func runOTA(sp *serialPort, args []string) error {
|
||||
|
||||
sp.mu.Lock()
|
||||
defer sp.mu.Unlock()
|
||||
m := &managedSerial{quiet: false, sp: sp}
|
||||
return runOTAOnPortUnlocked(m, data, func(p OTAProgress) {
|
||||
return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) {
|
||||
switch p.Phase {
|
||||
case "preparing", "ready":
|
||||
fmt.Println(p.Message)
|
||||
|
||||
@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
@ -34,21 +35,29 @@ func runServe(portName string, baud int, args []string) error {
|
||||
|
||||
link := newManagedSerial(portName, baud)
|
||||
link.quiet = true
|
||||
defer link.Close()
|
||||
|
||||
hub := newWSHub()
|
||||
streamCtl := newAccelStreamCtl()
|
||||
tapCtl := newTapNotifyCtl()
|
||||
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 runBatteryPoller(link, hub, 5*time.Second, stop)
|
||||
go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop)
|
||||
|
||||
var apiSrv *http.Server
|
||||
if *apiAddr != "" {
|
||||
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop)
|
||||
defer shutdownAPIServer(apiSrv)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
@ -81,5 +90,14 @@ func runServe(portName string, baud int, args []string) error {
|
||||
if *apiAddr == "" {
|
||||
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
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ func (h *wsHub) setState(st DashboardState) {
|
||||
return
|
||||
}
|
||||
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()
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if len(samples) == 0 {
|
||||
return clients
|
||||
@ -338,7 +362,7 @@ func (h *wsHub) patchClientAccelStream(clientID uint32, enabled bool) {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
h.writeJSON(c, data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -397,7 +421,7 @@ func (h *wsHub) patchLiveStream(enabled bool) {
|
||||
return
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
h.writeJSON(c, data)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
}
|
||||
h.broadcastJSON(data)
|
||||
}
|
||||
|
||||
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) {
|
||||
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
|
||||
if errors.Is(err, errUARTBusy) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("battery poll: %v", err)
|
||||
return
|
||||
@ -602,7 +620,7 @@ func (h *wsHub) mergeBattery(samples []batterySampleJSON) {
|
||||
return
|
||||
}
|
||||
for _, c := range conns {
|
||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
||||
h.writeJSON(c, data)
|
||||
}
|
||||
}
|
||||
|
||||
@ -619,7 +637,7 @@ func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, s
|
||||
continue
|
||||
}
|
||||
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
|
||||
if err != nil {
|
||||
if errors.Is(err, errUARTBusy) || err != nil {
|
||||
continue
|
||||
}
|
||||
hub.mergeBattery(batterySamplesFromPB(bat.GetSamples()))
|
||||
|
||||
@ -1,15 +1,10 @@
|
||||
# WebSocket API
|
||||
|
||||
`go run . -port /dev/ttyUSB0 serve` exposes two WebSocket endpoints. They share the same UART link but serve different purposes.
|
||||
`go run . -port /dev/ttyUSB0 serve` exposes the WebSocket enpoint
|
||||
|
||||
| URL | Port (default) | Role |
|
||||
|-----|----------------|------|
|
||||
| `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).
|
||||
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **input** push stream |
|
||||
|
||||
---
|
||||
|
||||
@ -19,44 +14,49 @@ CLI overview and UART commands: [`../README.md`](../README.md). HTTP endpoints:
|
||||
|
||||
1. Connect → server sends **`hello`** (receive off; lists available commands).
|
||||
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).
|
||||
3. After `set_stream` with `enable: true`, the server may send **`input`** 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).
|
||||
|
||||
### Two layers (accel and tap)
|
||||
### Two layers (firmware vs host)
|
||||
|
||||
| Layer | Commands | Effect |
|
||||
|-------|----------|--------|
|
||||
| **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) |
|
||||
| **Firmware (ESP-NOW)** | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel samples and/or tap events to the master |
|
||||
| **This connection (host)** | `set_stream` | Whether **you** receive push JSON, at what rate (`interval_ms`, 1 ms … 10 s), and how early the UART read starts (`pre_fetch`) |
|
||||
|
||||
- **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.
|
||||
- **UART polling** runs only if at least one connection has `receive_input: true` (`set_stream`) **and** at least one slave streams input (`set_input_stream`) or has tap notify enabled (`set_tap_notify`).
|
||||
- **`set_tap_notify` alone** configures which tap kinds the slave reports; it does **not** enable host push by itself — you still need `set_stream`.
|
||||
|
||||
Typical sequence:
|
||||
|
||||
1. `list_clients` → slave IDs
|
||||
2. Per slave: `set_accel_stream` / `set_tap_notify` as needed
|
||||
3. `set_stream` and/or `set_tap_stream` with `"enable": true`
|
||||
4. Read push messages in a loop
|
||||
2. Per slave: `set_input_stream` and/or `set_tap_notify` as needed
|
||||
3. `set_stream` with `"enable": true`
|
||||
4. Read **`input`** messages in a loop
|
||||
|
||||
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.
|
||||
There is **no per-slave filter** on push messages: each `input` contains all cached slaves. Filter by `client_id` in your app.
|
||||
|
||||
---
|
||||
|
||||
## Push stream messages
|
||||
|
||||
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.
|
||||
These are the samples you get after enabling receive. Timing is per WebSocket connection:
|
||||
|
||||
### `accel` (type `"accel"`)
|
||||
- **`interval_ms`** — minimum time between consecutive `input` pushes on this socket.
|
||||
- **`pre_fetch`** — milliseconds **before** each scheduled push when the host sends the UART cache read, so the master has time to collect data from all slaves before the JSON goes out.
|
||||
|
||||
Sent only when `set_stream` has `enable: true`, a slave streams accel, and the poll tick fires for this connection.
|
||||
The server UART poll uses the **minimum** `interval_ms` among all subscribers with `receive_input: true`.
|
||||
|
||||
**Success** — all slaves with a cache entry on the master (not only those with `valid: true`):
|
||||
### `input` (type `"input"`)
|
||||
|
||||
Sent when `set_stream` has `enable: true` and the poll tick fires for this connection (after the UART read started `pre_fetch` ms earlier). Each message combines the latest accel cache and visible tap state for every slave slot on the master.
|
||||
|
||||
**Success** — all slaves with a cache entry (not only those with `valid: true`):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "accel",
|
||||
"type": "input",
|
||||
"t": 1716900123456789012,
|
||||
"success": true,
|
||||
"clients": [
|
||||
@ -66,11 +66,14 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
|
||||
"x": 12,
|
||||
"y": -34,
|
||||
"z": 16384,
|
||||
"age_ms": 8
|
||||
"accel_age_ms": 8,
|
||||
"tap_kind": "single",
|
||||
"tap_age_ms": 3
|
||||
},
|
||||
{
|
||||
"client_id": 42,
|
||||
"valid": false
|
||||
"valid": false,
|
||||
"tap_kind": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -82,15 +85,19 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
|
||||
| `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 |
|
||||
| `valid` | `false` if no accel 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 |
|
||||
| `accel_age_ms` | Milliseconds since the master received this accel sample |
|
||||
| `tap_kind` | `"none"`, `"single"`, `"double"`, or `"triple"` |
|
||||
| `tap_age_ms` | Milliseconds since the tap was seen in the master cache; omit when `tap_kind` is `"none"` |
|
||||
|
||||
Tap events stay visible for **`tap_display_min_ms`** (2000 ms, also in `hello`) after the API first saw them, even if the hardware age grows.
|
||||
|
||||
**Failure** (e.g. UART busy):
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "accel",
|
||||
"type": "input",
|
||||
"t": 1716900123456789012,
|
||||
"success": false,
|
||||
"error": "uart busy"
|
||||
@ -99,53 +106,6 @@ Sent only when `set_stream` has `enable: true`, a slave streams accel, and the p
|
||||
|
||||
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)
|
||||
@ -159,13 +119,13 @@ Send one JSON object per message. Field `type` selects the command.
|
||||
"type": "hello",
|
||||
"serial_port": "/dev/ttyUSB0",
|
||||
"interval_ms": 16,
|
||||
"pre_fetch_ms": 2,
|
||||
"tap_display_min_ms": 2000,
|
||||
"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": [
|
||||
"list_clients",
|
||||
"set_stream", "get_stream",
|
||||
"set_accel_stream", "get_accel_stream",
|
||||
"set_tap_stream", "get_tap_stream",
|
||||
"set_input_stream", "get_input_stream",
|
||||
"set_tap_notify", "get_tap_notify",
|
||||
"set_led_ring", "get_battery"
|
||||
]
|
||||
@ -191,7 +151,7 @@ Response `client_list`:
|
||||
"used": true,
|
||||
"last_ping": 1234,
|
||||
"last_success_ping": 1200,
|
||||
"accel_stream": false,
|
||||
"input_stream": false,
|
||||
"tap_notify_single": false,
|
||||
"tap_notify_double": false,
|
||||
"tap_notify_triple": false
|
||||
@ -200,45 +160,38 @@ Response `client_list`:
|
||||
}
|
||||
```
|
||||
|
||||
### `set_stream` / `get_stream` (receive accel on this connection)
|
||||
### `set_stream` / `get_stream` (receive input on this connection)
|
||||
|
||||
```json
|
||||
{"type":"set_stream","enable":true,"interval_ms":32}
|
||||
{"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2}
|
||||
{"type":"get_stream"}
|
||||
```
|
||||
|
||||
| Field | Meaning |
|
||||
|-------|---------|
|
||||
| `enable` | Turn push stream on/off for this connection |
|
||||
| `interval_ms` | Minimum time between `input` pushes (1 … 10000) |
|
||||
| `pre_fetch` | Milliseconds before each push when the host starts the UART cache read; optional, default in `hello` (`pre_fetch_ms`) |
|
||||
|
||||
Response `stream_status`:
|
||||
|
||||
```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, per slave)
|
||||
|
||||
`client_id` required (> 0).
|
||||
`client_id` required (> 0). Enables accel streaming from the slave to the master.
|
||||
|
||||
```json
|
||||
{"type":"set_accel_stream","client_id":16,"enable":true}
|
||||
{"type":"get_accel_stream","client_id":16}
|
||||
{"type":"set_input_stream","client_id":16,"enable":true}
|
||||
{"type":"get_input_stream","client_id":16}
|
||||
```
|
||||
|
||||
Response `accel_stream_status`:
|
||||
Response `input_stream_status`:
|
||||
|
||||
```json
|
||||
{"type":"accel_stream_status","client_id":16,"enabled":true,"success":true}
|
||||
```
|
||||
|
||||
### `set_tap_stream` / `get_tap_stream` (receive tap on this connection)
|
||||
|
||||
```json
|
||||
{"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}
|
||||
{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
|
||||
```
|
||||
|
||||
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
|
||||
@ -266,83 +219,55 @@ Response `tap_notify_status`:
|
||||
|
||||
### `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`.
|
||||
Control the LED ring on the master or a slave.
|
||||
|
||||
```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}
|
||||
```
|
||||
|
||||
| `mode` | Notes |
|
||||
|--------|--------|
|
||||
| `clear` | Turn off |
|
||||
| `color` | Full ring RGB + `intensity` |
|
||||
| `progress` | `progress` 0–100 |
|
||||
| `digit` | `digit` 0–10 |
|
||||
| `blink` | `blink_ms`, `blink_count` |
|
||||
| `find-me` | Locate pod |
|
||||
|
||||
Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.
|
||||
|
||||
Response `led_ring_status`:
|
||||
|
||||
```json
|
||||
{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1}
|
||||
```
|
||||
|
||||
### `get_battery`
|
||||
|
||||
Body: `{"type":"get_battery","all_clients":true}` or `"client_id":16`. Default if omitted: all clients.
|
||||
Read cached battery samples from the master. Slaves push battery every **30 s**; this command reads the master cache.
|
||||
|
||||
Reply: `battery_status` with `samples[]` (see REST doc).
|
||||
|
||||
---
|
||||
|
||||
## 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())
|
||||
```json
|
||||
{"type":"get_battery","all_clients":true}
|
||||
{"type":"get_battery","client_id":16}
|
||||
```
|
||||
|
||||
### Tap stream
|
||||
Default if omitted: all clients.
|
||||
|
||||
```python
|
||||
import asyncio, json, websockets
|
||||
Response `battery_status`:
|
||||
|
||||
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": "set_tap_notify", "client_id": 16,
|
||||
"single": True, "double_tap": False, "triple": False
|
||||
}))
|
||||
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())
|
||||
```json
|
||||
{
|
||||
"type": "battery_status",
|
||||
"success": true,
|
||||
"samples": [
|
||||
{
|
||||
"client_id": 16,
|
||||
"lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
|
||||
"lipo2": {"valid": false},
|
||||
"age_ms": 1200
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dashboard WebSocket (`:8080/ws`)
|
||||
|
||||
Read-only from the browser’s 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.
|
||||
|
||||
@ -62,6 +62,8 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("open serial: %v", err)
|
||||
}
|
||||
registerShutdown(func() { _ = sp.Close() })
|
||||
enableShutdownOnInterrupt()
|
||||
defer sp.Close()
|
||||
switch cmd {
|
||||
case "version":
|
||||
|
||||
@ -2,7 +2,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
@ -69,16 +68,45 @@ const (
|
||||
)
|
||||
|
||||
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()
|
||||
defer m.mu.Unlock()
|
||||
err := runOTAOnPortUnlocked(m, firmware, onProgress)
|
||||
if m.otaActive {
|
||||
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 {
|
||||
m.invalidateLocked(err)
|
||||
}
|
||||
m.otaActive = false
|
||||
m.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
|
||||
func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
|
||||
if len(firmware) == 0 {
|
||||
return fmt.Errorf("empty firmware")
|
||||
}
|
||||
@ -120,32 +148,31 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
||||
onProgress(p)
|
||||
}
|
||||
|
||||
if m.sp == nil {
|
||||
if err := m.openLocked(); err != nil {
|
||||
if err := sp.port.SetReadTimeout(readTimeout); 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())
|
||||
return err
|
||||
}
|
||||
defer sp.port.SetReadTimeout(readTimeout)
|
||||
|
||||
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
|
||||
|
||||
flushSerialInput(sp)
|
||||
|
||||
if err := writeUartMessage(sp, &pb.UartMessage{
|
||||
Type: pb.MessageType_OTA_START,
|
||||
Payload: &pb.UartMessage_OtaStart{
|
||||
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
|
||||
},
|
||||
}, false); err != nil {
|
||||
}); err != nil {
|
||||
notify("error", "", 0, err.Error())
|
||||
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) {
|
||||
notify("preparing", otaStepMaster, 2, msg)
|
||||
})
|
||||
@ -179,7 +206,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
||||
Payload: &pb.UartMessage_OtaPayload{
|
||||
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
||||
},
|
||||
}, false); err != nil {
|
||||
}); err != nil {
|
||||
notify("error", "", 0, err.Error())
|
||||
return err
|
||||
}
|
||||
@ -219,7 +246,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
||||
Payload: &pb.UartMessage_OtaEnd{
|
||||
OtaEnd: &pb.OtaEndPayload{},
|
||||
},
|
||||
}, false); err != nil {
|
||||
}); err != nil {
|
||||
notify("error", "", 0, err.Error())
|
||||
return err
|
||||
}
|
||||
@ -333,7 +360,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
|
||||
}
|
||||
if queryTimeout <= 0 {
|
||||
@ -389,7 +416,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
|
||||
if err := sp.port.SetReadTimeout(wait); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := uartframe.ReadFrame(sp.port, nil)
|
||||
payload, err := uartframe.ReadFrame(sp.port, nil, wait)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -487,14 +514,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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if logFrame {
|
||||
log.Printf("sending %s (%d frame bytes)", msg.Type, len(frame))
|
||||
}
|
||||
_, err = sp.port.Write(frame)
|
||||
return err
|
||||
}
|
||||
@ -505,12 +529,24 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
||||
if time.Now().After(deadline) {
|
||||
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
|
||||
}
|
||||
st, err := readOtaStatus(sp)
|
||||
payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
|
||||
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() {
|
||||
case want:
|
||||
@ -526,7 +562,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
@ -553,6 +589,22 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
|
||||
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) {
|
||||
if len(payload) == 0 {
|
||||
return nil, fmt.Errorf("empty response")
|
||||
|
||||
@ -11,6 +11,9 @@ import (
|
||||
// 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)")
|
||||
|
||||
// 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.
|
||||
type managedSerial struct {
|
||||
portName string
|
||||
@ -19,6 +22,7 @@ type managedSerial struct {
|
||||
|
||||
mu sync.Mutex
|
||||
sp *serialPort
|
||||
otaActive bool // UART held for firmware upload; poll/API must not interleave
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return m.withPortLocked(true, fn)
|
||||
}
|
||||
|
||||
func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) error {
|
||||
if try {
|
||||
if !m.mu.TryLock() {
|
||||
func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if m.otaActive {
|
||||
return errUARTBusy
|
||||
}
|
||||
} else {
|
||||
m.mu.Lock()
|
||||
}
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if m.sp == nil {
|
||||
if err := m.openLocked(); err != nil {
|
||||
@ -104,6 +105,26 @@ func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) 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) {
|
||||
return m.exchangePayloadVia(m.withPort, payload, cmdName)
|
||||
}
|
||||
|
||||
@ -80,7 +80,7 @@ func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string, timeo
|
||||
}
|
||||
defer func() { _ = s.port.SetReadTimeout(readTimeout) }()
|
||||
|
||||
respPayload, err := uartframe.ReadFrame(s.port, nil)
|
||||
respPayload, err := uartframe.ReadFrame(s.port, nil, timeout)
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
payload, err := uartframe.ReadFrame(s.port, nil)
|
||||
payload, err := uartframe.ReadFrame(s.port, nil, readTimeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response: %w", err)
|
||||
}
|
||||
|
||||
75
goTool/shutdown.go
Normal file
75
goTool/shutdown.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -4,12 +4,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
StartMarker = 0xAA
|
||||
StopMarker = 0xCC
|
||||
MaxPayload = 252
|
||||
// Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4).
|
||||
MaxPayload = 248
|
||||
)
|
||||
|
||||
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.
|
||||
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 {
|
||||
buf = make([]byte, 256)
|
||||
}
|
||||
parser := NewParser()
|
||||
|
||||
var deadline time.Time
|
||||
if maxWait > 0 {
|
||||
deadline = time.Now().Add(maxWait)
|
||||
}
|
||||
|
||||
for {
|
||||
if !deadline.IsZero() && !time.Now().Before(deadline) {
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
n, err := r.Read(buf)
|
||||
if n > 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
|
||||
@ -938,8 +938,21 @@
|
||||
return rows;
|
||||
},
|
||||
applyOTAProgress(p) {
|
||||
this.ota.phase = p.phase || '';
|
||||
this.ota.step = p.step || this.ota.step || '';
|
||||
const prevPhase = this.ota.phase;
|
||||
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.message = p.message || '';
|
||||
if (p.image_size) this.ota.imageSize = p.image_size;
|
||||
|
||||
@ -31,8 +31,12 @@ idf_component_register(
|
||||
"cmd/cmd_ota_slave_progress.c"
|
||||
"ota_uart.c"
|
||||
"ota_espnow.c"
|
||||
"ota_session.c"
|
||||
"client_registry.c"
|
||||
"esp_now_comm.c"
|
||||
"esp_now_core.c"
|
||||
"esp_now_master.c"
|
||||
"esp_now_slave.c"
|
||||
"esp_now_proto.c"
|
||||
"bosch456.c"
|
||||
"board_input.c"
|
||||
|
||||
@ -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.
|
||||
|
||||
**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
|
||||
|
||||
```
|
||||
@ -176,7 +178,7 @@ Logging:
|
||||
|
||||
## 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
|
||||
@ -186,7 +188,8 @@ UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
|
||||
|-----|-------------|
|
||||
| `init_cmdHandler(queue)` | Start dispatcher task (priority 5) |
|
||||
| `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
|
||||
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
|
||||
@ -517,7 +520,10 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
||||
| `powerpod.c` | `app_main`, DIP/network config, init order |
|
||||
| `powerpod.h` | Pin defines |
|
||||
| `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_espnow.c/h` | Master: distribute staged image to slaves |
|
||||
| `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) |
|
||||
|
||||
@ -38,6 +38,7 @@ static volatile bool s_int_pending;
|
||||
static SemaphoreHandle_t s_accel_mutex;
|
||||
static bma456_tap_handler_t s_tap_handler;
|
||||
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);
|
||||
|
||||
@ -263,19 +264,48 @@ static esp_err_t configure_tap_interrupt(void) {
|
||||
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
|
||||
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);
|
||||
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
ret = bma456h_feature_enable(
|
||||
(BMA456H_SINGLE_TAP_EN | BMA456H_DOUBLE_TAP_EN | BMA456H_TRIPLE_TAP_EN),
|
||||
BMA4_ENABLE, &s_bma456);
|
||||
uint16_t tap_features = 0;
|
||||
if (s_tap_config.enable_single) {
|
||||
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) {
|
||||
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,
|
||||
&s_bma456);
|
||||
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
|
||||
|
||||
@ -10,6 +10,8 @@
|
||||
|
||||
#include "driver/i2c_types.h"
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
/** 7-bit I2C address (SDO low). */
|
||||
#define BMA456_I2C_ADDR 0x18
|
||||
@ -20,6 +22,40 @@
|
||||
/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */
|
||||
#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).
|
||||
* On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned;
|
||||
|
||||
@ -33,12 +33,20 @@ static void handle_accel_stream(const uint8_t *data, size_t len) {
|
||||
alox_UartMessage uart_msg;
|
||||
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(
|
||||
&uart_msg, alox_UartMessage_accel_stream_request_tag, accel_stream_request);
|
||||
if (req_ptr != NULL) {
|
||||
req = *req_ptr;
|
||||
if (req_ptr == NULL) {
|
||||
ESP_LOGW(TAG, "missing accel_stream_request");
|
||||
reply(false, 0, false, 0);
|
||||
return;
|
||||
}
|
||||
req = *req_ptr;
|
||||
}
|
||||
|
||||
if (req.write) {
|
||||
|
||||
@ -67,15 +67,29 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
|
||||
|
||||
if (len > 0) {
|
||||
alox_UartMessage uart_msg;
|
||||
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) {
|
||||
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_BATTERY_STATUS,
|
||||
alox_UartMessage_battery_status_response_tag);
|
||||
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) {
|
||||
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;
|
||||
uart_cmd_init_response(&response, alox_MessageType_BATTERY_STATUS,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
#include "cmd_handler.h"
|
||||
#include "ota_session.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_log.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
@ -88,33 +89,19 @@ esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb) {
|
||||
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)param;
|
||||
generic_msg_t msg;
|
||||
|
||||
while (1) {
|
||||
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;
|
||||
for (int i = 0; i < handler_count; i++) {
|
||||
if (handlers[i].msg_id == msg.msg_id) {
|
||||
|
||||
@ -21,6 +21,5 @@ void init_cmdHandler(QueueHandle_t queue);
|
||||
void vCmdDispatcherTask(void *param);
|
||||
|
||||
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
|
||||
|
||||
@ -81,8 +81,6 @@ static const ota_espnow_progress_cbs_t s_dist_progress = {
|
||||
static void ota_prepare_task(void *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);
|
||||
if (slot < 0) {
|
||||
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 =
|
||||
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start);
|
||||
if (req_ptr != NULL) {
|
||||
req = *req_ptr;
|
||||
if (req_ptr == NULL) {
|
||||
ESP_LOGW(TAG, "OTA_START: missing ota_start payload");
|
||||
send_ota_failed(3);
|
||||
return;
|
||||
}
|
||||
req = *req_ptr;
|
||||
|
||||
if (req.total_size == 0) {
|
||||
ESP_LOGW(TAG, "OTA_START: total_size required");
|
||||
send_ota_failed( 3);
|
||||
send_ota_failed(3);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ota_uart_is_active()) {
|
||||
ESP_LOGW(TAG, "OTA_START while session active");
|
||||
send_ota_failed( 4);
|
||||
send_ota_failed(4);
|
||||
return;
|
||||
}
|
||||
|
||||
send_ota_status(OTA_UART_ST_PREPARING, 0);
|
||||
|
||||
if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK,
|
||||
(void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
|
||||
NULL) != pdPASS) {
|
||||
ESP_LOGE(TAG, "failed to create ota_prepare task");
|
||||
send_ota_failed( 5);
|
||||
send_ota_failed(5);
|
||||
}
|
||||
}
|
||||
|
||||
@ -293,31 +296,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) {
|
||||
(void)data;
|
||||
(void)len;
|
||||
|
||||
if (ota_uart_is_active()) {
|
||||
send_ota_failed( 40);
|
||||
return;
|
||||
}
|
||||
static void ota_start_espnow_task(void *param) {
|
||||
(void)param;
|
||||
|
||||
const esp_partition_t *part = NULL;
|
||||
uint32_t image_size = 0;
|
||||
if (!ota_uart_get_staged_image(&part, &image_size)) {
|
||||
send_ota_failed( 41);
|
||||
send_ota_failed(41);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
|
||||
if (err != ESP_OK) {
|
||||
send_ota_failed( 42);
|
||||
send_ota_failed(42);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
err = ota_uart_apply_boot();
|
||||
if (err != ESP_OK) {
|
||||
send_ota_failed( (uint32_t)err);
|
||||
send_ota_failed((uint32_t)err);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -329,6 +329,30 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
|
||||
response.payload.ota_status.error = 0;
|
||||
uart_cmd_send(&response, TAG);
|
||||
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) {
|
||||
|
||||
@ -9,13 +9,29 @@ static void handle_ota_slave_progress(const uint8_t *data, size_t len) {
|
||||
alox_UartMessage uart_msg;
|
||||
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 =
|
||||
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag,
|
||||
ota_slave_progress_request);
|
||||
if (req != NULL) {
|
||||
filter = req->client_id;
|
||||
if (req == NULL) {
|
||||
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;
|
||||
|
||||
@ -39,12 +39,20 @@ static void handle_tap_notify(const uint8_t *data, size_t len) {
|
||||
alox_UartMessage uart_msg;
|
||||
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(
|
||||
&uart_msg, alox_UartMessage_tap_notify_request_tag, tap_notify_request);
|
||||
if (req_ptr != NULL) {
|
||||
req = *req_ptr;
|
||||
if (req_ptr == NULL) {
|
||||
ESP_LOGW(TAG, "missing tap_notify_request");
|
||||
reply(0, false, 0, false, false, false);
|
||||
return;
|
||||
}
|
||||
req = *req_ptr;
|
||||
}
|
||||
|
||||
if (req.write) {
|
||||
|
||||
1255
main/esp_now_comm.c
1255
main/esp_now_comm.c
File diff suppressed because it is too large
Load Diff
172
main/esp_now_core.c
Normal file
172
main/esp_now_core.c
Normal file
@ -0,0 +1,172 @@
|
||||
#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 bool s_send_cb_ready;
|
||||
|
||||
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;
|
||||
(void)status;
|
||||
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);
|
||||
}
|
||||
|
||||
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
|
||||
const alox_EspNowMessage *msg) {
|
||||
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");
|
||||
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);
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
|
||||
if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
|
||||
if (s_send_cb_ready && s_send_done != NULL) {
|
||||
xSemaphoreTake(s_send_done, 0);
|
||||
}
|
||||
|
||||
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));
|
||||
return err;
|
||||
}
|
||||
|
||||
if (s_send_cb_ready && s_send_done != NULL) {
|
||||
if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type);
|
||||
}
|
||||
}
|
||||
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();
|
||||
if (s_send_done != 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)");
|
||||
}
|
||||
}
|
||||
32
main/esp_now_core.h
Normal file
32
main/esp_now_core.h
Normal file
@ -0,0 +1,32 @@
|
||||
#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);
|
||||
esp_err_t esp_now_core_send_wait(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
|
||||
484
main/esp_now_master.c
Normal file
484
main/esp_now_master.c
Normal file
@ -0,0 +1,484 @@
|
||||
#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_uart.h"
|
||||
#include "esp_log.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);
|
||||
}
|
||||
|
||||
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_wait(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_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.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) {
|
||||
esp_now_core_send(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
main/esp_now_master.h
Normal file
11
main/esp_now_master.h
Normal 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
|
||||
579
main/esp_now_slave.c
Normal file
579
main/esp_now_slave.c
Normal file
@ -0,0 +1,579 @@
|
||||
#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)
|
||||
#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_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 void reset_join(void) {
|
||||
s_joined = 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_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;
|
||||
}
|
||||
if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) {
|
||||
s_last_discover_ms = now;
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "master lost, rejoining");
|
||||
reset_join();
|
||||
}
|
||||
|
||||
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
|
||||
s_joined = true;
|
||||
s_last_discover_ms = 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;
|
||||
}
|
||||
uint32_t now = esp_now_core_now_ms();
|
||||
if ((now - s_last_discover_ms) > SLAVE_MASTER_LOST_MS) {
|
||||
ESP_LOGW(TAG, "no master discover for %u ms, reconnecting",
|
||||
(unsigned)(now - s_last_discover_ms));
|
||||
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_ota_start_tag:
|
||||
case alox_EspNowMessage_ota_payload_tag:
|
||||
case alox_EspNowMessage_ota_end_tag:
|
||||
if (!from_joined_master(info->src_addr)) {
|
||||
break;
|
||||
}
|
||||
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_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;
|
||||
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
main/esp_now_slave.h
Normal file
14
main/esp_now_slave.h
Normal 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
|
||||
@ -35,7 +35,28 @@ static const char *TAG = "[OTA_ESPNOW]";
|
||||
|
||||
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
|
||||
|
||||
#define OTA_SLAVE_WORK_QUEUE_LEN 12
|
||||
#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 QueueHandle_t s_slave_work_queue;
|
||||
static bool s_distribution_active;
|
||||
|
||||
typedef struct {
|
||||
uint8_t count;
|
||||
@ -152,56 +173,37 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
||||
return (got & want_bits) == want_bits;
|
||||
}
|
||||
|
||||
bool ota_espnow_distribution_active(void) { return s_distribution_active; }
|
||||
|
||||
static void send_slave_status(const uint8_t master_mac[6], uint32_t status,
|
||||
uint32_t bytes_written, uint32_t error) {
|
||||
esp_now_comm_send_ota_status(master_mac, status, bytes_written, error);
|
||||
}
|
||||
|
||||
static void ota_slave_prepare_task(void *param) {
|
||||
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||
uint8_t master_mac[6];
|
||||
|
||||
if (!esp_now_comm_get_master_mac(master_mac)) {
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
static bool queue_slave_work(const ota_slave_work_t *work) {
|
||||
if (work == NULL || s_slave_work_queue == NULL) {
|
||||
return false;
|
||||
}
|
||||
|
||||
send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0);
|
||||
|
||||
int slot = ota_uart_prepare(total_size);
|
||||
if (slot < 0) {
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
if (xQueueSend(s_slave_work_queue, work, 0) != pdTRUE) {
|
||||
ESP_LOGW(TAG, "slave OTA work queue full (op=%d)", (int)work->op);
|
||||
return false;
|
||||
}
|
||||
|
||||
send_slave_status(master_mac, OTA_ST_READY, 0, 0);
|
||||
led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
||||
OTA_LED_ESPNOW_RX_B);
|
||||
vTaskDelete(NULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_start(const uint8_t master_mac[6],
|
||||
const alox_EspNowOtaStart *start) {
|
||||
if (start == NULL || start->total_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
|
||||
|
||||
if (ota_uart_is_active()) {
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
|
||||
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
|
||||
NULL) != pdPASS) {
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
|
||||
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);
|
||||
@ -246,7 +248,7 @@ void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
|
||||
}
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
|
||||
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);
|
||||
@ -268,6 +270,113 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
|
||||
(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) {
|
||||
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||
uint8_t master_mac[6];
|
||||
|
||||
if (!esp_now_comm_get_master_mac(master_mac)) {
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
send_slave_status(master_mac, OTA_ST_PREPARING, 0, 0);
|
||||
|
||||
int slot = ota_uart_prepare(total_size);
|
||||
if (slot < 0) {
|
||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 1);
|
||||
vTaskDelete(NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
send_slave_status(master_mac, OTA_ST_READY, 0, 0);
|
||||
led_ring_show_ota_progress(0, total_size, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
||||
OTA_LED_ESPNOW_RX_B);
|
||||
vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_start(const uint8_t master_mac[6],
|
||||
const alox_EspNowOtaStart *start) {
|
||||
if (start == NULL || start->total_size == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
|
||||
|
||||
if (ota_uart_is_active()) {
|
||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
|
||||
return;
|
||||
}
|
||||
|
||||
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
|
||||
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
|
||||
NULL) != pdPASS) {
|
||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
|
||||
}
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
|
||||
const alox_EspNowOtaPayload *payload) {
|
||||
if (payload == NULL) {
|
||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
|
||||
return;
|
||||
}
|
||||
|
||||
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_PAYLOAD, .payload = *payload};
|
||||
memcpy(work.master_mac, master_mac, 6);
|
||||
if (!queue_slave_work(&work)) {
|
||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 14);
|
||||
}
|
||||
}
|
||||
|
||||
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
|
||||
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_END};
|
||||
memcpy(work.master_mac, master_mac, 6);
|
||||
if (!queue_slave_work(&work)) {
|
||||
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 15);
|
||||
}
|
||||
}
|
||||
|
||||
void ota_espnow_master_on_status(const uint8_t slave_mac[6],
|
||||
const alox_EspNowOtaStatus *status) {
|
||||
if (status == NULL || s_eg == NULL) {
|
||||
@ -342,6 +451,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));
|
||||
if (progress != NULL) {
|
||||
s_dist.progress = *progress;
|
||||
@ -362,6 +473,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
ESP_LOGW(TAG, "OTA_START to slave %lu failed",
|
||||
(unsigned long)s_dist.id[i]);
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@ -369,6 +481,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
|
||||
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
@ -392,6 +505,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
|
||||
esp_err_to_name(err));
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return err;
|
||||
}
|
||||
|
||||
@ -407,6 +521,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
block_buf + sent, chunk);
|
||||
if (err != ESP_OK) {
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@ -424,6 +539,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
ESP_LOGE(TAG, "timeout block ack @%lu bytes",
|
||||
(unsigned long)s_dist.expected_bytes);
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
|
||||
@ -445,6 +561,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
|
||||
if (err != ESP_OK) {
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return err;
|
||||
}
|
||||
}
|
||||
@ -452,11 +569,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
||||
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
|
||||
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
return ESP_ERR_TIMEOUT;
|
||||
}
|
||||
|
||||
prog_set_aggregate(size);
|
||||
prog_end();
|
||||
s_distribution_active = false;
|
||||
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
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
|
||||
|
||||
25
main/ota_session.c
Normal file
25
main/ota_session.c
Normal 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
main/ota_session.h
Normal file
13
main/ota_session.h
Normal 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
|
||||
Loading…
x
Reference in New Issue
Block a user