Compare commits
22
Commits
16c521f71c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f006f8b912 | ||
|
|
be18b758ed | ||
|
|
490e0ee61f | ||
|
|
ac223ada72 | ||
|
|
35ce1476d8 | ||
|
|
f89ea3cbe3 | ||
|
|
99956e3362 | ||
|
|
ab1844ac32 | ||
|
|
e4ce18edd8 | ||
|
|
0eea27a876 | ||
|
|
0cbc4d0644 | ||
|
|
35b39fce46 | ||
|
|
41a66d4417 | ||
|
|
498b89d7ba | ||
|
|
31e539052a | ||
|
|
a85d48320e | ||
|
|
f512936d97 | ||
|
|
a8d4d42920 | ||
|
|
3cb0b5bbe9 | ||
|
|
eb67a46158 | ||
|
|
47c75110c9 | ||
|
|
ba20544762 |
@@ -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_espnow_echo_ping.c`, `cmd/cmd_ota.c` (OTA + `ota_espnow.c`).
|
||||||
|
|
||||||
|
**Nur Master / nur Cache (kein Slave-Roundtrip):** `cmd_client_info.c`, `cmd_battery.c`, `cmd_cache_status.c`, `cmd_version.c`, `cmd_set_log_level.c`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Datenfluss: UART
|
||||||
|
|
||||||
|
### 5.1 Rahmenformat (Transport)
|
||||||
|
|
||||||
|
Unabhängig von Protobuf — reines Bytestream-Framing:
|
||||||
|
|
||||||
|
| Feld | Wert |
|
||||||
|
|------|------|
|
||||||
|
| Start | `0xAA` |
|
||||||
|
| Länge | 1 Byte (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).
|
||||||
@@ -0,0 +1,564 @@
|
|||||||
|
# Powerpod ESP-Firmware — Dokumentation
|
||||||
|
|
||||||
|
Vollständige Referenz für das ESP-IDF-Projekt unter `main/` und `components/`. Diese Doku beschreibt **nur die Firmware** — kein goTool, keine Host-Implementierung.
|
||||||
|
|
||||||
|
- **Architektur & Datenflüsse:** [`ARCHITECTURE.md`](ARCHITECTURE.md)
|
||||||
|
- **Neues Feature end-to-end:** [`adding-a-feature.md`](adding-a-feature.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Inhaltsverzeichnis
|
||||||
|
|
||||||
|
1. [Projektziel](#1-projektziel)
|
||||||
|
2. [Hardware und Pins](#2-hardware-und-pins)
|
||||||
|
3. [Build und Flash](#3-build-und-flash)
|
||||||
|
4. [Boot-Konfiguration](#4-boot-konfiguration)
|
||||||
|
5. [UART-Protokoll](#5-uart-protokoll)
|
||||||
|
6. [ESP-NOW-Protokoll](#6-esp-now-protokoll)
|
||||||
|
7. [Befehlsreferenz (UART)](#7-befehlsreferenz-uart)
|
||||||
|
8. [OTA](#8-ota)
|
||||||
|
9. [BMA456 Beschleunigungssensor](#9-bma456-beschleunigungssensor)
|
||||||
|
10. [LED-Ring](#10-led-ring)
|
||||||
|
11. [Board-Input und Batterie](#11-board-input-und-batterie)
|
||||||
|
12. [Persistenz (NVS)](#12-persistenz-nvs)
|
||||||
|
13. [Protobuf und Code-Generierung](#13-protobuf-und-code-generierung)
|
||||||
|
14. [Modulreferenz](#14-modulreferenz)
|
||||||
|
15. [Logging-Tags](#15-logging-tags)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Projektziel
|
||||||
|
|
||||||
|
**Powerpod** ist Firmware für ESP32-S3-Knoten in einem verteilten System:
|
||||||
|
|
||||||
|
- Ein **Master** spricht per **UART** mit einem externen Host und verwaltet bis zu **16 Slaves** über **ESP-NOW**.
|
||||||
|
- **Slaves** haben keinen UART-Befehlspfad; sie joinen über periodisches **Discover**, senden Heartbeats und liefern Sensor-/Statusdaten.
|
||||||
|
- Master und Slave nutzen **identisches Firmware-Image**; Rolle und Funknetz werden beim Boot erkannt.
|
||||||
|
|
||||||
|
Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves und **cached** deren Telemetrie (`client_registry`) für schnelle UART-Abfragen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Hardware und Pins
|
||||||
|
|
||||||
|
> Pinbelegung ist in `powerpod.h` als vorläufig markiert — mit Schaltplan abgleichen.
|
||||||
|
|
||||||
|
| Signal | GPIO | Datei / Modul |
|
||||||
|
|--------|------|----------------|
|
||||||
|
| DIP Master/Slave | 4 | `powerpod.h` — Low = Master |
|
||||||
|
| I2C SCL / SDA | 5 / 6 | IO-Expander `0x20`, BMA456 `0x18` |
|
||||||
|
| UART1 TX / RX | **2 / 3** | `uart.h` (Adapter: ESP-TX → Host-RX) |
|
||||||
|
| LED-Ring (WS2812) | 7 | `led_ring.c` |
|
||||||
|
| BMA456 Interrupt | 10 | `bosch456.c` |
|
||||||
|
| Taster | 12 | `board_input.c` |
|
||||||
|
| LiPo ADC 1 | 1 | `board_input.c` |
|
||||||
|
| LiPo ADC 2 | 11 | `board_input.c` |
|
||||||
|
|
||||||
|
**UART (Host-Protokoll):** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control.
|
||||||
|
|
||||||
|
**ESP-IDF-Log (Debug):** `esp_log_*` geht auf **UART0** (Standard-Konsole, typisch USB am Dev-Board, **115200** Baud, `CONFIG_ESP_CONSOLE_UART_NUM=0`). Das ist **getrennt** vom Host-UART1 — goTool liest keine `esp_log`-Ausgabe. Ohne angeschlossenes Debug-Kabel werden aktivierte Logs trotzdem formatiert und an UART0 gesendet (CPU-Overhead bleibt); nur `ESP_LOG_NONE` / Level-Filter vermeiden die Arbeit.
|
||||||
|
|
||||||
|
**I2C:** 100 kHz, interne Pull-ups, gemeinsamer Bus für Expander und BMA456H.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Build und Flash
|
||||||
|
|
||||||
|
**Ziel-Chip:** ESP32-S3 (ESP-IDF).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
source ~/esp/esp-idf/export.sh # oder export.fish
|
||||||
|
cd /pfad/zu/powerpod
|
||||||
|
idf.py build
|
||||||
|
idf.py -p /dev/ttyUSB0 flash monitor
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Git-Hash** wird beim Build in `POWERPOD_GIT_HASH` eingebettet (`main/CMakeLists.txt`).
|
||||||
|
- **Firmware-Version:** `POWERPOD_FW_VERSION` (Default `1` in `esp_now_comm.c` / `cmd_version.c`).
|
||||||
|
|
||||||
|
**Komponenten:**
|
||||||
|
|
||||||
|
| Pfad | Inhalt |
|
||||||
|
|------|--------|
|
||||||
|
| `main/` | Anwendungslogik |
|
||||||
|
| `components/bma456/` | Bosch BMA456H-Treiber (Vendor) |
|
||||||
|
| `libs/nanopb/` | Protobuf-Codec für Embedded |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Boot-Konfiguration
|
||||||
|
|
||||||
|
### 4.1 Initialisierungsreihenfolge (`powerpod.c`)
|
||||||
|
|
||||||
|
1. `pod_settings_init()` — NVS
|
||||||
|
2. `board_input_init_adc_only()` — LiPo-ADC
|
||||||
|
3. Bei `POWERPOD_BATTERY_UV_ENABLE`: `battery_uv_evaluate_boot()` → ggf. Energiesparmodus (nur LED + ADC, **kein** weiterer Init)
|
||||||
|
4. DIP → `app_config.master`
|
||||||
|
5. I2C + IO-Expander → `app_config.network` (1–8)
|
||||||
|
6. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
|
||||||
|
7. `pod_settings_apply_accel_deadzone()` wenn Sensor da
|
||||||
|
8. OTA-Partition → `app_config.running_partition`
|
||||||
|
9. `board_input_start_lipo_monitor()` + `board_input_init_button()`
|
||||||
|
10. `esp_now_comm_init(&app_config)` — **normaler Pfad** (Master + Slave)
|
||||||
|
11. `led_ring_init()`
|
||||||
|
12. **Nur Master:** `cmd_queue` → `init_cmdHandler` → `init_uart` → alle `cmd_*_register()`
|
||||||
|
|
||||||
|
### 4.2 Master vs. Slave
|
||||||
|
|
||||||
|
| Funktion | Master | Slave |
|
||||||
|
|----------|--------|-------|
|
||||||
|
| UART Command-Handler | Ja | Nein |
|
||||||
|
| ESP-NOW Discover senden | Ja (Broadcast) | Nein |
|
||||||
|
| ESP-NOW auf Discover reagieren | Nein | Ja |
|
||||||
|
| `client_registry` für Fremdknoten | Ja | Nein (nur eigener Join-State) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. UART-Protokoll
|
||||||
|
|
||||||
|
*(Transport + Anwendung — Datenfluss-Diagramme in [`ARCHITECTURE.md`](ARCHITECTURE.md) §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_ECHO_PING` | M→S | `echo_ping` | Latenztest (Host-Timestamp + `master_time_us`) |
|
||||||
|
| `ESPNOW_ECHO_PONG` | S→M | `echo_pong` | Echo unverändert zurück zum Master |
|
||||||
|
| `ESPNOW_OTA_*` | M↔S | `ota_*` | Firmware-Verteilung |
|
||||||
|
|
||||||
|
### 6.3 Zeitkonstanten (`esp_now_comm.c`)
|
||||||
|
|
||||||
|
| Konstante | Wert |
|
||||||
|
|-----------|------|
|
||||||
|
| Discover-Intervall | 500 ms |
|
||||||
|
| Heartbeat | 1000 ms |
|
||||||
|
| Client-Timeout (Master) | 3 × Heartbeat = 3 s → `available=false` |
|
||||||
|
| Master-Verlust (Slave) | 5 s ohne Discover |
|
||||||
|
| Accel-Stream | 16 ms |
|
||||||
|
| Batterie-Report | 30 s (+ einmal 150 ms nach Join) |
|
||||||
|
| Echo-Ping Timeout (Master) | 500 ms (`ESPNOW_ECHO_PING_TIMEOUT_MS`) |
|
||||||
|
|
||||||
|
### 6.4 `EspNowSlavePresence`
|
||||||
|
|
||||||
|
Felder: `network`, `mac` (6 B), `version`, `slave_id`, `available`, `used`.
|
||||||
|
|
||||||
|
- `slave_id` = letztes Oktett der STA-MAC.
|
||||||
|
- Registry auf dem Master indexiert über **Sender-MAC** der ESP-NOW-Callback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Befehlsreferenz (UART)
|
||||||
|
|
||||||
|
Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uart_messages.proto`.
|
||||||
|
|
||||||
|
| ID | Name | Modul | Kurzbeschreibung |
|
||||||
|
|----|------|-------|------------------|
|
||||||
|
| 1 | ACK | — | Reserviert |
|
||||||
|
| 2 | ECHO | — | Reserviert |
|
||||||
|
| 3 | VERSION | `cmd_version.c` | FW-Version, Git-Hash, OTA-Partition |
|
||||||
|
| 4 | CLIENT_INFO | `cmd_client_info.c` | Liste `client_registry` |
|
||||||
|
| 5 | CLIENT_INPUT | — | Geplant |
|
||||||
|
| 6 | ACCEL_DEADZONE | `cmd_accel_deadzone.c` | Get/Set Deadzone lokal + ESP-NOW |
|
||||||
|
| 7 | ESPNOW_UNICAST_TEST | `cmd_espnow_unicast_test.c` | Link-Test zu Slave |
|
||||||
|
| 8 | LED_RING | `cmd_led_ring.c` | Ring lokal / ESP-NOW |
|
||||||
|
| 16 | OTA_START | `cmd_ota.c` | UART-OTA beginnen |
|
||||||
|
| 17 | OTA_PAYLOAD | `cmd_ota.c` | Chunk ≤200 B |
|
||||||
|
| 18 | OTA_END | `cmd_ota.c` | Abschluss + ESP-NOW-Verteilung |
|
||||||
|
| 19 | OTA_STATUS | `cmd_ota.c` | Gerät → Host Status |
|
||||||
|
| 20 | OTA_START_ESPNOW | `cmd_ota.c` | Nur ESP-NOW aus Staging |
|
||||||
|
| 21 | OTA_SLAVE_PROGRESS | `cmd_ota_slave_progress.c` | Fortschritt pro Slave |
|
||||||
|
| 22 | FIND_ME | `cmd_espnow_find_me.c` | LED Locate |
|
||||||
|
| 23 | RESTART | `cmd_restart.c` | Master oder Slave reboot |
|
||||||
|
| 25 | ACCEL_STREAM | `cmd_accel_stream.c` | ESP-NOW Accel-Stream an/aus |
|
||||||
|
| 26 | BATTERY_STATUS | `cmd_battery.c` | Cache LiPo Master + Slaves |
|
||||||
|
| 27 | TAP_NOTIFY | `cmd_tap_notify.c` | Tap-Weiterleitung konfigurieren |
|
||||||
|
| 29 | CACHE_STATUS | `cmd_cache_status.c` | Accel + Tap Cache (ein Round-Trip) |
|
||||||
|
| 30 | ESPNOW_ECHO_PING | `cmd_espnow_echo_ping.c` | Timestamp-Echo über ESP-NOW (Latenztest) |
|
||||||
|
| 31 | SET_LOG_LEVEL | `cmd_set_log_level.c` | ESP-IDF-Log-Level global (`"*"`) lesen/setzen |
|
||||||
|
|
||||||
|
### 7.1 VERSION (3)
|
||||||
|
|
||||||
|
- **Request:** nur Typ-Byte `0x03` oder leerer protobuf-Body.
|
||||||
|
- **Response:** `version`, `git_hash`, `running_partition`.
|
||||||
|
|
||||||
|
### 7.2 CLIENT_INFO (4)
|
||||||
|
|
||||||
|
- **Response:** wiederholtes `ClientInfo` pro Registry-Eintrag: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping`, Tap-/Stream-Flags.
|
||||||
|
|
||||||
|
### 7.3 ACCEL_DEADZONE (6)
|
||||||
|
|
||||||
|
- **Request:** `write`, `deadzone`, `client_id` (0=lokal), `all_clients`.
|
||||||
|
- **Write Master:** Registry + `esp_now_comm_send_accel_deadzone` pro Slave; lokal `bma456` + NVS.
|
||||||
|
- **Slave-Empfang:** `handle_slave_accel_deadzone` in `esp_now_comm.c`.
|
||||||
|
|
||||||
|
### 7.4 ACCEL_STREAM (25)
|
||||||
|
|
||||||
|
- Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`.
|
||||||
|
- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms.
|
||||||
|
|
||||||
|
### 7.5 TAP_NOTIFY (27)
|
||||||
|
|
||||||
|
- Konfiguriert auf Slave welche Tap-Arten `ESPNOW_TAP_EVENT` auslösen.
|
||||||
|
- Tap-Quelle: BMA456-Interrupt → `on_bma456_tap` in `esp_now_comm.c`.
|
||||||
|
|
||||||
|
### 7.6 CACHE_STATUS (29)
|
||||||
|
|
||||||
|
- **Request:** leer.
|
||||||
|
- **Response:** pro Slave mit Stream und/oder Tap-Notify: gecachte Accel-Werte (`age_ms`) und konsumierte Tap-Events (`client_registry_take_tap`).
|
||||||
|
|
||||||
|
### 7.7 BATTERY_STATUS (26)
|
||||||
|
|
||||||
|
- Liest **nur Cache** — keine ESP-NOW-Roundtrip-Pflicht pro Abfrage.
|
||||||
|
- Master-Batterie: `client_registry_set_master_battery` (Monitor-Task).
|
||||||
|
|
||||||
|
### 7.8 LED_RING (8)
|
||||||
|
|
||||||
|
| mode | Bedeutung |
|
||||||
|
|------|-----------|
|
||||||
|
| 0 | Clear |
|
||||||
|
| 1 | Progress 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`.
|
||||||
|
|
||||||
|
### 7.11 ESPNOW_ECHO_PING (30)
|
||||||
|
|
||||||
|
Round-Trip-Latenztest zu einem **Slave** (`client_id` > 0, muss in der Registry sein).
|
||||||
|
|
||||||
|
**Ablauf:**
|
||||||
|
|
||||||
|
1. Host (goTool) setzt `timestamp_us` (Unix-µs) und sendet `EspNowEchoPingRequest` per UART.
|
||||||
|
2. Master (`cmd_espnow_echo_ping.c`) löst MAC aus Registry auf, ruft `esp_now_comm_echo_ping()` auf.
|
||||||
|
3. Master sendet `ESPNOW_ECHO_PING` mit `host_timestamp_us` und `master_time_us` (`esp_timer_get_time()` kurz vor Send).
|
||||||
|
4. Slave (`handle_echo_ping`) antwortet mit `ESPNOW_ECHO_PONG` (Felder unverändert).
|
||||||
|
5. Master empfängt Pong, berechnet `esp_rtt_us = esp_timer_get_time() - master_time_us`, antwortet per UART.
|
||||||
|
|
||||||
|
**Request:** `client_id`, `timestamp_us` (vom Host gesetzt).
|
||||||
|
|
||||||
|
**Response:** `success`, `client_id`, `timestamp_us` (echoed), `esp_rtt_us` (nur bei Erfolg).
|
||||||
|
|
||||||
|
| Feld | Quelle | Bedeutung |
|
||||||
|
|------|--------|-----------|
|
||||||
|
| `timestamp_us` | Host → Slave → Host | Korrelations-ID; muss mit Request übereinstimmen |
|
||||||
|
| `esp_rtt_us` | Master | Rohe µs-Differenz von `esp_timer_get_time()` (Ping-Send → Pong-Empfang im `recv_cb`) |
|
||||||
|
|
||||||
|
**Implementierung:** `main/cmd/cmd_espnow_echo_ping.c`, `esp_now_comm_echo_ping()` in `esp_now_master.c`, Slave `handle_echo_ping` in `esp_now_slave.c`.
|
||||||
|
|
||||||
|
**Host-seitig (goTool):** `rtt_ms` = volle UART-Kette (Send bis Response-Empfang); unabhängig von `esp_rtt_us`.
|
||||||
|
|
||||||
|
### 7.12 SET_LOG_LEVEL (31)
|
||||||
|
|
||||||
|
Laufzeit-Steuerung des **globalen** ESP-IDF-Log-Levels auf dem Master (`esp_log_level_set("*", …)`). Kein ESP-NOW, kein NVS — nur Master.
|
||||||
|
|
||||||
|
**Request:** `write`, `level` (`esp_log_level_t`: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE).
|
||||||
|
|
||||||
|
| `write` | Verhalten |
|
||||||
|
|---------|-----------|
|
||||||
|
| `false` / leer | Aktuelles Level lesen (`esp_log_level_get("*")`) |
|
||||||
|
| `true` | Level setzen; ungültige Werte (>5) → `success=false` |
|
||||||
|
|
||||||
|
**Response:** `success`, `level` (aktuell bzw. gesetzt).
|
||||||
|
|
||||||
|
**Boot-Default:** `CONFIG_LOG_DEFAULT_LEVEL` in `sdkconfig` (aktuell **INFO** = 3). Kann per UART/Web-UI zur Laufzeit geändert werden; nach Reboot gilt wieder der sdkconfig-Wert.
|
||||||
|
|
||||||
|
**Ausgabe:** Logs erscheinen auf **UART0** (Debug-USB), nicht auf dem Host-UART1 (goTool).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 log-level
|
||||||
|
go run . -port /dev/ttyUSB0 log-level -set -level 3
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. OTA
|
||||||
|
|
||||||
|
### 8.1 UART-OTA (nur Master)
|
||||||
|
|
||||||
|
Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`.
|
||||||
|
|
||||||
|
| Phase | Host → Master | Master → Host |
|
||||||
|
|-------|---------------|---------------|
|
||||||
|
| Start | `OTA_START` + `total_size` | `OTA_STATUS` preparing → ready |
|
||||||
|
| Daten | `OTA_PAYLOAD` ≤200 B, `seq` | `block_ack` je 4096 B Flash |
|
||||||
|
| Ende | `OTA_END` | success/failed; startet ESP-NOW-Verteilung |
|
||||||
|
|
||||||
|
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
|
||||||
|
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
|
||||||
|
|
||||||
|
### 8.2 ESP-NOW-OTA (Master → Slaves)
|
||||||
|
|
||||||
|
`ota_espnow.c` — gleiche Status-Codes wie UART.
|
||||||
|
|
||||||
|
1. `ESPNOW_OTA_START` + `total_size` → Slave `ota_espnow_slave_on_start`
|
||||||
|
2. `ESPNOW_OTA_PAYLOAD` bis 200 B → 4 KiB Buffer auf Slave
|
||||||
|
3. `ESPNOW_OTA_END` → Boot-Partition setzen
|
||||||
|
4. Slave antwortet `ESPNOW_OTA_STATUS` (mit `send_message_ex` + Semaphore auf Master)
|
||||||
|
|
||||||
|
Nach Erfolg: **alle Knoten neu starten**.
|
||||||
|
|
||||||
|
### 8.3 OTA_SLAVE_PROGRESS (21)
|
||||||
|
|
||||||
|
Abfrage laufender oder letzter ESP-NOW-Verteilung: pro Slave `bytes_written`, `status`, `error`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. BMA456 Beschleunigungssensor
|
||||||
|
|
||||||
|
| Thema | Detail |
|
||||||
|
|-------|--------|
|
||||||
|
| Wrapper | `bosch456.c` / `bosch456.h` |
|
||||||
|
| Chip-Variante | BMA456**H** (`bma456h.c` im Component) |
|
||||||
|
| I2C | Adresse 0x18, 100 kHz |
|
||||||
|
| Polling | Task ~10 Hz |
|
||||||
|
| Interrupt GPIO 10 | Single/Double/Triple Tap |
|
||||||
|
| Deadzone | Software-Filter für Logs/Stream (Default 100 LSB) |
|
||||||
|
| API | `bma456_read_accel`, `bma456_set_accel_deadzone`, `bma456_set_tap_handler` |
|
||||||
|
|
||||||
|
Ohne Sensor: `bma456_is_ready() == false`, Firmware läuft weiter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. LED-Ring
|
||||||
|
|
||||||
|
- **95 LEDs**, WS2812 über RMT (`led_ring.c`).
|
||||||
|
- Segment-Karten für Ziffern 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`).
|
||||||
|
- Modi: `0` clear … `5` color, `6` **battery-low** (erste 4 LEDs rot ~10 %, 5 s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Board-Input und Batterie
|
||||||
|
|
||||||
|
`board_input.c`:
|
||||||
|
|
||||||
|
- **Taster** GPIO 12 — Logging bei Druck.
|
||||||
|
- **LiPo-ADC** GPIO 1 und GPIO 11 (Pin-Spannung in mV).
|
||||||
|
- Master: `master_monitor_task` aktualisiert Master-Batterie alle 30 s.
|
||||||
|
- Slave: `slave_send_battery_report_to_master` nach Join, Heartbeat und Query.
|
||||||
|
|
||||||
|
Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART.
|
||||||
|
|
||||||
|
### Software-UV-Schutz (`battery_uv.c`)
|
||||||
|
|
||||||
|
Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override möglich).
|
||||||
|
|
||||||
|
| Zustand | ADC-Schwelle | Logik |
|
||||||
|
|---------|--------------|-------|
|
||||||
|
| UV | < 2300 mV | Ein gültiger Kanal reicht |
|
||||||
|
| Geladen | ≥ 3000 mV | Alle gültigen Kanäle |
|
||||||
|
|
||||||
|
- NVS-Key `uv_latch` in `pod_settings.c`
|
||||||
|
- Boot: ADC früh, bei UV nur LED + ADC-Poll (kein ESP-NOW/WiFi/UART/I2C/Button)
|
||||||
|
- Runtime: `battery_uv_poll()` im LiPo-Monitor (2 Samples Debounce)
|
||||||
|
- LED-Anzeige: Mode `6` battery-low (`led_ring_show_battery_low`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Persistenz (NVS)
|
||||||
|
|
||||||
|
`pod_settings.c` — Namespace `powerpod`:
|
||||||
|
|
||||||
|
| Key | Inhalt |
|
||||||
|
|-----|--------|
|
||||||
|
| `accel_dz` | Accel-Deadzone LSB |
|
||||||
|
| `uv_latch` | Software-UV-Zustand (`1` = Energiesparmodus) |
|
||||||
|
|
||||||
|
Gespeichert bei lokalem Set (UART `client_id=0`, ESP-NOW auf Slave). Geladen nach `init_bma456()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Protobuf und Code-Generierung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make proto_generate # beide Schemas
|
||||||
|
make proto_generate_uart # nur uart_messages.proto
|
||||||
|
make proto_generate_espnow # nur esp_now_messages.proto
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ausgabe:** `main/proto/*.pb.c`, `*.pb.h`
|
||||||
|
**Optionen:** `main/proto/uart_messages.options` (nanopb max_size etc.)
|
||||||
|
|
||||||
|
Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `main/proto/...`).
|
||||||
|
|
||||||
|
**Paketname protobuf:** `alox` → C-Prefix `alox_`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Modulreferenz
|
||||||
|
|
||||||
|
### 14.1 Kern
|
||||||
|
|
||||||
|
| Datei | Rolle |
|
||||||
|
|-------|--------|
|
||||||
|
| `powerpod.c` | `app_main`, Init, Master-Register |
|
||||||
|
| `app_config.h` | Laufzeit-Konfiguration |
|
||||||
|
| `cmd_handler.c` | Queue + Dispatcher |
|
||||||
|
| `uart.c` | Framing |
|
||||||
|
| `uart_proto.c` | UartMessage send |
|
||||||
|
| `uart_cmd.c` | Handler-Hilfen |
|
||||||
|
|
||||||
|
### 14.2 Kommunikation
|
||||||
|
|
||||||
|
| Datei | Rolle |
|
||||||
|
|-------|--------|
|
||||||
|
| `esp_now_comm.c` | Init, recv-Router |
|
||||||
|
| `esp_now_core.c` | WiFi, Peer, gemeinsamer Send |
|
||||||
|
| `esp_now_master.c` | Master-Tasks, Registry, Unicast send |
|
||||||
|
| `esp_now_slave.c` | Join, Heartbeat, Accel/Tap send |
|
||||||
|
| `esp_now_proto.c` | nanopb für EspNowMessage |
|
||||||
|
| `client_registry.c` | Slave-Tabelle + Telemetrie-Cache |
|
||||||
|
|
||||||
|
### 14.3 Befehle (`main/cmd/`)
|
||||||
|
|
||||||
|
| Datei | UART-Typ |
|
||||||
|
|-------|----------|
|
||||||
|
| `cmd_version.c` | VERSION |
|
||||||
|
| `cmd_client_info.c` | CLIENT_INFO |
|
||||||
|
| `cmd_accel_deadzone.c` | ACCEL_DEADZONE |
|
||||||
|
| `cmd_accel_stream.c` | ACCEL_STREAM |
|
||||||
|
| `cmd_tap_notify.c` | TAP_NOTIFY |
|
||||||
|
| `cmd_cache_status.c` | CACHE_STATUS |
|
||||||
|
| `cmd_battery.c` | BATTERY_STATUS |
|
||||||
|
| `cmd_led_ring.c` | LED_RING |
|
||||||
|
| `cmd_espnow_find_me.c` | FIND_ME |
|
||||||
|
| `cmd_restart.c` | RESTART |
|
||||||
|
| `cmd_espnow_unicast_test.c` | ESPNOW_UNICAST_TEST |
|
||||||
|
| `cmd_espnow_echo_ping.c` | ESPNOW_ECHO_PING |
|
||||||
|
| `cmd_set_log_level.c` | SET_LOG_LEVEL |
|
||||||
|
| `cmd_ota.c` | OTA_* |
|
||||||
|
| `cmd_ota_slave_progress.c` | OTA_SLAVE_PROGRESS |
|
||||||
|
|
||||||
|
### 14.4 Domäne
|
||||||
|
|
||||||
|
| Datei | Rolle |
|
||||||
|
|-------|--------|
|
||||||
|
| `bosch456.c` | Accelerometer + Tap |
|
||||||
|
| `led_ring.c` | LED-Anzeige |
|
||||||
|
| `board_input.c` | Taster, ADC |
|
||||||
|
| `battery_uv.c` | Software-UV-Schutz, Energiesparmodus |
|
||||||
|
| `pod_settings.c` | NVS |
|
||||||
|
| `pod_reboot.c` | Verzögerter Restart |
|
||||||
|
| `ota_uart.c` | Flash-Puffer UART-OTA |
|
||||||
|
| `ota_espnow.c` | OTA Master/Slave; Slave-Work-Queue |
|
||||||
|
| `ota_session.c` | OTA-Sperre für UART-Dispatcher |
|
||||||
|
|
||||||
|
### 14.5 Protobuf
|
||||||
|
|
||||||
|
| Datei | Rolle |
|
||||||
|
|-------|--------|
|
||||||
|
| `proto/uart_messages.proto` | UART-Vertrag |
|
||||||
|
| `proto/esp_now_messages.proto` | ESP-NOW-Vertrag |
|
||||||
|
| `proto/pb_encode.c`, `pb_decode.c`, `pb_common.c` | nanopb Runtime |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Logging-Tags
|
||||||
|
|
||||||
|
**Level zur Laufzeit:** UART `SET_LOG_LEVEL` (31) oder Dashboard „ESP Log-Level“ — steuert nur die Konsolen-Ausgabe auf UART0, nicht das Host-Protokoll.
|
||||||
|
|
||||||
|
| Tag | Modul |
|
||||||
|
|-----|--------|
|
||||||
|
| `[Main]` | powerpod.c |
|
||||||
|
| `[LOG_LVL]` | cmd_set_log_level.c |
|
||||||
|
| `[UART]` | uart.c |
|
||||||
|
| `[CMDH]` | cmd_handler.c |
|
||||||
|
| `[UART_CMD]` | uart_cmd.c |
|
||||||
|
| `[ESPNOW]` | esp_now_comm.c |
|
||||||
|
| `[CLIENTS]` | client_registry.c |
|
||||||
|
| `[BMA456]` | bosch456.c |
|
||||||
|
| `[VERSION]` etc. | jeweiliger cmd_* |
|
||||||
|
|
||||||
|
Typischer Ablauf bei UART-Befehl:
|
||||||
|
|
||||||
|
```
|
||||||
|
[UART] received message cmd=0x06 len=…
|
||||||
|
[CMDH] trigger command ACCEL_DEADZONE (0x06)
|
||||||
|
[ACCEL_DZ] …
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anhang: Neues UART-Kommando hinzufügen
|
||||||
|
|
||||||
|
1. `uart_messages.proto` erweitern → `make proto_generate_uart`
|
||||||
|
2. `cmd/cmd_neu.c` mit `handle_*` + `uart_cmd_register()`
|
||||||
|
3. In `powerpod.c` `cmd_neu_register()` aufrufen (nur Master-Zweig)
|
||||||
|
4. Bei Slave-Bedarf: `esp_now_messages.proto` + `esp_now_comm_send_*` + Slave-Zweig in `espnow_recv_cb`
|
||||||
|
|
||||||
|
Siehe [`adding-a-feature.md`](adding-a-feature.md) für ein vollständiges Beispiel (Find Me).
|
||||||
@@ -345,7 +345,7 @@ idf.py build
|
|||||||
|
|
||||||
Ähnliche Features zum Abgucken:
|
Ähnliche Features zum Abgucken:
|
||||||
|
|
||||||
- **Nur Master, kein ESP-NOW:** `main/cmd/cmd_version.c`, `main/cmd/cmd_led_ring.c`
|
- **Nur Master, kein ESP-NOW:** `main/cmd/cmd_version.c`, `main/cmd/cmd_led_ring.c`, `main/cmd/cmd_set_log_level.c`
|
||||||
- **Nur Slave per ESP-NOW (Master leitet nur durch):** `main/cmd/cmd_espnow_unicast_test.c`
|
- **Nur Slave per ESP-NOW (Master leitet nur durch):** `main/cmd/cmd_espnow_unicast_test.c`
|
||||||
- **Master + alle Slaves / Filter:** `main/cmd/cmd_accel_deadzone.c`
|
- **Master + alle Slaves / Filter:** `main/cmd/cmd_accel_deadzone.c`
|
||||||
- **Großer ESP-NOW-Fluss mit Status:** `ota_espnow.c`, `main/cmd/cmd_ota.c`
|
- **Großer ESP-NOW-Fluss mit Status:** `ota_espnow.c`, `main/cmd/cmd_ota.c`
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
win:
|
||||||
|
GOOS=windows GOARCH=386 go build .
|
||||||
+47
-11
@@ -24,14 +24,19 @@ go run . -port /dev/ttyUSB0 clients
|
|||||||
|---------|--------------|-------------|
|
|---------|--------------|-------------|
|
||||||
| `version` | `0x03` | Prints `version` and `git_hash` from firmware |
|
| `version` | `0x03` | Prints `version` and `git_hash` from firmware |
|
||||||
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
| `clients` | `0x04` | Lists slaves registered on the master via ESP-NOW |
|
||||||
|
| `deadzone` | `0x06` | Get/set accelerometer deadzone LSB (`-set`, `-value`, `-client`, `-all`) |
|
||||||
|
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
|
||||||
|
| `cache-status` | `0x1d` | Subscribed accel + tap cache (`CACHE_STATUS`); one UART round-trip for 16 ms polling |
|
||||||
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
| `unicast-test` | `0x07` | Sends ESP-NOW unicast test to one slave (`-client`, `-seq`) |
|
||||||
|
| `echo-ping` | `0x1e` | ESP-NOW echo round-trip to one slave (`-client`); prints `rtt_ms` (host UART chain) and `esp_rtt_us` (master ESP-NOW, raw µs) |
|
||||||
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
| `test` | — | Run an automated scenario (JSON configs under `testdata/`) |
|
||||||
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
|
||||||
| `ota` | 16–19 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
|
| `ota` | 16–19 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
|
||||||
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
|
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
|
||||||
| `led-ring` | 8 | LED ring: `-mode clear\|progress\|digit\|blink\|find-me`, … |
|
| `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me\|battery-low`, `-client`, `-all` |
|
||||||
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
|
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
|
||||||
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
|
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
|
||||||
|
| `log-level` | `0x1f` | Get/set master ESP-IDF log level for tag `"*"` (`-set`, `-level` 0–5); output on UART0 debug, not host UART |
|
||||||
|
|
||||||
`clients` requires slaves to have responded to master discover broadcasts first.
|
`clients` requires slaves to have responded to master discover broadcasts first.
|
||||||
|
|
||||||
@@ -60,28 +65,43 @@ Polls the master over UART and pushes state to the browser via WebSocket (Alpine
|
|||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 serve
|
go run . -port /dev/ttyUSB0 serve
|
||||||
go run . -port /dev/ttyUSB0 serve -addr :8080 -interval 2s
|
go run . -port /dev/ttyUSB0 serve -addr :8080 -interval 2s
|
||||||
|
go run . -port /dev/ttyUSB0 serve -api-addr :8081 -accel-interval 16ms
|
||||||
make gotool-serve PORT=/dev/ttyUSB0
|
make gotool-serve PORT=/dev/ttyUSB0
|
||||||
```
|
```
|
||||||
|
|
||||||
Open [http://localhost:8080](http://localhost:8080) — shows master firmware info and the ESP-NOW client table from `CLIENT_INFO`.
|
Open [http://localhost:8080](http://localhost:8080) — shows master firmware info and the ESP-NOW client table from `CLIENT_INFO`.
|
||||||
|
|
||||||
|
**Tap (dashboard):** two independent controls per slave:
|
||||||
|
|
||||||
|
| Column | Meaning |
|
||||||
|
|--------|---------|
|
||||||
|
| Tap-Notify (S/D/T) | Which tap kinds the **slave** sends to the master over ESP-NOW (UART `TAP_NOTIFY`) — does **not** poll UART |
|
||||||
|
| Tap (An/Aus) | Host **receive**: poll master tap cache (~16 ms) and show last tap for **≥2 s** |
|
||||||
|
|
||||||
|
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`).
|
||||||
|
|
||||||
If the UART device is unplugged or the port disappears, `serve` keeps running and retries on each poll interval; the UI shows **UART off** until the port is available again.
|
If the UART device is unplugged or the port disappears, `serve` keeps running and retries on each poll interval; the UI shows **UART off** until the port is available again.
|
||||||
|
|
||||||
The dashboard can configure nodes using the same UART commands as the CLI:
|
### HTTP / WebSocket API
|
||||||
|
|
||||||
| UI action | CLI equivalent |
|
`serve` also listens on **`:8081`** for external programs (`-api-addr`, empty to disable). Same UART as the dashboard.
|
||||||
|-----------|------------------|
|
|
||||||
| Nur Master | `deadzone -set -value N -client 0` |
|
|
||||||
| Einzelner Slave | `deadzone -set -value N -client ID` |
|
|
||||||
| Alle Slaves | per-slave ESP-NOW (Master bleibt unverändert; CLI `-all` setzt auch den Master) |
|
|
||||||
| Unicast test | `unicast-test -client ID` |
|
|
||||||
|
|
||||||
HTTP API (used by the web UI): `GET/POST /api/deadzone`, `POST /api/unicast-test`, `POST /api/find-me`, `POST /api/restart`, `POST /api/ota` (multipart field `firmware`, max 2 MiB).
|
| Doc | Content |
|
||||||
|
|-----|---------|
|
||||||
|
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands and **`input` push stream** (accel + tap) |
|
||||||
|
| **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) |
|
||||||
|
|
||||||
|
CLI:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
|
||||||
|
go run . -port /dev/ttyUSB0 cache-status
|
||||||
|
```
|
||||||
|
|
||||||
| UI / API | Behaviour |
|
| UI / API | Behaviour |
|
||||||
|----------|-----------|
|
|----------|-----------|
|
||||||
| Firmware OTA card | Same as `ota` CLI; WebSocket `ota_progress` with `step` `master` (UART) then `slaves` (ESP-NOW) |
|
| Firmware OTA card | Same as `ota` CLI; dashboard WebSocket `ota_progress` ([REST doc](docs/API_REST.md)) |
|
||||||
| `POST /api/ota` | Upload `.bin` to master only — slaves are updated by firmware over ESP-NOW after `OTA_END` |
|
| `POST /api/ota` | Upload `.bin` to master — slaves updated by firmware over ESP-NOW after `OTA_END` |
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
go run . -port /dev/ttyUSB0 ota build/powerpod.bin
|
||||||
@@ -95,8 +115,24 @@ go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
|
|||||||
|
|
||||||
On success the slave serial log should show `UNICAST TEST OK from master … seq=42`.
|
On success the slave serial log should show `UNICAST TEST OK from master … seq=42`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 echo-ping -client 16
|
||||||
|
go run . -port /dev/ttyUSB0 log-level
|
||||||
|
go run . -port /dev/ttyUSB0 log-level -set -level 3
|
||||||
|
```
|
||||||
|
|
||||||
|
`log-level` controls `esp_log_*` on the master (UART0 USB console). The host protocol UART (GPIO 2/3) is unchanged.
|
||||||
|
|
||||||
|
Measures latency to one slave. `rtt_ms` is the full host round-trip (UART + ESP-NOW + UART back). `esp_rtt_us` is the master-side ESP-NOW leg only (`esp_timer_get_time()` delta, raw microseconds from firmware).
|
||||||
|
|
||||||
Example output:
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234
|
||||||
|
```
|
||||||
|
|
||||||
|
`clients` example:
|
||||||
|
|
||||||
```
|
```
|
||||||
clients (2):
|
clients (2):
|
||||||
[0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250
|
[0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// accelStreamCtl tracks which slaves the host wants to poll for accel (mirrors firmware).
|
||||||
|
type accelStreamCtl struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
enabled map[uint32]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newAccelStreamCtl() *accelStreamCtl {
|
||||||
|
return &accelStreamCtl{enabled: make(map[uint32]struct{})}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *accelStreamCtl) Set(clientID uint32, on bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if on {
|
||||||
|
c.enabled[clientID] = struct{}{}
|
||||||
|
} else {
|
||||||
|
delete(c.enabled, clientID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *accelStreamCtl) Any() bool {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return len(c.enabled) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *accelStreamCtl) SyncFromClients(clients []ClientView) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.enabled = make(map[uint32]struct{})
|
||||||
|
for _, cl := range clients {
|
||||||
|
if cl.AccelStream {
|
||||||
|
c.enabled[cl.ID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type accelStreamAPIRequest struct {
|
||||||
|
Write bool `json:"write"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
AllClients bool `json:"all_clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type accelStreamAPIResponse struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
SlavesUpdated uint32 `json:"slaves_updated"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientAccelStreamBody struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountAccelStreamAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, ctl *accelStreamCtl) {
|
||||||
|
mux.HandleFunc("GET /api/clients/{clientID}/accel-stream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID, err := parsePathClientID(r)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveAccelStreamGet(w, clientID, link, hub, ctl)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("PUT /api/clients/{clientID}/accel-stream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID, err := parsePathClientID(r)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveClientAccelStreamPut(w, r, clientID, link, hub, ctl)
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/accel-stream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveAccelStreamGetQuery(w, r, link, hub, ctl)
|
||||||
|
case http.MethodPost:
|
||||||
|
serveAccelStreamPost(w, r, link, hub, ctl)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func parsePathClientID(r *http.Request) (uint32, error) {
|
||||||
|
s := r.PathValue("clientID")
|
||||||
|
if s == "" {
|
||||||
|
return 0, fmt.Errorf("client_id required")
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil || v == 0 {
|
||||||
|
return 0, fmt.Errorf("invalid client_id")
|
||||||
|
}
|
||||||
|
return uint32(v), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAccelStreamClient(link *managedSerial, hub *wsHub, ctl *accelStreamCtl, clientID uint32, enable bool) accelStreamAPIResponse {
|
||||||
|
resp, err := link.AccelStream(&pb.AccelStreamRequest{
|
||||||
|
Write: true,
|
||||||
|
Enable: enable,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return accelStreamAPIResponse{
|
||||||
|
ClientID: clientID,
|
||||||
|
Error: err.Error(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out := accelStreamAPIResponse{
|
||||||
|
Enabled: enable,
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
SlavesUpdated: resp.GetSlavesUpdated(),
|
||||||
|
}
|
||||||
|
if resp.GetSuccess() {
|
||||||
|
if ctl != nil {
|
||||||
|
ctl.Set(clientID, enable)
|
||||||
|
}
|
||||||
|
if hub != nil {
|
||||||
|
hub.patchClientAccelStream(clientID, enable)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Enabled = resp.GetEnabled()
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveAccelStreamGet(w http.ResponseWriter, clientID uint32, link *managedSerial, hub *wsHub, ctl *accelStreamCtl) {
|
||||||
|
resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, accelStreamAPIResponse{
|
||||||
|
ClientID: clientID,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctl != nil {
|
||||||
|
ctl.Set(clientID, resp.GetEnabled())
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, accelStreamAPIResponse{
|
||||||
|
Enabled: resp.GetEnabled(),
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveAccelStreamGetQuery(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub, ctl *accelStreamCtl) {
|
||||||
|
clientID, err := parseUintQuery(r, "client_id", 0)
|
||||||
|
if err != nil || clientID == 0 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveAccelStreamGet(w, clientID, link, hub, ctl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveClientAccelStreamPut(w http.ResponseWriter, r *http.Request, clientID uint32, link *managedSerial, hub *wsHub, ctl *accelStreamCtl) {
|
||||||
|
var body clientAccelStreamBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := applyAccelStreamClient(link, hub, ctl, clientID, body.Enable)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
} else if !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveAccelStreamPost(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub, ctl *accelStreamCtl) {
|
||||||
|
var body accelStreamAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.AllClients {
|
||||||
|
updated, err := applyAccelStreamAll(link, body.Enable)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, accelStreamAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clients, _ := link.listClientsPoll()
|
||||||
|
for _, c := range clients {
|
||||||
|
if ctl != nil {
|
||||||
|
ctl.Set(c.GetId(), body.Enable)
|
||||||
|
}
|
||||||
|
if hub != nil {
|
||||||
|
hub.patchClientAccelStream(c.GetId(), body.Enable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, accelStreamAPIResponse{
|
||||||
|
Enabled: body.Enable,
|
||||||
|
Success: updated > 0,
|
||||||
|
SlavesUpdated: updated,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.ClientID == 0 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, accelStreamAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyAccelStreamClient(link, hub, ctl, body.ClientID, body.Enable)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" || !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyAccelStreamAll(link *managedSerial, enable bool) (uint32, error) {
|
||||||
|
clients, err := link.listClients()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
var updated uint32
|
||||||
|
for _, c := range clients {
|
||||||
|
resp, err := link.AccelStream(&pb.AccelStreamRequest{
|
||||||
|
Write: true,
|
||||||
|
Enable: enable,
|
||||||
|
ClientId: c.GetId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if resp.GetSuccess() {
|
||||||
|
updated++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clients) == 0 {
|
||||||
|
return 0, fmt.Errorf("no slaves registered")
|
||||||
|
}
|
||||||
|
if updated == 0 {
|
||||||
|
return 0, fmt.Errorf("accel stream not applied to any slave")
|
||||||
|
}
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountBatteryAPI(mux *http.ServeMux, link *managedSerial) {
|
||||||
|
mux.HandleFunc("/api/battery", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveBatteryGet(w, r, link)
|
||||||
|
case http.MethodPost:
|
||||||
|
serveBatteryPost(w, r, link)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveBatteryGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
req := batteryAPIRequest{}
|
||||||
|
if v := r.URL.Query().Get("all_clients"); v == "1" || v == "true" {
|
||||||
|
req.AllClients = true
|
||||||
|
}
|
||||||
|
if s := r.URL.Query().Get("client_id"); s != "" {
|
||||||
|
id, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, batteryAPIResponse{Error: "invalid client_id"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.ClientID = uint32(id)
|
||||||
|
} else if !req.AllClients {
|
||||||
|
req.AllClients = true
|
||||||
|
}
|
||||||
|
out := applyBatteryStatus(link, req)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" || !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveBatteryPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
var body batteryAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, batteryAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !body.AllClients && body.ClientID == 0 {
|
||||||
|
body.AllClients = true
|
||||||
|
}
|
||||||
|
out := applyBatteryStatus(link, body)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" || !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
func mountLedRingAPI(mux *http.ServeMux, link *managedSerial) {
|
||||||
|
mux.HandleFunc("/api/led-ring", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveLedRingPost(w, r, link)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveLedRingPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
var body ledRingAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, ledRingAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Mode == "" {
|
||||||
|
writeJSON(w, http.StatusBadRequest, ledRingAPIResponse{Error: "mode required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := applyLedRing(link, body)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
} else if !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type liveStreamAPIResponse struct {
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountLiveStreamAPI(mux *http.ServeMux, hub *wsHub) {
|
||||||
|
mux.HandleFunc("/api/live-stream", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveLiveStreamGet(w, hub)
|
||||||
|
case http.MethodPut:
|
||||||
|
serveLiveStreamPut(w, r, hub)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveLiveStreamGet(w http.ResponseWriter, hub *wsHub) {
|
||||||
|
enabled := false
|
||||||
|
if hub != nil {
|
||||||
|
enabled = hub.liveStreamEnabled()
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, liveStreamAPIResponse{Enabled: enabled, Success: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveLiveStreamPut(w http.ResponseWriter, r *http.Request, hub *wsHub) {
|
||||||
|
var body struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, liveStreamAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if hub != nil {
|
||||||
|
hub.patchLiveStream(body.Enable)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, liveStreamAPIResponse{
|
||||||
|
Enabled: body.Enable,
|
||||||
|
Success: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
+114
-2
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -40,6 +41,19 @@ type unicastAPIResponse struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type echoPingAPIRequest struct {
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type echoPingAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ClientID uint32 `json:"client_id,omitempty"`
|
||||||
|
TimestampUs uint64 `json:"timestamp_us,omitempty"`
|
||||||
|
RttMs float64 `json:"rtt_ms"`
|
||||||
|
EspRttUs uint32 `json:"esp_rtt_us"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type findMeAPIRequest struct {
|
type findMeAPIRequest struct {
|
||||||
ClientID uint32 `json:"client_id"`
|
ClientID uint32 `json:"client_id"`
|
||||||
}
|
}
|
||||||
@@ -60,6 +74,17 @@ type restartAPIResponse struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type logLevelAPIResponse struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Level uint32 `json:"level"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type logLevelAPIRequest struct {
|
||||||
|
Write bool `json:"write"`
|
||||||
|
Level uint32 `json:"level"`
|
||||||
|
}
|
||||||
|
|
||||||
type otaAPIResponse struct {
|
type otaAPIResponse struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
BytesWritten uint32 `json:"bytes_written,omitempty"`
|
BytesWritten uint32 `json:"bytes_written,omitempty"`
|
||||||
@@ -67,7 +92,12 @@ type otaAPIResponse struct {
|
|||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) {
|
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) {
|
||||||
|
mountLiveStreamAPI(mux, hub)
|
||||||
|
mountAccelStreamAPI(mux, link, hub, streamCtl)
|
||||||
|
mountTapAPI(mux, link, hub, tapCtl)
|
||||||
|
mountLedRingAPI(mux, link)
|
||||||
|
mountBatteryAPI(mux, link)
|
||||||
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/deadzone", func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
@@ -85,6 +115,13 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) {
|
|||||||
}
|
}
|
||||||
serveUnicastTest(w, r, link)
|
serveUnicastTest(w, r, link)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/api/echo-ping", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveEchoPing(w, r, link)
|
||||||
|
})
|
||||||
mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/find-me", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -99,6 +136,16 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub) {
|
|||||||
}
|
}
|
||||||
serveRestart(w, r, link)
|
serveRestart(w, r, link)
|
||||||
})
|
})
|
||||||
|
mux.HandleFunc("/api/log-level", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveLogLevelGet(w, r, link)
|
||||||
|
case http.MethodPost:
|
||||||
|
serveLogLevelPost(w, r, link)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/api/ota", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
@@ -141,7 +188,11 @@ func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial,
|
|||||||
if hub != nil {
|
if hub != nil {
|
||||||
hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()})
|
hub.broadcastRaw(OTAProgress{Type: "ota_progress", Phase: "error", Message: err.Error()})
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusServiceUnavailable, otaAPIResponse{Error: err.Error()})
|
status := http.StatusServiceUnavailable
|
||||||
|
if errors.Is(err, errOTAInProgress) {
|
||||||
|
status = http.StatusConflict
|
||||||
|
}
|
||||||
|
writeJSON(w, status, otaAPIResponse{Error: err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, otaAPIResponse{
|
writeJSON(w, http.StatusOK, otaAPIResponse{
|
||||||
@@ -250,6 +301,43 @@ func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error)
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveLogLevelGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
resp, err := link.SetLogLevel(false, 0)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, logLevelAPIResponse{
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
Level: resp.GetLevel(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveLogLevelPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
var body logLevelAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !body.Write {
|
||||||
|
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "write must be true"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Level > 5 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "level must be 0–5"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := link.SetLogLevel(true, body.Level)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, logLevelAPIResponse{
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
Level: resp.GetLevel(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
var body restartAPIRequest
|
var body restartAPIRequest
|
||||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
@@ -306,6 +394,30 @@ func serveUnicastTest(w http.ResponseWriter, r *http.Request, link *managedSeria
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func serveEchoPing(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
var body echoPingAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, echoPingAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.ClientID == 0 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, echoPingAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := link.EchoPing(body.ClientID)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, echoPingAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, echoPingAPIResponse{
|
||||||
|
Success: result.Success,
|
||||||
|
ClientID: result.ClientID,
|
||||||
|
TimestampUs: result.TimestampUs,
|
||||||
|
RttMs: result.RttMs,
|
||||||
|
EspRttUs: result.EspRttUs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func parseUintQuery(r *http.Request, key string, def uint32) (uint32, error) {
|
func parseUintQuery(r *http.Request, key string, def uint32) (uint32, error) {
|
||||||
s := r.URL.Query().Get(key)
|
s := r.URL.Query().Get(key)
|
||||||
if s == "" {
|
if s == "" {
|
||||||
|
|||||||
@@ -0,0 +1,875 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
AccelAgeMs uint32 `json:"accel_age_ms,omitempty"`
|
||||||
|
TapKind string `json:"tap_kind,omitempty"`
|
||||||
|
TapAgeMs uint32 `json:"tap_age_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 []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"
|
||||||
|
ReceiveInput bool `json:"receive_input"`
|
||||||
|
IntervalMs int `json:"interval_ms"`
|
||||||
|
PreFetch int `json:"pre_fetch"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
SlavesUpdated uint32 `json:"slaves_updated,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// APIClientInfo is one registered slave (or slot) from CLIENT_INFO.
|
||||||
|
type APIClientInfo struct {
|
||||||
|
ID uint32 `json:"id"`
|
||||||
|
MAC string `json:"mac"`
|
||||||
|
Version uint32 `json:"version"`
|
||||||
|
Available bool `json:"available"`
|
||||||
|
Used bool `json:"used"`
|
||||||
|
LastPing uint32 `json:"last_ping"`
|
||||||
|
LastSuccessPing uint32 `json:"last_success_ping"`
|
||||||
|
InputStream bool `json:"input_stream"`
|
||||||
|
TapNotifySingle bool `json:"tap_notify_single"`
|
||||||
|
TapNotifyDouble bool `json:"tap_notify_double"`
|
||||||
|
TapNotifyTriple bool `json:"tap_notify_triple"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientListMessage is the reply to list_clients.
|
||||||
|
type ClientListMessage struct {
|
||||||
|
Type string `json:"type"` // "client_list"
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Clients []APIClientInfo `json:"clients,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// TapNotifyStatusMessage is the reply to set_tap_notify / get_tap_notify (slave).
|
||||||
|
type TapNotifyStatusMessage struct {
|
||||||
|
Type string `json:"type"` // "tap_notify_status"
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
Single bool `json:"single"`
|
||||||
|
DoubleTap bool `json:"double_tap"`
|
||||||
|
Triple bool `json:"triple"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
SlavesUpdated uint32 `json:"slaves_updated,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type accelWSCommand struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
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"`
|
||||||
|
AllClients bool `json:"all_clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIInfoResponse struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
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"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type cachedTapEvent struct {
|
||||||
|
kind string
|
||||||
|
shownAt time.Time
|
||||||
|
ageMs uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type wsSubscriber struct {
|
||||||
|
conn *websocket.Conn
|
||||||
|
receiveInput bool
|
||||||
|
interval time.Duration
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) notifyConfigChanged() {
|
||||||
|
select {
|
||||||
|
case h.configChanged <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func clampAPIInterval(d time.Duration) time.Duration {
|
||||||
|
if d < minAPIStreamInterval {
|
||||||
|
return minAPIStreamInterval
|
||||||
|
}
|
||||||
|
if d > maxAPIStreamInterval {
|
||||||
|
return maxAPIStreamInterval
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
receiveInput: false,
|
||||||
|
interval: h.defaultInterval,
|
||||||
|
preFetch: h.defaultPreFetch,
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
h.clients[conn] = sub
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
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_stream enables input polling/push on this connection",
|
||||||
|
Commands: []string{
|
||||||
|
"list_clients",
|
||||||
|
"set_stream", "get_stream",
|
||||||
|
"set_input_stream", "get_input_stream",
|
||||||
|
"set_tap_notify", "get_tap_notify",
|
||||||
|
"set_led_ring", "get_battery",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if data, err := json.Marshal(hello); err == nil {
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
return sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) unregister(conn *websocket.Conn) {
|
||||||
|
h.mu.Lock()
|
||||||
|
delete(h.clients, conn)
|
||||||
|
anyInput := false
|
||||||
|
for _, sub := range h.clients {
|
||||||
|
if sub.receiveInput {
|
||||||
|
anyInput = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !anyInput {
|
||||||
|
h.recentTaps = nil
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
h.notifyConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) anyWantsInput() bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
for _, sub := range h.clients {
|
||||||
|
if sub.receiveInput {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) minWantedInterval() time.Duration {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
var min time.Duration
|
||||||
|
for _, sub := range h.clients {
|
||||||
|
if !sub.receiveInput {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if min == 0 || sub.interval < min {
|
||||||
|
min = sub.interval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if min == 0 {
|
||||||
|
return h.defaultInterval
|
||||||
|
}
|
||||||
|
return min
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) setStream(sub *wsSubscriber, enable bool, intervalMs, preFetchMs *int) StreamStatusMessage {
|
||||||
|
h.mu.Lock()
|
||||||
|
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",
|
||||||
|
ReceiveInput: enable,
|
||||||
|
IntervalMs: ms,
|
||||||
|
PreFetch: pf,
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) getStream(sub *wsSubscriber) StreamStatusMessage {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return StreamStatusMessage{
|
||||||
|
Type: "stream_status",
|
||||||
|
ReceiveInput: sub.receiveInput,
|
||||||
|
IntervalMs: int(sub.interval / time.Millisecond),
|
||||||
|
PreFetch: int(sub.preFetch / time.Millisecond),
|
||||||
|
Success: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) streamTiming(now time.Time) (needRead, needDeliver bool, waitPreFetch time.Duration) {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
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) ingestTapFromCache(cache *pb.CacheStatusResponse) {
|
||||||
|
if cache == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
if h.recentTaps == nil {
|
||||||
|
h.recentTaps = make(map[uint32]cachedTapEvent)
|
||||||
|
}
|
||||||
|
for _, c := range cache.GetClients() {
|
||||||
|
t := c.GetTap()
|
||||||
|
if t == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kind := tapKindLabelPB(t.GetKind())
|
||||||
|
if kind == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.recentTaps[c.GetClientId()] = cachedTapEvent{
|
||||||
|
kind: kind,
|
||||||
|
shownAt: now,
|
||||||
|
ageMs: t.GetAgeMs(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.pruneRecentTapsLocked(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *accelStreamHub) pruneRecentTapsLocked(now time.Time) {
|
||||||
|
if len(h.recentTaps) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cutoff := now.Add(-apiTapDisplayMinMs * time.Millisecond)
|
||||||
|
for id, ev := range h.recentTaps {
|
||||||
|
if ev.shownAt.Before(cutoff) {
|
||||||
|
delete(h.recentTaps, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
}
|
||||||
|
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) deliverInput(msg InputStreamMessage) {
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
h.mu.Lock()
|
||||||
|
defer h.mu.Unlock()
|
||||||
|
for conn, sub := range h.clients {
|
||||||
|
if !sub.receiveInput {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !sub.lastInputSent.IsZero() && now.Sub(sub.lastInputSent) < sub.interval {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sub.lastInputSent = now
|
||||||
|
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||||
|
delete(h.clients, conn)
|
||||||
|
_ = conn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
|
||||||
|
ticker := time.NewTicker(minAPIStreamInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
var pending *pendingInputCache
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-hub.configChanged:
|
||||||
|
pending = nil
|
||||||
|
case now := <-ticker.C:
|
||||||
|
if !hub.anyWantsInput() || !inputPollingActive(dash, ctl, tapCtl) {
|
||||||
|
pending = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
needRead, needDeliver, waitPreFetch := hub.streamTiming(now)
|
||||||
|
|
||||||
|
if needRead && pending == nil {
|
||||||
|
cache, err := link.readCacheStatusPoll()
|
||||||
|
if err != nil {
|
||||||
|
pending = &pendingInputCache{readErr: err, readAt: now}
|
||||||
|
} else {
|
||||||
|
hub.ingestTapFromCache(cache)
|
||||||
|
pending = &pendingInputCache{cache: cache, readAt: now}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !needDeliver || pending == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ts := now.UnixNano()
|
||||||
|
if pending.readErr != nil {
|
||||||
|
errMsg := pending.readErr.Error()
|
||||||
|
if errors.Is(pending.readErr, errUARTBusy) {
|
||||||
|
errMsg = "uart busy"
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
pending = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeStreamStatus(conn *websocket.Conn, msg StreamStatusMessage) {
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBatteryStatus(conn *websocket.Conn, out batteryAPIResponse) {
|
||||||
|
out.Type = "battery_status"
|
||||||
|
data, err := json.Marshal(out)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeLedRingStatus(conn *websocket.Conn, out ledRingAPIResponse) {
|
||||||
|
out.Type = "led_ring_status"
|
||||||
|
data, err := json.Marshal(out)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeInputStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) {
|
||||||
|
msg := InputStreamStatusMessage{
|
||||||
|
Type: "input_stream_status",
|
||||||
|
ClientID: out.ClientID,
|
||||||
|
Enabled: out.Enabled,
|
||||||
|
Success: out.Success,
|
||||||
|
SlavesUpdated: out.SlavesUpdated,
|
||||||
|
Error: out.Error,
|
||||||
|
}
|
||||||
|
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(),
|
||||||
|
MAC: formatMAC(c.GetMac()),
|
||||||
|
Version: c.GetVersion(),
|
||||||
|
Available: c.GetAvailable(),
|
||||||
|
Used: c.GetUsed(),
|
||||||
|
LastPing: c.GetLastPing(),
|
||||||
|
LastSuccessPing: c.GetLastSuccessPing(),
|
||||||
|
InputStream: c.GetAccelStreamEnabled(),
|
||||||
|
TapNotifySingle: c.GetTapNotifySingle(),
|
||||||
|
TapNotifyDouble: c.GetTapNotifyDouble(),
|
||||||
|
TapNotifyTriple: c.GetTapNotifyTriple(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeClientList(conn *websocket.Conn, msg ClientListMessage) {
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeTapNotifyStatus(conn *websocket.Conn, out tapNotifyAPIResponse) {
|
||||||
|
msg := TapNotifyStatusMessage{
|
||||||
|
Type: "tap_notify_status",
|
||||||
|
ClientID: out.ClientID,
|
||||||
|
Single: out.Single,
|
||||||
|
DoubleTap: out.DoubleTap,
|
||||||
|
Triple: out.Triple,
|
||||||
|
Success: out.Success,
|
||||||
|
SlavesUpdated: out.SlavesUpdated,
|
||||||
|
Error: out.Error,
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteMessage(websocket.TextMessage, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTapNotifyClientWS(link *managedSerial, dash *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
|
||||||
|
resp, err := link.TapNotify(&pb.TapNotifyRequest{
|
||||||
|
Write: true,
|
||||||
|
ClientId: clientID,
|
||||||
|
Single: single,
|
||||||
|
DoubleTap: doubleTap,
|
||||||
|
Triple: triple,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return tapNotifyAPIResponse{ClientID: clientID, Error: err.Error()}
|
||||||
|
}
|
||||||
|
out := tapNotifyAPIResponse{
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
SlavesUpdated: resp.GetSlavesUpdated(),
|
||||||
|
Single: resp.GetSingle(),
|
||||||
|
DoubleTap: resp.GetDoubleTap(),
|
||||||
|
Triple: resp.GetTriple(),
|
||||||
|
}
|
||||||
|
if resp.GetSuccess() {
|
||||||
|
if tapCtl != nil {
|
||||||
|
tapCtl.Set(clientID, single, doubleTap, triple)
|
||||||
|
}
|
||||||
|
if dash != nil {
|
||||||
|
dash.patchClientTapNotify(clientID, single, doubleTap, triple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) {
|
||||||
|
var cmd accelWSCommand
|
||||||
|
if err := json.Unmarshal(data, &cmd); err != nil {
|
||||||
|
writeStreamStatus(conn, StreamStatusMessage{Type: "stream_status", Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cmd.Type {
|
||||||
|
case "list_clients":
|
||||||
|
clients, err := link.listClientsPoll()
|
||||||
|
if err != nil {
|
||||||
|
writeClientList(conn, ClientListMessage{
|
||||||
|
Type: "client_list",
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]APIClientInfo, 0, len(clients))
|
||||||
|
for _, c := range clients {
|
||||||
|
out = append(out, clientInfoToAPI(c))
|
||||||
|
}
|
||||||
|
writeClientList(conn, ClientListMessage{
|
||||||
|
Type: "client_list",
|
||||||
|
Success: true,
|
||||||
|
Clients: out,
|
||||||
|
})
|
||||||
|
|
||||||
|
case "set_stream":
|
||||||
|
if cmd.Enable == nil {
|
||||||
|
writeStreamStatus(conn, StreamStatusMessage{
|
||||||
|
Type: "stream_status",
|
||||||
|
Error: "enable required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs, cmd.PreFetch))
|
||||||
|
|
||||||
|
case "get_stream":
|
||||||
|
writeStreamStatus(conn, hub.getStream(sub))
|
||||||
|
|
||||||
|
case "set_input_stream":
|
||||||
|
if cmd.ClientID == 0 {
|
||||||
|
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.Enable == nil {
|
||||||
|
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||||
|
ClientID: cmd.ClientID,
|
||||||
|
Error: "enable required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeInputStreamStatus(conn, applyAccelStreamClient(link, dash, ctl, cmd.ClientID, *cmd.Enable))
|
||||||
|
|
||||||
|
case "get_input_stream":
|
||||||
|
if cmd.ClientID == 0 {
|
||||||
|
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: cmd.ClientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||||
|
ClientID: cmd.ClientID,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctl != nil {
|
||||||
|
ctl.Set(cmd.ClientID, resp.GetEnabled())
|
||||||
|
}
|
||||||
|
writeInputStreamStatus(conn, accelStreamAPIResponse{
|
||||||
|
Enabled: resp.GetEnabled(),
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
})
|
||||||
|
|
||||||
|
case "set_tap_notify":
|
||||||
|
if cmd.AllClients {
|
||||||
|
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "single, double_tap, triple required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updated, err := applyTapNotifyAll(link, dash, tapCtl, *cmd.Single, *cmd.DoubleTap, *cmd.Triple)
|
||||||
|
if err != nil {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||||
|
Success: updated > 0,
|
||||||
|
SlavesUpdated: updated,
|
||||||
|
Single: *cmd.Single,
|
||||||
|
DoubleTap: *cmd.DoubleTap,
|
||||||
|
Triple: *cmd.Triple,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.ClientID == 0 {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||||
|
ClientID: cmd.ClientID,
|
||||||
|
Error: "single, double_tap, triple required",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeTapNotifyStatus(conn, applyTapNotifyClientWS(link, dash, tapCtl, cmd.ClientID, *cmd.Single, *cmd.DoubleTap, *cmd.Triple))
|
||||||
|
|
||||||
|
case "get_tap_notify":
|
||||||
|
if cmd.ClientID == 0 {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: cmd.ClientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||||
|
ClientID: cmd.ClientID,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tapCtl != nil {
|
||||||
|
tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple())
|
||||||
|
}
|
||||||
|
writeTapNotifyStatus(conn, tapNotifyAPIResponse{
|
||||||
|
ClientID: cmd.ClientID,
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
Single: resp.GetSingle(),
|
||||||
|
DoubleTap: resp.GetDoubleTap(),
|
||||||
|
Triple: resp.GetTriple(),
|
||||||
|
})
|
||||||
|
|
||||||
|
case "set_led_ring":
|
||||||
|
var body ledRingAPIRequest
|
||||||
|
if err := json.Unmarshal(data, &body); err != nil {
|
||||||
|
writeLedRingStatus(conn, ledRingAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if body.Mode == "" {
|
||||||
|
writeLedRingStatus(conn, ledRingAPIResponse{Error: "mode required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeLedRingStatus(conn, applyLedRing(link, body))
|
||||||
|
|
||||||
|
case "get_battery":
|
||||||
|
var body batteryAPIRequest
|
||||||
|
if err := json.Unmarshal(data, &body); err != nil {
|
||||||
|
writeBatteryStatus(conn, batteryAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !body.AllClients && body.ClientID == 0 {
|
||||||
|
body.AllClients = true
|
||||||
|
}
|
||||||
|
writeBatteryStatus(conn, applyBatteryStatus(link, body))
|
||||||
|
|
||||||
|
default:
|
||||||
|
writeStreamStatus(conn, StreamStatusMessage{
|
||||||
|
Type: "stream_status",
|
||||||
|
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)",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, portName string, hub *accelStreamHub) {
|
||||||
|
sub := hub.register(conn, portName)
|
||||||
|
defer hub.unregister(conn)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
for {
|
||||||
|
_, data, err := conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
handleAccelWSCommand(conn, sub, data, link, dash, ctl, tapCtl, hub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) {
|
||||||
|
defMs := int(defaultInterval / time.Millisecond)
|
||||||
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/" && r.URL.Path != "/api/v1" && r.URL.Path != "/api/v1/" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, APIInfoResponse{
|
||||||
|
Name: "powerpod-external-api",
|
||||||
|
Version: "1",
|
||||||
|
SerialPort: portName,
|
||||||
|
WebSocket: "/ws",
|
||||||
|
DefaultIntervalMs: defMs,
|
||||||
|
DefaultPreFetchMs: defaultPreFetchMs,
|
||||||
|
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
|
||||||
|
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
|
||||||
|
TapDisplayMinMs: apiTapDisplayMinMs,
|
||||||
|
Description: "WebSocket: set_input_stream + set_stream for input (accel + tap); set_tap_notify configures slave tap kinds",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("api websocket upgrade: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveExternalWS(conn, link, dash, ctl, tapCtl, portName, hub)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 runInputStreamer(link, hub, dash, ctl, tapCtl, stop)
|
||||||
|
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mountExternalAPI(mux, portName, defaultInterval, hub, link, dash, ctl, tapCtl)
|
||||||
|
mountLedRingAPI(mux, link)
|
||||||
|
mountBatteryAPI(mux, link)
|
||||||
|
|
||||||
|
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)",
|
||||||
|
addr, addr, defaultInterval.String())
|
||||||
|
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
log.Printf("external API server: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return srv
|
||||||
|
}
|
||||||
|
|
||||||
|
func shutdownAPIServer(srv *http.Server) {
|
||||||
|
if srv == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
type tapNotifyAPIRequest struct {
|
||||||
|
Write bool `json:"write"`
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
AllClients bool `json:"all_clients"`
|
||||||
|
Single bool `json:"single"`
|
||||||
|
DoubleTap bool `json:"double_tap"`
|
||||||
|
Triple bool `json:"triple"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tapNotifyAPIResponse struct {
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
Success bool `json:"success"`
|
||||||
|
SlavesUpdated uint32 `json:"slaves_updated"`
|
||||||
|
Single bool `json:"single"`
|
||||||
|
DoubleTap bool `json:"double_tap"`
|
||||||
|
Triple bool `json:"triple"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tapSnapshotAPIResponse struct {
|
||||||
|
Events []tapEventView `json:"events"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type tapEventView struct {
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
AgeMs uint32 `json:"age_ms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func mountTapAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||||
|
mux.HandleFunc("GET /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID, err := parsePathClientID(r)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveTapNotifyGet(w, clientID, link)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("PUT /api/clients/{clientID}/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
clientID, err := parsePathClientID(r)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveClientTapNotifyPut(w, r, clientID, link, hub, tapCtl)
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/api/tap-notify", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
serveTapNotifyGetQuery(w, r, link)
|
||||||
|
case http.MethodPost:
|
||||||
|
serveTapNotifyPost(w, r, link, hub, tapCtl)
|
||||||
|
default:
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
mux.HandleFunc("/api/tap-snapshot", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveTapSnapshotGet(w, r, link)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTapNotifyClient(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
|
||||||
|
return applyTapNotifyClientWS(link, hub, tapCtl, clientID, single, doubleTap, triple)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveTapNotifyGet(w http.ResponseWriter, clientID uint32, link *managedSerial) {
|
||||||
|
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{
|
||||||
|
ClientID: clientID,
|
||||||
|
Error: err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
Single: resp.GetSingle(),
|
||||||
|
DoubleTap: resp.GetDoubleTap(),
|
||||||
|
Triple: resp.GetTriple(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveTapNotifyGetQuery(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
clientID, err := parseUintQuery(r, "client_id", 0)
|
||||||
|
if err != nil || clientID == 0 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
serveTapNotifyGet(w, clientID, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientTapNotifyBody struct {
|
||||||
|
Single bool `json:"single"`
|
||||||
|
DoubleTap bool `json:"double_tap"`
|
||||||
|
Triple bool `json:"triple"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveClientTapNotifyPut(w http.ResponseWriter, r *http.Request, clientID uint32, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||||
|
var body clientTapNotifyBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := applyTapNotifyClient(link, hub, tapCtl, clientID, body.Single, body.DoubleTap, body.Triple)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" || !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveTapNotifyPost(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl) {
|
||||||
|
var body tapNotifyAPIRequest
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "invalid JSON"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.AllClients {
|
||||||
|
updated, err := applyTapNotifyAll(link, hub, tapCtl, body.Single, body.DoubleTap, body.Triple)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, tapNotifyAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, tapNotifyAPIResponse{
|
||||||
|
Success: updated > 0,
|
||||||
|
SlavesUpdated: updated,
|
||||||
|
Single: body.Single,
|
||||||
|
DoubleTap: body.DoubleTap,
|
||||||
|
Triple: body.Triple,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.ClientID == 0 {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapNotifyAPIResponse{Error: "client_id required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out := applyTapNotifyClient(link, hub, tapCtl, body.ClientID, body.Single, body.DoubleTap, body.Triple)
|
||||||
|
status := http.StatusOK
|
||||||
|
if out.Error != "" || !out.Success {
|
||||||
|
status = http.StatusServiceUnavailable
|
||||||
|
}
|
||||||
|
writeJSON(w, status, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTapNotifyAll(link *managedSerial, hub *wsHub, tapCtl *tapNotifyCtl, single, doubleTap, triple bool) (uint32, error) {
|
||||||
|
resp, err := link.TapNotify(&pb.TapNotifyRequest{
|
||||||
|
Write: true,
|
||||||
|
AllClients: true,
|
||||||
|
Single: single,
|
||||||
|
DoubleTap: doubleTap,
|
||||||
|
Triple: triple,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if !resp.GetSuccess() {
|
||||||
|
return 0, fmt.Errorf("tap notify not applied to any slave")
|
||||||
|
}
|
||||||
|
if hub != nil || tapCtl != nil {
|
||||||
|
clients, _ := link.listClientsPoll()
|
||||||
|
for _, c := range clients {
|
||||||
|
if tapCtl != nil {
|
||||||
|
tapCtl.Set(c.GetId(), single, doubleTap, triple)
|
||||||
|
}
|
||||||
|
if hub != nil {
|
||||||
|
hub.patchClientTapNotify(c.GetId(), single, doubleTap, triple)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resp.GetSlavesUpdated(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveTapSnapshotGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
|
||||||
|
clientID, err := parseUintQuery(r, "client_id", 0)
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusBadRequest, tapSnapshotAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cache, err := link.readCacheStatusPoll()
|
||||||
|
if err != nil {
|
||||||
|
writeJSON(w, http.StatusServiceUnavailable, tapSnapshotAPIResponse{Error: err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := tapSnapshotAPIResponse{Events: make([]tapEventView, 0)}
|
||||||
|
for _, e := range tapEventsFromCacheStatus(cache) {
|
||||||
|
if clientID != 0 && e.GetClientId() != clientID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out.Events = append(out.Events, tapEventView{
|
||||||
|
ClientID: e.GetClientId(),
|
||||||
|
Kind: tapKindLabel(e.GetKind()),
|
||||||
|
AgeMs: e.GetAgeMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, out)
|
||||||
|
}
|
||||||
@@ -347,6 +347,10 @@ func ledRingModeValue(mode string) (uint32, error) {
|
|||||||
return 3, nil
|
return 3, nil
|
||||||
case "find_me", "findme":
|
case "find_me", "findme":
|
||||||
return 4, nil
|
return 4, nil
|
||||||
|
case "battery_low", "batterylow":
|
||||||
|
return 6, nil
|
||||||
|
case "color", "solid", "fill":
|
||||||
|
return 5, nil
|
||||||
default:
|
default:
|
||||||
return 0, fmt.Errorf("unknown led_ring mode %q", mode)
|
return 0, fmt.Errorf("unknown led_ring mode %q", mode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ func TestLedRingModeValue(t *testing.T) {
|
|||||||
{"digit", 2},
|
{"digit", 2},
|
||||||
{"blink", 3},
|
{"blink", 3},
|
||||||
{"find-me", 4},
|
{"find-me", 4},
|
||||||
|
{"battery-low", 6},
|
||||||
}
|
}
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
got, err := ledRingModeValue(tc.mode)
|
got, err := ledRingModeValue(tc.mode)
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
lipoMinMv = 3000
|
||||||
|
lipoMaxMv = 4200
|
||||||
|
)
|
||||||
|
|
||||||
|
type lipoReadingJSON struct {
|
||||||
|
Valid bool `json:"valid"`
|
||||||
|
VoltageMv uint32 `json:"voltage_mv"`
|
||||||
|
Percent int `json:"percent,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type batterySampleJSON struct {
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||||
|
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||||
|
AgeMs uint32 `json:"age_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryAPIRequest struct {
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
AllClients bool `json:"all_clients"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type batteryAPIResponse struct {
|
||||||
|
Type string `json:"type,omitempty"` // battery_status (WebSocket)
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Samples []batterySampleJSON `json:"samples,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func lipoPercent(mv uint32) int {
|
||||||
|
if mv <= lipoMinMv {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if mv >= lipoMaxMv {
|
||||||
|
return 100
|
||||||
|
}
|
||||||
|
return int((mv - lipoMinMv) * 100 / (lipoMaxMv - lipoMinMv))
|
||||||
|
}
|
||||||
|
|
||||||
|
func lipoFromPBMsg(l *pb.LipoReading) lipoReadingJSON {
|
||||||
|
if l == nil {
|
||||||
|
return lipoReadingJSON{}
|
||||||
|
}
|
||||||
|
return lipoFromPB(l.GetValid(), l.GetVoltageMv())
|
||||||
|
}
|
||||||
|
|
||||||
|
func lipoFromPB(valid bool, mv uint32) lipoReadingJSON {
|
||||||
|
out := lipoReadingJSON{Valid: valid, VoltageMv: mv}
|
||||||
|
if valid {
|
||||||
|
out.Percent = lipoPercent(mv)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func batterySamplesFromPB(samples []*pb.BatterySample) []batterySampleJSON {
|
||||||
|
out := make([]batterySampleJSON, 0, len(samples))
|
||||||
|
for _, s := range samples {
|
||||||
|
out = append(out, batterySampleJSON{
|
||||||
|
ClientID: s.GetClientId(),
|
||||||
|
Lipo1: lipoFromPBMsg(s.GetLipo1()),
|
||||||
|
Lipo2: lipoFromPBMsg(s.GetLipo2()),
|
||||||
|
AgeMs: s.GetAgeMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyBatteryStatus(link *managedSerial, in batteryAPIRequest) batteryAPIResponse {
|
||||||
|
resp, err := link.BatteryStatus(&pb.BatteryStatusRequest{
|
||||||
|
ClientId: in.ClientID,
|
||||||
|
AllClients: in.AllClients,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return batteryAPIResponse{Error: err.Error()}
|
||||||
|
}
|
||||||
|
samples := batterySamplesFromPB(resp.GetSamples())
|
||||||
|
out := batteryAPIResponse{
|
||||||
|
Success: resp.GetSuccess() || len(samples) > 0,
|
||||||
|
Samples: samples,
|
||||||
|
}
|
||||||
|
if len(samples) == 0 && out.Error == "" {
|
||||||
|
out.Error = "battery status unavailable"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func findBatterySample(samples []batterySampleJSON, clientID uint32) (batterySampleJSON, bool) {
|
||||||
|
for _, s := range samples {
|
||||||
|
if s.ClientID == clientID {
|
||||||
|
return s, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return batterySampleJSON{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// applyBatterySamplesToState merges UART/REST battery samples into dashboard views.
|
||||||
|
func applyBatterySamplesToState(st *DashboardState, samples []batterySampleJSON) {
|
||||||
|
if st == nil || len(samples) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m, ok := findBatterySample(samples, 0); ok {
|
||||||
|
st.Master.Lipo1 = m.Lipo1
|
||||||
|
st.Master.Lipo2 = m.Lipo2
|
||||||
|
st.Master.BatteryAgeMs = m.AgeMs
|
||||||
|
}
|
||||||
|
for i := range st.Clients {
|
||||||
|
if s, ok := findBatterySample(samples, st.Clients[i].ID); ok {
|
||||||
|
st.Clients[i].Lipo1 = s.Lipo1
|
||||||
|
st.Clients[i].Lipo2 = s.Lipo2
|
||||||
|
st.Clients[i].BatteryAgeMs = s.AgeMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "powerpod/gotool/pb"
|
||||||
|
|
||||||
|
// accelSamplesFromCacheStatus maps combined CACHE_STATUS entries to AccelSample
|
||||||
|
// (for dashboard / WebSocket accel push).
|
||||||
|
func accelSamplesFromCacheStatus(r *pb.CacheStatusResponse) []*pb.AccelSample {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*pb.AccelSample, 0, len(r.GetClients()))
|
||||||
|
for _, c := range r.GetClients() {
|
||||||
|
if c.GetAccel() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
a := c.GetAccel()
|
||||||
|
out = append(out, &pb.AccelSample{
|
||||||
|
ClientId: c.GetClientId(),
|
||||||
|
Valid: a.GetValid(),
|
||||||
|
X: a.GetX(),
|
||||||
|
Y: a.GetY(),
|
||||||
|
Z: a.GetZ(),
|
||||||
|
AgeMs: a.GetAgeMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// tapEventsFromCacheStatus maps combined CACHE_STATUS entries to TapEvent
|
||||||
|
// (only clients with a consumed pending tap).
|
||||||
|
func tapEventsFromCacheStatus(r *pb.CacheStatusResponse) []*pb.TapEvent {
|
||||||
|
if r == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]*pb.TapEvent, 0, len(r.GetClients()))
|
||||||
|
for _, c := range r.GetClients() {
|
||||||
|
if c.GetTap() == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t := c.GetTap()
|
||||||
|
out = append(out, &pb.TapEvent{
|
||||||
|
ClientId: c.GetClientId(),
|
||||||
|
Valid: true,
|
||||||
|
Kind: t.GetKind(),
|
||||||
|
AgeMs: t.GetAgeMs(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
+363
-1
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
@@ -40,6 +41,182 @@ func (m *managedSerial) listClientsPoll() ([]*pb.ClientInfo, error) {
|
|||||||
return decodeClientsPayload(payload)
|
return decodeClientsPayload(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeBatteryStatusPayload(payload []byte) (*pb.BatteryStatusResponse, error) {
|
||||||
|
if len(payload) < 2 {
|
||||||
|
return nil, fmt.Errorf("short battery response")
|
||||||
|
}
|
||||||
|
if payload[0] != byte(pb.MessageType_BATTERY_STATUS) {
|
||||||
|
return nil, fmt.Errorf("unexpected command id 0x%02x (want 0x%02x)",
|
||||||
|
payload[0], byte(pb.MessageType_BATTERY_STATUS))
|
||||||
|
}
|
||||||
|
var msg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
if msg.GetType() != pb.MessageType_BATTERY_STATUS {
|
||||||
|
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
|
||||||
|
}
|
||||||
|
r := msg.GetBatteryStatusResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing battery_status_response")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) BatteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
|
||||||
|
var resp *pb.BatteryStatusResponse
|
||||||
|
err := m.withPort(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = sp.batteryStatus(req)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) BatteryStatusPoll(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_BATTERY_STATUS,
|
||||||
|
Payload: &pb.UartMessage_BatteryStatusRequest{
|
||||||
|
BatteryStatusRequest: req,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...)
|
||||||
|
respPayload, err := m.batteryStatusPayloadPoll(payload)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeBatteryStatusPayload(respPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) batteryStatusPayloadPoll(payload []byte) ([]byte, error) {
|
||||||
|
var resp []byte
|
||||||
|
err := m.withPortPoll(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = sp.exchangePayloadForBattery(payload, "BATTERY_STATUS")
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) batteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_BATTERY_STATUS,
|
||||||
|
Payload: &pb.UartMessage_BatteryStatusRequest{
|
||||||
|
BatteryStatusRequest: req,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...)
|
||||||
|
respPayload, err := s.exchangePayloadForBattery(payload, "BATTERY_STATUS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeBatteryStatusPayload(respPayload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
|
||||||
|
return m.accelStreamVia(m.withPort, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) AccelStreamPoll(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
|
||||||
|
return m.accelStreamVia(m.withPortPoll, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAccelStream enables or disables the ESP-NOW accel stream for one slave (master UART).
|
||||||
|
func (m *managedSerial) SetAccelStream(clientID uint32, enable bool) (*pb.AccelStreamResponse, error) {
|
||||||
|
return m.AccelStream(&pb.AccelStreamRequest{
|
||||||
|
Write: true,
|
||||||
|
Enable: enable,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAccelStream returns whether the accel stream is enabled for a slave on the master.
|
||||||
|
func (m *managedSerial) GetAccelStream(clientID uint32) (bool, error) {
|
||||||
|
resp, err := m.AccelStreamPoll(&pb.AccelStreamRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !resp.GetSuccess() {
|
||||||
|
return false, fmt.Errorf("accel stream read failed for client %d", clientID)
|
||||||
|
}
|
||||||
|
return resp.GetEnabled(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||||
|
return m.tapNotifyVia(m.withPort, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) TapNotifyPoll(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||||
|
return m.tapNotifyVia(m.withPortPoll, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) tapNotifyVia(
|
||||||
|
portFn func(func(*serialPort) error) error,
|
||||||
|
req *pb.TapNotifyRequest,
|
||||||
|
) (*pb.TapNotifyResponse, error) {
|
||||||
|
var resp *pb.TapNotifyResponse
|
||||||
|
err := portFn(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = sp.TapNotify(req)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) readCacheStatusPoll() (*pb.CacheStatusResponse, error) {
|
||||||
|
payload, err := m.exchangePoll(byte(pb.MessageType_CACHE_STATUS), "CACHE_STATUS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeCacheStatusPayload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeCacheStatusPayload(payload []byte) (*pb.CacheStatusResponse, error) {
|
||||||
|
if len(payload) < 1 {
|
||||||
|
return nil, fmt.Errorf("empty response payload")
|
||||||
|
}
|
||||||
|
if payload[0] != byte(pb.MessageType_CACHE_STATUS) {
|
||||||
|
return nil, fmt.Errorf("unexpected command id 0x%02x (want 0x%02x)",
|
||||||
|
payload[0], byte(pb.MessageType_CACHE_STATUS))
|
||||||
|
}
|
||||||
|
var msg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
if msg.GetType() != pb.MessageType_CACHE_STATUS {
|
||||||
|
return nil, fmt.Errorf("unexpected type %v", msg.GetType())
|
||||||
|
}
|
||||||
|
r := msg.GetCacheStatusResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing cache_status_response")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) accelStreamVia(
|
||||||
|
portFn func(func(*serialPort) error) error,
|
||||||
|
req *pb.AccelStreamRequest,
|
||||||
|
) (*pb.AccelStreamResponse, error) {
|
||||||
|
var resp *pb.AccelStreamResponse
|
||||||
|
err := portFn(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = sp.AccelStream(req)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
||||||
return m.accelDeadzoneVia(m.withPort, req)
|
return m.accelDeadzoneVia(m.withPort, req)
|
||||||
}
|
}
|
||||||
@@ -117,6 +294,68 @@ func (s *serialPort) listClients() ([]*pb.ClientInfo, error) {
|
|||||||
return decodeClientsPayload(payload)
|
return decodeClientsPayload(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_ACCEL_STREAM,
|
||||||
|
Payload: &pb.UartMessage_AccelStreamRequest{
|
||||||
|
AccelStreamRequest: req,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_ACCEL_STREAM)}, body...)
|
||||||
|
respPayload, err := s.exchangePayload(payload, "ACCEL_STREAM")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var respMsg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
r := respMsg.GetAccelStreamResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing accel_stream_response")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_TAP_NOTIFY,
|
||||||
|
Payload: &pb.UartMessage_TapNotifyRequest{
|
||||||
|
TapNotifyRequest: req,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_TAP_NOTIFY)}, body...)
|
||||||
|
respPayload, err := s.exchangePayload(payload, "TAP_NOTIFY")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var respMsg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
r := respMsg.GetTapNotifyResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing tap_notify_response")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) readCacheStatus() (*pb.CacheStatusResponse, error) {
|
||||||
|
payload, err := s.exchange(byte(pb.MessageType_CACHE_STATUS), "CACHE_STATUS")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return decodeCacheStatusPayload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serialPort) accelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
func (s *serialPort) accelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
||||||
msg := &pb.UartMessage{
|
msg := &pb.UartMessage{
|
||||||
Type: pb.MessageType_ACCEL_DEADZONE,
|
Type: pb.MessageType_ACCEL_DEADZONE,
|
||||||
@@ -172,16 +411,93 @@ func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EchoPingResult is the host-side round-trip for ESP-NOW echo ping.
|
||||||
|
type EchoPingResult struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
TimestampUs uint64 `json:"timestamp_us"`
|
||||||
|
RttMs float64 `json:"rtt_ms"` // goTool: full UART round-trip
|
||||||
|
EspRttUs uint32 `json:"esp_rtt_us"` // master: µs delta ping send → pong recv
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) echoPing(clientID uint32) (*EchoPingResult, error) {
|
||||||
|
t0 := time.Now()
|
||||||
|
timestampUs := uint64(t0.UnixMicro())
|
||||||
|
req := &pb.EspNowEchoPingRequest{
|
||||||
|
ClientId: clientID,
|
||||||
|
TimestampUs: timestampUs,
|
||||||
|
}
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_ESPNOW_ECHO_PING,
|
||||||
|
Payload: &pb.UartMessage_EspnowEchoPingRequest{
|
||||||
|
EspnowEchoPingRequest: req,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_ESPNOW_ECHO_PING)}, body...)
|
||||||
|
respPayload, err := s.exchangePayload(payload, "ESPNOW_ECHO_PING")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rttMs := float64(time.Since(t0).Microseconds()) / 1000.0
|
||||||
|
|
||||||
|
var respMsg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
r := respMsg.GetEspnowEchoPingResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing espnow_echo_ping_response")
|
||||||
|
}
|
||||||
|
if !r.GetSuccess() {
|
||||||
|
return &EchoPingResult{
|
||||||
|
Success: false,
|
||||||
|
ClientID: r.GetClientId(),
|
||||||
|
RttMs: rttMs,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if r.GetTimestampUs() != timestampUs {
|
||||||
|
return nil, fmt.Errorf("timestamp mismatch: sent %d got %d", timestampUs, r.GetTimestampUs())
|
||||||
|
}
|
||||||
|
return &EchoPingResult{
|
||||||
|
Success: true,
|
||||||
|
ClientID: r.GetClientId(),
|
||||||
|
TimestampUs: r.GetTimestampUs(),
|
||||||
|
RttMs: rttMs,
|
||||||
|
EspRttUs: r.GetEspRttUs(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managedSerial) FindMe(clientID uint32) error {
|
func (m *managedSerial) FindMe(clientID uint32) error {
|
||||||
return m.withPort(func(sp *serialPort) error {
|
return m.withPort(func(sp *serialPort) error {
|
||||||
return runFindMeClient(sp, clientID)
|
return runFindMeClient(sp, clientID)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) SetLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
|
||||||
|
var resp *pb.SetLogLevelResponse
|
||||||
|
err := m.withPort(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = runSetLogLevelClient(sp, write, level)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managedSerial) Restart(clientID uint32) error {
|
func (m *managedSerial) Restart(clientID uint32) error {
|
||||||
return m.withPort(func(sp *serialPort) error {
|
err := m.withPort(func(sp *serialPort) error {
|
||||||
return runRestartClient(sp, clientID)
|
return runRestartClient(sp, clientID)
|
||||||
})
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if clientID == 0 {
|
||||||
|
m.recoverAfterMasterRestart()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
func (s *serialPort) ledRingProgress(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
||||||
@@ -215,6 +531,28 @@ func (s *serialPort) GetVersion() (*pb.VersionResponse, error) { return s.getVer
|
|||||||
|
|
||||||
func (s *serialPort) ListClients() ([]*pb.ClientInfo, error) { return s.listClients() }
|
func (s *serialPort) ListClients() ([]*pb.ClientInfo, error) { return s.listClients() }
|
||||||
|
|
||||||
|
func (s *serialPort) SetAccelStream(clientID uint32, enable bool) (*pb.AccelStreamResponse, error) {
|
||||||
|
return s.AccelStream(&pb.AccelStreamRequest{
|
||||||
|
Write: true,
|
||||||
|
Enable: enable,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) GetAccelStream(clientID uint32) (bool, error) {
|
||||||
|
resp, err := s.AccelStream(&pb.AccelStreamRequest{
|
||||||
|
Write: false,
|
||||||
|
ClientId: clientID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if !resp.GetSuccess() {
|
||||||
|
return false, fmt.Errorf("accel stream read failed for client %d", clientID)
|
||||||
|
}
|
||||||
|
return resp.GetEnabled(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serialPort) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
func (s *serialPort) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
|
||||||
return s.accelDeadzone(req)
|
return s.accelDeadzone(req)
|
||||||
}
|
}
|
||||||
@@ -223,10 +561,34 @@ func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
|
|||||||
return s.espnowUnicastTest(clientID, seq)
|
return s.espnowUnicastTest(clientID, seq)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) EchoPing(clientID uint32) (*EchoPingResult, error) {
|
||||||
|
return s.echoPing(clientID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) EchoPing(clientID uint32) (*EchoPingResult, error) {
|
||||||
|
var result *EchoPingResult
|
||||||
|
err := m.withPort(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
result, e = sp.echoPing(clientID)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
||||||
return s.ledRingProgress(req)
|
return s.ledRingProgress(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
|
||||||
|
var resp *pb.LedRingProgressResponse
|
||||||
|
err := m.withPort(func(sp *serialPort) error {
|
||||||
|
var e error
|
||||||
|
resp, e = sp.LedRing(req)
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
|
func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
|
||||||
return s.espnowFindMe(clientID)
|
return s.espnowFindMe(clientID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ func runTest(portOverride string, baudOverride int, args []string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("open %s: %w", port, err)
|
return fmt.Errorf("open %s: %w", port, err)
|
||||||
}
|
}
|
||||||
|
registerShutdown(func() { _ = sp.Close() })
|
||||||
|
enableShutdownOnInterrupt()
|
||||||
defer sp.Close()
|
defer sp.Close()
|
||||||
|
|
||||||
if !*verbose {
|
if !*verbose {
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runCacheStatus(sp *serialPort) error {
|
||||||
|
r, err := sp.readCacheStatus()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clients := r.GetClients()
|
||||||
|
if len(clients) == 0 {
|
||||||
|
fmt.Println("(no slaves with accel stream or tap notify enabled)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, c := range clients {
|
||||||
|
id := c.GetClientId()
|
||||||
|
if a := c.GetAccel(); a != nil {
|
||||||
|
if !a.GetValid() {
|
||||||
|
fmt.Printf("client %d accel: no sample yet\n", id)
|
||||||
|
} else {
|
||||||
|
fmt.Printf("client %d accel: x=%d y=%d z=%d (age %d ms)\n",
|
||||||
|
id, a.GetX(), a.GetY(), a.GetZ(), a.GetAgeMs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t := c.GetTap(); t != nil {
|
||||||
|
fmt.Printf("client %d tap: %s (age %d ms)\n",
|
||||||
|
id, tapKindLabel(t.GetKind()), t.GetAgeMs())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+10
-2
@@ -18,9 +18,17 @@ func runClients(sp *serialPort) error {
|
|||||||
fmt.Printf("clients (%d):\n", len(clients))
|
fmt.Printf("clients (%d):\n", len(clients))
|
||||||
for i, c := range clients {
|
for i, c := range clients {
|
||||||
mac := hex.EncodeToString(c.GetMac())
|
mac := hex.EncodeToString(c.GetMac())
|
||||||
fmt.Printf(" [%d] id=%d mac=%s ver=%d available=%v used=%v last_ping=%d last_success_ping=%d\n",
|
fmt.Printf(" [%d] id=%d mac=%s ver=%d available=%v used=%v last_ping=%d last_success_ping=%d tap=%s/%s/%s\n",
|
||||||
i, c.GetId(), mac, c.GetVersion(), c.GetAvailable(), c.GetUsed(),
|
i, c.GetId(), mac, c.GetVersion(), c.GetAvailable(), c.GetUsed(),
|
||||||
c.GetLastPing(), c.GetLastSuccessPing())
|
c.GetLastPing(), c.GetLastSuccessPing(),
|
||||||
|
boolFlag(c.GetTapNotifySingle()), boolFlag(c.GetTapNotifyDouble()), boolFlag(c.GetTapNotifyTriple()))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func boolFlag(v bool) string {
|
||||||
|
if v {
|
||||||
|
return "on"
|
||||||
|
}
|
||||||
|
return "off"
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runEchoPing(sp *serialPort, args []string) error {
|
||||||
|
fs := flag.NewFlagSet("echo-ping", flag.ExitOnError)
|
||||||
|
clientID := fs.Uint("client", 0, "slave client id from `clients`")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if *clientID == 0 {
|
||||||
|
return fmt.Errorf("client id required (see `gotool clients`)")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := sp.echoPing(uint32(*clientID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("echo ping: success=%v client_id=%d rtt_ms=%.3f esp_rtt_us=%d\n",
|
||||||
|
r.Success, r.ClientID, r.RttMs, r.EspRttUs)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+13
-25
@@ -7,17 +7,12 @@ import (
|
|||||||
"powerpod/gotool/pb"
|
"powerpod/gotool/pb"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
ledRingModeClear = 0
|
|
||||||
ledRingModeProgress = 1
|
|
||||||
ledRingModeDigit = 2
|
|
||||||
ledRingModeBlink = 3
|
|
||||||
ledRingModeFindMe = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
func runLedRing(sp *serialPort, args []string) error {
|
func runLedRing(sp *serialPort, args []string) error {
|
||||||
fs := flag.NewFlagSet("led-ring", flag.ExitOnError)
|
fs := flag.NewFlagSet("led-ring", flag.ExitOnError)
|
||||||
mode := fs.String("mode", "progress", "clear, progress, digit, blink, or find-me")
|
mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, find-me, or battery-low")
|
||||||
|
clientID := fs.Uint("client", 0, "0=master ring, >0=slave via ESP-NOW")
|
||||||
|
allClients := fs.Bool("all", false, "broadcast to all slaves")
|
||||||
|
slavesOnly := fs.Bool("slaves-only", false, "with -all: do not change master ring")
|
||||||
progress := fs.Uint("progress", 0, "fill level 0–100 (mode=progress)")
|
progress := fs.Uint("progress", 0, "fill level 0–100 (mode=progress)")
|
||||||
digit := fs.Uint("digit", 0, "digit 0–10 (mode=digit)")
|
digit := fs.Uint("digit", 0, "digit 0–10 (mode=digit)")
|
||||||
r := fs.Uint("r", 0, "red 0–255")
|
r := fs.Uint("r", 0, "red 0–255")
|
||||||
@@ -30,20 +25,9 @@ func runLedRing(sp *serialPort, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var modeVal uint32
|
modeVal, err := ledRingModeFromString(*mode)
|
||||||
switch *mode {
|
if err != nil {
|
||||||
case "clear":
|
return err
|
||||||
modeVal = ledRingModeClear
|
|
||||||
case "progress":
|
|
||||||
modeVal = ledRingModeProgress
|
|
||||||
case "digit":
|
|
||||||
modeVal = ledRingModeDigit
|
|
||||||
case "blink":
|
|
||||||
modeVal = ledRingModeBlink
|
|
||||||
case "find-me", "find_me", "findme":
|
|
||||||
modeVal = ledRingModeFindMe
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unknown -mode %q (clear, progress, digit, blink, find-me)", *mode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{
|
resp, err := sp.ledRingProgress(&pb.LedRingProgressRequest{
|
||||||
@@ -56,11 +40,15 @@ func runLedRing(sp *serialPort, args []string) error {
|
|||||||
Intensity: uint32(*intensity),
|
Intensity: uint32(*intensity),
|
||||||
BlinkMs: uint32(*blinkMs),
|
BlinkMs: uint32(*blinkMs),
|
||||||
BlinkCount: uint32(*blinkCount),
|
BlinkCount: uint32(*blinkCount),
|
||||||
|
ClientId: uint32(*clientID),
|
||||||
|
AllClients: *allClients,
|
||||||
|
SlavesOnly: *slavesOnly,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Printf("success=%v mode=%d progress=%d digit=%d\n",
|
fmt.Printf("success=%v mode=%d progress=%d digit=%d client_id=%d slaves_updated=%d\n",
|
||||||
resp.GetSuccess(), resp.GetMode(), resp.GetProgress(), resp.GetDigit())
|
resp.GetSuccess(), resp.GetMode(), resp.GetProgress(), resp.GetDigit(),
|
||||||
|
resp.GetClientId(), resp.GetSlavesUpdated())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runLogLevel(sp *serialPort, args []string) error {
|
||||||
|
fs := flag.NewFlagSet("log-level", flag.ExitOnError)
|
||||||
|
write := fs.Bool("set", false, "write log level (default: read)")
|
||||||
|
level := fs.Uint("level", 0, "esp_log_level_t 0–5 (with -set)")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := sp.setLogLevel(*write, uint32(*level))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !resp.GetSuccess() {
|
||||||
|
return fmt.Errorf("set_log_level rejected (level=%d)", resp.GetLevel())
|
||||||
|
}
|
||||||
|
fmt.Printf("log_level=%d success=%v\n", resp.GetLevel(), resp.GetSuccess())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runSetLogLevelClient(sp *serialPort, write bool, level uint32) (*pb.SetLogLevelResponse, error) {
|
||||||
|
return sp.setLogLevel(write, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) setLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
|
||||||
|
msg := &pb.UartMessage{
|
||||||
|
Type: pb.MessageType_SET_LOG_LEVEL,
|
||||||
|
Payload: &pb.UartMessage_SetLogLevelRequest{
|
||||||
|
SetLogLevelRequest: &pb.SetLogLevelRequest{
|
||||||
|
Write: write,
|
||||||
|
Level: level,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body, err := proto.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode: %w", err)
|
||||||
|
}
|
||||||
|
payload := append([]byte{byte(pb.MessageType_SET_LOG_LEVEL)}, body...)
|
||||||
|
respPayload, err := s.exchangePayload(payload, "SET_LOG_LEVEL")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var respMsg pb.UartMessage
|
||||||
|
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode: %w", err)
|
||||||
|
}
|
||||||
|
r := respMsg.GetSetLogLevelResponse()
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("missing set_log_level_response")
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
+1
-2
@@ -16,8 +16,7 @@ func runOTA(sp *serialPort, args []string) error {
|
|||||||
|
|
||||||
sp.mu.Lock()
|
sp.mu.Lock()
|
||||||
defer sp.mu.Unlock()
|
defer sp.mu.Unlock()
|
||||||
m := &managedSerial{quiet: false, sp: sp}
|
return runOTAOnPortUnlocked(sp, data, func(p OTAProgress) {
|
||||||
return runOTAOnPortUnlocked(m, data, func(p OTAProgress) {
|
|
||||||
switch p.Phase {
|
switch p.Phase {
|
||||||
case "preparing", "ready":
|
case "preparing", "ready":
|
||||||
fmt.Println(p.Message)
|
fmt.Println(p.Message)
|
||||||
|
|||||||
+41
-8
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
@@ -21,7 +22,9 @@ var wsUpgrader = websocket.Upgrader{
|
|||||||
|
|
||||||
func runServe(portName string, baud int, args []string) error {
|
func runServe(portName string, baud int, args []string) error {
|
||||||
serveFlags := flag.NewFlagSet("serve", flag.ExitOnError)
|
serveFlags := flag.NewFlagSet("serve", flag.ExitOnError)
|
||||||
addr := serveFlags.String("addr", ":8080", "HTTP listen address")
|
addr := serveFlags.String("addr", ":8080", "dashboard HTTP listen address")
|
||||||
|
apiAddr := serveFlags.String("api-addr", ":8081", "external API HTTP listen address (empty to disable)")
|
||||||
|
accelInterval := serveFlags.Duration("accel-interval", defaultAccelStreamInterval, "accel WebSocket sample period on API server")
|
||||||
interval := serveFlags.Duration("interval", 2*time.Second, "UART poll interval")
|
interval := serveFlags.Duration("interval", 2*time.Second, "UART poll interval")
|
||||||
if err := serveFlags.Parse(args); err != nil {
|
if err := serveFlags.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -32,15 +35,33 @@ func runServe(portName string, baud int, args []string) error {
|
|||||||
|
|
||||||
link := newManagedSerial(portName, baud)
|
link := newManagedSerial(portName, baud)
|
||||||
link.quiet = true
|
link.quiet = true
|
||||||
defer link.Close()
|
|
||||||
|
|
||||||
hub := newWSHub()
|
hub := newWSHub()
|
||||||
|
streamCtl := newAccelStreamCtl()
|
||||||
|
tapCtl := newTapNotifyCtl()
|
||||||
stop := make(chan struct{})
|
stop := make(chan struct{})
|
||||||
defer close(stop)
|
|
||||||
go runPoller(link, portName, hub, *interval, 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)
|
||||||
|
|
||||||
|
if *apiAddr != "" {
|
||||||
|
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop)
|
||||||
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mountServeAPI(mux, link, hub)
|
mountServeAPI(mux, link, hub, streamCtl, tapCtl)
|
||||||
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
conn, err := wsUpgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,7 +85,19 @@ func runServe(portName string, baud int, args []string) error {
|
|||||||
}
|
}
|
||||||
mux.Handle("/", http.FileServer(http.FS(ui)))
|
mux.Handle("/", http.FileServer(http.FS(ui)))
|
||||||
|
|
||||||
log.Printf("dashboard http://localhost%s (UART %s @ %d baud, poll %s, auto-reconnect)",
|
log.Printf("dashboard http://localhost%s (UART %s @ %d baud, poll %s, live-stream %s, auto-reconnect)",
|
||||||
*addr, portName, baud, interval.String())
|
*addr, portName, baud, interval.String(), accelInterval.String())
|
||||||
return http.ListenAndServe(*addr, mux)
|
if *apiAddr == "" {
|
||||||
|
log.Printf("external API disabled (-api-addr \"\")")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runTapNotify(sp *serialPort, args []string) error {
|
||||||
|
fs := flag.NewFlagSet("tap-notify", flag.ExitOnError)
|
||||||
|
write := fs.Bool("set", false, "write tap notify flags (default: read)")
|
||||||
|
clientID := fs.Uint("client", 0, "client id (>0 required for read/set one slave)")
|
||||||
|
all := fs.Bool("all", false, "apply to all registered slaves (with -set)")
|
||||||
|
single := fs.Bool("single", false, "notify on single tap")
|
||||||
|
doubleTap := fs.Bool("double", false, "notify on double tap")
|
||||||
|
triple := fs.Bool("triple", false, "notify on triple tap")
|
||||||
|
if err := fs.Parse(args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*write && (*all || *clientID == 0) {
|
||||||
|
return fmt.Errorf("read requires -client <id>")
|
||||||
|
}
|
||||||
|
if *write && !*all && *clientID == 0 {
|
||||||
|
return fmt.Errorf("set requires -client <id> or -all")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := sp.TapNotify(&pb.TapNotifyRequest{
|
||||||
|
Write: *write,
|
||||||
|
ClientId: uint32(*clientID),
|
||||||
|
AllClients: *all,
|
||||||
|
Single: *single,
|
||||||
|
DoubleTap: *doubleTap,
|
||||||
|
Triple: *triple,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("client_id=%d success=%v slaves_updated=%d single=%v double=%v triple=%v\n",
|
||||||
|
r.GetClientId(), r.GetSuccess(), r.GetSlavesUpdated(),
|
||||||
|
r.GetSingle(), r.GetDoubleTap(), r.GetTriple())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tapKindLabel(k pb.TapKind) string {
|
||||||
|
switch k {
|
||||||
|
case pb.TapKind_TAP_SINGLE:
|
||||||
|
return "single"
|
||||||
|
case pb.TapKind_TAP_DOUBLE:
|
||||||
|
return "double"
|
||||||
|
case pb.TapKind_TAP_TRIPLE:
|
||||||
|
return "triple"
|
||||||
|
default:
|
||||||
|
return "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
+533
-17
@@ -19,7 +19,10 @@ type MasterView struct {
|
|||||||
GitHash string `json:"git_hash"`
|
GitHash string `json:"git_hash"`
|
||||||
RunningPartition string `json:"running_partition,omitempty"`
|
RunningPartition string `json:"running_partition,omitempty"`
|
||||||
Deadzone uint32 `json:"deadzone,omitempty"`
|
Deadzone uint32 `json:"deadzone,omitempty"`
|
||||||
OK bool `json:"ok"`
|
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||||
|
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||||
|
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
|
||||||
|
OK bool `json:"ok"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,6 +35,20 @@ type ClientView struct {
|
|||||||
Used bool `json:"used"`
|
Used bool `json:"used"`
|
||||||
LastPing uint32 `json:"last_ping"`
|
LastPing uint32 `json:"last_ping"`
|
||||||
LastSuccessPing uint32 `json:"last_success_ping"`
|
LastSuccessPing uint32 `json:"last_success_ping"`
|
||||||
|
AccelValid bool `json:"accel_valid"`
|
||||||
|
AccelX int32 `json:"accel_x"`
|
||||||
|
AccelY int32 `json:"accel_y"`
|
||||||
|
AccelZ int32 `json:"accel_z"`
|
||||||
|
AccelAgeMs uint32 `json:"accel_age_ms"`
|
||||||
|
AccelStream bool `json:"accel_stream"`
|
||||||
|
TapNotifySingle bool `json:"tap_notify_single"`
|
||||||
|
TapNotifyDouble bool `json:"tap_notify_double"`
|
||||||
|
TapNotifyTriple bool `json:"tap_notify_triple"`
|
||||||
|
LastTap string `json:"last_tap,omitempty"`
|
||||||
|
LastTapAt int64 `json:"last_tap_at,omitempty"`
|
||||||
|
Lipo1 lipoReadingJSON `json:"lipo1"`
|
||||||
|
Lipo2 lipoReadingJSON `json:"lipo2"`
|
||||||
|
BatteryAgeMs uint32 `json:"battery_age_ms,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardState struct {
|
type DashboardState struct {
|
||||||
@@ -40,14 +57,17 @@ type DashboardState struct {
|
|||||||
UARTConnected bool `json:"uart_connected"`
|
UARTConnected bool `json:"uart_connected"`
|
||||||
SerialOK bool `json:"serial_ok"`
|
SerialOK bool `json:"serial_ok"`
|
||||||
SerialError string `json:"serial_error,omitempty"`
|
SerialError string `json:"serial_error,omitempty"`
|
||||||
|
/** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */
|
||||||
|
LiveStream bool `json:"live_stream"`
|
||||||
Master MasterView `json:"master"`
|
Master MasterView `json:"master"`
|
||||||
Clients []ClientView `json:"clients"`
|
Clients []ClientView `json:"clients"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type wsHub struct {
|
type wsHub struct {
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
clients map[*websocket.Conn]struct{}
|
clients map[*websocket.Conn]struct{}
|
||||||
state DashboardState
|
state DashboardState
|
||||||
|
liveStream bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWSHub() *wsHub {
|
func newWSHub() *wsHub {
|
||||||
@@ -56,6 +76,19 @@ func newWSHub() *wsHub {
|
|||||||
|
|
||||||
func (h *wsHub) setState(st DashboardState) {
|
func (h *wsHub) setState(st DashboardState) {
|
||||||
h.mu.Lock()
|
h.mu.Lock()
|
||||||
|
prev := h.state
|
||||||
|
st.LiveStream = prev.LiveStream
|
||||||
|
st.Clients = preserveClientAccel(st.Clients, prev.Clients, st.LiveStream)
|
||||||
|
st.Clients = preserveClientBattery(st.Clients, prev.Clients)
|
||||||
|
st.Clients = preserveClientTap(st.Clients, prev.Clients)
|
||||||
|
if !st.Master.Lipo1.Valid && !st.Master.Lipo2.Valid {
|
||||||
|
if prev.Master.Lipo1.Valid || prev.Master.Lipo2.Valid {
|
||||||
|
st.Master.Lipo1 = prev.Master.Lipo1
|
||||||
|
st.Master.Lipo2 = prev.Master.Lipo2
|
||||||
|
st.Master.BatteryAgeMs = prev.Master.BatteryAgeMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.liveStream = st.LiveStream
|
||||||
h.state = st
|
h.state = st
|
||||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
for c := range h.clients {
|
for c := range h.clients {
|
||||||
@@ -68,7 +101,7 @@ func (h *wsHub) setState(st DashboardState) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, c := range conns {
|
for _, c := range conns {
|
||||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
h.writeJSON(c, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +112,7 @@ func (h *wsHub) register(c *websocket.Conn) {
|
|||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
|
|
||||||
if data, err := json.Marshal(snap); err == nil {
|
if data, err := json.Marshal(snap); err == nil {
|
||||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
h.writeJSON(c, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,24 +122,400 @@ func (h *wsHub) unregister(c *websocket.Conn) {
|
|||||||
h.mu.Unlock()
|
h.mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *wsHub) broadcastRaw(v any) {
|
// 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()
|
h.mu.RLock()
|
||||||
conns := make([]*websocket.Conn, 0, len(h.clients))
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
for c := range h.clients {
|
for c := range h.clients {
|
||||||
conns = append(conns, c)
|
conns = append(conns, c)
|
||||||
}
|
}
|
||||||
h.mu.RUnlock()
|
h.mu.RUnlock()
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(v)
|
func applyAccelSamples(clients []ClientView, samples []*pb.AccelSample) []ClientView {
|
||||||
|
if len(samples) == 0 {
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
byID := make(map[uint32]*pb.AccelSample, len(samples))
|
||||||
|
for _, s := range samples {
|
||||||
|
byID[s.GetClientId()] = s
|
||||||
|
}
|
||||||
|
out := make([]ClientView, len(clients))
|
||||||
|
for i, c := range clients {
|
||||||
|
out[i] = c
|
||||||
|
if !c.AccelStream {
|
||||||
|
out[i].AccelValid = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s, ok := byID[c.ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i].AccelValid = s.GetValid()
|
||||||
|
if s.GetValid() {
|
||||||
|
out[i].AccelX = s.GetX()
|
||||||
|
out[i].AccelY = s.GetY()
|
||||||
|
out[i].AccelZ = s.GetZ()
|
||||||
|
out[i].AccelAgeMs = s.GetAgeMs()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func preserveClientAccel(newClients, oldClients []ClientView, liveStream bool) []ClientView {
|
||||||
|
if len(oldClients) == 0 {
|
||||||
|
return newClients
|
||||||
|
}
|
||||||
|
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||||
|
for _, c := range oldClients {
|
||||||
|
oldByID[c.ID] = c
|
||||||
|
}
|
||||||
|
out := make([]ClientView, len(newClients))
|
||||||
|
for i, c := range newClients {
|
||||||
|
out[i] = c
|
||||||
|
if !liveStream && !c.AccelStream {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if liveStream && !c.AccelStream {
|
||||||
|
out[i].AccelValid = false
|
||||||
|
out[i].AccelX = 0
|
||||||
|
out[i].AccelY = 0
|
||||||
|
out[i].AccelZ = 0
|
||||||
|
out[i].AccelAgeMs = 0
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prev, ok := oldByID[c.ID]
|
||||||
|
if !ok || !prev.AccelValid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !c.AccelValid {
|
||||||
|
out[i].AccelValid = prev.AccelValid
|
||||||
|
out[i].AccelX = prev.AccelX
|
||||||
|
out[i].AccelY = prev.AccelY
|
||||||
|
out[i].AccelZ = prev.AccelZ
|
||||||
|
out[i].AccelAgeMs = prev.AccelAgeMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func preserveClientBattery(newClients, oldClients []ClientView) []ClientView {
|
||||||
|
if len(oldClients) == 0 {
|
||||||
|
return newClients
|
||||||
|
}
|
||||||
|
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||||
|
for _, c := range oldClients {
|
||||||
|
oldByID[c.ID] = c
|
||||||
|
}
|
||||||
|
out := make([]ClientView, len(newClients))
|
||||||
|
for i, c := range newClients {
|
||||||
|
out[i] = c
|
||||||
|
if c.Lipo1.Valid || c.Lipo2.Valid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prev, ok := oldByID[c.ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if prev.Lipo1.Valid || prev.Lipo2.Valid {
|
||||||
|
out[i].Lipo1 = prev.Lipo1
|
||||||
|
out[i].Lipo2 = prev.Lipo2
|
||||||
|
out[i].BatteryAgeMs = prev.BatteryAgeMs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyClientAccelStream(clients []ClientView) bool {
|
||||||
|
for _, c := range clients {
|
||||||
|
if c.AccelStream {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func anyClientTapNotify(clients []ClientView) bool {
|
||||||
|
for _, c := range clients {
|
||||||
|
if c.TapNotifySingle || c.TapNotifyDouble || c.TapNotifyTriple {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func tapKindLabelPB(k pb.TapKind) string {
|
||||||
|
switch k {
|
||||||
|
case pb.TapKind_TAP_SINGLE:
|
||||||
|
return "single"
|
||||||
|
case pb.TapKind_TAP_DOUBLE:
|
||||||
|
return "double"
|
||||||
|
case pb.TapKind_TAP_TRIPLE:
|
||||||
|
return "triple"
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyTapEvents(clients []ClientView, events []*pb.TapEvent) []ClientView {
|
||||||
|
if len(events) == 0 {
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
byID := make(map[uint32]*pb.TapEvent, len(events))
|
||||||
|
for _, e := range events {
|
||||||
|
if e.GetValid() {
|
||||||
|
byID[e.GetClientId()] = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(byID) == 0 {
|
||||||
|
return clients
|
||||||
|
}
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
out := make([]ClientView, len(clients))
|
||||||
|
for i, c := range clients {
|
||||||
|
out[i] = c
|
||||||
|
if !clientTapNotifyAny(c) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
e, ok := byID[c.ID]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i].LastTap = tapKindLabelPB(e.GetKind())
|
||||||
|
out[i].LastTapAt = now
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientTapDisplayMinMs = 2000
|
||||||
|
|
||||||
|
func clientTapNotifyAny(c ClientView) bool {
|
||||||
|
return c.TapNotifySingle || c.TapNotifyDouble || c.TapNotifyTriple
|
||||||
|
}
|
||||||
|
|
||||||
|
func preserveClientTap(newClients, oldClients []ClientView) []ClientView {
|
||||||
|
if len(oldClients) == 0 {
|
||||||
|
return newClients
|
||||||
|
}
|
||||||
|
oldByID := make(map[uint32]ClientView, len(oldClients))
|
||||||
|
for _, c := range oldClients {
|
||||||
|
oldByID[c.ID] = c
|
||||||
|
}
|
||||||
|
cutoff := time.Now().Add(-clientTapDisplayMinMs * time.Millisecond).UnixMilli()
|
||||||
|
out := make([]ClientView, len(newClients))
|
||||||
|
for i, c := range newClients {
|
||||||
|
out[i] = c
|
||||||
|
if c.LastTap != "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
prev, ok := oldByID[c.ID]
|
||||||
|
if !ok || prev.LastTap == "" || prev.LastTapAt < cutoff {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out[i].LastTap = prev.LastTap
|
||||||
|
out[i].LastTapAt = prev.LastTapAt
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// patchClientAccelStream updates stream flag immediately (e.g. after REST) and pushes WS.
|
||||||
|
func (h *wsHub) patchClientAccelStream(clientID uint32, enabled bool) {
|
||||||
|
h.mu.Lock()
|
||||||
|
for i := range h.state.Clients {
|
||||||
|
if h.state.Clients[i].ID != clientID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.state.Clients[i].AccelStream = enabled
|
||||||
|
if !enabled {
|
||||||
|
h.state.Clients[i].AccelValid = false
|
||||||
|
h.state.Clients[i].AccelX = 0
|
||||||
|
h.state.Clients[i].AccelY = 0
|
||||||
|
h.state.Clients[i].AccelZ = 0
|
||||||
|
h.state.Clients[i].AccelAgeMs = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
st := h.state
|
||||||
|
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, c := range conns {
|
for _, c := range conns {
|
||||||
_ = c.WriteMessage(websocket.TextMessage, data)
|
h.writeJSON(c, data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pollDashboard(link *managedSerial, portName string, last *DashboardState) DashboardState {
|
func (h *wsHub) anyAccelStreamEnabled() bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return anyClientAccelStream(h.state.Clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) anyTapNotifyEnabled() bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return anyClientTapNotify(h.state.Clients)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) liveStreamEnabled() bool {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
return h.liveStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) snapshotClients() []ClientView {
|
||||||
|
h.mu.RLock()
|
||||||
|
defer h.mu.RUnlock()
|
||||||
|
out := make([]ClientView, len(h.state.Clients))
|
||||||
|
copy(out, h.state.Clients)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// patchLiveStream toggles host CACHE_STATUS polling (~16 ms).
|
||||||
|
func (h *wsHub) patchLiveStream(enabled bool) {
|
||||||
|
h.mu.Lock()
|
||||||
|
h.liveStream = enabled
|
||||||
|
st := h.state
|
||||||
|
st.LiveStream = enabled
|
||||||
|
if !enabled {
|
||||||
|
for i := range st.Clients {
|
||||||
|
st.Clients[i].AccelValid = false
|
||||||
|
st.Clients[i].AccelX = 0
|
||||||
|
st.Clients[i].AccelY = 0
|
||||||
|
st.Clients[i].AccelZ = 0
|
||||||
|
st.Clients[i].AccelAgeMs = 0
|
||||||
|
st.Clients[i].LastTap = ""
|
||||||
|
st.Clients[i].LastTapAt = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h.state = st
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// patchClientTapNotify updates tap notify flags immediately (e.g. after REST) and pushes WS.
|
||||||
|
func (h *wsHub) patchClientTapNotify(clientID uint32, single, doubleTap, triple bool) {
|
||||||
|
h.mu.Lock()
|
||||||
|
for i := range h.state.Clients {
|
||||||
|
if h.state.Clients[i].ID != clientID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
h.state.Clients[i].TapNotifySingle = single
|
||||||
|
h.state.Clients[i].TapNotifyDouble = doubleTap
|
||||||
|
h.state.Clients[i].TapNotifyTriple = triple
|
||||||
|
if !single && !doubleTap && !triple {
|
||||||
|
h.state.Clients[i].LastTap = ""
|
||||||
|
h.state.Clients[i].LastTapAt = 0
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
st := h.state
|
||||||
|
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeAccel updates cached accel on clients and pushes state to dashboard WebSockets.
|
||||||
|
func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
|
||||||
|
if !h.liveStreamEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
st := h.state
|
||||||
|
st.Clients = applyAccelSamples(st.Clients, samples)
|
||||||
|
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||||
|
h.state = st
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) mergeTap(events []*pb.TapEvent) {
|
||||||
|
if len(events) == 0 || !h.liveStreamEnabled() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
st := h.state
|
||||||
|
st.Clients = applyTapEvents(st.Clients, events)
|
||||||
|
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||||
|
h.state = st
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) broadcastRaw(v any) {
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.broadcastJSON(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState {
|
||||||
st := DashboardState{
|
st := DashboardState{
|
||||||
UpdatedAt: time.Now().Format(time.RFC3339),
|
UpdatedAt: time.Now().Format(time.RFC3339),
|
||||||
SerialPort: portName,
|
SerialPort: portName,
|
||||||
@@ -152,15 +561,119 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState) D
|
|||||||
Used: c.GetUsed(),
|
Used: c.GetUsed(),
|
||||||
LastPing: c.GetLastPing(),
|
LastPing: c.GetLastPing(),
|
||||||
LastSuccessPing: c.GetLastSuccessPing(),
|
LastSuccessPing: c.GetLastSuccessPing(),
|
||||||
}
|
AccelStream: c.GetAccelStreamEnabled(),
|
||||||
if dz, err := readDeadzonePoll(link, c.GetId()); err == nil {
|
TapNotifySingle: c.GetTapNotifySingle(),
|
||||||
cv.Deadzone = dz
|
TapNotifyDouble: c.GetTapNotifyDouble(),
|
||||||
|
TapNotifyTriple: c.GetTapNotifyTriple(),
|
||||||
}
|
}
|
||||||
st.Clients = append(st.Clients, cv)
|
st.Clients = append(st.Clients, cv)
|
||||||
}
|
}
|
||||||
|
applyBatteryToState(link, &st)
|
||||||
|
if last == nil || !last.LiveStream {
|
||||||
|
for i, c := range clients {
|
||||||
|
if dz, err := readDeadzonePoll(link, c.GetId()); err == nil {
|
||||||
|
st.Clients[i].Deadzone = dz
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last != nil {
|
||||||
|
st.LiveStream = last.LiveStream
|
||||||
|
}
|
||||||
|
if streamCtl != nil {
|
||||||
|
streamCtl.SyncFromClients(st.Clients)
|
||||||
|
}
|
||||||
|
if tapCtl != nil {
|
||||||
|
tapCtl.SyncFromClients(st.Clients)
|
||||||
|
}
|
||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
applyBatterySamplesToState(st, batterySamplesFromPB(bat.GetSamples()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) mergeBattery(samples []batterySampleJSON) {
|
||||||
|
if len(samples) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.mu.Lock()
|
||||||
|
st := h.state
|
||||||
|
applyBatterySamplesToState(&st, samples)
|
||||||
|
st.UpdatedAt = time.Now().Format(time.RFC3339)
|
||||||
|
h.state = st
|
||||||
|
conns := make([]*websocket.Conn, 0, len(h.clients))
|
||||||
|
for c := range h.clients {
|
||||||
|
conns = append(conns, c)
|
||||||
|
}
|
||||||
|
h.mu.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(st)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, c := range conns {
|
||||||
|
h.writeJSON(c, data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if !link.IsConnected() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
|
||||||
|
if errors.Is(err, errUARTBusy) || err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hub.mergeBattery(batterySamplesFromPB(bat.GetSamples()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCacheStatusDashboardPoller(link *managedSerial, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
||||||
|
ticker := time.NewTicker(interval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-stop:
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
if !hub.liveStreamEnabled() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cache, err := link.readCacheStatusPoll()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hub.mergeAccel(accelSamplesFromCacheStatus(cache))
|
||||||
|
hub.mergeTap(tapEventsFromCacheStatus(cache))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *wsHub) clientCount() int {
|
||||||
|
h.mu.RLock()
|
||||||
|
n := len(h.clients)
|
||||||
|
h.mu.RUnlock()
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
func pausedPollState(portName string, last *DashboardState) DashboardState {
|
func pausedPollState(portName string, last *DashboardState) DashboardState {
|
||||||
if last != nil && last.UARTConnected {
|
if last != nil && last.UARTConnected {
|
||||||
st := *last
|
st := *last
|
||||||
@@ -208,22 +721,25 @@ func formatMAC(mac []byte) string {
|
|||||||
return hex.EncodeToString(mac)
|
return hex.EncodeToString(mac)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runPoller(link *managedSerial, portName string, hub *wsHub, interval time.Duration, stop <-chan struct{}) {
|
func runPoller(link *managedSerial, portName string, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl, interval time.Duration, stop <-chan struct{}) {
|
||||||
|
// streamCtl / tapCtl kept for external API; dashboard uses hub.state flags.
|
||||||
ticker := time.NewTicker(interval)
|
ticker := time.NewTicker(interval)
|
||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
uartUp := false
|
uartUp := false
|
||||||
var lastGood DashboardState
|
var lastGood DashboardState
|
||||||
publish := func() {
|
publish := func() {
|
||||||
st := pollDashboard(link, portName, &lastGood)
|
st := pollDashboard(link, portName, &lastGood, streamCtl, tapCtl)
|
||||||
|
hub.setState(st)
|
||||||
if st.UARTConnected && st.SerialOK {
|
if st.UARTConnected && st.SerialOK {
|
||||||
lastGood = st
|
hub.mu.RLock()
|
||||||
|
lastGood = hub.state
|
||||||
|
hub.mu.RUnlock()
|
||||||
}
|
}
|
||||||
if st.UARTConnected && !uartUp {
|
if st.UARTConnected && !uartUp {
|
||||||
log.Printf("UART %s connected", portName)
|
log.Printf("UART %s connected", portName)
|
||||||
}
|
}
|
||||||
uartUp = st.UARTConnected
|
uartUp = st.UARTConnected
|
||||||
hub.setState(st)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
publish()
|
publish()
|
||||||
|
|||||||
@@ -0,0 +1,368 @@
|
|||||||
|
# REST API
|
||||||
|
|
||||||
|
`go run . -port /dev/ttyUSB0 serve` starts two HTTP servers on the same UART link:
|
||||||
|
|
||||||
|
| Base URL | Flag | Used by |
|
||||||
|
|----------|------|---------|
|
||||||
|
| `http://localhost:8080` | `-addr` (default `:8080`) | Web dashboard + automation on the UI routes |
|
||||||
|
| `http://localhost:8081` | `-api-addr` (default `:8081`, `""` disables) | External programs; subset of routes + service info |
|
||||||
|
|
||||||
|
WebSocket streaming (accel/tap push): [`API_WEBSOCKET.md`](API_WEBSOCKET.md).
|
||||||
|
|
||||||
|
All JSON responses use `Content-Type: application/json`. On UART errors many routes return **503** with `"error"` in the body.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## External API (`:8081`)
|
||||||
|
|
||||||
|
### Service info
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /
|
||||||
|
GET /api/v1/
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "powerpod-external-api",
|
||||||
|
"version": "1",
|
||||||
|
"serial_port": "/dev/ttyUSB0",
|
||||||
|
"websocket": "/ws",
|
||||||
|
"default_interval_ms": 16,
|
||||||
|
"min_interval_ms": 1,
|
||||||
|
"max_interval_ms": 10000,
|
||||||
|
"tap_display_min_ms": 2000,
|
||||||
|
"description": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Battery
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/battery?all_clients=true
|
||||||
|
GET /api/battery?client_id=16
|
||||||
|
POST /api/battery
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
POST body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"all_clients": true}
|
||||||
|
{"client_id": 0}
|
||||||
|
{"client_id": 16}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"client_id": 16,
|
||||||
|
"lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
|
||||||
|
"lipo2": {"valid": false},
|
||||||
|
"age_ms": 1200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Slaves push battery to the master every **30 s**; these routes read the master cache.
|
||||||
|
|
||||||
|
WebSocket equivalent: `get_battery` on `ws://localhost:8081/ws` (reply type `battery_status`).
|
||||||
|
|
||||||
|
### LED ring
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/led-ring
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
Body:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
|
||||||
|
{"mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
|
||||||
|
{"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 |
|
||||||
|
| `battery-low` | First 4 LEDs red at ~10 % for 5 s (UV indicator test) |
|
||||||
|
|
||||||
|
Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.
|
||||||
|
|
||||||
|
Response: `success`, `slaves_updated`, optional `error`.
|
||||||
|
|
||||||
|
WebSocket: `set_led_ring` with the same fields plus `"type":"set_led_ring"` → `led_ring_status`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard API (`:8080`)
|
||||||
|
|
||||||
|
Used by the web UI; safe for scripts that drive the same features.
|
||||||
|
|
||||||
|
### Live stream (host `CACHE_STATUS` poll ~16 ms)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/live-stream
|
||||||
|
PUT /api/live-stream
|
||||||
|
Content-Type: application/json
|
||||||
|
{"enable": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"enabled": true, "success": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Enables fast UART polling for dashboard accel/tap display. Per-slave accel still requires accel-stream (below).
|
||||||
|
|
||||||
|
### Accel stream (firmware ESP-NOW, per slave)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/clients/16/accel-stream
|
||||||
|
PUT /api/clients/16/accel-stream
|
||||||
|
Content-Type: application/json
|
||||||
|
{"enable": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"enabled": true, "client_id": 16, "success": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
All slaves:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/accel-stream
|
||||||
|
Content-Type: application/json
|
||||||
|
{"write": true, "enable": true, "all_clients": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Polling on the host runs only while at least one slave has streaming enabled (here or via external WebSocket / dashboard).
|
||||||
|
|
||||||
|
### Tap notify (firmware; does not start host tap polling)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/clients/16/tap-notify
|
||||||
|
PUT /api/clients/16/tap-notify
|
||||||
|
Content-Type: application/json
|
||||||
|
{"single": true, "double_tap": false, "triple": false}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"client_id": 16,
|
||||||
|
"success": true,
|
||||||
|
"slaves_updated": 1,
|
||||||
|
"single": true,
|
||||||
|
"double_tap": false,
|
||||||
|
"triple": false
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
All slaves:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/tap-notify
|
||||||
|
Content-Type: application/json
|
||||||
|
{"single": true, "double_tap": false, "triple": false, "all_clients": true}
|
||||||
|
```
|
||||||
|
|
||||||
|
Host tap display / external `set_tap_stream` is separate.
|
||||||
|
|
||||||
|
### Tap snapshot (one-shot, via `CACHE_STATUS`)
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/tap-snapshot?client_id=16
|
||||||
|
```
|
||||||
|
|
||||||
|
Reads the combined cache (`CACHE_STATUS`); optional `client_id` filters pending tap events. Pending taps are consumed on read.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"events": [
|
||||||
|
{"client_id": 16, "kind": "single", "age_ms": 4}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Deadzone
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/deadzone?client_id=0
|
||||||
|
POST /api/deadzone
|
||||||
|
Content-Type: application/json
|
||||||
|
{"write": true, "deadzone": 128, "client_id": 0}
|
||||||
|
```
|
||||||
|
|
||||||
|
With `all_clients` + `slaves_only`: push to ESP-NOW slaves only (master BMA456 unchanged).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"deadzone": 128, "client_id": 0, "success": true, "slaves_updated": 2}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Unicast test
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/unicast-test
|
||||||
|
Content-Type: application/json
|
||||||
|
{"client_id": 16, "seq": 42}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Echo ping (ESP-NOW round-trip latency)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/echo-ping
|
||||||
|
Content-Type: application/json
|
||||||
|
{"client_id": 16}
|
||||||
|
```
|
||||||
|
|
||||||
|
`client_id` must be a registered slave id (`> 0`). The host sends a microsecond timestamp; the master forwards it over ESP-NOW and the slave echoes it back unchanged.
|
||||||
|
|
||||||
|
**Flow:** Host → UART → `cmd_espnow_echo_ping` → `ESPNOW_ECHO_PING` (with `master_time_us` from `esp_timer_get_time()`) → Slave → `ESPNOW_ECHO_PONG` → Master `recv_cb` → UART response.
|
||||||
|
|
||||||
|
**Response fields:**
|
||||||
|
|
||||||
|
| Field | Unit | Meaning |
|
||||||
|
|-------|------|---------|
|
||||||
|
| `success` | — | `true` if pong received within 500 ms |
|
||||||
|
| `client_id` | — | Echo of request |
|
||||||
|
| `timestamp_us` | µs (Unix) | Echoed host timestamp; must match request on success |
|
||||||
|
| `rtt_ms` | ms | **Host-side** round-trip: goTool send → UART response (full chain incl. USB serial) |
|
||||||
|
| `esp_rtt_us` | µs | **Master-side** ESP-NOW only: `esp_timer_get_time()` delta from ping send to pong recv |
|
||||||
|
|
||||||
|
`esp_rtt_us` is the raw firmware value (microseconds, not converted). The web dashboard displays it converted to milliseconds for readability (`esp_rtt_us / 1000`, 3 decimal places). The success banner stays visible for at least 5 seconds.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"client_id": 16,
|
||||||
|
"timestamp_us": 1717654321123456,
|
||||||
|
"rtt_ms": 49.729,
|
||||||
|
"esp_rtt_us": 18234
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
On failure (`success: false`), `esp_rtt_us` is omitted; `rtt_ms` still reflects the host round-trip. HTTP 503 if UART exchange fails (e.g. timeout, client not in registry).
|
||||||
|
|
||||||
|
**CLI:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 echo-ping -client 16
|
||||||
|
```
|
||||||
|
|
||||||
|
Example output:
|
||||||
|
|
||||||
|
```
|
||||||
|
echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234
|
||||||
|
```
|
||||||
|
|
||||||
|
### Find me
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/find-me
|
||||||
|
Content-Type: application/json
|
||||||
|
{"client_id": 16}
|
||||||
|
```
|
||||||
|
|
||||||
|
`client_id` `0` = master LED ring.
|
||||||
|
|
||||||
|
### Restart
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/restart
|
||||||
|
Content-Type: application/json
|
||||||
|
{"client_id": 16}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Log level (master ESP-IDF console)
|
||||||
|
|
||||||
|
Runtime get/set of the **global** log filter on the master (`esp_log_level_set("*", …)`). Output goes to **UART0** (USB debug, 115200), not the host protocol UART (GPIO 2/3, 921600).
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/log-level
|
||||||
|
POST /api/log-level
|
||||||
|
Content-Type: application/json
|
||||||
|
{"write": true, "level": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
| `level` | Meaning |
|
||||||
|
|---------|---------|
|
||||||
|
| 0 | None (no log output) |
|
||||||
|
| 1 | Error |
|
||||||
|
| 2 | Warn |
|
||||||
|
| 3 | Info |
|
||||||
|
| 4 | Debug |
|
||||||
|
| 5 | Verbose |
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"success": true, "level": 3}
|
||||||
|
```
|
||||||
|
|
||||||
|
**CLI:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 log-level
|
||||||
|
go run . -port /dev/ttyUSB0 log-level -set -level 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Dashboard: Master card → dropdown **ESP Log-Level** with **Lesen** / **Setzen**.
|
||||||
|
|
||||||
|
### OTA (master UART upload)
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /api/ota
|
||||||
|
Content-Type: multipart/form-data
|
||||||
|
```
|
||||||
|
|
||||||
|
Form field **`firmware`**: binary image, max **2 MiB**.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"success": true, "bytes_written": 123456, "target_slot": 1}
|
||||||
|
```
|
||||||
|
|
||||||
|
Firmware distributes to slaves over ESP-NOW after `OTA_END`. Progress also appears on dashboard WebSocket as `ota_progress` messages.
|
||||||
|
|
||||||
|
CLI equivalent: `go run . -port /dev/ttyUSB0 ota build/powerpod.bin`
|
||||||
|
|
||||||
|
### LED ring and battery
|
||||||
|
|
||||||
|
Same as external API:
|
||||||
|
|
||||||
|
- `POST /api/led-ring`
|
||||||
|
- `GET` / `POST` `/api/battery`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dashboard vs external
|
||||||
|
|
||||||
|
| Feature | Dashboard `:8080` | External `:8081` |
|
||||||
|
|---------|-------------------|------------------|
|
||||||
|
| Client list | Via dashboard WebSocket state / CLI `clients` | WebSocket `list_clients` |
|
||||||
|
| Accel/tap **push stream** | WebSocket state when live-stream on | WebSocket `set_stream` / `set_tap_stream` |
|
||||||
|
| Accel stream enable | REST `PUT .../accel-stream` | WebSocket `set_accel_stream` |
|
||||||
|
| Tap notify | REST `PUT .../tap-notify` | WebSocket `set_tap_notify` |
|
||||||
|
| LED / battery | REST | REST + WebSocket on `:8081` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## UI mapping
|
||||||
|
|
||||||
|
| UI action | REST / CLI |
|
||||||
|
|-----------|------------|
|
||||||
|
| Nur Master deadzone | `POST /api/deadzone` `client_id: 0` or CLI `deadzone -set -client 0` |
|
||||||
|
| Einzelner Slave | `client_id: <id>` |
|
||||||
|
| Alle Slaves deadzone | `all_clients` + `slaves_only` on POST |
|
||||||
|
| Unicast test | `POST /api/unicast-test` |
|
||||||
|
| Echo ping | `POST /api/echo-ping` (per-slave latency; UI shows Host ms + ESP ms) |
|
||||||
|
| Master log level | `GET` / `POST /api/log-level` or CLI `log-level` |
|
||||||
|
| Tap notify S/D/T | `PUT /api/clients/{id}/tap-notify` |
|
||||||
|
| Tap receive (UI) | Live stream + tap notify; see WebSocket doc for external API |
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
# WebSocket API
|
||||||
|
|
||||||
|
External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string).
|
||||||
|
|
||||||
|
Start with `go run . -port /dev/ttyUSB0 serve`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Connection flow
|
||||||
|
|
||||||
|
1. Connect → server sends `hello` (push off; defaults and command list).
|
||||||
|
2. Send JSON commands → one reply per message (`*_status` or `client_list`).
|
||||||
|
3. After `set_stream` with `enable: true`, server may push `input` messages without a prior command.
|
||||||
|
|
||||||
|
Commands and pushes share one socket — always branch on `type`.
|
||||||
|
|
||||||
|
On disconnect, `set_stream` state for that socket is dropped. Firmware settings (`set_input_stream`, `set_tap_notify`) stay on the master until changed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Two layers (firmware vs host)
|
||||||
|
|
||||||
|
|
||||||
|
| Layer | Commands | Effect |
|
||||||
|
| ------------------ | ------------------------------------ | ------------------------------------------------------------------ |
|
||||||
|
| Firmware (ESP-NOW) | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel and/or tap events to the master |
|
||||||
|
| This connection | `set_stream` | Whether you receive push JSON on this socket |
|
||||||
|
|
||||||
|
|
||||||
|
UART polling runs only when at least one connection has `receive_input: true` **and** at least one slave streams input or has tap notify enabled. `set_tap_notify` alone does not enable push — you still need `set_stream`.
|
||||||
|
|
||||||
|
### Push timing (per connection)
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Where | Meaning |
|
||||||
|
| ------------- | -------------------------------------- | ------------------------------------------------------------ |
|
||||||
|
| `interval_ms` | `hello`, `set_stream`, `stream_status` | Minimum ms between `input` pushes on this socket (1 … 10000) |
|
||||||
|
| `pre_fetch` | `set_stream`, `stream_status` | Ms before each push when the host starts the UART cache read |
|
||||||
|
|
||||||
|
|
||||||
|
Global UART poll interval = minimum `interval_ms` among all connections with push enabled.
|
||||||
|
|
||||||
|
Typical sequence:
|
||||||
|
|
||||||
|
1. `list_clients` → slave IDs
|
||||||
|
2. Per slave: `set_input_stream` and/or `set_tap_notify`
|
||||||
|
3. `set_stream` with `"enable": true`
|
||||||
|
4. Read `input` messages; filter by `client_id` in your app (no per-slave filter on the wire)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Push: `input`
|
||||||
|
|
||||||
|
Combines latest accel cache and visible tap state for every slave slot on the master.
|
||||||
|
|
||||||
|
**Success:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "input",
|
||||||
|
"t": 1716900123456789012,
|
||||||
|
"success": true,
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"client_id": 16,
|
||||||
|
"valid": true,
|
||||||
|
"x": 12,
|
||||||
|
"y": -34,
|
||||||
|
"z": 16384,
|
||||||
|
"accel_age_ms": 8,
|
||||||
|
"tap_kind": "single",
|
||||||
|
"tap_age_ms": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"client_id": 42,
|
||||||
|
"valid": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| -------------- | -------------------------------------------------------------------- |
|
||||||
|
| `t` | Unix timestamp in nanoseconds when the host read the cache |
|
||||||
|
| `success` | `true` if `CACHE_STATUS` succeeded |
|
||||||
|
| `clients[]` | One entry per slave slot (includes invalid/stale entries) |
|
||||||
|
| `client_id` | Same id as in `list_clients` |
|
||||||
|
| `valid` | `false` if no accel sample yet or stale; omit `x`/`y`/`z` when false |
|
||||||
|
| `x`, `y`, `z` | Raw accelerometer LSB (BMA456, ±2 g) |
|
||||||
|
| `accel_age_ms` | Ms since the master received this accel sample |
|
||||||
|
| `tap_kind` | `"single"`, `"double"`, or `"triple"`; omit when no recent tap |
|
||||||
|
| `tap_age_ms` | Ms since tap in master cache; omit with `tap_kind` |
|
||||||
|
|
||||||
|
|
||||||
|
Tap events stay visible for `tap_display_min_ms` (2000, in `hello`) after the API first saw them.
|
||||||
|
|
||||||
|
**Failure** (no `clients` array):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"input","t":1716900123456789012,"success":false,"error":"uart busy"}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
One JSON object per message; field `type` selects the command.
|
||||||
|
|
||||||
|
**Errors:** Replies use the matching response `type`. On failure: `success: false` (or omitted) and `"error": "…"`. Malformed JSON or unknown `type` → `stream_status` with `error`.
|
||||||
|
|
||||||
|
### `hello` (server → client)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "hello",
|
||||||
|
"serial_port": "/dev/ttyUSB0",
|
||||||
|
"interval_ms": 16,
|
||||||
|
"pre_fetch_ms": 2,
|
||||||
|
"tap_display_min_ms": 2000,
|
||||||
|
"commands": [
|
||||||
|
"list_clients",
|
||||||
|
"set_stream", "get_stream",
|
||||||
|
"set_input_stream", "get_input_stream",
|
||||||
|
"set_tap_notify", "get_tap_notify",
|
||||||
|
"set_led_ring", "get_battery"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `list_clients`
|
||||||
|
|
||||||
|
Request: `{"type":"list_clients"}`
|
||||||
|
|
||||||
|
Response `client_list`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "client_list",
|
||||||
|
"success": true,
|
||||||
|
"clients": [
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"mac": "aa:bb:cc:dd:ee:10",
|
||||||
|
"available": true,
|
||||||
|
"input_stream": false,
|
||||||
|
"tap_notify_single": false,
|
||||||
|
"tap_notify_double": false,
|
||||||
|
"tap_notify_triple": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Also per client: `version`, `used`, `last_ping`, `last_success_ping`.
|
||||||
|
|
||||||
|
### `set_stream` / `get_stream`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"set_stream","enable":true,"interval_ms":32,"pre_fetch":2}
|
||||||
|
{"type":"get_stream"}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `stream_status`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `set_input_stream` / `get_input_stream` (firmware)
|
||||||
|
|
||||||
|
`client_id` required (> 0).
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"set_input_stream","client_id":16,"enable":true}
|
||||||
|
{"type":"get_input_stream","client_id":16}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `input_stream_status`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `set_tap_notify` / `get_tap_notify` (firmware)
|
||||||
|
|
||||||
|
Set requires `single`, `double_tap`, `triple` per client, or `"all_clients": true` for broadcast.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"set_tap_notify","client_id":16,"single":true,"double_tap":false,"triple":false}
|
||||||
|
{"type":"get_tap_notify","client_id":16}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `tap_notify_status`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `set_led_ring`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
|
||||||
|
{"type":"set_led_ring","mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
|
||||||
|
{"type":"set_led_ring","mode":"find-me","all_clients":true,"slaves_only":true}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
| Request `mode` | Notes |
|
||||||
|
| -------------- | --------------------------- |
|
||||||
|
| `clear` | Turn off |
|
||||||
|
| `color` | Full ring RGB + `intensity` |
|
||||||
|
| `progress` | `progress` 0–100 |
|
||||||
|
| `digit` | `digit` 0–10 |
|
||||||
|
| `blink` | `blink_ms`, `blink_count` |
|
||||||
|
| `find-me` | Locate pod |
|
||||||
|
| `battery-low` | UV indicator (4 LEDs, 5 s) |
|
||||||
|
|
||||||
|
|
||||||
|
Target: `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`).
|
||||||
|
|
||||||
|
Response `led_ring_status` — `mode` is numeric: 0=clear, 1=progress, 2=digit, 3=blink, 4=find-me, 5=color, 6=battery-low.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"led_ring_status","success":true,"mode":5,"client_id":16,"slaves_updated":1}
|
||||||
|
```
|
||||||
|
|
||||||
|
### `get_battery`
|
||||||
|
|
||||||
|
Slaves push battery every 30 s; this reads the master cache. Default: all clients.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"type":"get_battery","all_clients":true}
|
||||||
|
{"type":"get_battery","client_id":16}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response `battery_status`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "battery_status",
|
||||||
|
"success": true,
|
||||||
|
"samples": [
|
||||||
|
{
|
||||||
|
"client_id": 16,
|
||||||
|
"lipo1": {"valid": true, "voltage_mv": 3850, "percent": 71},
|
||||||
|
"lipo2": {"valid": false},
|
||||||
|
"age_ms": 1200
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"powerpod/gotool/pb"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ledRingModeClear = 0
|
||||||
|
ledRingModeProgress = 1
|
||||||
|
ledRingModeDigit = 2
|
||||||
|
ledRingModeBlink = 3
|
||||||
|
ledRingModeFindMe = 4
|
||||||
|
ledRingModeColor = 5
|
||||||
|
ledRingModeBatteryLow = 6
|
||||||
|
)
|
||||||
|
|
||||||
|
type ledRingAPIRequest struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
ClientID uint32 `json:"client_id"`
|
||||||
|
AllClients bool `json:"all_clients"`
|
||||||
|
SlavesOnly bool `json:"slaves_only"`
|
||||||
|
Progress uint32 `json:"progress"`
|
||||||
|
Digit uint32 `json:"digit"`
|
||||||
|
R uint32 `json:"r"`
|
||||||
|
G uint32 `json:"g"`
|
||||||
|
B uint32 `json:"b"`
|
||||||
|
Intensity uint32 `json:"intensity"`
|
||||||
|
BlinkMs uint32 `json:"blink_ms"`
|
||||||
|
BlinkCount uint32 `json:"blink_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ledRingAPIResponse struct {
|
||||||
|
Type string `json:"type,omitempty"` // led_ring_status (WebSocket)
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Mode uint32 `json:"mode,omitempty"`
|
||||||
|
Progress uint32 `json:"progress,omitempty"`
|
||||||
|
Digit uint32 `json:"digit,omitempty"`
|
||||||
|
ClientID uint32 `json:"client_id,omitempty"`
|
||||||
|
SlavesUpdated uint32 `json:"slaves_updated,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledRingModeFromString(s string) (uint32, error) {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(s)) {
|
||||||
|
case "clear", "":
|
||||||
|
return ledRingModeClear, nil
|
||||||
|
case "color", "solid", "fill":
|
||||||
|
return ledRingModeColor, nil
|
||||||
|
case "progress":
|
||||||
|
return ledRingModeProgress, nil
|
||||||
|
case "digit":
|
||||||
|
return ledRingModeDigit, nil
|
||||||
|
case "blink":
|
||||||
|
return ledRingModeBlink, nil
|
||||||
|
case "find-me", "find_me", "findme":
|
||||||
|
return ledRingModeFindMe, nil
|
||||||
|
case "battery-low", "battery_low", "batterylow":
|
||||||
|
return ledRingModeBatteryLow, nil
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown mode %q (clear, color, progress, digit, blink, find-me, battery-low)", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ledRingPBFromAPI(in ledRingAPIRequest) (*pb.LedRingProgressRequest, error) {
|
||||||
|
mode, err := ledRingModeFromString(in.Mode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &pb.LedRingProgressRequest{
|
||||||
|
Mode: mode,
|
||||||
|
Progress: in.Progress,
|
||||||
|
Digit: in.Digit,
|
||||||
|
R: in.R,
|
||||||
|
G: in.G,
|
||||||
|
B: in.B,
|
||||||
|
Intensity: in.Intensity,
|
||||||
|
BlinkMs: in.BlinkMs,
|
||||||
|
BlinkCount: in.BlinkCount,
|
||||||
|
ClientId: in.ClientID,
|
||||||
|
AllClients: in.AllClients,
|
||||||
|
SlavesOnly: in.SlavesOnly,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyLedRing(link *managedSerial, in ledRingAPIRequest) ledRingAPIResponse {
|
||||||
|
req, err := ledRingPBFromAPI(in)
|
||||||
|
if err != nil {
|
||||||
|
return ledRingAPIResponse{Error: err.Error()}
|
||||||
|
}
|
||||||
|
resp, err := link.LedRing(req)
|
||||||
|
if err != nil {
|
||||||
|
return ledRingAPIResponse{Error: err.Error()}
|
||||||
|
}
|
||||||
|
out := ledRingAPIResponse{
|
||||||
|
Success: resp.GetSuccess(),
|
||||||
|
Mode: resp.GetMode(),
|
||||||
|
Progress: resp.GetProgress(),
|
||||||
|
Digit: resp.GetDigit(),
|
||||||
|
ClientID: resp.GetClientId(),
|
||||||
|
SlavesUpdated: resp.GetSlavesUpdated(),
|
||||||
|
}
|
||||||
|
if !out.Success && out.Error == "" {
|
||||||
|
out.Error = "led ring command rejected"
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
+16
-2
@@ -16,14 +16,18 @@ func usage() {
|
|||||||
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
|
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
|
||||||
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
|
fmt.Fprintf(os.Stderr, " clients registered ESP-NOW slaves on the master\n")
|
||||||
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
|
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one UART round-trip)\n")
|
||||||
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
|
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " echo-ping ESP-NOW timestamp echo round-trip to one slave\n")
|
||||||
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
|
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
|
||||||
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
|
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n")
|
||||||
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n")
|
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n")
|
||||||
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
|
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
|
||||||
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n")
|
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n")
|
||||||
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
|
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
|
||||||
fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n\n")
|
fmt.Fprintf(os.Stderr, " restart reboot master or slave (ESP-NOW)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " log-level get/set master ESP-IDF log level (global)\n\n")
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,7 @@ func main() {
|
|||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
runErr = runServe(*portName, *baud, flag.Args()[1:])
|
runErr = runServe(*portName, *baud, flag.Args()[1:])
|
||||||
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress":
|
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
|
||||||
if *portName == "" {
|
if *portName == "" {
|
||||||
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
|
||||||
usage()
|
usage()
|
||||||
@@ -60,6 +64,8 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("open serial: %v", err)
|
log.Fatalf("open serial: %v", err)
|
||||||
}
|
}
|
||||||
|
registerShutdown(func() { _ = sp.Close() })
|
||||||
|
enableShutdownOnInterrupt()
|
||||||
defer sp.Close()
|
defer sp.Close()
|
||||||
switch cmd {
|
switch cmd {
|
||||||
case "version":
|
case "version":
|
||||||
@@ -68,14 +74,22 @@ func main() {
|
|||||||
runErr = runClients(sp)
|
runErr = runClients(sp)
|
||||||
case "deadzone", "accel-deadzone":
|
case "deadzone", "accel-deadzone":
|
||||||
runErr = runDeadzone(sp, flag.Args()[1:])
|
runErr = runDeadzone(sp, flag.Args()[1:])
|
||||||
|
case "tap-notify", "tap_notify":
|
||||||
|
runErr = runTapNotify(sp, flag.Args()[1:])
|
||||||
|
case "cache-status", "cache_status":
|
||||||
|
runErr = runCacheStatus(sp)
|
||||||
case "unicast-test", "unicast_test":
|
case "unicast-test", "unicast_test":
|
||||||
runErr = runUnicastTest(sp, flag.Args()[1:])
|
runErr = runUnicastTest(sp, flag.Args()[1:])
|
||||||
|
case "echo-ping", "echo_ping":
|
||||||
|
runErr = runEchoPing(sp, flag.Args()[1:])
|
||||||
case "led-ring", "led_ring":
|
case "led-ring", "led_ring":
|
||||||
runErr = runLedRing(sp, flag.Args()[1:])
|
runErr = runLedRing(sp, flag.Args()[1:])
|
||||||
case "find-me", "find_me":
|
case "find-me", "find_me":
|
||||||
runErr = runFindMe(sp, flag.Args()[1:])
|
runErr = runFindMe(sp, flag.Args()[1:])
|
||||||
case "restart":
|
case "restart":
|
||||||
runErr = runRestart(sp, flag.Args()[1:])
|
runErr = runRestart(sp, flag.Args()[1:])
|
||||||
|
case "log-level", "log_level":
|
||||||
|
runErr = runLogLevel(sp, flag.Args()[1:])
|
||||||
case "ota":
|
case "ota":
|
||||||
runErr = runOTA(sp, flag.Args()[1:])
|
runErr = runOTA(sp, flag.Args()[1:])
|
||||||
case "ota-progress", "ota_progress":
|
case "ota-progress", "ota_progress":
|
||||||
|
|||||||
+152
-66
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
@@ -21,6 +20,9 @@ const (
|
|||||||
otaDistQueryInterval = 500 * time.Millisecond
|
otaDistQueryInterval = 500 * time.Millisecond
|
||||||
otaDistQueryTimeout = 2 * time.Second
|
otaDistQueryTimeout = 2 * time.Second
|
||||||
otaDistEmitMinInterval = 150 * time.Millisecond
|
otaDistEmitMinInterval = 150 * time.Millisecond
|
||||||
|
// Pace host chunks so the master UART RX ring is not overrun (~20 frames/block).
|
||||||
|
otaHostChunkPace = 3 * time.Millisecond
|
||||||
|
otaBlockMaxRetries = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -69,16 +71,45 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
|
func runOTAUpload(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
|
||||||
|
push := func(phase, msg string) {
|
||||||
|
if onProgress == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
onProgress(OTAProgress{
|
||||||
|
Type: "ota_progress", Phase: phase, Step: otaStepMaster,
|
||||||
|
Percent: 0, Message: msg, MasterMessage: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
push("preparing", "UART wird vorbereitet…")
|
||||||
|
|
||||||
|
// Block until the UART is free, then hold m.mu for the entire upload so
|
||||||
|
// dashboard/API polling cannot interleave on the serial port.
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
if m.otaActive {
|
||||||
err := runOTAOnPortUnlocked(m, firmware, onProgress)
|
m.mu.Unlock()
|
||||||
|
return errOTAInProgress
|
||||||
|
}
|
||||||
|
m.otaActive = true
|
||||||
|
if m.sp == nil {
|
||||||
|
if err := m.openLocked(); err != nil {
|
||||||
|
m.otaActive = false
|
||||||
|
m.mu.Unlock()
|
||||||
|
push("error", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sp := m.sp
|
||||||
|
|
||||||
|
err := runOTAOnPortUnlocked(sp, firmware, onProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.invalidateLocked(err)
|
m.invalidateLocked(err)
|
||||||
}
|
}
|
||||||
|
m.otaActive = false
|
||||||
|
m.mu.Unlock()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgressFn) error {
|
func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgressFn) error {
|
||||||
if len(firmware) == 0 {
|
if len(firmware) == 0 {
|
||||||
return fmt.Errorf("empty firmware")
|
return fmt.Errorf("empty firmware")
|
||||||
}
|
}
|
||||||
@@ -120,32 +151,31 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
|||||||
onProgress(p)
|
onProgress(p)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.sp == nil {
|
if err := sp.port.SetReadTimeout(readTimeout); err != nil {
|
||||||
if err := m.openLocked(); err != nil {
|
|
||||||
notify("error", "", 0, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sp := m.sp
|
|
||||||
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
|
|
||||||
notify("error", "", 0, err.Error())
|
notify("error", "", 0, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer sp.port.SetReadTimeout(readTimeout)
|
|
||||||
|
|
||||||
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
|
notify("preparing", otaStepMaster, 0, fmt.Sprintf("Master: OTA start (%d bytes)…", imageSize))
|
||||||
|
|
||||||
|
flushSerialInput(sp)
|
||||||
|
|
||||||
if err := writeUartMessage(sp, &pb.UartMessage{
|
if err := writeUartMessage(sp, &pb.UartMessage{
|
||||||
Type: pb.MessageType_OTA_START,
|
Type: pb.MessageType_OTA_START,
|
||||||
Payload: &pb.UartMessage_OtaStart{
|
Payload: &pb.UartMessage_OtaStart{
|
||||||
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
|
OtaStart: &pb.OtaStartPayload{TotalSize: uint32(imageSize)},
|
||||||
},
|
},
|
||||||
}, false); err != nil {
|
}); err != nil {
|
||||||
notify("error", "", 0, err.Error())
|
notify("error", "", 0, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := sp.port.SetReadTimeout(otaPrepareTimeout); err != nil {
|
||||||
|
notify("error", "", 0, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = sp.port.SetReadTimeout(readTimeout) }()
|
||||||
|
|
||||||
ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) {
|
ready, err := waitOtaStatus(sp, otaStReady, otaPrepareTimeout, func(msg string) {
|
||||||
notify("preparing", otaStepMaster, 2, msg)
|
notify("preparing", otaStepMaster, 2, msg)
|
||||||
})
|
})
|
||||||
@@ -162,52 +192,83 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
|||||||
|
|
||||||
var seq uint32
|
var seq uint32
|
||||||
for offset := 0; offset < imageSize; {
|
for offset := 0; offset < imageSize; {
|
||||||
bytesInBlock := 0
|
blockStart := offset
|
||||||
for bytesInBlock < otaFlashBlockSize && offset < imageSize {
|
blockStartSeq := seq
|
||||||
n := otaHostChunkSize
|
|
||||||
room := otaFlashBlockSize - bytesInBlock
|
|
||||||
if n > room {
|
|
||||||
n = room
|
|
||||||
}
|
|
||||||
if offset+n > imageSize {
|
|
||||||
n = imageSize - offset
|
|
||||||
}
|
|
||||||
chunk := firmware[offset : offset+n]
|
|
||||||
|
|
||||||
if err := writeUartMessage(sp, &pb.UartMessage{
|
sendBlock := func() (fullBlock bool, err error) {
|
||||||
Type: pb.MessageType_OTA_PAYLOAD,
|
bytesInBlock := 0
|
||||||
Payload: &pb.UartMessage_OtaPayload{
|
for bytesInBlock < otaFlashBlockSize && offset < imageSize {
|
||||||
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
n := otaHostChunkSize
|
||||||
},
|
room := otaFlashBlockSize - bytesInBlock
|
||||||
}, false); err != nil {
|
if n > room {
|
||||||
notify("error", "", 0, err.Error())
|
n = room
|
||||||
return err
|
}
|
||||||
}
|
if offset+n > imageSize {
|
||||||
seq++
|
n = imageSize - offset
|
||||||
offset += n
|
}
|
||||||
bytesInBlock += n
|
chunk := firmware[offset : offset+n]
|
||||||
|
|
||||||
pct := offset * 100 / imageSize
|
if err := writeUartMessage(sp, &pb.UartMessage{
|
||||||
if pct > 99 {
|
Type: pb.MessageType_OTA_PAYLOAD,
|
||||||
pct = 99
|
Payload: &pb.UartMessage_OtaPayload{
|
||||||
|
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
time.Sleep(otaHostChunkPace)
|
||||||
|
seq++
|
||||||
|
offset += n
|
||||||
|
bytesInBlock += n
|
||||||
|
|
||||||
|
pct := offset * 100 / imageSize
|
||||||
|
if pct > 99 {
|
||||||
|
pct = 99
|
||||||
|
}
|
||||||
|
notify("uploading", otaStepMaster, pct,
|
||||||
|
fmt.Sprintf("Master: %d / %d bytes", offset, imageSize))
|
||||||
}
|
}
|
||||||
notify("uploading", otaStepMaster, pct, fmt.Sprintf("Master: %d / %d bytes", offset, imageSize))
|
return bytesInBlock == otaFlashBlockSize, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytesInBlock == otaFlashBlockSize {
|
fullBlock, err := sendBlock()
|
||||||
st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
|
if err != nil {
|
||||||
if err != nil {
|
notify("error", "", 0, err.Error())
|
||||||
notify("error", "", 0, err.Error())
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
pct := offset * 100 / imageSize
|
|
||||||
if pct > 99 {
|
|
||||||
pct = 99
|
|
||||||
}
|
|
||||||
notify("uploading", otaStepMaster, pct,
|
|
||||||
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
|
|
||||||
OTAProgress{Bytes: st.GetBytesWritten()})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !fullBlock {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var st *pb.OtaStatusPayload
|
||||||
|
var ackErr error
|
||||||
|
for attempt := 0; attempt < otaBlockMaxRetries; attempt++ {
|
||||||
|
if attempt > 0 {
|
||||||
|
offset = blockStart
|
||||||
|
seq = blockStartSeq
|
||||||
|
if _, err := sendBlock(); err != nil {
|
||||||
|
notify("error", "", 0, err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
st, ackErr = waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
|
||||||
|
if ackErr == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ackErr != nil {
|
||||||
|
notify("error", "", 0, ackErr.Error())
|
||||||
|
return ackErr
|
||||||
|
}
|
||||||
|
|
||||||
|
pct := offset * 100 / imageSize
|
||||||
|
if pct > 99 {
|
||||||
|
pct = 99
|
||||||
|
}
|
||||||
|
notify("uploading", otaStepMaster, pct,
|
||||||
|
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
|
||||||
|
OTAProgress{Bytes: st.GetBytesWritten()})
|
||||||
}
|
}
|
||||||
|
|
||||||
masterPct = 100
|
masterPct = 100
|
||||||
@@ -219,7 +280,7 @@ func runOTAOnPortUnlocked(m *managedSerial, firmware []byte, onProgress otaProgr
|
|||||||
Payload: &pb.UartMessage_OtaEnd{
|
Payload: &pb.UartMessage_OtaEnd{
|
||||||
OtaEnd: &pb.OtaEndPayload{},
|
OtaEnd: &pb.OtaEndPayload{},
|
||||||
},
|
},
|
||||||
}, false); err != nil {
|
}); err != nil {
|
||||||
notify("error", "", 0, err.Error())
|
notify("error", "", 0, err.Error())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -333,7 +394,7 @@ func queryOtaSlaveProgressLocked(sp *serialPort, clientID uint32,
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
if err := writeUartMessage(sp, req, false); err != nil {
|
if err := writeUartMessage(sp, req); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if queryTimeout <= 0 {
|
if queryTimeout <= 0 {
|
||||||
@@ -389,7 +450,7 @@ func readUartMessageUntil(sp *serialPort, deadline time.Time, want pb.MessageTyp
|
|||||||
if err := sp.port.SetReadTimeout(wait); err != nil {
|
if err := sp.port.SetReadTimeout(wait); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
payload, err := uartframe.ReadFrame(sp.port, nil)
|
payload, err := uartframe.ReadFrame(sp.port, nil, wait)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -487,14 +548,11 @@ func waitOtaComplete(sp *serialPort, timeout time.Duration,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeUartMessage(sp *serialPort, msg *pb.UartMessage, logFrame bool) error {
|
func writeUartMessage(sp *serialPort, msg *pb.UartMessage) error {
|
||||||
frame, err := encodeUartMessage(msg)
|
frame, err := encodeUartMessage(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if logFrame {
|
|
||||||
log.Printf("sending %s (%d frame bytes)", msg.Type, len(frame))
|
|
||||||
}
|
|
||||||
_, err = sp.port.Write(frame)
|
_, err = sp.port.Write(frame)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -505,12 +563,24 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
|||||||
if time.Now().After(deadline) {
|
if time.Now().After(deadline) {
|
||||||
return nil, fmt.Errorf("timeout waiting for OTA status %d", want)
|
return nil, fmt.Errorf("timeout waiting for OTA status %d", want)
|
||||||
}
|
}
|
||||||
if err := sp.port.SetReadTimeout(time.Until(deadline)); err != nil {
|
readWait := time.Until(deadline)
|
||||||
|
if readWait > otaStatusPollTimeout {
|
||||||
|
readWait = otaStatusPollTimeout
|
||||||
|
}
|
||||||
|
if err := sp.port.SetReadTimeout(readWait); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
st, err := readOtaStatus(sp)
|
payload, err := uartframe.ReadFrame(sp.port, nil, readWait)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
continue
|
||||||
|
}
|
||||||
|
msg, err := decodeUartPayload(payload)
|
||||||
|
if err != nil || msg.GetType() != pb.MessageType_OTA_STATUS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
st := msg.GetOtaStatus()
|
||||||
|
if st == nil {
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
switch st.GetStatus() {
|
switch st.GetStatus() {
|
||||||
case want:
|
case want:
|
||||||
@@ -526,7 +596,7 @@ func waitOtaStatus(sp *serialPort, want uint32, timeout time.Duration, onPrepari
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
|
func readOtaStatus(sp *serialPort) (*pb.OtaStatusPayload, error) {
|
||||||
payload, err := uartframe.ReadFrame(sp.port, nil)
|
payload, err := uartframe.ReadFrame(sp.port, nil, readTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read response: %w", err)
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
}
|
}
|
||||||
@@ -553,6 +623,22 @@ func encodeUartMessage(msg *pb.UartMessage) ([]byte, error) {
|
|||||||
return uartframe.EncodeFrame(payload)
|
return uartframe.EncodeFrame(payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flushSerialInput drops stale RX bytes (not full frames — avoids ReadFrame blocking).
|
||||||
|
func flushSerialInput(sp *serialPort) {
|
||||||
|
if sp == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = sp.port.SetReadTimeout(10 * time.Millisecond)
|
||||||
|
buf := make([]byte, 256)
|
||||||
|
deadline := time.Now().Add(50 * time.Millisecond)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
n, err := sp.port.Read(buf)
|
||||||
|
if n == 0 || err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
|
func decodeUartPayload(payload []byte) (*pb.UartMessage, error) {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil, fmt.Errorf("empty response")
|
return nil, fmt.Errorf("empty response")
|
||||||
|
|||||||
+1832
-117
File diff suppressed because it is too large
Load Diff
+33
-12
@@ -11,14 +11,18 @@ import (
|
|||||||
// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug).
|
// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug).
|
||||||
var errUARTBusy = errors.New("uart busy (OTA in progress)")
|
var errUARTBusy = errors.New("uart busy (OTA in progress)")
|
||||||
|
|
||||||
|
// errOTAInProgress is returned when a second OTA upload is attempted while one is running.
|
||||||
|
var errOTAInProgress = errors.New("OTA upload already in progress")
|
||||||
|
|
||||||
// managedSerial keeps the UART open and reconnects after I/O failures or unplug.
|
// managedSerial keeps the UART open and reconnects after I/O failures or unplug.
|
||||||
type managedSerial struct {
|
type managedSerial struct {
|
||||||
portName string
|
portName string
|
||||||
baud int
|
baud int
|
||||||
quiet bool
|
quiet bool
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sp *serialPort
|
sp *serialPort
|
||||||
|
otaActive bool // UART held for firmware upload; poll/API must not interleave
|
||||||
}
|
}
|
||||||
|
|
||||||
func newManagedSerial(portName string, baud int) *managedSerial {
|
func newManagedSerial(portName string, baud int) *managedSerial {
|
||||||
@@ -76,20 +80,17 @@ func (m *managedSerial) withPort(fn func(*serialPort) error) error {
|
|||||||
return m.withPortLocked(false, fn)
|
return m.withPortLocked(false, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
// withPortPoll is like withPort but returns errUARTBusy instead of blocking during OTA.
|
// withPortPoll is like withPort but returns errUARTBusy during OTA (no TryLock race).
|
||||||
func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error {
|
func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error {
|
||||||
return m.withPortLocked(true, fn)
|
return m.withPortLocked(true, fn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) error {
|
func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error {
|
||||||
if try {
|
m.mu.Lock()
|
||||||
if !m.mu.TryLock() {
|
|
||||||
return errUARTBusy
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
m.mu.Lock()
|
|
||||||
}
|
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
if m.otaActive {
|
||||||
|
return errUARTBusy
|
||||||
|
}
|
||||||
|
|
||||||
if m.sp == nil {
|
if m.sp == nil {
|
||||||
if err := m.openLocked(); err != nil {
|
if err := m.openLocked(); err != nil {
|
||||||
@@ -104,6 +105,26 @@ func (m *managedSerial) withPortLocked(try bool, fn func(*serialPort) error) err
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *managedSerial) recoverAfterMasterRestart() {
|
||||||
|
const bootWait = 6 * time.Second
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
m.closeLocked()
|
||||||
|
m.mu.Unlock()
|
||||||
|
|
||||||
|
log.Printf("UART: master restart — waiting %s for boot", bootWait)
|
||||||
|
time.Sleep(bootWait)
|
||||||
|
|
||||||
|
m.mu.Lock()
|
||||||
|
defer m.mu.Unlock()
|
||||||
|
if err := m.openLocked(); err != nil {
|
||||||
|
log.Printf("UART reconnect after master restart: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
flushSerialInput(m.sp)
|
||||||
|
log.Printf("UART %s ready after master restart", m.portName)
|
||||||
|
}
|
||||||
|
|
||||||
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
|
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
|
||||||
return m.exchangePayloadVia(m.withPort, payload, cmdName)
|
return m.exchangePayloadVia(m.withPort, payload, cmdName)
|
||||||
}
|
}
|
||||||
@@ -119,7 +140,7 @@ func (m *managedSerial) exchangePayloadVia(
|
|||||||
var resp []byte
|
var resp []byte
|
||||||
err := portFn(func(sp *serialPort) error {
|
err := portFn(func(sp *serialPort) error {
|
||||||
var e error
|
var e error
|
||||||
resp, e = sp.exchangePayloadLocked(payload, cmdName)
|
resp, e = sp.exchangePayloadLocked(payload, cmdName, readTimeout)
|
||||||
return e
|
return e
|
||||||
})
|
})
|
||||||
return resp, err
|
return resp, err
|
||||||
|
|||||||
+21
-4
@@ -12,6 +12,9 @@ import (
|
|||||||
|
|
||||||
const readTimeout = 3 * time.Second
|
const readTimeout = 3 * time.Second
|
||||||
|
|
||||||
|
// batteryReadTimeout: master may query each slave over ESP-NOW (~400 ms each).
|
||||||
|
const batteryReadTimeout = 12 * time.Second
|
||||||
|
|
||||||
type serialPort struct {
|
type serialPort struct {
|
||||||
port serial.Port
|
port serial.Port
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -44,10 +47,16 @@ func (s *serialPort) Close() error {
|
|||||||
func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
|
func (s *serialPort) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
return s.exchangePayloadLocked(payload, cmdName)
|
return s.exchangePayloadLocked(payload, cmdName, readTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string) ([]byte, error) {
|
func (s *serialPort) exchangePayloadForBattery(payload []byte, cmdName string) ([]byte, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.exchangePayloadLocked(payload, cmdName, batteryReadTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string, timeout time.Duration) ([]byte, error) {
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
return nil, fmt.Errorf("empty payload")
|
return nil, fmt.Errorf("empty payload")
|
||||||
}
|
}
|
||||||
@@ -63,7 +72,15 @@ func (s *serialPort) exchangePayloadLocked(payload []byte, cmdName string) ([]by
|
|||||||
return nil, fmt.Errorf("write: %w", err)
|
return nil, fmt.Errorf("write: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
respPayload, err := uartframe.ReadFrame(s.port, nil)
|
if timeout <= 0 {
|
||||||
|
timeout = readTimeout
|
||||||
|
}
|
||||||
|
if err := s.port.SetReadTimeout(timeout); err != nil {
|
||||||
|
return nil, fmt.Errorf("set read timeout: %w", err)
|
||||||
|
}
|
||||||
|
defer func() { _ = s.port.SetReadTimeout(readTimeout) }()
|
||||||
|
|
||||||
|
respPayload, err := uartframe.ReadFrame(s.port, nil, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read response: %w", err)
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
}
|
}
|
||||||
@@ -96,7 +113,7 @@ func (s *serialPort) exchangeLocked(cmdID byte, cmdName string) ([]byte, error)
|
|||||||
return nil, fmt.Errorf("write: %w", err)
|
return nil, fmt.Errorf("write: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := uartframe.ReadFrame(s.port, nil)
|
payload, err := uartframe.ReadFrame(s.port, nil, readTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("read response: %w", err)
|
return nil, fmt.Errorf("read response: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// tapNotifyCtl tracks which slaves have tap notify enabled (mirrors firmware / dashboard).
|
||||||
|
type tapNotifyCtl struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
flags map[uint32]tapNotifyFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
type tapNotifyFlags struct {
|
||||||
|
single bool
|
||||||
|
doubleTap bool
|
||||||
|
triple bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTapNotifyCtl() *tapNotifyCtl {
|
||||||
|
return &tapNotifyCtl{flags: make(map[uint32]tapNotifyFlags)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tapNotifyCtl) Set(clientID uint32, single, doubleTap, triple bool) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
if !single && !doubleTap && !triple {
|
||||||
|
delete(c.flags, clientID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.flags[clientID] = tapNotifyFlags{single: single, doubleTap: doubleTap, triple: triple}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tapNotifyCtl) Any() bool {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
return len(c.flags) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *tapNotifyCtl) SyncFromClients(clients []ClientView) {
|
||||||
|
c.mu.Lock()
|
||||||
|
defer c.mu.Unlock()
|
||||||
|
c.flags = make(map[uint32]tapNotifyFlags)
|
||||||
|
for _, cl := range clients {
|
||||||
|
if cl.TapNotifySingle || cl.TapNotifyDouble || cl.TapNotifyTriple {
|
||||||
|
c.flags[cl.ID] = tapNotifyFlags{
|
||||||
|
single: cl.TapNotifySingle,
|
||||||
|
doubleTap: cl.TapNotifyDouble,
|
||||||
|
triple: cl.TapNotifyTriple,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+1
-1
@@ -44,7 +44,7 @@ Ordered steps: UART commands, delays, or esptool reset.
|
|||||||
**unicast_test** — `input`: `slave` or `client_id`, `seq`
|
**unicast_test** — `input`: `slave` or `client_id`, `seq`
|
||||||
`expect`: `success`, `seq`
|
`expect`: `success`, `seq`
|
||||||
|
|
||||||
**led_ring** — `input`: `mode` (`clear`, `progress`, `digit`, `blink`, `find_me`), `progress`, `digit`, `r`/`g`/`b`, `intensity`, `blink_ms`, `blink_count`
|
**led_ring** — `input`: `mode` (`clear`, `progress`, `digit`, `blink`, `find_me`, `battery_low`), `progress`, `digit`, `r`/`g`/`b`, `intensity`, `blink_ms`, `blink_count`
|
||||||
`expect`: `success`, `mode`, `progress`, `digit`
|
`expect`: `success`, `mode`, `progress`, `digit`
|
||||||
|
|
||||||
**find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring)
|
**find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring)
|
||||||
|
|||||||
+13
-2
@@ -4,12 +4,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
StartMarker = 0xAA
|
StartMarker = 0xAA
|
||||||
StopMarker = 0xCC
|
StopMarker = 0xCC
|
||||||
MaxPayload = 252
|
// Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4).
|
||||||
|
MaxPayload = 248
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -97,13 +99,22 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ReadFrame reads bytes from r until one full frame is parsed or an error occurs.
|
// ReadFrame reads bytes from r until one full frame is parsed or an error occurs.
|
||||||
func ReadFrame(r io.Reader, buf []byte) ([]byte, error) {
|
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
|
||||||
|
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
buf = make([]byte, 256)
|
buf = make([]byte, 256)
|
||||||
}
|
}
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
|
|
||||||
|
var deadline time.Time
|
||||||
|
if maxWait > 0 {
|
||||||
|
deadline = time.Now().Add(maxWait)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
if !deadline.IsZero() && !time.Now().Before(deadline) {
|
||||||
|
return nil, ErrTimeout
|
||||||
|
}
|
||||||
n, err := r.Read(buf)
|
n, err := r.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
for i := 0; i < n; i++ {
|
for i := 0; i < n; i++ {
|
||||||
|
|||||||
+689
-13
@@ -55,11 +55,31 @@
|
|||||||
.badge-offline { background: #5c6570; color: #f0f3f5; }
|
.badge-offline { background: #5c6570; color: #f0f3f5; }
|
||||||
.badge.bg-secondary { background: #4a5560 !important; color: #f0f3f5; }
|
.badge.bg-secondary { background: #4a5560 !important; color: #f0f3f5; }
|
||||||
|
|
||||||
.mac {
|
.mac, .accel {
|
||||||
font-family: ui-monospace, monospace;
|
font-family: ui-monospace, monospace;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--pp-accent);
|
color: var(--pp-accent);
|
||||||
}
|
}
|
||||||
|
.accel-stale { color: var(--pp-text-muted); }
|
||||||
|
|
||||||
|
.tap-toggle {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.15rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--pp-text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.tap-toggle input { margin: 0; }
|
||||||
|
.tap-hit {
|
||||||
|
color: #ffd166;
|
||||||
|
font-weight: 600;
|
||||||
|
animation: tap-flash 2s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes tap-flash {
|
||||||
|
from { color: #fff; transform: scale(1.08); }
|
||||||
|
to { color: #ffd166; transform: scale(1); }
|
||||||
|
}
|
||||||
|
|
||||||
.pp-table {
|
.pp-table {
|
||||||
--bs-table-color: var(--pp-text);
|
--bs-table-color: var(--pp-text);
|
||||||
@@ -199,6 +219,12 @@
|
|||||||
<dd class="col-7" x-text="state.master.running_partition || '—'"></dd>
|
<dd class="col-7" x-text="state.master.running_partition || '—'"></dd>
|
||||||
<dt class="col-5 text-muted">Deadzone</dt>
|
<dt class="col-5 text-muted">Deadzone</dt>
|
||||||
<dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd>
|
<dd class="col-7" x-text="state.master.deadzone != null ? state.master.deadzone + ' LSB' : '—'"></dd>
|
||||||
|
<dt class="col-5 text-muted">Log-Level</dt>
|
||||||
|
<dd class="col-7" x-text="formatLogLevel(masterLogLevel)"></dd>
|
||||||
|
<dt class="col-5 text-muted">LiPo 1</dt>
|
||||||
|
<dd class="col-7" x-text="formatLipo(state.master?.lipo1)"></dd>
|
||||||
|
<dt class="col-5 text-muted">LiPo 2</dt>
|
||||||
|
<dd class="col-7" x-text="formatLipo(state.master?.lipo2)"></dd>
|
||||||
</dl>
|
</dl>
|
||||||
</template>
|
</template>
|
||||||
<template x-if="state.master && !state.master.ok">
|
<template x-if="state.master && !state.master.ok">
|
||||||
@@ -240,6 +266,31 @@
|
|||||||
Restart
|
Restart
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="form-label text-muted small mb-1 mt-3" for="master-log-level">
|
||||||
|
ESP Log-Level (global)
|
||||||
|
</label>
|
||||||
|
<div class="d-flex flex-wrap gap-2 align-items-end">
|
||||||
|
<select id="master-log-level" class="form-select form-select-sm config-input"
|
||||||
|
x-model.number="masterLogLevel"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
<option value="0">None (aus)</option>
|
||||||
|
<option value="1">Error</option>
|
||||||
|
<option value="2">Warn</option>
|
||||||
|
<option value="3">Info</option>
|
||||||
|
<option value="4">Debug</option>
|
||||||
|
<option value="5">Verbose</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
@click="readMasterLogLevel()"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Lesen
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
@click="setMasterLogLevel()"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Setzen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected">
|
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected">
|
||||||
UART nicht verbunden — Eingabe gesperrt.
|
UART nicht verbunden — Eingabe gesperrt.
|
||||||
</p>
|
</p>
|
||||||
@@ -265,8 +316,29 @@
|
|||||||
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
|
<span class="badge bg-secondary" x-text="(state.clients || []).length + ' registered'"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-muted small px-3 pt-2 mb-0">Slaves per ESP-NOW — Master-Deadzone bleibt separat.</p>
|
<p class="text-muted small px-3 pt-2 mb-0">
|
||||||
<div class="card-body p-0 pt-2">
|
<strong>Live-Stream</strong> startet die schnelle <code>CACHE_STATUS</code>-Abfrage (~16 ms).
|
||||||
|
Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Accel-Stream; Tap-Notify (S/D/T) steuert
|
||||||
|
die Tap-Arten auf dem Slave.
|
||||||
|
</p>
|
||||||
|
<div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center">
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="state.live_stream ? 'btn-warning' : 'btn-success'"
|
||||||
|
@click="setLiveStream(!state.live_stream)"
|
||||||
|
:disabled="busy || !state.uart_connected || !(state.clients || []).length"
|
||||||
|
x-text="state.live_stream ? 'Live-Stream aus' : 'Live-Stream an'"></button>
|
||||||
|
<span class="text-muted small">Tap alle Slaves:</span>
|
||||||
|
<label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label>
|
||||||
|
<label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label>
|
||||||
|
<label class="tap-toggle"><input type="checkbox" x-model="allTapTriple" :disabled="busy"> T</label>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
@click="setTapNotifyAll(allTapSingle, allTapDouble, allTapTriple)"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Tap setzen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-0 pt-1">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table pp-table table-hover">
|
<table class="table pp-table table-hover">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -276,12 +348,17 @@
|
|||||||
<th>Ver</th>
|
<th>Ver</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Deadzone</th>
|
<th>Deadzone</th>
|
||||||
|
<th>Accel (LSB)</th>
|
||||||
|
<th>Akku</th>
|
||||||
|
<th>Accel</th>
|
||||||
|
<th>Tap-Notify</th>
|
||||||
|
<th>Tap</th>
|
||||||
<th>Aktion</th>
|
<th>Aktion</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<template x-if="!(state.clients || []).length">
|
<template x-if="!(state.clients || []).length">
|
||||||
<tr><td colspan="6" class="text-muted text-center py-4">No clients</td></tr>
|
<tr><td colspan="11" class="text-muted text-center py-4">No clients</td></tr>
|
||||||
</template>
|
</template>
|
||||||
<template x-for="c in (state.clients || [])" :key="c.id + c.mac">
|
<template x-for="c in (state.clients || [])" :key="c.id + c.mac">
|
||||||
<tr>
|
<tr>
|
||||||
@@ -294,6 +371,47 @@
|
|||||||
x-text="c.available ? 'available' : 'inactive'"></span>
|
x-text="c.available ? 'available' : 'inactive'"></span>
|
||||||
</td>
|
</td>
|
||||||
<td x-text="c.deadzone != null ? c.deadzone : '—'"></td>
|
<td x-text="c.deadzone != null ? c.deadzone : '—'"></td>
|
||||||
|
<td>
|
||||||
|
<span class="accel"
|
||||||
|
:class="accelCellClass(c)"
|
||||||
|
x-text="formatAccel(c)"
|
||||||
|
:title="accelTitle(c)"></span>
|
||||||
|
</td>
|
||||||
|
<td class="small" x-text="formatLipoPair(c)" :title="lipoTitle(c)"></td>
|
||||||
|
<td>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="c.accel_stream ? 'btn-warning' : 'btn-outline-success'"
|
||||||
|
@click="setAccelStream(c.id, !c.accel_stream)"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"
|
||||||
|
x-text="c.accel_stream ? 'Aus' : 'An'"
|
||||||
|
title="ESP-NOW Accel-Stream auf Slave"></button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||||
|
<label class="tap-toggle" title="Single tap">
|
||||||
|
<input type="checkbox"
|
||||||
|
:checked="c.tap_notify_single"
|
||||||
|
@change="setTapNotify(c.id, $event.target.checked, c.tap_notify_double, c.tap_notify_triple)"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"> S
|
||||||
|
</label>
|
||||||
|
<label class="tap-toggle" title="Double tap">
|
||||||
|
<input type="checkbox"
|
||||||
|
:checked="c.tap_notify_double"
|
||||||
|
@change="setTapNotify(c.id, c.tap_notify_single, $event.target.checked, c.tap_notify_triple)"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"> D
|
||||||
|
</label>
|
||||||
|
<label class="tap-toggle" title="Triple tap">
|
||||||
|
<input type="checkbox"
|
||||||
|
:checked="c.tap_notify_triple"
|
||||||
|
@change="setTapNotify(c.id, c.tap_notify_single, c.tap_notify_double, $event.target.checked)"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"> T
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span :class="tapCellClass(c)" x-text="formatLastTap(c)" :title="tapTitle(c)"></span>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex flex-wrap gap-1 align-items-center">
|
<div class="d-flex flex-wrap gap-1 align-items-center">
|
||||||
<input type="number" class="form-control form-control-sm dz-input"
|
<input type="number" class="form-control form-control-sm dz-input"
|
||||||
@@ -312,11 +430,23 @@
|
|||||||
title="ESP-NOW Unicast-Test">
|
title="ESP-NOW Unicast-Test">
|
||||||
Test
|
Test
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-warning btn-sm"
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
@click="findMe(c.id)"
|
@click="echoPing(c.id)"
|
||||||
:disabled="busy || !state.uart_connected || !c.available"
|
:disabled="busy || !state.uart_connected || !c.available"
|
||||||
title="LED-Ring Find me (ESP-NOW)">
|
title="ESP-NOW Timestamp-Echo (Round-Trip-Latenz)">
|
||||||
Find me
|
Ping
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
|
@click="ledRing({ clientId: c.id })"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"
|
||||||
|
title="LED-Ring (aktueller Modus)">
|
||||||
|
LED
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm"
|
||||||
|
@click="ledRing({ clientId: c.id, mode: 'find-me' })"
|
||||||
|
:disabled="busy || !state.uart_connected || !c.available"
|
||||||
|
title="Find me">
|
||||||
|
Find
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
@click="restart(c.id)"
|
@click="restart(c.id)"
|
||||||
@@ -335,6 +465,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">LED-Ring</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
Modi: <code>clear</code>, <code>color</code> (ganzer Ring), <code>progress</code> (0–100 %),
|
||||||
|
<code>digit</code> (0–10), <code>blink</code>, <code>find-me</code>, <code>battery-low</code>.
|
||||||
|
Ziel: Master (<code>client_id=0</code>), ein Slave oder alle Slaves (Broadcast).
|
||||||
|
</p>
|
||||||
|
<div class="row g-3 align-items-end">
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small text-muted">Modus</label>
|
||||||
|
<select class="form-select form-select-sm" x-model="led.mode" :disabled="busy">
|
||||||
|
<option value="color">Farbe (alle LEDs)</option>
|
||||||
|
<option value="clear">Aus (clear)</option>
|
||||||
|
<option value="progress">Progress</option>
|
||||||
|
<option value="digit">Ziffer/Symbol</option>
|
||||||
|
<option value="blink">Blink</option>
|
||||||
|
<option value="find-me">Find me</option>
|
||||||
|
<option value="battery-low">Battery low</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label small text-muted">RGB / Intensität</label>
|
||||||
|
<div class="d-flex flex-wrap gap-2">
|
||||||
|
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
|
||||||
|
placeholder="R" x-model.number="led.r" :disabled="busy">
|
||||||
|
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
|
||||||
|
placeholder="G" x-model.number="led.g" :disabled="busy">
|
||||||
|
<input type="number" class="form-control form-control-sm" style="width:4rem" min="0" max="255"
|
||||||
|
placeholder="B" x-model.number="led.b" :disabled="busy">
|
||||||
|
<input type="number" class="form-control form-control-sm" style="width:5rem" min="0" max="255"
|
||||||
|
title="0 = Geräte-Default (~5 %)"
|
||||||
|
placeholder="Int." x-model.number="led.intensity" :disabled="busy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2" x-show="led.mode === 'progress'">
|
||||||
|
<label class="form-label small text-muted">Progress %</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" min="0" max="100"
|
||||||
|
x-model.number="led.progress" :disabled="busy">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2" x-show="led.mode === 'digit'">
|
||||||
|
<label class="form-label small text-muted">Ziffer 0–10</label>
|
||||||
|
<input type="number" class="form-control form-control-sm" min="0" max="10"
|
||||||
|
x-model.number="led.digit" :disabled="busy">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2" x-show="led.mode === 'blink'">
|
||||||
|
<label class="form-label small text-muted">Blink ms × Anzahl</label>
|
||||||
|
<div class="d-flex gap-1">
|
||||||
|
<input type="number" class="form-control form-control-sm" min="1"
|
||||||
|
x-model.number="led.blinkMs" :disabled="busy">
|
||||||
|
<input type="number" class="form-control form-control-sm" min="1"
|
||||||
|
x-model.number="led.blinkCount" :disabled="busy">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex flex-wrap gap-2">
|
||||||
|
<button type="button" class="btn btn-primary btn-sm"
|
||||||
|
@click="ledRing({ clientId: 0 })"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Master
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
@click="ledRing({ allClients: true, slavesOnly: true })"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Alle Slaves
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
@click="ledRing({ allClients: true })"
|
||||||
|
:disabled="busy || !state.uart_connected">
|
||||||
|
Alle + Master
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="col-12">
|
<section class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Firmware OTA (A/B)</div>
|
<div class="card-header">Firmware OTA (A/B)</div>
|
||||||
@@ -448,8 +655,15 @@
|
|||||||
ws: null,
|
ws: null,
|
||||||
wsConnected: false,
|
wsConnected: false,
|
||||||
masterDz: 100,
|
masterDz: 100,
|
||||||
|
masterLogLevel: 0,
|
||||||
allDz: 100,
|
allDz: 100,
|
||||||
|
allTapSingle: false,
|
||||||
|
allTapDouble: false,
|
||||||
|
allTapTriple: false,
|
||||||
slaveDz: {},
|
slaveDz: {},
|
||||||
|
TAP_DISPLAY_MS: 2000,
|
||||||
|
tapDisplay: {},
|
||||||
|
_tapClock: 0,
|
||||||
otaFile: null,
|
otaFile: null,
|
||||||
ota: {
|
ota: {
|
||||||
active: false, phase: '', step: '', percent: 0,
|
active: false, phase: '', step: '', percent: 0,
|
||||||
@@ -461,12 +675,31 @@
|
|||||||
busy: false,
|
busy: false,
|
||||||
configMsg: '',
|
configMsg: '',
|
||||||
configMsgOk: false,
|
configMsgOk: false,
|
||||||
|
_flashTimer: null,
|
||||||
|
led: {
|
||||||
|
mode: 'color',
|
||||||
|
r: 0,
|
||||||
|
g: 120,
|
||||||
|
b: 255,
|
||||||
|
intensity: 0,
|
||||||
|
progress: 50,
|
||||||
|
digit: 0,
|
||||||
|
blinkMs: 350,
|
||||||
|
blinkCount: 1
|
||||||
|
},
|
||||||
connect() {
|
connect() {
|
||||||
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||||
const url = proto + '//' + location.host + '/ws';
|
const url = proto + '//' + location.host + '/ws';
|
||||||
|
if (this._batteryTimer) clearInterval(this._batteryTimer);
|
||||||
|
this._batteryTimer = setInterval(() => this.refreshBattery(), 5000);
|
||||||
|
if (this._tapTimer) clearInterval(this._tapTimer);
|
||||||
|
this._tapTimer = setInterval(() => { this._tapClock++; }, 250);
|
||||||
const connect = () => {
|
const connect = () => {
|
||||||
this.ws = new WebSocket(url);
|
this.ws = new WebSocket(url);
|
||||||
this.ws.onopen = () => { this.wsConnected = true; };
|
this.ws.onopen = () => {
|
||||||
|
this.wsConnected = true;
|
||||||
|
this.refreshBattery();
|
||||||
|
};
|
||||||
this.ws.onclose = () => {
|
this.ws.onclose = () => {
|
||||||
this.wsConnected = false;
|
this.wsConnected = false;
|
||||||
setTimeout(connect, 2000);
|
setTimeout(connect, 2000);
|
||||||
@@ -478,7 +711,14 @@
|
|||||||
this.applyOTAProgress(msg);
|
this.applyOTAProgress(msg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (msg.type === 'battery_status') {
|
||||||
|
if (msg.samples?.length) this.applyBatterySamples(msg.samples);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prev = this.state;
|
||||||
this.state = msg;
|
this.state = msg;
|
||||||
|
this.preserveBatteryInState(prev, this.state);
|
||||||
|
this.syncTapDisplay(msg.clients || []);
|
||||||
if (msg.master?.deadzone != null) {
|
if (msg.master?.deadzone != null) {
|
||||||
this.masterDz = msg.master.deadzone;
|
this.masterDz = msg.master.deadzone;
|
||||||
}
|
}
|
||||||
@@ -492,10 +732,173 @@
|
|||||||
};
|
};
|
||||||
connect();
|
connect();
|
||||||
},
|
},
|
||||||
|
preserveBatteryInState(prev, next) {
|
||||||
|
if (!prev || !next) return;
|
||||||
|
const keepLipo = (oldL, newL) => {
|
||||||
|
if (newL?.valid) return newL;
|
||||||
|
if (oldL?.valid) return oldL;
|
||||||
|
return newL ?? oldL;
|
||||||
|
};
|
||||||
|
const keepAge = (oldAge, newAge, hasValid) => {
|
||||||
|
if (hasValid && newAge != null) return newAge;
|
||||||
|
if (oldAge != null && !hasValid) return oldAge;
|
||||||
|
return newAge ?? oldAge;
|
||||||
|
};
|
||||||
|
if (next.master) {
|
||||||
|
const pm = prev.master || {};
|
||||||
|
const l1 = keepLipo(pm.lipo1, next.master.lipo1);
|
||||||
|
const l2 = keepLipo(pm.lipo2, next.master.lipo2);
|
||||||
|
next.master.lipo1 = l1;
|
||||||
|
next.master.lipo2 = l2;
|
||||||
|
next.master.battery_age_ms = keepAge(
|
||||||
|
pm.battery_age_ms, next.master.battery_age_ms, !!(l1?.valid || l2?.valid));
|
||||||
|
}
|
||||||
|
if (!Array.isArray(next.clients)) return;
|
||||||
|
const prevById = Object.fromEntries((prev.clients || []).map((c) => [c.id, c]));
|
||||||
|
next.clients = next.clients.map((c) => {
|
||||||
|
const p = prevById[c.id];
|
||||||
|
if (!p) return c;
|
||||||
|
const l1 = keepLipo(p.lipo1, c.lipo1);
|
||||||
|
const l2 = keepLipo(p.lipo2, c.lipo2);
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
lipo1: l1,
|
||||||
|
lipo2: l2,
|
||||||
|
battery_age_ms: keepAge(p.battery_age_ms, c.battery_age_ms, !!(l1?.valid || l2?.valid))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
applyBatterySamples(samples) {
|
||||||
|
if (!samples?.length) return;
|
||||||
|
const master = { ...(this.state.master || {}) };
|
||||||
|
const clients = [...(this.state.clients || [])];
|
||||||
|
let masterChanged = false;
|
||||||
|
let clientsChanged = false;
|
||||||
|
for (const s of samples) {
|
||||||
|
if (s.client_id === 0) {
|
||||||
|
master.lipo1 = s.lipo1;
|
||||||
|
master.lipo2 = s.lipo2;
|
||||||
|
master.battery_age_ms = s.age_ms;
|
||||||
|
masterChanged = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const idx = clients.findIndex((x) => x.id === s.client_id);
|
||||||
|
if (idx >= 0) {
|
||||||
|
clients[idx] = {
|
||||||
|
...clients[idx],
|
||||||
|
lipo1: s.lipo1,
|
||||||
|
lipo2: s.lipo2,
|
||||||
|
battery_age_ms: s.age_ms,
|
||||||
|
};
|
||||||
|
clientsChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!masterChanged && !clientsChanged) return;
|
||||||
|
this.state = {
|
||||||
|
...this.state,
|
||||||
|
...(masterChanged ? { master } : {}),
|
||||||
|
...(clientsChanged ? { clients } : {}),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async refreshBattery() {
|
||||||
|
if (!this.state?.uart_connected) return;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/battery?all_clients=1');
|
||||||
|
if (!r.ok) return;
|
||||||
|
const data = await r.json();
|
||||||
|
if (data.samples?.length) this.applyBatterySamples(data.samples);
|
||||||
|
} catch (_) {}
|
||||||
|
},
|
||||||
|
formatLogLevel(level) {
|
||||||
|
const labels = ['None', 'Error', 'Warn', 'Info', 'Debug', 'Verbose'];
|
||||||
|
if (level == null || level < 0 || level > 5) return '—';
|
||||||
|
return `${labels[level]} (${level})`;
|
||||||
|
},
|
||||||
formatMac(hex) {
|
formatMac(hex) {
|
||||||
if (!hex || hex.length !== 12) return hex || '';
|
if (!hex || hex.length !== 12) return hex || '';
|
||||||
return hex.match(/.{2}/g).join(':');
|
return hex.match(/.{2}/g).join(':');
|
||||||
},
|
},
|
||||||
|
formatLipo(l) {
|
||||||
|
if (!l?.valid) return '—';
|
||||||
|
const v = (l.voltage_mv / 1000).toFixed(2);
|
||||||
|
return l.percent != null ? `${v} V (${l.percent}%)` : `${v} V`;
|
||||||
|
},
|
||||||
|
formatLipoPair(c) {
|
||||||
|
return `1: ${this.formatLipo(c?.lipo1)} · 2: ${this.formatLipo(c?.lipo2)}`;
|
||||||
|
},
|
||||||
|
lipoTitle(c) {
|
||||||
|
if (!c?.lipo1?.valid && !c?.lipo2?.valid) return 'Keine ADC-Daten (Cache ~30 s)';
|
||||||
|
let t = `LiPo1 ${c.lipo1?.voltage_mv ?? '—'} mV, LiPo2 ${c.lipo2?.voltage_mv ?? '—'} mV`;
|
||||||
|
if (c.battery_age_ms != null) t += `, Alter ${c.battery_age_ms} ms`;
|
||||||
|
return t;
|
||||||
|
},
|
||||||
|
formatAccel(c) {
|
||||||
|
if (!this.state?.live_stream) return '—';
|
||||||
|
if (!c?.accel_stream) return '—';
|
||||||
|
if (!c?.accel_valid) return '…';
|
||||||
|
return `${c.accel_x} / ${c.accel_y} / ${c.accel_z}`;
|
||||||
|
},
|
||||||
|
accelTitle(c) {
|
||||||
|
if (!this.state?.live_stream) return 'Live-Stream aus — oben einschalten';
|
||||||
|
if (!c?.accel_stream) return 'Accel-Stream für diesen Slave nicht aktiv';
|
||||||
|
if (!c?.accel_valid) return 'Warte auf erste ESP-NOW Samples…';
|
||||||
|
const age = c.accel_age_ms != null ? `${c.accel_age_ms} ms alt` : '';
|
||||||
|
return `x=${c.accel_x} y=${c.accel_y} z=${c.accel_z} (raw LSB, ±2g)${age ? ' · ' + age : ''}`;
|
||||||
|
},
|
||||||
|
accelCellClass(c) {
|
||||||
|
if (!c?.accel_valid) return 'accel-stale';
|
||||||
|
if (c.accel_age_ms != null && c.accel_age_ms > 200) return 'accel-stale';
|
||||||
|
return '';
|
||||||
|
},
|
||||||
|
tapNotifyAny(c) {
|
||||||
|
return !!(c?.tap_notify_single || c?.tap_notify_double || c?.tap_notify_triple);
|
||||||
|
},
|
||||||
|
syncTapDisplay(clients) {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const c of clients) {
|
||||||
|
if (!c?.last_tap || !c?.last_tap_at) continue;
|
||||||
|
const prev = this.tapDisplay[c.id];
|
||||||
|
if (!prev || c.last_tap_at >= prev.shownAt) {
|
||||||
|
this.tapDisplay[c.id] = { kind: c.last_tap, shownAt: c.last_tap_at };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const id of Object.keys(this.tapDisplay)) {
|
||||||
|
if (now - this.tapDisplay[id].shownAt > this.TAP_DISPLAY_MS + 500) {
|
||||||
|
delete this.tapDisplay[id];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
activeTapDisplay(c) {
|
||||||
|
void this._tapClock;
|
||||||
|
const d = this.tapDisplay[c?.id];
|
||||||
|
if (!d) return null;
|
||||||
|
if (Date.now() - d.shownAt >= this.TAP_DISPLAY_MS) return null;
|
||||||
|
return d;
|
||||||
|
},
|
||||||
|
formatLastTap(c) {
|
||||||
|
if (!this.state?.live_stream) return '—';
|
||||||
|
if (!this.tapNotifyAny(c)) return '—';
|
||||||
|
const labels = { single: 'Single', double: 'Double', triple: 'Triple' };
|
||||||
|
const d = this.activeTapDisplay(c);
|
||||||
|
if (d) return labels[d.kind] || d.kind;
|
||||||
|
if (!c?.last_tap) return '…';
|
||||||
|
return '—';
|
||||||
|
},
|
||||||
|
tapTitle(c) {
|
||||||
|
if (!this.state?.live_stream) return 'Live-Stream aus';
|
||||||
|
if (!this.tapNotifyAny(c)) return 'Tap-Notify nicht konfiguriert (S/D/T)';
|
||||||
|
const d = this.activeTapDisplay(c);
|
||||||
|
if (d) {
|
||||||
|
const age = (Date.now() - d.shownAt) + ' ms her';
|
||||||
|
return `Tap: ${d.kind} · ${age}`;
|
||||||
|
}
|
||||||
|
if (!c?.last_tap) return 'Warte auf Tap-Event…';
|
||||||
|
return 'Bereit — letzter Tap ausgeblendet';
|
||||||
|
},
|
||||||
|
tapCellClass(c) {
|
||||||
|
if (this.activeTapDisplay(c)) return 'tap-hit';
|
||||||
|
return 'text-muted';
|
||||||
|
},
|
||||||
formatSize(n) {
|
formatSize(n) {
|
||||||
if (n == null) return '';
|
if (n == null) return '';
|
||||||
if (n < 1024) return n + ' B';
|
if (n < 1024) return n + ' B';
|
||||||
@@ -590,8 +993,21 @@
|
|||||||
return rows;
|
return rows;
|
||||||
},
|
},
|
||||||
applyOTAProgress(p) {
|
applyOTAProgress(p) {
|
||||||
this.ota.phase = p.phase || '';
|
const prevPhase = this.ota.phase;
|
||||||
this.ota.step = p.step || this.ota.step || '';
|
const prevStep = this.ota.step;
|
||||||
|
if (p.phase) {
|
||||||
|
// Ignore out-of-order master upload updates after distribution started.
|
||||||
|
if (!(p.phase === 'uploading' && prevPhase === 'distributing')) {
|
||||||
|
this.ota.phase = p.phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (p.step) {
|
||||||
|
if (!(p.step === 'master' && (prevStep === 'slaves' || prevPhase === 'distributing'))) {
|
||||||
|
this.ota.step = p.step;
|
||||||
|
}
|
||||||
|
} else if (!this.ota.step) {
|
||||||
|
this.ota.step = '';
|
||||||
|
}
|
||||||
this.ota.percent = p.percent ?? this.ota.percent;
|
this.ota.percent = p.percent ?? this.ota.percent;
|
||||||
this.ota.message = p.message || '';
|
this.ota.message = p.message || '';
|
||||||
if (p.image_size) this.ota.imageSize = p.image_size;
|
if (p.image_size) this.ota.imageSize = p.image_size;
|
||||||
@@ -670,10 +1086,14 @@
|
|||||||
this.busy = false;
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
flash(msg, ok) {
|
flash(msg, ok, durationMs = 5000) {
|
||||||
this.configMsg = msg;
|
this.configMsg = msg;
|
||||||
this.configMsgOk = ok;
|
this.configMsgOk = ok;
|
||||||
setTimeout(() => { this.configMsg = ''; }, 5000);
|
if (this._flashTimer) clearTimeout(this._flashTimer);
|
||||||
|
this._flashTimer = setTimeout(() => {
|
||||||
|
this.configMsg = '';
|
||||||
|
this._flashTimer = null;
|
||||||
|
}, durationMs);
|
||||||
},
|
},
|
||||||
async setDeadzone(clientId, deadzone, opts = {}) {
|
async setDeadzone(clientId, deadzone, opts = {}) {
|
||||||
if (deadzone == null || deadzone < 0) {
|
if (deadzone == null || deadzone < 0) {
|
||||||
@@ -732,6 +1152,182 @@
|
|||||||
async setMasterDeadzone() {
|
async setMasterDeadzone() {
|
||||||
await this.setDeadzone(0, this.masterDz);
|
await this.setDeadzone(0, this.masterDz);
|
||||||
},
|
},
|
||||||
|
async readMasterLogLevel() {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/log-level');
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || 'Log-Level lesen fehlgeschlagen', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.masterLogLevel = data.level;
|
||||||
|
this.flash(`Master: Log-Level ${this.formatLogLevel(data.level)}`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setMasterLogLevel() {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/log-level', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ write: true, level: this.masterLogLevel })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || 'Log-Level setzen fehlgeschlagen', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.masterLogLevel = data.level;
|
||||||
|
this.flash(`Master: Log-Level ${this.formatLogLevel(data.level)} gesetzt`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
patchLiveStream(enabled) {
|
||||||
|
let clients = this.state.clients || [];
|
||||||
|
if (!enabled) {
|
||||||
|
clients = clients.map((c) => ({
|
||||||
|
...c,
|
||||||
|
accel_valid: false,
|
||||||
|
accel_x: 0,
|
||||||
|
accel_y: 0,
|
||||||
|
accel_z: 0,
|
||||||
|
accel_age_ms: 0,
|
||||||
|
last_tap: '',
|
||||||
|
last_tap_at: 0
|
||||||
|
}));
|
||||||
|
this.tapDisplay = {};
|
||||||
|
}
|
||||||
|
this.state = { ...this.state, live_stream: enabled, clients };
|
||||||
|
},
|
||||||
|
async setLiveStream(enable) {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/live-stream', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enable })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || 'Live-Stream fehlgeschlagen', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.patchLiveStream(!!data.enabled);
|
||||||
|
this.flash(`Live-Stream ${data.enabled ? 'an' : 'aus'}`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
patchClientAccelStream(clientId, enabled) {
|
||||||
|
const clients = (this.state.clients || []).map((c) => {
|
||||||
|
if (c.id !== clientId) return c;
|
||||||
|
const next = { ...c, accel_stream: enabled };
|
||||||
|
if (!enabled) {
|
||||||
|
next.accel_valid = false;
|
||||||
|
next.accel_x = 0;
|
||||||
|
next.accel_y = 0;
|
||||||
|
next.accel_z = 0;
|
||||||
|
next.accel_age_ms = 0;
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
this.state = { ...this.state, clients };
|
||||||
|
},
|
||||||
|
async setAccelStream(clientId, enable) {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/clients/${clientId}/accel-stream`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ enable })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || `Accel-Stream Slave ${clientId} fehlgeschlagen`, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.patchClientAccelStream(clientId, !!data.enabled);
|
||||||
|
this.flash(`Slave ${clientId}: Accel-Stream ${data.enabled ? 'an' : 'aus'}`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
patchClientTapNotify(clientId, single, doubleTap, triple) {
|
||||||
|
const clients = (this.state.clients || []).map((c) => {
|
||||||
|
if (c.id !== clientId) return c;
|
||||||
|
const next = {
|
||||||
|
...c,
|
||||||
|
tap_notify_single: single,
|
||||||
|
tap_notify_double: doubleTap,
|
||||||
|
tap_notify_triple: triple
|
||||||
|
};
|
||||||
|
if (!single && !doubleTap && !triple) {
|
||||||
|
next.last_tap = '';
|
||||||
|
next.last_tap_at = 0;
|
||||||
|
delete this.tapDisplay[c.id];
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
this.state = { ...this.state, clients };
|
||||||
|
},
|
||||||
|
async setTapNotify(clientId, single, doubleTap, triple) {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch(`/api/clients/${clientId}/tap-notify`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ single, double_tap: doubleTap, triple })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || `Tap-Notify Slave ${clientId} fehlgeschlagen`, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.patchClientTapNotify(clientId, !!data.single, !!data.double_tap, !!data.triple);
|
||||||
|
const on = [data.single && 'S', data.double_tap && 'D', data.triple && 'T'].filter(Boolean).join('/') || 'aus';
|
||||||
|
this.flash(`Slave ${clientId}: Tap-Notify ${on}`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async setTapNotifyAll(single, doubleTap, triple) {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/tap-notify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ all_clients: true, single, double_tap: doubleTap, triple })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || 'Tap-Notify für alle Slaves fehlgeschlagen', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const c of (this.state.clients || [])) {
|
||||||
|
this.patchClientTapNotify(c.id, !!single, !!doubleTap, !!triple);
|
||||||
|
}
|
||||||
|
const on = [single && 'S', doubleTap && 'D', triple && 'T'].filter(Boolean).join('/') || 'aus';
|
||||||
|
this.flash(`Alle Slaves: Tap-Notify ${on} (${data.slaves_updated} aktualisiert)`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
async setDeadzoneAll(deadzone) {
|
async setDeadzoneAll(deadzone) {
|
||||||
if (deadzone == null || deadzone < 0) {
|
if (deadzone == null || deadzone < 0) {
|
||||||
this.flash('Ungültiger Deadzone-Wert', false);
|
this.flash('Ungültiger Deadzone-Wert', false);
|
||||||
@@ -759,6 +1355,43 @@
|
|||||||
this.busy = false;
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
formatMs(v) {
|
||||||
|
return v != null && Number.isFinite(Number(v)) ? Number(v).toFixed(3) : '—';
|
||||||
|
},
|
||||||
|
formatUsAsMs(us) {
|
||||||
|
return us != null && Number.isFinite(Number(us))
|
||||||
|
? (Number(us) / 1000).toFixed(3)
|
||||||
|
: '—';
|
||||||
|
},
|
||||||
|
async echoPing(clientId) {
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/echo-ping', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ client_id: clientId })
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok) {
|
||||||
|
this.flash(data.error || `Echo-Ping Slave ${clientId} fehlgeschlagen`, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.success) {
|
||||||
|
this.flash(`Echo-Ping Slave ${clientId} fehlgeschlagen (${this.formatMs(data.rtt_ms)} ms)`, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.flash(
|
||||||
|
`Echo-Ping Slave ${clientId}: Host ${this.formatMs(data.rtt_ms)} ms, ` +
|
||||||
|
`ESP ${this.formatUsAsMs(data.esp_rtt_us)} ms`,
|
||||||
|
true,
|
||||||
|
5000
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
async restart(clientId = 0) {
|
async restart(clientId = 0) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
try {
|
try {
|
||||||
@@ -780,6 +1413,49 @@
|
|||||||
this.busy = false;
|
this.busy = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async ledRing(opts = {}) {
|
||||||
|
const clientId = opts.clientId ?? 0;
|
||||||
|
const mode = opts.mode ?? this.led.mode;
|
||||||
|
this.busy = true;
|
||||||
|
try {
|
||||||
|
const r = await fetch('/api/led-ring', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
mode,
|
||||||
|
client_id: clientId,
|
||||||
|
all_clients: !!opts.allClients,
|
||||||
|
slaves_only: !!opts.slavesOnly,
|
||||||
|
r: this.led.r,
|
||||||
|
g: this.led.g,
|
||||||
|
b: this.led.b,
|
||||||
|
intensity: this.led.intensity,
|
||||||
|
progress: this.led.progress,
|
||||||
|
digit: this.led.digit,
|
||||||
|
blink_ms: this.led.blinkMs,
|
||||||
|
blink_count: this.led.blinkCount
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const data = await r.json();
|
||||||
|
if (!r.ok || !data.success) {
|
||||||
|
this.flash(data.error || 'LED-Ring fehlgeschlagen', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let label = 'Master';
|
||||||
|
if (opts.allClients) {
|
||||||
|
label = opts.slavesOnly
|
||||||
|
? `Alle Slaves (${data.slaves_updated})`
|
||||||
|
: `Alle + Master (${data.slaves_updated} Slaves)`;
|
||||||
|
} else if (clientId > 0) {
|
||||||
|
label = `Slave ${clientId}`;
|
||||||
|
}
|
||||||
|
this.flash(`LED ${mode} → ${label}`, true);
|
||||||
|
} catch (e) {
|
||||||
|
this.flash(String(e), false);
|
||||||
|
} finally {
|
||||||
|
this.busy = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
async findMe(clientId = 0) {
|
async findMe(clientId = 0) {
|
||||||
this.busy = true;
|
this.busy = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -18,20 +18,31 @@ idf_component_register(
|
|||||||
"cmd/cmd_version.c"
|
"cmd/cmd_version.c"
|
||||||
"cmd/cmd_client_info.c"
|
"cmd/cmd_client_info.c"
|
||||||
"cmd/cmd_accel_deadzone.c"
|
"cmd/cmd_accel_deadzone.c"
|
||||||
|
"cmd/cmd_accel_stream.c"
|
||||||
|
"cmd/cmd_tap_notify.c"
|
||||||
|
"cmd/cmd_cache_status.c"
|
||||||
"cmd/cmd_espnow_unicast_test.c"
|
"cmd/cmd_espnow_unicast_test.c"
|
||||||
|
"cmd/cmd_espnow_echo_ping.c"
|
||||||
"cmd/cmd_espnow_find_me.c"
|
"cmd/cmd_espnow_find_me.c"
|
||||||
"cmd/cmd_restart.c"
|
"cmd/cmd_restart.c"
|
||||||
"pod_reboot.c"
|
"pod_reboot.c"
|
||||||
"cmd/cmd_led_ring.c"
|
"cmd/cmd_led_ring.c"
|
||||||
|
"cmd/cmd_battery.c"
|
||||||
|
"cmd/cmd_set_log_level.c"
|
||||||
"cmd/cmd_ota.c"
|
"cmd/cmd_ota.c"
|
||||||
"cmd/cmd_ota_slave_progress.c"
|
"cmd/cmd_ota_slave_progress.c"
|
||||||
"ota_uart.c"
|
"ota_uart.c"
|
||||||
"ota_espnow.c"
|
"ota_espnow.c"
|
||||||
|
"ota_session.c"
|
||||||
"client_registry.c"
|
"client_registry.c"
|
||||||
"esp_now_comm.c"
|
"esp_now_comm.c"
|
||||||
|
"esp_now_core.c"
|
||||||
|
"esp_now_master.c"
|
||||||
|
"esp_now_slave.c"
|
||||||
"esp_now_proto.c"
|
"esp_now_proto.c"
|
||||||
"bosch456.c"
|
"bosch456.c"
|
||||||
"board_input.c"
|
"board_input.c"
|
||||||
|
"battery_uv.c"
|
||||||
"pod_settings.c"
|
"pod_settings.c"
|
||||||
"proto/uart_messages.pb.c"
|
"proto/uart_messages.pb.c"
|
||||||
"proto/esp_now_messages.pb.c"
|
"proto/esp_now_messages.pb.c"
|
||||||
@@ -53,7 +64,10 @@ idf_component_register(
|
|||||||
esp_driver_i2c
|
esp_driver_i2c
|
||||||
esp_adc
|
esp_adc
|
||||||
app_update
|
app_update
|
||||||
|
esp_timer
|
||||||
bma456)
|
bma456)
|
||||||
|
|
||||||
target_compile_definitions(${COMPONENT_LIB}
|
target_compile_definitions(${COMPONENT_LIB}
|
||||||
PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"")
|
PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"")
|
||||||
|
# Optional: disable software UV protection
|
||||||
|
# target_compile_definitions(${COMPONENT_LIB} PRIVATE POWERPOD_BATTERY_UV_ENABLE=0)
|
||||||
|
|||||||
+144
-18
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the **same binary**; role and ESP-NOW network are selected at boot via DIP switches and an I2C IO expander.
|
ESP32-S3 firmware for Powerpod nodes. Master and slave devices run the **same binary**; role and ESP-NOW network are selected at boot via DIP switches and an I2C IO expander.
|
||||||
|
|
||||||
|
**Architektur & ESP-only Doku (ohne goTool):** [`../docs/ARCHITECTURE.md`](../docs/ARCHITECTURE.md) (Datenflüsse UART/Commands/ESP-NOW), [`../docs/DOCUMENTATION.md`](../docs/DOCUMENTATION.md) (vollständige Referenz).
|
||||||
|
|
||||||
## System overview
|
## System overview
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -53,18 +55,20 @@ Pins (`powerpod.h`):
|
|||||||
| BMA456 INT | 10 |
|
| BMA456 INT | 10 |
|
||||||
| Button (Taster) | 12 |
|
| Button (Taster) | 12 |
|
||||||
| LiPo sense 1 (ADC) | 1 |
|
| LiPo sense 1 (ADC) | 1 |
|
||||||
| LiPo sense 2 (ADC) | 12 (skipped if same as button) |
|
| LiPo sense 2 (ADC) | 11 |
|
||||||
|
|
||||||
> **TODO:** GPIO assignments above are provisional; confirm pinning against the real board before release.
|
> **TODO:** GPIO assignments above are provisional; confirm pinning against the real board before release.
|
||||||
|
|
||||||
Startup order:
|
Startup order (normal path; UV energy-save skips I2C/BMA456/ESP-NOW/UART/button — see **Software UV protection**):
|
||||||
|
|
||||||
1. Read DIP + IO expander → `app_config`
|
1. `pod_settings_init()` — NVS
|
||||||
2. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
|
2. LiPo ADC init + optional UV boot check (`battery_uv.c`)
|
||||||
3. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
|
3. Read DIP + IO expander → `app_config`
|
||||||
4. `led_ring_init()`
|
4. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
|
||||||
5. `board_input_init()` — button press logs, LiPo ADC logs every **10 s**
|
5. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
|
||||||
6. **Master only:** command queue, UART, registered commands (e.g. VERSION)
|
6. `led_ring_init()`
|
||||||
|
7. LiPo monitor task + button (`board_input.c`)
|
||||||
|
8. **Master only:** command queue, UART, registered commands (e.g. VERSION)
|
||||||
|
|
||||||
## BMA456 accelerometer (`bosch456.c`)
|
## BMA456 accelerometer (`bosch456.c`)
|
||||||
|
|
||||||
@@ -115,6 +119,10 @@ Schema: `proto/esp_now_messages.proto`. Encode/decode: `esp_now_proto.c`. The ES
|
|||||||
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
|
| `ESPNOW_UNICAST_TEST` | Master → slave | `EspNowUnicastTest` (`seq`) |
|
||||||
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
|
| `ESPNOW_FIND_ME` | Master → slave | `EspNowFindMe` (`client_id` filter) — LED locate sequence |
|
||||||
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
|
| `ESPNOW_RESTART` | Master → slave | `EspNowRestart` (`client_id` filter) — reboot slave |
|
||||||
|
| `ESPNOW_ACCEL_SAMPLE` | Slave → master | `EspNowAccelSample` (`slave_id`, `x`, `y`, `z` raw LSB) — ~every 16 ms |
|
||||||
|
| `ESPNOW_SET_TAP_NOTIFY` | Master → slave | `EspNowTapNotify` (`client_id`, `single`, `double_tap`, `triple`) — which tap kinds to forward |
|
||||||
|
| `ESPNOW_TAP_EVENT` | Slave → master | `EspNowTapEvent` (`client_id`, `kind`) — on BMA456 tap interrupt if notify enabled |
|
||||||
|
| `ESPNOW_BATTERY_REPORT` | Slave → master | `EspNowBatteryReport` (`client_id`, `lipo1/2` mV) — ~every 30 s; cached in `client_registry` |
|
||||||
| `ESPNOW_OTA_START` | Master → slave (unicast) | `EspNowOtaStart` (`total_size`) |
|
| `ESPNOW_OTA_START` | Master → slave (unicast) | `EspNowOtaStart` (`total_size`) |
|
||||||
| `ESPNOW_OTA_PAYLOAD` | Master → slave | `EspNowOtaPayload` (`seq`, up to 200 B `data`) |
|
| `ESPNOW_OTA_PAYLOAD` | Master → slave | `EspNowOtaPayload` (`seq`, up to 200 B `data`) |
|
||||||
| `ESPNOW_OTA_END` | Master → slave | `EspNowOtaEnd` |
|
| `ESPNOW_OTA_END` | Master → slave | `EspNowOtaEnd` |
|
||||||
@@ -172,7 +180,7 @@ Logging:
|
|||||||
|
|
||||||
## Command handler
|
## Command handler
|
||||||
|
|
||||||
Generic dispatch for host commands (UART today; `msg_post()` for in-firmware sources later).
|
Generic dispatch for host commands over UART only.
|
||||||
|
|
||||||
```
|
```
|
||||||
UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
|
UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
|
||||||
@@ -182,7 +190,8 @@ UART → generic_msg_t queue → vCmdDispatcherTask → registered handler
|
|||||||
|-----|-------------|
|
|-----|-------------|
|
||||||
| `init_cmdHandler(queue)` | Start dispatcher task (priority 5) |
|
| `init_cmdHandler(queue)` | Start dispatcher task (priority 5) |
|
||||||
| `msg_register_handler(id, cb)` | Register callback; max 32 handlers |
|
| `msg_register_handler(id, cb)` | Register callback; max 32 handlers |
|
||||||
| `msg_post(id, data, len)` | Enqueue from firmware (e.g. future ESP-NOW → PC path) |
|
|
||||||
|
During an OTA session (`ota_session_busy()`), the dispatcher rejects all UART commands except OTA_* and `OTA_SLAVE_PROGRESS` (see `ota_session.c`).
|
||||||
|
|
||||||
```c
|
```c
|
||||||
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
|
typedef void (*msg_callback_t)(const uint8_t *data, size_t len);
|
||||||
@@ -209,6 +218,7 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
|
|||||||
| 6 | `ACCEL_DEADZONE` | Implemented (`cmd/cmd_accel_deadzone.c`) — get/set accel filter LSB |
|
| 6 | `ACCEL_DEADZONE` | Implemented (`cmd/cmd_accel_deadzone.c`) — get/set accel filter LSB |
|
||||||
| 7 | `ESPNOW_UNICAST_TEST` | Implemented (`cmd/cmd_espnow_unicast_test.c`) |
|
| 7 | `ESPNOW_UNICAST_TEST` | Implemented (`cmd/cmd_espnow_unicast_test.c`) |
|
||||||
| 8 | `LED_RING` | Implemented (`cmd/cmd_led_ring.c`) — ring progress bar (0–100 %, RGB, intensity) |
|
| 8 | `LED_RING` | Implemented (`cmd/cmd_led_ring.c`) — ring progress bar (0–100 %, RGB, intensity) |
|
||||||
|
| 26 | `BATTERY_STATUS` | Implemented (`cmd/cmd_battery.c`) — cached LiPo 1/2 per pod from `client_registry` (UART read, no slave round-trip) |
|
||||||
| 16 | `OTA_START` | Implemented (`cmd/cmd_ota.c`) — begin UART OTA on inactive slot |
|
| 16 | `OTA_START` | Implemented (`cmd/cmd_ota.c`) — begin UART OTA on inactive slot |
|
||||||
| 17 | `OTA_PAYLOAD` | Implemented — up to 200 B per frame; device buffers 4 KiB |
|
| 17 | `OTA_PAYLOAD` | Implemented — up to 200 B per frame; device buffers 4 KiB |
|
||||||
| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, push image to slaves via ESP-NOW, set boot |
|
| 18 | `OTA_END` | Implemented — flush, `esp_ota_end`, push image to slaves via ESP-NOW, set boot |
|
||||||
@@ -217,6 +227,11 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
|
|||||||
| 21 | `OTA_SLAVE_PROGRESS` | Implemented (`cmd/cmd_ota_slave_progress.c`) — query per-slave ESP-NOW OTA progress |
|
| 21 | `OTA_SLAVE_PROGRESS` | Implemented (`cmd/cmd_ota_slave_progress.c`) — query per-slave ESP-NOW OTA progress |
|
||||||
| 22 | `FIND_ME` | Implemented (`cmd/cmd_espnow_find_me.c`) — `client_id=0` local ring, `>0` ESP-NOW to slave |
|
| 22 | `FIND_ME` | Implemented (`cmd/cmd_espnow_find_me.c`) — `client_id=0` local ring, `>0` ESP-NOW to slave |
|
||||||
| 23 | `RESTART` | Implemented (`cmd/cmd_restart.c`) — `client_id=0` reboot master, `>0` ESP-NOW reboot slave |
|
| 23 | `RESTART` | Implemented (`cmd/cmd_restart.c`) — `client_id=0` reboot master, `>0` ESP-NOW reboot slave |
|
||||||
|
| 25 | `ACCEL_STREAM` | Implemented — enable/disable slave ESP-NOW accel stream to master |
|
||||||
|
| 27 | `TAP_NOTIFY` | Implemented (`cmd/cmd_tap_notify.c`) — get/set which tap kinds notify via ESP-NOW |
|
||||||
|
| 29 | `CACHE_STATUS` | Implemented (`cmd/cmd_cache_status.c`) — subscribed accel + tap cache (one UART round-trip) |
|
||||||
|
| 30 | `ESPNOW_ECHO_PING` | Implemented (`cmd/cmd_espnow_echo_ping.c`) — ESP-NOW timestamp echo (latency test) |
|
||||||
|
| 31 | `SET_LOG_LEVEL` | Implemented (`cmd/cmd_set_log_level.c`) — get/set global `esp_log_level_set("*", …)` on master |
|
||||||
|
|
||||||
Regenerate C code:
|
Regenerate C code:
|
||||||
|
|
||||||
@@ -310,6 +325,52 @@ Sets the **software** deadzone used by `bosch456.c` when logging accel (see [BMA
|
|||||||
|
|
||||||
**Response:** `accel_deadzone_response` with applied `deadzone`, `success`, and `slaves_updated` (ESP-NOW count).
|
**Response:** `accel_deadzone_response` with applied `deadzone`, `success`, and `slaves_updated` (ESP-NOW count).
|
||||||
|
|
||||||
|
### TAP_NOTIFY command
|
||||||
|
|
||||||
|
Configure which BMA456 tap kinds a **slave** forwards to the master over ESP-NOW. The slave only sends `ESPNOW_TAP_EVENT` when the matching notify flag is enabled (set locally on the slave via ESP-NOW).
|
||||||
|
|
||||||
|
**Request:** framed `1b` (`0x1b`) + `tap_notify_request`:
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
|-------|---------|
|
||||||
|
| `write` | `false` = read, `true` = write |
|
||||||
|
| `single`, `double_tap`, `triple` | Which tap kinds to notify (write) |
|
||||||
|
| `client_id` | Slave id (read/write one slave) |
|
||||||
|
| `all_clients` | Master: ESP-NOW unicast to every registered slave |
|
||||||
|
|
||||||
|
**Response:** `tap_notify_response` (`client_id`, `success`, `slaves_updated`, `single`, `double_tap`, `triple`).
|
||||||
|
|
||||||
|
Notify flags are mirrored in `ClientInfo` (`tap_notify_single/double/triple`) for the dashboard.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 tap-notify -client 16 -set -single
|
||||||
|
go run . -port /dev/ttyUSB0 tap-notify -client 16
|
||||||
|
```
|
||||||
|
|
||||||
|
### CACHE_STATUS command
|
||||||
|
|
||||||
|
Read **cached** accel and/or tap data on the **master** in one UART round-trip. Slaves send `ESPNOW_ACCEL_SAMPLE` every **16 ms** when streaming; tap events arrive via `ESPNOW_TAP_EVENT` and are held up to **16 ms** (`CLIENT_REGISTRY_TAP_MAX_AGE_MS`). Pending taps are **consumed** on read (like the former `TAP_SNAPSHOT`).
|
||||||
|
|
||||||
|
**Request:** framed `1d` (`0x1d`) only — no body (`CacheStatusRequest` empty).
|
||||||
|
|
||||||
|
**Response:** `cache_status_response.clients[]` — one entry per slave with `accel_stream_enabled` and/or any tap-notify flag:
|
||||||
|
|
||||||
|
| Field | When present |
|
||||||
|
|-------|----------------|
|
||||||
|
| `client_id` | Always (for listed slaves) |
|
||||||
|
| `accel` | Slave has accel stream on (`valid`, `x`/`y`/`z`, `age_ms` when sample fresh) |
|
||||||
|
| `tap` | Tap notify on **and** a pending tap was consumed (`kind`, `age_ms`) |
|
||||||
|
|
||||||
|
Unsubscribed submessages are omitted on the wire (proto3 defaults). The master walks `client_registry` once per request (`cmd/cmd_cache_status.c`).
|
||||||
|
|
||||||
|
Host tools poll this at **16 ms** when live-stream / WebSocket receive is enabled. Tap events stay visible for **2 s** in the UI/API after first sight.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 cache-status
|
||||||
|
```
|
||||||
|
|
||||||
|
External API (`serve -api-addr :8081`) uses the same command for WebSocket `accel` / `tap` push.
|
||||||
|
|
||||||
### ESPNOW_UNICAST_TEST command
|
### ESPNOW_UNICAST_TEST command
|
||||||
|
|
||||||
Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging `ACCEL_DEADZONE` unicast.
|
Minimal master→slave ESP-NOW unicast check (no BMA456). Use this before debugging `ACCEL_DEADZONE` unicast.
|
||||||
@@ -333,6 +394,30 @@ go run . -port /dev/ttyUSB0 find-me
|
|||||||
go run . -port /dev/ttyUSB0 find-me -client 16
|
go run . -port /dev/ttyUSB0 find-me -client 16
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### SET_LOG_LEVEL command
|
||||||
|
|
||||||
|
Read or set the **global** ESP-IDF log filter on the master (`esp_log_level_set("*", level)`). Does not affect the host UART protocol (UART1); `esp_log_*` output goes to the **debug console UART0** (USB, 115200).
|
||||||
|
|
||||||
|
**Request:** framed `31` (`0x1f`) + `set_log_level_request` (`write`, `level` 0–5).
|
||||||
|
|
||||||
|
**Response:** `set_log_level_response` (`success`, `level`).
|
||||||
|
|
||||||
|
| `level` | `esp_log_level_t` |
|
||||||
|
|---------|-------------------|
|
||||||
|
| 0 | NONE |
|
||||||
|
| 1 | ERROR |
|
||||||
|
| 2 | WARN |
|
||||||
|
| 3 | INFO |
|
||||||
|
| 4 | DEBUG |
|
||||||
|
| 5 | VERBOSE |
|
||||||
|
|
||||||
|
Boot default follows `CONFIG_LOG_DEFAULT_LEVEL` in `sdkconfig` (not persisted across reboot).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go run . -port /dev/ttyUSB0 log-level
|
||||||
|
go run . -port /dev/ttyUSB0 log-level -set -level 0
|
||||||
|
```
|
||||||
|
|
||||||
### RESTART command
|
### RESTART command
|
||||||
|
|
||||||
Reboot the master (`client_id=0`) or one slave via ESP-NOW (`client_id` = registry id). The device sends the UART response, then restarts after ~150 ms.
|
Reboot the master (`client_id=0`) or one slave via ESP-NOW (`client_id` = registry id). The device sends the UART response, then restarts after ~150 ms.
|
||||||
@@ -345,6 +430,31 @@ go run . -port /dev/ttyUSB0 restart
|
|||||||
go run . -port /dev/ttyUSB0 restart -client 16
|
go run . -port /dev/ttyUSB0 restart -client 16
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### BATTERY_STATUS command
|
||||||
|
|
||||||
|
Read **cached** LiPo ADC values on the **master** (master local + one entry per registered slave). Slaves push `ESPNOW_BATTERY_REPORT` every **30 s**; the master stores them in `client_registry` (`lipo1/2_valid`, `lipo1/2_mv`, `battery_updated_at`). The master refreshes its own pack on the same interval in `master_monitor_task`.
|
||||||
|
|
||||||
|
**Request:** framed `26` + optional `battery_status_request` (`client_id`, `all_clients`).
|
||||||
|
|
||||||
|
**Response:** `battery_status_response` with `samples[]` (`client_id`, `lipo1`, `lipo2`, `age_ms`).
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Host / goTool: all_clients returns master (id 0) + slaves from cache
|
||||||
|
```
|
||||||
|
|
||||||
|
### Software UV protection (LiPo)
|
||||||
|
|
||||||
|
When `POWERPOD_BATTERY_UV_ENABLE` is `1` (default in `powerpod.h`, disable via CMake), the firmware monitors ADC pin voltages on GPIO 1 and 11:
|
||||||
|
|
||||||
|
| Condition | ADC threshold | Action |
|
||||||
|
|-----------|---------------|--------|
|
||||||
|
| Under-voltage | any valid channel < **2300 mV** (3.0 V pack) | NVS latch `uv_latch`, restart into energy-save mode |
|
||||||
|
| Charged | all valid channels ≥ **3000 mV** (4.0 V pack) | Clear latch, restart into normal boot |
|
||||||
|
|
||||||
|
**Energy-save boot:** LED mode `6` (first 4 LEDs red ~10 % for 5 s), then no WiFi/ESP-NOW, UART, I2C, BMA456, or button — only LiPo ADC polling every 30 s until charged.
|
||||||
|
|
||||||
|
**Test LED pattern from host:** `led-ring -mode battery-low` (works even when UV feature is disabled).
|
||||||
|
|
||||||
### LED_RING command
|
### LED_RING command
|
||||||
|
|
||||||
Control the 95-LED ring from the host. The firmware **does not** animate digits locally; only UART updates the display.
|
Control the 95-LED ring from the host. The firmware **does not** animate digits locally; only UART updates the display.
|
||||||
@@ -353,14 +463,19 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits
|
|||||||
|
|
||||||
| Field | Meaning |
|
| Field | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| `mode` | `0` = clear, `1` = progress bar, `2` = digit, `3` = blink full ring, `4` = find-me (R/G/B ×3 @ full brightness) |
|
| `mode` | `0` = clear, `1` = progress, `2` = digit (0–10), `3` = blink, `4` = find-me, `5` = solid color (all LEDs), `6` = battery-low (first 4 LEDs red ~10 % for 5 s) |
|
||||||
| `progress` | 0–100 (% of ring lit, mode `1`) |
|
| `progress` | 0–100 (% of ring lit, mode `1`) |
|
||||||
| `digit` | 0–10 (mode `2`, same segment maps as built-in digits) |
|
| `digit` | 0–10 (mode `2`, segment maps in `led_ring.c`) |
|
||||||
| `r`, `g`, `b` | Color 0–255 |
|
| `r`, `g`, `b` | Color 0–255 |
|
||||||
| `intensity` | Brightness 0–255 (scaled into RGB; `0` → firmware default ~5 %) |
|
| `intensity` | Brightness 0–255 (scaled into RGB; `0` → firmware default ~5 %) |
|
||||||
| `blink_ms`, `blink_count` | Pulse length and count (mode `3`; defaults 350 ms, 1) |
|
| `blink_ms`, `blink_count` | Pulse length and count (mode `3`; defaults 350 ms, 1) |
|
||||||
|
| `client_id` | `0` = master ring only; `>0` = ESP-NOW unicast to one slave |
|
||||||
|
| `all_clients` | Broadcast to all registered slaves |
|
||||||
|
| `slaves_only` | With `all_clients`: do not change master ring |
|
||||||
|
|
||||||
**Response:** `led_ring_progress_response` (`success`, `mode`, `progress`, `digit`).
|
**Response:** `led_ring_progress_response` (`success`, `mode`, `progress`, `digit`, `client_id`, `slaves_updated`).
|
||||||
|
|
||||||
|
Slaves receive the same command via ESP-NOW `ESPNOW_LED_RING` and run it locally.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go run . -port /dev/ttyUSB0 led-ring -mode progress -progress 75 -g 80 -b 255
|
go run . -port /dev/ttyUSB0 led-ring -mode progress -progress 75 -g 80 -b 255
|
||||||
@@ -370,6 +485,9 @@ go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2
|
|||||||
go run . -port /dev/ttyUSB0 find-me
|
go run . -port /dev/ttyUSB0 find-me
|
||||||
go run . -port /dev/ttyUSB0 find-me -client 16
|
go run . -port /dev/ttyUSB0 find-me -client 16
|
||||||
go run . -port /dev/ttyUSB0 led-ring -mode find-me
|
go run . -port /dev/ttyUSB0 led-ring -mode find-me
|
||||||
|
go run . -port /dev/ttyUSB0 led-ring -mode battery-low -client 0
|
||||||
|
go run . -port /dev/ttyUSB0 led-ring -mode color -r 255 -g 0 -b 0 -client 16
|
||||||
|
go run . -port /dev/ttyUSB0 led-ring -mode digit -digit 5 -all
|
||||||
```
|
```
|
||||||
|
|
||||||
### CLIENT_INFO command
|
### CLIENT_INFO command
|
||||||
@@ -378,7 +496,7 @@ go run . -port /dev/ttyUSB0 led-ring -mode find-me
|
|||||||
|
|
||||||
**Response:** payload `04` + nanopb `UartMessage` with `client_info_response.clients` — one `ClientInfo` per registered slave (from ESP-NOW `SLAVE_INFO`).
|
**Response:** payload `04` + nanopb `UartMessage` with `client_info_response.clients` — one `ClientInfo` per registered slave (from ESP-NOW `SLAVE_INFO`).
|
||||||
|
|
||||||
Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping` — **milliseconds since** the last packet / last successful heartbeat (computed when `CLIENT_INFO` is answered; typically 0–1000 while the slave is heartbeating every 1 s).
|
Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `last_success_ping`, `tap_notify_single`, `tap_notify_double`, `tap_notify_triple` — **milliseconds since** the last packet / last successful heartbeat (computed when `CLIENT_INFO` is answered; typically 0–1000 while the slave is heartbeating every 1 s).
|
||||||
|
|
||||||
## Client registry
|
## Client registry
|
||||||
|
|
||||||
@@ -389,6 +507,7 @@ Fields per client: `id`, `mac`, `version`, `available`, `used`, `last_ping`, `la
|
|||||||
| `client_registry_heartbeat(mac, id, version, …)` | Same as upsert for heartbeats; reactivates inactive clients |
|
| `client_registry_heartbeat(mac, id, version, …)` | Same as upsert for heartbeats; reactivates inactive clients |
|
||||||
| `client_registry_check_timeouts(timeout_ms)` | Mark stale clients inactive (master monitor task) |
|
| `client_registry_check_timeouts(timeout_ms)` | Mark stale clients inactive (master monitor task) |
|
||||||
| `client_registry_count()` / `client_registry_at(i)` | Iterate for UART encoding |
|
| `client_registry_count()` / `client_registry_at(i)` | Iterate for UART encoding |
|
||||||
|
| `client_registry_set_tap_notify()` / `client_registry_take_tap()` | Tap notify flags + short-lived tap cache (16 ms) |
|
||||||
|
|
||||||
Slaves register when the master receives `SLAVE_INFO` on the matching network; `HEARTBEAT` keeps them marked available. The registry **MAC is always the ESP-NOW source address** (`recv_info.src_addr`), not the optional `mac` bytes in the protobuf (used only on the wire for debugging).
|
Slaves register when the master receives `SLAVE_INFO` on the matching network; `HEARTBEAT` keeps them marked available. The registry **MAC is always the ESP-NOW source address** (`recv_info.src_addr`), not the optional `mac` bytes in the protobuf (used only on the wire for debugging).
|
||||||
|
|
||||||
@@ -443,7 +562,10 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
|||||||
| `powerpod.c` | `app_main`, DIP/network config, init order |
|
| `powerpod.c` | `app_main`, DIP/network config, init order |
|
||||||
| `powerpod.h` | Pin defines |
|
| `powerpod.h` | Pin defines |
|
||||||
| `app_config.h` | `app_config_t` |
|
| `app_config.h` | `app_config_t` |
|
||||||
| `esp_now_comm.c/h` | WiFi, ESP-NOW, discover / slave info / OTA send |
|
| `esp_now_comm.c/h` | ESP-NOW init and recv router |
|
||||||
|
| `esp_now_core.c/h` | Shared WiFi, peer, send |
|
||||||
|
| `esp_now_master.c/h` | Master discover, monitor, unicast |
|
||||||
|
| `esp_now_slave.c/h` | Slave join, heartbeat, telemetry |
|
||||||
| `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) |
|
| `ota_uart.c/h` | Shared 4 KiB OTA flash buffer (UART + ESP-NOW) |
|
||||||
| `ota_espnow.c/h` | Master: distribute staged image to slaves |
|
| `ota_espnow.c/h` | Master: distribute staged image to slaves |
|
||||||
| `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) |
|
| `cmd/cmd_ota.c/h` | UART OTA command handlers (master only) |
|
||||||
@@ -454,11 +576,15 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
|
|||||||
| `cmd/cmd_version.c/h` | VERSION handler |
|
| `cmd/cmd_version.c/h` | VERSION handler |
|
||||||
| `cmd/cmd_client_info.c/h` | CLIENT_INFO handler |
|
| `cmd/cmd_client_info.c/h` | CLIENT_INFO handler |
|
||||||
| `client_registry.c/h` | Registered slave table |
|
| `client_registry.c/h` | Registered slave table |
|
||||||
| `bosch456.c/h` | BMA456H I2C driver, accel poll, tap INT, deadzone filter |
|
| `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter |
|
||||||
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 |
|
| `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config |
|
||||||
| `pod_settings.c/h` | NVS persistence (accel deadzone, …) |
|
| `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — subscribed accel + tap cache poll |
|
||||||
|
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO11 |
|
||||||
|
| `battery_uv.c/h` | Software LiPo UV latch, energy-save boot (`POWERPOD_BATTERY_UV_ENABLE`) |
|
||||||
|
| `pod_settings.c/h` | NVS persistence (accel deadzone, UV latch, …) |
|
||||||
| `led_ring.c/h` | LED ring (digit display, progress bar) |
|
| `led_ring.c/h` | LED ring (digit display, progress bar) |
|
||||||
| `cmd/cmd_led_ring.c` | UART `LED_RING` progress command |
|
| `cmd/cmd_led_ring.c` | UART `LED_RING` progress command |
|
||||||
|
| `cmd/cmd_set_log_level.c` | UART `SET_LOG_LEVEL` — runtime ESP-IDF log level |
|
||||||
| `proto/uart_messages.proto` | UART protocol schema |
|
| `proto/uart_messages.proto` | UART protocol schema |
|
||||||
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
|
| `proto/esp_now_messages.proto` | ESP-NOW protocol schema |
|
||||||
| `esp_now_proto.c/h` | Encode/decode `EspNowMessage` |
|
| `esp_now_proto.c/h` | Encode/decode `EspNowMessage` |
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
#include "battery_uv.h"
|
||||||
|
|
||||||
|
#if POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
|
||||||
|
#include "led_ring.h"
|
||||||
|
#include "pod_reboot.h"
|
||||||
|
#include "pod_settings.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/task.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[BATTERY_UV]";
|
||||||
|
|
||||||
|
#define UV_DEBOUNCE_SAMPLES 2u
|
||||||
|
#define UV_POLL_INTERVAL_MS 30000u
|
||||||
|
|
||||||
|
static uint8_t s_uv_streak;
|
||||||
|
static uint8_t s_charge_streak;
|
||||||
|
|
||||||
|
bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); }
|
||||||
|
|
||||||
|
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
|
||||||
|
if (reading == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reading->lipo1_valid && reading->lipo1_mv < BATTERY_UV_ADC_MV) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (reading->lipo2_valid && reading->lipo2_mv < BATTERY_UV_ADC_MV) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
|
||||||
|
if (reading == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool any_valid = false;
|
||||||
|
if (reading->lipo1_valid) {
|
||||||
|
any_valid = true;
|
||||||
|
if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reading->lipo2_valid) {
|
||||||
|
any_valid = true;
|
||||||
|
if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return any_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool battery_uv_evaluate_boot(void) {
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
|
||||||
|
if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) {
|
||||||
|
ESP_LOGI(TAG, "charged — clearing UV latch");
|
||||||
|
pod_settings_clear_uv_latched();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pod_settings_is_uv_latched() || battery_uv_reading_is_under(&reading)) {
|
||||||
|
if (!pod_settings_is_uv_latched()) {
|
||||||
|
ESP_LOGW(TAG, "under-voltage at boot — latching UV");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "UV latched — energy-save boot");
|
||||||
|
}
|
||||||
|
pod_settings_set_uv_latched(true);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battery_uv_poll(const board_lipo_reading_t *reading) {
|
||||||
|
if (reading == NULL || pod_settings_is_uv_latched()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (battery_uv_reading_is_under(reading)) {
|
||||||
|
s_uv_streak++;
|
||||||
|
ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak,
|
||||||
|
(unsigned)UV_DEBOUNCE_SAMPLES);
|
||||||
|
if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) {
|
||||||
|
ESP_LOGW(TAG, "UV confirmed — latching and restarting");
|
||||||
|
pod_settings_set_uv_latched(true);
|
||||||
|
pod_schedule_restart();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_uv_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void battery_uv_run_mode(void) {
|
||||||
|
s_charge_streak = 0;
|
||||||
|
s_uv_streak = 0;
|
||||||
|
|
||||||
|
led_ring_init();
|
||||||
|
led_ring_show_battery_low();
|
||||||
|
|
||||||
|
ESP_LOGW(TAG, "energy-save mode — ADC poll every %u ms", UV_POLL_INTERVAL_MS);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
|
||||||
|
if (battery_uv_reading_is_charged(&reading)) {
|
||||||
|
s_charge_streak++;
|
||||||
|
ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak,
|
||||||
|
(unsigned)UV_DEBOUNCE_SAMPLES);
|
||||||
|
if (s_charge_streak >= UV_DEBOUNCE_SAMPLES) {
|
||||||
|
ESP_LOGI(TAG, "charged — clearing UV latch and restarting");
|
||||||
|
pod_settings_clear_uv_latched();
|
||||||
|
pod_schedule_restart();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
s_charge_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(UV_POLL_INTERVAL_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#ifndef BATTERY_UV_H
|
||||||
|
#define BATTERY_UV_H
|
||||||
|
|
||||||
|
#include "board_input.h"
|
||||||
|
#include "powerpod.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#if POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
|
||||||
|
/** ADC pin voltage (mV) below which UV is triggered (3.0 V pack via 33k/10k divider). */
|
||||||
|
#define BATTERY_UV_ADC_MV 2300u
|
||||||
|
/** ADC pin voltage (mV) at which charging is detected (4.0 V pack, rounded). */
|
||||||
|
#define BATTERY_CHARGE_ADC_MV 3000u
|
||||||
|
|
||||||
|
bool battery_uv_is_latched(void);
|
||||||
|
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading);
|
||||||
|
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading);
|
||||||
|
|
||||||
|
/** true → enter UV energy-save path (does not return). */
|
||||||
|
bool battery_uv_evaluate_boot(void);
|
||||||
|
|
||||||
|
void battery_uv_poll(const board_lipo_reading_t *reading);
|
||||||
|
|
||||||
|
/** Blocks: LED indicator, then minimal ADC polling until charged. */
|
||||||
|
void battery_uv_run_mode(void);
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
static inline bool battery_uv_is_latched(void) { return false; }
|
||||||
|
|
||||||
|
static inline bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
|
||||||
|
(void)reading;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
|
||||||
|
(void)reading;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline bool battery_uv_evaluate_boot(void) { return false; }
|
||||||
|
|
||||||
|
static inline void battery_uv_poll(const board_lipo_reading_t *reading) {
|
||||||
|
(void)reading;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void battery_uv_run_mode(void) {}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
+109
-55
@@ -1,12 +1,16 @@
|
|||||||
#include "board_input.h"
|
#include "board_input.h"
|
||||||
#include "powerpod.h"
|
#include "powerpod.h"
|
||||||
|
#include "client_registry.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
|
#if POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
#include "battery_uv.h"
|
||||||
|
#endif
|
||||||
#include "esp_adc/adc_oneshot.h"
|
#include "esp_adc/adc_oneshot.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/idf_additions.h"
|
#include "freertos/idf_additions.h"
|
||||||
#include "freertos/queue.h"
|
#include "freertos/queue.h"
|
||||||
#include <stdint.h>
|
#include <string.h>
|
||||||
|
|
||||||
static const char *TAG_BTN = "[BTN]";
|
static const char *TAG_BTN = "[BTN]";
|
||||||
static const char *TAG_LIPO = "[LIPO]";
|
static const char *TAG_LIPO = "[LIPO]";
|
||||||
@@ -14,66 +18,108 @@ static const char *TAG_LIPO = "[LIPO]";
|
|||||||
#define LIPO_SAMPLE_INTERVAL_MS 10000
|
#define LIPO_SAMPLE_INTERVAL_MS 10000
|
||||||
#define BUTTON_QUEUE_LEN 4
|
#define BUTTON_QUEUE_LEN 4
|
||||||
#define BUTTON_DEBOUNCE_MS 80
|
#define BUTTON_DEBOUNCE_MS 80
|
||||||
|
#define LIPO_ADC_FULL_SCALE_MV 3300
|
||||||
|
#define LIPO_ADC_MAX_RAW 4095
|
||||||
|
|
||||||
static QueueHandle_t s_button_queue;
|
static QueueHandle_t s_button_queue;
|
||||||
static adc_oneshot_unit_handle_t s_adc;
|
|
||||||
static bool s_lipo1_ok;
|
|
||||||
static bool s_lipo2_ok;
|
|
||||||
static adc_channel_t s_lipo1_ch;
|
|
||||||
static adc_channel_t s_lipo2_ch;
|
|
||||||
|
|
||||||
static esp_err_t adc_init_channel(int gpio, adc_channel_t *out_ch, bool *out_ok) {
|
typedef struct {
|
||||||
adc_unit_t unit;
|
adc_oneshot_unit_handle_t unit;
|
||||||
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit, out_ch);
|
adc_channel_t ch;
|
||||||
|
bool ok;
|
||||||
|
} lipo_adc_t;
|
||||||
|
|
||||||
|
static lipo_adc_t s_lipo1;
|
||||||
|
static lipo_adc_t s_lipo2;
|
||||||
|
|
||||||
|
static esp_err_t adc_init_gpio(int gpio, lipo_adc_t *out) {
|
||||||
|
out->unit = NULL;
|
||||||
|
out->ok = false;
|
||||||
|
|
||||||
|
adc_unit_t unit_id;
|
||||||
|
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit_id, &out->ch);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err));
|
ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err));
|
||||||
*out_ok = false;
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
if (unit != ADC_UNIT_1) {
|
|
||||||
ESP_LOGW(TAG_LIPO, "GPIO%d on ADC unit %d (expected ADC1)", gpio, (int)unit);
|
adc_oneshot_unit_init_cfg_t init_cfg = {
|
||||||
|
.unit_id = unit_id,
|
||||||
|
};
|
||||||
|
err = adc_oneshot_new_unit(&init_cfg, &out->unit);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG_LIPO, "ADC unit %d init GPIO%d failed: %s", (int)unit_id, gpio,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
adc_oneshot_chan_cfg_t chan_cfg = {
|
adc_oneshot_chan_cfg_t chan_cfg = {
|
||||||
.atten = ADC_ATTEN_DB_12,
|
.atten = ADC_ATTEN_DB_12,
|
||||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||||
};
|
};
|
||||||
err = adc_oneshot_config_channel(s_adc, *out_ch, &chan_cfg);
|
err = adc_oneshot_config_channel(out->unit, out->ch, &chan_cfg);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG_LIPO, "ADC config GPIO%d failed: %s", gpio, esp_err_to_name(err));
|
ESP_LOGW(TAG_LIPO, "ADC config GPIO%d failed: %s", gpio, esp_err_to_name(err));
|
||||||
*out_ok = false;
|
adc_oneshot_del_unit(out->unit);
|
||||||
|
out->unit = NULL;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
*out_ok = true;
|
|
||||||
|
out->ok = true;
|
||||||
|
ESP_LOGI(TAG_LIPO, "GPIO%d ready (ADC unit %d)", gpio, (int)unit_id);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t raw_to_mv(int raw) {
|
||||||
|
if (raw < 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void sample_one_channel(const lipo_adc_t *adc, uint32_t *mv_out,
|
||||||
|
bool *valid_out) {
|
||||||
|
*valid_out = false;
|
||||||
|
*mv_out = 0;
|
||||||
|
if (adc == NULL || !adc->ok || adc->unit == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int raw = 0;
|
||||||
|
if (adc_oneshot_read(adc->unit, adc->ch, &raw) == ESP_OK) {
|
||||||
|
*valid_out = true;
|
||||||
|
*mv_out = raw_to_mv(raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void board_input_read_lipo(board_lipo_reading_t *out) {
|
||||||
|
if (out == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(out, 0, sizeof(*out));
|
||||||
|
sample_one_channel(&s_lipo1, &out->lipo1_mv, &out->lipo1_valid);
|
||||||
|
sample_one_channel(&s_lipo2, &out->lipo2_mv, &out->lipo2_valid);
|
||||||
|
}
|
||||||
|
|
||||||
static void lipo_monitor_task(void *param) {
|
static void lipo_monitor_task(void *param) {
|
||||||
(void)param;
|
(void)param;
|
||||||
|
|
||||||
ESP_LOGI(TAG_LIPO, "monitor task (interval %d ms)", LIPO_SAMPLE_INTERVAL_MS);
|
ESP_LOGI(TAG_LIPO, "monitor task (interval %d ms)", LIPO_SAMPLE_INTERVAL_MS);
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
int raw1 = -1;
|
board_lipo_reading_t reading;
|
||||||
int raw2 = -1;
|
board_input_read_lipo(&reading);
|
||||||
int mv1 = -1;
|
client_registry_set_master_battery(&reading);
|
||||||
int mv2 = -1;
|
|
||||||
|
|
||||||
if (s_lipo1_ok) {
|
|
||||||
raw1 = 0;
|
|
||||||
if (adc_oneshot_read(s_adc, s_lipo1_ch, &raw1) == ESP_OK) {
|
|
||||||
mv1 = (raw1 * 3300) / 4095;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (s_lipo2_ok) {
|
|
||||||
raw2 = 0;
|
|
||||||
if (adc_oneshot_read(s_adc, s_lipo2_ch, &raw2) == ESP_OK) {
|
|
||||||
mv2 = (raw2 * 3300) / 4095;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ESP_LOGI(TAG_LIPO,
|
ESP_LOGI(TAG_LIPO,
|
||||||
"LIPO1 GPIO%d raw=%d (~%d mV) LIPO2 GPIO%d raw=%d (~%d mV)",
|
"LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV",
|
||||||
V_LIPO_1_GPIO, raw1, mv1, V_LIPO_2_GPIO, raw2, mv2);
|
V_LIPO_1_GPIO, reading.lipo1_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)reading.lipo1_mv, V_LIPO_2_GPIO,
|
||||||
|
reading.lipo2_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)reading.lipo2_mv);
|
||||||
|
|
||||||
|
#if POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
battery_uv_poll(&reading);
|
||||||
|
#endif
|
||||||
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
|
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
|
||||||
}
|
}
|
||||||
@@ -145,29 +191,30 @@ static esp_err_t init_button(void) {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t init_lipo_adc(void) {
|
static esp_err_t init_lipo_adc_hw(void) {
|
||||||
adc_oneshot_unit_init_cfg_t init_cfg = {
|
memset(&s_lipo1, 0, sizeof(s_lipo1));
|
||||||
.unit_id = ADC_UNIT_1,
|
memset(&s_lipo2, 0, sizeof(s_lipo2));
|
||||||
};
|
|
||||||
esp_err_t err = adc_oneshot_new_unit(&init_cfg, &s_adc);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG_LIPO, "ADC init failed: %s", esp_err_to_name(err));
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
|
|
||||||
adc_init_channel(V_LIPO_1_GPIO, &s_lipo1_ch, &s_lipo1_ok);
|
adc_init_gpio(V_LIPO_1_GPIO, &s_lipo1);
|
||||||
|
|
||||||
if (V_LIPO_2_GPIO == TASTER_GPIO) {
|
if (V_LIPO_2_GPIO == TASTER_GPIO) {
|
||||||
ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)",
|
ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)",
|
||||||
V_LIPO_2_GPIO);
|
V_LIPO_2_GPIO);
|
||||||
s_lipo2_ok = false;
|
|
||||||
} else {
|
} else {
|
||||||
adc_init_channel(V_LIPO_2_GPIO, &s_lipo2_ch, &s_lipo2_ok);
|
adc_init_gpio(V_LIPO_2_GPIO, &s_lipo2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s_lipo1_ok && !s_lipo2_ok) {
|
if (!s_lipo1.ok && !s_lipo2.ok) {
|
||||||
adc_oneshot_del_unit(s_adc);
|
return ESP_FAIL;
|
||||||
s_adc = NULL;
|
}
|
||||||
|
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t board_input_init_adc_only(void) { return init_lipo_adc_hw(); }
|
||||||
|
|
||||||
|
esp_err_t board_input_start_lipo_monitor(void) {
|
||||||
|
if (!s_lipo1.ok && !s_lipo2.ok) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,16 +225,23 @@ static esp_err_t init_lipo_adc(void) {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
esp_err_t board_input_init_button(void) { return init_button(); }
|
||||||
|
|
||||||
esp_err_t board_input_init(void) {
|
esp_err_t board_input_init(void) {
|
||||||
esp_err_t err = init_button();
|
esp_err_t err = board_input_init_adc_only();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG_LIPO, "ADC init failed: %s", esp_err_to_name(err));
|
||||||
|
} else {
|
||||||
|
err = board_input_start_lipo_monitor();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG_LIPO, "monitor task failed: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = board_input_init_button();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGW(TAG_BTN, "init failed: %s", esp_err_to_name(err));
|
ESP_LOGW(TAG_BTN, "init failed: %s", esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
err = init_lipo_adc();
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG_LIPO, "init failed: %s", esp_err_to_name(err));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-1
@@ -1,12 +1,34 @@
|
|||||||
#ifndef BOARD_INPUT_H
|
#ifndef BOARD_INPUT_H
|
||||||
#define BOARD_INPUT_H
|
#define BOARD_INPUT_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
bool lipo1_valid;
|
||||||
|
bool lipo2_valid;
|
||||||
|
uint32_t lipo1_mv;
|
||||||
|
uint32_t lipo2_mv;
|
||||||
|
} board_lipo_reading_t;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Button (log on press) and LiPo ADC sampling (log every 10 s).
|
* Button (log on press) and LiPo ADC sampling (background log every 10 s).
|
||||||
* TODO: Pin assignments come from powerpod.h and may not match final hardware yet.
|
* TODO: Pin assignments come from powerpod.h and may not match final hardware yet.
|
||||||
*/
|
*/
|
||||||
esp_err_t board_input_init(void);
|
esp_err_t board_input_init(void);
|
||||||
|
|
||||||
|
/** LiPo ADC channels only (no button, no background task). */
|
||||||
|
esp_err_t board_input_init_adc_only(void);
|
||||||
|
|
||||||
|
/** Start 10 s LiPo monitor task (normal boot only). */
|
||||||
|
esp_err_t board_input_start_lipo_monitor(void);
|
||||||
|
|
||||||
|
/** Front-panel button debounce task. */
|
||||||
|
esp_err_t board_input_init_button(void);
|
||||||
|
|
||||||
|
/** On-demand ADC read of both LiPo sense inputs (if configured). */
|
||||||
|
void board_input_read_lipo(board_lipo_reading_t *out);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+96
-9
@@ -2,7 +2,7 @@
|
|||||||
* BMA456H integration for Powerpod (ESP-IDF I2C master + Bosch SensorAPI).
|
* BMA456H integration for Powerpod (ESP-IDF I2C master + Bosch SensorAPI).
|
||||||
*
|
*
|
||||||
* Polls accelerometer at 10 Hz; tap events arrive on BMA456_INT_GPIO.
|
* Polls accelerometer at 10 Hz; tap events arrive on BMA456_INT_GPIO.
|
||||||
* Accel logging is filtered in software (deadzone); see ACCEL_DEADZONE UART command.
|
* Accel logging is filtered in software (deadzone); slaves stream samples via ESP-NOW.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "bosch456.h"
|
#include "bosch456.h"
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/idf_additions.h"
|
#include "freertos/idf_additions.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include <rom/ets_sys.h>
|
#include <rom/ets_sys.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
@@ -34,6 +35,10 @@ static int16_t s_last_z;
|
|||||||
static bool s_have_last_sample;
|
static bool s_have_last_sample;
|
||||||
|
|
||||||
static volatile bool s_int_pending;
|
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);
|
static esp_err_t check_bma4(const char *api_name, int8_t rslt);
|
||||||
|
|
||||||
@@ -121,6 +126,35 @@ void bma456_set_accel_deadzone(uint32_t deadzone_lsb) {
|
|||||||
|
|
||||||
uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; }
|
uint32_t bma456_get_accel_deadzone(void) { return s_accel_deadzone; }
|
||||||
|
|
||||||
|
void bma456_set_tap_handler(bma456_tap_handler_t handler, void *ctx) {
|
||||||
|
s_tap_handler = handler;
|
||||||
|
s_tap_handler_ctx = ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t bma456_read_accel(int16_t *x, int16_t *y, int16_t *z) {
|
||||||
|
if (!s_bma456_ready || x == NULL || y == NULL || z == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
if (s_accel_mutex == NULL ||
|
||||||
|
xSemaphoreTake(s_accel_mutex, pdMS_TO_TICKS(500)) != pdTRUE) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct bma4_accel sens_data = {0};
|
||||||
|
int8_t ret = bma4_read_accel_xyz(&sens_data, &s_bma456);
|
||||||
|
xSemaphoreGive(s_accel_mutex);
|
||||||
|
|
||||||
|
if (ret != BMA4_OK) {
|
||||||
|
bma4_error_codes_print_result("bma4_read_accel_xyz", ret);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
*x = sens_data.x;
|
||||||
|
*y = sens_data.y;
|
||||||
|
*z = sens_data.z;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z) {
|
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z) {
|
||||||
if (!s_bma456_ready || !sample_exceeds_deadzone(x, y, z)) {
|
if (!s_bma456_ready || !sample_exceeds_deadzone(x, y, z)) {
|
||||||
return;
|
return;
|
||||||
@@ -157,10 +191,19 @@ static void handle_tap_interrupt(void) {
|
|||||||
|
|
||||||
if (tap_out.single_tap) {
|
if (tap_out.single_tap) {
|
||||||
ESP_LOGI(TAG, "tap: single");
|
ESP_LOGI(TAG, "tap: single");
|
||||||
|
if (s_tap_handler != NULL) {
|
||||||
|
s_tap_handler(BMA456_TAP_SINGLE, s_tap_handler_ctx);
|
||||||
|
}
|
||||||
} else if (tap_out.double_tap) {
|
} else if (tap_out.double_tap) {
|
||||||
ESP_LOGI(TAG, "tap: double");
|
ESP_LOGI(TAG, "tap: double");
|
||||||
|
if (s_tap_handler != NULL) {
|
||||||
|
s_tap_handler(BMA456_TAP_DOUBLE, s_tap_handler_ctx);
|
||||||
|
}
|
||||||
} else if (tap_out.triple_tap) {
|
} else if (tap_out.triple_tap) {
|
||||||
ESP_LOGI(TAG, "tap: triple");
|
ESP_LOGI(TAG, "tap: triple");
|
||||||
|
if (s_tap_handler != NULL) {
|
||||||
|
s_tap_handler(BMA456_TAP_TRIPLE, s_tap_handler_ctx);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,11 +230,19 @@ static void read_sensor_task(void *param) {
|
|||||||
struct bma4_accel sens_data = {0};
|
struct bma4_accel sens_data = {0};
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
int8_t ret = bma4_read_accel_xyz(&sens_data, &s_bma456);
|
bool got_sample = false;
|
||||||
if (ret == BMA4_OK) {
|
if (s_accel_mutex != NULL &&
|
||||||
|
xSemaphoreTake(s_accel_mutex, pdMS_TO_TICKS(500)) == pdTRUE) {
|
||||||
|
int8_t ret = bma4_read_accel_xyz(&sens_data, &s_bma456);
|
||||||
|
xSemaphoreGive(s_accel_mutex);
|
||||||
|
if (ret == BMA4_OK) {
|
||||||
|
got_sample = true;
|
||||||
|
} else {
|
||||||
|
bma4_error_codes_print_result("bma4_read_accel_xyz", ret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (got_sample) {
|
||||||
bma456_report_accel_if_changed(sens_data.x, sens_data.y, sens_data.z);
|
bma456_report_accel_if_changed(sens_data.x, sens_data.y, sens_data.z);
|
||||||
} else {
|
|
||||||
bma4_error_codes_print_result("bma4_read_accel_xyz", ret);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s_int_pending) {
|
if (s_int_pending) {
|
||||||
@@ -213,19 +264,48 @@ static esp_err_t configure_tap_interrupt(void) {
|
|||||||
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
|
if (check_bma4("bma456h_tap_get_parameter", ret) != ESP_OK) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
tap_settings.tap_sens_thres = 0;
|
tap_settings.tap_sens_thres = s_tap_config.tap_sens_thres;
|
||||||
|
tap_settings.max_gest_dur = s_tap_config.max_gest_dur;
|
||||||
|
tap_settings.tap_shock_dur = s_tap_config.tap_shock_dur;
|
||||||
|
tap_settings.quite_time_after_gest = s_tap_config.quite_time_after_gest;
|
||||||
|
tap_settings.wait_for_timeout = s_tap_config.wait_for_timeout;
|
||||||
|
tap_settings.axis_sel = s_tap_config.axis_sel;
|
||||||
ret = bma456h_tap_set_parameter(&tap_settings, &s_bma456);
|
ret = bma456h_tap_set_parameter(&tap_settings, &s_bma456);
|
||||||
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
|
if (check_bma4("bma456h_tap_set_parameter", ret) != ESP_OK) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
ret = bma456h_feature_enable(
|
uint16_t tap_features = 0;
|
||||||
(BMA456H_SINGLE_TAP_EN | BMA456H_DOUBLE_TAP_EN | BMA456H_TRIPLE_TAP_EN),
|
if (s_tap_config.enable_single) {
|
||||||
BMA4_ENABLE, &s_bma456);
|
tap_features |= BMA456H_SINGLE_TAP_EN;
|
||||||
|
}
|
||||||
|
if (s_tap_config.enable_double) {
|
||||||
|
tap_features |= BMA456H_DOUBLE_TAP_EN;
|
||||||
|
}
|
||||||
|
if (s_tap_config.enable_triple) {
|
||||||
|
tap_features |= BMA456H_TRIPLE_TAP_EN;
|
||||||
|
}
|
||||||
|
if (tap_features == 0) {
|
||||||
|
ESP_LOGW(TAG, "tap config: no tap kinds enabled");
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = bma456h_feature_enable(tap_features, BMA4_ENABLE, &s_bma456);
|
||||||
if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) {
|
if (check_bma4("bma456h_feature_enable", ret) != ESP_OK) {
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"tap config sens=%u max_gest=%u shock=%u quiet=%u wait=%u axis=%u "
|
||||||
|
"(single=%d double=%d triple=%d)",
|
||||||
|
(unsigned)s_tap_config.tap_sens_thres,
|
||||||
|
(unsigned)s_tap_config.max_gest_dur,
|
||||||
|
(unsigned)s_tap_config.tap_shock_dur,
|
||||||
|
(unsigned)s_tap_config.quite_time_after_gest,
|
||||||
|
(unsigned)s_tap_config.wait_for_timeout,
|
||||||
|
(unsigned)s_tap_config.axis_sel, s_tap_config.enable_single,
|
||||||
|
s_tap_config.enable_double, s_tap_config.enable_triple);
|
||||||
|
|
||||||
ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE,
|
ret = bma456h_map_interrupt(int_line, BMA456H_TAP_OUT_INT, BMA4_ENABLE,
|
||||||
&s_bma456);
|
&s_bma456);
|
||||||
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
|
if (check_bma4("bma456h_map_interrupt", ret) != ESP_OK) {
|
||||||
@@ -343,6 +423,13 @@ esp_err_t init_bma456(i2c_master_bus_handle_t bus_handle) {
|
|||||||
goto fail;
|
goto fail;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_accel_mutex == NULL) {
|
||||||
|
s_accel_mutex = xSemaphoreCreateMutex();
|
||||||
|
if (s_accel_mutex == NULL) {
|
||||||
|
goto fail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (xTaskCreate(read_sensor_task, "bma456_poll", 4096, NULL, 1, NULL) !=
|
if (xTaskCreate(read_sensor_task, "bma456_poll", 4096, NULL, 1, NULL) !=
|
||||||
pdPASS) {
|
pdPASS) {
|
||||||
goto fail;
|
goto fail;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
|
|
||||||
#include "driver/i2c_types.h"
|
#include "driver/i2c_types.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
/** 7-bit I2C address (SDO low). */
|
/** 7-bit I2C address (SDO low). */
|
||||||
#define BMA456_I2C_ADDR 0x18
|
#define BMA456_I2C_ADDR 0x18
|
||||||
@@ -20,6 +22,40 @@
|
|||||||
/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */
|
/** Software filter: log accel only when |axis - last| > deadzone (raw LSB). */
|
||||||
#define BMA456_DEFAULT_ACCEL_DEADZONE 100u
|
#define BMA456_DEFAULT_ACCEL_DEADZONE 100u
|
||||||
|
|
||||||
|
/**
|
||||||
|
* BMA456H multitap tuning (see BST-BMA456-AN000).
|
||||||
|
*
|
||||||
|
* Time fields use register units: value × 5 ms (e.g. 100 → 500 ms).
|
||||||
|
* tap_sens_thres: 0 = most sensitive … 15 = least (~78 mg per LSB).
|
||||||
|
* axis_sel: 0 = X, 1 = Y, 2 = Z.
|
||||||
|
* wait_for_timeout: 0 = report immediately, 1 = wait max_gest_dur for classification.
|
||||||
|
*/
|
||||||
|
typedef struct {
|
||||||
|
uint16_t tap_sens_thres;
|
||||||
|
uint16_t max_gest_dur;
|
||||||
|
uint16_t tap_shock_dur;
|
||||||
|
uint16_t quite_time_after_gest;
|
||||||
|
uint16_t wait_for_timeout;
|
||||||
|
uint16_t axis_sel;
|
||||||
|
bool enable_single;
|
||||||
|
bool enable_double;
|
||||||
|
bool enable_triple;
|
||||||
|
} bma456_tap_config_t;
|
||||||
|
|
||||||
|
/** Edit these values to tune tap detection, then rebuild. */
|
||||||
|
#define BMA456_TAP_CONFIG_DEFAULT \
|
||||||
|
{ \
|
||||||
|
.tap_sens_thres = 5, /* sensitive; 0=max, Bosch default=9 */ \
|
||||||
|
.max_gest_dur = 100, /* 500 ms window for double/triple */ \
|
||||||
|
.tap_shock_dur = 6, /* 30 ms debounce after each impulse */ \
|
||||||
|
.quite_time_after_gest = 60, /* 300 ms min gap between gestures */ \
|
||||||
|
.wait_for_timeout = 0, /* 0 = faster single-tap response */ \
|
||||||
|
.axis_sel = 2, /* Z axis */ \
|
||||||
|
.enable_single = true, \
|
||||||
|
.enable_double = true, \
|
||||||
|
.enable_triple = true, \
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probe and configure the sensor on bus_handle (100 kHz device).
|
* Probe and configure the sensor on bus_handle (100 kHz device).
|
||||||
* On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned;
|
* On failure the device is removed and ESP_ERR_NOT_FOUND / ESP_FAIL is returned;
|
||||||
@@ -35,4 +71,19 @@ uint32_t bma456_get_accel_deadzone(void);
|
|||||||
/** Log accel when any axis moved more than deadzone since last reported sample. */
|
/** Log accel when any axis moved more than deadzone since last reported sample. */
|
||||||
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z);
|
void bma456_report_accel_if_changed(int16_t x, int16_t y, int16_t z);
|
||||||
|
|
||||||
|
/** Tap kinds from BMA456H multitap output. */
|
||||||
|
typedef enum {
|
||||||
|
BMA456_TAP_SINGLE = 1,
|
||||||
|
BMA456_TAP_DOUBLE = 2,
|
||||||
|
BMA456_TAP_TRIPLE = 3,
|
||||||
|
} bma456_tap_kind_t;
|
||||||
|
|
||||||
|
typedef void (*bma456_tap_handler_t)(bma456_tap_kind_t kind, void *ctx);
|
||||||
|
|
||||||
|
/** Optional callback invoked from sensor task on tap interrupt (may be NULL). */
|
||||||
|
void bma456_set_tap_handler(bma456_tap_handler_t handler, void *ctx);
|
||||||
|
|
||||||
|
/** On-demand read of current accel XYZ (raw LSB). Returns ESP_ERR_INVALID_STATE if sensor not ready. */
|
||||||
|
esp_err_t bma456_read_accel(int16_t *x, int16_t *y, int16_t *z);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ typedef struct {
|
|||||||
|
|
||||||
static client_slot_t s_clients[CLIENT_REGISTRY_MAX];
|
static client_slot_t s_clients[CLIENT_REGISTRY_MAX];
|
||||||
|
|
||||||
|
static struct {
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
uint32_t updated_at;
|
||||||
|
} s_master_battery;
|
||||||
|
|
||||||
uint32_t client_registry_now_ms(void) {
|
uint32_t client_registry_now_ms(void) {
|
||||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||||
}
|
}
|
||||||
@@ -241,6 +246,286 @@ size_t client_registry_set_accel_deadzone_all(uint32_t deadzone) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void clear_client_accel(client_slot_t *slot) {
|
||||||
|
if (slot == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slot->info.accel_valid = false;
|
||||||
|
slot->info.accel_x = 0;
|
||||||
|
slot->info.accel_y = 0;
|
||||||
|
slot->info.accel_z = 0;
|
||||||
|
slot->info.accel_updated_at = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void clear_client_tap(client_slot_t *slot) {
|
||||||
|
if (slot == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
slot->info.tap_valid = false;
|
||||||
|
slot->info.tap_kind = 0;
|
||||||
|
slot->info.tap_updated_at = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool tap_kind_enabled(const client_info_t *info, uint32_t kind) {
|
||||||
|
if (info == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
switch (kind) {
|
||||||
|
case 1:
|
||||||
|
return info->tap_notify_single;
|
||||||
|
case 2:
|
||||||
|
return info->tap_notify_double;
|
||||||
|
case 3:
|
||||||
|
return info->tap_notify_triple;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_set_accel_stream(uint32_t client_id, bool enabled) {
|
||||||
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s_clients[i].info.accel_stream_enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
clear_client_accel(&s_clients[i]);
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_get_accel_stream(uint32_t client_id,
|
||||||
|
bool *enabled_out) {
|
||||||
|
if (enabled_out == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
const client_info_t *info = client_registry_find_by_id(client_id);
|
||||||
|
if (info == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
*enabled_out = info->accel_stream_enabled;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t client_registry_set_accel_stream_all(bool enabled) {
|
||||||
|
size_t n = 0;
|
||||||
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
if (!s_clients[i].active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s_clients[i].info.accel_stream_enabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
clear_client_accel(&s_clients[i]);
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_update_accel(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, int16_t x, int16_t y,
|
||||||
|
int16_t z) {
|
||||||
|
if (mac == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_slot_t *slot = find_slot(mac);
|
||||||
|
if (slot == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (slot->info.id != slave_id) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (!slot->info.accel_stream_enabled) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot->info.accel_x = x;
|
||||||
|
slot->info.accel_y = y;
|
||||||
|
slot->info.accel_z = z;
|
||||||
|
slot->info.accel_valid = true;
|
||||||
|
slot->info.accel_updated_at = now_ms();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_set_tap_notify(uint32_t client_id, bool single,
|
||||||
|
bool double_tap, bool triple) {
|
||||||
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s_clients[i].info.tap_notify_single = single;
|
||||||
|
s_clients[i].info.tap_notify_double = double_tap;
|
||||||
|
s_clients[i].info.tap_notify_triple = triple;
|
||||||
|
if (!single && !double_tap && !triple) {
|
||||||
|
clear_client_tap(&s_clients[i]);
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_get_tap_notify(uint32_t client_id, bool *single_out,
|
||||||
|
bool *double_tap_out,
|
||||||
|
bool *triple_out) {
|
||||||
|
if (single_out == NULL || double_tap_out == NULL || triple_out == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
const client_info_t *info = client_registry_find_by_id(client_id);
|
||||||
|
if (info == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
*single_out = info->tap_notify_single;
|
||||||
|
*double_tap_out = info->tap_notify_double;
|
||||||
|
*triple_out = info->tap_notify_triple;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t client_registry_set_tap_notify_all(bool single, bool double_tap,
|
||||||
|
bool triple) {
|
||||||
|
size_t n = 0;
|
||||||
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
if (!s_clients[i].active) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
s_clients[i].info.tap_notify_single = single;
|
||||||
|
s_clients[i].info.tap_notify_double = double_tap;
|
||||||
|
s_clients[i].info.tap_notify_triple = triple;
|
||||||
|
if (!single && !double_tap && !triple) {
|
||||||
|
clear_client_tap(&s_clients[i]);
|
||||||
|
}
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_update_tap(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, uint32_t kind) {
|
||||||
|
if (mac == NULL || kind < 1 || kind > 3) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_slot_t *slot = find_slot(mac);
|
||||||
|
if (slot == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
if (slot->info.id != slave_id) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
if (!tap_kind_enabled(&slot->info, kind)) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot->info.tap_kind = kind;
|
||||||
|
slot->info.tap_valid = true;
|
||||||
|
slot->info.tap_updated_at = now_ms();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void client_registry_expire_tap(client_info_t *info) {
|
||||||
|
if (info == NULL || !info->tap_valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (client_registry_ms_since(info->tap_updated_at) >
|
||||||
|
CLIENT_REGISTRY_TAP_MAX_AGE_MS) {
|
||||||
|
info->tap_valid = false;
|
||||||
|
info->tap_kind = 0;
|
||||||
|
info->tap_updated_at = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void client_registry_clear_tap(client_info_t *info) {
|
||||||
|
if (info == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
info->tap_valid = false;
|
||||||
|
info->tap_kind = 0;
|
||||||
|
info->tap_updated_at = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool client_registry_take_tap(uint32_t client_id, uint32_t *kind_out,
|
||||||
|
uint32_t *age_ms_out) {
|
||||||
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
if (!s_clients[i].active || s_clients[i].info.id != client_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
client_info_t *info = &s_clients[i].info;
|
||||||
|
client_registry_expire_tap(info);
|
||||||
|
if (!info->tap_valid) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (kind_out != NULL) {
|
||||||
|
*kind_out = info->tap_kind;
|
||||||
|
}
|
||||||
|
if (age_ms_out != NULL) {
|
||||||
|
*age_ms_out = client_registry_ms_since(info->tap_updated_at);
|
||||||
|
}
|
||||||
|
client_registry_clear_tap(info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void client_registry_set_master_battery(const board_lipo_reading_t *reading) {
|
||||||
|
if (reading == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_master_battery.reading = *reading;
|
||||||
|
s_master_battery.updated_at = now_ms();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool client_registry_get_master_battery(board_lipo_reading_t *reading_out,
|
||||||
|
uint32_t *age_ms_out) {
|
||||||
|
if (reading_out == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
*reading_out = s_master_battery.reading;
|
||||||
|
if (age_ms_out != NULL) {
|
||||||
|
*age_ms_out = client_registry_ms_since(s_master_battery.updated_at);
|
||||||
|
}
|
||||||
|
return s_master_battery.updated_at != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t client_registry_update_battery(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, bool lipo1_valid,
|
||||||
|
uint32_t lipo1_mv, bool lipo2_valid,
|
||||||
|
uint32_t lipo2_mv) {
|
||||||
|
if (mac == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
client_slot_t *slot = find_slot(mac);
|
||||||
|
if (slot == NULL) {
|
||||||
|
bool is_new = false;
|
||||||
|
esp_err_t err = client_registry_upsert(mac, slave_id, 0, true, false, &is_new);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
slot = find_slot(mac);
|
||||||
|
if (slot == NULL) {
|
||||||
|
return ESP_ERR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "battery auto-registered id=%lu (report before heartbeat)",
|
||||||
|
(unsigned long)slave_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (slot->info.id != slave_id) {
|
||||||
|
ESP_LOGW(TAG, "battery id %lu → %lu for mac %02x:…:%02x",
|
||||||
|
(unsigned long)slot->info.id, (unsigned long)slave_id, mac[0],
|
||||||
|
mac[5]);
|
||||||
|
slot->info.id = slave_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
slot->info.lipo1_valid = lipo1_valid;
|
||||||
|
slot->info.lipo2_valid = lipo2_valid;
|
||||||
|
slot->info.lipo1_mv = lipo1_mv;
|
||||||
|
slot->info.lipo2_mv = lipo2_mv;
|
||||||
|
slot->info.battery_updated_at = now_ms();
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
const client_info_t *client_registry_at(size_t index) {
|
const client_info_t *client_registry_at(size_t index) {
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
for (size_t i = 0; i < CLIENT_REGISTRY_MAX; i++) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#ifndef CLIENT_REGISTRY_H
|
#ifndef CLIENT_REGISTRY_H
|
||||||
#define CLIENT_REGISTRY_H
|
#define CLIENT_REGISTRY_H
|
||||||
|
|
||||||
|
#include "board_input.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
@@ -21,9 +22,33 @@ typedef struct {
|
|||||||
uint32_t version;
|
uint32_t version;
|
||||||
/** Accel deadzone in raw LSB per axis (master copy for ESP-NOW config). */
|
/** Accel deadzone in raw LSB per axis (master copy for ESP-NOW config). */
|
||||||
uint32_t accel_deadzone;
|
uint32_t accel_deadzone;
|
||||||
|
/** Latest accel from slave ESP-NOW stream (master only). */
|
||||||
|
bool accel_valid;
|
||||||
|
int16_t accel_x;
|
||||||
|
int16_t accel_y;
|
||||||
|
int16_t accel_z;
|
||||||
|
uint32_t accel_updated_at;
|
||||||
|
/** Host-enabled ESP-NOW accel stream to master. */
|
||||||
|
bool accel_stream_enabled;
|
||||||
|
/** Host-enabled ESP-NOW tap notify flags. */
|
||||||
|
bool tap_notify_single;
|
||||||
|
bool tap_notify_double;
|
||||||
|
bool tap_notify_triple;
|
||||||
|
/** Latest tap from slave ESP-NOW (master only, short-lived cache). */
|
||||||
|
bool tap_valid;
|
||||||
|
uint32_t tap_kind;
|
||||||
|
uint32_t tap_updated_at;
|
||||||
|
/** Latest LiPo ADC from slave ESP-NOW battery report (~30 s). */
|
||||||
|
bool lipo1_valid;
|
||||||
|
bool lipo2_valid;
|
||||||
|
uint32_t lipo1_mv;
|
||||||
|
uint32_t lipo2_mv;
|
||||||
|
uint32_t battery_updated_at;
|
||||||
} client_info_t;
|
} client_info_t;
|
||||||
|
|
||||||
#define CLIENT_REGISTRY_DEFAULT_ACCEL_DEADZONE 100u
|
#define CLIENT_REGISTRY_DEFAULT_ACCEL_DEADZONE 100u
|
||||||
|
/** Tap events older than this are discarded (matches accel stream interval). */
|
||||||
|
#define CLIENT_REGISTRY_TAP_MAX_AGE_MS 16u
|
||||||
|
|
||||||
void client_registry_init(void);
|
void client_registry_init(void);
|
||||||
|
|
||||||
@@ -63,4 +88,49 @@ esp_err_t client_registry_get_accel_deadzone(uint32_t client_id,
|
|||||||
/** Push deadzone to all active registry entries; returns count updated. */
|
/** Push deadzone to all active registry entries; returns count updated. */
|
||||||
size_t client_registry_set_accel_deadzone_all(uint32_t deadzone);
|
size_t client_registry_set_accel_deadzone_all(uint32_t deadzone);
|
||||||
|
|
||||||
|
/** Store latest accel sample from a slave (matched by sender MAC). */
|
||||||
|
esp_err_t client_registry_update_accel(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, int16_t x, int16_t y,
|
||||||
|
int16_t z);
|
||||||
|
|
||||||
|
esp_err_t client_registry_set_accel_stream(uint32_t client_id, bool enabled);
|
||||||
|
esp_err_t client_registry_get_accel_stream(uint32_t client_id, bool *enabled_out);
|
||||||
|
size_t client_registry_set_accel_stream_all(bool enabled);
|
||||||
|
|
||||||
|
esp_err_t client_registry_set_tap_notify(uint32_t client_id, bool single,
|
||||||
|
bool double_tap, bool triple);
|
||||||
|
esp_err_t client_registry_get_tap_notify(uint32_t client_id, bool *single_out,
|
||||||
|
bool *double_tap_out,
|
||||||
|
bool *triple_out);
|
||||||
|
size_t client_registry_set_tap_notify_all(bool single, bool double_tap,
|
||||||
|
bool triple);
|
||||||
|
|
||||||
|
/** Store tap event from slave (matched by sender MAC). kind: 1=single, 2=double, 3=triple. */
|
||||||
|
esp_err_t client_registry_update_tap(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, uint32_t kind);
|
||||||
|
|
||||||
|
/** Drop cached tap if older than CLIENT_REGISTRY_TAP_MAX_AGE_MS. */
|
||||||
|
void client_registry_expire_tap(client_info_t *info);
|
||||||
|
|
||||||
|
/** Clear cached tap after UART snapshot or expiry. */
|
||||||
|
void client_registry_clear_tap(client_info_t *info);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If client has a fresh tap (age <= CLIENT_REGISTRY_TAP_MAX_AGE_MS), copy it out
|
||||||
|
* and clear the cache. Returns true when an event was returned.
|
||||||
|
*/
|
||||||
|
bool client_registry_take_tap(uint32_t client_id, uint32_t *kind_out,
|
||||||
|
uint32_t *age_ms_out);
|
||||||
|
|
||||||
|
/** Master local LiPo (client_id 0 in UART battery responses). */
|
||||||
|
void client_registry_set_master_battery(const board_lipo_reading_t *reading);
|
||||||
|
bool client_registry_get_master_battery(board_lipo_reading_t *reading_out,
|
||||||
|
uint32_t *age_ms_out);
|
||||||
|
|
||||||
|
/** Store latest battery report from a slave (matched by sender MAC). */
|
||||||
|
esp_err_t client_registry_update_battery(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t slave_id, bool lipo1_valid,
|
||||||
|
uint32_t lipo1_mv, bool lipo2_valid,
|
||||||
|
uint32_t lipo2_mv);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
#include "client_registry.h"
|
||||||
|
#include "cmd_accel_stream.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[ACCEL_STREAM]";
|
||||||
|
|
||||||
|
static void reply(bool enabled, uint32_t client_id, bool success,
|
||||||
|
uint32_t slaves_updated) {
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_ACCEL_STREAM,
|
||||||
|
alox_UartMessage_accel_stream_response_tag);
|
||||||
|
response.payload.accel_stream_response.enabled = enabled;
|
||||||
|
response.payload.accel_stream_response.client_id = client_id;
|
||||||
|
response.payload.accel_stream_response.success = success;
|
||||||
|
response.payload.accel_stream_response.slaves_updated = slaves_updated;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t push_stream_to_slave(const client_info_t *client, bool enable) {
|
||||||
|
if (client == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
esp_err_t err = client_registry_set_accel_stream(client->id, enable);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return esp_now_comm_send_accel_stream(client->mac, client->id, enable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_accel_stream(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
alox_AccelStreamRequest req = alox_AccelStreamRequest_init_zero;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
ESP_LOGW(TAG, "missing accel_stream_request");
|
||||||
|
reply(false, 0, false, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req = *req_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.write) {
|
||||||
|
if (req.all_clients) {
|
||||||
|
size_t n = client_registry_set_accel_stream_all(req.enable);
|
||||||
|
uint32_t sent = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < client_registry_count(); i++) {
|
||||||
|
const client_info_t *client = client_registry_at(i);
|
||||||
|
if (client == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (esp_now_comm_send_accel_stream(client->mac, client->id,
|
||||||
|
req.enable) == ESP_OK) {
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "accel stream %s for %u/%u slaves",
|
||||||
|
req.enable ? "on" : "off", (unsigned)sent, (unsigned)n);
|
||||||
|
reply(req.enable, 0, sent > 0, sent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.client_id == 0) {
|
||||||
|
ESP_LOGW(TAG, "client_id required (or all_clients)");
|
||||||
|
reply(req.enable, 0, false, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_info_t *client = client_registry_find_by_id(req.client_id);
|
||||||
|
if (client == NULL) {
|
||||||
|
ESP_LOGW(TAG, "client id %lu not found", (unsigned long)req.client_id);
|
||||||
|
reply(req.enable, req.client_id, false, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = push_stream_to_slave(client, req.enable);
|
||||||
|
reply(req.enable, req.client_id, err == ESP_OK, err == ESP_OK ? 1u : 0u);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.all_clients || req.client_id == 0) {
|
||||||
|
reply(false, 0, false, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool enabled = false;
|
||||||
|
esp_err_t err = client_registry_get_accel_stream(req.client_id, &enabled);
|
||||||
|
reply(enabled, req.client_id, err == ESP_OK, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_accel_stream_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_ACCEL_STREAM, handle_accel_stream);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_ACCEL_STREAM_H
|
||||||
|
#define CMD_ACCEL_STREAM_H
|
||||||
|
|
||||||
|
void cmd_accel_stream_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
#include "cmd_battery.h"
|
||||||
|
#include "board_input.h"
|
||||||
|
#include "client_registry.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[BATTERY]";
|
||||||
|
|
||||||
|
static void fill_lipo(alox_LipoReading *dst, bool *has_dst, bool valid,
|
||||||
|
uint32_t mv) {
|
||||||
|
if (dst == NULL || has_dst == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
*has_dst = true;
|
||||||
|
dst->valid = valid;
|
||||||
|
dst->voltage_mv = valid ? mv : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool append_battery_sample(alox_BatteryStatusResponse *resp,
|
||||||
|
uint32_t client_id, bool lipo1_valid,
|
||||||
|
uint32_t lipo1_mv, bool lipo2_valid,
|
||||||
|
uint32_t lipo2_mv, uint32_t age_ms) {
|
||||||
|
if (resp->samples_count >=
|
||||||
|
sizeof(resp->samples) / sizeof(resp->samples[0])) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_BatterySample *sample = &resp->samples[resp->samples_count++];
|
||||||
|
sample->client_id = client_id;
|
||||||
|
fill_lipo(&sample->lipo1, &sample->has_lipo1, lipo1_valid, lipo1_mv);
|
||||||
|
fill_lipo(&sample->lipo2, &sample->has_lipo2, lipo2_valid, lipo2_mv);
|
||||||
|
sample->age_ms = age_ms;
|
||||||
|
return lipo1_valid || lipo2_valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool append_master_sample(alox_BatteryStatusResponse *resp) {
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
client_registry_set_master_battery(&reading);
|
||||||
|
|
||||||
|
return append_battery_sample(resp, 0, reading.lipo1_valid, reading.lipo1_mv,
|
||||||
|
reading.lipo2_valid, reading.lipo2_mv, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool append_slave_cached(alox_BatteryStatusResponse *resp,
|
||||||
|
const client_info_t *client) {
|
||||||
|
if (client == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (client->battery_updated_at == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return append_battery_sample(
|
||||||
|
resp, client->id, client->lipo1_valid, client->lipo1_mv,
|
||||||
|
client->lipo2_valid, client->lipo2_mv,
|
||||||
|
client_registry_ms_since(client->battery_updated_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_battery_status(const uint8_t *data, size_t len) {
|
||||||
|
alox_BatteryStatusRequest req = alox_BatteryStatusRequest_init_zero;
|
||||||
|
|
||||||
|
if (len > 0) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
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) {
|
||||||
|
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,
|
||||||
|
alox_UartMessage_battery_status_response_tag);
|
||||||
|
alox_BatteryStatusResponse *resp =
|
||||||
|
&response.payload.battery_status_response;
|
||||||
|
resp->success = false;
|
||||||
|
resp->samples_count = 0;
|
||||||
|
|
||||||
|
bool any = false;
|
||||||
|
|
||||||
|
if (req.all_clients) {
|
||||||
|
any |= append_master_sample(resp);
|
||||||
|
for (size_t i = 0; i < client_registry_count(); i++) {
|
||||||
|
const client_info_t *client = client_registry_at(i);
|
||||||
|
if (client == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
any |= append_slave_cached(resp, client);
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "battery cache all_clients → %u samples",
|
||||||
|
(unsigned)resp->samples_count);
|
||||||
|
} else if (req.client_id == 0) {
|
||||||
|
any = append_master_sample(resp);
|
||||||
|
ESP_LOGI(TAG, "battery cache master");
|
||||||
|
} else {
|
||||||
|
const client_info_t *client = client_registry_find_by_id(req.client_id);
|
||||||
|
if (client != NULL) {
|
||||||
|
any = append_slave_cached(resp, client);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "client %lu not in registry", (unsigned long)req.client_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp->success = any;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_battery_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_BATTERY_STATUS, handle_battery_status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_BATTERY_H
|
||||||
|
#define CMD_BATTERY_H
|
||||||
|
|
||||||
|
void cmd_battery_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#include "client_registry.h"
|
||||||
|
#include "cmd_cache_status.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[CACHE_STAT]";
|
||||||
|
|
||||||
|
static bool tap_notify_any(const client_info_t *client) {
|
||||||
|
return client != NULL &&
|
||||||
|
(client->tap_notify_single || client->tap_notify_double ||
|
||||||
|
client->tap_notify_triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
static alox_TapKind tap_kind_from_registry(uint32_t kind) {
|
||||||
|
switch (kind) {
|
||||||
|
case 1:
|
||||||
|
return alox_TapKind_TAP_SINGLE;
|
||||||
|
case 2:
|
||||||
|
return alox_TapKind_TAP_DOUBLE;
|
||||||
|
case 3:
|
||||||
|
return alox_TapKind_TAP_TRIPLE;
|
||||||
|
default:
|
||||||
|
return alox_TapKind_TAP_NONE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_cache_status(alox_CacheStatusResponse *out) {
|
||||||
|
if (out == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out->clients_count = 0;
|
||||||
|
|
||||||
|
size_t count = client_registry_count();
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
const client_info_t *client = client_registry_at(i);
|
||||||
|
if (client == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool want_accel = client->accel_stream_enabled;
|
||||||
|
const bool want_tap = tap_notify_any(client);
|
||||||
|
if (!want_accel && !want_tap) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (out->clients_count >=
|
||||||
|
sizeof(out->clients) / sizeof(out->clients[0])) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_CacheClientStatus *entry = &out->clients[out->clients_count++];
|
||||||
|
entry->client_id = client->id;
|
||||||
|
entry->has_accel = false;
|
||||||
|
entry->has_tap = false;
|
||||||
|
|
||||||
|
if (want_accel) {
|
||||||
|
entry->has_accel = true;
|
||||||
|
entry->accel.valid = client->accel_valid;
|
||||||
|
if (client->accel_valid) {
|
||||||
|
entry->accel.x = client->accel_x;
|
||||||
|
entry->accel.y = client->accel_y;
|
||||||
|
entry->accel.z = client->accel_z;
|
||||||
|
entry->accel.age_ms =
|
||||||
|
client_registry_ms_since(client->accel_updated_at);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (want_tap) {
|
||||||
|
uint32_t kind = 0;
|
||||||
|
uint32_t age_ms = 0;
|
||||||
|
if (client_registry_take_tap(client->id, &kind, &age_ms)) {
|
||||||
|
entry->has_tap = true;
|
||||||
|
entry->tap.kind = tap_kind_from_registry(kind);
|
||||||
|
entry->tap.age_ms = age_ms;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_cache_status(const uint8_t *data, size_t len) {
|
||||||
|
(void)data;
|
||||||
|
(void)len;
|
||||||
|
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_CACHE_STATUS,
|
||||||
|
alox_UartMessage_cache_status_response_tag);
|
||||||
|
fill_cache_status(&response.payload.cache_status_response);
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_cache_status_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_CACHE_STATUS, handle_cache_status);
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
void cmd_cache_status_register(void);
|
||||||
@@ -27,6 +27,10 @@ static bool encode_clients_list(pb_ostream_t *stream, const pb_field_t *field,
|
|||||||
proto.last_success_ping =
|
proto.last_success_ping =
|
||||||
client_registry_ms_since(client->last_success_ping_at);
|
client_registry_ms_since(client->last_success_ping_at);
|
||||||
proto.version = client->version;
|
proto.version = client->version;
|
||||||
|
proto.accel_stream_enabled = client->accel_stream_enabled;
|
||||||
|
proto.tap_notify_single = client->tap_notify_single;
|
||||||
|
proto.tap_notify_double = client->tap_notify_double;
|
||||||
|
proto.tap_notify_triple = client->tap_notify_triple;
|
||||||
proto.mac.funcs.encode = uart_cmd_encode_bytes;
|
proto.mac.funcs.encode = uart_cmd_encode_bytes;
|
||||||
proto.mac.arg = &mac;
|
proto.mac.arg = &mac;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
#include "client_registry.h"
|
||||||
|
#include "cmd_espnow_echo_ping.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[ECHO_PING]";
|
||||||
|
|
||||||
|
static void reply(bool success, uint32_t client_id, uint64_t timestamp_us,
|
||||||
|
uint32_t esp_rtt_us) {
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_ESPNOW_ECHO_PING,
|
||||||
|
alox_UartMessage_espnow_echo_ping_response_tag);
|
||||||
|
response.payload.espnow_echo_ping_response.success = success;
|
||||||
|
response.payload.espnow_echo_ping_response.client_id = client_id;
|
||||||
|
response.payload.espnow_echo_ping_response.timestamp_us = timestamp_us;
|
||||||
|
response.payload.espnow_echo_ping_response.esp_rtt_us = esp_rtt_us;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_espnow_echo_ping(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
|
||||||
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "decode failed");
|
||||||
|
reply(false, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alox_EspNowEchoPingRequest *req = UART_CMD_REQ(
|
||||||
|
&uart_msg, alox_UartMessage_espnow_echo_ping_request_tag,
|
||||||
|
espnow_echo_ping_request);
|
||||||
|
if (req == NULL || req->client_id == 0) {
|
||||||
|
ESP_LOGW(TAG, "need client_id in request");
|
||||||
|
reply(false, 0, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_info_t *client = client_registry_find_by_id(req->client_id);
|
||||||
|
if (client == NULL) {
|
||||||
|
ESP_LOGW(TAG, "client id %lu not in registry",
|
||||||
|
(unsigned long)req->client_id);
|
||||||
|
reply(false, req->client_id, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UART request client_id=%lu host_ts=%llu",
|
||||||
|
(unsigned long)req->client_id,
|
||||||
|
(unsigned long long)req->timestamp_us);
|
||||||
|
|
||||||
|
esp_now_echo_ping_result_t ping_result = {0};
|
||||||
|
esp_err_t err =
|
||||||
|
esp_now_comm_echo_ping(client->mac, req->timestamp_us, &ping_result);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "echo ping to id=%lu failed: %s", (unsigned long)req->client_id,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
reply(false, req->client_id, 0, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UART reply client_id=%lu host_ts=%llu esp_rtt_us=%lu",
|
||||||
|
(unsigned long)req->client_id,
|
||||||
|
(unsigned long long)ping_result.echoed_us,
|
||||||
|
(unsigned long)ping_result.esp_rtt_us);
|
||||||
|
reply(true, req->client_id, ping_result.echoed_us, ping_result.esp_rtt_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_espnow_echo_ping_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_ESPNOW_ECHO_PING, handle_espnow_echo_ping);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_ESPNOW_ECHO_PING_H
|
||||||
|
#define CMD_ESPNOW_ECHO_PING_H
|
||||||
|
|
||||||
|
void cmd_espnow_echo_ping_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
+20
-21
@@ -1,4 +1,5 @@
|
|||||||
#include "cmd_handler.h"
|
#include "cmd_handler.h"
|
||||||
|
#include "ota_session.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
@@ -48,6 +49,18 @@ static const char *message_type_name(uint16_t id) {
|
|||||||
return "FIND_ME";
|
return "FIND_ME";
|
||||||
case alox_MessageType_RESTART:
|
case alox_MessageType_RESTART:
|
||||||
return "RESTART";
|
return "RESTART";
|
||||||
|
case alox_MessageType_ACCEL_STREAM:
|
||||||
|
return "ACCEL_STREAM";
|
||||||
|
case alox_MessageType_BATTERY_STATUS:
|
||||||
|
return "BATTERY_STATUS";
|
||||||
|
case alox_MessageType_TAP_NOTIFY:
|
||||||
|
return "TAP_NOTIFY";
|
||||||
|
case alox_MessageType_CACHE_STATUS:
|
||||||
|
return "CACHE_STATUS";
|
||||||
|
case alox_MessageType_ESPNOW_ECHO_PING:
|
||||||
|
return "ESPNOW_ECHO_PING";
|
||||||
|
case alox_MessageType_SET_LOG_LEVEL:
|
||||||
|
return "SET_LOG_LEVEL";
|
||||||
default:
|
default:
|
||||||
return "UNKNOWN";
|
return "UNKNOWN";
|
||||||
}
|
}
|
||||||
@@ -80,33 +93,19 @@ esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb) {
|
|||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len) {
|
|
||||||
if (cmd_queue == NULL) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
generic_msg_t msg = {.msg_id = id, .len = len, .payload = NULL};
|
|
||||||
if (len > 0) {
|
|
||||||
msg.payload = malloc(len);
|
|
||||||
if (msg.payload == NULL) {
|
|
||||||
return ESP_ERR_NO_MEM;
|
|
||||||
}
|
|
||||||
memcpy(msg.payload, data, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (xQueueSend(cmd_queue, &msg, pdMS_TO_TICKS(100)) != pdPASS) {
|
|
||||||
free(msg.payload);
|
|
||||||
return ESP_ERR_TIMEOUT;
|
|
||||||
}
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
void vCmdDispatcherTask(void *param) {
|
void vCmdDispatcherTask(void *param) {
|
||||||
(void)param;
|
(void)param;
|
||||||
generic_msg_t msg;
|
generic_msg_t msg;
|
||||||
|
|
||||||
while (1) {
|
while (1) {
|
||||||
if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) {
|
if (xQueueReceive(cmd_queue, &msg, portMAX_DELAY) == pdPASS) {
|
||||||
|
if (!ota_session_uart_cmd_allowed(msg.msg_id)) {
|
||||||
|
ESP_LOGW(TAG, "reject %s (0x%02x) during OTA session",
|
||||||
|
message_type_name(msg.msg_id), (unsigned)msg.msg_id);
|
||||||
|
free(msg.payload);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
bool handled = false;
|
bool handled = false;
|
||||||
for (int i = 0; i < handler_count; i++) {
|
for (int i = 0; i < handler_count; i++) {
|
||||||
if (handlers[i].msg_id == msg.msg_id) {
|
if (handlers[i].msg_id == msg.msg_id) {
|
||||||
|
|||||||
@@ -21,6 +21,5 @@ void init_cmdHandler(QueueHandle_t queue);
|
|||||||
void vCmdDispatcherTask(void *param);
|
void vCmdDispatcherTask(void *param);
|
||||||
|
|
||||||
esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb);
|
esp_err_t msg_register_handler(uint16_t id, msg_callback_t cb);
|
||||||
esp_err_t msg_post(uint16_t id, const uint8_t *data, size_t len);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+134
-72
@@ -1,5 +1,7 @@
|
|||||||
#include "cmd_led_ring.h"
|
#include "cmd_led_ring.h"
|
||||||
|
#include "client_registry.h"
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
#include "led_ring.h"
|
#include "led_ring.h"
|
||||||
#include "uart_cmd.h"
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
@@ -10,6 +12,8 @@ static const char *TAG = "[LED_RING_CMD]";
|
|||||||
#define LED_RING_MODE_DIGIT 2
|
#define LED_RING_MODE_DIGIT 2
|
||||||
#define LED_RING_MODE_BLINK 3
|
#define LED_RING_MODE_BLINK 3
|
||||||
#define LED_RING_MODE_FIND_ME 4
|
#define LED_RING_MODE_FIND_ME 4
|
||||||
|
#define LED_RING_MODE_COLOR 5
|
||||||
|
#define LED_RING_MODE_BATTERY_LOW 6
|
||||||
|
|
||||||
static uint8_t clamp_u8(uint32_t v) {
|
static uint8_t clamp_u8(uint32_t v) {
|
||||||
if (v > 255) {
|
if (v > 255) {
|
||||||
@@ -32,7 +36,86 @@ static uint8_t resolve_intensity(uint32_t intensity) {
|
|||||||
return clamp_u8(intensity);
|
return clamp_u8(intensity);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit) {
|
bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req) {
|
||||||
|
if (req == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t mode = req->mode;
|
||||||
|
uint8_t r = clamp_u8(req->r);
|
||||||
|
uint8_t g = clamp_u8(req->g);
|
||||||
|
uint8_t b = clamp_u8(req->b);
|
||||||
|
uint8_t intensity = resolve_intensity(req->intensity);
|
||||||
|
led_command_t cmd = {0};
|
||||||
|
|
||||||
|
switch (mode) {
|
||||||
|
case LED_RING_MODE_CLEAR:
|
||||||
|
cmd.mode = LED_CMD_CLEAR;
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case LED_RING_MODE_COLOR:
|
||||||
|
cmd.mode = LED_CMD_SET_COLOR;
|
||||||
|
cmd.r = r;
|
||||||
|
cmd.g = g;
|
||||||
|
cmd.b = b;
|
||||||
|
cmd.intensity = intensity;
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case LED_RING_MODE_PROGRESS: {
|
||||||
|
cmd.mode = LED_CMD_PROGRESS;
|
||||||
|
cmd.progress = clamp_progress(req->progress);
|
||||||
|
cmd.r = r;
|
||||||
|
cmd.g = g;
|
||||||
|
cmd.b = b;
|
||||||
|
cmd.intensity = intensity;
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LED_RING_MODE_DIGIT:
|
||||||
|
if (req->digit > 10) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cmd.mode = LED_CMD_SET_DIGIT;
|
||||||
|
cmd.value = (uint8_t)req->digit;
|
||||||
|
cmd.r = r;
|
||||||
|
cmd.g = g;
|
||||||
|
cmd.b = b;
|
||||||
|
cmd.intensity = intensity;
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case LED_RING_MODE_FIND_ME:
|
||||||
|
led_ring_find_me();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case LED_RING_MODE_BATTERY_LOW:
|
||||||
|
led_ring_show_battery_low();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case LED_RING_MODE_BLINK:
|
||||||
|
cmd.mode = LED_CMD_BLINK;
|
||||||
|
cmd.r = r;
|
||||||
|
cmd.g = g;
|
||||||
|
cmd.b = b;
|
||||||
|
cmd.intensity = intensity;
|
||||||
|
cmd.blink_ms = (uint16_t)(req->blink_ms > 0 ? req->blink_ms : 350);
|
||||||
|
cmd.blink_count = req->blink_count > 0 ? (uint8_t)req->blink_count : 1;
|
||||||
|
if (cmd.blink_count == 0) {
|
||||||
|
cmd.blink_count = 1;
|
||||||
|
}
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit,
|
||||||
|
uint32_t client_id, uint32_t slaves_updated) {
|
||||||
alox_UartMessage response;
|
alox_UartMessage response;
|
||||||
uart_cmd_init_response(&response, alox_MessageType_LED_RING,
|
uart_cmd_init_response(&response, alox_MessageType_LED_RING,
|
||||||
alox_UartMessage_led_ring_progress_response_tag);
|
alox_UartMessage_led_ring_progress_response_tag);
|
||||||
@@ -40,16 +123,26 @@ static void reply(bool success, uint32_t mode, uint32_t progress, uint32_t digit
|
|||||||
response.payload.led_ring_progress_response.mode = mode;
|
response.payload.led_ring_progress_response.mode = mode;
|
||||||
response.payload.led_ring_progress_response.progress = progress;
|
response.payload.led_ring_progress_response.progress = progress;
|
||||||
response.payload.led_ring_progress_response.digit = digit;
|
response.payload.led_ring_progress_response.digit = digit;
|
||||||
|
response.payload.led_ring_progress_response.client_id = client_id;
|
||||||
|
response.payload.led_ring_progress_response.slaves_updated = slaves_updated;
|
||||||
uart_cmd_send(&response, TAG);
|
uart_cmd_send(&response, TAG);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static esp_err_t push_led_ring_to_slave(const client_info_t *client,
|
||||||
|
const alox_LedRingProgressRequest *req) {
|
||||||
|
if (client == NULL || req == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
return esp_now_comm_send_led_ring(client->mac, client->id, req);
|
||||||
|
}
|
||||||
|
|
||||||
static void handle_led_ring(const uint8_t *data, size_t len) {
|
static void handle_led_ring(const uint8_t *data, size_t len) {
|
||||||
alox_UartMessage uart_msg;
|
alox_UartMessage uart_msg;
|
||||||
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
|
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
|
||||||
|
|
||||||
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");
|
ESP_LOGW(TAG, "decode failed");
|
||||||
reply(false, 0, 0, 0);
|
reply(false, 0, 0, 0, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,84 +154,53 @@ static void handle_led_ring(const uint8_t *data, size_t len) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint32_t mode = req.mode;
|
uint32_t mode = req.mode;
|
||||||
uint8_t r = clamp_u8(req.r);
|
|
||||||
uint8_t g = clamp_u8(req.g);
|
|
||||||
uint8_t b = clamp_u8(req.b);
|
|
||||||
uint8_t intensity = resolve_intensity(req.intensity);
|
|
||||||
|
|
||||||
led_command_t cmd = {0};
|
if (req.all_clients) {
|
||||||
|
size_t n = client_registry_count();
|
||||||
switch (mode) {
|
uint32_t sent = 0;
|
||||||
case LED_RING_MODE_CLEAR:
|
for (size_t i = 0; i < n; i++) {
|
||||||
cmd.mode = LED_CMD_CLEAR;
|
const client_info_t *client = client_registry_at(i);
|
||||||
led_ring_send_command(&cmd);
|
if (client == NULL) {
|
||||||
ESP_LOGI(TAG, "clear");
|
continue;
|
||||||
reply(true, mode, 0, 0);
|
}
|
||||||
return;
|
if (push_led_ring_to_slave(client, &req) == ESP_OK) {
|
||||||
|
sent++;
|
||||||
case LED_RING_MODE_PROGRESS: {
|
}
|
||||||
uint8_t progress = clamp_progress(req.progress);
|
|
||||||
cmd.mode = LED_CMD_PROGRESS;
|
|
||||||
cmd.progress = progress;
|
|
||||||
cmd.r = r;
|
|
||||||
cmd.g = g;
|
|
||||||
cmd.b = b;
|
|
||||||
cmd.intensity = intensity;
|
|
||||||
led_ring_send_command(&cmd);
|
|
||||||
ESP_LOGI(TAG, "progress %u%% rgb=%u,%u,%u", (unsigned)progress,
|
|
||||||
(unsigned)r, (unsigned)g, (unsigned)b);
|
|
||||||
reply(true, mode, progress, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case LED_RING_MODE_DIGIT: {
|
|
||||||
if (req.digit > 10) {
|
|
||||||
ESP_LOGW(TAG, "digit %lu out of range", (unsigned long)req.digit);
|
|
||||||
reply(false, mode, 0, req.digit);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
cmd.mode = LED_CMD_SET_DIGIT;
|
bool local_ok = true;
|
||||||
cmd.value = (uint8_t)req.digit;
|
if (!req.slaves_only) {
|
||||||
cmd.r = r;
|
local_ok = cmd_led_ring_apply(&req);
|
||||||
cmd.g = g;
|
|
||||||
cmd.b = b;
|
|
||||||
cmd.intensity = intensity;
|
|
||||||
led_ring_send_command(&cmd);
|
|
||||||
ESP_LOGI(TAG, "digit %u rgb=%u,%u,%u", (unsigned)cmd.value, (unsigned)r,
|
|
||||||
(unsigned)g, (unsigned)b);
|
|
||||||
reply(true, mode, 0, req.digit);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
case LED_RING_MODE_FIND_ME:
|
|
||||||
led_ring_find_me();
|
|
||||||
ESP_LOGI(TAG, "find-me");
|
|
||||||
reply(true, mode, 0, 0);
|
|
||||||
return;
|
|
||||||
|
|
||||||
case LED_RING_MODE_BLINK: {
|
|
||||||
cmd.mode = LED_CMD_BLINK;
|
|
||||||
cmd.r = r;
|
|
||||||
cmd.g = g;
|
|
||||||
cmd.b = b;
|
|
||||||
cmd.intensity = intensity;
|
|
||||||
cmd.blink_ms = (uint16_t)(req.blink_ms > 0 ? req.blink_ms : 350);
|
|
||||||
cmd.blink_count = req.blink_count > 0 ? (uint8_t)req.blink_count : 1;
|
|
||||||
if (cmd.blink_count == 0) {
|
|
||||||
cmd.blink_count = 1;
|
|
||||||
}
|
}
|
||||||
led_ring_send_command(&cmd);
|
ESP_LOGI(TAG, "LED ring mode %lu → %u/%u slaves%s", (unsigned long)mode,
|
||||||
ESP_LOGI(TAG, "blink x%u %u ms rgb=%u,%u,%u", (unsigned)cmd.blink_count,
|
(unsigned)sent, (unsigned)n, req.slaves_only ? "" : " + master");
|
||||||
(unsigned)cmd.blink_ms, (unsigned)r, (unsigned)g, (unsigned)b);
|
reply(local_ok || sent > 0, mode, req.progress, req.digit, 0, sent);
|
||||||
reply(true, mode, 0, 0);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
if (req.client_id == 0) {
|
||||||
ESP_LOGW(TAG, "unknown mode %lu", (unsigned long)mode);
|
bool ok = cmd_led_ring_apply(&req);
|
||||||
reply(false, mode, 0, 0);
|
ESP_LOGI(TAG, "LED ring mode %lu on master", (unsigned long)mode);
|
||||||
|
reply(ok, mode, req.progress, req.digit, 0, 0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const client_info_t *client = client_registry_find_by_id(req.client_id);
|
||||||
|
if (client == NULL) {
|
||||||
|
ESP_LOGW(TAG, "client id %lu not in registry", (unsigned long)req.client_id);
|
||||||
|
reply(false, mode, req.progress, req.digit, req.client_id, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err = push_led_ring_to_slave(client, &req);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "LED ring mode %lu → slave %lu", (unsigned long)mode,
|
||||||
|
(unsigned long)req.client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "LED ring to slave %lu failed: %s",
|
||||||
|
(unsigned long)req.client_id, esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
reply(err == ESP_OK, mode, req.progress, req.digit, req.client_id,
|
||||||
|
err == ESP_OK ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void cmd_led_ring_register(void) {
|
void cmd_led_ring_register(void) {
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
#ifndef CMD_LED_RING_H
|
#ifndef CMD_LED_RING_H
|
||||||
#define CMD_LED_RING_H
|
#define CMD_LED_RING_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include "uart_messages.pb.h"
|
||||||
|
|
||||||
|
/** Apply LED ring command locally (master or slave). */
|
||||||
|
bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req);
|
||||||
|
|
||||||
void cmd_led_ring_register(void);
|
void cmd_led_ring_register(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+55
-30
@@ -81,8 +81,6 @@ static const ota_espnow_progress_cbs_t s_dist_progress = {
|
|||||||
static void ota_prepare_task(void *param) {
|
static void ota_prepare_task(void *param) {
|
||||||
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||||
|
|
||||||
send_ota_status(OTA_UART_ST_PREPARING, 0);
|
|
||||||
|
|
||||||
int slot = ota_uart_prepare(total_size);
|
int slot = ota_uart_prepare(total_size);
|
||||||
if (slot < 0) {
|
if (slot < 0) {
|
||||||
send_ota_failed(1);
|
send_ota_failed(1);
|
||||||
@@ -116,27 +114,32 @@ static void handle_ota_start(const uint8_t *data, size_t len) {
|
|||||||
|
|
||||||
const alox_OtaStartPayload *req_ptr =
|
const alox_OtaStartPayload *req_ptr =
|
||||||
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start);
|
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_start_tag, ota_start);
|
||||||
if (req_ptr != NULL) {
|
if (req_ptr == NULL) {
|
||||||
req = *req_ptr;
|
ESP_LOGW(TAG, "OTA_START: missing ota_start payload");
|
||||||
|
send_ota_failed(3);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
req = *req_ptr;
|
||||||
|
|
||||||
if (req.total_size == 0) {
|
if (req.total_size == 0) {
|
||||||
ESP_LOGW(TAG, "OTA_START: total_size required");
|
ESP_LOGW(TAG, "OTA_START: total_size required");
|
||||||
send_ota_failed( 3);
|
send_ota_failed(3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ota_uart_is_active()) {
|
if (ota_uart_is_active()) {
|
||||||
ESP_LOGW(TAG, "OTA_START while session active");
|
ESP_LOGW(TAG, "OTA_START while session active");
|
||||||
send_ota_failed( 4);
|
send_ota_failed(4);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
send_ota_status(OTA_UART_ST_PREPARING, 0);
|
||||||
|
|
||||||
if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK,
|
if (xTaskCreate(ota_prepare_task, "ota_prepare", OTA_PREPARE_STACK,
|
||||||
(void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
|
(void *)(uintptr_t)req.total_size, OTA_PREPARE_PRIO,
|
||||||
NULL) != pdPASS) {
|
NULL) != pdPASS) {
|
||||||
ESP_LOGE(TAG, "failed to create ota_prepare task");
|
ESP_LOGE(TAG, "failed to create ota_prepare task");
|
||||||
send_ota_failed( 5);
|
send_ota_failed(5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +175,18 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ota_feed_result_t r =
|
ota_feed_result_t r = ota_uart_feed_chunk(req_ptr->seq, req_ptr->data.bytes,
|
||||||
ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size);
|
req_ptr->data.size);
|
||||||
|
if (r == OTA_FEED_SEQ_GAP) {
|
||||||
|
send_ota_failed(16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r == OTA_FEED_SEQ_DUP) {
|
||||||
|
if (ota_uart_block_ready_for_reack()) {
|
||||||
|
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (r == OTA_FEED_ERROR) {
|
if (r == OTA_FEED_ERROR) {
|
||||||
send_ota_failed( 13);
|
send_ota_failed( 13);
|
||||||
return;
|
return;
|
||||||
@@ -186,15 +199,6 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
|
|||||||
led_ring_show_ota_progress(done, total, OTA_LED_UART_R, OTA_LED_UART_G,
|
led_ring_show_ota_progress(done, total, OTA_LED_UART_R, OTA_LED_UART_G,
|
||||||
OTA_LED_UART_B);
|
OTA_LED_UART_B);
|
||||||
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
|
send_ota_status(OTA_UART_ST_BLOCK_ACK, 0);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == OTA_FEED_OK) {
|
|
||||||
uint32_t total = ota_uart_total_size();
|
|
||||||
if (total > 0) {
|
|
||||||
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
|
|
||||||
OTA_LED_UART_R, OTA_LED_UART_G, OTA_LED_UART_B);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -293,31 +297,28 @@ static void handle_ota_end(const uint8_t *data, size_t len) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
|
static void ota_start_espnow_task(void *param) {
|
||||||
(void)data;
|
(void)param;
|
||||||
(void)len;
|
|
||||||
|
|
||||||
if (ota_uart_is_active()) {
|
|
||||||
send_ota_failed( 40);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const esp_partition_t *part = NULL;
|
const esp_partition_t *part = NULL;
|
||||||
uint32_t image_size = 0;
|
uint32_t image_size = 0;
|
||||||
if (!ota_uart_get_staged_image(&part, &image_size)) {
|
if (!ota_uart_get_staged_image(&part, &image_size)) {
|
||||||
send_ota_failed( 41);
|
send_ota_failed(41);
|
||||||
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
|
esp_err_t err = ota_espnow_distribute(part, image_size, &s_dist_progress);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
send_ota_failed( 42);
|
send_ota_failed(42);
|
||||||
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
err = ota_uart_apply_boot();
|
err = ota_uart_apply_boot();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
send_ota_failed( (uint32_t)err);
|
send_ota_failed((uint32_t)err);
|
||||||
|
vTaskDelete(NULL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,6 +330,30 @@ static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
|
|||||||
response.payload.ota_status.error = 0;
|
response.payload.ota_status.error = 0;
|
||||||
uart_cmd_send(&response, TAG);
|
uart_cmd_send(&response, TAG);
|
||||||
led_ring_ota_success();
|
led_ring_ota_success();
|
||||||
|
vTaskDelete(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
|
||||||
|
(void)data;
|
||||||
|
(void)len;
|
||||||
|
|
||||||
|
if (ota_uart_is_active()) {
|
||||||
|
send_ota_failed(40);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const esp_partition_t *part = NULL;
|
||||||
|
uint32_t image_size = 0;
|
||||||
|
if (!ota_uart_get_staged_image(&part, &image_size)) {
|
||||||
|
send_ota_failed(41);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xTaskCreate(ota_start_espnow_task, "ota_espnow", OTA_DIST_STACK, NULL,
|
||||||
|
OTA_DIST_PRIO, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create ota_start_espnow task");
|
||||||
|
send_ota_failed(43);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void cmd_ota_register(void) {
|
void cmd_ota_register(void) {
|
||||||
|
|||||||
@@ -9,13 +9,29 @@ static void handle_ota_slave_progress(const uint8_t *data, size_t len) {
|
|||||||
alox_UartMessage uart_msg;
|
alox_UartMessage uart_msg;
|
||||||
uint32_t filter = 0;
|
uint32_t filter = 0;
|
||||||
|
|
||||||
if (uart_cmd_decode(data, len, &uart_msg) == ESP_OK) {
|
if (len > 0) {
|
||||||
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "decode failed");
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(
|
||||||
|
&response, alox_MessageType_OTA_SLAVE_PROGRESS,
|
||||||
|
alox_UartMessage_ota_slave_progress_response_tag);
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const alox_OtaSlaveProgressRequest *req =
|
const alox_OtaSlaveProgressRequest *req =
|
||||||
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag,
|
UART_CMD_REQ(&uart_msg, alox_UartMessage_ota_slave_progress_request_tag,
|
||||||
ota_slave_progress_request);
|
ota_slave_progress_request);
|
||||||
if (req != NULL) {
|
if (req == NULL) {
|
||||||
filter = req->client_id;
|
ESP_LOGW(TAG, "missing ota_slave_progress_request");
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(
|
||||||
|
&response, alox_MessageType_OTA_SLAVE_PROGRESS,
|
||||||
|
alox_UartMessage_ota_slave_progress_response_tag);
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
filter = req->client_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
alox_UartMessage response;
|
alox_UartMessage response;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#include "cmd_set_log_level.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[LOG_LVL]";
|
||||||
|
|
||||||
|
static bool valid_level(uint32_t level) { return level <= ESP_LOG_VERBOSE; }
|
||||||
|
|
||||||
|
static void reply(uint32_t level, bool success) {
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_SET_LOG_LEVEL,
|
||||||
|
alox_UartMessage_set_log_level_response_tag);
|
||||||
|
response.payload.set_log_level_response.success = success;
|
||||||
|
response.payload.set_log_level_response.level = level;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_set_log_level(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
alox_SetLogLevelRequest req = alox_SetLogLevelRequest_init_zero;
|
||||||
|
|
||||||
|
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "decode failed");
|
||||||
|
reply((uint32_t)esp_log_level_get("*"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alox_SetLogLevelRequest *req_ptr = UART_CMD_REQ(
|
||||||
|
&uart_msg, alox_UartMessage_set_log_level_request_tag, set_log_level_request);
|
||||||
|
if (req_ptr != NULL) {
|
||||||
|
req = *req_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.write) {
|
||||||
|
if (!valid_level(req.level)) {
|
||||||
|
ESP_LOGW(TAG, "invalid level %lu", (unsigned long)req.level);
|
||||||
|
reply((uint32_t)esp_log_level_get("*"), false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_log_level_set("*", (esp_log_level_t)req.level);
|
||||||
|
ESP_LOGI(TAG, "global log level set to %lu", (unsigned long)req.level);
|
||||||
|
reply(req.level, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reply((uint32_t)esp_log_level_get("*"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_set_log_level_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_SET_LOG_LEVEL, handle_set_log_level);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_SET_LOG_LEVEL_H
|
||||||
|
#define CMD_SET_LOG_LEVEL_H
|
||||||
|
|
||||||
|
void cmd_set_log_level_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#include "client_registry.h"
|
||||||
|
#include "cmd_tap_notify.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
|
#include "uart_cmd.h"
|
||||||
|
|
||||||
|
static const char *TAG = "[TAP_NOTIFY]";
|
||||||
|
|
||||||
|
static void reply(uint32_t client_id, bool success, uint32_t slaves_updated,
|
||||||
|
bool single, bool double_tap, bool triple) {
|
||||||
|
alox_UartMessage response;
|
||||||
|
uart_cmd_init_response(&response, alox_MessageType_TAP_NOTIFY,
|
||||||
|
alox_UartMessage_tap_notify_response_tag);
|
||||||
|
response.payload.tap_notify_response.client_id = client_id;
|
||||||
|
response.payload.tap_notify_response.success = success;
|
||||||
|
response.payload.tap_notify_response.slaves_updated = slaves_updated;
|
||||||
|
response.payload.tap_notify_response.single = single;
|
||||||
|
response.payload.tap_notify_response.double_tap = double_tap;
|
||||||
|
response.payload.tap_notify_response.triple = triple;
|
||||||
|
uart_cmd_send(&response, TAG);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t push_tap_notify_to_slave(const client_info_t *client,
|
||||||
|
bool single, bool double_tap,
|
||||||
|
bool triple) {
|
||||||
|
if (client == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
esp_err_t err =
|
||||||
|
client_registry_set_tap_notify(client->id, single, double_tap, triple);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
return esp_now_comm_send_tap_notify(client->mac, client->id, single,
|
||||||
|
double_tap, triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_tap_notify(const uint8_t *data, size_t len) {
|
||||||
|
alox_UartMessage uart_msg;
|
||||||
|
alox_TapNotifyRequest req = alox_TapNotifyRequest_init_zero;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
ESP_LOGW(TAG, "missing tap_notify_request");
|
||||||
|
reply(0, false, 0, false, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
req = *req_ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.write) {
|
||||||
|
if (req.all_clients) {
|
||||||
|
size_t n = client_registry_set_tap_notify_all(req.single, req.double_tap,
|
||||||
|
req.triple);
|
||||||
|
uint32_t sent = 0;
|
||||||
|
|
||||||
|
for (size_t i = 0; i < client_registry_count(); i++) {
|
||||||
|
const client_info_t *client = client_registry_at(i);
|
||||||
|
if (client == NULL) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (esp_now_comm_send_tap_notify(client->mac, client->id, req.single,
|
||||||
|
req.double_tap,
|
||||||
|
req.triple) == ESP_OK) {
|
||||||
|
sent++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "tap notify single=%d double=%d triple=%d for %u/%u slaves",
|
||||||
|
req.single, req.double_tap, req.triple, (unsigned)sent,
|
||||||
|
(unsigned)n);
|
||||||
|
reply(0, sent > 0, sent, req.single, req.double_tap, req.triple);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.client_id == 0) {
|
||||||
|
ESP_LOGW(TAG, "client_id required (or all_clients)");
|
||||||
|
reply(0, false, 0, req.single, req.double_tap, req.triple);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const client_info_t *client = client_registry_find_by_id(req.client_id);
|
||||||
|
if (client == NULL) {
|
||||||
|
ESP_LOGW(TAG, "client id %lu not found", (unsigned long)req.client_id);
|
||||||
|
reply(req.client_id, false, 0, req.single, req.double_tap, req.triple);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t err =
|
||||||
|
push_tap_notify_to_slave(client, req.single, req.double_tap, req.triple);
|
||||||
|
reply(req.client_id, err == ESP_OK, err == ESP_OK ? 1u : 0u, req.single,
|
||||||
|
req.double_tap, req.triple);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.all_clients || req.client_id == 0) {
|
||||||
|
reply(0, false, 0, false, false, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool single = false;
|
||||||
|
bool double_tap = false;
|
||||||
|
bool triple = false;
|
||||||
|
esp_err_t err = client_registry_get_tap_notify(req.client_id, &single,
|
||||||
|
&double_tap, &triple);
|
||||||
|
reply(req.client_id, err == ESP_OK, 0, single, double_tap, triple);
|
||||||
|
}
|
||||||
|
|
||||||
|
void cmd_tap_notify_register(void) {
|
||||||
|
uart_cmd_register(alox_MessageType_TAP_NOTIFY, handle_tap_notify);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
#ifndef CMD_TAP_NOTIFY_H
|
||||||
|
#define CMD_TAP_NOTIFY_H
|
||||||
|
|
||||||
|
void cmd_tap_notify_register(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
+19
-713
@@ -1,688 +1,23 @@
|
|||||||
#include "bosch456.h"
|
|
||||||
#include "client_registry.h"
|
|
||||||
#include "esp_now_comm.h"
|
#include "esp_now_comm.h"
|
||||||
#include "led_ring.h"
|
#include "client_registry.h"
|
||||||
#include "ota_espnow.h"
|
#include "esp_now_core.h"
|
||||||
#include "pod_reboot.h"
|
#include "esp_now_master.h"
|
||||||
#include "pod_settings.h"
|
#include "esp_now_slave.h"
|
||||||
#include "esp_now_proto.h"
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
#include "esp_event.h"
|
|
||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "esp_mac.h"
|
|
||||||
#include "esp_netif.h"
|
|
||||||
#include "esp_now.h"
|
#include "esp_now.h"
|
||||||
#include "esp_wifi.h"
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/idf_additions.h"
|
|
||||||
#include "ota_uart.h"
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#ifndef POWERPOD_FW_VERSION
|
|
||||||
#define POWERPOD_FW_VERSION 1u
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#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 SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
|
|
||||||
|
|
||||||
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
|
|
||||||
0xff, 0xff, 0xff};
|
|
||||||
|
|
||||||
static const char *TAG = "[ESPNOW]";
|
static const char *TAG = "[ESPNOW]";
|
||||||
|
|
||||||
static app_config_t s_config;
|
|
||||||
static uint8_t s_wifi_channel;
|
|
||||||
static uint8_t s_own_mac[ESP_NOW_ETH_ALEN];
|
|
||||||
static bool s_slave_joined;
|
|
||||||
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
|
|
||||||
static uint32_t s_last_discover_ms;
|
|
||||||
|
|
||||||
static SemaphoreHandle_t s_send_done;
|
|
||||||
static bool s_send_cb_ready;
|
|
||||||
|
|
||||||
static uint32_t now_ms(void) {
|
|
||||||
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t network_to_channel(uint8_t network) {
|
|
||||||
if (network < 1 || network > 13) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
return network;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool mac_equal(const uint8_t *a, const uint8_t *b) {
|
|
||||||
return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void 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]);
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t ensure_broadcast_peer(void) { return ensure_peer(ESPNOW_BCAST); }
|
|
||||||
|
|
||||||
static esp_err_t send_message_ex(const uint8_t *dest_mac,
|
|
||||||
const alox_EspNowMessage *msg, bool wait_done);
|
|
||||||
|
|
||||||
static void fill_presence(alox_EspNowSlavePresence *presence) {
|
|
||||||
presence->network = s_config.network;
|
|
||||||
presence->version = POWERPOD_FW_VERSION;
|
|
||||||
presence->slave_id = s_own_mac[5];
|
|
||||||
presence->available = true;
|
|
||||||
presence->used = false;
|
|
||||||
esp_now_proto_setup_presence_encode(presence, s_own_mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t send_message(const uint8_t *dest_mac,
|
|
||||||
const alox_EspNowMessage *msg) {
|
|
||||||
return send_message_ex(dest_mac, msg, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t send_message_ex(const uint8_t *dest_mac,
|
|
||||||
const alox_EspNowMessage *msg, bool wait_done) {
|
|
||||||
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 (ensure_peer(dest_mac) != ESP_OK) {
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (wait_done && 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 (wait_done && 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 send_message(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 send_message(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 send_message(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 send_message(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 send_message_ex(dest_mac, &msg, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 send_message_ex(dest_mac, &msg, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 send_message_ex(dest_mac, &msg, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t send_ota_status(const uint8_t *dest_mac, uint32_t status,
|
|
||||||
uint32_t bytes_written, uint32_t error) {
|
|
||||||
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 send_message_ex(dest_mac, &msg, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN],
|
|
||||||
uint32_t total_size) {
|
|
||||||
if (mac == NULL || !s_config.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 || !s_config.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 || !s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
return send_ota_end(mac);
|
|
||||||
}
|
|
||||||
|
|
||||||
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 || s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
return send_ota_status(master_mac, status, bytes_written, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
|
|
||||||
if (mac_out == NULL || s_config.master || !s_slave_joined) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
|
|
||||||
uint32_t client_id) {
|
|
||||||
if (mac == NULL || !s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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 || !s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
|
|
||||||
uint32_t seq) {
|
|
||||||
if (mac == NULL || !s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
|
||||||
uint32_t client_id, uint32_t deadzone) {
|
|
||||||
if (mac == NULL || !s_config.master) {
|
|
||||||
return ESP_ERR_INVALID_STATE;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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 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);
|
|
||||||
send_message(dest_mac, &msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void slave_reset_join(void) {
|
|
||||||
s_slave_joined = false;
|
|
||||||
memset(s_master_mac, 0, sizeof(s_master_mac));
|
|
||||||
s_last_discover_ms = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handle_slave_unicast_test(const uint8_t *master_mac,
|
|
||||||
const alox_EspNowUnicastTest *test) {
|
|
||||||
char mac_str[18];
|
|
||||||
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_slave_joined);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handle_slave_restart(const uint8_t *master_mac,
|
|
||||||
const alox_EspNowRestart *req) {
|
|
||||||
uint32_t my_id = s_own_mac[5];
|
|
||||||
|
|
||||||
if (req->client_id != 0 && req->client_id != my_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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_slave_find_me(const uint8_t *master_mac,
|
|
||||||
const alox_EspNowFindMe *req) {
|
|
||||||
uint32_t my_id = s_own_mac[5];
|
|
||||||
|
|
||||||
if (req->client_id != 0 && req->client_id != my_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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_slave_accel_deadzone(const uint8_t *master_mac,
|
|
||||||
const alox_EspNowAccelDeadzone *cfg) {
|
|
||||||
uint32_t my_id = s_own_mac[5];
|
|
||||||
|
|
||||||
if (cfg->client_id != 0 && cfg->client_id != my_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_slave_joined && !mac_equal(master_mac, s_master_mac)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
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, "slave deadzone %lu applied but not saved to NVS",
|
|
||||||
(unsigned long)cfg->deadzone);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
|
|
||||||
const uint8_t mac[CLIENT_MAC_LEN]) {
|
|
||||||
if (presence->network != s_config.network) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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];
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void handle_discover(const uint8_t *sender_mac,
|
|
||||||
const alox_EspNowDiscover *discover) {
|
|
||||||
if (discover->network != s_config.network) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t now = now_ms();
|
|
||||||
|
|
||||||
if (s_slave_joined) {
|
|
||||||
if (!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");
|
|
||||||
slave_reset_join();
|
|
||||||
}
|
|
||||||
|
|
||||||
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
|
|
||||||
s_slave_joined = true;
|
|
||||||
s_last_discover_ms = now;
|
|
||||||
ensure_peer(sender_mac);
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
mac_to_str(sender_mac, mac_str, sizeof(mac_str));
|
|
||||||
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network,
|
|
||||||
mac_str);
|
|
||||||
|
|
||||||
send_presence(sender_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void slave_check_master_timeout(void) {
|
|
||||||
if (!s_slave_joined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t now = now_ms();
|
|
||||||
if (s_last_discover_ms == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
slave_reset_join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void slave_heartbeat_task(void *param) {
|
|
||||||
(void)param;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "slave heartbeat task (interval %u ms)",
|
|
||||||
(unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS);
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
|
||||||
|
|
||||||
slave_check_master_timeout();
|
|
||||||
|
|
||||||
if (!s_slave_joined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void master_monitor_task(void *param) {
|
|
||||||
(void)param;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "master monitor task (timeout %u ms)",
|
|
||||||
(unsigned)ESPNOW_CLIENT_TIMEOUT_MS);
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
|
||||||
client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
static void espnow_recv_cb(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||||
int len) {
|
int len) {
|
||||||
if (info == NULL || data == NULL || len <= 0) {
|
if (esp_now_core_is_master()) {
|
||||||
return;
|
esp_now_master_on_recv(info, data, len);
|
||||||
|
} else {
|
||||||
|
esp_now_slave_on_recv(info, data, len);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!s_config.master) {
|
|
||||||
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
|
||||||
|
|
||||||
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "slave: ESP-NOW decode failed (%d bytes)", len);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s_slave_joined && mac_equal(info->src_addr, s_master_mac)) {
|
|
||||||
ensure_peer(info->src_addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (msg.which_payload) {
|
|
||||||
case alox_EspNowMessage_discover_tag:
|
|
||||||
handle_discover(info->src_addr, &msg.payload.discover);
|
|
||||||
break;
|
|
||||||
case alox_EspNowMessage_unicast_test_tag:
|
|
||||||
handle_slave_unicast_test(info->src_addr, &msg.payload.unicast_test);
|
|
||||||
break;
|
|
||||||
case alox_EspNowMessage_accel_deadzone_tag:
|
|
||||||
handle_slave_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone);
|
|
||||||
break;
|
|
||||||
case alox_EspNowMessage_find_me_tag:
|
|
||||||
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
handle_slave_find_me(info->src_addr, &msg.payload.find_me);
|
|
||||||
break;
|
|
||||||
case alox_EspNowMessage_restart_tag:
|
|
||||||
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
handle_slave_restart(info->src_addr, &msg.payload.restart);
|
|
||||||
break;
|
|
||||||
case alox_EspNowMessage_ota_start_tag:
|
|
||||||
case alox_EspNowMessage_ota_payload_tag:
|
|
||||||
case alox_EspNowMessage_ota_end_tag:
|
|
||||||
if (!s_slave_joined || !mac_equal(info->src_addr, s_master_mac)) {
|
|
||||||
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:
|
|
||||||
ESP_LOGW(TAG, "slave: unhandled ESP-NOW which=%u type=%u", msg.which_payload,
|
|
||||||
(unsigned)msg.type);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
|
||||||
|
|
||||||
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
|
|
||||||
ESP_LOGW(TAG, "master: ESP-NOW decode failed (%d bytes)", len);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.which_payload == alox_EspNowMessage_ota_status_tag) {
|
|
||||||
ensure_peer(info->src_addr);
|
|
||||||
ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg);
|
|
||||||
if (presence != NULL) {
|
|
||||||
/* Registry key is the ESP-NOW sender MAC, not the optional protobuf mac field. */
|
|
||||||
ensure_peer(info->src_addr);
|
|
||||||
handle_client_presence(presence, info->src_addr);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void master_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 = s_config.network;
|
|
||||||
|
|
||||||
ESP_LOGI(TAG, "master discover task on network %u ch %u",
|
|
||||||
(unsigned)s_config.network, (unsigned)s_wifi_channel);
|
|
||||||
|
|
||||||
while (1) {
|
|
||||||
send_message(ESPNOW_BCAST, &msg);
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static esp_err_t init_wifi_stack(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));
|
|
||||||
|
|
||||||
return ESP_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
esp_err_t esp_now_comm_init(const app_config_t *config) {
|
esp_err_t esp_now_comm_init(const app_config_t *config) {
|
||||||
@@ -690,56 +25,27 @@ esp_err_t esp_now_comm_init(const app_config_t *config) {
|
|||||||
return ESP_ERR_INVALID_ARG;
|
return ESP_ERR_INVALID_ARG;
|
||||||
}
|
}
|
||||||
|
|
||||||
memset(&s_config, 0, sizeof(s_config));
|
esp_now_core_store_config(config);
|
||||||
memcpy(&s_config, config, sizeof(s_config));
|
|
||||||
client_registry_init();
|
client_registry_init();
|
||||||
slave_reset_join();
|
|
||||||
|
|
||||||
s_wifi_channel = network_to_channel(config->network);
|
esp_err_t err = esp_now_core_init_radio(esp_now_core_wifi_channel());
|
||||||
ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA));
|
|
||||||
|
|
||||||
char mac_str[18];
|
|
||||||
mac_to_str(s_own_mac, mac_str, sizeof(mac_str));
|
|
||||||
ESP_LOGI(TAG, "role=%s network=%u channel=%u mac=%s",
|
|
||||||
config->master ? "master" : "slave", (unsigned)config->network,
|
|
||||||
(unsigned)s_wifi_channel, mac_str);
|
|
||||||
|
|
||||||
esp_err_t err = init_wifi_stack(s_wifi_channel);
|
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "wifi init failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "wifi init failed: %s", esp_err_to_name(err));
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(esp_now_core_own_mac(), mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "role=%s network=%u channel=%u mac=%s",
|
||||||
|
config->master ? "master" : "slave", (unsigned)config->network,
|
||||||
|
(unsigned)esp_now_core_wifi_channel(), mac_str);
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_now_init());
|
ESP_ERROR_CHECK(esp_now_init());
|
||||||
ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb));
|
ESP_ERROR_CHECK(esp_now_register_recv_cb(espnow_recv_cb));
|
||||||
|
esp_now_core_init_send_done();
|
||||||
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, "ESP-NOW send-done callback unavailable (OTA may drop packets)");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config->master) {
|
if (config->master) {
|
||||||
ESP_ERROR_CHECK(ensure_broadcast_peer());
|
return esp_now_master_start();
|
||||||
if (xTaskCreate(master_discover_task, "espnow_disc", 4096, NULL, 4,
|
|
||||||
NULL) != pdPASS) {
|
|
||||||
ESP_LOGE(TAG, "failed to create discover task");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
if (xTaskCreate(master_monitor_task, "espnow_mon", 4096, NULL, 4, NULL) !=
|
|
||||||
pdPASS) {
|
|
||||||
ESP_LOGE(TAG, "failed to create monitor task");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (xTaskCreate(slave_heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) !=
|
|
||||||
pdPASS) {
|
|
||||||
ESP_LOGE(TAG, "failed to create heartbeat task");
|
|
||||||
return ESP_FAIL;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return esp_now_slave_start();
|
||||||
return ESP_OK;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,20 @@
|
|||||||
#include "app_config.h"
|
#include "app_config.h"
|
||||||
#include "client_registry.h"
|
#include "client_registry.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include "esp_now_messages.pb.h"
|
||||||
|
#include "uart_messages.pb.h"
|
||||||
|
|
||||||
esp_err_t esp_now_comm_init(const app_config_t *config);
|
esp_err_t esp_now_comm_init(const app_config_t *config);
|
||||||
|
|
||||||
|
/** Master: enable/disable accel ESP-NOW stream on one slave. */
|
||||||
|
esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id, bool enable);
|
||||||
|
|
||||||
|
/** Master: configure tap notify flags on one slave. */
|
||||||
|
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);
|
||||||
|
|
||||||
/** Master: unicast accel deadzone to one slave (client_id is echoed for filtering). */
|
/** Master: unicast accel deadzone to one slave (client_id is echoed for filtering). */
|
||||||
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
uint32_t client_id, uint32_t deadzone);
|
uint32_t client_id, uint32_t deadzone);
|
||||||
@@ -15,10 +26,26 @@ esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
|||||||
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
|
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
uint32_t seq);
|
uint32_t seq);
|
||||||
|
|
||||||
|
/** Result of a master-side ESP-NOW echo ping round-trip. */
|
||||||
|
typedef struct {
|
||||||
|
uint64_t echoed_us;
|
||||||
|
uint32_t esp_rtt_us;
|
||||||
|
} esp_now_echo_ping_result_t;
|
||||||
|
|
||||||
|
/** Master: ESP-NOW echo ping round-trip; fills result on success. */
|
||||||
|
esp_err_t esp_now_comm_echo_ping(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint64_t timestamp_us,
|
||||||
|
esp_now_echo_ping_result_t *result);
|
||||||
|
|
||||||
/** Master: trigger find-me LED sequence on one slave. */
|
/** Master: trigger find-me LED sequence on one slave. */
|
||||||
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
|
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
uint32_t client_id);
|
uint32_t client_id);
|
||||||
|
|
||||||
|
/** Master: LED ring command on one slave. */
|
||||||
|
esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id,
|
||||||
|
const alox_LedRingProgressRequest *req);
|
||||||
|
|
||||||
/** Master: request reboot on one slave. */
|
/** Master: request reboot on one slave. */
|
||||||
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
|
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
uint32_t client_id);
|
uint32_t client_id);
|
||||||
|
|||||||
@@ -0,0 +1,282 @@
|
|||||||
|
#include "esp_now_core.h"
|
||||||
|
#include "esp_now_proto.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_mac.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
#include "esp_now.h"
|
||||||
|
#include "esp_wifi.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/idf_additions.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "[ESPNOW_CORE]";
|
||||||
|
|
||||||
|
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff};
|
||||||
|
|
||||||
|
static app_config_t s_config;
|
||||||
|
static uint8_t s_wifi_channel;
|
||||||
|
static uint8_t s_own_mac[ESP_NOW_ETH_ALEN];
|
||||||
|
static SemaphoreHandle_t s_send_done;
|
||||||
|
static SemaphoreHandle_t s_send_lock;
|
||||||
|
static bool s_send_cb_ready;
|
||||||
|
static volatile esp_now_send_status_t s_last_send_status;
|
||||||
|
static volatile bool s_last_send_ok;
|
||||||
|
|
||||||
|
#define ESPNOW_SEND_DONE_TIMEOUT_MS 500u
|
||||||
|
#define ESPNOW_SEND_MAX_ATTEMPTS 8u
|
||||||
|
#define ESPNOW_SEND_RETRY_DELAY_MS 10u
|
||||||
|
#define ESPNOW_NOMEM_RETRY_DELAY_MS 50u
|
||||||
|
|
||||||
|
#define ESPNOW_RELIABLE_MAX_ATTEMPTS 24u
|
||||||
|
#define ESPNOW_RELIABLE_NOMEM_DELAY_MS 50u
|
||||||
|
|
||||||
|
static uint8_t network_to_channel(uint8_t network) {
|
||||||
|
if (network < 1 || network > 13) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return network;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void espnow_send_done_cb(const esp_now_send_info_t *tx_info,
|
||||||
|
esp_now_send_status_t status) {
|
||||||
|
(void)tx_info;
|
||||||
|
s_last_send_status = status;
|
||||||
|
s_last_send_ok = (status == ESP_NOW_SEND_SUCCESS);
|
||||||
|
if (s_send_done != NULL) {
|
||||||
|
xSemaphoreGive(s_send_done);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void esp_now_core_store_config(const app_config_t *config) {
|
||||||
|
if (config == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
memset(&s_config, 0, sizeof(s_config));
|
||||||
|
memcpy(&s_config, config, sizeof(s_config));
|
||||||
|
s_wifi_channel = network_to_channel(config->network);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app_config_t *esp_now_core_get_config(void) { return &s_config; }
|
||||||
|
|
||||||
|
bool esp_now_core_is_master(void) { return s_config.master; }
|
||||||
|
|
||||||
|
uint8_t esp_now_core_network(void) { return s_config.network; }
|
||||||
|
|
||||||
|
uint8_t esp_now_core_wifi_channel(void) { return s_wifi_channel; }
|
||||||
|
|
||||||
|
const uint8_t *esp_now_core_own_mac(void) { return s_own_mac; }
|
||||||
|
|
||||||
|
uint32_t esp_now_core_now_ms(void) {
|
||||||
|
return (uint32_t)(xTaskGetTickCount() * portTICK_PERIOD_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b) {
|
||||||
|
return memcmp(a, b, ESP_NOW_ETH_ALEN) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len) {
|
||||||
|
snprintf(out, out_len, "%02x:%02x:%02x:%02x:%02x:%02x", mac[0], mac[1],
|
||||||
|
mac[2], mac[3], mac[4], mac[5]);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_ensure_peer(const uint8_t *mac) {
|
||||||
|
if (esp_now_is_peer_exist(mac)) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_peer_info_t peer = {0};
|
||||||
|
memcpy(peer.peer_addr, mac, ESP_NOW_ETH_ALEN);
|
||||||
|
peer.channel = s_wifi_channel;
|
||||||
|
peer.ifidx = WIFI_IF_STA;
|
||||||
|
peer.encrypt = false;
|
||||||
|
|
||||||
|
esp_err_t err = esp_now_add_peer(&peer);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "add peer failed: %s", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_ensure_broadcast_peer(void) {
|
||||||
|
return esp_now_core_ensure_peer(ESPNOW_BCAST);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_with_backpressure(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg,
|
||||||
|
uint32_t max_attempts,
|
||||||
|
uint32_t nomem_delay_ms,
|
||||||
|
uint32_t retry_delay_ms,
|
||||||
|
bool quiet_retries) {
|
||||||
|
if (s_send_lock == NULL ||
|
||||||
|
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buf[ESPNOW_PB_MAX_SIZE];
|
||||||
|
size_t len = 0;
|
||||||
|
esp_err_t result = ESP_FAIL;
|
||||||
|
|
||||||
|
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "encode failed");
|
||||||
|
result = err;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > ESP_NOW_MAX_DATA_LEN) {
|
||||||
|
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
|
||||||
|
(unsigned)ESP_NOW_MAX_DATA_LEN);
|
||||||
|
result = ESP_ERR_INVALID_SIZE;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) {
|
||||||
|
result = ESP_FAIL;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint32_t attempt = 0; attempt < max_attempts; attempt++) {
|
||||||
|
if (s_send_cb_ready && s_send_done != NULL) {
|
||||||
|
xSemaphoreTake(s_send_done, 0);
|
||||||
|
}
|
||||||
|
s_last_send_ok = false;
|
||||||
|
|
||||||
|
err = esp_now_send(dest_mac, buf, len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
const bool last = (attempt + 1 >= max_attempts);
|
||||||
|
if (!quiet_retries || last) {
|
||||||
|
ESP_LOGW(TAG, "send type=%u failed (attempt %lu/%lu): %s",
|
||||||
|
(unsigned)msg->type, (unsigned long)(attempt + 1),
|
||||||
|
(unsigned long)max_attempts, esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(err == ESP_ERR_ESPNOW_NO_MEM ? nomem_delay_ms
|
||||||
|
: retry_delay_ms));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_send_cb_ready && s_send_done != NULL) {
|
||||||
|
if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(ESPNOW_SEND_DONE_TIMEOUT_MS)) !=
|
||||||
|
pdTRUE) {
|
||||||
|
const bool last = (attempt + 1 >= max_attempts);
|
||||||
|
if (!quiet_retries || last) {
|
||||||
|
ESP_LOGW(TAG, "send type=%u done timeout (attempt %lu/%lu)",
|
||||||
|
(unsigned)msg->type, (unsigned long)(attempt + 1),
|
||||||
|
(unsigned long)max_attempts);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!s_last_send_ok) {
|
||||||
|
const bool last = (attempt + 1 >= max_attempts);
|
||||||
|
if (!quiet_retries || last) {
|
||||||
|
ESP_LOGW(TAG, "send type=%u peer status=%d (attempt %lu/%lu)",
|
||||||
|
(unsigned)msg->type, (int)s_last_send_status,
|
||||||
|
(unsigned long)(attempt + 1), (unsigned long)max_attempts);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(retry_delay_ms));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = ESP_OK;
|
||||||
|
goto out;
|
||||||
|
}
|
||||||
|
|
||||||
|
out:
|
||||||
|
xSemaphoreGive(s_send_lock);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg) {
|
||||||
|
return send_with_backpressure(dest_mac, msg, ESPNOW_SEND_MAX_ATTEMPTS,
|
||||||
|
ESPNOW_NOMEM_RETRY_DELAY_MS,
|
||||||
|
ESPNOW_SEND_RETRY_DELAY_MS, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg) {
|
||||||
|
return send_with_backpressure(dest_mac, msg, ESPNOW_RELIABLE_MAX_ATTEMPTS,
|
||||||
|
ESPNOW_RELIABLE_NOMEM_DELAY_MS,
|
||||||
|
ESPNOW_SEND_RETRY_DELAY_MS, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg) {
|
||||||
|
if (s_send_lock == NULL ||
|
||||||
|
xSemaphoreTake(s_send_lock, pdMS_TO_TICKS(5000)) != pdTRUE) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t buf[ESPNOW_PB_MAX_SIZE];
|
||||||
|
size_t len = 0;
|
||||||
|
|
||||||
|
esp_err_t err = esp_now_proto_encode(msg, buf, sizeof(buf), &len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "encode failed");
|
||||||
|
xSemaphoreGive(s_send_lock);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (len > ESP_NOW_MAX_DATA_LEN) {
|
||||||
|
ESP_LOGW(TAG, "encoded len %u > ESP-NOW max %u", (unsigned)len,
|
||||||
|
(unsigned)ESP_NOW_MAX_DATA_LEN);
|
||||||
|
xSemaphoreGive(s_send_lock);
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (esp_now_core_ensure_peer(dest_mac) != ESP_OK) {
|
||||||
|
xSemaphoreGive(s_send_lock);
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = esp_now_send(dest_mac, buf, len);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
xSemaphoreGive(s_send_lock);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_send(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg) {
|
||||||
|
return esp_now_core_send_wait(dest_mac, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_init_radio(uint8_t channel) {
|
||||||
|
ESP_ERROR_CHECK(esp_netif_init());
|
||||||
|
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
||||||
|
|
||||||
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||||
|
|
||||||
|
wifi_config_t wifi_config = {0};
|
||||||
|
wifi_config.sta.channel = channel;
|
||||||
|
wifi_config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN;
|
||||||
|
wifi_config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL;
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_start());
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||||
|
ESP_ERROR_CHECK(esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE));
|
||||||
|
|
||||||
|
ESP_ERROR_CHECK(esp_read_mac(s_own_mac, ESP_MAC_WIFI_STA));
|
||||||
|
s_wifi_channel = channel;
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void esp_now_core_init_send_done(void) {
|
||||||
|
s_send_done = xSemaphoreCreateBinary();
|
||||||
|
s_send_lock = xSemaphoreCreateMutex();
|
||||||
|
if (s_send_done != NULL && s_send_lock != NULL &&
|
||||||
|
esp_now_register_send_cb(espnow_send_done_cb) == ESP_OK) {
|
||||||
|
s_send_cb_ready = true;
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "send-done callback unavailable (OTA may drop packets)");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
#ifndef ESP_NOW_CORE_H
|
||||||
|
#define ESP_NOW_CORE_H
|
||||||
|
|
||||||
|
#include "app_config.h"
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_now_messages.pb.h"
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
void esp_now_core_store_config(const app_config_t *config);
|
||||||
|
const app_config_t *esp_now_core_get_config(void);
|
||||||
|
bool esp_now_core_is_master(void);
|
||||||
|
uint8_t esp_now_core_network(void);
|
||||||
|
uint8_t esp_now_core_wifi_channel(void);
|
||||||
|
const uint8_t *esp_now_core_own_mac(void);
|
||||||
|
|
||||||
|
uint32_t esp_now_core_now_ms(void);
|
||||||
|
bool esp_now_core_mac_equal(const uint8_t *a, const uint8_t *b);
|
||||||
|
void esp_now_core_mac_to_str(const uint8_t *mac, char *out, size_t out_len);
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_ensure_peer(const uint8_t *mac);
|
||||||
|
esp_err_t esp_now_core_ensure_broadcast_peer(void);
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_send(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg);
|
||||||
|
/** Like send but does not wait for esp_now send-done (lower latency). */
|
||||||
|
esp_err_t esp_now_core_send_fast(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg);
|
||||||
|
esp_err_t esp_now_core_send_wait(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg);
|
||||||
|
/** OTA payloads: wait for send-done with extra NO_MEM backoff (queue drain). */
|
||||||
|
esp_err_t esp_now_core_send_reliable(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowMessage *msg);
|
||||||
|
|
||||||
|
esp_err_t esp_now_core_init_radio(uint8_t channel);
|
||||||
|
void esp_now_core_init_send_done(void);
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,597 @@
|
|||||||
|
#include "esp_now_master.h"
|
||||||
|
#include "client_registry.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
|
#include "esp_now_core.h"
|
||||||
|
#include "esp_now_proto.h"
|
||||||
|
#include "board_input.h"
|
||||||
|
#include "ota_espnow.h"
|
||||||
|
#include "ota_session.h"
|
||||||
|
#include "ota_uart.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/idf_additions.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static const uint8_t ESPNOW_BCAST[ESP_NOW_ETH_ALEN] = {0xff, 0xff, 0xff,
|
||||||
|
0xff, 0xff, 0xff};
|
||||||
|
|
||||||
|
#define ESPNOW_DISCOVER_INTERVAL_MS 500
|
||||||
|
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
|
||||||
|
#define ESPNOW_HEARTBEAT_MISS_COUNT 3
|
||||||
|
#define ESPNOW_CLIENT_TIMEOUT_MS \
|
||||||
|
(ESPNOW_HEARTBEAT_INTERVAL_MS * ESPNOW_HEARTBEAT_MISS_COUNT)
|
||||||
|
#define ESPNOW_BATTERY_INTERVAL_MS 30000
|
||||||
|
|
||||||
|
static const char *TAG = "[ESPNOW_M]";
|
||||||
|
|
||||||
|
static esp_err_t send_accel_stream(const uint8_t *dest_mac, uint32_t client_id,
|
||||||
|
bool enable) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM;
|
||||||
|
msg.which_payload = alox_EspNowMessage_accel_stream_tag;
|
||||||
|
msg.payload.accel_stream.enable = enable;
|
||||||
|
msg.payload.accel_stream.client_id = client_id;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_accel_deadzone(const uint8_t *dest_mac, uint32_t client_id,
|
||||||
|
uint32_t deadzone) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_SET_ACCEL_DEADZONE;
|
||||||
|
msg.which_payload = alox_EspNowMessage_accel_deadzone_tag;
|
||||||
|
msg.payload.accel_deadzone.deadzone = deadzone;
|
||||||
|
msg.payload.accel_deadzone.client_id = client_id;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t seq) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_UNICAST_TEST;
|
||||||
|
msg.which_payload = alox_EspNowMessage_unicast_test_tag;
|
||||||
|
msg.payload.unicast_test.seq = seq;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#define ESPNOW_ECHO_PING_TIMEOUT_MS 500
|
||||||
|
#define ECHO_PING_TAG "[ECHO_PING]"
|
||||||
|
|
||||||
|
static SemaphoreHandle_t s_echo_pong_sem;
|
||||||
|
static bool s_echo_waiting;
|
||||||
|
static uint64_t s_echo_expect_ts;
|
||||||
|
static uint32_t s_echo_esp_rtt_us;
|
||||||
|
static uint8_t s_echo_expect_mac[ESP_NOW_ETH_ALEN];
|
||||||
|
|
||||||
|
static esp_err_t echo_ping_init(void) {
|
||||||
|
if (s_echo_pong_sem != NULL) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
s_echo_pong_sem = xSemaphoreCreateBinary();
|
||||||
|
if (s_echo_pong_sem == NULL) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_echo_ping(const uint8_t *dest_mac, uint64_t host_timestamp_us,
|
||||||
|
uint64_t master_time_us) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_ECHO_PING;
|
||||||
|
msg.which_payload = alox_EspNowMessage_echo_ping_tag;
|
||||||
|
msg.payload.echo_ping.host_timestamp_us = host_timestamp_us;
|
||||||
|
msg.payload.echo_ping.master_time_us = master_time_us;
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(dest_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(ECHO_PING_TAG, "ESP-NOW PING send to %s host_ts=%llu master_time_us=%llu",
|
||||||
|
mac_str, (unsigned long long)host_timestamp_us,
|
||||||
|
(unsigned long long)master_time_us);
|
||||||
|
return esp_now_core_send_fast(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void echo_ping_on_pong(const uint8_t mac[ESP_NOW_ETH_ALEN],
|
||||||
|
const alox_EspNowEchoPong *pong) {
|
||||||
|
if (pong == NULL || !s_echo_waiting) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!esp_now_core_mac_equal(mac, s_echo_expect_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pong->host_timestamp_us != s_echo_expect_ts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int64_t now_us = esp_timer_get_time();
|
||||||
|
s_echo_esp_rtt_us =
|
||||||
|
(uint32_t)(now_us - (int64_t)pong->master_time_us);
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(ECHO_PING_TAG,
|
||||||
|
"ESP-NOW PONG recv from %s host_ts=%llu master_time_us=%llu "
|
||||||
|
"recv_time_us=%lld esp_rtt_us=%lu",
|
||||||
|
mac_str, (unsigned long long)pong->host_timestamp_us,
|
||||||
|
(unsigned long long)pong->master_time_us, (long long)now_us,
|
||||||
|
(unsigned long)s_echo_esp_rtt_us);
|
||||||
|
xSemaphoreGive(s_echo_pong_sem);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_find_me(const uint8_t *dest_mac, uint32_t client_id) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_FIND_ME;
|
||||||
|
msg.which_payload = alox_EspNowMessage_find_me_tag;
|
||||||
|
msg.payload.find_me.client_id = client_id;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_led_ring(const uint8_t *dest_mac, uint32_t client_id,
|
||||||
|
const alox_LedRingProgressRequest *req) {
|
||||||
|
if (req == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_LED_RING;
|
||||||
|
msg.which_payload = alox_EspNowMessage_led_ring_tag;
|
||||||
|
msg.payload.led_ring.client_id = client_id;
|
||||||
|
msg.payload.led_ring.mode = req->mode;
|
||||||
|
msg.payload.led_ring.progress = req->progress;
|
||||||
|
msg.payload.led_ring.digit = req->digit;
|
||||||
|
msg.payload.led_ring.r = req->r;
|
||||||
|
msg.payload.led_ring.g = req->g;
|
||||||
|
msg.payload.led_ring.b = req->b;
|
||||||
|
msg.payload.led_ring.intensity = req->intensity;
|
||||||
|
msg.payload.led_ring.blink_ms = req->blink_ms;
|
||||||
|
msg.payload.led_ring.blink_count = req->blink_count;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_restart(const uint8_t *dest_mac, uint32_t client_id) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_RESTART;
|
||||||
|
msg.which_payload = alox_EspNowMessage_restart_tag;
|
||||||
|
msg.payload.restart.client_id = client_id;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_tap_notify(const uint8_t *dest_mac, uint32_t client_id,
|
||||||
|
bool single, bool double_tap, bool triple) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY;
|
||||||
|
msg.which_payload = alox_EspNowMessage_tap_notify_tag;
|
||||||
|
msg.payload.tap_notify.client_id = client_id;
|
||||||
|
msg.payload.tap_notify.single = single;
|
||||||
|
msg.payload.tap_notify.double_tap = double_tap;
|
||||||
|
msg.payload.tap_notify.triple = triple;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_ota_start(const uint8_t *dest_mac, uint32_t total_size) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_OTA_START;
|
||||||
|
msg.which_payload = alox_EspNowMessage_ota_start_tag;
|
||||||
|
msg.payload.ota_start.total_size = total_size;
|
||||||
|
return esp_now_core_send_wait(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq,
|
||||||
|
const uint8_t *data, size_t len) {
|
||||||
|
if (data == NULL || len == 0 || len > OTA_UART_HOST_CHUNK_SIZE) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_OTA_PAYLOAD;
|
||||||
|
msg.which_payload = alox_EspNowMessage_ota_payload_tag;
|
||||||
|
msg.payload.ota_payload.seq = seq;
|
||||||
|
msg.payload.ota_payload.data.size = len;
|
||||||
|
memcpy(msg.payload.ota_payload.data.bytes, data, len);
|
||||||
|
return esp_now_core_send_reliable(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_ota_end(const uint8_t *dest_mac) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_OTA_END;
|
||||||
|
msg.which_payload = alox_EspNowMessage_ota_end_tag;
|
||||||
|
return esp_now_core_send_wait(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_ota_start(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t total_size) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
return send_ota_start(mac, total_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_ota_payload(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t seq, const uint8_t *data,
|
||||||
|
size_t len) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
return send_ota_payload(mac, seq, data, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_ota_end(const uint8_t mac[CLIENT_MAC_LEN]) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
return send_ota_end(mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_restart(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_restart(mac, client_id);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast RESTART to %s client_id=%lu", mac_str,
|
||||||
|
(unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast RESTART to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_find_me(mac, client_id);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast FIND_ME to %s client_id=%lu", mac_str,
|
||||||
|
(unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast FIND_ME to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_led_ring(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id,
|
||||||
|
const alox_LedRingProgressRequest *req) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master() || req == NULL) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_led_ring(mac, client_id, req);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast LED_RING mode %lu to %s client_id=%lu",
|
||||||
|
(unsigned long)req->mode, mac_str, (unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast LED_RING to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t seq) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_unicast_test(mac, seq);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast TEST to %s seq=%lu", mac_str, (unsigned long)seq);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast TEST to %s failed: %s", mac_str, esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_echo_ping(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint64_t timestamp_us,
|
||||||
|
esp_now_echo_ping_result_t *result) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
if (echo_ping_init() != ESP_OK) {
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreTake(s_echo_pong_sem, 0);
|
||||||
|
s_echo_waiting = true;
|
||||||
|
s_echo_expect_ts = timestamp_us;
|
||||||
|
s_echo_esp_rtt_us = 0;
|
||||||
|
memcpy(s_echo_expect_mac, mac, ESP_NOW_ETH_ALEN);
|
||||||
|
|
||||||
|
uint64_t master_time_us = (uint64_t)esp_timer_get_time();
|
||||||
|
esp_err_t err = send_echo_ping(mac, timestamp_us, master_time_us);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
s_echo_waiting = false;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xSemaphoreTake(s_echo_pong_sem,
|
||||||
|
pdMS_TO_TICKS(ESPNOW_ECHO_PING_TIMEOUT_MS)) != pdTRUE) {
|
||||||
|
s_echo_waiting = false;
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGW(ECHO_PING_TAG,
|
||||||
|
"ESP-NOW PONG timeout to %s host_ts=%llu",
|
||||||
|
mac_str, (unsigned long long)timestamp_us);
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
s_echo_waiting = false;
|
||||||
|
if (result != NULL) {
|
||||||
|
result->echoed_us = timestamp_us;
|
||||||
|
result->esp_rtt_us = s_echo_esp_rtt_us;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_accel_stream(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id, bool enable) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_accel_stream(mac, client_id, enable);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast SET_ACCEL_STREAM to %s: %s client_id=%lu", mac_str,
|
||||||
|
enable ? "on" : "off", (unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast SET_ACCEL_STREAM to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_tap_notify(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id, bool single,
|
||||||
|
bool double_tap, bool triple) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err =
|
||||||
|
send_tap_notify(mac, client_id, single, double_tap, triple);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"unicast SET_TAP_NOTIFY to %s: single=%d double=%d triple=%d "
|
||||||
|
"client_id=%lu",
|
||||||
|
mac_str, single, double_tap, triple, (unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast SET_TAP_NOTIFY to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t client_id,
|
||||||
|
uint32_t deadzone) {
|
||||||
|
if (mac == NULL || !esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
esp_err_t err = send_accel_deadzone(mac, client_id, deadzone);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "unicast SET_ACCEL_DEADZONE to %s: deadzone=%lu client_id=%lu",
|
||||||
|
mac_str, (unsigned long)deadzone, (unsigned long)client_id);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "unicast SET_ACCEL_DEADZONE to %s failed: %s", mac_str,
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
const alox_EspNowAccelSample *sample) {
|
||||||
|
if (sample == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = client_registry_update_accel(
|
||||||
|
mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y,
|
||||||
|
(int16_t)sample->z);
|
||||||
|
if (err == ESP_ERR_NOT_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_tap_event(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
const alox_EspNowTapEvent *event) {
|
||||||
|
if (event == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err =
|
||||||
|
client_registry_update_tap(mac, event->slave_id, event->kind);
|
||||||
|
if (err == ESP_ERR_NOT_FOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "tap event id=%lu kind=%lu rejected from %02x:…:%02x",
|
||||||
|
(unsigned long)event->slave_id, (unsigned long)event->kind, mac[0],
|
||||||
|
mac[5]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_battery_report(const uint8_t mac[CLIENT_MAC_LEN],
|
||||||
|
const alox_EspNowBatteryReport *report) {
|
||||||
|
if (report == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = client_registry_update_battery(
|
||||||
|
mac, report->client_id, report->lipo1_valid, report->lipo1_mv,
|
||||||
|
report->lipo2_valid, report->lipo2_mv);
|
||||||
|
if (err == ESP_ERR_NOT_FOUND) {
|
||||||
|
ESP_LOGW(TAG, "battery report from unregistered slave id=%lu",
|
||||||
|
(unsigned long)report->client_id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "battery report id=%lu rejected: %s",
|
||||||
|
(unsigned long)report->client_id, esp_err_to_name(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGI(TAG, "battery cached id=%lu L1=%s %lu mV L2=%s %lu mV",
|
||||||
|
(unsigned long)report->client_id,
|
||||||
|
report->lipo1_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)report->lipo1_mv, report->lipo2_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)report->lipo2_mv);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_client_presence(const alox_EspNowSlavePresence *presence,
|
||||||
|
const uint8_t mac[CLIENT_MAC_LEN]) {
|
||||||
|
if (presence->network != esp_now_core_network()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_now_core_ensure_peer(mac);
|
||||||
|
|
||||||
|
bool is_new = false;
|
||||||
|
bool reactivated = false;
|
||||||
|
esp_err_t err = client_registry_heartbeat(
|
||||||
|
mac, presence->slave_id, presence->version, presence->used, &is_new,
|
||||||
|
&reactivated);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "client registry full");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(mac, mac_str, sizeof(mac_str));
|
||||||
|
if (is_new) {
|
||||||
|
ESP_LOGI(TAG, "client registered id=%lu mac=%s ver=%lu",
|
||||||
|
(unsigned long)presence->slave_id, mac_str,
|
||||||
|
(unsigned long)presence->version);
|
||||||
|
} else if (reactivated) {
|
||||||
|
ESP_LOGI(TAG, "client reconnected id=%lu mac=%s",
|
||||||
|
(unsigned long)presence->slave_id, mac_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||||
|
int len) {
|
||||||
|
if (info == NULL || data == NULL || len <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "decode failed (%d bytes)", len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ota_espnow_distribution_active()) {
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_ota_status_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_ota_status_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
ota_espnow_master_on_status(info->src_addr, &msg.payload.ota_status);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_accel_sample_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
handle_accel_sample(info->src_addr, &msg.payload.accel_sample);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_tap_event_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
handle_tap_event(info->src_addr, &msg.payload.tap_event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_battery_report_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
handle_battery_report(info->src_addr, &msg.payload.battery_report);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_echo_pong_tag) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
echo_ping_on_pong(info->src_addr, &msg.payload.echo_pong);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type == alox_EspNowMessageType_ESPNOW_BATTERY_REPORT &&
|
||||||
|
msg.which_payload != alox_EspNowMessage_battery_report_tag) {
|
||||||
|
ESP_LOGW(TAG, "BATTERY_REPORT type but which=%u", msg.which_payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const alox_EspNowSlavePresence *presence = esp_now_proto_get_presence(&msg);
|
||||||
|
if (presence != NULL) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
handle_client_presence(presence, info->src_addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void discover_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_DISCOVER;
|
||||||
|
msg.which_payload = alox_EspNowMessage_discover_tag;
|
||||||
|
msg.payload.discover.network = esp_now_core_network();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "discover on network %u ch %u", (unsigned)esp_now_core_network(),
|
||||||
|
(unsigned)esp_now_core_wifi_channel());
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
msg.payload.discover.master_ota_pending = ota_session_busy();
|
||||||
|
if (!ota_espnow_distribution_active()) {
|
||||||
|
esp_now_core_send_fast(ESPNOW_BCAST, &msg);
|
||||||
|
}
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(ESPNOW_DISCOVER_INTERVAL_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void monitor_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
uint32_t last_local_battery_ms = 0;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "monitor (client timeout %u ms)",
|
||||||
|
(unsigned)ESPNOW_CLIENT_TIMEOUT_MS);
|
||||||
|
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
client_registry_set_master_battery(&reading);
|
||||||
|
last_local_battery_ms = esp_now_core_now_ms();
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
||||||
|
client_registry_check_timeouts(ESPNOW_CLIENT_TIMEOUT_MS);
|
||||||
|
|
||||||
|
uint32_t t = esp_now_core_now_ms();
|
||||||
|
if (t - last_local_battery_ms >= ESPNOW_BATTERY_INTERVAL_MS) {
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
client_registry_set_master_battery(&reading);
|
||||||
|
last_local_battery_ms = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_master_start(void) {
|
||||||
|
ESP_ERROR_CHECK(esp_now_core_ensure_broadcast_peer());
|
||||||
|
|
||||||
|
if (xTaskCreate(discover_task, "espnow_disc", 4096, NULL, 4, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create discover task");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (xTaskCreate(monitor_task, "espnow_mon", 4096, NULL, 4, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create monitor task");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,646 @@
|
|||||||
|
#include "esp_now_slave.h"
|
||||||
|
#include "bosch456.h"
|
||||||
|
#include "cmd_led_ring.h"
|
||||||
|
#include "esp_now_comm.h"
|
||||||
|
#include "esp_now_core.h"
|
||||||
|
#include "esp_now_proto.h"
|
||||||
|
#include "board_input.h"
|
||||||
|
#include "led_ring.h"
|
||||||
|
#include "ota_espnow.h"
|
||||||
|
#include "ota_uart.h"
|
||||||
|
#include "pod_reboot.h"
|
||||||
|
#include "pod_settings.h"
|
||||||
|
#include "esp_log.h"
|
||||||
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/idf_additions.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#ifndef POWERPOD_FW_VERSION
|
||||||
|
#define POWERPOD_FW_VERSION 1u
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#define ESPNOW_HEARTBEAT_INTERVAL_MS 1000
|
||||||
|
#define SLAVE_MASTER_LOST_MS (ESPNOW_HEARTBEAT_INTERVAL_MS * 5)
|
||||||
|
/** While master or slave OTA is in progress (discover may be sparse). */
|
||||||
|
#define SLAVE_MASTER_OTA_GRACE_MS 300000u
|
||||||
|
#define ESPNOW_ACCEL_INTERVAL_MS 16
|
||||||
|
#define ESPNOW_BATTERY_INTERVAL_MS 30000
|
||||||
|
#define SLAVE_BATTERY_AFTER_JOIN_MS 150
|
||||||
|
|
||||||
|
static const char *TAG = "[ESPNOW_S]";
|
||||||
|
|
||||||
|
static bool s_joined;
|
||||||
|
static bool s_master_ota_grace;
|
||||||
|
static bool s_accel_stream_enabled;
|
||||||
|
static bool s_tap_notify_single;
|
||||||
|
static bool s_tap_notify_double;
|
||||||
|
static bool s_tap_notify_triple;
|
||||||
|
static uint8_t s_master_mac[ESP_NOW_ETH_ALEN];
|
||||||
|
static uint32_t s_last_discover_ms;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
SLAVE_TX_SLAVE_INFO = 1,
|
||||||
|
SLAVE_TX_BATTERY,
|
||||||
|
} slave_tx_op_t;
|
||||||
|
|
||||||
|
static QueueHandle_t s_tx_queue;
|
||||||
|
|
||||||
|
static bool from_joined_master(const uint8_t *master_mac) {
|
||||||
|
return s_joined && esp_now_core_mac_equal(master_mac, s_master_mac);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_presence(alox_EspNowSlavePresence *presence) {
|
||||||
|
const uint8_t *own = esp_now_core_own_mac();
|
||||||
|
presence->network = esp_now_core_network();
|
||||||
|
presence->version = POWERPOD_FW_VERSION;
|
||||||
|
presence->slave_id = own[5];
|
||||||
|
presence->available = true;
|
||||||
|
presence->used = false;
|
||||||
|
esp_now_proto_setup_presence_encode(presence, own);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_presence(const uint8_t *dest_mac, alox_EspNowMessageType type) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
alox_EspNowSlavePresence *presence = NULL;
|
||||||
|
|
||||||
|
msg.type = type;
|
||||||
|
if (type == alox_EspNowMessageType_ESPNOW_SLAVE_INFO) {
|
||||||
|
msg.which_payload = alox_EspNowMessage_slave_info_tag;
|
||||||
|
presence = &msg.payload.slave_info;
|
||||||
|
} else {
|
||||||
|
msg.which_payload = alox_EspNowMessage_heartbeat_tag;
|
||||||
|
presence = &msg.payload.heartbeat;
|
||||||
|
}
|
||||||
|
fill_presence(presence);
|
||||||
|
esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_accel_sample(const uint8_t *dest_mac, uint32_t slave_id,
|
||||||
|
int16_t x, int16_t y, int16_t z) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE;
|
||||||
|
msg.which_payload = alox_EspNowMessage_accel_sample_tag;
|
||||||
|
msg.payload.accel_sample.slave_id = slave_id;
|
||||||
|
msg.payload.accel_sample.x = x;
|
||||||
|
msg.payload.accel_sample.y = y;
|
||||||
|
msg.payload.accel_sample.z = z;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_tap_event(const uint8_t *dest_mac, uint32_t slave_id,
|
||||||
|
uint32_t kind) {
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_TAP_EVENT;
|
||||||
|
msg.which_payload = alox_EspNowMessage_tap_event_tag;
|
||||||
|
msg.payload.tap_event.slave_id = slave_id;
|
||||||
|
msg.payload.tap_event.kind = kind;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_battery_report(const uint8_t *dest_mac,
|
||||||
|
const alox_EspNowBatteryReport *report) {
|
||||||
|
if (report == NULL) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_BATTERY_REPORT;
|
||||||
|
msg.which_payload = alox_EspNowMessage_battery_report_tag;
|
||||||
|
msg.payload.battery_report = *report;
|
||||||
|
return esp_now_core_send(dest_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static uint32_t slave_master_lost_ms(void) {
|
||||||
|
if (ota_uart_is_active() || s_master_ota_grace) {
|
||||||
|
return SLAVE_MASTER_OTA_GRACE_MS;
|
||||||
|
}
|
||||||
|
return SLAVE_MASTER_LOST_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void touch_master_presence(uint32_t now) { s_last_discover_ms = now; }
|
||||||
|
|
||||||
|
static void reset_join(void) {
|
||||||
|
s_joined = false;
|
||||||
|
s_master_ota_grace = false;
|
||||||
|
s_accel_stream_enabled = false;
|
||||||
|
memset(s_master_mac, 0, sizeof(s_master_mac));
|
||||||
|
s_last_discover_ms = 0;
|
||||||
|
if (s_tx_queue != NULL) {
|
||||||
|
xQueueReset(s_tx_queue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void queue_tx(slave_tx_op_t op) {
|
||||||
|
if (s_tx_queue == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (xQueueSend(s_tx_queue, &op, 0) != pdTRUE) {
|
||||||
|
ESP_LOGW(TAG, "tx queue full (op=%d)", (int)op);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void send_battery_to_master(void) {
|
||||||
|
if (!s_joined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
board_lipo_reading_t reading;
|
||||||
|
board_input_read_lipo(&reading);
|
||||||
|
|
||||||
|
alox_EspNowBatteryReport report = alox_EspNowBatteryReport_init_zero;
|
||||||
|
report.client_id = esp_now_core_own_mac()[5];
|
||||||
|
report.lipo1_valid = reading.lipo1_valid;
|
||||||
|
report.lipo2_valid = reading.lipo2_valid;
|
||||||
|
report.lipo1_mv = reading.lipo1_mv;
|
||||||
|
report.lipo2_mv = reading.lipo2_mv;
|
||||||
|
|
||||||
|
esp_err_t err = send_battery_report(s_master_mac, &report);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "battery report send failed id=%lu: %s",
|
||||||
|
(unsigned long)report.client_id, esp_err_to_name(err));
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "battery report sent id=%lu L1=%s %lu mV L2=%s %lu mV",
|
||||||
|
(unsigned long)report.client_id,
|
||||||
|
report.lipo1_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)report.lipo1_mv,
|
||||||
|
report.lipo2_valid ? "ok" : "n/a",
|
||||||
|
(unsigned long)report.lipo2_mv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_comm_send_ota_status(const uint8_t master_mac[CLIENT_MAC_LEN],
|
||||||
|
uint32_t status, uint32_t bytes_written,
|
||||||
|
uint32_t error) {
|
||||||
|
if (master_mac == NULL || esp_now_core_is_master()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
|
}
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_OTA_STATUS;
|
||||||
|
msg.which_payload = alox_EspNowMessage_ota_status_tag;
|
||||||
|
msg.payload.ota_status.status = status;
|
||||||
|
msg.payload.ota_status.bytes_written = bytes_written;
|
||||||
|
msg.payload.ota_status.error = error;
|
||||||
|
return esp_now_core_send_wait(master_mac, &msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool esp_now_comm_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
|
||||||
|
return esp_now_slave_get_master_mac(mac_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool esp_now_slave_get_master_mac(uint8_t mac_out[CLIENT_MAC_LEN]) {
|
||||||
|
if (mac_out == NULL || !s_joined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
memcpy(mac_out, s_master_mac, CLIENT_MAC_LEN);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void tx_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
slave_tx_op_t op;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "deferred tx task ready");
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (xQueueReceive(s_tx_queue, &op, portMAX_DELAY) != pdTRUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!s_joined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (op) {
|
||||||
|
case SLAVE_TX_SLAVE_INFO:
|
||||||
|
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_SLAVE_INFO);
|
||||||
|
break;
|
||||||
|
case SLAVE_TX_BATTERY:
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(SLAVE_BATTERY_AFTER_JOIN_MS));
|
||||||
|
send_battery_to_master();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_unicast_test(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowUnicastTest *test) {
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "UNICAST TEST OK from master %s seq=%lu (joined=%d)", mac_str,
|
||||||
|
(unsigned long)test->seq, (int)s_joined);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_echo_ping(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowEchoPing *ping) {
|
||||||
|
if (ping == NULL || !s_joined ||
|
||||||
|
!esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW PING recv from %s host_ts=%llu master_time_us=%llu",
|
||||||
|
mac_str, (unsigned long long)ping->host_timestamp_us,
|
||||||
|
(unsigned long long)ping->master_time_us);
|
||||||
|
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
msg.type = alox_EspNowMessageType_ESPNOW_ECHO_PONG;
|
||||||
|
msg.which_payload = alox_EspNowMessage_echo_pong_tag;
|
||||||
|
msg.payload.echo_pong.host_timestamp_us = ping->host_timestamp_us;
|
||||||
|
msg.payload.echo_pong.master_time_us = ping->master_time_us;
|
||||||
|
|
||||||
|
esp_err_t err = esp_now_core_send_fast(s_master_mac, &msg);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "ECHO PONG send failed: %s", esp_err_to_name(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW PONG send to %s host_ts=%llu master_time_us=%llu",
|
||||||
|
mac_str, (unsigned long long)ping->host_timestamp_us,
|
||||||
|
(unsigned long long)ping->master_time_us);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_restart(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowRestart *req) {
|
||||||
|
const uint8_t *own = esp_now_core_own_mac();
|
||||||
|
uint32_t my_id = own[5];
|
||||||
|
|
||||||
|
if (req->client_id != 0 && req->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "RESTART from master %s (id=%lu)", mac_str, (unsigned long)my_id);
|
||||||
|
pod_schedule_restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_battery_query(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowBatteryQuery *query) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (query->client_id != 0 && query->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
send_battery_to_master();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_led_ring(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowLedRing *msg) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (msg->client_id != 0 && msg->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_LedRingProgressRequest req = alox_LedRingProgressRequest_init_zero;
|
||||||
|
req.mode = msg->mode;
|
||||||
|
req.progress = msg->progress;
|
||||||
|
req.digit = msg->digit;
|
||||||
|
req.r = msg->r;
|
||||||
|
req.g = msg->g;
|
||||||
|
req.b = msg->b;
|
||||||
|
req.intensity = msg->intensity;
|
||||||
|
req.blink_ms = msg->blink_ms;
|
||||||
|
req.blink_count = msg->blink_count;
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "LED_RING mode %lu from master %s (id=%lu)",
|
||||||
|
(unsigned long)req.mode, mac_str, (unsigned long)my_id);
|
||||||
|
cmd_led_ring_apply(&req);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_find_me(const uint8_t *master_mac, const alox_EspNowFindMe *req) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (req->client_id != 0 && req->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "FIND_ME from master %s (id=%lu)", mac_str, (unsigned long)my_id);
|
||||||
|
led_ring_find_me();
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_accel_stream(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowAccelStream *cfg) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (cfg->client_id != 0 && cfg->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_accel_stream_enabled = cfg->enable;
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "accel stream %s from master %s (id=%lu)",
|
||||||
|
cfg->enable ? "on" : "off", mac_str, (unsigned long)my_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_tap_notify(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowTapNotify *cfg) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (cfg->client_id != 0 && cfg->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_tap_notify_single = cfg->single;
|
||||||
|
s_tap_notify_double = cfg->double_tap;
|
||||||
|
s_tap_notify_triple = cfg->triple;
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"tap notify single=%d double=%d triple=%d from master %s (id=%lu)",
|
||||||
|
cfg->single, cfg->double_tap, cfg->triple, mac_str,
|
||||||
|
(unsigned long)my_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_accel_deadzone(const uint8_t *master_mac,
|
||||||
|
const alox_EspNowAccelDeadzone *cfg) {
|
||||||
|
uint32_t my_id = esp_now_core_own_mac()[5];
|
||||||
|
if (cfg->client_id != 0 && cfg->client_id != my_id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (s_joined && !esp_now_core_mac_equal(master_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(master_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG,
|
||||||
|
"accel deadzone from master %s: %lu LSB id=%lu (sensor %s)", mac_str,
|
||||||
|
(unsigned long)cfg->deadzone, (unsigned long)my_id,
|
||||||
|
bma456_is_ready() ? "ok" : "not installed");
|
||||||
|
bma456_set_accel_deadzone(cfg->deadzone);
|
||||||
|
if (pod_settings_save_accel_deadzone(cfg->deadzone) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "deadzone %lu applied but not saved to NVS",
|
||||||
|
(unsigned long)cfg->deadzone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void handle_discover(const uint8_t *sender_mac,
|
||||||
|
const alox_EspNowDiscover *discover) {
|
||||||
|
if (discover->network != esp_now_core_network()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t now = esp_now_core_now_ms();
|
||||||
|
|
||||||
|
if (s_joined) {
|
||||||
|
if (!esp_now_core_mac_equal(sender_mac, s_master_mac)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_master_ota_grace = discover->master_ota_pending;
|
||||||
|
if ((now - s_last_discover_ms) <= slave_master_lost_ms()) {
|
||||||
|
touch_master_presence(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ota_uart_is_active()) {
|
||||||
|
touch_master_presence(now);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "master lost, rejoining");
|
||||||
|
reset_join();
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
|
||||||
|
s_joined = true;
|
||||||
|
s_master_ota_grace = discover->master_ota_pending;
|
||||||
|
touch_master_presence(now);
|
||||||
|
esp_now_core_ensure_peer(sender_mac);
|
||||||
|
|
||||||
|
char mac_str[18];
|
||||||
|
esp_now_core_mac_to_str(sender_mac, mac_str, sizeof(mac_str));
|
||||||
|
ESP_LOGI(TAG, "joined network %u, master %s", (unsigned)discover->network,
|
||||||
|
mac_str);
|
||||||
|
|
||||||
|
queue_tx(SLAVE_TX_SLAVE_INFO);
|
||||||
|
queue_tx(SLAVE_TX_BATTERY);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void check_master_timeout(void) {
|
||||||
|
if (!s_joined || s_last_discover_ms == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ota_uart_is_active()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
uint32_t now = esp_now_core_now_ms();
|
||||||
|
uint32_t limit = slave_master_lost_ms();
|
||||||
|
if ((now - s_last_discover_ms) > limit) {
|
||||||
|
ESP_LOGW(TAG, "no master discover for %u ms (limit %u), reconnecting",
|
||||||
|
(unsigned)(now - s_last_discover_ms), (unsigned)limit);
|
||||||
|
reset_join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void accel_stream_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
const uint8_t *own = esp_now_core_own_mac();
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "accel stream task (interval %u ms)",
|
||||||
|
(unsigned)ESPNOW_ACCEL_INTERVAL_MS);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(ESPNOW_ACCEL_INTERVAL_MS));
|
||||||
|
if (!s_joined || !s_accel_stream_enabled || !bma456_is_ready()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int16_t x = 0;
|
||||||
|
int16_t y = 0;
|
||||||
|
int16_t z = 0;
|
||||||
|
if (bma456_read_accel(&x, &y, &z) != ESP_OK) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
(void)send_accel_sample(s_master_mac, own[5], x, y, z);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void on_bma456_tap(bma456_tap_kind_t kind, void *ctx) {
|
||||||
|
(void)ctx;
|
||||||
|
if (!s_joined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bool enabled = false;
|
||||||
|
switch (kind) {
|
||||||
|
case BMA456_TAP_SINGLE:
|
||||||
|
enabled = s_tap_notify_single;
|
||||||
|
break;
|
||||||
|
case BMA456_TAP_DOUBLE:
|
||||||
|
enabled = s_tap_notify_double;
|
||||||
|
break;
|
||||||
|
case BMA456_TAP_TRIPLE:
|
||||||
|
enabled = s_tap_notify_triple;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)send_tap_event(s_master_mac, esp_now_core_own_mac()[5], (uint32_t)kind);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void heartbeat_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
uint32_t last_battery_ms = 0;
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "heartbeat task (interval %u ms)",
|
||||||
|
(unsigned)ESPNOW_HEARTBEAT_INTERVAL_MS);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(ESPNOW_HEARTBEAT_INTERVAL_MS));
|
||||||
|
check_master_timeout();
|
||||||
|
if (!s_joined) {
|
||||||
|
last_battery_ms = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
send_presence(s_master_mac, alox_EspNowMessageType_ESPNOW_HEARTBEAT);
|
||||||
|
uint32_t now = esp_now_core_now_ms();
|
||||||
|
if (last_battery_ms == 0 ||
|
||||||
|
(now - last_battery_ms) >= ESPNOW_BATTERY_INTERVAL_MS) {
|
||||||
|
send_battery_to_master();
|
||||||
|
last_battery_ms = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
|
||||||
|
int len) {
|
||||||
|
if (info == NULL || data == NULL || len <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alox_EspNowMessage msg = alox_EspNowMessage_init_zero;
|
||||||
|
if (esp_now_proto_decode(data, (size_t)len, &msg) != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "decode failed (%d bytes)", len);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
esp_now_core_ensure_peer(info->src_addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ota_uart_is_active()) {
|
||||||
|
switch (msg.which_payload) {
|
||||||
|
case alox_EspNowMessage_discover_tag:
|
||||||
|
handle_discover(info->src_addr, &msg.payload.discover);
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_ota_start_tag:
|
||||||
|
case alox_EspNowMessage_ota_payload_tag:
|
||||||
|
case alox_EspNowMessage_ota_end_tag:
|
||||||
|
if (!from_joined_master(info->src_addr)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
touch_master_presence(esp_now_core_now_ms());
|
||||||
|
if (msg.which_payload == alox_EspNowMessage_ota_start_tag) {
|
||||||
|
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
|
||||||
|
} else if (msg.which_payload == alox_EspNowMessage_ota_payload_tag) {
|
||||||
|
ota_espnow_slave_on_payload(info->src_addr, &msg.payload.ota_payload);
|
||||||
|
} else {
|
||||||
|
ota_espnow_slave_on_end(info->src_addr);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (msg.which_payload) {
|
||||||
|
case alox_EspNowMessage_discover_tag:
|
||||||
|
handle_discover(info->src_addr, &msg.payload.discover);
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_unicast_test_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_unicast_test(info->src_addr, &msg.payload.unicast_test);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_echo_ping_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_echo_ping(info->src_addr, &msg.payload.echo_ping);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_accel_deadzone_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_accel_deadzone(info->src_addr, &msg.payload.accel_deadzone);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_accel_stream_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_accel_stream(info->src_addr, &msg.payload.accel_stream);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_tap_notify_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_tap_notify(info->src_addr, &msg.payload.tap_notify);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_battery_query_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_battery_query(info->src_addr, &msg.payload.battery_query);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_led_ring_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_led_ring(info->src_addr, &msg.payload.led_ring);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_find_me_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_find_me(info->src_addr, &msg.payload.find_me);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_restart_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
handle_restart(info->src_addr, &msg.payload.restart);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case alox_EspNowMessage_ota_start_tag:
|
||||||
|
if (from_joined_master(info->src_addr)) {
|
||||||
|
ota_espnow_slave_on_start(info->src_addr, &msg.payload.ota_start);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGW(TAG, "unhandled which=%u type=%u", msg.which_payload,
|
||||||
|
(unsigned)msg.type);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t esp_now_slave_start(void) {
|
||||||
|
reset_join();
|
||||||
|
|
||||||
|
s_tx_queue = xQueueCreate(4, sizeof(slave_tx_op_t));
|
||||||
|
if (s_tx_queue == NULL) {
|
||||||
|
ESP_LOGE(TAG, "failed to create tx queue");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
if (xTaskCreate(tx_task, "espnow_stx", 4096, NULL, 5, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create tx task");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (xTaskCreate(heartbeat_task, "espnow_hb", 4096, NULL, 4, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create heartbeat task");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
if (xTaskCreate(accel_stream_task, "espnow_accel", 4096, NULL, 5, NULL) !=
|
||||||
|
pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create accel stream task");
|
||||||
|
return ESP_FAIL;
|
||||||
|
}
|
||||||
|
ota_espnow_slave_init();
|
||||||
|
bma456_set_tap_handler(on_bma456_tap, NULL);
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
@@ -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
-1
@@ -5,6 +5,7 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include "led_strip.h"
|
#include "led_strip.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
@@ -23,8 +24,12 @@ static led_strip_handle_t led_ring;
|
|||||||
#define LED_RING_FIND_ME_ON_MS 300
|
#define LED_RING_FIND_ME_ON_MS 300
|
||||||
#define LED_RING_FIND_ME_OFF_MS 150
|
#define LED_RING_FIND_ME_OFF_MS 150
|
||||||
#define LED_RING_FIND_ME_BLINKS_PER_COLOR 3
|
#define LED_RING_FIND_ME_BLINKS_PER_COLOR 3
|
||||||
|
#define LED_RING_BATTERY_LOW_R 255
|
||||||
|
#define LED_RING_BATTERY_LOW_G 0
|
||||||
|
#define LED_RING_BATTERY_LOW_B 0
|
||||||
|
|
||||||
static QueueHandle_t led_queue;
|
static QueueHandle_t led_queue;
|
||||||
|
static SemaphoreHandle_t s_battery_low_done;
|
||||||
|
|
||||||
// Led Matrix Maps
|
// Led Matrix Maps
|
||||||
const uint8_t d0[] = {46, 47, 60, 61, 62, 75, 78, 79,
|
const uint8_t d0[] = {46, 47, 60, 61, 62, 75, 78, 79,
|
||||||
@@ -116,6 +121,8 @@ void vTaskLedRing(void *pvParameters) {
|
|||||||
for (int i = 0; i < digit.count; i++) {
|
for (int i = 0; i < digit.count; i++) {
|
||||||
led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], r, g, b);
|
led_strip_set_pixel(led_ring, RING_LEDS - digit.leds[i], r, g, b);
|
||||||
}
|
}
|
||||||
|
} else if (cmd.mode == LED_CMD_SET_COLOR) {
|
||||||
|
ring_fill_color(r, g, b);
|
||||||
} else if (cmd.mode == LED_CMD_PROGRESS) {
|
} else if (cmd.mode == LED_CMD_PROGRESS) {
|
||||||
uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100;
|
uint32_t lit = ((uint32_t)cmd.progress * RING_LEDS + 50) / 100;
|
||||||
if (lit > RING_LEDS) {
|
if (lit > RING_LEDS) {
|
||||||
@@ -143,6 +150,22 @@ void vTaskLedRing(void *pvParameters) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
|
} else if (cmd.mode == LED_CMD_BATTERY_LOW) {
|
||||||
|
uint8_t r = LED_RING_BATTERY_LOW_R;
|
||||||
|
uint8_t g = LED_RING_BATTERY_LOW_G;
|
||||||
|
uint8_t b = LED_RING_BATTERY_LOW_B;
|
||||||
|
led_ring_scale_rgb(&r, &g, &b, LED_RING_BATTERY_LOW_INTENSITY);
|
||||||
|
for (uint32_t i = 0; i < LED_RING_BATTERY_LOW_LEDS; i++) {
|
||||||
|
led_strip_set_pixel(led_ring, i, r, g, b);
|
||||||
|
}
|
||||||
|
led_strip_refresh(led_ring);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(LED_RING_BATTERY_LOW_HOLD_MS));
|
||||||
|
led_strip_clear(led_ring);
|
||||||
|
led_strip_refresh(led_ring);
|
||||||
|
if (s_battery_low_done != NULL) {
|
||||||
|
xSemaphoreGive(s_battery_low_done);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
led_strip_refresh(led_ring);
|
led_strip_refresh(led_ring);
|
||||||
}
|
}
|
||||||
@@ -151,12 +174,13 @@ void vTaskLedRing(void *pvParameters) {
|
|||||||
|
|
||||||
void led_ring_init(void) {
|
void led_ring_init(void) {
|
||||||
led_queue = xQueueCreate(10, sizeof(led_command_t));
|
led_queue = xQueueCreate(10, sizeof(led_command_t));
|
||||||
|
s_battery_low_done = xSemaphoreCreateBinary();
|
||||||
xTaskCreate(vTaskLedRing, "led_task", 4096, NULL, 5, NULL);
|
xTaskCreate(vTaskLedRing, "led_task", 4096, NULL, 5, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
void led_ring_send_command(led_command_t *cmd) {
|
void led_ring_send_command(led_command_t *cmd) {
|
||||||
if (led_queue != NULL) {
|
if (led_queue != NULL) {
|
||||||
xQueueSend(led_queue, cmd, portMAX_DELAY);
|
(void)xQueueSend(led_queue, cmd, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,3 +246,13 @@ void led_ring_find_me(void) {
|
|||||||
led_command_t cmd = {.mode = LED_CMD_FIND_ME};
|
led_command_t cmd = {.mode = LED_CMD_FIND_ME};
|
||||||
led_ring_send_command(&cmd);
|
led_ring_send_command(&cmd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void led_ring_show_battery_low(void) {
|
||||||
|
if (led_queue == NULL || s_battery_low_done == NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
(void)xSemaphoreTake(s_battery_low_done, 0);
|
||||||
|
led_command_t cmd = {.mode = LED_CMD_BATTERY_LOW};
|
||||||
|
led_ring_send_command(&cmd);
|
||||||
|
(void)xSemaphoreTake(s_battery_low_done, portMAX_DELAY);
|
||||||
|
}
|
||||||
|
|||||||
+9
-1
@@ -7,6 +7,10 @@
|
|||||||
#define LED_RING_DEFAULT_INTENSITY 13
|
#define LED_RING_DEFAULT_INTENSITY 13
|
||||||
/** Full brightness for find-me and similar alerts. */
|
/** Full brightness for find-me and similar alerts. */
|
||||||
#define LED_RING_FULL_INTENSITY 255
|
#define LED_RING_FULL_INTENSITY 255
|
||||||
|
/** ~10 % brightness for battery-low indicator. */
|
||||||
|
#define LED_RING_BATTERY_LOW_INTENSITY 26
|
||||||
|
#define LED_RING_BATTERY_LOW_LEDS 4
|
||||||
|
#define LED_RING_BATTERY_LOW_HOLD_MS 5000
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LED_CMD_CLEAR,
|
LED_CMD_CLEAR,
|
||||||
@@ -14,7 +18,8 @@ typedef enum {
|
|||||||
LED_CMD_SET_COLOR,
|
LED_CMD_SET_COLOR,
|
||||||
LED_CMD_PROGRESS,
|
LED_CMD_PROGRESS,
|
||||||
LED_CMD_BLINK,
|
LED_CMD_BLINK,
|
||||||
LED_CMD_FIND_ME
|
LED_CMD_FIND_ME,
|
||||||
|
LED_CMD_BATTERY_LOW
|
||||||
} led_mode_t;
|
} led_mode_t;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
@@ -45,4 +50,7 @@ void led_ring_ota_failed(void);
|
|||||||
/** Red / green / blue: 3 blinks each at full intensity. */
|
/** Red / green / blue: 3 blinks each at full intensity. */
|
||||||
void led_ring_find_me(void);
|
void led_ring_find_me(void);
|
||||||
|
|
||||||
|
/** First 4 LEDs red at ~10 % for 5 s, then off (blocking in LED task). */
|
||||||
|
void led_ring_show_battery_low(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+298
-82
@@ -18,9 +18,9 @@ static const char *TAG = "[OTA_ESPNOW]";
|
|||||||
#define OTA_ESPNOW_PREPARE_PRIO 5
|
#define OTA_ESPNOW_PREPARE_PRIO 5
|
||||||
|
|
||||||
#define OTA_PREPARE_TIMEOUT_MS 120000u
|
#define OTA_PREPARE_TIMEOUT_MS 120000u
|
||||||
#define OTA_BLOCK_TIMEOUT_MS 30000u
|
#define OTA_BLOCK_TIMEOUT_PER_SLAVE_MS 2000u
|
||||||
|
#define OTA_BLOCK_MAX_RETRIES 2u
|
||||||
#define OTA_END_TIMEOUT_MS 60000u
|
#define OTA_END_TIMEOUT_MS 60000u
|
||||||
#define OTA_PAYLOAD_DELAY_MS 3
|
|
||||||
|
|
||||||
#define OTA_ST_PREPARING 1u
|
#define OTA_ST_PREPARING 1u
|
||||||
#define OTA_ST_READY 2u
|
#define OTA_ST_READY 2u
|
||||||
@@ -35,7 +35,29 @@ static const char *TAG = "[OTA_ESPNOW]";
|
|||||||
|
|
||||||
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
|
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
|
||||||
|
|
||||||
|
/** ~21 payloads per 4 KiB block; headroom for bursts + status/end. */
|
||||||
|
#define OTA_SLAVE_WORK_QUEUE_LEN 32
|
||||||
|
#define OTA_SLAVE_WORK_STACK 8192
|
||||||
|
#define OTA_SLAVE_WORK_PRIO 5
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
OTA_SLAVE_WORK_STATUS = 1,
|
||||||
|
OTA_SLAVE_WORK_PAYLOAD,
|
||||||
|
OTA_SLAVE_WORK_END,
|
||||||
|
} ota_slave_work_op_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
ota_slave_work_op_t op;
|
||||||
|
uint8_t master_mac[6];
|
||||||
|
uint32_t status;
|
||||||
|
uint32_t bytes_written;
|
||||||
|
uint32_t error;
|
||||||
|
alox_EspNowOtaPayload payload;
|
||||||
|
} ota_slave_work_t;
|
||||||
|
|
||||||
static EventGroupHandle_t s_eg;
|
static EventGroupHandle_t s_eg;
|
||||||
|
static QueueHandle_t s_slave_work_queue;
|
||||||
|
static bool s_distribution_active;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t count;
|
uint8_t count;
|
||||||
@@ -152,11 +174,204 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
|
|||||||
return (got & want_bits) == want_bits;
|
return (got & want_bits) == want_bits;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static uint32_t block_ack_timeout_ms(void) {
|
||||||
|
if (s_dist.count == 0) {
|
||||||
|
return OTA_BLOCK_TIMEOUT_PER_SLAVE_MS;
|
||||||
|
}
|
||||||
|
return (uint32_t)s_dist.count * OTA_BLOCK_TIMEOUT_PER_SLAVE_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void log_missing_block_acks(uint32_t expected_bytes) {
|
||||||
|
if (s_eg == NULL || s_dist.count == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
EventBits_t bits = xEventGroupGetBits(s_eg);
|
||||||
|
for (uint8_t i = 0; i < s_dist.count; i++) {
|
||||||
|
uint32_t bit = (1u << (unsigned)i);
|
||||||
|
if (bits & bit) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const ota_prog_entry_t *e = &s_prog.entries[i];
|
||||||
|
ESP_LOGE(TAG,
|
||||||
|
"slave %lu missing block ack @%lu (last status=%lu bytes=%lu err=%lu)",
|
||||||
|
(unsigned long)s_dist.id[i], (unsigned long)expected_bytes,
|
||||||
|
(unsigned long)e->status, (unsigned long)e->bytes_written,
|
||||||
|
(unsigned long)e->error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static esp_err_t send_block_payloads(const uint8_t *block_buf, uint32_t block_len,
|
||||||
|
uint32_t *seq_io) {
|
||||||
|
uint32_t sent = 0;
|
||||||
|
while (sent < block_len) {
|
||||||
|
uint32_t chunk = block_len - sent;
|
||||||
|
if (chunk > OTA_UART_HOST_CHUNK_SIZE) {
|
||||||
|
chunk = OTA_UART_HOST_CHUNK_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint8_t i = 0; i < s_dist.count; i++) {
|
||||||
|
esp_err_t err = esp_now_comm_send_ota_payload(s_dist.mac[i], *seq_io,
|
||||||
|
block_buf + sent, chunk);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*seq_io)++;
|
||||||
|
sent += chunk;
|
||||||
|
}
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ota_espnow_distribution_active(void) { return s_distribution_active; }
|
||||||
|
|
||||||
static void send_slave_status(const uint8_t master_mac[6], uint32_t status,
|
static void send_slave_status(const uint8_t master_mac[6], uint32_t status,
|
||||||
uint32_t bytes_written, uint32_t error) {
|
uint32_t bytes_written, uint32_t error) {
|
||||||
esp_now_comm_send_ota_status(master_mac, status, bytes_written, error);
|
esp_now_comm_send_ota_status(master_mac, status, bytes_written, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool queue_slave_work(const ota_slave_work_t *work) {
|
||||||
|
if (work == NULL || s_slave_work_queue == NULL) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (xQueueSend(s_slave_work_queue, work, 0) != pdTRUE) {
|
||||||
|
ESP_LOGW(TAG, "slave OTA work queue full (op=%d)", (int)work->op);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void queue_slave_status(const uint8_t master_mac[6], uint32_t status,
|
||||||
|
uint32_t bytes_written, uint32_t error) {
|
||||||
|
ota_slave_work_t work = {
|
||||||
|
.op = OTA_SLAVE_WORK_STATUS,
|
||||||
|
.status = status,
|
||||||
|
.bytes_written = bytes_written,
|
||||||
|
.error = error,
|
||||||
|
};
|
||||||
|
memcpy(work.master_mac, master_mac, 6);
|
||||||
|
(void)queue_slave_work(&work);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_slave_payload(const uint8_t master_mac[6],
|
||||||
|
const alox_EspNowOtaPayload *payload) {
|
||||||
|
if (payload == NULL || payload->data.size == 0) {
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ota_uart_is_active()) {
|
||||||
|
ESP_LOGW(TAG, "OTA_PAYLOAD seq=%lu but no active session",
|
||||||
|
(unsigned long)payload->seq);
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, 0, 12);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload->seq == 0) {
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW OTA payloads started");
|
||||||
|
}
|
||||||
|
|
||||||
|
ota_feed_result_t r = ota_uart_feed_chunk(payload->seq, payload->data.bytes,
|
||||||
|
payload->data.size);
|
||||||
|
if (r == OTA_FEED_SEQ_GAP) {
|
||||||
|
led_ring_ota_failed();
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 16);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r == OTA_FEED_SEQ_DUP) {
|
||||||
|
if (ota_uart_block_ready_for_reack()) {
|
||||||
|
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, ota_uart_bytes_written(),
|
||||||
|
0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r == OTA_FEED_ERROR) {
|
||||||
|
led_ring_ota_failed();
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (r == OTA_FEED_BLOCK_WRITTEN) {
|
||||||
|
uint32_t written = ota_uart_bytes_written();
|
||||||
|
uint32_t total = ota_uart_total_size();
|
||||||
|
ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written);
|
||||||
|
led_ring_show_ota_progress(written, total, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
||||||
|
OTA_LED_ESPNOW_RX_B);
|
||||||
|
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (r == OTA_FEED_OK) {
|
||||||
|
uint32_t total = ota_uart_total_size();
|
||||||
|
if (total > 0) {
|
||||||
|
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
|
||||||
|
OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
||||||
|
OTA_LED_ESPNOW_RX_B);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void process_slave_end(const uint8_t master_mac[6]) {
|
||||||
|
ESP_LOGI(TAG, "ESP-NOW OTA_END");
|
||||||
|
if (!ota_uart_is_active()) {
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, 0, 20);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t written = ota_uart_bytes_written();
|
||||||
|
bool success = false;
|
||||||
|
esp_err_t err = ota_uart_finish(true, &success);
|
||||||
|
if (err != ESP_OK || !success) {
|
||||||
|
led_ring_ota_failed();
|
||||||
|
send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0);
|
||||||
|
led_ring_ota_success();
|
||||||
|
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
|
||||||
|
(unsigned long)written);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void ota_slave_work_task(void *param) {
|
||||||
|
(void)param;
|
||||||
|
ota_slave_work_t work;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
if (xQueueReceive(s_slave_work_queue, &work, portMAX_DELAY) != pdTRUE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (work.op) {
|
||||||
|
case OTA_SLAVE_WORK_STATUS:
|
||||||
|
send_slave_status(work.master_mac, work.status, work.bytes_written,
|
||||||
|
work.error);
|
||||||
|
break;
|
||||||
|
case OTA_SLAVE_WORK_PAYLOAD:
|
||||||
|
process_slave_payload(work.master_mac, &work.payload);
|
||||||
|
break;
|
||||||
|
case OTA_SLAVE_WORK_END:
|
||||||
|
process_slave_end(work.master_mac);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ota_espnow_slave_init(void) {
|
||||||
|
if (s_slave_work_queue != NULL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
s_slave_work_queue = xQueueCreate(OTA_SLAVE_WORK_QUEUE_LEN, sizeof(ota_slave_work_t));
|
||||||
|
if (s_slave_work_queue == NULL) {
|
||||||
|
ESP_LOGE(TAG, "failed to create slave OTA work queue");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (xTaskCreate(ota_slave_work_task, "ota_slave_wrk", OTA_SLAVE_WORK_STACK, NULL,
|
||||||
|
OTA_SLAVE_WORK_PRIO, NULL) != pdPASS) {
|
||||||
|
ESP_LOGE(TAG, "failed to create slave OTA work task");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static void ota_slave_prepare_task(void *param) {
|
static void ota_slave_prepare_task(void *param) {
|
||||||
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
uint32_t total_size = (uint32_t)(uintptr_t)param;
|
||||||
uint8_t master_mac[6];
|
uint8_t master_mac[6];
|
||||||
@@ -190,82 +405,37 @@ void ota_espnow_slave_on_start(const uint8_t master_mac[6],
|
|||||||
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
|
ESP_LOGI(TAG, "ESP-NOW OTA_START (%lu bytes)", (unsigned long)start->total_size);
|
||||||
|
|
||||||
if (ota_uart_is_active()) {
|
if (ota_uart_is_active()) {
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
|
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 4);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
|
if (xTaskCreate(ota_slave_prepare_task, "ota_esp_prep", OTA_ESPNOW_PREPARE_STACK,
|
||||||
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
|
(void *)(uintptr_t)start->total_size, OTA_ESPNOW_PREPARE_PRIO,
|
||||||
NULL) != pdPASS) {
|
NULL) != pdPASS) {
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
|
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
|
void ota_espnow_slave_on_payload(const uint8_t master_mac[6],
|
||||||
const alox_EspNowOtaPayload *payload) {
|
const alox_EspNowOtaPayload *payload) {
|
||||||
if (payload == NULL || payload->data.size == 0) {
|
if (payload == NULL) {
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
|
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 11);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ota_uart_is_active()) {
|
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_PAYLOAD, .payload = *payload};
|
||||||
ESP_LOGW(TAG, "OTA_PAYLOAD seq=%lu but no active session",
|
memcpy(work.master_mac, master_mac, 6);
|
||||||
(unsigned long)payload->seq);
|
if (!queue_slave_work(&work)) {
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 12);
|
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 14);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (payload->seq == 0) {
|
|
||||||
ESP_LOGI(TAG, "ESP-NOW OTA payloads started");
|
|
||||||
}
|
|
||||||
|
|
||||||
ota_feed_result_t r =
|
|
||||||
ota_uart_feed(payload->data.bytes, payload->data.size);
|
|
||||||
if (r == OTA_FEED_ERROR) {
|
|
||||||
led_ring_ota_failed();
|
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, ota_uart_bytes_written(), 13);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (r == OTA_FEED_BLOCK_WRITTEN) {
|
|
||||||
uint32_t written = ota_uart_bytes_written();
|
|
||||||
uint32_t total = ota_uart_total_size();
|
|
||||||
ESP_LOGI(TAG, "block written %lu bytes -> ack master", (unsigned long)written);
|
|
||||||
led_ring_show_ota_progress(written, total, OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
|
||||||
OTA_LED_ESPNOW_RX_B);
|
|
||||||
send_slave_status(master_mac, OTA_ST_BLOCK_ACK, written, 0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == OTA_FEED_OK) {
|
|
||||||
uint32_t total = ota_uart_total_size();
|
|
||||||
if (total > 0) {
|
|
||||||
led_ring_show_ota_progress(ota_uart_bytes_received(), total,
|
|
||||||
OTA_LED_ESPNOW_RX_R, OTA_LED_ESPNOW_RX_G,
|
|
||||||
OTA_LED_ESPNOW_RX_B);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
|
void ota_espnow_slave_on_end(const uint8_t master_mac[6]) {
|
||||||
ESP_LOGI(TAG, "ESP-NOW OTA_END");
|
ota_slave_work_t work = {.op = OTA_SLAVE_WORK_END};
|
||||||
if (!ota_uart_is_active()) {
|
memcpy(work.master_mac, master_mac, 6);
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, 0, 20);
|
if (!queue_slave_work(&work)) {
|
||||||
return;
|
queue_slave_status(master_mac, OTA_ST_FAILED, 0, 15);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t written = ota_uart_bytes_written();
|
|
||||||
bool success = false;
|
|
||||||
esp_err_t err = ota_uart_finish(true, &success);
|
|
||||||
if (err != ESP_OK || !success) {
|
|
||||||
led_ring_ota_failed();
|
|
||||||
send_slave_status(master_mac, OTA_ST_FAILED, written, (uint32_t)err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
send_slave_status(master_mac, OTA_ST_SUCCESS, written, 0);
|
|
||||||
led_ring_ota_success();
|
|
||||||
ESP_LOGI(TAG, "slave OTA success (%lu bytes), reboot to run",
|
|
||||||
(unsigned long)written);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ota_espnow_master_on_status(const uint8_t slave_mac[6],
|
void ota_espnow_master_on_status(const uint8_t slave_mac[6],
|
||||||
@@ -342,6 +512,8 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s_distribution_active = true;
|
||||||
|
|
||||||
memset(&s_dist.progress, 0, sizeof(s_dist.progress));
|
memset(&s_dist.progress, 0, sizeof(s_dist.progress));
|
||||||
if (progress != NULL) {
|
if (progress != NULL) {
|
||||||
s_dist.progress = *progress;
|
s_dist.progress = *progress;
|
||||||
@@ -362,6 +534,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
ESP_LOGW(TAG, "OTA_START to slave %lu failed",
|
ESP_LOGW(TAG, "OTA_START to slave %lu failed",
|
||||||
(unsigned long)s_dist.id[i]);
|
(unsigned long)s_dist.id[i]);
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,6 +542,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
|
if (!wait_target_bits(target_mask, OTA_PREPARE_TIMEOUT_MS)) {
|
||||||
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
|
ESP_LOGE(TAG, "timeout waiting for slave OTA ready");
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,38 +566,77 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
|
ESP_LOGE(TAG, "partition read @%lu failed: %s", (unsigned long)offset,
|
||||||
esp_err_to_name(err));
|
esp_err_to_name(err));
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t sent = 0;
|
|
||||||
while (sent < block_len) {
|
|
||||||
uint32_t chunk = block_len - sent;
|
|
||||||
if (chunk > OTA_UART_HOST_CHUNK_SIZE) {
|
|
||||||
chunk = OTA_UART_HOST_CHUNK_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (uint8_t i = 0; i < s_dist.count; i++) {
|
|
||||||
err = esp_now_comm_send_ota_payload(s_dist.mac[i], seq,
|
|
||||||
block_buf + sent, chunk);
|
|
||||||
if (err != ESP_OK) {
|
|
||||||
prog_end();
|
|
||||||
return err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
seq++;
|
|
||||||
sent += chunk;
|
|
||||||
vTaskDelay(pdMS_TO_TICKS(OTA_PAYLOAD_DELAY_MS));
|
|
||||||
}
|
|
||||||
|
|
||||||
const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE);
|
const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE);
|
||||||
s_dist.expected_bytes = offset + block_len;
|
s_dist.expected_bytes = offset + block_len;
|
||||||
|
const uint32_t block_start_seq = seq;
|
||||||
|
|
||||||
if (full_block) {
|
if (full_block) {
|
||||||
xEventGroupClearBits(s_eg, target_mask);
|
xEventGroupClearBits(s_eg, target_mask);
|
||||||
if (!wait_target_bits(target_mask, OTA_BLOCK_TIMEOUT_MS)) {
|
}
|
||||||
ESP_LOGE(TAG, "timeout block ack @%lu bytes",
|
|
||||||
(unsigned long)s_dist.expected_bytes);
|
bool block_sent = false;
|
||||||
|
for (uint32_t send_attempt = 0; send_attempt <= OTA_BLOCK_MAX_RETRIES;
|
||||||
|
send_attempt++) {
|
||||||
|
if (send_attempt > 0) {
|
||||||
|
seq = block_start_seq;
|
||||||
|
if (full_block) {
|
||||||
|
xEventGroupClearBits(s_eg, target_mask);
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "block send failed @%lu — resend %lu/%lu",
|
||||||
|
(unsigned long)s_dist.expected_bytes,
|
||||||
|
(unsigned long)send_attempt,
|
||||||
|
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
||||||
|
}
|
||||||
|
err = send_block_payloads(block_buf, block_len, &seq);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
block_sent = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!block_sent) {
|
||||||
|
ESP_LOGE(TAG, "block send failed @%lu after %lu retries",
|
||||||
|
(unsigned long)s_dist.expected_bytes,
|
||||||
|
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
||||||
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (full_block) {
|
||||||
|
const uint32_t ack_timeout = block_ack_timeout_ms();
|
||||||
|
bool acked = false;
|
||||||
|
for (uint32_t attempt = 0; attempt <= OTA_BLOCK_MAX_RETRIES; attempt++) {
|
||||||
|
if (wait_target_bits(target_mask, ack_timeout)) {
|
||||||
|
acked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
log_missing_block_acks(s_dist.expected_bytes);
|
||||||
|
if (attempt >= OTA_BLOCK_MAX_RETRIES) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ESP_LOGW(TAG, "block ack timeout @%lu — resend %lu/%lu",
|
||||||
|
(unsigned long)s_dist.expected_bytes,
|
||||||
|
(unsigned long)(attempt + 1),
|
||||||
|
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
||||||
|
xEventGroupClearBits(s_eg, target_mask);
|
||||||
|
seq = block_start_seq;
|
||||||
|
err = send_block_payloads(block_buf, block_len, &seq);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!acked) {
|
||||||
|
ESP_LOGE(TAG, "timeout block ack @%lu bytes after %lu retries",
|
||||||
|
(unsigned long)s_dist.expected_bytes,
|
||||||
|
(unsigned long)OTA_BLOCK_MAX_RETRIES);
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
|
ESP_LOGI(TAG, "block ack @%lu/%lu (%lu%%)",
|
||||||
@@ -445,6 +658,7 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
|
err = esp_now_comm_send_ota_end(s_dist.mac[i]);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -452,11 +666,13 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
|
|||||||
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
|
if (!wait_target_bits(target_mask, OTA_END_TIMEOUT_MS)) {
|
||||||
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
|
ESP_LOGE(TAG, "timeout waiting for slave OTA success");
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
return ESP_ERR_TIMEOUT;
|
return ESP_ERR_TIMEOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
prog_set_aggregate(size);
|
prog_set_aggregate(size);
|
||||||
prog_end();
|
prog_end();
|
||||||
|
s_distribution_active = false;
|
||||||
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
|
ESP_LOGI(TAG, "ESP-NOW OTA complete for %u slave(s)", (unsigned)s_dist.count);
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,4 +37,10 @@ void ota_espnow_slave_on_end(const uint8_t master_mac[6]);
|
|||||||
void ota_espnow_progress_query(uint32_t filter_client_id,
|
void ota_espnow_progress_query(uint32_t filter_client_id,
|
||||||
alox_OtaSlaveProgressResponse *out);
|
alox_OtaSlaveProgressResponse *out);
|
||||||
|
|
||||||
|
/** True while master is pushing a staged image to slaves. */
|
||||||
|
bool ota_espnow_distribution_active(void);
|
||||||
|
|
||||||
|
/** Slave: work queue for OTA (no esp_now_send from recv callback). */
|
||||||
|
void ota_espnow_slave_init(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#ifndef OTA_SESSION_H
|
||||||
|
#define OTA_SESSION_H
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
/** UART upload or ESP-NOW slave distribution in progress. */
|
||||||
|
bool ota_session_busy(void);
|
||||||
|
|
||||||
|
/** During OTA only UART OTA-related commands are accepted on the master. */
|
||||||
|
bool ota_session_uart_cmd_allowed(uint16_t msg_id);
|
||||||
|
|
||||||
|
#endif
|
||||||
+29
-1
@@ -12,6 +12,7 @@ typedef struct {
|
|||||||
uint32_t total_size;
|
uint32_t total_size;
|
||||||
uint32_t received;
|
uint32_t received;
|
||||||
uint32_t written;
|
uint32_t written;
|
||||||
|
uint32_t expected_seq;
|
||||||
int target_slot;
|
int target_slot;
|
||||||
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
|
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
|
||||||
size_t block_len;
|
size_t block_len;
|
||||||
@@ -112,10 +113,30 @@ int ota_uart_prepare(uint32_t total_size) {
|
|||||||
return s_ota.target_slot;
|
return s_ota.target_slot;
|
||||||
}
|
}
|
||||||
|
|
||||||
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len) {
|
bool ota_uart_block_ready_for_reack(void) {
|
||||||
|
if (!s_ota.active) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return s_ota.written > 0 &&
|
||||||
|
(s_ota.written % OTA_UART_FLASH_BLOCK_SIZE) == 0 &&
|
||||||
|
s_ota.block_len == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data,
|
||||||
|
size_t len) {
|
||||||
if (!s_ota.active || data == NULL || len == 0) {
|
if (!s_ota.active || data == NULL || len == 0) {
|
||||||
return OTA_FEED_ERROR;
|
return OTA_FEED_ERROR;
|
||||||
}
|
}
|
||||||
|
if (seq < s_ota.expected_seq) {
|
||||||
|
return OTA_FEED_SEQ_DUP;
|
||||||
|
}
|
||||||
|
if (seq > s_ota.expected_seq) {
|
||||||
|
ESP_LOGW(TAG, "seq gap: got %lu expected %lu", (unsigned long)seq,
|
||||||
|
(unsigned long)s_ota.expected_seq);
|
||||||
|
return OTA_FEED_SEQ_GAP;
|
||||||
|
}
|
||||||
|
s_ota.expected_seq++;
|
||||||
|
|
||||||
if (len > OTA_UART_HOST_CHUNK_SIZE) {
|
if (len > OTA_UART_HOST_CHUNK_SIZE) {
|
||||||
ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len,
|
ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len,
|
||||||
OTA_UART_HOST_CHUNK_SIZE);
|
OTA_UART_HOST_CHUNK_SIZE);
|
||||||
@@ -200,6 +221,13 @@ esp_err_t ota_uart_finish(bool set_boot, bool *success_out) {
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (s_ota.total_size > 0 && s_ota.received != s_ota.total_size) {
|
||||||
|
ESP_LOGE(TAG, "size mismatch: received=%lu expected=%lu",
|
||||||
|
(unsigned long)s_ota.received, (unsigned long)s_ota.total_size);
|
||||||
|
ota_uart_abort();
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
err = esp_ota_end(s_ota.handle);
|
err = esp_ota_end(s_ota.handle);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
|
||||||
|
|||||||
+10
-2
@@ -28,6 +28,8 @@ typedef enum {
|
|||||||
typedef enum {
|
typedef enum {
|
||||||
OTA_FEED_OK = 0,
|
OTA_FEED_OK = 0,
|
||||||
OTA_FEED_BLOCK_WRITTEN,
|
OTA_FEED_BLOCK_WRITTEN,
|
||||||
|
OTA_FEED_SEQ_DUP,
|
||||||
|
OTA_FEED_SEQ_GAP,
|
||||||
OTA_FEED_ERROR,
|
OTA_FEED_ERROR,
|
||||||
} ota_feed_result_t;
|
} ota_feed_result_t;
|
||||||
|
|
||||||
@@ -41,8 +43,14 @@ int ota_uart_prepare(uint32_t total_size);
|
|||||||
|
|
||||||
void ota_uart_abort(void);
|
void ota_uart_abort(void);
|
||||||
|
|
||||||
/** Append up to 200 bytes; flushes 4 KiB blocks to flash when full. */
|
/**
|
||||||
ota_feed_result_t ota_uart_feed(const uint8_t *data, size_t len);
|
* Append up to 200 bytes with strict seq checking (0, 1, 2, …).
|
||||||
|
* Duplicates (seq < expected) return OTA_FEED_SEQ_DUP; gaps return OTA_FEED_SEQ_GAP.
|
||||||
|
*/
|
||||||
|
ota_feed_result_t ota_uart_feed_chunk(uint32_t seq, const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
/** True when a full 4 KiB block is in flash (used to re-ACK host block retries). */
|
||||||
|
bool ota_uart_block_ready_for_reack(void);
|
||||||
|
|
||||||
uint32_t ota_uart_bytes_written(void);
|
uint32_t ota_uart_bytes_written(void);
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
static const char *TAG = "[SETTINGS]";
|
static const char *TAG = "[SETTINGS]";
|
||||||
static const char *NS = "powerpod";
|
static const char *NS = "powerpod";
|
||||||
static const char *KEY_ACCEL_DZ = "accel_dz";
|
static const char *KEY_ACCEL_DZ = "accel_dz";
|
||||||
|
static const char *KEY_UV_LATCH = "uv_latch";
|
||||||
|
|
||||||
#define ACCEL_DEADZONE_MAX 4095u
|
#define ACCEL_DEADZONE_MAX 4095u
|
||||||
|
|
||||||
@@ -108,3 +109,53 @@ void pod_settings_apply_accel_deadzone(void) {
|
|||||||
bma456_set_accel_deadzone(deadzone);
|
bma456_set_accel_deadzone(deadzone);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool pod_settings_is_uv_latched(void) {
|
||||||
|
if (!s_nvs_ready) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_handle_t handle;
|
||||||
|
esp_err_t err = nvs_open(NS, NVS_READONLY, &handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t value = 0;
|
||||||
|
err = nvs_get_u8(handle, KEY_UV_LATCH, &value);
|
||||||
|
nvs_close(handle);
|
||||||
|
|
||||||
|
return err == ESP_OK && value != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t pod_settings_set_uv_latched(bool latched) {
|
||||||
|
esp_err_t err = ensure_nvs();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
nvs_handle_t handle;
|
||||||
|
err = nvs_open(NS, NVS_READWRITE, &handle);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = nvs_set_u8(handle, KEY_UV_LATCH, latched ? 1u : 0u);
|
||||||
|
if (err == ESP_OK) {
|
||||||
|
err = nvs_commit(handle);
|
||||||
|
}
|
||||||
|
nvs_close(handle);
|
||||||
|
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "save uv_latch failed: %s", esp_err_to_name(err));
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGI(TAG, "UV latch %s", latched ? "set" : "cleared");
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t pod_settings_clear_uv_latched(void) {
|
||||||
|
return pod_settings_set_uv_latched(false);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define POD_SETTINGS_H
|
#define POD_SETTINGS_H
|
||||||
|
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include <stdbool.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
/** Initialize NVS (idempotent) and log stored settings. Call once early in app_main. */
|
/** Initialize NVS (idempotent) and log stored settings. Call once early in app_main. */
|
||||||
@@ -16,4 +17,9 @@ uint32_t pod_settings_load_accel_deadzone(void);
|
|||||||
/** Apply NVS deadzone to BMA456 when the sensor is present. */
|
/** Apply NVS deadzone to BMA456 when the sensor is present. */
|
||||||
void pod_settings_apply_accel_deadzone(void);
|
void pod_settings_apply_accel_deadzone(void);
|
||||||
|
|
||||||
|
/** LiPo under-voltage latch persisted across reboot (software UV protection). */
|
||||||
|
bool pod_settings_is_uv_latched(void);
|
||||||
|
esp_err_t pod_settings_set_uv_latched(bool latched);
|
||||||
|
esp_err_t pod_settings_clear_uv_latched(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+26
-2
@@ -1,7 +1,11 @@
|
|||||||
#include "app_config.h"
|
#include "app_config.h"
|
||||||
#include "cmd_handler.h"
|
#include "cmd_handler.h"
|
||||||
#include "cmd_accel_deadzone.h"
|
#include "cmd_accel_deadzone.h"
|
||||||
|
#include "cmd_accel_stream.h"
|
||||||
|
#include "cmd_tap_notify.h"
|
||||||
|
#include "cmd_cache_status.h"
|
||||||
#include "cmd_espnow_unicast_test.h"
|
#include "cmd_espnow_unicast_test.h"
|
||||||
|
#include "cmd_espnow_echo_ping.h"
|
||||||
#include "cmd_espnow_find_me.h"
|
#include "cmd_espnow_find_me.h"
|
||||||
#include "cmd_restart.h"
|
#include "cmd_restart.h"
|
||||||
#include "cmd_client_info.h"
|
#include "cmd_client_info.h"
|
||||||
@@ -9,6 +13,8 @@
|
|||||||
#include "cmd_ota.h"
|
#include "cmd_ota.h"
|
||||||
#include "cmd_ota_slave_progress.h"
|
#include "cmd_ota_slave_progress.h"
|
||||||
#include "cmd_led_ring.h"
|
#include "cmd_led_ring.h"
|
||||||
|
#include "cmd_battery.h"
|
||||||
|
#include "cmd_set_log_level.h"
|
||||||
#include "esp_now_comm.h"
|
#include "esp_now_comm.h"
|
||||||
#include "powerpod.h"
|
#include "powerpod.h"
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
@@ -25,6 +31,7 @@
|
|||||||
#include "led_ring.h"
|
#include "led_ring.h"
|
||||||
#include "pod_settings.h"
|
#include "pod_settings.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
|
#include "battery_uv.h"
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
|
|
||||||
enum MASTER_STATES {
|
enum MASTER_STATES {
|
||||||
@@ -72,6 +79,16 @@ void app_main(void) {
|
|||||||
ESP_LOGW(TAG, "settings NVS init failed; using defaults");
|
ESP_LOGW(TAG, "settings NVS init failed; using defaults");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (board_input_init_adc_only() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "LiPo ADC init failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
#if POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
if (battery_uv_evaluate_boot()) {
|
||||||
|
battery_uv_run_mode();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
// Get Master Mode Pin
|
// Get Master Mode Pin
|
||||||
gpio_reset_pin(DIP_MASTER);
|
gpio_reset_pin(DIP_MASTER);
|
||||||
gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT);
|
gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT);
|
||||||
@@ -161,6 +178,9 @@ void app_main(void) {
|
|||||||
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
|
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
|
||||||
app_config.running_partition, ota_slot);
|
app_config.running_partition, ota_slot);
|
||||||
|
|
||||||
|
board_input_start_lipo_monitor();
|
||||||
|
board_input_init_button();
|
||||||
|
|
||||||
err = esp_now_comm_init(&app_config);
|
err = esp_now_comm_init(&app_config);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "ESP-NOW init failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "ESP-NOW init failed: %s", esp_err_to_name(err));
|
||||||
@@ -168,8 +188,6 @@ void app_main(void) {
|
|||||||
|
|
||||||
led_ring_init();
|
led_ring_init();
|
||||||
|
|
||||||
board_input_init();
|
|
||||||
|
|
||||||
if (app_config.master) {
|
if (app_config.master) {
|
||||||
cmd_queue = xQueueCreate(64, sizeof(generic_msg_t));
|
cmd_queue = xQueueCreate(64, sizeof(generic_msg_t));
|
||||||
init_cmdHandler(cmd_queue);
|
init_cmdHandler(cmd_queue);
|
||||||
@@ -177,12 +195,18 @@ void app_main(void) {
|
|||||||
cmd_version_register();
|
cmd_version_register();
|
||||||
cmd_client_info_register();
|
cmd_client_info_register();
|
||||||
cmd_accel_deadzone_register();
|
cmd_accel_deadzone_register();
|
||||||
|
cmd_accel_stream_register();
|
||||||
|
cmd_tap_notify_register();
|
||||||
|
cmd_cache_status_register();
|
||||||
cmd_espnow_unicast_test_register();
|
cmd_espnow_unicast_test_register();
|
||||||
|
cmd_espnow_echo_ping_register();
|
||||||
cmd_espnow_find_me_register();
|
cmd_espnow_find_me_register();
|
||||||
cmd_restart_register();
|
cmd_restart_register();
|
||||||
cmd_led_ring_register();
|
cmd_led_ring_register();
|
||||||
|
cmd_battery_register();
|
||||||
cmd_ota_register();
|
cmd_ota_register();
|
||||||
cmd_ota_slave_progress_register();
|
cmd_ota_slave_progress_register();
|
||||||
|
cmd_set_log_level_register();
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)");
|
ESP_LOGI(TAG, "LED ring: UART LED_RING commands only (no local demo loop)");
|
||||||
|
|||||||
+7
-3
@@ -1,6 +1,11 @@
|
|||||||
#ifndef POWERPOD_H
|
#ifndef POWERPOD_H
|
||||||
#define POWERPOD_H
|
#define POWERPOD_H
|
||||||
|
|
||||||
|
/** 1 = software LiPo UV latch + energy-save mode; 0 = disabled (LED mode 6 still works). */
|
||||||
|
#ifndef POWERPOD_BATTERY_UV_ENABLE
|
||||||
|
#define POWERPOD_BATTERY_UV_ENABLE 0
|
||||||
|
#endif
|
||||||
|
|
||||||
#define DIP_MASTER 4
|
#define DIP_MASTER 4
|
||||||
#define I2C_SCL 5
|
#define I2C_SCL 5
|
||||||
#define I2C_SDA 6
|
#define I2C_SDA 6
|
||||||
@@ -11,9 +16,8 @@
|
|||||||
/** Front-panel button (active low, internal pull-up). */
|
/** Front-panel button (active low, internal pull-up). */
|
||||||
#define TASTER_GPIO 12
|
#define TASTER_GPIO 12
|
||||||
|
|
||||||
/** LiPo voltage sense inputs (ADC1-capable GPIOs). */
|
/** LiPo voltage sense inputs (ADC-capable GPIOs; GPIO11 = ADC2 on ESP32-S3). */
|
||||||
#define V_LIPO_1_GPIO 1
|
#define V_LIPO_1_GPIO 1
|
||||||
/** Shares GPIO with TASTER on current bench wiring; second ADC is skipped in board_input.c. */
|
#define V_LIPO_2_GPIO 11
|
||||||
#define V_LIPO_2_GPIO 12
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
PB_BIND(alox_EspNowUnicastTest, alox_EspNowUnicastTest, AUTO)
|
PB_BIND(alox_EspNowUnicastTest, alox_EspNowUnicastTest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowEchoPing, alox_EspNowEchoPing, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowEchoPong, alox_EspNowEchoPong, AUTO)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_EspNowFindMe, alox_EspNowFindMe, AUTO)
|
PB_BIND(alox_EspNowFindMe, alox_EspNowFindMe, AUTO)
|
||||||
|
|
||||||
|
|
||||||
@@ -24,6 +30,27 @@ PB_BIND(alox_EspNowSlavePresence, alox_EspNowSlavePresence, AUTO)
|
|||||||
PB_BIND(alox_EspNowAccelDeadzone, alox_EspNowAccelDeadzone, AUTO)
|
PB_BIND(alox_EspNowAccelDeadzone, alox_EspNowAccelDeadzone, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowAccelStream, alox_EspNowAccelStream, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowAccelSample, alox_EspNowAccelSample, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowBatteryQuery, alox_EspNowBatteryQuery, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowTapNotify, alox_EspNowTapNotify, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowTapEvent, alox_EspNowTapEvent, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowBatteryReport, alox_EspNowBatteryReport, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowLedRing, alox_EspNowLedRing, AUTO)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_EspNowOtaStart, alox_EspNowOtaStart, AUTO)
|
PB_BIND(alox_EspNowOtaStart, alox_EspNowOtaStart, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,16 @@ typedef enum _alox_EspNowMessageType {
|
|||||||
alox_EspNowMessageType_ESPNOW_OTA_END = 8,
|
alox_EspNowMessageType_ESPNOW_OTA_END = 8,
|
||||||
alox_EspNowMessageType_ESPNOW_OTA_STATUS = 9,
|
alox_EspNowMessageType_ESPNOW_OTA_STATUS = 9,
|
||||||
alox_EspNowMessageType_ESPNOW_FIND_ME = 10,
|
alox_EspNowMessageType_ESPNOW_FIND_ME = 10,
|
||||||
alox_EspNowMessageType_ESPNOW_RESTART = 11
|
alox_EspNowMessageType_ESPNOW_RESTART = 11,
|
||||||
|
alox_EspNowMessageType_ESPNOW_ACCEL_SAMPLE = 12,
|
||||||
|
alox_EspNowMessageType_ESPNOW_SET_ACCEL_STREAM = 13,
|
||||||
|
alox_EspNowMessageType_ESPNOW_LED_RING = 14,
|
||||||
|
alox_EspNowMessageType_ESPNOW_BATTERY_QUERY = 15,
|
||||||
|
alox_EspNowMessageType_ESPNOW_BATTERY_REPORT = 16,
|
||||||
|
alox_EspNowMessageType_ESPNOW_SET_TAP_NOTIFY = 17,
|
||||||
|
alox_EspNowMessageType_ESPNOW_TAP_EVENT = 18,
|
||||||
|
alox_EspNowMessageType_ESPNOW_ECHO_PING = 19,
|
||||||
|
alox_EspNowMessageType_ESPNOW_ECHO_PONG = 20
|
||||||
} alox_EspNowMessageType;
|
} alox_EspNowMessageType;
|
||||||
|
|
||||||
/* Struct definitions */
|
/* Struct definitions */
|
||||||
@@ -30,6 +39,19 @@ typedef struct _alox_EspNowUnicastTest {
|
|||||||
uint32_t seq;
|
uint32_t seq;
|
||||||
} alox_EspNowUnicastTest;
|
} alox_EspNowUnicastTest;
|
||||||
|
|
||||||
|
/* * Master → slave: echo ping (host ts + master monotonic time for RTT). */
|
||||||
|
typedef struct _alox_EspNowEchoPing {
|
||||||
|
uint64_t host_timestamp_us;
|
||||||
|
/* * esp_timer_get_time() (µs since boot) when master forwarded this message. */
|
||||||
|
uint64_t master_time_us;
|
||||||
|
} alox_EspNowEchoPing;
|
||||||
|
|
||||||
|
/* * Slave → master: echo ping payload unchanged. */
|
||||||
|
typedef struct _alox_EspNowEchoPong {
|
||||||
|
uint64_t host_timestamp_us;
|
||||||
|
uint64_t master_time_us;
|
||||||
|
} alox_EspNowEchoPong;
|
||||||
|
|
||||||
/* * Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
|
/* * Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
|
||||||
typedef struct _alox_EspNowFindMe {
|
typedef struct _alox_EspNowFindMe {
|
||||||
/* * 0 = any slave; otherwise only slave_id must match */
|
/* * 0 = any slave; otherwise only slave_id must match */
|
||||||
@@ -43,6 +65,8 @@ typedef struct _alox_EspNowRestart {
|
|||||||
|
|
||||||
typedef struct _alox_EspNowDiscover {
|
typedef struct _alox_EspNowDiscover {
|
||||||
uint32_t network;
|
uint32_t network;
|
||||||
|
/* * Master is in an OTA session (UART upload or ESP-NOW distribution). */
|
||||||
|
bool master_ota_pending;
|
||||||
} alox_EspNowDiscover;
|
} alox_EspNowDiscover;
|
||||||
|
|
||||||
typedef struct _alox_EspNowSlavePresence {
|
typedef struct _alox_EspNowSlavePresence {
|
||||||
@@ -59,6 +83,63 @@ typedef struct _alox_EspNowAccelDeadzone {
|
|||||||
uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */
|
uint32_t client_id; /* 0 = all slaves; otherwise only matching slave_id applies */
|
||||||
} alox_EspNowAccelDeadzone;
|
} alox_EspNowAccelDeadzone;
|
||||||
|
|
||||||
|
/* * Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||||
|
typedef struct _alox_EspNowAccelStream {
|
||||||
|
bool enable;
|
||||||
|
uint32_t client_id;
|
||||||
|
} alox_EspNowAccelStream;
|
||||||
|
|
||||||
|
/* * Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||||
|
typedef struct _alox_EspNowAccelSample {
|
||||||
|
uint32_t slave_id;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
int32_t z;
|
||||||
|
} alox_EspNowAccelSample;
|
||||||
|
|
||||||
|
/* * Master → slave: on-demand LiPo read (optional; slaves also push every ~30 s). */
|
||||||
|
typedef struct _alox_EspNowBatteryQuery {
|
||||||
|
uint32_t client_id;
|
||||||
|
} alox_EspNowBatteryQuery;
|
||||||
|
|
||||||
|
/* * Master → slave: which tap kinds should be reported via ESP-NOW. */
|
||||||
|
typedef struct _alox_EspNowTapNotify {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool single;
|
||||||
|
bool double_tap;
|
||||||
|
bool triple;
|
||||||
|
} alox_EspNowTapNotify;
|
||||||
|
|
||||||
|
/* * Slave → master: tap detected on BMA456 (event, not periodic). */
|
||||||
|
typedef struct _alox_EspNowTapEvent {
|
||||||
|
uint32_t slave_id;
|
||||||
|
/* * 1=single, 2=double, 3=triple */
|
||||||
|
uint32_t kind;
|
||||||
|
} alox_EspNowTapEvent;
|
||||||
|
|
||||||
|
/* * Slave → master: LiPo voltages (periodic ~30 s and on query). */
|
||||||
|
typedef struct _alox_EspNowBatteryReport {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool lipo1_valid;
|
||||||
|
bool lipo2_valid;
|
||||||
|
uint32_t lipo1_mv;
|
||||||
|
uint32_t lipo2_mv;
|
||||||
|
} alox_EspNowBatteryReport;
|
||||||
|
|
||||||
|
/* * Master → slave: LED ring command (same modes as UART LedRingProgressRequest). */
|
||||||
|
typedef struct _alox_EspNowLedRing {
|
||||||
|
uint32_t client_id;
|
||||||
|
uint32_t mode;
|
||||||
|
uint32_t progress;
|
||||||
|
uint32_t digit;
|
||||||
|
uint32_t r;
|
||||||
|
uint32_t g;
|
||||||
|
uint32_t b;
|
||||||
|
uint32_t intensity;
|
||||||
|
uint32_t blink_ms;
|
||||||
|
uint32_t blink_count;
|
||||||
|
} alox_EspNowLedRing;
|
||||||
|
|
||||||
/* Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). */
|
/* Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS). */
|
||||||
typedef struct _alox_EspNowOtaStart {
|
typedef struct _alox_EspNowOtaStart {
|
||||||
uint32_t total_size;
|
uint32_t total_size;
|
||||||
@@ -98,6 +179,15 @@ typedef struct _alox_EspNowMessage {
|
|||||||
alox_EspNowOtaStatus ota_status;
|
alox_EspNowOtaStatus ota_status;
|
||||||
alox_EspNowFindMe find_me;
|
alox_EspNowFindMe find_me;
|
||||||
alox_EspNowRestart restart;
|
alox_EspNowRestart restart;
|
||||||
|
alox_EspNowAccelSample accel_sample;
|
||||||
|
alox_EspNowAccelStream accel_stream;
|
||||||
|
alox_EspNowLedRing led_ring;
|
||||||
|
alox_EspNowBatteryQuery battery_query;
|
||||||
|
alox_EspNowBatteryReport battery_report;
|
||||||
|
alox_EspNowTapNotify tap_notify;
|
||||||
|
alox_EspNowTapEvent tap_event;
|
||||||
|
alox_EspNowEchoPing echo_ping;
|
||||||
|
alox_EspNowEchoPong echo_pong;
|
||||||
} payload;
|
} payload;
|
||||||
} alox_EspNowMessage;
|
} alox_EspNowMessage;
|
||||||
|
|
||||||
@@ -108,8 +198,17 @@ extern "C" {
|
|||||||
|
|
||||||
/* Helper constants for enums */
|
/* Helper constants for enums */
|
||||||
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
|
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
|
||||||
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_RESTART
|
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_ECHO_PONG
|
||||||
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_RESTART+1))
|
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_ECHO_PONG+1))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -126,22 +225,40 @@ extern "C" {
|
|||||||
|
|
||||||
/* Initializer values for message structs */
|
/* Initializer values for message structs */
|
||||||
#define alox_EspNowUnicastTest_init_default {0}
|
#define alox_EspNowUnicastTest_init_default {0}
|
||||||
|
#define alox_EspNowEchoPing_init_default {0, 0}
|
||||||
|
#define alox_EspNowEchoPong_init_default {0, 0}
|
||||||
#define alox_EspNowFindMe_init_default {0}
|
#define alox_EspNowFindMe_init_default {0}
|
||||||
#define alox_EspNowRestart_init_default {0}
|
#define alox_EspNowRestart_init_default {0}
|
||||||
#define alox_EspNowDiscover_init_default {0}
|
#define alox_EspNowDiscover_init_default {0, 0}
|
||||||
#define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0}
|
#define alox_EspNowSlavePresence_init_default {0, {{NULL}, NULL}, 0, 0, 0, 0}
|
||||||
#define alox_EspNowAccelDeadzone_init_default {0, 0}
|
#define alox_EspNowAccelDeadzone_init_default {0, 0}
|
||||||
|
#define alox_EspNowAccelStream_init_default {0, 0}
|
||||||
|
#define alox_EspNowAccelSample_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_EspNowBatteryQuery_init_default {0}
|
||||||
|
#define alox_EspNowTapNotify_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_EspNowTapEvent_init_default {0, 0}
|
||||||
|
#define alox_EspNowBatteryReport_init_default {0, 0, 0, 0, 0}
|
||||||
|
#define alox_EspNowLedRing_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define alox_EspNowOtaStart_init_default {0}
|
#define alox_EspNowOtaStart_init_default {0}
|
||||||
#define alox_EspNowOtaPayload_init_default {0, {0, {0}}}
|
#define alox_EspNowOtaPayload_init_default {0, {0, {0}}}
|
||||||
#define alox_EspNowOtaEnd_init_default {0}
|
#define alox_EspNowOtaEnd_init_default {0}
|
||||||
#define alox_EspNowOtaStatus_init_default {0, 0, 0}
|
#define alox_EspNowOtaStatus_init_default {0, 0, 0}
|
||||||
#define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}}
|
#define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}}
|
||||||
#define alox_EspNowUnicastTest_init_zero {0}
|
#define alox_EspNowUnicastTest_init_zero {0}
|
||||||
|
#define alox_EspNowEchoPing_init_zero {0, 0}
|
||||||
|
#define alox_EspNowEchoPong_init_zero {0, 0}
|
||||||
#define alox_EspNowFindMe_init_zero {0}
|
#define alox_EspNowFindMe_init_zero {0}
|
||||||
#define alox_EspNowRestart_init_zero {0}
|
#define alox_EspNowRestart_init_zero {0}
|
||||||
#define alox_EspNowDiscover_init_zero {0}
|
#define alox_EspNowDiscover_init_zero {0, 0}
|
||||||
#define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0}
|
#define alox_EspNowSlavePresence_init_zero {0, {{NULL}, NULL}, 0, 0, 0, 0}
|
||||||
#define alox_EspNowAccelDeadzone_init_zero {0, 0}
|
#define alox_EspNowAccelDeadzone_init_zero {0, 0}
|
||||||
|
#define alox_EspNowAccelStream_init_zero {0, 0}
|
||||||
|
#define alox_EspNowAccelSample_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_EspNowBatteryQuery_init_zero {0}
|
||||||
|
#define alox_EspNowTapNotify_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_EspNowTapEvent_init_zero {0, 0}
|
||||||
|
#define alox_EspNowBatteryReport_init_zero {0, 0, 0, 0, 0}
|
||||||
|
#define alox_EspNowLedRing_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define alox_EspNowOtaStart_init_zero {0}
|
#define alox_EspNowOtaStart_init_zero {0}
|
||||||
#define alox_EspNowOtaPayload_init_zero {0, {0, {0}}}
|
#define alox_EspNowOtaPayload_init_zero {0, {0, {0}}}
|
||||||
#define alox_EspNowOtaEnd_init_zero {0}
|
#define alox_EspNowOtaEnd_init_zero {0}
|
||||||
@@ -150,9 +267,14 @@ extern "C" {
|
|||||||
|
|
||||||
/* Field tags (for use in manual encoding/decoding) */
|
/* Field tags (for use in manual encoding/decoding) */
|
||||||
#define alox_EspNowUnicastTest_seq_tag 1
|
#define alox_EspNowUnicastTest_seq_tag 1
|
||||||
|
#define alox_EspNowEchoPing_host_timestamp_us_tag 1
|
||||||
|
#define alox_EspNowEchoPing_master_time_us_tag 2
|
||||||
|
#define alox_EspNowEchoPong_host_timestamp_us_tag 1
|
||||||
|
#define alox_EspNowEchoPong_master_time_us_tag 2
|
||||||
#define alox_EspNowFindMe_client_id_tag 1
|
#define alox_EspNowFindMe_client_id_tag 1
|
||||||
#define alox_EspNowRestart_client_id_tag 1
|
#define alox_EspNowRestart_client_id_tag 1
|
||||||
#define alox_EspNowDiscover_network_tag 1
|
#define alox_EspNowDiscover_network_tag 1
|
||||||
|
#define alox_EspNowDiscover_master_ota_pending_tag 2
|
||||||
#define alox_EspNowSlavePresence_network_tag 1
|
#define alox_EspNowSlavePresence_network_tag 1
|
||||||
#define alox_EspNowSlavePresence_mac_tag 2
|
#define alox_EspNowSlavePresence_mac_tag 2
|
||||||
#define alox_EspNowSlavePresence_version_tag 3
|
#define alox_EspNowSlavePresence_version_tag 3
|
||||||
@@ -161,6 +283,34 @@ extern "C" {
|
|||||||
#define alox_EspNowSlavePresence_used_tag 6
|
#define alox_EspNowSlavePresence_used_tag 6
|
||||||
#define alox_EspNowAccelDeadzone_deadzone_tag 1
|
#define alox_EspNowAccelDeadzone_deadzone_tag 1
|
||||||
#define alox_EspNowAccelDeadzone_client_id_tag 2
|
#define alox_EspNowAccelDeadzone_client_id_tag 2
|
||||||
|
#define alox_EspNowAccelStream_enable_tag 1
|
||||||
|
#define alox_EspNowAccelStream_client_id_tag 2
|
||||||
|
#define alox_EspNowAccelSample_slave_id_tag 1
|
||||||
|
#define alox_EspNowAccelSample_x_tag 2
|
||||||
|
#define alox_EspNowAccelSample_y_tag 3
|
||||||
|
#define alox_EspNowAccelSample_z_tag 4
|
||||||
|
#define alox_EspNowBatteryQuery_client_id_tag 1
|
||||||
|
#define alox_EspNowTapNotify_client_id_tag 1
|
||||||
|
#define alox_EspNowTapNotify_single_tag 2
|
||||||
|
#define alox_EspNowTapNotify_double_tap_tag 3
|
||||||
|
#define alox_EspNowTapNotify_triple_tag 4
|
||||||
|
#define alox_EspNowTapEvent_slave_id_tag 1
|
||||||
|
#define alox_EspNowTapEvent_kind_tag 2
|
||||||
|
#define alox_EspNowBatteryReport_client_id_tag 1
|
||||||
|
#define alox_EspNowBatteryReport_lipo1_valid_tag 2
|
||||||
|
#define alox_EspNowBatteryReport_lipo2_valid_tag 3
|
||||||
|
#define alox_EspNowBatteryReport_lipo1_mv_tag 4
|
||||||
|
#define alox_EspNowBatteryReport_lipo2_mv_tag 5
|
||||||
|
#define alox_EspNowLedRing_client_id_tag 1
|
||||||
|
#define alox_EspNowLedRing_mode_tag 2
|
||||||
|
#define alox_EspNowLedRing_progress_tag 3
|
||||||
|
#define alox_EspNowLedRing_digit_tag 4
|
||||||
|
#define alox_EspNowLedRing_r_tag 5
|
||||||
|
#define alox_EspNowLedRing_g_tag 6
|
||||||
|
#define alox_EspNowLedRing_b_tag 7
|
||||||
|
#define alox_EspNowLedRing_intensity_tag 8
|
||||||
|
#define alox_EspNowLedRing_blink_ms_tag 9
|
||||||
|
#define alox_EspNowLedRing_blink_count_tag 10
|
||||||
#define alox_EspNowOtaStart_total_size_tag 1
|
#define alox_EspNowOtaStart_total_size_tag 1
|
||||||
#define alox_EspNowOtaPayload_seq_tag 1
|
#define alox_EspNowOtaPayload_seq_tag 1
|
||||||
#define alox_EspNowOtaPayload_data_tag 2
|
#define alox_EspNowOtaPayload_data_tag 2
|
||||||
@@ -179,6 +329,15 @@ extern "C" {
|
|||||||
#define alox_EspNowMessage_ota_status_tag 10
|
#define alox_EspNowMessage_ota_status_tag 10
|
||||||
#define alox_EspNowMessage_find_me_tag 11
|
#define alox_EspNowMessage_find_me_tag 11
|
||||||
#define alox_EspNowMessage_restart_tag 12
|
#define alox_EspNowMessage_restart_tag 12
|
||||||
|
#define alox_EspNowMessage_accel_sample_tag 13
|
||||||
|
#define alox_EspNowMessage_accel_stream_tag 14
|
||||||
|
#define alox_EspNowMessage_led_ring_tag 15
|
||||||
|
#define alox_EspNowMessage_battery_query_tag 16
|
||||||
|
#define alox_EspNowMessage_battery_report_tag 17
|
||||||
|
#define alox_EspNowMessage_tap_notify_tag 18
|
||||||
|
#define alox_EspNowMessage_tap_event_tag 19
|
||||||
|
#define alox_EspNowMessage_echo_ping_tag 20
|
||||||
|
#define alox_EspNowMessage_echo_pong_tag 21
|
||||||
|
|
||||||
/* Struct field encoding specification for nanopb */
|
/* Struct field encoding specification for nanopb */
|
||||||
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
|
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
|
||||||
@@ -186,6 +345,18 @@ X(a, STATIC, SINGULAR, UINT32, seq, 1)
|
|||||||
#define alox_EspNowUnicastTest_CALLBACK NULL
|
#define alox_EspNowUnicastTest_CALLBACK NULL
|
||||||
#define alox_EspNowUnicastTest_DEFAULT NULL
|
#define alox_EspNowUnicastTest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowEchoPing_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, host_timestamp_us, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, master_time_us, 2)
|
||||||
|
#define alox_EspNowEchoPing_CALLBACK NULL
|
||||||
|
#define alox_EspNowEchoPing_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowEchoPong_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, host_timestamp_us, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, master_time_us, 2)
|
||||||
|
#define alox_EspNowEchoPong_CALLBACK NULL
|
||||||
|
#define alox_EspNowEchoPong_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_EspNowFindMe_FIELDLIST(X, a) \
|
#define alox_EspNowFindMe_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
|
||||||
#define alox_EspNowFindMe_CALLBACK NULL
|
#define alox_EspNowFindMe_CALLBACK NULL
|
||||||
@@ -197,7 +368,8 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 1)
|
|||||||
#define alox_EspNowRestart_DEFAULT NULL
|
#define alox_EspNowRestart_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_EspNowDiscover_FIELDLIST(X, a) \
|
#define alox_EspNowDiscover_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, network, 1)
|
X(a, STATIC, SINGULAR, UINT32, network, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, master_ota_pending, 2)
|
||||||
#define alox_EspNowDiscover_CALLBACK NULL
|
#define alox_EspNowDiscover_CALLBACK NULL
|
||||||
#define alox_EspNowDiscover_DEFAULT NULL
|
#define alox_EspNowDiscover_DEFAULT NULL
|
||||||
|
|
||||||
@@ -217,6 +389,62 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2)
|
|||||||
#define alox_EspNowAccelDeadzone_CALLBACK NULL
|
#define alox_EspNowAccelDeadzone_CALLBACK NULL
|
||||||
#define alox_EspNowAccelDeadzone_DEFAULT NULL
|
#define alox_EspNowAccelDeadzone_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowAccelStream_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, enable, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 2)
|
||||||
|
#define alox_EspNowAccelStream_CALLBACK NULL
|
||||||
|
#define alox_EspNowAccelStream_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowAccelSample_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, slave_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, x, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, y, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, z, 4)
|
||||||
|
#define alox_EspNowAccelSample_CALLBACK NULL
|
||||||
|
#define alox_EspNowAccelSample_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowBatteryQuery_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
|
||||||
|
#define alox_EspNowBatteryQuery_CALLBACK NULL
|
||||||
|
#define alox_EspNowBatteryQuery_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowTapNotify_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, single, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, double_tap, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, triple, 4)
|
||||||
|
#define alox_EspNowTapNotify_CALLBACK NULL
|
||||||
|
#define alox_EspNowTapNotify_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowTapEvent_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, slave_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, kind, 2)
|
||||||
|
#define alox_EspNowTapEvent_CALLBACK NULL
|
||||||
|
#define alox_EspNowTapEvent_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowBatteryReport_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, lipo1_valid, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, lipo2_valid, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, lipo1_mv, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, lipo2_mv, 5)
|
||||||
|
#define alox_EspNowBatteryReport_CALLBACK NULL
|
||||||
|
#define alox_EspNowBatteryReport_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowLedRing_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, digit, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, r, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, g, 6) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, b, 7) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, intensity, 8) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, blink_ms, 9) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, blink_count, 10)
|
||||||
|
#define alox_EspNowLedRing_CALLBACK NULL
|
||||||
|
#define alox_EspNowLedRing_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_EspNowOtaStart_FIELDLIST(X, a) \
|
#define alox_EspNowOtaStart_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
|
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
|
||||||
#define alox_EspNowOtaStart_CALLBACK NULL
|
#define alox_EspNowOtaStart_CALLBACK NULL
|
||||||
@@ -252,7 +480,16 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,ota_payload,payload.ota_payload),
|
|||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_end,payload.ota_end), 9) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_end,payload.ota_end), 9) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,ota_status,payload.ota_status), 10) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,find_me,payload.find_me), 11) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,find_me,payload.find_me), 11) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,restart,payload.restart), 12)
|
X(a, STATIC, ONEOF, MESSAGE, (payload,restart,payload.restart), 12) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_sample,payload.accel_sample), 13) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream,payload.accel_stream), 14) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_query,payload.battery_query), 16) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_report,payload.battery_report), 17) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify,payload.tap_notify), 18) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,echo_ping,payload.echo_ping), 20) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,echo_pong,payload.echo_pong), 21)
|
||||||
#define alox_EspNowMessage_CALLBACK NULL
|
#define alox_EspNowMessage_CALLBACK NULL
|
||||||
#define alox_EspNowMessage_DEFAULT NULL
|
#define alox_EspNowMessage_DEFAULT NULL
|
||||||
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
|
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
|
||||||
@@ -266,13 +503,31 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,restart,payload.restart), 12)
|
|||||||
#define alox_EspNowMessage_payload_ota_status_MSGTYPE alox_EspNowOtaStatus
|
#define alox_EspNowMessage_payload_ota_status_MSGTYPE alox_EspNowOtaStatus
|
||||||
#define alox_EspNowMessage_payload_find_me_MSGTYPE alox_EspNowFindMe
|
#define alox_EspNowMessage_payload_find_me_MSGTYPE alox_EspNowFindMe
|
||||||
#define alox_EspNowMessage_payload_restart_MSGTYPE alox_EspNowRestart
|
#define alox_EspNowMessage_payload_restart_MSGTYPE alox_EspNowRestart
|
||||||
|
#define alox_EspNowMessage_payload_accel_sample_MSGTYPE alox_EspNowAccelSample
|
||||||
|
#define alox_EspNowMessage_payload_accel_stream_MSGTYPE alox_EspNowAccelStream
|
||||||
|
#define alox_EspNowMessage_payload_led_ring_MSGTYPE alox_EspNowLedRing
|
||||||
|
#define alox_EspNowMessage_payload_battery_query_MSGTYPE alox_EspNowBatteryQuery
|
||||||
|
#define alox_EspNowMessage_payload_battery_report_MSGTYPE alox_EspNowBatteryReport
|
||||||
|
#define alox_EspNowMessage_payload_tap_notify_MSGTYPE alox_EspNowTapNotify
|
||||||
|
#define alox_EspNowMessage_payload_tap_event_MSGTYPE alox_EspNowTapEvent
|
||||||
|
#define alox_EspNowMessage_payload_echo_ping_MSGTYPE alox_EspNowEchoPing
|
||||||
|
#define alox_EspNowMessage_payload_echo_pong_MSGTYPE alox_EspNowEchoPong
|
||||||
|
|
||||||
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
|
extern const pb_msgdesc_t alox_EspNowUnicastTest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowEchoPing_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowEchoPong_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
|
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowRestart_msg;
|
extern const pb_msgdesc_t alox_EspNowRestart_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowDiscover_msg;
|
extern const pb_msgdesc_t alox_EspNowDiscover_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowSlavePresence_msg;
|
extern const pb_msgdesc_t alox_EspNowSlavePresence_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg;
|
extern const pb_msgdesc_t alox_EspNowAccelDeadzone_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowAccelStream_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowAccelSample_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowBatteryQuery_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowTapNotify_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowTapEvent_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowBatteryReport_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowLedRing_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
|
extern const pb_msgdesc_t alox_EspNowOtaStart_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowOtaPayload_msg;
|
extern const pb_msgdesc_t alox_EspNowOtaPayload_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowOtaEnd_msg;
|
extern const pb_msgdesc_t alox_EspNowOtaEnd_msg;
|
||||||
@@ -281,11 +536,20 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
|
|||||||
|
|
||||||
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
|
||||||
#define alox_EspNowUnicastTest_fields &alox_EspNowUnicastTest_msg
|
#define alox_EspNowUnicastTest_fields &alox_EspNowUnicastTest_msg
|
||||||
|
#define alox_EspNowEchoPing_fields &alox_EspNowEchoPing_msg
|
||||||
|
#define alox_EspNowEchoPong_fields &alox_EspNowEchoPong_msg
|
||||||
#define alox_EspNowFindMe_fields &alox_EspNowFindMe_msg
|
#define alox_EspNowFindMe_fields &alox_EspNowFindMe_msg
|
||||||
#define alox_EspNowRestart_fields &alox_EspNowRestart_msg
|
#define alox_EspNowRestart_fields &alox_EspNowRestart_msg
|
||||||
#define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg
|
#define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg
|
||||||
#define alox_EspNowSlavePresence_fields &alox_EspNowSlavePresence_msg
|
#define alox_EspNowSlavePresence_fields &alox_EspNowSlavePresence_msg
|
||||||
#define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg
|
#define alox_EspNowAccelDeadzone_fields &alox_EspNowAccelDeadzone_msg
|
||||||
|
#define alox_EspNowAccelStream_fields &alox_EspNowAccelStream_msg
|
||||||
|
#define alox_EspNowAccelSample_fields &alox_EspNowAccelSample_msg
|
||||||
|
#define alox_EspNowBatteryQuery_fields &alox_EspNowBatteryQuery_msg
|
||||||
|
#define alox_EspNowTapNotify_fields &alox_EspNowTapNotify_msg
|
||||||
|
#define alox_EspNowTapEvent_fields &alox_EspNowTapEvent_msg
|
||||||
|
#define alox_EspNowBatteryReport_fields &alox_EspNowBatteryReport_msg
|
||||||
|
#define alox_EspNowLedRing_fields &alox_EspNowLedRing_msg
|
||||||
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
|
#define alox_EspNowOtaStart_fields &alox_EspNowOtaStart_msg
|
||||||
#define alox_EspNowOtaPayload_fields &alox_EspNowOtaPayload_msg
|
#define alox_EspNowOtaPayload_fields &alox_EspNowOtaPayload_msg
|
||||||
#define alox_EspNowOtaEnd_fields &alox_EspNowOtaEnd_msg
|
#define alox_EspNowOtaEnd_fields &alox_EspNowOtaEnd_msg
|
||||||
@@ -297,13 +561,22 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
|
|||||||
/* alox_EspNowMessage_size depends on runtime parameters */
|
/* alox_EspNowMessage_size depends on runtime parameters */
|
||||||
#define ALOX_ESP_NOW_MESSAGES_PB_H_MAX_SIZE alox_EspNowOtaPayload_size
|
#define ALOX_ESP_NOW_MESSAGES_PB_H_MAX_SIZE alox_EspNowOtaPayload_size
|
||||||
#define alox_EspNowAccelDeadzone_size 12
|
#define alox_EspNowAccelDeadzone_size 12
|
||||||
#define alox_EspNowDiscover_size 6
|
#define alox_EspNowAccelSample_size 24
|
||||||
|
#define alox_EspNowAccelStream_size 8
|
||||||
|
#define alox_EspNowBatteryQuery_size 6
|
||||||
|
#define alox_EspNowBatteryReport_size 22
|
||||||
|
#define alox_EspNowDiscover_size 8
|
||||||
|
#define alox_EspNowEchoPing_size 22
|
||||||
|
#define alox_EspNowEchoPong_size 22
|
||||||
#define alox_EspNowFindMe_size 6
|
#define alox_EspNowFindMe_size 6
|
||||||
|
#define alox_EspNowLedRing_size 60
|
||||||
#define alox_EspNowOtaEnd_size 0
|
#define alox_EspNowOtaEnd_size 0
|
||||||
#define alox_EspNowOtaPayload_size 209
|
#define alox_EspNowOtaPayload_size 209
|
||||||
#define alox_EspNowOtaStart_size 6
|
#define alox_EspNowOtaStart_size 6
|
||||||
#define alox_EspNowOtaStatus_size 18
|
#define alox_EspNowOtaStatus_size 18
|
||||||
#define alox_EspNowRestart_size 6
|
#define alox_EspNowRestart_size 6
|
||||||
|
#define alox_EspNowTapEvent_size 12
|
||||||
|
#define alox_EspNowTapNotify_size 12
|
||||||
#define alox_EspNowUnicastTest_size 6
|
#define alox_EspNowUnicastTest_size 6
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|||||||
@@ -17,12 +17,34 @@ enum EspNowMessageType {
|
|||||||
ESPNOW_OTA_STATUS = 9;
|
ESPNOW_OTA_STATUS = 9;
|
||||||
ESPNOW_FIND_ME = 10;
|
ESPNOW_FIND_ME = 10;
|
||||||
ESPNOW_RESTART = 11;
|
ESPNOW_RESTART = 11;
|
||||||
|
ESPNOW_ACCEL_SAMPLE = 12;
|
||||||
|
ESPNOW_SET_ACCEL_STREAM = 13;
|
||||||
|
ESPNOW_LED_RING = 14;
|
||||||
|
ESPNOW_BATTERY_QUERY = 15;
|
||||||
|
ESPNOW_BATTERY_REPORT = 16;
|
||||||
|
ESPNOW_SET_TAP_NOTIFY = 17;
|
||||||
|
ESPNOW_TAP_EVENT = 18;
|
||||||
|
ESPNOW_ECHO_PING = 19;
|
||||||
|
ESPNOW_ECHO_PONG = 20;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EspNowUnicastTest {
|
message EspNowUnicastTest {
|
||||||
uint32 seq = 1;
|
uint32 seq = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Master → slave: echo ping (host ts + master monotonic time for RTT). */
|
||||||
|
message EspNowEchoPing {
|
||||||
|
uint64 host_timestamp_us = 1;
|
||||||
|
/** esp_timer_get_time() (µs since boot) when master forwarded this message. */
|
||||||
|
uint64 master_time_us = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slave → master: echo ping payload unchanged. */
|
||||||
|
message EspNowEchoPong {
|
||||||
|
uint64 host_timestamp_us = 1;
|
||||||
|
uint64 master_time_us = 2;
|
||||||
|
}
|
||||||
|
|
||||||
/** Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
|
/** Master → slave: locate pod (LED ring R/G/B ×3 @ full brightness). */
|
||||||
message EspNowFindMe {
|
message EspNowFindMe {
|
||||||
/** 0 = any slave; otherwise only slave_id must match */
|
/** 0 = any slave; otherwise only slave_id must match */
|
||||||
@@ -36,6 +58,8 @@ message EspNowRestart {
|
|||||||
|
|
||||||
message EspNowDiscover {
|
message EspNowDiscover {
|
||||||
uint32 network = 1;
|
uint32 network = 1;
|
||||||
|
/** Master is in an OTA session (UART upload or ESP-NOW distribution). */
|
||||||
|
bool master_ota_pending = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message EspNowSlavePresence {
|
message EspNowSlavePresence {
|
||||||
@@ -52,6 +76,63 @@ message EspNowAccelDeadzone {
|
|||||||
uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies
|
uint32 client_id = 2; // 0 = all slaves; otherwise only matching slave_id applies
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Master → slave: enable/disable periodic accel ESP-NOW stream (~16 ms). */
|
||||||
|
message EspNowAccelStream {
|
||||||
|
bool enable = 1;
|
||||||
|
uint32 client_id = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slave → master: latest BMA456 sample (sent ~every 16 ms). */
|
||||||
|
message EspNowAccelSample {
|
||||||
|
uint32 slave_id = 1;
|
||||||
|
sint32 x = 2;
|
||||||
|
sint32 y = 3;
|
||||||
|
sint32 z = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Master → slave: on-demand LiPo read (optional; slaves also push every ~30 s). */
|
||||||
|
message EspNowBatteryQuery {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Master → slave: which tap kinds should be reported via ESP-NOW. */
|
||||||
|
message EspNowTapNotify {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
bool single = 2;
|
||||||
|
bool double_tap = 3;
|
||||||
|
bool triple = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slave → master: tap detected on BMA456 (event, not periodic). */
|
||||||
|
message EspNowTapEvent {
|
||||||
|
uint32 slave_id = 1;
|
||||||
|
/** 1=single, 2=double, 3=triple */
|
||||||
|
uint32 kind = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slave → master: LiPo voltages (periodic ~30 s and on query). */
|
||||||
|
message EspNowBatteryReport {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
bool lipo1_valid = 2;
|
||||||
|
bool lipo2_valid = 3;
|
||||||
|
uint32 lipo1_mv = 4;
|
||||||
|
uint32 lipo2_mv = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Master → slave: LED ring command (same modes as UART LedRingProgressRequest). */
|
||||||
|
message EspNowLedRing {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
uint32 mode = 2;
|
||||||
|
uint32 progress = 3;
|
||||||
|
uint32 digit = 4;
|
||||||
|
uint32 r = 5;
|
||||||
|
uint32 g = 6;
|
||||||
|
uint32 b = 7;
|
||||||
|
uint32 intensity = 8;
|
||||||
|
uint32 blink_ms = 9;
|
||||||
|
uint32 blink_count = 10;
|
||||||
|
}
|
||||||
|
|
||||||
// Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS).
|
// Master → slave: begin OTA (erase inactive slot; slave replies ESPNOW_OTA_STATUS).
|
||||||
message EspNowOtaStart {
|
message EspNowOtaStart {
|
||||||
uint32 total_size = 1;
|
uint32 total_size = 1;
|
||||||
@@ -87,5 +168,14 @@ message EspNowMessage {
|
|||||||
EspNowOtaStatus ota_status = 10;
|
EspNowOtaStatus ota_status = 10;
|
||||||
EspNowFindMe find_me = 11;
|
EspNowFindMe find_me = 11;
|
||||||
EspNowRestart restart = 12;
|
EspNowRestart restart = 12;
|
||||||
|
EspNowAccelSample accel_sample = 13;
|
||||||
|
EspNowAccelStream accel_stream = 14;
|
||||||
|
EspNowLedRing led_ring = 15;
|
||||||
|
EspNowBatteryQuery battery_query = 16;
|
||||||
|
EspNowBatteryReport battery_report = 17;
|
||||||
|
EspNowTapNotify tap_notify = 18;
|
||||||
|
EspNowTapEvent tap_event = 19;
|
||||||
|
EspNowEchoPing echo_ping = 20;
|
||||||
|
EspNowEchoPong echo_pong = 21;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,63 @@ PB_BIND(alox_AccelDeadzoneRequest, alox_AccelDeadzoneRequest, AUTO)
|
|||||||
PB_BIND(alox_AccelDeadzoneResponse, alox_AccelDeadzoneResponse, AUTO)
|
PB_BIND(alox_AccelDeadzoneResponse, alox_AccelDeadzoneResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_AccelStreamRequest, alox_AccelStreamRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_AccelStreamResponse, alox_AccelStreamResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_BatteryStatusRequest, alox_BatteryStatusRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_LipoReading, alox_LipoReading, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_BatterySample, alox_BatterySample, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_BatteryStatusResponse, alox_BatteryStatusResponse, 2)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_AccelSample, alox_AccelSample, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_TapNotifyRequest, alox_TapNotifyRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_TapNotifyResponse, alox_TapNotifyResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_TapEvent, alox_TapEvent, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_CacheStatusRequest, alox_CacheStatusRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_CacheClientAccel, alox_CacheClientAccel, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_CacheClientTap, alox_CacheClientTap, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_CacheClientStatus, alox_CacheClientStatus, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_CacheStatusResponse, alox_CacheStatusResponse, 2)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
|
PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_EspNowUnicastTestResponse, alox_EspNowUnicastTestResponse, AUTO)
|
PB_BIND(alox_EspNowUnicastTestResponse, alox_EspNowUnicastTestResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowEchoPingRequest, alox_EspNowEchoPingRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_EspNowEchoPingResponse, alox_EspNowEchoPingResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_LedRingProgressRequest, alox_LedRingProgressRequest, AUTO)
|
PB_BIND(alox_LedRingProgressRequest, alox_LedRingProgressRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
@@ -60,6 +111,12 @@ PB_BIND(alox_RestartRequest, alox_RestartRequest, AUTO)
|
|||||||
PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO)
|
PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_SetLogLevelRequest, alox_SetLogLevelRequest, AUTO)
|
||||||
|
|
||||||
|
|
||||||
|
PB_BIND(alox_SetLogLevelResponse, alox_SetLogLevelResponse, AUTO)
|
||||||
|
|
||||||
|
|
||||||
PB_BIND(alox_OtaStartPayload, alox_OtaStartPayload, AUTO)
|
PB_BIND(alox_OtaStartPayload, alox_OtaStartPayload, AUTO)
|
||||||
|
|
||||||
|
|
||||||
@@ -84,3 +141,5 @@ PB_BIND(alox_OtaSlaveProgressResponse, alox_OtaSlaveProgressResponse, 2)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+586
-18
@@ -27,9 +27,25 @@ typedef enum _alox_MessageType {
|
|||||||
alox_MessageType_OTA_START_ESPNOW = 20,
|
alox_MessageType_OTA_START_ESPNOW = 20,
|
||||||
alox_MessageType_OTA_SLAVE_PROGRESS = 21,
|
alox_MessageType_OTA_SLAVE_PROGRESS = 21,
|
||||||
alox_MessageType_FIND_ME = 22,
|
alox_MessageType_FIND_ME = 22,
|
||||||
alox_MessageType_RESTART = 23
|
alox_MessageType_RESTART = 23,
|
||||||
|
alox_MessageType_ACCEL_STREAM = 25,
|
||||||
|
alox_MessageType_BATTERY_STATUS = 26,
|
||||||
|
alox_MessageType_TAP_NOTIFY = 27,
|
||||||
|
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||||
|
alox_MessageType_CACHE_STATUS = 29,
|
||||||
|
/* * Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||||
|
alox_MessageType_ESPNOW_ECHO_PING = 30,
|
||||||
|
/* * Host → master: get/set ESP-IDF log level for tag "*" (global). */
|
||||||
|
alox_MessageType_SET_LOG_LEVEL = 31
|
||||||
} alox_MessageType;
|
} alox_MessageType;
|
||||||
|
|
||||||
|
typedef enum _alox_TapKind {
|
||||||
|
alox_TapKind_TAP_NONE = 0,
|
||||||
|
alox_TapKind_TAP_SINGLE = 1,
|
||||||
|
alox_TapKind_TAP_DOUBLE = 2,
|
||||||
|
alox_TapKind_TAP_TRIPLE = 3
|
||||||
|
} alox_TapKind;
|
||||||
|
|
||||||
/* Struct definitions */
|
/* Struct definitions */
|
||||||
typedef struct _alox_Ack {
|
typedef struct _alox_Ack {
|
||||||
char dummy_field;
|
char dummy_field;
|
||||||
@@ -54,6 +70,12 @@ typedef struct _alox_ClientInfo {
|
|||||||
uint32_t last_ping;
|
uint32_t last_ping;
|
||||||
uint32_t last_success_ping;
|
uint32_t last_success_ping;
|
||||||
uint32_t version;
|
uint32_t version;
|
||||||
|
/* * Master: ESP-NOW accel stream enabled for this slave. */
|
||||||
|
bool accel_stream_enabled;
|
||||||
|
/* * Master: ESP-NOW tap notify flags for this slave. */
|
||||||
|
bool tap_notify_single;
|
||||||
|
bool tap_notify_double;
|
||||||
|
bool tap_notify_triple;
|
||||||
} alox_ClientInfo;
|
} alox_ClientInfo;
|
||||||
|
|
||||||
typedef struct _alox_ClientInfoResponse {
|
typedef struct _alox_ClientInfoResponse {
|
||||||
@@ -88,6 +110,125 @@ typedef struct _alox_AccelDeadzoneResponse {
|
|||||||
uint32_t slaves_updated;
|
uint32_t slaves_updated;
|
||||||
} alox_AccelDeadzoneResponse;
|
} alox_AccelDeadzoneResponse;
|
||||||
|
|
||||||
|
/* Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||||
|
write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients). */
|
||||||
|
typedef struct _alox_AccelStreamRequest {
|
||||||
|
bool write;
|
||||||
|
bool enable;
|
||||||
|
uint32_t client_id;
|
||||||
|
bool all_clients;
|
||||||
|
} alox_AccelStreamRequest;
|
||||||
|
|
||||||
|
typedef struct _alox_AccelStreamResponse {
|
||||||
|
bool enabled;
|
||||||
|
uint32_t client_id;
|
||||||
|
bool success;
|
||||||
|
uint32_t slaves_updated;
|
||||||
|
} alox_AccelStreamResponse;
|
||||||
|
|
||||||
|
/* * Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */
|
||||||
|
typedef struct _alox_BatteryStatusRequest {
|
||||||
|
/* * 0 = master only; >0 = one slave; ignored when all_clients */
|
||||||
|
uint32_t client_id;
|
||||||
|
/* * Master (client_id 0) plus every registered slave */
|
||||||
|
bool all_clients;
|
||||||
|
} alox_BatteryStatusRequest;
|
||||||
|
|
||||||
|
typedef struct _alox_LipoReading {
|
||||||
|
bool valid;
|
||||||
|
/* * Estimated pack voltage in millivolts from ADC */
|
||||||
|
uint32_t voltage_mv;
|
||||||
|
} alox_LipoReading;
|
||||||
|
|
||||||
|
typedef struct _alox_BatterySample {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool has_lipo1;
|
||||||
|
alox_LipoReading lipo1;
|
||||||
|
bool has_lipo2;
|
||||||
|
alox_LipoReading lipo2;
|
||||||
|
/* * Milliseconds since last ESP-NOW battery report from this pod. */
|
||||||
|
uint32_t age_ms;
|
||||||
|
} alox_BatterySample;
|
||||||
|
|
||||||
|
typedef struct _alox_BatteryStatusResponse {
|
||||||
|
bool success;
|
||||||
|
pb_size_t samples_count;
|
||||||
|
alox_BatterySample samples[17];
|
||||||
|
} alox_BatteryStatusResponse;
|
||||||
|
|
||||||
|
/* * Legacy host-side sample shape (dashboard helpers); use CACHE_STATUS on the wire. */
|
||||||
|
typedef struct _alox_AccelSample {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool valid;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
int32_t z;
|
||||||
|
/* * Milliseconds since last ESP-NOW sample from this slave. */
|
||||||
|
uint32_t age_ms;
|
||||||
|
} alox_AccelSample;
|
||||||
|
|
||||||
|
/* * Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
|
||||||
|
typedef struct _alox_TapNotifyRequest {
|
||||||
|
bool write;
|
||||||
|
uint32_t client_id;
|
||||||
|
bool all_clients;
|
||||||
|
bool single;
|
||||||
|
bool double_tap;
|
||||||
|
bool triple;
|
||||||
|
} alox_TapNotifyRequest;
|
||||||
|
|
||||||
|
typedef struct _alox_TapNotifyResponse {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool success;
|
||||||
|
uint32_t slaves_updated;
|
||||||
|
bool single;
|
||||||
|
bool double_tap;
|
||||||
|
bool triple;
|
||||||
|
} alox_TapNotifyResponse;
|
||||||
|
|
||||||
|
/* * Legacy tap event shape (dashboard helpers); use CACHE_STATUS on the wire. */
|
||||||
|
typedef struct _alox_TapEvent {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool valid;
|
||||||
|
alox_TapKind kind;
|
||||||
|
uint32_t age_ms;
|
||||||
|
} alox_TapEvent;
|
||||||
|
|
||||||
|
/* * Host → master: one-shot read of subscribed cached slave data (no request body). */
|
||||||
|
typedef struct _alox_CacheStatusRequest {
|
||||||
|
char dummy_field;
|
||||||
|
} alox_CacheStatusRequest;
|
||||||
|
|
||||||
|
/* * Accel slice inside CACHE_STATUS (no client_id — use parent CacheClientStatus). */
|
||||||
|
typedef struct _alox_CacheClientAccel {
|
||||||
|
bool valid;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
int32_t z;
|
||||||
|
uint32_t age_ms;
|
||||||
|
} alox_CacheClientAccel;
|
||||||
|
|
||||||
|
/* * Tap slice inside CACHE_STATUS; only present when a pending tap was consumed. */
|
||||||
|
typedef struct _alox_CacheClientTap {
|
||||||
|
alox_TapKind kind;
|
||||||
|
uint32_t age_ms;
|
||||||
|
} alox_CacheClientTap;
|
||||||
|
|
||||||
|
/* * One slave with accel and/or tap notify enabled; only subscribed fields are set. */
|
||||||
|
typedef struct _alox_CacheClientStatus {
|
||||||
|
uint32_t client_id;
|
||||||
|
bool has_accel;
|
||||||
|
alox_CacheClientAccel accel;
|
||||||
|
bool has_tap;
|
||||||
|
alox_CacheClientTap tap;
|
||||||
|
} alox_CacheClientStatus;
|
||||||
|
|
||||||
|
typedef struct _alox_CacheStatusResponse {
|
||||||
|
/* * Slaves with accel_stream and/or tap notify; omitted fields are not subscribed. */
|
||||||
|
pb_size_t clients_count;
|
||||||
|
alox_CacheClientStatus clients[16];
|
||||||
|
} alox_CacheStatusResponse;
|
||||||
|
|
||||||
typedef struct _alox_EspNowUnicastTestRequest {
|
typedef struct _alox_EspNowUnicastTestRequest {
|
||||||
uint32_t client_id;
|
uint32_t client_id;
|
||||||
uint32_t seq;
|
uint32_t seq;
|
||||||
@@ -98,8 +239,24 @@ typedef struct _alox_EspNowUnicastTestResponse {
|
|||||||
uint32_t seq;
|
uint32_t seq;
|
||||||
} alox_EspNowUnicastTestResponse;
|
} alox_EspNowUnicastTestResponse;
|
||||||
|
|
||||||
/* Host → device: LED ring display (progress bar, digit, clear, blink, or find-me).
|
/* * Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
|
||||||
mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink full ring, 4=find-me (R/G/B ×3 @ full brightness). */
|
typedef struct _alox_EspNowEchoPingRequest {
|
||||||
|
uint32_t client_id;
|
||||||
|
/* * Microseconds since Unix epoch (host clock). */
|
||||||
|
uint64_t timestamp_us;
|
||||||
|
} alox_EspNowEchoPingRequest;
|
||||||
|
|
||||||
|
typedef struct _alox_EspNowEchoPingResponse {
|
||||||
|
bool success;
|
||||||
|
uint32_t client_id;
|
||||||
|
/* * Echoed host timestamp from goTool request. */
|
||||||
|
uint64_t timestamp_us;
|
||||||
|
/* * esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
|
||||||
|
uint32_t esp_rtt_us;
|
||||||
|
} alox_EspNowEchoPingResponse;
|
||||||
|
|
||||||
|
/* Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
|
||||||
|
mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low. */
|
||||||
typedef struct _alox_LedRingProgressRequest {
|
typedef struct _alox_LedRingProgressRequest {
|
||||||
uint32_t mode;
|
uint32_t mode;
|
||||||
/* * 0–100: fraction of ring LEDs to light (mode=progress) */
|
/* * 0–100: fraction of ring LEDs to light (mode=progress) */
|
||||||
@@ -115,6 +272,12 @@ typedef struct _alox_LedRingProgressRequest {
|
|||||||
uint32_t blink_ms;
|
uint32_t blink_ms;
|
||||||
/* * Number of pulses (mode=blink, default 1) */
|
/* * Number of pulses (mode=blink, default 1) */
|
||||||
uint32_t blink_count;
|
uint32_t blink_count;
|
||||||
|
/* * 0 = master ring only; >0 = one slave; ignored when all_clients */
|
||||||
|
uint32_t client_id;
|
||||||
|
/* * Broadcast to all registered slaves (and optionally master unless slaves_only) */
|
||||||
|
bool all_clients;
|
||||||
|
/* * With all_clients: do not change master ring */
|
||||||
|
bool slaves_only;
|
||||||
} alox_LedRingProgressRequest;
|
} alox_LedRingProgressRequest;
|
||||||
|
|
||||||
typedef struct _alox_LedRingProgressResponse {
|
typedef struct _alox_LedRingProgressResponse {
|
||||||
@@ -122,6 +285,8 @@ typedef struct _alox_LedRingProgressResponse {
|
|||||||
uint32_t mode;
|
uint32_t mode;
|
||||||
uint32_t progress;
|
uint32_t progress;
|
||||||
uint32_t digit;
|
uint32_t digit;
|
||||||
|
uint32_t client_id;
|
||||||
|
uint32_t slaves_updated;
|
||||||
} alox_LedRingProgressResponse;
|
} alox_LedRingProgressResponse;
|
||||||
|
|
||||||
/* * Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */
|
/* * Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */
|
||||||
@@ -144,6 +309,18 @@ typedef struct _alox_RestartResponse {
|
|||||||
uint32_t client_id;
|
uint32_t client_id;
|
||||||
} alox_RestartResponse;
|
} alox_RestartResponse;
|
||||||
|
|
||||||
|
/* * Host → master: read/write global log level (esp_log_level_set("*", …)). */
|
||||||
|
typedef struct _alox_SetLogLevelRequest {
|
||||||
|
bool write;
|
||||||
|
/* * esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE */
|
||||||
|
uint32_t level;
|
||||||
|
} alox_SetLogLevelRequest;
|
||||||
|
|
||||||
|
typedef struct _alox_SetLogLevelResponse {
|
||||||
|
bool success;
|
||||||
|
uint32_t level;
|
||||||
|
} alox_SetLogLevelResponse;
|
||||||
|
|
||||||
/* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */
|
/* Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS). */
|
||||||
typedef struct _alox_OtaStartPayload {
|
typedef struct _alox_OtaStartPayload {
|
||||||
uint32_t total_size;
|
uint32_t total_size;
|
||||||
@@ -218,6 +395,18 @@ typedef struct _alox_UartMessage {
|
|||||||
alox_EspNowFindMeResponse espnow_find_me_response;
|
alox_EspNowFindMeResponse espnow_find_me_response;
|
||||||
alox_RestartRequest restart_request;
|
alox_RestartRequest restart_request;
|
||||||
alox_RestartResponse restart_response;
|
alox_RestartResponse restart_response;
|
||||||
|
alox_AccelStreamRequest accel_stream_request;
|
||||||
|
alox_AccelStreamResponse accel_stream_response;
|
||||||
|
alox_BatteryStatusRequest battery_status_request;
|
||||||
|
alox_BatteryStatusResponse battery_status_response;
|
||||||
|
alox_TapNotifyRequest tap_notify_request;
|
||||||
|
alox_TapNotifyResponse tap_notify_response;
|
||||||
|
alox_CacheStatusRequest cache_status_request;
|
||||||
|
alox_CacheStatusResponse cache_status_response;
|
||||||
|
alox_EspNowEchoPingRequest espnow_echo_ping_request;
|
||||||
|
alox_EspNowEchoPingResponse espnow_echo_ping_response;
|
||||||
|
alox_SetLogLevelRequest set_log_level_request;
|
||||||
|
alox_SetLogLevelResponse set_log_level_response;
|
||||||
} payload;
|
} payload;
|
||||||
} alox_UartMessage;
|
} alox_UartMessage;
|
||||||
|
|
||||||
@@ -228,8 +417,12 @@ extern "C" {
|
|||||||
|
|
||||||
/* Helper constants for enums */
|
/* Helper constants for enums */
|
||||||
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN
|
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN
|
||||||
#define _alox_MessageType_MAX alox_MessageType_RESTART
|
#define _alox_MessageType_MAX alox_MessageType_SET_LOG_LEVEL
|
||||||
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_RESTART+1))
|
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_SET_LOG_LEVEL+1))
|
||||||
|
|
||||||
|
#define _alox_TapKind_MIN alox_TapKind_TAP_NONE
|
||||||
|
#define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE
|
||||||
|
#define _alox_TapKind_ARRAYSIZE ((alox_TapKind)(alox_TapKind_TAP_TRIPLE+1))
|
||||||
|
|
||||||
#define alox_UartMessage_type_ENUMTYPE alox_MessageType
|
#define alox_UartMessage_type_ENUMTYPE alox_MessageType
|
||||||
|
|
||||||
@@ -251,6 +444,27 @@ extern "C" {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define alox_TapEvent_kind_ENUMTYPE alox_TapKind
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#define alox_CacheClientTap_kind_ENUMTYPE alox_TapKind
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -263,20 +477,39 @@ extern "C" {
|
|||||||
#define alox_Ack_init_default {0}
|
#define alox_Ack_init_default {0}
|
||||||
#define alox_EchoPayload_init_default {{{NULL}, NULL}}
|
#define alox_EchoPayload_init_default {{{NULL}, NULL}}
|
||||||
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
#define alox_VersionResponse_init_default {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
||||||
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
|
#define alox_ClientInfo_init_default {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
|
#define alox_ClientInfoResponse_init_default {{{NULL}, NULL}}
|
||||||
#define alox_ClientInput_init_default {0, 0, 0, 0}
|
#define alox_ClientInput_init_default {0, 0, 0, 0}
|
||||||
#define alox_ClientInputResponse_init_default {{{NULL}, NULL}}
|
#define alox_ClientInputResponse_init_default {{{NULL}, NULL}}
|
||||||
#define alox_AccelDeadzoneRequest_init_default {0, 0, 0, 0}
|
#define alox_AccelDeadzoneRequest_init_default {0, 0, 0, 0}
|
||||||
#define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
|
#define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_AccelStreamRequest_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_AccelStreamResponse_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_BatteryStatusRequest_init_default {0, 0}
|
||||||
|
#define alox_LipoReading_init_default {0, 0}
|
||||||
|
#define alox_BatterySample_init_default {0, false, alox_LipoReading_init_default, false, alox_LipoReading_init_default, 0}
|
||||||
|
#define alox_BatteryStatusResponse_init_default {0, 0, {alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default, alox_BatterySample_init_default}}
|
||||||
|
#define alox_AccelSample_init_default {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapNotifyRequest_init_default {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapNotifyResponse_init_default {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapEvent_init_default {0, 0, _alox_TapKind_MIN, 0}
|
||||||
|
#define alox_CacheStatusRequest_init_default {0}
|
||||||
|
#define alox_CacheClientAccel_init_default {0, 0, 0, 0, 0}
|
||||||
|
#define alox_CacheClientTap_init_default {_alox_TapKind_MIN, 0}
|
||||||
|
#define alox_CacheClientStatus_init_default {0, false, alox_CacheClientAccel_init_default, false, alox_CacheClientTap_init_default}
|
||||||
|
#define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}}
|
||||||
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
|
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
|
||||||
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
|
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
|
||||||
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define alox_EspNowEchoPingRequest_init_default {0, 0}
|
||||||
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0}
|
#define alox_EspNowEchoPingResponse_init_default {0, 0, 0, 0}
|
||||||
|
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0, 0, 0}
|
||||||
#define alox_EspNowFindMeRequest_init_default {0}
|
#define alox_EspNowFindMeRequest_init_default {0}
|
||||||
#define alox_EspNowFindMeResponse_init_default {0, 0}
|
#define alox_EspNowFindMeResponse_init_default {0, 0}
|
||||||
#define alox_RestartRequest_init_default {0}
|
#define alox_RestartRequest_init_default {0}
|
||||||
#define alox_RestartResponse_init_default {0, 0}
|
#define alox_RestartResponse_init_default {0, 0}
|
||||||
|
#define alox_SetLogLevelRequest_init_default {0, 0}
|
||||||
|
#define alox_SetLogLevelResponse_init_default {0, 0}
|
||||||
#define alox_OtaStartPayload_init_default {0}
|
#define alox_OtaStartPayload_init_default {0}
|
||||||
#define alox_OtaPayload_init_default {0, {0, {0}}}
|
#define alox_OtaPayload_init_default {0, {0, {0}}}
|
||||||
#define alox_OtaEndPayload_init_default {0}
|
#define alox_OtaEndPayload_init_default {0}
|
||||||
@@ -288,20 +521,39 @@ extern "C" {
|
|||||||
#define alox_Ack_init_zero {0}
|
#define alox_Ack_init_zero {0}
|
||||||
#define alox_EchoPayload_init_zero {{{NULL}, NULL}}
|
#define alox_EchoPayload_init_zero {{{NULL}, NULL}}
|
||||||
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
#define alox_VersionResponse_init_zero {0, {{NULL}, NULL}, {{NULL}, NULL}}
|
||||||
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0}
|
#define alox_ClientInfo_init_zero {0, 0, 0, {{NULL}, NULL}, 0, 0, 0, 0, 0, 0, 0}
|
||||||
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
|
#define alox_ClientInfoResponse_init_zero {{{NULL}, NULL}}
|
||||||
#define alox_ClientInput_init_zero {0, 0, 0, 0}
|
#define alox_ClientInput_init_zero {0, 0, 0, 0}
|
||||||
#define alox_ClientInputResponse_init_zero {{{NULL}, NULL}}
|
#define alox_ClientInputResponse_init_zero {{{NULL}, NULL}}
|
||||||
#define alox_AccelDeadzoneRequest_init_zero {0, 0, 0, 0}
|
#define alox_AccelDeadzoneRequest_init_zero {0, 0, 0, 0}
|
||||||
#define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
|
#define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_AccelStreamRequest_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_AccelStreamResponse_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_BatteryStatusRequest_init_zero {0, 0}
|
||||||
|
#define alox_LipoReading_init_zero {0, 0}
|
||||||
|
#define alox_BatterySample_init_zero {0, false, alox_LipoReading_init_zero, false, alox_LipoReading_init_zero, 0}
|
||||||
|
#define alox_BatteryStatusResponse_init_zero {0, 0, {alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero, alox_BatterySample_init_zero}}
|
||||||
|
#define alox_AccelSample_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapNotifyRequest_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapNotifyResponse_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_TapEvent_init_zero {0, 0, _alox_TapKind_MIN, 0}
|
||||||
|
#define alox_CacheStatusRequest_init_zero {0}
|
||||||
|
#define alox_CacheClientAccel_init_zero {0, 0, 0, 0, 0}
|
||||||
|
#define alox_CacheClientTap_init_zero {_alox_TapKind_MIN, 0}
|
||||||
|
#define alox_CacheClientStatus_init_zero {0, false, alox_CacheClientAccel_init_zero, false, alox_CacheClientTap_init_zero}
|
||||||
|
#define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}}
|
||||||
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
|
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
|
||||||
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
|
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
|
||||||
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0}
|
#define alox_EspNowEchoPingRequest_init_zero {0, 0}
|
||||||
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0}
|
#define alox_EspNowEchoPingResponse_init_zero {0, 0, 0, 0}
|
||||||
|
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||||
|
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0, 0, 0}
|
||||||
#define alox_EspNowFindMeRequest_init_zero {0}
|
#define alox_EspNowFindMeRequest_init_zero {0}
|
||||||
#define alox_EspNowFindMeResponse_init_zero {0, 0}
|
#define alox_EspNowFindMeResponse_init_zero {0, 0}
|
||||||
#define alox_RestartRequest_init_zero {0}
|
#define alox_RestartRequest_init_zero {0}
|
||||||
#define alox_RestartResponse_init_zero {0, 0}
|
#define alox_RestartResponse_init_zero {0, 0}
|
||||||
|
#define alox_SetLogLevelRequest_init_zero {0, 0}
|
||||||
|
#define alox_SetLogLevelResponse_init_zero {0, 0}
|
||||||
#define alox_OtaStartPayload_init_zero {0}
|
#define alox_OtaStartPayload_init_zero {0}
|
||||||
#define alox_OtaPayload_init_zero {0, {0, {0}}}
|
#define alox_OtaPayload_init_zero {0, {0, {0}}}
|
||||||
#define alox_OtaEndPayload_init_zero {0}
|
#define alox_OtaEndPayload_init_zero {0}
|
||||||
@@ -322,6 +574,10 @@ extern "C" {
|
|||||||
#define alox_ClientInfo_last_ping_tag 5
|
#define alox_ClientInfo_last_ping_tag 5
|
||||||
#define alox_ClientInfo_last_success_ping_tag 6
|
#define alox_ClientInfo_last_success_ping_tag 6
|
||||||
#define alox_ClientInfo_version_tag 7
|
#define alox_ClientInfo_version_tag 7
|
||||||
|
#define alox_ClientInfo_accel_stream_enabled_tag 8
|
||||||
|
#define alox_ClientInfo_tap_notify_single_tag 9
|
||||||
|
#define alox_ClientInfo_tap_notify_double_tag 10
|
||||||
|
#define alox_ClientInfo_tap_notify_triple_tag 11
|
||||||
#define alox_ClientInfoResponse_clients_tag 1
|
#define alox_ClientInfoResponse_clients_tag 1
|
||||||
#define alox_ClientInput_id_tag 1
|
#define alox_ClientInput_id_tag 1
|
||||||
#define alox_ClientInput_lage_x_tag 2
|
#define alox_ClientInput_lage_x_tag 2
|
||||||
@@ -336,10 +592,67 @@ extern "C" {
|
|||||||
#define alox_AccelDeadzoneResponse_client_id_tag 2
|
#define alox_AccelDeadzoneResponse_client_id_tag 2
|
||||||
#define alox_AccelDeadzoneResponse_success_tag 3
|
#define alox_AccelDeadzoneResponse_success_tag 3
|
||||||
#define alox_AccelDeadzoneResponse_slaves_updated_tag 4
|
#define alox_AccelDeadzoneResponse_slaves_updated_tag 4
|
||||||
|
#define alox_AccelStreamRequest_write_tag 1
|
||||||
|
#define alox_AccelStreamRequest_enable_tag 2
|
||||||
|
#define alox_AccelStreamRequest_client_id_tag 3
|
||||||
|
#define alox_AccelStreamRequest_all_clients_tag 4
|
||||||
|
#define alox_AccelStreamResponse_enabled_tag 1
|
||||||
|
#define alox_AccelStreamResponse_client_id_tag 2
|
||||||
|
#define alox_AccelStreamResponse_success_tag 3
|
||||||
|
#define alox_AccelStreamResponse_slaves_updated_tag 4
|
||||||
|
#define alox_BatteryStatusRequest_client_id_tag 1
|
||||||
|
#define alox_BatteryStatusRequest_all_clients_tag 2
|
||||||
|
#define alox_LipoReading_valid_tag 1
|
||||||
|
#define alox_LipoReading_voltage_mv_tag 2
|
||||||
|
#define alox_BatterySample_client_id_tag 1
|
||||||
|
#define alox_BatterySample_lipo1_tag 2
|
||||||
|
#define alox_BatterySample_lipo2_tag 3
|
||||||
|
#define alox_BatterySample_age_ms_tag 4
|
||||||
|
#define alox_BatteryStatusResponse_success_tag 1
|
||||||
|
#define alox_BatteryStatusResponse_samples_tag 2
|
||||||
|
#define alox_AccelSample_client_id_tag 1
|
||||||
|
#define alox_AccelSample_valid_tag 2
|
||||||
|
#define alox_AccelSample_x_tag 3
|
||||||
|
#define alox_AccelSample_y_tag 4
|
||||||
|
#define alox_AccelSample_z_tag 5
|
||||||
|
#define alox_AccelSample_age_ms_tag 6
|
||||||
|
#define alox_TapNotifyRequest_write_tag 1
|
||||||
|
#define alox_TapNotifyRequest_client_id_tag 2
|
||||||
|
#define alox_TapNotifyRequest_all_clients_tag 3
|
||||||
|
#define alox_TapNotifyRequest_single_tag 4
|
||||||
|
#define alox_TapNotifyRequest_double_tap_tag 5
|
||||||
|
#define alox_TapNotifyRequest_triple_tag 6
|
||||||
|
#define alox_TapNotifyResponse_client_id_tag 1
|
||||||
|
#define alox_TapNotifyResponse_success_tag 2
|
||||||
|
#define alox_TapNotifyResponse_slaves_updated_tag 3
|
||||||
|
#define alox_TapNotifyResponse_single_tag 4
|
||||||
|
#define alox_TapNotifyResponse_double_tap_tag 5
|
||||||
|
#define alox_TapNotifyResponse_triple_tag 6
|
||||||
|
#define alox_TapEvent_client_id_tag 1
|
||||||
|
#define alox_TapEvent_valid_tag 2
|
||||||
|
#define alox_TapEvent_kind_tag 3
|
||||||
|
#define alox_TapEvent_age_ms_tag 4
|
||||||
|
#define alox_CacheClientAccel_valid_tag 1
|
||||||
|
#define alox_CacheClientAccel_x_tag 2
|
||||||
|
#define alox_CacheClientAccel_y_tag 3
|
||||||
|
#define alox_CacheClientAccel_z_tag 4
|
||||||
|
#define alox_CacheClientAccel_age_ms_tag 5
|
||||||
|
#define alox_CacheClientTap_kind_tag 1
|
||||||
|
#define alox_CacheClientTap_age_ms_tag 2
|
||||||
|
#define alox_CacheClientStatus_client_id_tag 1
|
||||||
|
#define alox_CacheClientStatus_accel_tag 2
|
||||||
|
#define alox_CacheClientStatus_tap_tag 3
|
||||||
|
#define alox_CacheStatusResponse_clients_tag 1
|
||||||
#define alox_EspNowUnicastTestRequest_client_id_tag 1
|
#define alox_EspNowUnicastTestRequest_client_id_tag 1
|
||||||
#define alox_EspNowUnicastTestRequest_seq_tag 2
|
#define alox_EspNowUnicastTestRequest_seq_tag 2
|
||||||
#define alox_EspNowUnicastTestResponse_success_tag 1
|
#define alox_EspNowUnicastTestResponse_success_tag 1
|
||||||
#define alox_EspNowUnicastTestResponse_seq_tag 2
|
#define alox_EspNowUnicastTestResponse_seq_tag 2
|
||||||
|
#define alox_EspNowEchoPingRequest_client_id_tag 1
|
||||||
|
#define alox_EspNowEchoPingRequest_timestamp_us_tag 2
|
||||||
|
#define alox_EspNowEchoPingResponse_success_tag 1
|
||||||
|
#define alox_EspNowEchoPingResponse_client_id_tag 2
|
||||||
|
#define alox_EspNowEchoPingResponse_timestamp_us_tag 3
|
||||||
|
#define alox_EspNowEchoPingResponse_esp_rtt_us_tag 4
|
||||||
#define alox_LedRingProgressRequest_mode_tag 1
|
#define alox_LedRingProgressRequest_mode_tag 1
|
||||||
#define alox_LedRingProgressRequest_progress_tag 2
|
#define alox_LedRingProgressRequest_progress_tag 2
|
||||||
#define alox_LedRingProgressRequest_digit_tag 3
|
#define alox_LedRingProgressRequest_digit_tag 3
|
||||||
@@ -349,16 +662,25 @@ extern "C" {
|
|||||||
#define alox_LedRingProgressRequest_intensity_tag 7
|
#define alox_LedRingProgressRequest_intensity_tag 7
|
||||||
#define alox_LedRingProgressRequest_blink_ms_tag 8
|
#define alox_LedRingProgressRequest_blink_ms_tag 8
|
||||||
#define alox_LedRingProgressRequest_blink_count_tag 9
|
#define alox_LedRingProgressRequest_blink_count_tag 9
|
||||||
|
#define alox_LedRingProgressRequest_client_id_tag 10
|
||||||
|
#define alox_LedRingProgressRequest_all_clients_tag 11
|
||||||
|
#define alox_LedRingProgressRequest_slaves_only_tag 12
|
||||||
#define alox_LedRingProgressResponse_success_tag 1
|
#define alox_LedRingProgressResponse_success_tag 1
|
||||||
#define alox_LedRingProgressResponse_mode_tag 2
|
#define alox_LedRingProgressResponse_mode_tag 2
|
||||||
#define alox_LedRingProgressResponse_progress_tag 3
|
#define alox_LedRingProgressResponse_progress_tag 3
|
||||||
#define alox_LedRingProgressResponse_digit_tag 4
|
#define alox_LedRingProgressResponse_digit_tag 4
|
||||||
|
#define alox_LedRingProgressResponse_client_id_tag 5
|
||||||
|
#define alox_LedRingProgressResponse_slaves_updated_tag 6
|
||||||
#define alox_EspNowFindMeRequest_client_id_tag 1
|
#define alox_EspNowFindMeRequest_client_id_tag 1
|
||||||
#define alox_EspNowFindMeResponse_success_tag 1
|
#define alox_EspNowFindMeResponse_success_tag 1
|
||||||
#define alox_EspNowFindMeResponse_client_id_tag 2
|
#define alox_EspNowFindMeResponse_client_id_tag 2
|
||||||
#define alox_RestartRequest_client_id_tag 1
|
#define alox_RestartRequest_client_id_tag 1
|
||||||
#define alox_RestartResponse_success_tag 1
|
#define alox_RestartResponse_success_tag 1
|
||||||
#define alox_RestartResponse_client_id_tag 2
|
#define alox_RestartResponse_client_id_tag 2
|
||||||
|
#define alox_SetLogLevelRequest_write_tag 1
|
||||||
|
#define alox_SetLogLevelRequest_level_tag 2
|
||||||
|
#define alox_SetLogLevelResponse_success_tag 1
|
||||||
|
#define alox_SetLogLevelResponse_level_tag 2
|
||||||
#define alox_OtaStartPayload_total_size_tag 1
|
#define alox_OtaStartPayload_total_size_tag 1
|
||||||
#define alox_OtaPayload_seq_tag 1
|
#define alox_OtaPayload_seq_tag 1
|
||||||
#define alox_OtaPayload_data_tag 2
|
#define alox_OtaPayload_data_tag 2
|
||||||
@@ -399,6 +721,18 @@ extern "C" {
|
|||||||
#define alox_UartMessage_espnow_find_me_response_tag 20
|
#define alox_UartMessage_espnow_find_me_response_tag 20
|
||||||
#define alox_UartMessage_restart_request_tag 21
|
#define alox_UartMessage_restart_request_tag 21
|
||||||
#define alox_UartMessage_restart_response_tag 22
|
#define alox_UartMessage_restart_response_tag 22
|
||||||
|
#define alox_UartMessage_accel_stream_request_tag 25
|
||||||
|
#define alox_UartMessage_accel_stream_response_tag 26
|
||||||
|
#define alox_UartMessage_battery_status_request_tag 27
|
||||||
|
#define alox_UartMessage_battery_status_response_tag 28
|
||||||
|
#define alox_UartMessage_tap_notify_request_tag 29
|
||||||
|
#define alox_UartMessage_tap_notify_response_tag 30
|
||||||
|
#define alox_UartMessage_cache_status_request_tag 33
|
||||||
|
#define alox_UartMessage_cache_status_response_tag 34
|
||||||
|
#define alox_UartMessage_espnow_echo_ping_request_tag 35
|
||||||
|
#define alox_UartMessage_espnow_echo_ping_response_tag 36
|
||||||
|
#define alox_UartMessage_set_log_level_request_tag 37
|
||||||
|
#define alox_UartMessage_set_log_level_response_tag 38
|
||||||
|
|
||||||
/* Struct field encoding specification for nanopb */
|
/* Struct field encoding specification for nanopb */
|
||||||
#define alox_UartMessage_FIELDLIST(X, a) \
|
#define alox_UartMessage_FIELDLIST(X, a) \
|
||||||
@@ -423,7 +757,19 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring_progress_response,payload.l
|
|||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_request,payload.espnow_find_me_request), 19) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_request,payload.espnow_find_me_request), 19) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_response,payload.espnow_find_me_response), 20) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_find_me_response,payload.espnow_find_me_response), 20) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_request,payload.restart_request), 21) \
|
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_request,payload.restart_request), 21) \
|
||||||
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_response,payload.restart_response), 22)
|
X(a, STATIC, ONEOF, MESSAGE, (payload,restart_response,payload.restart_response), 22) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_request,payload.accel_stream_request), 25) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,accel_stream_response,payload.accel_stream_response), 26) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_request,payload.battery_status_request), 27) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.battery_status_response), 28) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_response,payload.tap_notify_response), 30) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_request,payload.cache_status_request), 33) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_status_response), 34) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_echo_ping_request,payload.espnow_echo_ping_request), 35) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_echo_ping_response,payload.espnow_echo_ping_response), 36) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_request,payload.set_log_level_request), 37) \
|
||||||
|
X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_response,payload.set_log_level_response), 38)
|
||||||
#define alox_UartMessage_CALLBACK NULL
|
#define alox_UartMessage_CALLBACK NULL
|
||||||
#define alox_UartMessage_DEFAULT NULL
|
#define alox_UartMessage_DEFAULT NULL
|
||||||
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
|
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
|
||||||
@@ -447,6 +793,18 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,restart_response,payload.restart_res
|
|||||||
#define alox_UartMessage_payload_espnow_find_me_response_MSGTYPE alox_EspNowFindMeResponse
|
#define alox_UartMessage_payload_espnow_find_me_response_MSGTYPE alox_EspNowFindMeResponse
|
||||||
#define alox_UartMessage_payload_restart_request_MSGTYPE alox_RestartRequest
|
#define alox_UartMessage_payload_restart_request_MSGTYPE alox_RestartRequest
|
||||||
#define alox_UartMessage_payload_restart_response_MSGTYPE alox_RestartResponse
|
#define alox_UartMessage_payload_restart_response_MSGTYPE alox_RestartResponse
|
||||||
|
#define alox_UartMessage_payload_accel_stream_request_MSGTYPE alox_AccelStreamRequest
|
||||||
|
#define alox_UartMessage_payload_accel_stream_response_MSGTYPE alox_AccelStreamResponse
|
||||||
|
#define alox_UartMessage_payload_battery_status_request_MSGTYPE alox_BatteryStatusRequest
|
||||||
|
#define alox_UartMessage_payload_battery_status_response_MSGTYPE alox_BatteryStatusResponse
|
||||||
|
#define alox_UartMessage_payload_tap_notify_request_MSGTYPE alox_TapNotifyRequest
|
||||||
|
#define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse
|
||||||
|
#define alox_UartMessage_payload_cache_status_request_MSGTYPE alox_CacheStatusRequest
|
||||||
|
#define alox_UartMessage_payload_cache_status_response_MSGTYPE alox_CacheStatusResponse
|
||||||
|
#define alox_UartMessage_payload_espnow_echo_ping_request_MSGTYPE alox_EspNowEchoPingRequest
|
||||||
|
#define alox_UartMessage_payload_espnow_echo_ping_response_MSGTYPE alox_EspNowEchoPingResponse
|
||||||
|
#define alox_UartMessage_payload_set_log_level_request_MSGTYPE alox_SetLogLevelRequest
|
||||||
|
#define alox_UartMessage_payload_set_log_level_response_MSGTYPE alox_SetLogLevelResponse
|
||||||
|
|
||||||
#define alox_Ack_FIELDLIST(X, a) \
|
#define alox_Ack_FIELDLIST(X, a) \
|
||||||
|
|
||||||
@@ -472,7 +830,11 @@ X(a, STATIC, SINGULAR, BOOL, used, 3) \
|
|||||||
X(a, CALLBACK, SINGULAR, BYTES, mac, 4) \
|
X(a, CALLBACK, SINGULAR, BYTES, mac, 4) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, last_ping, 5) \
|
X(a, STATIC, SINGULAR, UINT32, last_ping, 5) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, last_success_ping, 6) \
|
X(a, STATIC, SINGULAR, UINT32, last_success_ping, 6) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, version, 7)
|
X(a, STATIC, SINGULAR, UINT32, version, 7) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, accel_stream_enabled, 8) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, tap_notify_single, 9) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, tap_notify_double, 10) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, tap_notify_triple, 11)
|
||||||
#define alox_ClientInfo_CALLBACK pb_default_field_callback
|
#define alox_ClientInfo_CALLBACK pb_default_field_callback
|
||||||
#define alox_ClientInfo_DEFAULT NULL
|
#define alox_ClientInfo_DEFAULT NULL
|
||||||
|
|
||||||
@@ -512,6 +874,124 @@ X(a, STATIC, SINGULAR, UINT32, slaves_updated, 4)
|
|||||||
#define alox_AccelDeadzoneResponse_CALLBACK NULL
|
#define alox_AccelDeadzoneResponse_CALLBACK NULL
|
||||||
#define alox_AccelDeadzoneResponse_DEFAULT NULL
|
#define alox_AccelDeadzoneResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_AccelStreamRequest_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, write, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, enable, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, all_clients, 4)
|
||||||
|
#define alox_AccelStreamRequest_CALLBACK NULL
|
||||||
|
#define alox_AccelStreamRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_AccelStreamResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, enabled, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, success, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 4)
|
||||||
|
#define alox_AccelStreamResponse_CALLBACK NULL
|
||||||
|
#define alox_AccelStreamResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_BatteryStatusRequest_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, all_clients, 2)
|
||||||
|
#define alox_BatteryStatusRequest_CALLBACK NULL
|
||||||
|
#define alox_BatteryStatusRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_LipoReading_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, valid, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, voltage_mv, 2)
|
||||||
|
#define alox_LipoReading_CALLBACK NULL
|
||||||
|
#define alox_LipoReading_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_BatterySample_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, lipo1, 2) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, lipo2, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, age_ms, 4)
|
||||||
|
#define alox_BatterySample_CALLBACK NULL
|
||||||
|
#define alox_BatterySample_DEFAULT NULL
|
||||||
|
#define alox_BatterySample_lipo1_MSGTYPE alox_LipoReading
|
||||||
|
#define alox_BatterySample_lipo2_MSGTYPE alox_LipoReading
|
||||||
|
|
||||||
|
#define alox_BatteryStatusResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, success, 1) \
|
||||||
|
X(a, STATIC, REPEATED, MESSAGE, samples, 2)
|
||||||
|
#define alox_BatteryStatusResponse_CALLBACK NULL
|
||||||
|
#define alox_BatteryStatusResponse_DEFAULT NULL
|
||||||
|
#define alox_BatteryStatusResponse_samples_MSGTYPE alox_BatterySample
|
||||||
|
|
||||||
|
#define alox_AccelSample_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, valid, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, x, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, y, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, z, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, age_ms, 6)
|
||||||
|
#define alox_AccelSample_CALLBACK NULL
|
||||||
|
#define alox_AccelSample_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_TapNotifyRequest_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, write, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, all_clients, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, single, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, double_tap, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, triple, 6)
|
||||||
|
#define alox_TapNotifyRequest_CALLBACK NULL
|
||||||
|
#define alox_TapNotifyRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_TapNotifyResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, success, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, single, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, double_tap, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, triple, 6)
|
||||||
|
#define alox_TapNotifyResponse_CALLBACK NULL
|
||||||
|
#define alox_TapNotifyResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_TapEvent_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, valid, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UENUM, kind, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, age_ms, 4)
|
||||||
|
#define alox_TapEvent_CALLBACK NULL
|
||||||
|
#define alox_TapEvent_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_CacheStatusRequest_FIELDLIST(X, a) \
|
||||||
|
|
||||||
|
#define alox_CacheStatusRequest_CALLBACK NULL
|
||||||
|
#define alox_CacheStatusRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_CacheClientAccel_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, valid, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, x, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, y, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, SINT32, z, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, age_ms, 5)
|
||||||
|
#define alox_CacheClientAccel_CALLBACK NULL
|
||||||
|
#define alox_CacheClientAccel_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_CacheClientTap_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UENUM, kind, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, age_ms, 2)
|
||||||
|
#define alox_CacheClientTap_CALLBACK NULL
|
||||||
|
#define alox_CacheClientTap_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_CacheClientStatus_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, accel, 2) \
|
||||||
|
X(a, STATIC, OPTIONAL, MESSAGE, tap, 3)
|
||||||
|
#define alox_CacheClientStatus_CALLBACK NULL
|
||||||
|
#define alox_CacheClientStatus_DEFAULT NULL
|
||||||
|
#define alox_CacheClientStatus_accel_MSGTYPE alox_CacheClientAccel
|
||||||
|
#define alox_CacheClientStatus_tap_MSGTYPE alox_CacheClientTap
|
||||||
|
|
||||||
|
#define alox_CacheStatusResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, REPEATED, MESSAGE, clients, 1)
|
||||||
|
#define alox_CacheStatusResponse_CALLBACK NULL
|
||||||
|
#define alox_CacheStatusResponse_DEFAULT NULL
|
||||||
|
#define alox_CacheStatusResponse_clients_MSGTYPE alox_CacheClientStatus
|
||||||
|
|
||||||
#define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \
|
#define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, seq, 2)
|
X(a, STATIC, SINGULAR, UINT32, seq, 2)
|
||||||
@@ -524,6 +1004,20 @@ X(a, STATIC, SINGULAR, UINT32, seq, 2)
|
|||||||
#define alox_EspNowUnicastTestResponse_CALLBACK NULL
|
#define alox_EspNowUnicastTestResponse_CALLBACK NULL
|
||||||
#define alox_EspNowUnicastTestResponse_DEFAULT NULL
|
#define alox_EspNowUnicastTestResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowEchoPingRequest_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, timestamp_us, 2)
|
||||||
|
#define alox_EspNowEchoPingRequest_CALLBACK NULL
|
||||||
|
#define alox_EspNowEchoPingRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_EspNowEchoPingResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, success, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 2) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT64, timestamp_us, 3) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, esp_rtt_us, 4)
|
||||||
|
#define alox_EspNowEchoPingResponse_CALLBACK NULL
|
||||||
|
#define alox_EspNowEchoPingResponse_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_LedRingProgressRequest_FIELDLIST(X, a) \
|
#define alox_LedRingProgressRequest_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, mode, 1) \
|
X(a, STATIC, SINGULAR, UINT32, mode, 1) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, progress, 2) \
|
X(a, STATIC, SINGULAR, UINT32, progress, 2) \
|
||||||
@@ -533,7 +1027,10 @@ X(a, STATIC, SINGULAR, UINT32, g, 5) \
|
|||||||
X(a, STATIC, SINGULAR, UINT32, b, 6) \
|
X(a, STATIC, SINGULAR, UINT32, b, 6) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, intensity, 7) \
|
X(a, STATIC, SINGULAR, UINT32, intensity, 7) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, blink_ms, 8) \
|
X(a, STATIC, SINGULAR, UINT32, blink_ms, 8) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, blink_count, 9)
|
X(a, STATIC, SINGULAR, UINT32, blink_count, 9) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 10) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, all_clients, 11) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, slaves_only, 12)
|
||||||
#define alox_LedRingProgressRequest_CALLBACK NULL
|
#define alox_LedRingProgressRequest_CALLBACK NULL
|
||||||
#define alox_LedRingProgressRequest_DEFAULT NULL
|
#define alox_LedRingProgressRequest_DEFAULT NULL
|
||||||
|
|
||||||
@@ -541,7 +1038,9 @@ X(a, STATIC, SINGULAR, UINT32, blink_count, 9)
|
|||||||
X(a, STATIC, SINGULAR, BOOL, success, 1) \
|
X(a, STATIC, SINGULAR, BOOL, success, 1) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
|
X(a, STATIC, SINGULAR, UINT32, mode, 2) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
|
X(a, STATIC, SINGULAR, UINT32, progress, 3) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, digit, 4)
|
X(a, STATIC, SINGULAR, UINT32, digit, 4) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, client_id, 5) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, slaves_updated, 6)
|
||||||
#define alox_LedRingProgressResponse_CALLBACK NULL
|
#define alox_LedRingProgressResponse_CALLBACK NULL
|
||||||
#define alox_LedRingProgressResponse_DEFAULT NULL
|
#define alox_LedRingProgressResponse_DEFAULT NULL
|
||||||
|
|
||||||
@@ -567,6 +1066,18 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2)
|
|||||||
#define alox_RestartResponse_CALLBACK NULL
|
#define alox_RestartResponse_CALLBACK NULL
|
||||||
#define alox_RestartResponse_DEFAULT NULL
|
#define alox_RestartResponse_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_SetLogLevelRequest_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, write, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, level, 2)
|
||||||
|
#define alox_SetLogLevelRequest_CALLBACK NULL
|
||||||
|
#define alox_SetLogLevelRequest_DEFAULT NULL
|
||||||
|
|
||||||
|
#define alox_SetLogLevelResponse_FIELDLIST(X, a) \
|
||||||
|
X(a, STATIC, SINGULAR, BOOL, success, 1) \
|
||||||
|
X(a, STATIC, SINGULAR, UINT32, level, 2)
|
||||||
|
#define alox_SetLogLevelResponse_CALLBACK NULL
|
||||||
|
#define alox_SetLogLevelResponse_DEFAULT NULL
|
||||||
|
|
||||||
#define alox_OtaStartPayload_FIELDLIST(X, a) \
|
#define alox_OtaStartPayload_FIELDLIST(X, a) \
|
||||||
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
|
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
|
||||||
#define alox_OtaStartPayload_CALLBACK NULL
|
#define alox_OtaStartPayload_CALLBACK NULL
|
||||||
@@ -625,14 +1136,33 @@ extern const pb_msgdesc_t alox_ClientInput_msg;
|
|||||||
extern const pb_msgdesc_t alox_ClientInputResponse_msg;
|
extern const pb_msgdesc_t alox_ClientInputResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_AccelDeadzoneRequest_msg;
|
extern const pb_msgdesc_t alox_AccelDeadzoneRequest_msg;
|
||||||
extern const pb_msgdesc_t alox_AccelDeadzoneResponse_msg;
|
extern const pb_msgdesc_t alox_AccelDeadzoneResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_AccelStreamRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_AccelStreamResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_BatteryStatusRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_LipoReading_msg;
|
||||||
|
extern const pb_msgdesc_t alox_BatterySample_msg;
|
||||||
|
extern const pb_msgdesc_t alox_BatteryStatusResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_AccelSample_msg;
|
||||||
|
extern const pb_msgdesc_t alox_TapNotifyRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_TapNotifyResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_TapEvent_msg;
|
||||||
|
extern const pb_msgdesc_t alox_CacheStatusRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_CacheClientAccel_msg;
|
||||||
|
extern const pb_msgdesc_t alox_CacheClientTap_msg;
|
||||||
|
extern const pb_msgdesc_t alox_CacheClientStatus_msg;
|
||||||
|
extern const pb_msgdesc_t alox_CacheStatusResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
|
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
|
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowEchoPingRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_EspNowEchoPingResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_LedRingProgressRequest_msg;
|
extern const pb_msgdesc_t alox_LedRingProgressRequest_msg;
|
||||||
extern const pb_msgdesc_t alox_LedRingProgressResponse_msg;
|
extern const pb_msgdesc_t alox_LedRingProgressResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg;
|
extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg;
|
||||||
extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg;
|
extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_RestartRequest_msg;
|
extern const pb_msgdesc_t alox_RestartRequest_msg;
|
||||||
extern const pb_msgdesc_t alox_RestartResponse_msg;
|
extern const pb_msgdesc_t alox_RestartResponse_msg;
|
||||||
|
extern const pb_msgdesc_t alox_SetLogLevelRequest_msg;
|
||||||
|
extern const pb_msgdesc_t alox_SetLogLevelResponse_msg;
|
||||||
extern const pb_msgdesc_t alox_OtaStartPayload_msg;
|
extern const pb_msgdesc_t alox_OtaStartPayload_msg;
|
||||||
extern const pb_msgdesc_t alox_OtaPayload_msg;
|
extern const pb_msgdesc_t alox_OtaPayload_msg;
|
||||||
extern const pb_msgdesc_t alox_OtaEndPayload_msg;
|
extern const pb_msgdesc_t alox_OtaEndPayload_msg;
|
||||||
@@ -652,14 +1182,33 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
|
|||||||
#define alox_ClientInputResponse_fields &alox_ClientInputResponse_msg
|
#define alox_ClientInputResponse_fields &alox_ClientInputResponse_msg
|
||||||
#define alox_AccelDeadzoneRequest_fields &alox_AccelDeadzoneRequest_msg
|
#define alox_AccelDeadzoneRequest_fields &alox_AccelDeadzoneRequest_msg
|
||||||
#define alox_AccelDeadzoneResponse_fields &alox_AccelDeadzoneResponse_msg
|
#define alox_AccelDeadzoneResponse_fields &alox_AccelDeadzoneResponse_msg
|
||||||
|
#define alox_AccelStreamRequest_fields &alox_AccelStreamRequest_msg
|
||||||
|
#define alox_AccelStreamResponse_fields &alox_AccelStreamResponse_msg
|
||||||
|
#define alox_BatteryStatusRequest_fields &alox_BatteryStatusRequest_msg
|
||||||
|
#define alox_LipoReading_fields &alox_LipoReading_msg
|
||||||
|
#define alox_BatterySample_fields &alox_BatterySample_msg
|
||||||
|
#define alox_BatteryStatusResponse_fields &alox_BatteryStatusResponse_msg
|
||||||
|
#define alox_AccelSample_fields &alox_AccelSample_msg
|
||||||
|
#define alox_TapNotifyRequest_fields &alox_TapNotifyRequest_msg
|
||||||
|
#define alox_TapNotifyResponse_fields &alox_TapNotifyResponse_msg
|
||||||
|
#define alox_TapEvent_fields &alox_TapEvent_msg
|
||||||
|
#define alox_CacheStatusRequest_fields &alox_CacheStatusRequest_msg
|
||||||
|
#define alox_CacheClientAccel_fields &alox_CacheClientAccel_msg
|
||||||
|
#define alox_CacheClientTap_fields &alox_CacheClientTap_msg
|
||||||
|
#define alox_CacheClientStatus_fields &alox_CacheClientStatus_msg
|
||||||
|
#define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg
|
||||||
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
|
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
|
||||||
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
|
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
|
||||||
|
#define alox_EspNowEchoPingRequest_fields &alox_EspNowEchoPingRequest_msg
|
||||||
|
#define alox_EspNowEchoPingResponse_fields &alox_EspNowEchoPingResponse_msg
|
||||||
#define alox_LedRingProgressRequest_fields &alox_LedRingProgressRequest_msg
|
#define alox_LedRingProgressRequest_fields &alox_LedRingProgressRequest_msg
|
||||||
#define alox_LedRingProgressResponse_fields &alox_LedRingProgressResponse_msg
|
#define alox_LedRingProgressResponse_fields &alox_LedRingProgressResponse_msg
|
||||||
#define alox_EspNowFindMeRequest_fields &alox_EspNowFindMeRequest_msg
|
#define alox_EspNowFindMeRequest_fields &alox_EspNowFindMeRequest_msg
|
||||||
#define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg
|
#define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg
|
||||||
#define alox_RestartRequest_fields &alox_RestartRequest_msg
|
#define alox_RestartRequest_fields &alox_RestartRequest_msg
|
||||||
#define alox_RestartResponse_fields &alox_RestartResponse_msg
|
#define alox_RestartResponse_fields &alox_RestartResponse_msg
|
||||||
|
#define alox_SetLogLevelRequest_fields &alox_SetLogLevelRequest_msg
|
||||||
|
#define alox_SetLogLevelResponse_fields &alox_SetLogLevelResponse_msg
|
||||||
#define alox_OtaStartPayload_fields &alox_OtaStartPayload_msg
|
#define alox_OtaStartPayload_fields &alox_OtaStartPayload_msg
|
||||||
#define alox_OtaPayload_fields &alox_OtaPayload_msg
|
#define alox_OtaPayload_fields &alox_OtaPayload_msg
|
||||||
#define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg
|
#define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg
|
||||||
@@ -675,17 +1224,31 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
|
|||||||
/* alox_ClientInfo_size depends on runtime parameters */
|
/* alox_ClientInfo_size depends on runtime parameters */
|
||||||
/* alox_ClientInfoResponse_size depends on runtime parameters */
|
/* alox_ClientInfoResponse_size depends on runtime parameters */
|
||||||
/* alox_ClientInputResponse_size depends on runtime parameters */
|
/* alox_ClientInputResponse_size depends on runtime parameters */
|
||||||
#define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_OtaSlaveProgressResponse_size
|
#define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_CacheStatusResponse_size
|
||||||
#define alox_AccelDeadzoneRequest_size 16
|
#define alox_AccelDeadzoneRequest_size 16
|
||||||
#define alox_AccelDeadzoneResponse_size 20
|
#define alox_AccelDeadzoneResponse_size 20
|
||||||
|
#define alox_AccelSample_size 32
|
||||||
|
#define alox_AccelStreamRequest_size 12
|
||||||
|
#define alox_AccelStreamResponse_size 16
|
||||||
#define alox_Ack_size 0
|
#define alox_Ack_size 0
|
||||||
|
#define alox_BatterySample_size 32
|
||||||
|
#define alox_BatteryStatusRequest_size 8
|
||||||
|
#define alox_BatteryStatusResponse_size 580
|
||||||
|
#define alox_CacheClientAccel_size 26
|
||||||
|
#define alox_CacheClientStatus_size 44
|
||||||
|
#define alox_CacheClientTap_size 8
|
||||||
|
#define alox_CacheStatusRequest_size 0
|
||||||
|
#define alox_CacheStatusResponse_size 736
|
||||||
#define alox_ClientInput_size 22
|
#define alox_ClientInput_size 22
|
||||||
|
#define alox_EspNowEchoPingRequest_size 17
|
||||||
|
#define alox_EspNowEchoPingResponse_size 25
|
||||||
#define alox_EspNowFindMeRequest_size 6
|
#define alox_EspNowFindMeRequest_size 6
|
||||||
#define alox_EspNowFindMeResponse_size 8
|
#define alox_EspNowFindMeResponse_size 8
|
||||||
#define alox_EspNowUnicastTestRequest_size 12
|
#define alox_EspNowUnicastTestRequest_size 12
|
||||||
#define alox_EspNowUnicastTestResponse_size 8
|
#define alox_EspNowUnicastTestResponse_size 8
|
||||||
#define alox_LedRingProgressRequest_size 54
|
#define alox_LedRingProgressRequest_size 64
|
||||||
#define alox_LedRingProgressResponse_size 20
|
#define alox_LedRingProgressResponse_size 32
|
||||||
|
#define alox_LipoReading_size 8
|
||||||
#define alox_OtaEndPayload_size 0
|
#define alox_OtaEndPayload_size 0
|
||||||
#define alox_OtaPayload_size 209
|
#define alox_OtaPayload_size 209
|
||||||
#define alox_OtaSlaveProgressEntry_size 30
|
#define alox_OtaSlaveProgressEntry_size 30
|
||||||
@@ -695,6 +1258,11 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
|
|||||||
#define alox_OtaStatusPayload_size 24
|
#define alox_OtaStatusPayload_size 24
|
||||||
#define alox_RestartRequest_size 6
|
#define alox_RestartRequest_size 6
|
||||||
#define alox_RestartResponse_size 8
|
#define alox_RestartResponse_size 8
|
||||||
|
#define alox_SetLogLevelRequest_size 8
|
||||||
|
#define alox_SetLogLevelResponse_size 8
|
||||||
|
#define alox_TapEvent_size 16
|
||||||
|
#define alox_TapNotifyRequest_size 16
|
||||||
|
#define alox_TapNotifyResponse_size 20
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} /* extern "C" */
|
} /* extern "C" */
|
||||||
|
|||||||
@@ -22,6 +22,17 @@ enum MessageType {
|
|||||||
OTA_SLAVE_PROGRESS = 21;
|
OTA_SLAVE_PROGRESS = 21;
|
||||||
FIND_ME = 22;
|
FIND_ME = 22;
|
||||||
RESTART = 23;
|
RESTART = 23;
|
||||||
|
reserved 24;
|
||||||
|
ACCEL_STREAM = 25;
|
||||||
|
BATTERY_STATUS = 26;
|
||||||
|
TAP_NOTIFY = 27;
|
||||||
|
reserved 28;
|
||||||
|
/** Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
|
||||||
|
CACHE_STATUS = 29;
|
||||||
|
/** Host → master → slave → master: timestamp echo round-trip (latency test). */
|
||||||
|
ESPNOW_ECHO_PING = 30;
|
||||||
|
/** Host → master: get/set ESP-IDF log level for tag "*" (global). */
|
||||||
|
SET_LOG_LEVEL = 31;
|
||||||
}
|
}
|
||||||
|
|
||||||
message UartMessage {
|
message UartMessage {
|
||||||
@@ -48,6 +59,18 @@ message UartMessage {
|
|||||||
EspNowFindMeResponse espnow_find_me_response = 20;
|
EspNowFindMeResponse espnow_find_me_response = 20;
|
||||||
RestartRequest restart_request = 21;
|
RestartRequest restart_request = 21;
|
||||||
RestartResponse restart_response = 22;
|
RestartResponse restart_response = 22;
|
||||||
|
AccelStreamRequest accel_stream_request = 25;
|
||||||
|
AccelStreamResponse accel_stream_response = 26;
|
||||||
|
BatteryStatusRequest battery_status_request = 27;
|
||||||
|
BatteryStatusResponse battery_status_response = 28;
|
||||||
|
TapNotifyRequest tap_notify_request = 29;
|
||||||
|
TapNotifyResponse tap_notify_response = 30;
|
||||||
|
CacheStatusRequest cache_status_request = 33;
|
||||||
|
CacheStatusResponse cache_status_response = 34;
|
||||||
|
EspNowEchoPingRequest espnow_echo_ping_request = 35;
|
||||||
|
EspNowEchoPingResponse espnow_echo_ping_response = 36;
|
||||||
|
SetLogLevelRequest set_log_level_request = 37;
|
||||||
|
SetLogLevelResponse set_log_level_response = 38;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +95,12 @@ message ClientInfo {
|
|||||||
uint32 last_ping = 5;
|
uint32 last_ping = 5;
|
||||||
uint32 last_success_ping = 6;
|
uint32 last_success_ping = 6;
|
||||||
uint32 version = 7;
|
uint32 version = 7;
|
||||||
|
/** Master: ESP-NOW accel stream enabled for this slave. */
|
||||||
|
bool accel_stream_enabled = 8;
|
||||||
|
/** Master: ESP-NOW tap notify flags for this slave. */
|
||||||
|
bool tap_notify_single = 9;
|
||||||
|
bool tap_notify_double = 10;
|
||||||
|
bool tap_notify_triple = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ClientInfoResponse {
|
message ClientInfoResponse {
|
||||||
@@ -106,6 +135,124 @@ message AccelDeadzoneResponse {
|
|||||||
uint32 slaves_updated = 4;
|
uint32 slaves_updated = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
|
||||||
|
// write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
|
||||||
|
message AccelStreamRequest {
|
||||||
|
bool write = 1;
|
||||||
|
bool enable = 2;
|
||||||
|
uint32 client_id = 3;
|
||||||
|
bool all_clients = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message AccelStreamResponse {
|
||||||
|
bool enabled = 1;
|
||||||
|
uint32 client_id = 2;
|
||||||
|
bool success = 3;
|
||||||
|
uint32 slaves_updated = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Host → master: read LiPo ADC voltages (master local and/or slaves via ESP-NOW). */
|
||||||
|
message BatteryStatusRequest {
|
||||||
|
/** 0 = master only; >0 = one slave; ignored when all_clients */
|
||||||
|
uint32 client_id = 1;
|
||||||
|
/** Master (client_id 0) plus every registered slave */
|
||||||
|
bool all_clients = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LipoReading {
|
||||||
|
bool valid = 1;
|
||||||
|
/** Estimated pack voltage in millivolts from ADC */
|
||||||
|
uint32 voltage_mv = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BatterySample {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
LipoReading lipo1 = 2;
|
||||||
|
LipoReading lipo2 = 3;
|
||||||
|
/** Milliseconds since last ESP-NOW battery report from this pod. */
|
||||||
|
uint32 age_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BatteryStatusResponse {
|
||||||
|
bool success = 1;
|
||||||
|
repeated BatterySample samples = 2 [(nanopb).max_count = 17];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy host-side sample shape (dashboard helpers); use CACHE_STATUS on the wire. */
|
||||||
|
message AccelSample {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
bool valid = 2;
|
||||||
|
sint32 x = 3;
|
||||||
|
sint32 y = 4;
|
||||||
|
sint32 z = 5;
|
||||||
|
/** Milliseconds since last ESP-NOW sample from this slave. */
|
||||||
|
uint32 age_ms = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Host → master: enable/disable tap ESP-NOW notify per slave (single/double/triple). */
|
||||||
|
message TapNotifyRequest {
|
||||||
|
bool write = 1;
|
||||||
|
uint32 client_id = 2;
|
||||||
|
bool all_clients = 3;
|
||||||
|
bool single = 4;
|
||||||
|
bool double_tap = 5;
|
||||||
|
bool triple = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message TapNotifyResponse {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
bool success = 2;
|
||||||
|
uint32 slaves_updated = 3;
|
||||||
|
bool single = 4;
|
||||||
|
bool double_tap = 5;
|
||||||
|
bool triple = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TapKind {
|
||||||
|
TAP_NONE = 0;
|
||||||
|
TAP_SINGLE = 1;
|
||||||
|
TAP_DOUBLE = 2;
|
||||||
|
TAP_TRIPLE = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Legacy tap event shape (dashboard helpers); use CACHE_STATUS on the wire. */
|
||||||
|
message TapEvent {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
bool valid = 2;
|
||||||
|
TapKind kind = 3;
|
||||||
|
uint32 age_ms = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Host → master: one-shot read of subscribed cached slave data (no request body). */
|
||||||
|
message CacheStatusRequest {}
|
||||||
|
|
||||||
|
/** Accel slice inside CACHE_STATUS (no client_id — use parent CacheClientStatus). */
|
||||||
|
message CacheClientAccel {
|
||||||
|
bool valid = 1;
|
||||||
|
sint32 x = 2;
|
||||||
|
sint32 y = 3;
|
||||||
|
sint32 z = 4;
|
||||||
|
uint32 age_ms = 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tap slice inside CACHE_STATUS; only present when a pending tap was consumed. */
|
||||||
|
message CacheClientTap {
|
||||||
|
TapKind kind = 1;
|
||||||
|
uint32 age_ms = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** One slave with accel and/or tap notify enabled; only subscribed fields are set. */
|
||||||
|
message CacheClientStatus {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
CacheClientAccel accel = 2;
|
||||||
|
CacheClientTap tap = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message CacheStatusResponse {
|
||||||
|
/** Slaves with accel_stream and/or tap notify; omitted fields are not subscribed. */
|
||||||
|
repeated CacheClientStatus clients = 1 [(nanopb).max_count = 16];
|
||||||
|
}
|
||||||
|
|
||||||
message EspNowUnicastTestRequest {
|
message EspNowUnicastTestRequest {
|
||||||
uint32 client_id = 1;
|
uint32 client_id = 1;
|
||||||
uint32 seq = 2;
|
uint32 seq = 2;
|
||||||
@@ -116,8 +263,24 @@ message EspNowUnicastTestResponse {
|
|||||||
uint32 seq = 2;
|
uint32 seq = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Host → device: LED ring display (progress bar, digit, clear, blink, or find-me).
|
/** Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
|
||||||
// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink full ring, 4=find-me (R/G/B ×3 @ full brightness).
|
message EspNowEchoPingRequest {
|
||||||
|
uint32 client_id = 1;
|
||||||
|
/** Microseconds since Unix epoch (host clock). */
|
||||||
|
uint64 timestamp_us = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message EspNowEchoPingResponse {
|
||||||
|
bool success = 1;
|
||||||
|
uint32 client_id = 2;
|
||||||
|
/** Echoed host timestamp from goTool request. */
|
||||||
|
uint64 timestamp_us = 3;
|
||||||
|
/** esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
|
||||||
|
uint32 esp_rtt_us = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
|
||||||
|
// mode: 0=clear, 1=progress (0–100 %), 2=digit (0–10), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low.
|
||||||
message LedRingProgressRequest {
|
message LedRingProgressRequest {
|
||||||
uint32 mode = 1;
|
uint32 mode = 1;
|
||||||
/** 0–100: fraction of ring LEDs to light (mode=progress) */
|
/** 0–100: fraction of ring LEDs to light (mode=progress) */
|
||||||
@@ -133,6 +296,12 @@ message LedRingProgressRequest {
|
|||||||
uint32 blink_ms = 8;
|
uint32 blink_ms = 8;
|
||||||
/** Number of pulses (mode=blink, default 1) */
|
/** Number of pulses (mode=blink, default 1) */
|
||||||
uint32 blink_count = 9;
|
uint32 blink_count = 9;
|
||||||
|
/** 0 = master ring only; >0 = one slave; ignored when all_clients */
|
||||||
|
uint32 client_id = 10;
|
||||||
|
/** Broadcast to all registered slaves (and optionally master unless slaves_only) */
|
||||||
|
bool all_clients = 11;
|
||||||
|
/** With all_clients: do not change master ring */
|
||||||
|
bool slaves_only = 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
message LedRingProgressResponse {
|
message LedRingProgressResponse {
|
||||||
@@ -140,6 +309,8 @@ message LedRingProgressResponse {
|
|||||||
uint32 mode = 2;
|
uint32 mode = 2;
|
||||||
uint32 progress = 3;
|
uint32 progress = 3;
|
||||||
uint32 digit = 4;
|
uint32 digit = 4;
|
||||||
|
uint32 client_id = 5;
|
||||||
|
uint32 slaves_updated = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */
|
/** Host → master: find-me on local ring (client_id=0) or ESP-NOW unicast to one slave. */
|
||||||
@@ -162,6 +333,18 @@ message RestartResponse {
|
|||||||
uint32 client_id = 2;
|
uint32 client_id = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Host → master: read/write global log level (esp_log_level_set("*", …)). */
|
||||||
|
message SetLogLevelRequest {
|
||||||
|
bool write = 1;
|
||||||
|
/** esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE */
|
||||||
|
uint32 level = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SetLogLevelResponse {
|
||||||
|
bool success = 1;
|
||||||
|
uint32 level = 2;
|
||||||
|
}
|
||||||
|
|
||||||
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
|
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
|
||||||
message OtaStartPayload {
|
message OtaStartPayload {
|
||||||
uint32 total_size = 1;
|
uint32 total_size = 1;
|
||||||
|
|||||||
+2
-1
@@ -52,7 +52,8 @@ void init_uart(QueueHandle_t cmd_queue) {
|
|||||||
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
|
||||||
};
|
};
|
||||||
|
|
||||||
err = uart_driver_install(UART_NUM, UART_BUF_SIZE * 2, UART_BUF_SIZE, 0, NULL, 0);
|
err = uart_driver_install(UART_NUM, UART_DRIVER_RX_BUF_SIZE,
|
||||||
|
UART_DRIVER_TX_BUF_SIZE, 0, NULL, 0);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
|
|||||||
+10
-3
@@ -9,10 +9,17 @@
|
|||||||
|
|
||||||
#define UART_NUM UART_NUM_1
|
#define UART_NUM UART_NUM_1
|
||||||
#define UART_BAUD_RATE 921600
|
#define UART_BAUD_RATE 921600
|
||||||
#define UART_TXD_PIN 3
|
// #define UART_TXD_PIN 3
|
||||||
#define UART_RXD_PIN 2
|
// #define UART_RXD_PIN 2
|
||||||
|
|
||||||
#define UART_BUF_SIZE 2048
|
#define UART_TXD_PIN 2
|
||||||
|
#define UART_RXD_PIN 3
|
||||||
|
|
||||||
|
|
||||||
|
#define UART_BUF_SIZE 4096
|
||||||
|
/** Driver RX ring — must hold a full OTA block burst (~20 × ~215 B frames). */
|
||||||
|
#define UART_DRIVER_RX_BUF_SIZE 16384
|
||||||
|
#define UART_DRIVER_TX_BUF_SIZE 4096
|
||||||
#define START_MARKER 0xAA
|
#define START_MARKER 0xAA
|
||||||
#define STOP_MARKER 0xCC
|
#define STOP_MARKER 0xCC
|
||||||
#define MAX_BUF_SIZE 252
|
#define MAX_BUF_SIZE 252
|
||||||
|
|||||||
Reference in New Issue
Block a user