Compare commits

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-23 18:22:52 +02:00
skruecken be18b758ed Added LIPO 2 Reading 2026-06-23 17:54:18 +02:00
skrueckenandCursor 490e0ee61f Add UART SET_LOG_LEVEL for runtime master ESP-IDF logging.
Expose the command via goTool CLI/REST and dashboard controls so log verbosity can be tuned without reflashing.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 18:03:34 +02:00
skruecken ac223ada72 Added ECHO cmd and fixed some timing issues 2026-06-06 17:44:40 +02:00
skruecken 35ce1476d8 Missed for Previous Commit 2026-06-06 17:17:42 +02:00
skruecken f89ea3cbe3 Stabelized OTA Update with Retries 2026-06-06 17:15:55 +02:00
skruecken 99956e3362 Update InputClientSample struct to make TapKind optional and improve WebSocket API documentation. Adjust README to reflect changes in input push stream format and clarify connection flow. Enhance API_WEBSOCKET.md with detailed command and push message descriptions. 2026-06-06 12:26:31 +02:00
58 changed files with 2571 additions and 411 deletions
+2 -2
View File
@@ -205,9 +205,9 @@ sequenceDiagram
```
**Beispiel-Implementierung:** `main/cmd/cmd_accel_deadzone.c`
**Weitere Bridge-Handler:** `cmd_accel_stream.c`, `cmd_tap_notify.c`, `cmd_led_ring.c`, `cmd_espnow_find_me.c`, `cmd_restart.c`, `cmd_espnow_unicast_test.c`, `cmd/cmd_ota.c` (OTA + `ota_espnow.c`).
**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`.
**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`.
---
+90 -12
View File
@@ -52,9 +52,11 @@ Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves
| BMA456 Interrupt | 10 | `bosch456.c` |
| Taster | 12 | `board_input.c` |
| LiPo ADC 1 | 1 | `board_input.c` |
| LiPo ADC 2 | 12 | Entfällt wenn = Taster-GPIO |
| LiPo ADC 2 | 11 | `board_input.c` |
**UART:** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control.
**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.
@@ -89,15 +91,17 @@ idf.py -p /dev/ttyUSB0 flash monitor
### 4.1 Initialisierungsreihenfolge (`powerpod.c`)
1. `pod_settings_init()` — NVS
2. DIP → `app_config.master`
3. I2C + IO-Expander → `app_config.network` (18)
4. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
5. `pod_settings_apply_accel_deadzone()` wenn Sensor da
6. OTA-Partition → `app_config.running_partition`
7. `board_input_init()`
8. `esp_now_comm_init(&app_config)`**immer** (Master + Slave)
9. `led_ring_init()`
10. **Nur Master:** `cmd_queue``init_cmdHandler``init_uart` → alle `cmd_*_register()`
2. `board_input_init_adc_only()` — LiPo-ADC
3. Bei `POWERPOD_BATTERY_UV_ENABLE`: `battery_uv_evaluate_boot()` → ggf. Energiesparmodus (nur LED + ADC, **kein** weiterer Init)
4. DIP → `app_config.master`
5. I2C + IO-Expander → `app_config.network` (18)
6. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
7. `pod_settings_apply_accel_deadzone()` wenn Sensor da
8. OTA-Partition → `app_config.running_partition`
9. `board_input_start_lipo_monitor()` + `board_input_init_button()`
10. `esp_now_comm_init(&app_config)`**normaler Pfad** (Master + Slave)
11. `led_ring_init()`
12. **Nur Master:** `cmd_queue``init_cmdHandler``init_uart` → alle `cmd_*_register()`
### 4.2 Master vs. Slave
@@ -181,6 +185,8 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz
| `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`)
@@ -193,6 +199,7 @@ UART_CMD_REQ(&uart_msg, alox_UartMessage_accel_deadzone_request_tag, accel_deadz
| 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`
@@ -229,6 +236,8 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
| 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)
@@ -287,6 +296,53 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
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
@@ -343,6 +399,7 @@ Ohne Sensor: `bma456_is_ready() == false`, Firmware läuft weiter.
- Segment-Karten für Ziffern 010 in `digit_lookup[]`.
- **Kein** lokaler Demo-Loop — Anzeige nur über UART (`cmd_led_ring.c`) oder ESP-NOW `ESPNOW_LED_RING`.
- Helligkeit: `intensity` 0 → Default ~5 % (`LED_RING_DEFAULT_INTENSITY`).
- Modi: `0` clear … `5` color, `6` **battery-low** (erste 4 LEDs rot ~10 %, 5 s).
---
@@ -351,12 +408,26 @@ Ohne Sensor: `bma456_is_ready() == false`, Firmware läuft weiter.
`board_input.c`:
- **Taster** GPIO 12 — Logging bei Druck.
- **LiPo-ADC** GPIO 1 (und optional 2, wenn nicht Taster).
- **LiPo-ADC** GPIO 1 und GPIO 11 (Pin-Spannung in mV).
- Master: `master_monitor_task` aktualisiert Master-Batterie alle 30 s.
- Slave: `slave_send_battery_report_to_master` nach Join, Heartbeat und Query.
Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART.
### Software-UV-Schutz (`battery_uv.c`)
Aktivierbar mit `POWERPOD_BATTERY_UV_ENABLE` in `powerpod.h` (CMake-Override möglich).
| Zustand | ADC-Schwelle | Logik |
|---------|--------------|-------|
| UV | &lt; 2300 mV | Ein gültiger Kanal reicht |
| Geladen | ≥ 3000 mV | Alle gültigen Kanäle |
- NVS-Key `uv_latch` in `pod_settings.c`
- Boot: ADC früh, bei UV nur LED + ADC-Poll (kein ESP-NOW/WiFi/UART/I2C/Button)
- Runtime: `battery_uv_poll()` im LiPo-Monitor (2 Samples Debounce)
- LED-Anzeige: Mode `6` battery-low (`led_ring_show_battery_low`)
---
## 12. Persistenz (NVS)
@@ -366,6 +437,7 @@ Spannungen in Millivolt in Registry und `BATTERY_STATUS`-UART.
| 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()`.
@@ -427,6 +499,8 @@ Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `m
| `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 |
@@ -437,6 +511,7 @@ Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `m
| `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 |
@@ -455,9 +530,12 @@ Includes in generierten `.pb.c` müssen `"uart_messages.pb.h"` heißen (nicht `m
## 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 |
+1 -1
View File
@@ -345,7 +345,7 @@ idf.py build
Ä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`
- **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`
+2
View File
@@ -0,0 +1,2 @@
win:
GOOS=windows GOARCH=386 go build .
+20 -2
View File
@@ -28,13 +28,15 @@ go run . -port /dev/ttyUSB0 clients
| `tap-notify` | `0x1b` | Get/set which tap kinds (single/double/triple) notify via ESP-NOW (`-set`, `-client`, `-all`, `-single`, `-double`, `-triple`) |
| `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`) |
| `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/`) |
| `serve` | — | Web dashboard at `http://localhost:8080` (WebSocket live updates) |
| `ota` | 1619 | UART firmware upload to master; firmware then pushes to slaves via ESP-NOW |
| `ota-progress` | 21 | Query per-slave ESP-NOW OTA progress on the master (`-client N`, default all) |
| `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me`, `-client`, `-all` |
| `led-ring` | 8 | LED ring: `-mode clear\|color\|progress\|digit\|blink\|find-me\|battery-low`, `-client`, `-all` |
| `find-me` | 22 | Locate pod (`-client 0` master, `>0` slave via ESP-NOW) |
| `restart` | 23 | Reboot master or slave (`-client 0` / `>0`) |
| `log-level` | `0x1f` | Get/set master ESP-IDF log level for tag `"*"` (`-set`, `-level` 05); output on UART0 debug, not host UART |
`clients` requires slaves to have responded to master discover broadcasts first.
@@ -86,7 +88,7 @@ If the UART device is unplugged or the port disappears, `serve` keeps running an
| Doc | Content |
|-----|---------|
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands, **`accel` / `tap` push stream** format, dashboard `ws://…:8080/ws` |
| **[docs/API_WEBSOCKET.md](docs/API_WEBSOCKET.md)** | `ws://…:8081/ws` commands and **`input` push stream** (accel + tap) |
| **[docs/API_REST.md](docs/API_REST.md)** | REST on `:8080` (dashboard) and `:8081` (battery, LED, service info) |
CLI:
@@ -113,8 +115,24 @@ go run . -port /dev/ttyUSB0 unicast-test -client 16 -seq 42
On success the slave serial log should show `UNICAST TEST OK from master … seq=42`.
```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:
```
echo ping: success=true client_id=16 rtt_ms=49.729 esp_rtt_us=18234
```
`clients` example:
```
clients (2):
[0] id=42 mac=aabbccddeeff ver=1 available=true used=false last_ping=250 last_success_ping=250
+102
View File
@@ -41,6 +41,19 @@ type unicastAPIResponse struct {
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 {
ClientID uint32 `json:"client_id"`
}
@@ -61,6 +74,17 @@ type restartAPIResponse struct {
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 {
Success bool `json:"success"`
BytesWritten uint32 `json:"bytes_written,omitempty"`
@@ -91,6 +115,13 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
}
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) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -105,6 +136,16 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
}
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) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
@@ -260,6 +301,43 @@ func applyDeadzoneToSlaves(link *managedSerial, deadzone uint32) (uint32, error)
return updated, nil
}
func serveLogLevelGet(w http.ResponseWriter, r *http.Request, link *managedSerial) {
resp, err := link.SetLogLevel(false, 0)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, logLevelAPIResponse{
Success: resp.GetSuccess(),
Level: resp.GetLevel(),
})
}
func serveLogLevelPost(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body logLevelAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "invalid JSON"})
return
}
if !body.Write {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "write must be true"})
return
}
if body.Level > 5 {
writeJSON(w, http.StatusBadRequest, logLevelAPIResponse{Error: "level must be 05"})
return
}
resp, err := link.SetLogLevel(true, body.Level)
if err != nil {
writeJSON(w, http.StatusServiceUnavailable, logLevelAPIResponse{Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, logLevelAPIResponse{
Success: resp.GetSuccess(),
Level: resp.GetLevel(),
})
}
func serveRestart(w http.ResponseWriter, r *http.Request, link *managedSerial) {
var body restartAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
@@ -316,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) {
s := r.URL.Query().Get(key)
if s == "" {
+1 -2
View File
@@ -31,7 +31,7 @@ type InputClientSample struct {
Y int32 `json:"y,omitempty"`
Z int32 `json:"z,omitempty"`
AccelAgeMs uint32 `json:"accel_age_ms,omitempty"`
TapKind string `json:"tap_kind"`
TapKind string `json:"tap_kind,omitempty"`
TapAgeMs uint32 `json:"tap_age_ms,omitempty"`
}
@@ -389,7 +389,6 @@ func (h *accelStreamHub) inputClientsFromCacheLocked(cache *pb.CacheStatusRespon
for _, c := range cache.GetClients() {
sample := InputClientSample{
ClientID: c.GetClientId(),
TapKind: "none",
}
if a := c.GetAccel(); a != nil {
sample.Valid = a.GetValid()
+2
View File
@@ -347,6 +347,8 @@ func ledRingModeValue(mode string) (uint32, error) {
return 3, nil
case "find_me", "findme":
return 4, nil
case "battery_low", "batterylow":
return 6, nil
case "color", "solid", "fill":
return 5, nil
default:
+1
View File
@@ -12,6 +12,7 @@ func TestLedRingModeValue(t *testing.T) {
{"digit", 2},
{"blink", 3},
{"find-me", 4},
{"battery-low", 6},
}
for _, tc := range tests {
got, err := ledRingModeValue(tc.mode)
+85
View File
@@ -2,6 +2,7 @@ package main
import (
"fmt"
"time"
"google.golang.org/protobuf/proto"
@@ -410,12 +411,82 @@ func (s *serialPort) espnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
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 {
return m.withPort(func(sp *serialPort) error {
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 {
err := m.withPort(func(sp *serialPort) error {
return runRestartClient(sp, clientID)
@@ -490,6 +561,20 @@ func (s *serialPort) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastT
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) {
return s.ledRingProgress(req)
}
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"flag"
"fmt"
)
func runEchoPing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("echo-ping", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id from `clients`")
if err := fs.Parse(args); err != nil {
return err
}
if *clientID == 0 {
return fmt.Errorf("client id required (see `gotool clients`)")
}
r, err := sp.echoPing(uint32(*clientID))
if err != nil {
return err
}
fmt.Printf("echo ping: success=%v client_id=%d rtt_ms=%.3f esp_rtt_us=%d\n",
r.Success, r.ClientID, r.RttMs, r.EspRttUs)
return nil
}
+1 -1
View File
@@ -9,7 +9,7 @@ import (
func runLedRing(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("led-ring", flag.ExitOnError)
mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, or find-me")
mode := fs.String("mode", "progress", "clear, color, progress, digit, blink, find-me, or battery-low")
clientID := fs.Uint("client", 0, "0=master ring, >0=slave via ESP-NOW")
allClients := fs.Bool("all", false, "broadcast to all slaves")
slavesOnly := fs.Bool("slaves-only", false, "with -all: do not change master ring")
+61
View File
@@ -0,0 +1,61 @@
package main
import (
"flag"
"fmt"
"google.golang.org/protobuf/proto"
"powerpod/gotool/pb"
)
func runLogLevel(sp *serialPort, args []string) error {
fs := flag.NewFlagSet("log-level", flag.ExitOnError)
write := fs.Bool("set", false, "write log level (default: read)")
level := fs.Uint("level", 0, "esp_log_level_t 05 (with -set)")
if err := fs.Parse(args); err != nil {
return err
}
resp, err := sp.setLogLevel(*write, uint32(*level))
if err != nil {
return err
}
if !resp.GetSuccess() {
return fmt.Errorf("set_log_level rejected (level=%d)", resp.GetLevel())
}
fmt.Printf("log_level=%d success=%v\n", resp.GetLevel(), resp.GetSuccess())
return nil
}
func runSetLogLevelClient(sp *serialPort, write bool, level uint32) (*pb.SetLogLevelResponse, error) {
return sp.setLogLevel(write, level)
}
func (s *serialPort) setLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_SET_LOG_LEVEL,
Payload: &pb.UartMessage_SetLogLevelRequest{
SetLogLevelRequest: &pb.SetLogLevelRequest{
Write: write,
Level: level,
},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_SET_LOG_LEVEL)}, body...)
respPayload, err := s.exchangePayload(payload, "SET_LOG_LEVEL")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetSetLogLevelResponse()
if r == nil {
return nil, fmt.Errorf("missing set_log_level_response")
}
return r, nil
}
+1 -1
View File
@@ -633,7 +633,7 @@ func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, s
case <-stop:
return
case <-ticker.C:
if hub.clientCount() == 0 {
if !link.IsConnected() {
continue
}
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
+84
View File
@@ -96,6 +96,7 @@ Body:
| `digit` | `digit` 010 |
| `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.
@@ -216,6 +217,54 @@ 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
@@ -234,6 +283,39 @@ 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
@@ -280,5 +362,7 @@ Same as external API:
| 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 |
+70 -90
View File
@@ -1,58 +1,59 @@
# WebSocket API
`go run . -port /dev/ttyUSB0 serve` exposes the WebSocket enpoint
External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string).
| URL | Port (default) | Role |
|-----|----------------|------|
| `ws://localhost:8081/ws` | External API (`-api-addr`) | Request/response commands + optional **input** push stream |
Start with `go run . -port /dev/ttyUSB0 serve`.
---
## External API (`:8081/ws`)
## Connection flow
### Connection flow
1. Connect → server sends `hello` (push off; defaults and command list).
2. Send JSON commands → one reply per message (`*_status` or `client_list`).
3. After `set_stream` with `enable: true`, server may push `input` messages without a prior command.
1. Connect → server sends **`hello`** (receive off; lists available commands).
2. Send JSON commands → server replies with a matching `*_status` or `client_list` message (one reply per command).
3. After `set_stream` with `enable: true`, the server may send **`input`** messages **without** a prior command (push stream).
Commands and pushes share one socket — always branch on `type`.
Commands and stream pushes are multiplexed on one socket. While streaming, always parse `type` and branch (status vs sample vs error).
On disconnect, `set_stream` state for that socket is dropped. Firmware settings (`set_input_stream`, `set_tap_notify`) stay on the master until changed.
---
## Two layers (firmware vs host)
### Two layers (firmware vs host)
| Layer | Commands | Effect |
|-------|----------|--------|
| **Firmware (ESP-NOW)** | `set_input_stream`, `set_tap_notify` | Per `client_id`: slave sends accel samples and/or tap events to the master |
| **This connection (host)** | `set_stream` | Whether **you** receive push JSON, at what rate (`interval_ms`, 1 ms … 10 s), and how early the UART read starts (`pre_fetch`) |
| ------------------ | ------------------------------------ | ------------------------------------------------------------------ |
| 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 if at least one connection has `receive_input: true` (`set_stream`) **and** at least one slave streams input (`set_input_stream`) or has tap notify enabled (`set_tap_notify`).
- **`set_tap_notify` alone** configures which tap kinds the slave reports; it does **not** enable host push by itself — you still need `set_stream`.
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` as needed
2. Per slave: `set_input_stream` and/or `set_tap_notify`
3. `set_stream` with `"enable": true`
4. Read **`input`** messages in a loop
There is **no per-slave filter** on push messages: each `input` contains all cached slaves. Filter by `client_id` in your app.
4. Read `input` messages; filter by `client_id` in your app (no per-slave filter on the wire)
---
## Push stream messages
## Push: `input`
These are the samples you get after enabling receive. Timing is per WebSocket connection:
Combines latest accel cache and visible tap state for every slave slot on the master.
- **`interval_ms`** — minimum time between consecutive `input` pushes on this socket.
- **`pre_fetch`** — milliseconds **before** each scheduled push when the host sends the UART cache read, so the master has time to collect data from all slaves before the JSON goes out.
The server UART poll uses the **minimum** `interval_ms` among all subscribers with `receive_input: true`.
### `input` (type `"input"`)
Sent when `set_stream` has `enable: true` and the poll tick fires for this connection (after the UART read started `pre_fetch` ms earlier). Each message combines the latest accel cache and visible tap state for every slave slot on the master.
**Success** — all slaves with a cache entry (not only those with `valid: true`):
**Success:**
```json
{
@@ -72,47 +73,43 @@ Sent when `set_stream` has `enable: true` and the poll tick fires for this conne
},
{
"client_id": 42,
"valid": false,
"tap_kind": "none"
"valid": false
}
]
}
```
| Field | Meaning |
|-------|---------|
| `t` | Unix timestamp in **nanoseconds** when the host read the cache |
| -------------- | -------------------------------------------------------------------- |
| `t` | Unix timestamp in nanoseconds when the host read the cache |
| `success` | `true` if `CACHE_STATUS` succeeded |
| `clients[]` | One entry per slave slot in the master cache |
| `client_id` | ESP-NOW client id (same as `list_clients`) |
| `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 scale on the pod) |
| `accel_age_ms` | Milliseconds since the master received this accel sample |
| `tap_kind` | `"none"`, `"single"`, `"double"`, or `"triple"` |
| `tap_age_ms` | Milliseconds since the tap was seen in the master cache; omit when `tap_kind` is `"none"` |
| `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 ms, also in `hello`) after the API first saw them, even if the hardware age grows.
**Failure** (e.g. UART busy):
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"
}
{"type":"input","t":1716900123456789012,"success":false,"error":"uart busy"}
```
No `clients` array on failure.
---
## Commands (request → response)
## Commands
Send one JSON object per message. Field `type` selects the command.
One JSON object per message; field `type` selects the command.
### `hello` (server → client, on connect)
**Errors:** Replies use the matching response `type`. On failure: `success: false` (or omitted) and `"error": "…"`. Malformed JSON or unknown `type``stream_status` with `error`.
### `hello` (server → client)
```json
{
@@ -121,7 +118,6 @@ Send one JSON object per message. Field `type` selects the command.
"interval_ms": 16,
"pre_fetch_ms": 2,
"tap_display_min_ms": 2000,
"note": "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection",
"commands": [
"list_clients",
"set_stream", "get_stream",
@@ -146,11 +142,7 @@ Response `client_list`:
{
"id": 16,
"mac": "aa:bb:cc:dd:ee:10",
"version": 1,
"available": true,
"used": true,
"last_ping": 1234,
"last_success_ping": 1200,
"input_stream": false,
"tap_notify_single": false,
"tap_notify_double": false,
@@ -160,28 +152,24 @@ Response `client_list`:
}
```
### `set_stream` / `get_stream` (receive input on this connection)
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"}
```
| Field | Meaning |
|-------|---------|
| `enable` | Turn push stream on/off for this connection |
| `interval_ms` | Minimum time between `input` pushes (1 … 10000) |
| `pre_fetch` | Milliseconds before each push when the host starts the UART cache read; optional, default in `hello` (`pre_fetch_ms`) |
Response `stream_status`:
```json
{"type":"stream_status","receive_input":true,"interval_ms":32,"pre_fetch":2,"success":true}
```
### `set_input_stream` / `get_input_stream` (firmware, per slave)
### `set_input_stream` / `get_input_stream` (firmware)
`client_id` required (> 0). Enables accel streaming from the slave to the master.
`client_id` required (> 0).
```json
{"type":"set_input_stream","client_id":16,"enable":true}
@@ -194,51 +182,44 @@ Response `input_stream_status`:
{"type":"input_stream_status","client_id":16,"enabled":true,"success":true}
```
### `set_tap_notify` / `get_tap_notify` (firmware, per slave)
### `set_tap_notify` / `get_tap_notify` (firmware)
Per client: `single`, `double_tap`, `triple` required on set.
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}
```
Broadcast: `"all_clients": true` with the three booleans.
Response `tap_notify_status`:
```json
{
"type": "tap_notify_status",
"client_id": 16,
"success": true,
"single": true,
"double_tap": false,
"triple": false
}
{"type":"tap_notify_status","client_id":16,"success":true,"single":true,"double_tap":false,"triple":false}
```
### `set_led_ring`
Control the LED ring on the master or a slave.
```json
{"type":"set_led_ring","mode":"color","client_id":16,"r":255,"g":0,"b":0,"intensity":128}
{"type":"set_led_ring","mode":"digit","client_id":0,"digit":3,"r":0,"g":255,"b":0}
{"type":"set_led_ring","mode":"find-me","all_clients":true,"slaves_only":true}
```
| `mode` | Notes |
|--------|--------|
| Request `mode` | Notes |
| -------------- | --------------------------- |
| `clear` | Turn off |
| `color` | Full ring RGB + `intensity` |
| `progress` | `progress` 0100 |
| `digit` | `digit` 010 |
| `blink` | `blink_ms`, `blink_count` |
| `find-me` | Locate pod |
| `battery-low` | UV indicator (4 LEDs, 5 s) |
Use `client_id` (`0` = master) or `all_clients` (+ optional `slaves_only`) for broadcast.
Response `led_ring_status`:
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}
@@ -246,15 +227,13 @@ Response `led_ring_status`:
### `get_battery`
Read cached battery samples from the master. Slaves push battery every **30 s**; this command reads the master cache.
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}
```
Default if omitted: all clients.
Response `battery_status`:
```json
@@ -271,3 +250,4 @@ Response `battery_status`:
]
}
```
+4 -1
View File
@@ -14,6 +14,7 @@ const (
ledRingModeBlink = 3
ledRingModeFindMe = 4
ledRingModeColor = 5
ledRingModeBatteryLow = 6
)
type ledRingAPIRequest struct {
@@ -56,8 +57,10 @@ func ledRingModeFromString(s string) (uint32, error) {
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)", s)
return 0, fmt.Errorf("unknown mode %q (clear, color, progress, digit, blink, find-me, battery-low)", s)
}
}
+8 -2
View File
@@ -19,13 +19,15 @@ func usage() {
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
fmt.Fprintf(os.Stderr, " 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, " 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, " serve web dashboard (Bootstrap + WebSocket)\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, " led-ring set LED ring progress bar (0100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
fmt.Fprintf(os.Stderr, " 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()
}
@@ -52,7 +54,7 @@ func main() {
os.Exit(2)
}
runErr = runServe(*portName, *baud, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "led-ring", "led_ring", "find-me", "find_me", "restart", "ota", "ota-progress", "ota_progress":
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-notify", "tap_notify", "cache-status", "cache_status", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
if *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage()
@@ -78,12 +80,16 @@ func main() {
runErr = runCacheStatus(sp)
case "unicast-test", "unicast_test":
runErr = runUnicastTest(sp, flag.Args()[1:])
case "echo-ping", "echo_ping":
runErr = runEchoPing(sp, flag.Args()[1:])
case "led-ring", "led_ring":
runErr = runLedRing(sp, flag.Args()[1:])
case "find-me", "find_me":
runErr = runFindMe(sp, flag.Args()[1:])
case "restart":
runErr = runRestart(sp, flag.Args()[1:])
case "log-level", "log_level":
runErr = runLogLevel(sp, flag.Args()[1:])
case "ota":
runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress":
+40 -6
View File
@@ -20,6 +20,9 @@ const (
otaDistQueryInterval = 500 * time.Millisecond
otaDistQueryTimeout = 2 * time.Second
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 (
@@ -189,6 +192,10 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
var seq uint32
for offset := 0; offset < imageSize; {
blockStart := offset
blockStartSeq := seq
sendBlock := func() (fullBlock bool, err error) {
bytesInBlock := 0
for bytesInBlock < otaFlashBlockSize && offset < imageSize {
n := otaHostChunkSize
@@ -207,9 +214,9 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
OtaPayload: &pb.OtaPayload{Seq: seq, Data: chunk},
},
}); err != nil {
notify("error", "", 0, err.Error())
return err
return false, err
}
time.Sleep(otaHostChunkPace)
seq++
offset += n
bytesInBlock += n
@@ -218,15 +225,43 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
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 {
st, err := waitOtaStatus(sp, otaStBlockAck, otaDefaultTimeout, nil)
fullBlock, err := sendBlock()
if err != nil {
notify("error", "", 0, err.Error())
return err
}
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
@@ -235,7 +270,6 @@ func runOTAOnPortUnlocked(sp *serialPort, firmware []byte, onProgress otaProgres
fmt.Sprintf("Master: Block geschrieben (%d bytes)", st.GetBytesWritten()),
OTAProgress{Bytes: st.GetBytesWritten()})
}
}
masterPct = 100
masterMsg = "Master: UART-Upload abgeschlossen"
+424 -89
View File
@@ -46,6 +46,10 @@ const (
MessageType_TAP_NOTIFY MessageType = 27
// * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence).
MessageType_CACHE_STATUS MessageType = 29
// * Host → master → slave → master: timestamp echo round-trip (latency test).
MessageType_ESPNOW_ECHO_PING MessageType = 30
// * Host → master: get/set ESP-IDF log level for tag "*" (global).
MessageType_SET_LOG_LEVEL MessageType = 31
)
// Enum value maps for MessageType.
@@ -72,6 +76,8 @@ var (
26: "BATTERY_STATUS",
27: "TAP_NOTIFY",
29: "CACHE_STATUS",
30: "ESPNOW_ECHO_PING",
31: "SET_LOG_LEVEL",
}
MessageType_value = map[string]int32{
"UNKNOWN": 0,
@@ -95,6 +101,8 @@ var (
"BATTERY_STATUS": 26,
"TAP_NOTIFY": 27,
"CACHE_STATUS": 29,
"ESPNOW_ECHO_PING": 30,
"SET_LOG_LEVEL": 31,
}
)
@@ -211,6 +219,10 @@ type UartMessage struct {
// *UartMessage_TapNotifyResponse
// *UartMessage_CacheStatusRequest
// *UartMessage_CacheStatusResponse
// *UartMessage_EspnowEchoPingRequest
// *UartMessage_EspnowEchoPingResponse
// *UartMessage_SetLogLevelRequest
// *UartMessage_SetLogLevelResponse
Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
@@ -521,6 +533,42 @@ func (x *UartMessage) GetCacheStatusResponse() *CacheStatusResponse {
return nil
}
func (x *UartMessage) GetEspnowEchoPingRequest() *EspNowEchoPingRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowEchoPingRequest); ok {
return x.EspnowEchoPingRequest
}
}
return nil
}
func (x *UartMessage) GetEspnowEchoPingResponse() *EspNowEchoPingResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_EspnowEchoPingResponse); ok {
return x.EspnowEchoPingResponse
}
}
return nil
}
func (x *UartMessage) GetSetLogLevelRequest() *SetLogLevelRequest {
if x != nil {
if x, ok := x.Payload.(*UartMessage_SetLogLevelRequest); ok {
return x.SetLogLevelRequest
}
}
return nil
}
func (x *UartMessage) GetSetLogLevelResponse() *SetLogLevelResponse {
if x != nil {
if x, ok := x.Payload.(*UartMessage_SetLogLevelResponse); ok {
return x.SetLogLevelResponse
}
}
return nil
}
type isUartMessage_Payload interface {
isUartMessage_Payload()
}
@@ -641,6 +689,22 @@ type UartMessage_CacheStatusResponse struct {
CacheStatusResponse *CacheStatusResponse `protobuf:"bytes,34,opt,name=cache_status_response,json=cacheStatusResponse,proto3,oneof"`
}
type UartMessage_EspnowEchoPingRequest struct {
EspnowEchoPingRequest *EspNowEchoPingRequest `protobuf:"bytes,35,opt,name=espnow_echo_ping_request,json=espnowEchoPingRequest,proto3,oneof"`
}
type UartMessage_EspnowEchoPingResponse struct {
EspnowEchoPingResponse *EspNowEchoPingResponse `protobuf:"bytes,36,opt,name=espnow_echo_ping_response,json=espnowEchoPingResponse,proto3,oneof"`
}
type UartMessage_SetLogLevelRequest struct {
SetLogLevelRequest *SetLogLevelRequest `protobuf:"bytes,37,opt,name=set_log_level_request,json=setLogLevelRequest,proto3,oneof"`
}
type UartMessage_SetLogLevelResponse struct {
SetLogLevelResponse *SetLogLevelResponse `protobuf:"bytes,38,opt,name=set_log_level_response,json=setLogLevelResponse,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@@ -699,6 +763,14 @@ func (*UartMessage_CacheStatusRequest) isUartMessage_Payload() {}
func (*UartMessage_CacheStatusResponse) isUartMessage_Payload() {}
func (*UartMessage_EspnowEchoPingRequest) isUartMessage_Payload() {}
func (*UartMessage_EspnowEchoPingResponse) isUartMessage_Payload() {}
func (*UartMessage_SetLogLevelRequest) isUartMessage_Payload() {}
func (*UartMessage_SetLogLevelResponse) isUartMessage_Payload() {}
type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
@@ -2329,8 +2401,132 @@ func (x *EspNowUnicastTestResponse) GetSeq() uint32 {
return 0
}
// * Host → master: ESP-NOW echo ping to one slave (timestamp echoed back).
type EspNowEchoPingRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
// * Microseconds since Unix epoch (host clock).
TimestampUs uint64 `protobuf:"varint,2,opt,name=timestamp_us,json=timestampUs,proto3" json:"timestamp_us,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowEchoPingRequest) Reset() {
*x = EspNowEchoPingRequest{}
mi := &file_uart_messages_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowEchoPingRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowEchoPingRequest) ProtoMessage() {}
func (x *EspNowEchoPingRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[27]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowEchoPingRequest.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{27}
}
func (x *EspNowEchoPingRequest) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *EspNowEchoPingRequest) GetTimestampUs() uint64 {
if x != nil {
return x.TimestampUs
}
return 0
}
type EspNowEchoPingResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
ClientId uint32 `protobuf:"varint,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
// * Echoed host timestamp from goTool request.
TimestampUs uint64 `protobuf:"varint,3,opt,name=timestamp_us,json=timestampUs,proto3" json:"timestamp_us,omitempty"`
// * esp_timer_get_time() delta from ping send to pong recv (master→slave→master).
EspRttUs uint32 `protobuf:"varint,4,opt,name=esp_rtt_us,json=espRttUs,proto3" json:"esp_rtt_us,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *EspNowEchoPingResponse) Reset() {
*x = EspNowEchoPingResponse{}
mi := &file_uart_messages_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *EspNowEchoPingResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*EspNowEchoPingResponse) ProtoMessage() {}
func (x *EspNowEchoPingResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[28]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use EspNowEchoPingResponse.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{28}
}
func (x *EspNowEchoPingResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *EspNowEchoPingResponse) GetClientId() uint32 {
if x != nil {
return x.ClientId
}
return 0
}
func (x *EspNowEchoPingResponse) GetTimestampUs() uint64 {
if x != nil {
return x.TimestampUs
}
return 0
}
func (x *EspNowEchoPingResponse) GetEspRttUs() uint32 {
if x != nil {
return x.EspRttUs
}
return 0
}
// Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low.
type LedRingProgressRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Mode uint32 `protobuf:"varint,1,opt,name=mode,proto3" json:"mode,omitempty"`
@@ -2359,7 +2555,7 @@ type LedRingProgressRequest struct {
func (x *LedRingProgressRequest) Reset() {
*x = LedRingProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[27]
mi := &file_uart_messages_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2371,7 +2567,7 @@ func (x *LedRingProgressRequest) String() string {
func (*LedRingProgressRequest) ProtoMessage() {}
func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[27]
mi := &file_uart_messages_proto_msgTypes[29]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2384,7 +2580,7 @@ func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressRequest.ProtoReflect.Descriptor instead.
func (*LedRingProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{27}
return file_uart_messages_proto_rawDescGZIP(), []int{29}
}
func (x *LedRingProgressRequest) GetMode() uint32 {
@@ -2485,7 +2681,7 @@ type LedRingProgressResponse struct {
func (x *LedRingProgressResponse) Reset() {
*x = LedRingProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[28]
mi := &file_uart_messages_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2497,7 +2693,7 @@ func (x *LedRingProgressResponse) String() string {
func (*LedRingProgressResponse) ProtoMessage() {}
func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[28]
mi := &file_uart_messages_proto_msgTypes[30]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2510,7 +2706,7 @@ func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressResponse.ProtoReflect.Descriptor instead.
func (*LedRingProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{28}
return file_uart_messages_proto_rawDescGZIP(), []int{30}
}
func (x *LedRingProgressResponse) GetSuccess() bool {
@@ -2565,7 +2761,7 @@ type EspNowFindMeRequest struct {
func (x *EspNowFindMeRequest) Reset() {
*x = EspNowFindMeRequest{}
mi := &file_uart_messages_proto_msgTypes[29]
mi := &file_uart_messages_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2577,7 +2773,7 @@ func (x *EspNowFindMeRequest) String() string {
func (*EspNowFindMeRequest) ProtoMessage() {}
func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[29]
mi := &file_uart_messages_proto_msgTypes[31]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2590,7 +2786,7 @@ func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead.
func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{29}
return file_uart_messages_proto_rawDescGZIP(), []int{31}
}
func (x *EspNowFindMeRequest) GetClientId() uint32 {
@@ -2610,7 +2806,7 @@ type EspNowFindMeResponse struct {
func (x *EspNowFindMeResponse) Reset() {
*x = EspNowFindMeResponse{}
mi := &file_uart_messages_proto_msgTypes[30]
mi := &file_uart_messages_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2622,7 +2818,7 @@ func (x *EspNowFindMeResponse) String() string {
func (*EspNowFindMeResponse) ProtoMessage() {}
func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[30]
mi := &file_uart_messages_proto_msgTypes[32]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2635,7 +2831,7 @@ func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead.
func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{30}
return file_uart_messages_proto_rawDescGZIP(), []int{32}
}
func (x *EspNowFindMeResponse) GetSuccess() bool {
@@ -2662,7 +2858,7 @@ type RestartRequest struct {
func (x *RestartRequest) Reset() {
*x = RestartRequest{}
mi := &file_uart_messages_proto_msgTypes[31]
mi := &file_uart_messages_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2674,7 +2870,7 @@ func (x *RestartRequest) String() string {
func (*RestartRequest) ProtoMessage() {}
func (x *RestartRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[31]
mi := &file_uart_messages_proto_msgTypes[33]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2687,7 +2883,7 @@ func (x *RestartRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartRequest.ProtoReflect.Descriptor instead.
func (*RestartRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{31}
return file_uart_messages_proto_rawDescGZIP(), []int{33}
}
func (x *RestartRequest) GetClientId() uint32 {
@@ -2707,7 +2903,7 @@ type RestartResponse struct {
func (x *RestartResponse) Reset() {
*x = RestartResponse{}
mi := &file_uart_messages_proto_msgTypes[32]
mi := &file_uart_messages_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2719,7 +2915,7 @@ func (x *RestartResponse) String() string {
func (*RestartResponse) ProtoMessage() {}
func (x *RestartResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[32]
mi := &file_uart_messages_proto_msgTypes[34]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2732,7 +2928,7 @@ func (x *RestartResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartResponse.ProtoReflect.Descriptor instead.
func (*RestartResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{32}
return file_uart_messages_proto_rawDescGZIP(), []int{34}
}
func (x *RestartResponse) GetSuccess() bool {
@@ -2749,6 +2945,112 @@ func (x *RestartResponse) GetClientId() uint32 {
return 0
}
// * Host → master: read/write global log level (esp_log_level_set("*", …)).
type SetLogLevelRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
Write bool `protobuf:"varint,1,opt,name=write,proto3" json:"write,omitempty"`
// * esp_log_level_t: 0=NONE, 1=ERROR, 2=WARN, 3=INFO, 4=DEBUG, 5=VERBOSE
Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetLogLevelRequest) Reset() {
*x = SetLogLevelRequest{}
mi := &file_uart_messages_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetLogLevelRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetLogLevelRequest) ProtoMessage() {}
func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[35]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead.
func (*SetLogLevelRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{35}
}
func (x *SetLogLevelRequest) GetWrite() bool {
if x != nil {
return x.Write
}
return false
}
func (x *SetLogLevelRequest) GetLevel() uint32 {
if x != nil {
return x.Level
}
return 0
}
type SetLogLevelResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
Level uint32 `protobuf:"varint,2,opt,name=level,proto3" json:"level,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *SetLogLevelResponse) Reset() {
*x = SetLogLevelResponse{}
mi := &file_uart_messages_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *SetLogLevelResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*SetLogLevelResponse) ProtoMessage() {}
func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead.
func (*SetLogLevelResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36}
}
func (x *SetLogLevelResponse) GetSuccess() bool {
if x != nil {
return x.Success
}
return false
}
func (x *SetLogLevelResponse) GetLevel() uint32 {
if x != nil {
return x.Level
}
return 0
}
// Host → device: begin UART OTA (erase inactive OTA slot; device replies OTA_STATUS).
type OtaStartPayload struct {
state protoimpl.MessageState `protogen:"open.v1"`
@@ -2759,7 +3061,7 @@ type OtaStartPayload struct {
func (x *OtaStartPayload) Reset() {
*x = OtaStartPayload{}
mi := &file_uart_messages_proto_msgTypes[33]
mi := &file_uart_messages_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2771,7 +3073,7 @@ func (x *OtaStartPayload) String() string {
func (*OtaStartPayload) ProtoMessage() {}
func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[33]
mi := &file_uart_messages_proto_msgTypes[37]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2784,7 +3086,7 @@ func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead.
func (*OtaStartPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{33}
return file_uart_messages_proto_rawDescGZIP(), []int{37}
}
func (x *OtaStartPayload) GetTotalSize() uint32 {
@@ -2805,7 +3107,7 @@ type OtaPayload struct {
func (x *OtaPayload) Reset() {
*x = OtaPayload{}
mi := &file_uart_messages_proto_msgTypes[34]
mi := &file_uart_messages_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2817,7 +3119,7 @@ func (x *OtaPayload) String() string {
func (*OtaPayload) ProtoMessage() {}
func (x *OtaPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[34]
mi := &file_uart_messages_proto_msgTypes[38]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2830,7 +3132,7 @@ func (x *OtaPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead.
func (*OtaPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{34}
return file_uart_messages_proto_rawDescGZIP(), []int{38}
}
func (x *OtaPayload) GetSeq() uint32 {
@@ -2856,7 +3158,7 @@ type OtaEndPayload struct {
func (x *OtaEndPayload) Reset() {
*x = OtaEndPayload{}
mi := &file_uart_messages_proto_msgTypes[35]
mi := &file_uart_messages_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2868,7 +3170,7 @@ func (x *OtaEndPayload) String() string {
func (*OtaEndPayload) ProtoMessage() {}
func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[35]
mi := &file_uart_messages_proto_msgTypes[39]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2881,7 +3183,7 @@ func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead.
func (*OtaEndPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{35}
return file_uart_messages_proto_rawDescGZIP(), []int{39}
}
// Device → host status (also used as ACK after each 4 KiB written).
@@ -2898,7 +3200,7 @@ type OtaStatusPayload struct {
func (x *OtaStatusPayload) Reset() {
*x = OtaStatusPayload{}
mi := &file_uart_messages_proto_msgTypes[36]
mi := &file_uart_messages_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2910,7 +3212,7 @@ func (x *OtaStatusPayload) String() string {
func (*OtaStatusPayload) ProtoMessage() {}
func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36]
mi := &file_uart_messages_proto_msgTypes[40]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2923,7 +3225,7 @@ func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead.
func (*OtaStatusPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36}
return file_uart_messages_proto_rawDescGZIP(), []int{40}
}
func (x *OtaStatusPayload) GetStatus() uint32 {
@@ -2964,7 +3266,7 @@ type OtaSlaveProgressRequest struct {
func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[37]
mi := &file_uart_messages_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2976,7 +3278,7 @@ func (x *OtaSlaveProgressRequest) String() string {
func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[37]
mi := &file_uart_messages_proto_msgTypes[41]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2989,7 +3291,7 @@ func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{37}
return file_uart_messages_proto_rawDescGZIP(), []int{41}
}
func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
@@ -3013,7 +3315,7 @@ type OtaSlaveProgressEntry struct {
func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[38]
mi := &file_uart_messages_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3025,7 +3327,7 @@ func (x *OtaSlaveProgressEntry) String() string {
func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[38]
mi := &file_uart_messages_proto_msgTypes[42]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3038,7 +3340,7 @@ func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{38}
return file_uart_messages_proto_rawDescGZIP(), []int{42}
}
func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
@@ -3089,7 +3391,7 @@ type OtaSlaveProgressResponse struct {
func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[39]
mi := &file_uart_messages_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -3101,7 +3403,7 @@ func (x *OtaSlaveProgressResponse) String() string {
func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[39]
mi := &file_uart_messages_proto_msgTypes[43]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -3114,7 +3416,7 @@ func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{39}
return file_uart_messages_proto_rawDescGZIP(), []int{43}
}
func (x *OtaSlaveProgressResponse) GetActive() bool {
@@ -3156,7 +3458,7 @@ var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" +
"\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xec\x11\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xc0\x14\n" +
"\vUartMessage\x12%\n" +
"\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" +
"\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" +
@@ -3191,7 +3493,11 @@ const file_uart_messages_proto_rawDesc = "" +
"\x12tap_notify_request\x18\x1d \x01(\v2\x16.alox.TapNotifyRequestH\x00R\x10tapNotifyRequest\x12I\n" +
"\x13tap_notify_response\x18\x1e \x01(\v2\x17.alox.TapNotifyResponseH\x00R\x11tapNotifyResponse\x12L\n" +
"\x14cache_status_request\x18! \x01(\v2\x18.alox.CacheStatusRequestH\x00R\x12cacheStatusRequest\x12O\n" +
"\x15cache_status_response\x18\" \x01(\v2\x19.alox.CacheStatusResponseH\x00R\x13cacheStatusResponseB\t\n" +
"\x15cache_status_response\x18\" \x01(\v2\x19.alox.CacheStatusResponseH\x00R\x13cacheStatusResponse\x12V\n" +
"\x18espnow_echo_ping_request\x18# \x01(\v2\x1b.alox.EspNowEchoPingRequestH\x00R\x15espnowEchoPingRequest\x12Y\n" +
"\x19espnow_echo_ping_response\x18$ \x01(\v2\x1c.alox.EspNowEchoPingResponseH\x00R\x16espnowEchoPingResponse\x12M\n" +
"\x15set_log_level_request\x18% \x01(\v2\x18.alox.SetLogLevelRequestH\x00R\x12setLogLevelRequest\x12P\n" +
"\x16set_log_level_response\x18& \x01(\v2\x19.alox.SetLogLevelResponseH\x00R\x13setLogLevelResponseB\t\n" +
"\apayload\"\x05\n" +
"\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" +
@@ -3311,7 +3617,16 @@ const file_uart_messages_proto_rawDesc = "" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
"\x19EspNowUnicastTestResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x10\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"\xc1\x02\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"W\n" +
"\x15EspNowEchoPingRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\x12!\n" +
"\ftimestamp_us\x18\x02 \x01(\x04R\vtimestampUs\"\x90\x01\n" +
"\x16EspNowEchoPingResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\x12!\n" +
"\ftimestamp_us\x18\x03 \x01(\x04R\vtimestampUs\x12\x1c\n" +
"\n" +
"esp_rtt_us\x18\x04 \x01(\rR\bespRttUs\"\xc1\x02\n" +
"\x16LedRingProgressRequest\x12\x12\n" +
"\x04mode\x18\x01 \x01(\rR\x04mode\x12\x1a\n" +
"\bprogress\x18\x02 \x01(\rR\bprogress\x12\x14\n" +
@@ -3345,7 +3660,13 @@ const file_uart_messages_proto_rawDesc = "" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\"H\n" +
"\x0fRestartResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"0\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\"@\n" +
"\x12SetLogLevelRequest\x12\x14\n" +
"\x05write\x18\x01 \x01(\bR\x05write\x12\x14\n" +
"\x05level\x18\x02 \x01(\rR\x05level\"E\n" +
"\x13SetLogLevelResponse\x12\x18\n" +
"\asuccess\x18\x01 \x01(\bR\asuccess\x12\x14\n" +
"\x05level\x18\x02 \x01(\rR\x05level\"0\n" +
"\x0fOtaStartPayload\x12\x1d\n" +
"\n" +
"total_size\x18\x01 \x01(\rR\ttotalSize\":\n" +
@@ -3376,7 +3697,7 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" +
"\vslave_count\x18\x04 \x01(\rR\n" +
"slaveCount\x12:\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xf1\x02\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\x9a\x03\n" +
"\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" +
@@ -3400,7 +3721,9 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0eBATTERY_STATUS\x10\x1a\x12\x0e\n" +
"\n" +
"TAP_NOTIFY\x10\x1b\x12\x10\n" +
"\fCACHE_STATUS\x10\x1d\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\fCACHE_STATUS\x10\x1d\x12\x14\n" +
"\x10ESPNOW_ECHO_PING\x10\x1e\x12\x11\n" +
"\rSET_LOG_LEVEL\x10\x1f\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\aTapKind\x12\f\n" +
"\bTAP_NONE\x10\x00\x12\x0e\n" +
"\n" +
@@ -3423,7 +3746,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
}
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 40)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 44)
var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType
(TapKind)(0), // 1: alox.TapKind
@@ -3454,19 +3777,23 @@ var file_uart_messages_proto_goTypes = []any{
(*CacheStatusResponse)(nil), // 26: alox.CacheStatusResponse
(*EspNowUnicastTestRequest)(nil), // 27: alox.EspNowUnicastTestRequest
(*EspNowUnicastTestResponse)(nil), // 28: alox.EspNowUnicastTestResponse
(*LedRingProgressRequest)(nil), // 29: alox.LedRingProgressRequest
(*LedRingProgressResponse)(nil), // 30: alox.LedRingProgressResponse
(*EspNowFindMeRequest)(nil), // 31: alox.EspNowFindMeRequest
(*EspNowFindMeResponse)(nil), // 32: alox.EspNowFindMeResponse
(*RestartRequest)(nil), // 33: alox.RestartRequest
(*RestartResponse)(nil), // 34: alox.RestartResponse
(*OtaStartPayload)(nil), // 35: alox.OtaStartPayload
(*OtaPayload)(nil), // 36: alox.OtaPayload
(*OtaEndPayload)(nil), // 37: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 38: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 39: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 40: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 41: alox.OtaSlaveProgressResponse
(*EspNowEchoPingRequest)(nil), // 29: alox.EspNowEchoPingRequest
(*EspNowEchoPingResponse)(nil), // 30: alox.EspNowEchoPingResponse
(*LedRingProgressRequest)(nil), // 31: alox.LedRingProgressRequest
(*LedRingProgressResponse)(nil), // 32: alox.LedRingProgressResponse
(*EspNowFindMeRequest)(nil), // 33: alox.EspNowFindMeRequest
(*EspNowFindMeResponse)(nil), // 34: alox.EspNowFindMeResponse
(*RestartRequest)(nil), // 35: alox.RestartRequest
(*RestartResponse)(nil), // 36: alox.RestartResponse
(*SetLogLevelRequest)(nil), // 37: alox.SetLogLevelRequest
(*SetLogLevelResponse)(nil), // 38: alox.SetLogLevelResponse
(*OtaStartPayload)(nil), // 39: alox.OtaStartPayload
(*OtaPayload)(nil), // 40: alox.OtaPayload
(*OtaEndPayload)(nil), // 41: alox.OtaEndPayload
(*OtaStatusPayload)(nil), // 42: alox.OtaStatusPayload
(*OtaSlaveProgressRequest)(nil), // 43: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressEntry)(nil), // 44: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 45: alox.OtaSlaveProgressResponse
}
var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@@ -3475,22 +3802,22 @@ var file_uart_messages_proto_depIdxs = []int32{
5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse
7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse
9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse
35, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
36, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
37, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
38, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
39, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
40, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
41, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
42, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest
11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
27, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
28, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
39, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
41, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
29, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
30, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
31, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
32, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse
33, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest
34, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse
43, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
45, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
31, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
32, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
33, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
34, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse
35, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest
36, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse
12, // 22: alox.UartMessage.accel_stream_request:type_name -> alox.AccelStreamRequest
13, // 23: alox.UartMessage.accel_stream_response:type_name -> alox.AccelStreamResponse
14, // 24: alox.UartMessage.battery_status_request:type_name -> alox.BatteryStatusRequest
@@ -3499,22 +3826,26 @@ var file_uart_messages_proto_depIdxs = []int32{
20, // 27: alox.UartMessage.tap_notify_response:type_name -> alox.TapNotifyResponse
22, // 28: alox.UartMessage.cache_status_request:type_name -> alox.CacheStatusRequest
26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse
6, // 30: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
8, // 31: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
15, // 32: alox.BatterySample.lipo1:type_name -> alox.LipoReading
15, // 33: alox.BatterySample.lipo2:type_name -> alox.LipoReading
16, // 34: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
1, // 35: alox.TapEvent.kind:type_name -> alox.TapKind
1, // 36: alox.CacheClientTap.kind:type_name -> alox.TapKind
23, // 37: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
24, // 38: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
25, // 39: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
40, // 40: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
41, // [41:41] is the sub-list for method output_type
41, // [41:41] is the sub-list for method input_type
41, // [41:41] is the sub-list for extension type_name
41, // [41:41] is the sub-list for extension extendee
0, // [0:41] is the sub-list for field type_name
29, // 30: alox.UartMessage.espnow_echo_ping_request:type_name -> alox.EspNowEchoPingRequest
30, // 31: alox.UartMessage.espnow_echo_ping_response:type_name -> alox.EspNowEchoPingResponse
37, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest
38, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse
6, // 34: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
8, // 35: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
15, // 36: alox.BatterySample.lipo1:type_name -> alox.LipoReading
15, // 37: alox.BatterySample.lipo2:type_name -> alox.LipoReading
16, // 38: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
1, // 39: alox.TapEvent.kind:type_name -> alox.TapKind
1, // 40: alox.CacheClientTap.kind:type_name -> alox.TapKind
23, // 41: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
24, // 42: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
25, // 43: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
44, // 44: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
45, // [45:45] is the sub-list for method output_type
45, // [45:45] is the sub-list for method input_type
45, // [45:45] is the sub-list for extension type_name
45, // [45:45] is the sub-list for extension extendee
0, // [0:45] is the sub-list for field type_name
}
func init() { file_uart_messages_proto_init() }
@@ -3552,6 +3883,10 @@ func file_uart_messages_proto_init() {
(*UartMessage_TapNotifyResponse)(nil),
(*UartMessage_CacheStatusRequest)(nil),
(*UartMessage_CacheStatusResponse)(nil),
(*UartMessage_EspnowEchoPingRequest)(nil),
(*UartMessage_EspnowEchoPingResponse)(nil),
(*UartMessage_SetLogLevelRequest)(nil),
(*UartMessage_SetLogLevelResponse)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -3559,7 +3894,7 @@ func file_uart_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)),
NumEnums: 2,
NumMessages: 40,
NumMessages: 44,
NumExtensions: 0,
NumServices: 0,
},
+1 -1
View File
@@ -44,7 +44,7 @@ Ordered steps: UART commands, delays, or esptool reset.
**unicast_test** — `input`: `slave` or `client_id`, `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`
**find_me** — `input`: `client` / `client_id` or `slave` (`0` = master ring)
+146 -12
View File
@@ -219,6 +219,8 @@
<dd class="col-7" x-text="state.master.running_partition || '—'"></dd>
<dt class="col-5 text-muted">Deadzone</dt>
<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>
@@ -264,6 +266,31 @@
Restart
</button>
</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">
UART nicht verbunden — Eingabe gesperrt.
</p>
@@ -403,6 +430,12 @@
title="ESP-NOW Unicast-Test">
Test
</button>
<button type="button" class="btn btn-outline-info btn-sm"
@click="echoPing(c.id)"
:disabled="busy || !state.uart_connected || !c.available"
title="ESP-NOW Timestamp-Echo (Round-Trip-Latenz)">
Ping
</button>
<button type="button" class="btn btn-outline-info btn-sm"
@click="ledRing({ clientId: c.id })"
:disabled="busy || !state.uart_connected || !c.available"
@@ -438,7 +471,7 @@
<div class="card-body">
<p class="text-muted small mb-3">
Modi: <code>clear</code>, <code>color</code> (ganzer Ring), <code>progress</code> (0100&nbsp;%),
<code>digit</code> (010), <code>blink</code>, <code>find-me</code>.
<code>digit</code> (010), <code>blink</code>, <code>find-me</code>, <code>battery-low</code>.
Ziel: Master (<code>client_id=0</code>), ein Slave oder alle Slaves (Broadcast).
</p>
<div class="row g-3 align-items-end">
@@ -451,6 +484,7 @@
<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">
@@ -621,6 +655,7 @@
ws: null,
wsConnected: false,
masterDz: 100,
masterLogLevel: 0,
allDz: 100,
allTapSingle: false,
allTapDouble: false,
@@ -640,6 +675,7 @@
busy: false,
configMsg: '',
configMsgOk: false,
_flashTimer: null,
led: {
mode: 'color',
r: 0,
@@ -734,21 +770,35 @@
},
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) {
if (!this.state.master) this.state.master = {};
this.state.master.lipo1 = s.lipo1;
this.state.master.lipo2 = s.lipo2;
this.state.master.battery_age_ms = s.age_ms;
master.lipo1 = s.lipo1;
master.lipo2 = s.lipo2;
master.battery_age_ms = s.age_ms;
masterChanged = true;
continue;
}
const c = (this.state.clients || []).find((x) => x.id === s.client_id);
if (c) {
c.lipo1 = s.lipo1;
c.lipo2 = s.lipo2;
c.battery_age_ms = s.age_ms;
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;
@@ -759,6 +809,11 @@
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) {
if (!hex || hex.length !== 12) return hex || '';
return hex.match(/.{2}/g).join(':');
@@ -1031,10 +1086,14 @@
this.busy = false;
}
},
flash(msg, ok) {
flash(msg, ok, durationMs = 5000) {
this.configMsg = msg;
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 = {}) {
if (deadzone == null || deadzone < 0) {
@@ -1093,6 +1152,44 @@
async setMasterDeadzone() {
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) {
@@ -1258,6 +1355,43 @@
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) {
this.busy = true;
try {
+6
View File
@@ -22,11 +22,13 @@ idf_component_register(
"cmd/cmd_tap_notify.c"
"cmd/cmd_cache_status.c"
"cmd/cmd_espnow_unicast_test.c"
"cmd/cmd_espnow_echo_ping.c"
"cmd/cmd_espnow_find_me.c"
"cmd/cmd_restart.c"
"pod_reboot.c"
"cmd/cmd_led_ring.c"
"cmd/cmd_battery.c"
"cmd/cmd_set_log_level.c"
"cmd/cmd_ota.c"
"cmd/cmd_ota_slave_progress.c"
"ota_uart.c"
@@ -40,6 +42,7 @@ idf_component_register(
"esp_now_proto.c"
"bosch456.c"
"board_input.c"
"battery_uv.c"
"pod_settings.c"
"proto/uart_messages.pb.c"
"proto/esp_now_messages.pb.c"
@@ -61,7 +64,10 @@ idf_component_register(
esp_driver_i2c
esp_adc
app_update
esp_timer
bma456)
target_compile_definitions(${COMPONENT_LIB}
PRIVATE "POWERPOD_GIT_HASH=\"${POWERPOD_GIT_HASH}\"")
# Optional: disable software UV protection
# target_compile_definitions(${COMPONENT_LIB} PRIVATE POWERPOD_BATTERY_UV_ENABLE=0)
+55 -11
View File
@@ -55,18 +55,20 @@ Pins (`powerpod.h`):
| BMA456 INT | 10 |
| Button (Taster) | 12 |
| 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.
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`
2. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
3. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
4. `led_ring_init()`
5. `board_input_init()` — button press logs, LiPo ADC logs every **10 s**
6. **Master only:** command queue, UART, registered commands (e.g. VERSION)
1. `pod_settings_init()` — NVS
2. LiPo ADC init + optional UV boot check (`battery_uv.c`)
3. Read DIP + IO expander → `app_config`
4. **I2C bus** — IO expander `0x20`; optional **BMA456H** (`init_bma456`, same bus)
5. `esp_now_comm_init(&app_config)` — WiFi + ESP-NOW
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`)
@@ -228,6 +230,8 @@ Host and master speak nanopb-encoded `UartMessage` inside UART frames (byte 0 =
| 25 | `ACCEL_STREAM` | Implemented — enable/disable slave ESP-NOW accel stream to master |
| 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:
@@ -390,6 +394,30 @@ go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
```
### SET_LOG_LEVEL command
Read or set the **global** ESP-IDF log filter on the master (`esp_log_level_set("*", level)`). Does not affect the host UART protocol (UART1); `esp_log_*` output goes to the **debug console UART0** (USB, 115200).
**Request:** framed `31` (`0x1f`) + `set_log_level_request` (`write`, `level` 05).
**Response:** `set_log_level_response` (`success`, `level`).
| `level` | `esp_log_level_t` |
|---------|-------------------|
| 0 | NONE |
| 1 | ERROR |
| 2 | WARN |
| 3 | INFO |
| 4 | DEBUG |
| 5 | VERBOSE |
Boot default follows `CONFIG_LOG_DEFAULT_LEVEL` in `sdkconfig` (not persisted across reboot).
```bash
go run . -port /dev/ttyUSB0 log-level
go run . -port /dev/ttyUSB0 log-level -set -level 0
```
### RESTART command
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.
@@ -414,6 +442,19 @@ Read **cached** LiPo ADC values on the **master** (master local + one entry per
# Host / goTool: all_clients returns master (id 0) + slaves from cache
```
### Software UV protection (LiPo)
When `POWERPOD_BATTERY_UV_ENABLE` is `1` (default in `powerpod.h`, disable via CMake), the firmware monitors ADC pin voltages on GPIO 1 and 11:
| Condition | ADC threshold | Action |
|-----------|---------------|--------|
| Under-voltage | any valid channel &lt; **2300 mV** (3.0 V pack) | NVS latch `uv_latch`, restart into energy-save mode |
| Charged | all valid channels ≥ **3000 mV** (4.0 V pack) | Clear latch, restart into normal boot |
**Energy-save boot:** LED mode `6` (first 4 LEDs red ~10 % for 5 s), then no WiFi/ESP-NOW, UART, I2C, BMA456, or button — only LiPo ADC polling every 30 s until charged.
**Test LED pattern from host:** `led-ring -mode battery-low` (works even when UV feature is disabled).
### LED_RING command
Control the 95-LED ring from the host. The firmware **does not** animate digits locally; only UART updates the display.
@@ -422,7 +463,7 @@ Control the 95-LED ring from the host. The firmware **does not** animate digits
| Field | Meaning |
|-------|---------|
| `mode` | `0` = clear, `1` = progress, `2` = digit (010), `3` = blink, `4` = find-me, `5` = solid color (all LEDs) |
| `mode` | `0` = clear, `1` = progress, `2` = digit (010), `3` = blink, `4` = find-me, `5` = solid color (all LEDs), `6` = battery-low (first 4 LEDs red ~10 % for 5 s) |
| `progress` | 0100 (% of ring lit, mode `1`) |
| `digit` | 010 (mode `2`, segment maps in `led_ring.c`) |
| `r`, `g`, `b` | Color 0255 |
@@ -444,6 +485,7 @@ go run . -port /dev/ttyUSB0 led-ring -mode blink -g 255 -blink-count 2
go run . -port /dev/ttyUSB0 find-me
go run . -port /dev/ttyUSB0 find-me -client 16
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
```
@@ -537,10 +579,12 @@ Target: ESP32-S3. Close serial monitor on the UART adapter port before running `
| `bosch456.c/h` | BMA456H I2C driver, accel poll, on-demand read, tap INT, deadzone filter |
| `cmd/cmd_tap_notify.c` | UART `TAP_NOTIFY` — ESP-NOW tap notify config |
| `cmd/cmd_cache_status.c` | UART `CACHE_STATUS` — subscribed accel + tap cache poll |
| `board_input.c/h` | Taster GPIO12, LiPo ADC on GPIO1 / GPIO12 |
| `pod_settings.c/h` | NVS persistence (accel deadzone, …) |
| `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) |
| `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/esp_now_messages.proto` | ESP-NOW protocol schema |
| `esp_now_proto.c/h` | Encode/decode `EspNowMessage` |
+130
View File
@@ -0,0 +1,130 @@
#include "battery_uv.h"
#if POWERPOD_BATTERY_UV_ENABLE
#include "led_ring.h"
#include "pod_reboot.h"
#include "pod_settings.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "[BATTERY_UV]";
#define UV_DEBOUNCE_SAMPLES 2u
#define UV_POLL_INTERVAL_MS 30000u
static uint8_t s_uv_streak;
static uint8_t s_charge_streak;
bool battery_uv_is_latched(void) { return pod_settings_is_uv_latched(); }
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
if (reading->lipo1_valid && reading->lipo1_mv < BATTERY_UV_ADC_MV) {
return true;
}
if (reading->lipo2_valid && reading->lipo2_mv < BATTERY_UV_ADC_MV) {
return true;
}
return false;
}
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
if (reading == NULL) {
return false;
}
bool any_valid = false;
if (reading->lipo1_valid) {
any_valid = true;
if (reading->lipo1_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
if (reading->lipo2_valid) {
any_valid = true;
if (reading->lipo2_mv < BATTERY_CHARGE_ADC_MV) {
return false;
}
}
return any_valid;
}
bool battery_uv_evaluate_boot(void) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
if (pod_settings_is_uv_latched() && battery_uv_reading_is_charged(&reading)) {
ESP_LOGI(TAG, "charged — clearing UV latch");
pod_settings_clear_uv_latched();
return false;
}
if (pod_settings_is_uv_latched() || battery_uv_reading_is_under(&reading)) {
if (!pod_settings_is_uv_latched()) {
ESP_LOGW(TAG, "under-voltage at boot — latching UV");
} else {
ESP_LOGW(TAG, "UV latched — energy-save boot");
}
pod_settings_set_uv_latched(true);
return true;
}
return false;
}
void battery_uv_poll(const board_lipo_reading_t *reading) {
if (reading == NULL || pod_settings_is_uv_latched()) {
return;
}
if (battery_uv_reading_is_under(reading)) {
s_uv_streak++;
ESP_LOGW(TAG, "under-voltage sample %u/%u", (unsigned)s_uv_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_uv_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGW(TAG, "UV confirmed — latching and restarting");
pod_settings_set_uv_latched(true);
pod_schedule_restart();
}
return;
}
s_uv_streak = 0;
}
void battery_uv_run_mode(void) {
s_charge_streak = 0;
s_uv_streak = 0;
led_ring_init();
led_ring_show_battery_low();
ESP_LOGW(TAG, "energy-save mode — ADC poll every %u ms", UV_POLL_INTERVAL_MS);
while (1) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
if (battery_uv_reading_is_charged(&reading)) {
s_charge_streak++;
ESP_LOGI(TAG, "charge sample %u/%u", (unsigned)s_charge_streak,
(unsigned)UV_DEBOUNCE_SAMPLES);
if (s_charge_streak >= UV_DEBOUNCE_SAMPLES) {
ESP_LOGI(TAG, "charged — clearing UV latch and restarting");
pod_settings_clear_uv_latched();
pod_schedule_restart();
}
} else {
s_charge_streak = 0;
}
vTaskDelay(pdMS_TO_TICKS(UV_POLL_INTERVAL_MS));
}
}
#endif
+51
View File
@@ -0,0 +1,51 @@
#ifndef BATTERY_UV_H
#define BATTERY_UV_H
#include "board_input.h"
#include "powerpod.h"
#include <stdbool.h>
#if POWERPOD_BATTERY_UV_ENABLE
/** ADC pin voltage (mV) below which UV is triggered (3.0 V pack via 33k/10k divider). */
#define BATTERY_UV_ADC_MV 2300u
/** ADC pin voltage (mV) at which charging is detected (4.0 V pack, rounded). */
#define BATTERY_CHARGE_ADC_MV 3000u
bool battery_uv_is_latched(void);
bool battery_uv_reading_is_under(const board_lipo_reading_t *reading);
bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading);
/** true → enter UV energy-save path (does not return). */
bool battery_uv_evaluate_boot(void);
void battery_uv_poll(const board_lipo_reading_t *reading);
/** Blocks: LED indicator, then minimal ADC polling until charged. */
void battery_uv_run_mode(void);
#else
static inline bool battery_uv_is_latched(void) { return false; }
static inline bool battery_uv_reading_is_under(const board_lipo_reading_t *reading) {
(void)reading;
return false;
}
static inline bool battery_uv_reading_is_charged(const board_lipo_reading_t *reading) {
(void)reading;
return false;
}
static inline bool battery_uv_evaluate_boot(void) { return false; }
static inline void battery_uv_poll(const board_lipo_reading_t *reading) {
(void)reading;
}
static inline void battery_uv_run_mode(void) {}
#endif
#endif
+74 -40
View File
@@ -1,6 +1,10 @@
#include "board_input.h"
#include "powerpod.h"
#include "client_registry.h"
#include "driver/gpio.h"
#if POWERPOD_BATTERY_UV_ENABLE
#include "battery_uv.h"
#endif
#include "esp_adc/adc_oneshot.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
@@ -18,34 +22,51 @@ static const char *TAG_LIPO = "[LIPO]";
#define LIPO_ADC_MAX_RAW 4095
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) {
adc_unit_t unit;
esp_err_t err = adc_oneshot_io_to_channel(gpio, &unit, out_ch);
typedef struct {
adc_oneshot_unit_handle_t unit;
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) {
ESP_LOGW(TAG_LIPO, "GPIO%d not an ADC channel: %s", gpio, esp_err_to_name(err));
*out_ok = false;
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 = {
.atten = ADC_ATTEN_DB_12,
.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) {
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;
}
*out_ok = true;
out->ok = true;
ESP_LOGI(TAG_LIPO, "GPIO%d ready (ADC unit %d)", gpio, (int)unit_id);
return ESP_OK;
}
@@ -56,15 +77,15 @@ static uint32_t raw_to_mv(int raw) {
return (uint32_t)((raw * LIPO_ADC_FULL_SCALE_MV) / LIPO_ADC_MAX_RAW);
}
static void sample_one_channel(adc_channel_t ch, bool ok, uint32_t *mv_out,
static void sample_one_channel(const lipo_adc_t *adc, uint32_t *mv_out,
bool *valid_out) {
*valid_out = false;
*mv_out = 0;
if (!ok || s_adc == NULL) {
if (adc == NULL || !adc->ok || adc->unit == NULL) {
return;
}
int raw = 0;
if (adc_oneshot_read(s_adc, ch, &raw) == ESP_OK) {
if (adc_oneshot_read(adc->unit, adc->ch, &raw) == ESP_OK) {
*valid_out = true;
*mv_out = raw_to_mv(raw);
}
@@ -75,8 +96,8 @@ void board_input_read_lipo(board_lipo_reading_t *out) {
return;
}
memset(out, 0, sizeof(*out));
sample_one_channel(s_lipo1_ch, s_lipo1_ok, &out->lipo1_mv, &out->lipo1_valid);
sample_one_channel(s_lipo2_ch, s_lipo2_ok, &out->lipo2_mv, &out->lipo2_valid);
sample_one_channel(&s_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) {
@@ -87,6 +108,7 @@ static void lipo_monitor_task(void *param) {
while (1) {
board_lipo_reading_t reading;
board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
ESP_LOGI(TAG_LIPO,
"LIPO1 GPIO%d %s %lu mV LIPO2 GPIO%d %s %lu mV",
@@ -95,6 +117,10 @@ static void lipo_monitor_task(void *param) {
reading.lipo2_valid ? "ok" : "n/a",
(unsigned long)reading.lipo2_mv);
#if POWERPOD_BATTERY_UV_ENABLE
battery_uv_poll(&reading);
#endif
vTaskDelay(pdMS_TO_TICKS(LIPO_SAMPLE_INTERVAL_MS));
}
}
@@ -165,29 +191,30 @@ static esp_err_t init_button(void) {
return ESP_OK;
}
static esp_err_t init_lipo_adc(void) {
adc_oneshot_unit_init_cfg_t init_cfg = {
.unit_id = ADC_UNIT_1,
};
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;
}
static esp_err_t init_lipo_adc_hw(void) {
memset(&s_lipo1, 0, sizeof(s_lipo1));
memset(&s_lipo2, 0, sizeof(s_lipo2));
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) {
ESP_LOGW(TAG_LIPO, "LIPO2 on GPIO%d skipped (button uses same pin)",
V_LIPO_2_GPIO);
s_lipo2_ok = false;
} 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) {
adc_oneshot_del_unit(s_adc);
s_adc = NULL;
if (!s_lipo1.ok && !s_lipo2.ok) {
return ESP_FAIL;
}
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;
}
@@ -198,16 +225,23 @@ static esp_err_t init_lipo_adc(void) {
return ESP_OK;
}
esp_err_t board_input_init_button(void) { return init_button(); }
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) {
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;
}
+9
View File
@@ -19,6 +19,15 @@ typedef struct {
*/
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);
+4 -8
View File
@@ -33,18 +33,14 @@ static bool append_battery_sample(alox_BatteryStatusResponse *resp,
return lipo1_valid || lipo2_valid;
}
static bool append_master_cached(alox_BatteryStatusResponse *resp) {
static bool append_master_sample(alox_BatteryStatusResponse *resp) {
board_lipo_reading_t reading;
uint32_t age_ms = 0;
if (!client_registry_get_master_battery(&reading, &age_ms)) {
board_input_read_lipo(&reading);
client_registry_set_master_battery(&reading);
age_ms = 0;
}
return append_battery_sample(resp, 0, reading.lipo1_valid, reading.lipo1_mv,
reading.lipo2_valid, reading.lipo2_mv, age_ms);
reading.lipo2_valid, reading.lipo2_mv, 0);
}
static bool append_slave_cached(alox_BatteryStatusResponse *resp,
@@ -102,7 +98,7 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
bool any = false;
if (req.all_clients) {
any |= append_master_cached(resp);
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) {
@@ -113,7 +109,7 @@ static void handle_battery_status(const uint8_t *data, size_t len) {
ESP_LOGI(TAG, "battery cache all_clients → %u samples",
(unsigned)resp->samples_count);
} else if (req.client_id == 0) {
any = append_master_cached(resp);
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);
+70
View File
@@ -0,0 +1,70 @@
#include "client_registry.h"
#include "cmd_espnow_echo_ping.h"
#include "esp_log.h"
#include "esp_now_comm.h"
#include "uart_cmd.h"
static const char *TAG = "[ECHO_PING]";
static void reply(bool success, uint32_t client_id, uint64_t timestamp_us,
uint32_t esp_rtt_us) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_ESPNOW_ECHO_PING,
alox_UartMessage_espnow_echo_ping_response_tag);
response.payload.espnow_echo_ping_response.success = success;
response.payload.espnow_echo_ping_response.client_id = client_id;
response.payload.espnow_echo_ping_response.timestamp_us = timestamp_us;
response.payload.espnow_echo_ping_response.esp_rtt_us = esp_rtt_us;
uart_cmd_send(&response, TAG);
}
static void handle_espnow_echo_ping(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply(false, 0, 0, 0);
return;
}
const alox_EspNowEchoPingRequest *req = UART_CMD_REQ(
&uart_msg, alox_UartMessage_espnow_echo_ping_request_tag,
espnow_echo_ping_request);
if (req == NULL || req->client_id == 0) {
ESP_LOGW(TAG, "need client_id in request");
reply(false, 0, 0, 0);
return;
}
const client_info_t *client = client_registry_find_by_id(req->client_id);
if (client == NULL) {
ESP_LOGW(TAG, "client id %lu not in registry",
(unsigned long)req->client_id);
reply(false, req->client_id, 0, 0);
return;
}
ESP_LOGI(TAG, "UART request client_id=%lu host_ts=%llu",
(unsigned long)req->client_id,
(unsigned long long)req->timestamp_us);
esp_now_echo_ping_result_t ping_result = {0};
esp_err_t err =
esp_now_comm_echo_ping(client->mac, req->timestamp_us, &ping_result);
if (err != ESP_OK) {
ESP_LOGW(TAG, "echo ping to id=%lu failed: %s", (unsigned long)req->client_id,
esp_err_to_name(err));
reply(false, req->client_id, 0, 0);
return;
}
ESP_LOGI(TAG, "UART reply client_id=%lu host_ts=%llu esp_rtt_us=%lu",
(unsigned long)req->client_id,
(unsigned long long)ping_result.echoed_us,
(unsigned long)ping_result.esp_rtt_us);
reply(true, req->client_id, ping_result.echoed_us, ping_result.esp_rtt_us);
}
void cmd_espnow_echo_ping_register(void) {
uart_cmd_register(alox_MessageType_ESPNOW_ECHO_PING, handle_espnow_echo_ping);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CMD_ESPNOW_ECHO_PING_H
#define CMD_ESPNOW_ECHO_PING_H
void cmd_espnow_echo_ping_register(void);
#endif
+4
View File
@@ -57,6 +57,10 @@ static const char *message_type_name(uint16_t id) {
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:
return "UNKNOWN";
}
+5
View File
@@ -13,6 +13,7 @@ static const char *TAG = "[LED_RING_CMD]";
#define LED_RING_MODE_BLINK 3
#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) {
if (v > 255) {
@@ -90,6 +91,10 @@ bool cmd_led_ring_apply(const alox_LedRingProgressRequest *req) {
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;
+12 -11
View File
@@ -175,8 +175,18 @@ static void handle_ota_payload(const uint8_t *data, size_t len) {
return;
}
ota_feed_result_t r =
ota_uart_feed(req_ptr->data.bytes, req_ptr->data.size);
ota_feed_result_t r = ota_uart_feed_chunk(req_ptr->seq, req_ptr->data.bytes,
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) {
send_ota_failed( 13);
return;
@@ -189,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,
OTA_LED_UART_B);
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);
}
}
}
+51
View File
@@ -0,0 +1,51 @@
#include "cmd_set_log_level.h"
#include "esp_log.h"
#include "uart_cmd.h"
static const char *TAG = "[LOG_LVL]";
static bool valid_level(uint32_t level) { return level <= ESP_LOG_VERBOSE; }
static void reply(uint32_t level, bool success) {
alox_UartMessage response;
uart_cmd_init_response(&response, alox_MessageType_SET_LOG_LEVEL,
alox_UartMessage_set_log_level_response_tag);
response.payload.set_log_level_response.success = success;
response.payload.set_log_level_response.level = level;
uart_cmd_send(&response, TAG);
}
static void handle_set_log_level(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg;
alox_SetLogLevelRequest req = alox_SetLogLevelRequest_init_zero;
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
ESP_LOGW(TAG, "decode failed");
reply((uint32_t)esp_log_level_get("*"), false);
return;
}
const alox_SetLogLevelRequest *req_ptr = UART_CMD_REQ(
&uart_msg, alox_UartMessage_set_log_level_request_tag, set_log_level_request);
if (req_ptr != NULL) {
req = *req_ptr;
}
if (req.write) {
if (!valid_level(req.level)) {
ESP_LOGW(TAG, "invalid level %lu", (unsigned long)req.level);
reply((uint32_t)esp_log_level_get("*"), false);
return;
}
esp_log_level_set("*", (esp_log_level_t)req.level);
ESP_LOGI(TAG, "global log level set to %lu", (unsigned long)req.level);
reply(req.level, true);
return;
}
reply((uint32_t)esp_log_level_get("*"), true);
}
void cmd_set_log_level_register(void) {
uart_cmd_register(alox_MessageType_SET_LOG_LEVEL, handle_set_log_level);
}
+6
View File
@@ -0,0 +1,6 @@
#ifndef CMD_SET_LOG_LEVEL_H
#define CMD_SET_LOG_LEVEL_H
void cmd_set_log_level_register(void);
#endif
+11
View File
@@ -26,6 +26,17 @@ esp_err_t esp_now_comm_send_accel_deadzone(const uint8_t mac[CLIENT_MAC_LEN],
esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
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. */
esp_err_t esp_now_comm_send_find_me(const uint8_t mac[CLIENT_MAC_LEN],
uint32_t client_id);
+123 -13
View File
@@ -21,7 +21,18 @@ 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) {
@@ -33,7 +44,8 @@ static uint8_t network_to_channel(uint8_t network) {
static void espnow_send_done_cb(const esp_now_send_info_t *tx_info,
esp_now_send_status_t status) {
(void)tx_info;
(void)status;
s_last_send_status = status;
s_last_send_ok = (status == ESP_NOW_SEND_SUCCESS);
if (s_send_done != NULL) {
xSemaphoreGive(s_send_done);
}
@@ -93,43 +105,140 @@ 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;
}
if (s_send_cb_ready && s_send_done != NULL) {
xSemaphoreTake(s_send_done, 0);
}
err = esp_now_send(dest_mac, buf, len);
if (err != ESP_OK) {
ESP_LOGW(TAG, "send type=%u failed: %s", (unsigned)msg->type,
esp_err_to_name(err));
return err;
}
if (s_send_cb_ready && s_send_done != NULL) {
if (xSemaphoreTake(s_send_done, pdMS_TO_TICKS(50)) != pdTRUE) {
ESP_LOGW(TAG, "send type=%u done timeout", (unsigned)msg->type);
}
}
xSemaphoreGive(s_send_lock);
return err;
}
@@ -163,7 +272,8 @@ esp_err_t esp_now_core_init_radio(uint8_t channel) {
void esp_now_core_init_send_done(void) {
s_send_done = xSemaphoreCreateBinary();
if (s_send_done != NULL &&
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 {
+6
View File
@@ -23,8 +23,14 @@ 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);
+115 -2
View File
@@ -5,8 +5,10 @@
#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>
@@ -51,6 +53,66 @@ static esp_err_t send_unicast_test(const uint8_t *dest_mac, uint32_t 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;
@@ -119,7 +181,7 @@ static esp_err_t send_ota_payload(const uint8_t *dest_mac, uint32_t seq,
msg.payload.ota_payload.seq = seq;
msg.payload.ota_payload.data.size = len;
memcpy(msg.payload.ota_payload.data.bytes, data, len);
return esp_now_core_send_wait(dest_mac, &msg);
return esp_now_core_send_reliable(dest_mac, &msg);
}
static esp_err_t send_ota_end(const uint8_t *dest_mac) {
@@ -224,6 +286,48 @@ esp_err_t esp_now_comm_send_unicast_test(const uint8_t mac[CLIENT_MAC_LEN],
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()) {
@@ -415,6 +519,12 @@ void esp_now_master_on_recv(const esp_now_recv_info_t *info, const uint8_t *data
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);
@@ -439,7 +549,10 @@ static void discover_task(void *param) {
(unsigned)esp_now_core_wifi_channel());
while (1) {
esp_now_core_send(ESPNOW_BCAST, &msg);
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));
}
}
+73 -6
View File
@@ -21,6 +21,8 @@
#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
@@ -28,6 +30,7 @@
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;
@@ -106,8 +109,18 @@ static esp_err_t send_battery_report(const uint8_t *dest_mac,
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;
@@ -216,6 +229,36 @@ static void handle_unicast_test(const uint8_t *master_mac,
(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();
@@ -358,8 +401,13 @@ static void handle_discover(const uint8_t *sender_mac,
if (!esp_now_core_mac_equal(sender_mac, s_master_mac)) {
return;
}
if ((now - s_last_discover_ms) <= SLAVE_MASTER_LOST_MS) {
s_last_discover_ms = now;
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");
@@ -368,7 +416,8 @@ static void handle_discover(const uint8_t *sender_mac,
memcpy(s_master_mac, sender_mac, ESP_NOW_ETH_ALEN);
s_joined = true;
s_last_discover_ms = now;
s_master_ota_grace = discover->master_ota_pending;
touch_master_presence(now);
esp_now_core_ensure_peer(sender_mac);
char mac_str[18];
@@ -384,10 +433,14 @@ 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();
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));
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();
}
}
@@ -481,12 +534,16 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
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) {
@@ -510,6 +567,11 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
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);
@@ -545,6 +607,11 @@ void esp_now_slave_on_recv(const esp_now_recv_info_t *info, const uint8_t *data,
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);
+33 -1
View File
@@ -5,6 +5,7 @@
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "led_strip.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_OFF_MS 150
#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 SemaphoreHandle_t s_battery_low_done;
// Led Matrix Maps
const uint8_t d0[] = {46, 47, 60, 61, 62, 75, 78, 79,
@@ -145,6 +150,22 @@ void vTaskLedRing(void *pvParameters) {
}
}
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);
}
@@ -153,12 +174,13 @@ void vTaskLedRing(void *pvParameters) {
void led_ring_init(void) {
led_queue = xQueueCreate(10, sizeof(led_command_t));
s_battery_low_done = xSemaphoreCreateBinary();
xTaskCreate(vTaskLedRing, "led_task", 4096, NULL, 5, NULL);
}
void led_ring_send_command(led_command_t *cmd) {
if (led_queue != NULL) {
xQueueSend(led_queue, cmd, portMAX_DELAY);
(void)xQueueSend(led_queue, cmd, 0);
}
}
@@ -224,3 +246,13 @@ void led_ring_find_me(void) {
led_command_t cmd = {.mode = LED_CMD_FIND_ME};
led_ring_send_command(&cmd);
}
void led_ring_show_battery_low(void) {
if (led_queue == NULL || s_battery_low_done == NULL) {
return;
}
(void)xSemaphoreTake(s_battery_low_done, 0);
led_command_t cmd = {.mode = LED_CMD_BATTERY_LOW};
led_ring_send_command(&cmd);
(void)xSemaphoreTake(s_battery_low_done, portMAX_DELAY);
}
+9 -1
View File
@@ -7,6 +7,10 @@
#define LED_RING_DEFAULT_INTENSITY 13
/** Full brightness for find-me and similar alerts. */
#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 {
LED_CMD_CLEAR,
@@ -14,7 +18,8 @@ typedef enum {
LED_CMD_SET_COLOR,
LED_CMD_PROGRESS,
LED_CMD_BLINK,
LED_CMD_FIND_ME
LED_CMD_FIND_ME,
LED_CMD_BATTERY_LOW
} led_mode_t;
typedef struct {
@@ -45,4 +50,7 @@ void led_ring_ota_failed(void);
/** Red / green / blue: 3 blinks each at full intensity. */
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
+123 -26
View File
@@ -18,9 +18,9 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_ESPNOW_PREPARE_PRIO 5
#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_PAYLOAD_DELAY_MS 3
#define OTA_ST_PREPARING 1u
#define OTA_ST_READY 2u
@@ -35,7 +35,8 @@ static const char *TAG = "[OTA_ESPNOW]";
#define OTA_MAX_TARGETS CLIENT_REGISTRY_MAX
#define OTA_SLAVE_WORK_QUEUE_LEN 12
/** ~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
@@ -173,6 +174,54 @@ static bool wait_target_bits(uint32_t want_bits, uint32_t timeout_ms) {
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,
@@ -221,8 +270,20 @@ static void process_slave_payload(const uint8_t master_mac[6],
ESP_LOGI(TAG, "ESP-NOW OTA payloads started");
}
ota_feed_result_t r =
ota_uart_feed(payload->data.bytes, payload->data.size);
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);
@@ -509,35 +570,71 @@ static esp_err_t distribute_image(const esp_partition_t *partition,
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;
const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE);
s_dist.expected_bytes = offset + block_len;
const uint32_t block_start_seq = seq;
if (full_block) {
xEventGroupClearBits(s_eg, target_mask);
}
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);
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;
}
}
seq++;
sent += chunk;
vTaskDelay(pdMS_TO_TICKS(OTA_PAYLOAD_DELAY_MS));
}
const bool full_block = (block_len >= OTA_UART_FLASH_BLOCK_SIZE);
s_dist.expected_bytes = offset + block_len;
if (full_block) {
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);
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();
s_distribution_active = false;
return ESP_ERR_TIMEOUT;
+29 -1
View File
@@ -12,6 +12,7 @@ typedef struct {
uint32_t total_size;
uint32_t received;
uint32_t written;
uint32_t expected_seq;
int target_slot;
uint8_t block_buf[OTA_UART_FLASH_BLOCK_SIZE];
size_t block_len;
@@ -112,10 +113,30 @@ int ota_uart_prepare(uint32_t total_size) {
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) {
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) {
ESP_LOGW(TAG, "chunk %u > %u, truncating", (unsigned)len,
OTA_UART_HOST_CHUNK_SIZE);
@@ -200,6 +221,13 @@ esp_err_t ota_uart_finish(bool set_boot, bool *success_out) {
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);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed: %s", esp_err_to_name(err));
+10 -2
View File
@@ -28,6 +28,8 @@ typedef enum {
typedef enum {
OTA_FEED_OK = 0,
OTA_FEED_BLOCK_WRITTEN,
OTA_FEED_SEQ_DUP,
OTA_FEED_SEQ_GAP,
OTA_FEED_ERROR,
} ota_feed_result_t;
@@ -41,8 +43,14 @@ int ota_uart_prepare(uint32_t total_size);
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);
+51
View File
@@ -7,6 +7,7 @@
static const char *TAG = "[SETTINGS]";
static const char *NS = "powerpod";
static const char *KEY_ACCEL_DZ = "accel_dz";
static const char *KEY_UV_LATCH = "uv_latch";
#define ACCEL_DEADZONE_MAX 4095u
@@ -108,3 +109,53 @@ void pod_settings_apply_accel_deadzone(void) {
bma456_set_accel_deadzone(deadzone);
}
}
bool pod_settings_is_uv_latched(void) {
if (!s_nvs_ready) {
return false;
}
nvs_handle_t handle;
esp_err_t err = nvs_open(NS, NVS_READONLY, &handle);
if (err != ESP_OK) {
return false;
}
uint8_t value = 0;
err = nvs_get_u8(handle, KEY_UV_LATCH, &value);
nvs_close(handle);
return err == ESP_OK && value != 0;
}
esp_err_t pod_settings_set_uv_latched(bool latched) {
esp_err_t err = ensure_nvs();
if (err != ESP_OK) {
return err;
}
nvs_handle_t handle;
err = nvs_open(NS, NVS_READWRITE, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_open failed: %s", esp_err_to_name(err));
return err;
}
err = nvs_set_u8(handle, KEY_UV_LATCH, latched ? 1u : 0u);
if (err == ESP_OK) {
err = nvs_commit(handle);
}
nvs_close(handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "save uv_latch failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "UV latch %s", latched ? "set" : "cleared");
return ESP_OK;
}
esp_err_t pod_settings_clear_uv_latched(void) {
return pod_settings_set_uv_latched(false);
}
+6
View File
@@ -2,6 +2,7 @@
#define POD_SETTINGS_H
#include "esp_err.h"
#include <stdbool.h>
#include <stdint.h>
/** 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. */
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
+17 -1
View File
@@ -5,6 +5,7 @@
#include "cmd_tap_notify.h"
#include "cmd_cache_status.h"
#include "cmd_espnow_unicast_test.h"
#include "cmd_espnow_echo_ping.h"
#include "cmd_espnow_find_me.h"
#include "cmd_restart.h"
#include "cmd_client_info.h"
@@ -13,6 +14,7 @@
#include "cmd_ota_slave_progress.h"
#include "cmd_led_ring.h"
#include "cmd_battery.h"
#include "cmd_set_log_level.h"
#include "esp_now_comm.h"
#include "powerpod.h"
#include "driver/gpio.h"
@@ -29,6 +31,7 @@
#include "led_ring.h"
#include "pod_settings.h"
#include "uart.h"
#include "battery_uv.h"
#include <stdint.h>
enum MASTER_STATES {
@@ -76,6 +79,16 @@ void app_main(void) {
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
gpio_reset_pin(DIP_MASTER);
gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT);
@@ -165,7 +178,8 @@ void app_main(void) {
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
app_config.running_partition, ota_slot);
board_input_init();
board_input_start_lipo_monitor();
board_input_init_button();
err = esp_now_comm_init(&app_config);
if (err != ESP_OK) {
@@ -185,12 +199,14 @@ void app_main(void) {
cmd_tap_notify_register();
cmd_cache_status_register();
cmd_espnow_unicast_test_register();
cmd_espnow_echo_ping_register();
cmd_espnow_find_me_register();
cmd_restart_register();
cmd_led_ring_register();
cmd_battery_register();
cmd_ota_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)");
+7 -3
View File
@@ -1,6 +1,11 @@
#ifndef 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 I2C_SCL 5
#define I2C_SDA 6
@@ -11,9 +16,8 @@
/** Front-panel button (active low, internal pull-up). */
#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
/** Shares GPIO with TASTER on current bench wiring; second ADC is skipped in board_input.c. */
#define V_LIPO_2_GPIO 12
#define V_LIPO_2_GPIO 11
#endif
+6
View File
@@ -9,6 +9,12 @@
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)
+63 -8
View File
@@ -29,7 +29,9 @@ typedef enum _alox_EspNowMessageType {
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_TAP_EVENT = 18,
alox_EspNowMessageType_ESPNOW_ECHO_PING = 19,
alox_EspNowMessageType_ESPNOW_ECHO_PONG = 20
} alox_EspNowMessageType;
/* Struct definitions */
@@ -37,6 +39,19 @@ typedef struct _alox_EspNowUnicastTest {
uint32_t seq;
} 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). */
typedef struct _alox_EspNowFindMe {
/* * 0 = any slave; otherwise only slave_id must match */
@@ -50,6 +65,8 @@ typedef struct _alox_EspNowRestart {
typedef struct _alox_EspNowDiscover {
uint32_t network;
/* * Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending;
} alox_EspNowDiscover;
typedef struct _alox_EspNowSlavePresence {
@@ -169,6 +186,8 @@ typedef struct _alox_EspNowMessage {
alox_EspNowBatteryReport battery_report;
alox_EspNowTapNotify tap_notify;
alox_EspNowTapEvent tap_event;
alox_EspNowEchoPing echo_ping;
alox_EspNowEchoPong echo_pong;
} payload;
} alox_EspNowMessage;
@@ -179,8 +198,10 @@ extern "C" {
/* Helper constants for enums */
#define _alox_EspNowMessageType_MIN alox_EspNowMessageType_ESPNOW_UNKNOWN
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_TAP_EVENT
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_TAP_EVENT+1))
#define _alox_EspNowMessageType_MAX alox_EspNowMessageType_ESPNOW_ECHO_PONG
#define _alox_EspNowMessageType_ARRAYSIZE ((alox_EspNowMessageType)(alox_EspNowMessageType_ESPNOW_ECHO_PONG+1))
@@ -204,9 +225,11 @@ extern "C" {
/* Initializer values for message structs */
#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_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_EspNowAccelDeadzone_init_default {0, 0}
#define alox_EspNowAccelStream_init_default {0, 0}
@@ -222,9 +245,11 @@ extern "C" {
#define alox_EspNowOtaStatus_init_default {0, 0, 0}
#define alox_EspNowMessage_init_default {_alox_EspNowMessageType_MIN, 0, {alox_EspNowDiscover_init_default}}
#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_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_EspNowAccelDeadzone_init_zero {0, 0}
#define alox_EspNowAccelStream_init_zero {0, 0}
@@ -242,9 +267,14 @@ extern "C" {
/* Field tags (for use in manual encoding/decoding) */
#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_EspNowRestart_client_id_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_mac_tag 2
#define alox_EspNowSlavePresence_version_tag 3
@@ -306,6 +336,8 @@ extern "C" {
#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 */
#define alox_EspNowUnicastTest_FIELDLIST(X, a) \
@@ -313,6 +345,18 @@ X(a, STATIC, SINGULAR, UINT32, seq, 1)
#define alox_EspNowUnicastTest_CALLBACK NULL
#define alox_EspNowUnicastTest_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) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_EspNowFindMe_CALLBACK NULL
@@ -324,7 +368,8 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 1)
#define alox_EspNowRestart_DEFAULT NULL
#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_DEFAULT NULL
@@ -442,7 +487,9 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,led_ring,payload.led_ring), 15) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_query,payload.battery_query), 16) \
X(a, STATIC, ONEOF, MESSAGE, (payload,battery_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,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_DEFAULT NULL
#define alox_EspNowMessage_payload_discover_MSGTYPE alox_EspNowDiscover
@@ -463,8 +510,12 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_event,payload.tap_event), 19)
#define alox_EspNowMessage_payload_battery_report_MSGTYPE alox_EspNowBatteryReport
#define alox_EspNowMessage_payload_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_EspNowEchoPing_msg;
extern const pb_msgdesc_t alox_EspNowEchoPong_msg;
extern const pb_msgdesc_t alox_EspNowFindMe_msg;
extern const pb_msgdesc_t alox_EspNowRestart_msg;
extern const pb_msgdesc_t alox_EspNowDiscover_msg;
@@ -485,6 +536,8 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
/* Defines for backwards compatibility with code written before nanopb-0.4.0 */
#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_EspNowRestart_fields &alox_EspNowRestart_msg
#define alox_EspNowDiscover_fields &alox_EspNowDiscover_msg
@@ -512,7 +565,9 @@ extern const pb_msgdesc_t alox_EspNowMessage_msg;
#define alox_EspNowAccelStream_size 8
#define alox_EspNowBatteryQuery_size 6
#define alox_EspNowBatteryReport_size 22
#define alox_EspNowDiscover_size 6
#define alox_EspNowDiscover_size 8
#define alox_EspNowEchoPing_size 22
#define alox_EspNowEchoPong_size 22
#define alox_EspNowFindMe_size 6
#define alox_EspNowLedRing_size 60
#define alox_EspNowOtaEnd_size 0
+19
View File
@@ -24,12 +24,27 @@ enum EspNowMessageType {
ESPNOW_BATTERY_REPORT = 16;
ESPNOW_SET_TAP_NOTIFY = 17;
ESPNOW_TAP_EVENT = 18;
ESPNOW_ECHO_PING = 19;
ESPNOW_ECHO_PONG = 20;
}
message EspNowUnicastTest {
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). */
message EspNowFindMe {
/** 0 = any slave; otherwise only slave_id must match */
@@ -43,6 +58,8 @@ message EspNowRestart {
message EspNowDiscover {
uint32 network = 1;
/** Master is in an OTA session (UART upload or ESP-NOW distribution). */
bool master_ota_pending = 2;
}
message EspNowSlavePresence {
@@ -158,5 +175,7 @@ message EspNowMessage {
EspNowBatteryReport battery_report = 17;
EspNowTapNotify tap_notify = 18;
EspNowTapEvent tap_event = 19;
EspNowEchoPing echo_ping = 20;
EspNowEchoPong echo_pong = 21;
}
}
+12
View File
@@ -87,6 +87,12 @@ PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
PB_BIND(alox_EspNowUnicastTestResponse, alox_EspNowUnicastTestResponse, AUTO)
PB_BIND(alox_EspNowEchoPingRequest, alox_EspNowEchoPingRequest, AUTO)
PB_BIND(alox_EspNowEchoPingResponse, alox_EspNowEchoPingResponse, AUTO)
PB_BIND(alox_LedRingProgressRequest, alox_LedRingProgressRequest, AUTO)
@@ -105,6 +111,12 @@ PB_BIND(alox_RestartRequest, alox_RestartRequest, AUTO)
PB_BIND(alox_RestartResponse, alox_RestartResponse, AUTO)
PB_BIND(alox_SetLogLevelRequest, alox_SetLogLevelRequest, AUTO)
PB_BIND(alox_SetLogLevelResponse, alox_SetLogLevelResponse, AUTO)
PB_BIND(alox_OtaStartPayload, alox_OtaStartPayload, AUTO)
+113 -5
View File
@@ -32,7 +32,11 @@ typedef enum _alox_MessageType {
alox_MessageType_BATTERY_STATUS = 26,
alox_MessageType_TAP_NOTIFY = 27,
/* * Combined cached accel + tap poll (one UART round-trip, ~16 ms cadence). */
alox_MessageType_CACHE_STATUS = 29
alox_MessageType_CACHE_STATUS = 29,
/* * Host → master → slave → master: timestamp echo round-trip (latency test). */
alox_MessageType_ESPNOW_ECHO_PING = 30,
/* * Host → master: get/set ESP-IDF log level for tag "*" (global). */
alox_MessageType_SET_LOG_LEVEL = 31
} alox_MessageType;
typedef enum _alox_TapKind {
@@ -235,8 +239,24 @@ typedef struct _alox_EspNowUnicastTestResponse {
uint32_t seq;
} alox_EspNowUnicastTestResponse;
/* * Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
typedef struct _alox_EspNowEchoPingRequest {
uint32_t client_id;
/* * Microseconds since Unix epoch (host clock). */
uint64_t timestamp_us;
} alox_EspNowEchoPingRequest;
typedef struct _alox_EspNowEchoPingResponse {
bool success;
uint32_t client_id;
/* * Echoed host timestamp from goTool request. */
uint64_t timestamp_us;
/* * esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
uint32_t esp_rtt_us;
} alox_EspNowEchoPingResponse;
/* Host → master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color. */
mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low. */
typedef struct _alox_LedRingProgressRequest {
uint32_t mode;
/* * 0100: fraction of ring LEDs to light (mode=progress) */
@@ -289,6 +309,18 @@ typedef struct _alox_RestartResponse {
uint32_t client_id;
} 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). */
typedef struct _alox_OtaStartPayload {
uint32_t total_size;
@@ -371,6 +403,10 @@ typedef struct _alox_UartMessage {
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;
} alox_UartMessage;
@@ -381,8 +417,8 @@ extern "C" {
/* Helper constants for enums */
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN
#define _alox_MessageType_MAX alox_MessageType_CACHE_STATUS
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_CACHE_STATUS+1))
#define _alox_MessageType_MAX alox_MessageType_SET_LOG_LEVEL
#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
@@ -429,6 +465,10 @@ extern "C" {
@@ -460,12 +500,16 @@ extern "C" {
#define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}}
#define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_EspNowEchoPingRequest_init_default {0, 0}
#define alox_EspNowEchoPingResponse_init_default {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_default {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_default {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_default {0}
#define alox_EspNowFindMeResponse_init_default {0, 0}
#define alox_RestartRequest_init_default {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_OtaPayload_init_default {0, {0, {0}}}
#define alox_OtaEndPayload_init_default {0}
@@ -500,12 +544,16 @@ extern "C" {
#define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_EspNowEchoPingRequest_init_zero {0, 0}
#define alox_EspNowEchoPingResponse_init_zero {0, 0, 0, 0}
#define alox_LedRingProgressRequest_init_zero {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
#define alox_LedRingProgressResponse_init_zero {0, 0, 0, 0, 0, 0}
#define alox_EspNowFindMeRequest_init_zero {0}
#define alox_EspNowFindMeResponse_init_zero {0, 0}
#define alox_RestartRequest_init_zero {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_OtaPayload_init_zero {0, {0, {0}}}
#define alox_OtaEndPayload_init_zero {0}
@@ -599,6 +647,12 @@ extern "C" {
#define alox_EspNowUnicastTestRequest_seq_tag 2
#define alox_EspNowUnicastTestResponse_success_tag 1
#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_progress_tag 2
#define alox_LedRingProgressRequest_digit_tag 3
@@ -623,6 +677,10 @@ extern "C" {
#define alox_RestartRequest_client_id_tag 1
#define alox_RestartResponse_success_tag 1
#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_OtaPayload_seq_tag 1
#define alox_OtaPayload_data_tag 2
@@ -671,6 +729,10 @@ extern "C" {
#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 */
#define alox_UartMessage_FIELDLIST(X, a) \
@@ -703,7 +765,11 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,battery_status_response,payload.batt
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_request,payload.tap_notify_request), 29) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_notify_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,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_DEFAULT NULL
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
@@ -735,6 +801,10 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,cache_status_response,payload.cache_
#define alox_UartMessage_payload_tap_notify_response_MSGTYPE alox_TapNotifyResponse
#define alox_UartMessage_payload_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) \
@@ -934,6 +1004,20 @@ X(a, STATIC, SINGULAR, UINT32, seq, 2)
#define alox_EspNowUnicastTestResponse_CALLBACK 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) \
X(a, STATIC, SINGULAR, UINT32, mode, 1) \
X(a, STATIC, SINGULAR, UINT32, progress, 2) \
@@ -982,6 +1066,18 @@ X(a, STATIC, SINGULAR, UINT32, client_id, 2)
#define alox_RestartResponse_CALLBACK NULL
#define alox_RestartResponse_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) \
X(a, STATIC, SINGULAR, UINT32, total_size, 1)
#define alox_OtaStartPayload_CALLBACK NULL
@@ -1057,12 +1153,16 @@ extern const pb_msgdesc_t alox_CacheClientStatus_msg;
extern const pb_msgdesc_t alox_CacheStatusResponse_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_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_LedRingProgressResponse_msg;
extern const pb_msgdesc_t alox_EspNowFindMeRequest_msg;
extern const pb_msgdesc_t alox_EspNowFindMeResponse_msg;
extern const pb_msgdesc_t alox_RestartRequest_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_OtaPayload_msg;
extern const pb_msgdesc_t alox_OtaEndPayload_msg;
@@ -1099,12 +1199,16 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_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_LedRingProgressResponse_fields &alox_LedRingProgressResponse_msg
#define alox_EspNowFindMeRequest_fields &alox_EspNowFindMeRequest_msg
#define alox_EspNowFindMeResponse_fields &alox_EspNowFindMeResponse_msg
#define alox_RestartRequest_fields &alox_RestartRequest_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_OtaPayload_fields &alox_OtaPayload_msg
#define alox_OtaEndPayload_fields &alox_OtaEndPayload_msg
@@ -1136,6 +1240,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_CacheStatusRequest_size 0
#define alox_CacheStatusResponse_size 736
#define alox_ClientInput_size 22
#define alox_EspNowEchoPingRequest_size 17
#define alox_EspNowEchoPingResponse_size 25
#define alox_EspNowFindMeRequest_size 6
#define alox_EspNowFindMeResponse_size 8
#define alox_EspNowUnicastTestRequest_size 12
@@ -1152,6 +1258,8 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_OtaStatusPayload_size 24
#define alox_RestartRequest_size 6
#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
+37 -1
View File
@@ -29,6 +29,10 @@ enum MessageType {
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 {
@@ -63,6 +67,10 @@ message UartMessage {
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;
}
}
@@ -255,8 +263,24 @@ message EspNowUnicastTestResponse {
uint32 seq = 2;
}
/** Host → master: ESP-NOW echo ping to one slave (timestamp echoed back). */
message EspNowEchoPingRequest {
uint32 client_id = 1;
/** Microseconds since Unix epoch (host clock). */
uint64 timestamp_us = 2;
}
message EspNowEchoPingResponse {
bool success = 1;
uint32 client_id = 2;
/** Echoed host timestamp from goTool request. */
uint64 timestamp_us = 3;
/** esp_timer_get_time() delta from ping send to pong recv (master→slave→master). */
uint32 esp_rtt_us = 4;
}
// Host master: LED ring on master (client_id=0) and/or slaves via ESP-NOW.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color.
// mode: 0=clear, 1=progress (0100 %), 2=digit (010), 3=blink, 4=find-me, 5=all LEDs solid color, 6=battery-low.
message LedRingProgressRequest {
uint32 mode = 1;
/** 0100: fraction of ring LEDs to light (mode=progress) */
@@ -309,6 +333,18 @@ message RestartResponse {
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).
message OtaStartPayload {
uint32 total_size = 1;
+2 -1
View File
@@ -52,7 +52,8 @@ void init_uart(QueueHandle_t cmd_queue) {
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
};
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) {
ESP_LOGE(TAG, "uart_driver_install failed: %s", esp_err_to_name(err));
return;
+4 -1
View File
@@ -16,7 +16,10 @@
#define UART_RXD_PIN 3
#define UART_BUF_SIZE 2048
#define UART_BUF_SIZE 4096
/** Driver RX ring — must hold a full OTA block burst (~20 × ~215 B frames). */
#define UART_DRIVER_RX_BUF_SIZE 16384
#define UART_DRIVER_TX_BUF_SIZE 4096
#define START_MARKER 0xAA
#define STOP_MARKER 0xCC
#define MAX_BUF_SIZE 252