Compare commits

...
3 Commits
Author SHA1 Message Date
skrueckenandCursor af20635047 Harden BLE host link and expose latency tools in the dashboard.
Add BLE RTT/stream tests and live tick median in the UI, fix concurrent WebSocket writes and late BATTERY replies, and skip 2M PHY updates that assert under ESP-NOW coexistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 23:27:39 +02:00
skrueckenandCursor aa67edfe94 Add dynamic UART/BLE host connect for serve, dashboard, and WS.
Serve starts without a required port; REST/WS/UI can scan and switch transports, with BLE live ticks and UART-only OTA.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 22:40:28 +02:00
skrueckenandCursor 92a13d7fd0 Add BLE host transport selectable via DIP8 alongside UART.
Master can speak the same framed protobuf protocol over NimBLE or UART; goTool gains -ble, ble-rtt, and ble-stream timing checks for ESP-NOW coexistence.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 16:11:10 +02:00
44 changed files with 4555 additions and 730 deletions
+9 -4
View File
@@ -79,8 +79,9 @@ sequenceDiagram
| Feld | Quelle | Bedeutung | | Feld | Quelle | Bedeutung |
|------|--------|-----------| |------|--------|-----------|
| `master` | `DIP_MASTER` (GPIO 4): Low = Master | Steuert UART + Registry-Nutzung | | `master` | `DIP_MASTER` (GPIO 4): Low = Master | Steuert Host-Transport + Registry |
| `network` | IO-Expander, Bits 5–8 (nibble reversed) | 1–8 → WiFi/ESP-NOW-Kanal | | `network` | IO-Expander 8er-DIP-Bank, High-Nibble PCB-reversed; DIP1–7 one-hot | 1–7 → WiFi/ESP-NOW-Kanal |
| `host_ble` | Expander DIP8 (nach Reverse+Invert) | `false`=UART, `true`=BLE (Boot only) |
| `running_partition` | `esp_ota_get_running_partition()` | Aktives OTA-Label (`ota_0` / `ota_1`) | | `running_partition` | `esp_ota_get_running_partition()` | Aktives OTA-Label (`ota_0` / `ota_1`) |
**Dateien:** `main/app_config.h`, `main/powerpod.c`, `main/powerpod.h` (Pins). **Dateien:** `main/app_config.h`, `main/powerpod.c`, `main/powerpod.h` (Pins).
@@ -96,17 +97,21 @@ sequenceDiagram
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Dispatch: cmd_handler.c (Queue + vCmdDispatcherTask) │ │ Dispatch: cmd_handler.c (Queue + vCmdDispatcherTask) │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Transport UART: uart.c (Framing) │ ESP-NOW: esp_now_comm │ │ Host transport: uart.c / ble_host.c (Framing via host_transport) │
│ ESP-NOW: esp_now_comm │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Protokoll: uart_messages.proto │ esp_now_messages.proto │ │ Protokoll: uart_messages.proto │ esp_now_messages.proto │
│ Codec: nanopb (proto/*.pb.c) │ esp_now_proto.c │ │ Codec: nanopb (proto/*.pb.c) │ esp_now_proto.c │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ Domäne: client_registry, bosch456, led_ring, ota_*, board │ │ Domäne: client_registry, bosch456, led_ring, ota_*, board │
├─────────────────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────────────────┤
│ ESP-IDF: UART, WiFi, ESP-NOW, I2C, ADC, OTA, NVS, FreeRTOS │ │ ESP-IDF: UART, NimBLE, WiFi, ESP-NOW, I2C, ADC, OTA, NVS │
└─────────────────────────────────────────────────────────────┘ └─────────────────────────────────────────────────────────────┘
``` ```
**Host UART vs BLE:** Nur einer aktiv (DIP8). Gleiches Framing (`0xAA`…`0xCC`) und dieselbe `cmd_queue`. BLE: NUS-ähnliche GATT-Char, Write=RX, Notify=TX. `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA nur UART**. Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (8–16 ms) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
--- ---
## 4. Datenfluss: Commands (Befehle) ## 4. Datenfluss: Commands (Befehle)
+9 -6
View File
@@ -47,14 +47,15 @@ Zielbild für den Host: Befehle an den Master senden; der Master steuert Slaves
|--------|------|----------------| |--------|------|----------------|
| DIP Master/Slave | 4 | `powerpod.h` — Low = Master | | DIP Master/Slave | 4 | `powerpod.h` — Low = Master |
| I2C SCL / SDA | 5 / 6 | IO-Expander `0x20`, BMA456 `0x18` | | I2C SCL / SDA | 5 / 6 | IO-Expander `0x20`, BMA456 `0x18` |
| UART1 TX / RX | **2 / 3** | `uart.h` (Adapter: ESP-TX → Host-RX) | | Expander DIP-Bank (8) | PCF/TCA @ `0x20` | DIP1–7 = Netzwerk 1–7 (one-hot); **DIP8 ON = BLE host**, OFF = UART. High-Nibble auf PCB vertauscht → `reverse_high_nibble_lut`. Wechsel nur nach Reboot. |
| UART1 TX / RX | **2 / 3** | `uart.h` (Adapter: ESP-TX → Host-RX); nur wenn DIP8 = UART |
| LED-Ring (WS2812) | 7 | `led_ring.c` | | LED-Ring (WS2812) | 7 | `led_ring.c` |
| BMA456 Interrupt | 10 | `bosch456.c` | | BMA456 Interrupt | 10 | `bosch456.c` |
| Taster | 12 | `board_input.c` | | Taster | 12 | `board_input.c` |
| LiPo ADC 1 | 1 | `board_input.c` | | LiPo ADC 1 | 1 | `board_input.c` |
| LiPo ADC 2 | 11 | `board_input.c` | | LiPo ADC 2 | 11 | `board_input.c` |
**UART (Host-Protokoll):** `UART_NUM_1`, **921600** Baud, 8N1, kein Flow-Control. **Host-Protokoll (UART oder BLE):** Gleiches Framing (`0xAA`…`0xCC`) + nanopb. **UART:** `UART_NUM_1`, **921600** Baud, 8N1. **BLE:** Gerät `Powerpod`, NUS-UUIDs `6e400001`/`6e400002` (Write=RX, Notify=TX). goTool CLI: `-port …` oder `-ble` / `-ble-name Powerpod`. **`gotool serve`** startet ohne Pflicht-Gerät; Dashboard/REST/WS verbinden UART oder BLE dynamisch (`-port`/`-ble` nur optionale Vorbelegung). **OTA nur UART** (DIP8=0). Bei BLE + Accel-Stream: periodische `BLE_STREAM_TICK`-Notifies (`period_ms` 8–16) speisen Live-Accel im Dashboard/API.
**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. **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.
@@ -94,20 +95,20 @@ idf.py -p /dev/ttyUSB0 flash monitor
2. `board_input_init_adc_only()` — LiPo-ADC 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) 3. Bei `POWERPOD_BATTERY_UV_ENABLE`: `battery_uv_evaluate_boot()` → ggf. Energiesparmodus (nur LED + ADC, **kein** weiterer Init)
4. DIP → `app_config.master` 4. DIP → `app_config.master`
5. I2C + IO-Expander → `app_config.network` (1–8) 5. I2C + IO-Expander → `app_config.network` (1–7) + `app_config.host_ble` (DIP8)
6. `init_bma456()` — optional, bei Fehler weiter ohne Sensor 6. `init_bma456()` — optional, bei Fehler weiter ohne Sensor
7. `pod_settings_apply_accel_deadzone()` wenn Sensor da 7. `pod_settings_apply_accel_deadzone()` wenn Sensor da
8. OTA-Partition → `app_config.running_partition` 8. OTA-Partition → `app_config.running_partition`
9. `board_input_start_lipo_monitor()` + `board_input_init_button()` 9. `board_input_start_lipo_monitor()` + `board_input_init_button()`
10. `esp_now_comm_init(&app_config)` — **normaler Pfad** (Master + Slave) 10. `esp_now_comm_init(&app_config)` — **normaler Pfad** (Master + Slave)
11. `led_ring_init()` 11. `led_ring_init()`
12. **Nur Master:** `cmd_queue` → `init_cmdHandler` → `init_uart` → alle `cmd_*_register()` 12. **Nur Master:** `cmd_queue` → `init_cmdHandler` → `init_uart` **oder** `init_ble_host` → alle `cmd_*_register()`
### 4.2 Master vs. Slave ### 4.2 Master vs. Slave
| Funktion | Master | Slave | | Funktion | Master | Slave |
|----------|--------|-------| |----------|--------|-------|
| UART Command-Handler | Ja | Nein | | Host Command-Handler (UART oder BLE) | Ja | Nein |
| ESP-NOW Discover senden | Ja (Broadcast) | Nein | | ESP-NOW Discover senden | Ja (Broadcast) | Nein |
| ESP-NOW auf Discover reagieren | Nein | Ja | | ESP-NOW auf Discover reagieren | Nein | Ja |
| `client_registry` für Fremdknoten | Ja | Nein (nur eigener Join-State) | | `client_registry` für Fremdknoten | Ja | Nein (nur eigener Join-State) |
@@ -233,10 +234,11 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
| 21 | OTA_SLAVE_PROGRESS | `cmd_ota_slave_progress.c` | Fortschritt pro Slave | | 21 | OTA_SLAVE_PROGRESS | `cmd_ota_slave_progress.c` | Fortschritt pro Slave |
| 22 | FIND_ME | `cmd_espnow_find_me.c` | LED Locate | | 22 | FIND_ME | `cmd_espnow_find_me.c` | LED Locate |
| 23 | RESTART | `cmd_restart.c` | Master oder Slave reboot | | 23 | RESTART | `cmd_restart.c` | Master oder Slave reboot |
| 25 | ACCEL_STREAM | `cmd_accel_stream.c` | ESP-NOW Accel-Stream an/aus | | 25 | ACCEL_STREAM | `cmd_accel_stream.c` | ESP-NOW Accel-Stream an/aus (+ `period_ms` für BLE-Push) |
| 26 | BATTERY_STATUS | `cmd_battery.c` | Cache LiPo Master + Slaves | | 26 | BATTERY_STATUS | `cmd_battery.c` | Cache LiPo Master + Slaves |
| 27 | TAP_NOTIFY | `cmd_tap_notify.c` | Tap-Weiterleitung konfigurieren | | 27 | TAP_NOTIFY | `cmd_tap_notify.c` | Tap-Weiterleitung konfigurieren |
| 29 | CACHE_STATUS | `cmd_cache_status.c` | Accel + Tap Cache (ein Round-Trip) | | 29 | CACHE_STATUS | `cmd_cache_status.c` | Accel + Tap Cache (ein Round-Trip) |
| 33 | BLE_STREAM_TICK | `ble_stream.c` | Master→Host Notify (nur BLE): Snapshot + `esp_timestamp_us` |
| 30 | ESPNOW_ECHO_PING | `cmd_espnow_echo_ping.c` | Timestamp-Echo über ESP-NOW (Latenztest) | | 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 | | 31 | SET_LOG_LEVEL | `cmd_set_log_level.c` | ESP-IDF-Log-Level global (`"*"`) lesen/setzen |
| 32 | TAP_CONFIG | `cmd_tap_config.c` | BMA456 Multitap-Parameter + NVS | | 32 | TAP_CONFIG | `cmd_tap_config.c` | BMA456 Multitap-Parameter + NVS |
@@ -258,6 +260,7 @@ Nur auf dem **Master** registriert (`powerpod.c`). IDs aus `MessageType` in `uar
### 7.4 ACCEL_STREAM (25) ### 7.4 ACCEL_STREAM (25)
- Optional `period_ms` (8–16): BLE-Host-Push-Intervall für `BLE_STREAM_TICK` (UART ignoriert; Default 16).
- Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`. - Master setzt `client_registry_set_accel_stream` und sendet `ESPNOW_SET_ACCEL_STREAM`.
- Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms. - Slave-Task `slave_accel_stream_task` sendet `ESPNOW_ACCEL_SAMPLE` alle 16 ms.
+8 -6
View File
@@ -61,16 +61,18 @@ See [`testdata/README.md`](testdata/README.md) for the JSON schema.
### Web dashboard ### Web dashboard
Polls the master over UART and pushes state to the browser via WebSocket (Alpine.js + Bootstrap 5). Starts without a required host link. Connect UART or BLE from the UI (or pre-connect with `-port` / `-ble`). State is pushed to the browser via WebSocket (Alpine.js + Bootstrap 5). OTA remains UART-only.
```bash ```bash
go run . serve
go run . -port /dev/ttyUSB0 serve go run . -port /dev/ttyUSB0 serve
go run . -port /dev/ttyUSB0 serve -addr :8080 -interval 2s go run . -ble serve
go run . -port /dev/ttyUSB0 serve -api-addr :8081 -accel-interval 16ms go run . serve -addr :8080 -interval 2s
go run . serve -api-addr :8081 -accel-interval 16ms
make gotool-serve PORT=/dev/ttyUSB0 make gotool-serve PORT=/dev/ttyUSB0
``` ```
Open [http://localhost:8080](http://localhost:8080) — shows master firmware info and the ESP-NOW client table from `CLIENT_INFO`. Open [http://localhost:8080](http://localhost:8080) — Host-Verbindung panel (UART ports / BLE scan), master firmware info, ESP-NOW client table.
**Tap (dashboard):** two independent controls per slave: **Tap (dashboard):** two independent controls per slave:
@@ -81,11 +83,11 @@ Open [http://localhost:8080](http://localhost:8080) — shows master firmware in
Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`). Enable notify first, then turn receive on to see events. Same split as the external WebSocket API (`set_tap_notify` vs `set_tap_stream`).
If the UART device is unplugged or the port disappears, `serve` keeps running and retries on each poll interval; the UI shows **UART off** until the port is available again. If the host disconnects, `serve` keeps running; the badge shows **off** until you reconnect from the UI or via REST/WS.
### HTTP / WebSocket API ### HTTP / WebSocket API
`serve` also listens on **`:8081`** for external programs (`-api-addr`, empty to disable). Same UART as the dashboard. `serve` also listens on **`:8081`** for external programs (`-api-addr`, empty to disable). Same `managedHost` as the dashboard (UART or BLE).
| Doc | Content | | Doc | Content |
|-----|---------| |-----|---------|
+129
View File
@@ -0,0 +1,129 @@
package main
import (
"encoding/json"
"errors"
"io"
"net/http"
"time"
)
type bleRttAPIRequest struct {
Count *int `json:"count"`
Warmup *int `json:"warmup"`
SettleS *float64 `json:"settle_s"`
}
type bleStreamAPIRequest struct {
ClientID *uint32 `json:"client_id"`
PeriodMs *uint32 `json:"period_ms"`
Count *int `json:"count"`
TolMs *float64 `json:"tol_ms"`
TimeoutS *float64 `json:"timeout_s"`
}
func mountBleTestAPI(mux *http.ServeMux, link *managedHost) {
mux.HandleFunc("/api/ble-rtt", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveBleRtt(w, r, link)
})
mux.HandleFunc("/api/ble-stream", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
serveBleStreamTest(w, r, link)
})
}
func requireBLEHost(w http.ResponseWriter, link *managedHost) bool {
if link.Transport() != hostTransportBLE || !link.IsConnected() {
writeJSON(w, http.StatusBadRequest, bleRttResult{
Error: "BLE host required (connect via BLE, DIP8=1)",
})
return false
}
return true
}
func serveBleRtt(w http.ResponseWriter, r *http.Request, link *managedHost) {
if !requireBLEHost(w, link) {
return
}
var body bleRttAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
writeJSON(w, http.StatusBadRequest, bleRttResult{Error: "invalid JSON"})
return
}
count, warmup := 50, 5
settle := 0.0
if body.Count != nil {
count = *body.Count
}
if body.Warmup != nil {
warmup = *body.Warmup
}
if body.SettleS != nil {
settle = *body.SettleS
}
if !link.beginPausePoll() {
writeJSON(w, http.StatusConflict, bleRttResult{Error: "host busy"})
return
}
defer link.endPausePoll()
out := measureBleRtt(link, count, warmup, time.Duration(settle*float64(time.Second)))
status := http.StatusOK
if !out.Success {
status = http.StatusBadRequest
}
writeJSON(w, status, out)
}
func serveBleStreamTest(w http.ResponseWriter, r *http.Request, link *managedHost) {
if !requireBLEHost(w, link) {
return
}
var body bleStreamAPIRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
writeJSON(w, http.StatusBadRequest, bleStreamResult{Error: "invalid JSON"})
return
}
count := 50
periodMs := uint32(16)
tolMs := 2.0
timeoutS := 15.0
var clientID uint32
if body.Count != nil {
count = *body.Count
}
if body.PeriodMs != nil {
periodMs = *body.PeriodMs
}
if body.TolMs != nil {
tolMs = *body.TolMs
}
if body.TimeoutS != nil {
timeoutS = *body.TimeoutS
}
if body.ClientID != nil {
clientID = *body.ClientID
}
if !link.beginPausePoll() {
writeJSON(w, http.StatusConflict, bleStreamResult{Error: "host busy"})
return
}
defer link.endPausePoll()
out := measureBleStream(link, clientID, periodMs, count, tolMs, timeoutS)
status := http.StatusOK
if !out.Success {
status = http.StatusBadRequest
}
writeJSON(w, status, out)
}
+140
View File
@@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"net/http"
"time"
)
type hostAPIResponse struct {
Transport string `json:"transport"`
Endpoint string `json:"endpoint"`
Connected bool `json:"connected"`
BleName string `json:"ble_name,omitempty"`
Error string `json:"error,omitempty"`
}
type hostConnectRequest struct {
Transport string `json:"transport"` // uart|ble
Port string `json:"port,omitempty"`
Address string `json:"address,omitempty"`
Name string `json:"name,omitempty"`
Baud int `json:"baud,omitempty"`
}
type bleScanRequest struct {
TimeoutS float64 `json:"timeout_s"`
NameFilter string `json:"name_filter"`
}
type bleScanResponse struct {
Devices []bleScanDevice `json:"devices"`
Error string `json:"error,omitempty"`
}
type serialPortsResponse struct {
Ports []string `json:"ports"`
Error string `json:"error,omitempty"`
}
func mountHostAPI(mux *http.ServeMux, link *managedHost) {
mux.HandleFunc("/api/host", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
st := link.HostStatus()
writeJSON(w, http.StatusOK, hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
BleName: st["ble_name"].(string),
})
})
mux.HandleFunc("/api/host/connect", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body hostConnectRequest
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeJSON(w, http.StatusBadRequest, hostAPIResponse{Error: "invalid JSON"})
return
}
var err error
switch body.Transport {
case hostTransportUART:
err = link.ConnectUART(body.Port, body.Baud)
case hostTransportBLE:
err = link.ConnectBLE(body.Address, body.Name)
default:
writeJSON(w, http.StatusBadRequest, hostAPIResponse{Error: "transport must be uart or ble"})
return
}
st := link.HostStatus()
resp := hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
BleName: st["ble_name"].(string),
}
if err != nil {
resp.Error = err.Error()
resp.Connected = false
writeJSON(w, http.StatusBadRequest, resp)
return
}
writeJSON(w, http.StatusOK, resp)
})
mux.HandleFunc("/api/host/disconnect", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
_ = link.Disconnect()
st := link.HostStatus()
writeJSON(w, http.StatusOK, hostAPIResponse{
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: false,
})
})
mux.HandleFunc("/api/serial/ports", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
ports, err := listSerialPorts()
if err != nil {
writeJSON(w, http.StatusOK, serialPortsResponse{Ports: []string{}, Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, serialPortsResponse{Ports: ports})
})
mux.HandleFunc("/api/ble/scan", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
var body bleScanRequest
_ = json.NewDecoder(r.Body).Decode(&body)
timeout := time.Duration(body.TimeoutS * float64(time.Second))
if timeout <= 0 {
timeout = 5 * time.Second
}
// Disconnect active BLE before scan (BlueZ adapter contention).
if link.Transport() == hostTransportBLE {
_ = link.Disconnect()
}
devs, err := scanBLEDevices(timeout, body.NameFilter)
if err != nil {
writeJSON(w, http.StatusOK, bleScanResponse{Devices: []bleScanDevice{}, Error: err.Error()})
return
}
writeJSON(w, http.StatusOK, bleScanResponse{Devices: devs})
})
}
+6
View File
@@ -93,6 +93,8 @@ type otaAPIResponse struct {
} }
func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) { func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) {
mountHostAPI(mux, link)
mountBleTestAPI(mux, link)
mountLiveStreamAPI(mux, hub) mountLiveStreamAPI(mux, hub)
mountAccelStreamAPI(mux, link, hub, streamCtl) mountAccelStreamAPI(mux, link, hub, streamCtl)
mountTapAPI(mux, link, hub, tapCtl) mountTapAPI(mux, link, hub, tapCtl)
@@ -157,6 +159,10 @@ func mountServeAPI(mux *http.ServeMux, link *managedSerial, hub *wsHub, streamCt
} }
func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) { func serveOTAUpload(w http.ResponseWriter, r *http.Request, link *managedSerial, hub *wsHub) {
if link.Transport() != hostTransportUART || !link.IsConnected() {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "OTA requires UART host (DIP8=0)"})
return
}
if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil { if err := r.ParseMultipartForm(otaMaxFirmwareSize); err != nil {
writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "invalid form"}) writeJSON(w, http.StatusBadRequest, otaAPIResponse{Error: "invalid form"})
return return
+240 -89
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"log" "log"
"net/http" "net/http"
"sync" "sync"
@@ -39,6 +40,9 @@ type InputClientSample struct {
type InputStreamMessage struct { type InputStreamMessage struct {
Type string `json:"type"` // "hello" | "input" Type string `json:"type"` // "hello" | "input"
Serial string `json:"serial_port,omitempty"` Serial string `json:"serial_port,omitempty"`
Transport string `json:"transport,omitempty"`
Endpoint string `json:"endpoint,omitempty"`
Connected *bool `json:"connected,omitempty"`
IntervalMs int `json:"interval_ms,omitempty"` IntervalMs int `json:"interval_ms,omitempty"`
PreFetchMs int `json:"pre_fetch_ms,omitempty"` PreFetchMs int `json:"pre_fetch_ms,omitempty"`
TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"` TapDisplayMinMs int `json:"tap_display_min_ms,omitempty"`
@@ -51,6 +55,33 @@ type InputStreamMessage struct {
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
} }
// HostStatusMessage is the reply to get_host / host_connect / host_disconnect.
type HostStatusMessage struct {
Type string `json:"type"` // "host_status"
Transport string `json:"transport"`
Endpoint string `json:"endpoint"`
Connected bool `json:"connected"`
BleName string `json:"ble_name,omitempty"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// SerialPortsMessage is the reply to list_serial_ports.
type SerialPortsMessage struct {
Type string `json:"type"` // "serial_ports"
Ports []string `json:"ports"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// BleScanResultMessage is the reply to ble_scan.
type BleScanResultMessage struct {
Type string `json:"type"` // "ble_scan_result"
Devices []bleScanDevice `json:"devices"`
Success bool `json:"success"`
Error string `json:"error,omitempty"`
}
// StreamStatusMessage is the reply to set_stream / get_stream (this connection). // StreamStatusMessage is the reply to set_stream / get_stream (this connection).
type StreamStatusMessage struct { type StreamStatusMessage struct {
Type string `json:"type"` // "stream_status" Type string `json:"type"` // "stream_status"
@@ -116,12 +147,23 @@ type accelWSCommand struct {
DoubleTap *bool `json:"double_tap"` DoubleTap *bool `json:"double_tap"`
Triple *bool `json:"triple"` Triple *bool `json:"triple"`
AllClients bool `json:"all_clients"` AllClients bool `json:"all_clients"`
// Host connect / BLE scan
Transport string `json:"transport"`
Port string `json:"port"`
Address string `json:"address"`
Name string `json:"name"`
Baud int `json:"baud"`
TimeoutS float64 `json:"timeout_s"`
NameFilter string `json:"name_filter"`
} }
type APIInfoResponse struct { type APIInfoResponse struct {
Name string `json:"name"` Name string `json:"name"`
Version string `json:"version"` Version string `json:"version"`
SerialPort string `json:"serial_port"` SerialPort string `json:"serial_port"`
Transport string `json:"transport"`
Endpoint string `json:"endpoint"`
Connected bool `json:"connected"`
WebSocket string `json:"websocket"` WebSocket string `json:"websocket"`
DefaultIntervalMs int `json:"default_interval_ms"` DefaultIntervalMs int `json:"default_interval_ms"`
DefaultPreFetchMs int `json:"default_pre_fetch_ms"` DefaultPreFetchMs int `json:"default_pre_fetch_ms"`
@@ -139,12 +181,31 @@ type cachedTapEvent struct {
type wsSubscriber struct { type wsSubscriber struct {
conn *websocket.Conn conn *websocket.Conn
writeMu sync.Mutex // gorilla: one writer per connection
receiveInput bool receiveInput bool
interval time.Duration interval time.Duration
preFetch time.Duration preFetch time.Duration
lastInputSent time.Time lastInputSent time.Time
} }
func (sub *wsSubscriber) writeBytes(data []byte) error {
if sub == nil {
return fmt.Errorf("nil subscriber")
}
sub.writeMu.Lock()
defer sub.writeMu.Unlock()
_ = sub.conn.SetWriteDeadline(time.Now().Add(2 * time.Second))
return sub.conn.WriteMessage(websocket.TextMessage, data)
}
func (sub *wsSubscriber) writeJSON(v any) {
data, err := json.Marshal(v)
if err != nil {
return
}
_ = sub.writeBytes(data)
}
type pendingInputCache struct { type pendingInputCache struct {
cache *pb.CacheStatusResponse cache *pb.CacheStatusResponse
readAt time.Time readAt time.Time
@@ -196,7 +257,7 @@ func clampPreFetch(d time.Duration) time.Duration {
return d return d
} }
func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubscriber { func (h *accelStreamHub) register(conn *websocket.Conn, link *managedSerial) *wsSubscriber {
sub := &wsSubscriber{ sub := &wsSubscriber{
conn: conn, conn: conn,
receiveInput: false, receiveInput: false,
@@ -207,14 +268,20 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
h.clients[conn] = sub h.clients[conn] = sub
h.mu.Unlock() h.mu.Unlock()
st := link.HostStatus()
connected := st["connected"].(bool)
hello := InputStreamMessage{ hello := InputStreamMessage{
Type: "hello", Type: "hello",
Serial: portName, Serial: st["endpoint"].(string),
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: &connected,
IntervalMs: int(h.defaultInterval / time.Millisecond), IntervalMs: int(h.defaultInterval / time.Millisecond),
PreFetchMs: int(h.defaultPreFetch / time.Millisecond), PreFetchMs: int(h.defaultPreFetch / time.Millisecond),
TapDisplayMinMs: apiTapDisplayMinMs, TapDisplayMinMs: apiTapDisplayMinMs,
Note: "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection", Note: "set_tap_notify configures slave S/D/T only; set_stream enables input polling/push on this connection",
Commands: []string{ Commands: []string{
"get_host", "list_serial_ports", "ble_scan", "host_connect", "host_disconnect",
"list_clients", "list_clients",
"set_stream", "get_stream", "set_stream", "get_stream",
"set_input_stream", "get_input_stream", "set_input_stream", "get_input_stream",
@@ -222,9 +289,7 @@ func (h *accelStreamHub) register(conn *websocket.Conn, portName string) *wsSubs
"set_led_ring", "get_battery", "set_led_ring", "get_battery",
}, },
} }
if data, err := json.Marshal(hello); err == nil { sub.writeJSON(hello)
_ = conn.WriteMessage(websocket.TextMessage, data)
}
return sub return sub
} }
@@ -420,7 +485,11 @@ func (h *accelStreamHub) deliverInput(msg InputStreamMessage) {
now := time.Now() now := time.Now()
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() type pending struct {
conn *websocket.Conn
sub *wsSubscriber
}
var out []pending
for conn, sub := range h.clients { for conn, sub := range h.clients {
if !sub.receiveInput { if !sub.receiveInput {
continue continue
@@ -429,13 +498,26 @@ func (h *accelStreamHub) deliverInput(msg InputStreamMessage) {
continue continue
} }
sub.lastInputSent = now sub.lastInputSent = now
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil { out = append(out, pending{conn: conn, sub: sub})
delete(h.clients, conn) }
_ = conn.Close() h.mu.Unlock()
for _, p := range out {
if err := p.sub.writeBytes(data); err != nil {
h.mu.Lock()
delete(h.clients, p.conn)
h.mu.Unlock()
_ = p.conn.Close()
} }
} }
} }
func (h *accelStreamHub) clientsFromCache(cache *pb.CacheStatusResponse) []InputClientSample {
h.mu.Lock()
defer h.mu.Unlock()
return h.inputClientsFromCacheLocked(cache, time.Now())
}
func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) { func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, stop <-chan struct{}) {
ticker := time.NewTicker(minAPIStreamInterval) ticker := time.NewTicker(minAPIStreamInterval)
defer ticker.Stop() defer ticker.Stop()
@@ -449,11 +531,31 @@ func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
case <-hub.configChanged: case <-hub.configChanged:
pending = nil pending = nil
case now := <-ticker.C: case now := <-ticker.C:
if !hub.anyWantsInput() || !inputPollingActive(dash, ctl, tapCtl) { if !hub.anyWantsInput() || !inputPollingActive(dash, ctl, tapCtl) || link.PollPaused() {
pending = nil pending = nil
continue continue
} }
// Prefer BLE_STREAM_TICK when on BLE (firmware push).
if link.Transport() == hostTransportBLE {
payload, err := link.NextStreamTick(20 * time.Millisecond)
if err == nil {
cache, cerr := cacheStatusFromBLEStreamPayload(payload)
if cerr == nil {
hub.ingestTapFromCache(cache)
hub.deliverInput(InputStreamMessage{
Type: "input",
T: time.Now().UnixNano(),
Success: true,
Clients: hub.clientsFromCache(cache),
})
pending = nil
continue
}
}
// Fall through to CACHE_STATUS poll if no tick yet.
}
needRead, needDeliver, waitPreFetch := hub.streamTiming(now) needRead, needDeliver, waitPreFetch := hub.streamTiming(now)
if needRead && pending == nil { if needRead && pending == nil {
@@ -487,14 +589,11 @@ func runInputStreamer(link *managedSerial, hub *accelStreamHub, dash *wsHub, ctl
} }
if pending.cache != nil && now.Sub(pending.readAt) >= waitPreFetch { if pending.cache != nil && now.Sub(pending.readAt) >= waitPreFetch {
hub.mu.RLock()
clients := hub.inputClientsFromCacheLocked(pending.cache, now)
hub.mu.RUnlock()
hub.deliverInput(InputStreamMessage{ hub.deliverInput(InputStreamMessage{
Type: "input", Type: "input",
T: ts, T: ts,
Success: true, Success: true,
Clients: clients, Clients: hub.clientsFromCache(pending.cache),
}) })
pending = nil pending = nil
} }
@@ -512,46 +611,29 @@ func inputPollingActive(dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl)
return dash != nil && dash.anyAccelStreamEnabled() return dash != nil && dash.anyAccelStreamEnabled()
} }
func writeStreamStatus(conn *websocket.Conn, msg StreamStatusMessage) { func writeStreamStatus(sub *wsSubscriber, msg StreamStatusMessage) {
data, err := json.Marshal(msg) sub.writeJSON(msg)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeBatteryStatus(conn *websocket.Conn, out batteryAPIResponse) { func writeBatteryStatus(sub *wsSubscriber, out batteryAPIResponse) {
out.Type = "battery_status" out.Type = "battery_status"
data, err := json.Marshal(out) sub.writeJSON(out)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeLedRingStatus(conn *websocket.Conn, out ledRingAPIResponse) { func writeLedRingStatus(sub *wsSubscriber, out ledRingAPIResponse) {
out.Type = "led_ring_status" out.Type = "led_ring_status"
data, err := json.Marshal(out) sub.writeJSON(out)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeInputStreamStatus(conn *websocket.Conn, out accelStreamAPIResponse) { func writeInputStreamStatus(sub *wsSubscriber, out accelStreamAPIResponse) {
msg := InputStreamStatusMessage{ sub.writeJSON(InputStreamStatusMessage{
Type: "input_stream_status", Type: "input_stream_status",
ClientID: out.ClientID, ClientID: out.ClientID,
Enabled: out.Enabled, Enabled: out.Enabled,
Success: out.Success, Success: out.Success,
SlavesUpdated: out.SlavesUpdated, SlavesUpdated: out.SlavesUpdated,
Error: out.Error, Error: out.Error,
} })
data, err := json.Marshal(msg)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo { func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo {
@@ -570,16 +652,12 @@ func clientInfoToAPI(c *pb.ClientInfo) APIClientInfo {
} }
} }
func writeClientList(conn *websocket.Conn, msg ClientListMessage) { func writeClientList(sub *wsSubscriber, msg ClientListMessage) {
data, err := json.Marshal(msg) sub.writeJSON(msg)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func writeTapNotifyStatus(conn *websocket.Conn, out tapNotifyAPIResponse) { func writeTapNotifyStatus(sub *wsSubscriber, out tapNotifyAPIResponse) {
msg := TapNotifyStatusMessage{ sub.writeJSON(TapNotifyStatusMessage{
Type: "tap_notify_status", Type: "tap_notify_status",
ClientID: out.ClientID, ClientID: out.ClientID,
Single: out.Single, Single: out.Single,
@@ -588,12 +666,7 @@ func writeTapNotifyStatus(conn *websocket.Conn, out tapNotifyAPIResponse) {
Success: out.Success, Success: out.Success,
SlavesUpdated: out.SlavesUpdated, SlavesUpdated: out.SlavesUpdated,
Error: out.Error, Error: out.Error,
} })
data, err := json.Marshal(msg)
if err != nil {
return
}
_ = conn.WriteMessage(websocket.TextMessage, data)
} }
func applyTapNotifyClientWS(link *managedSerial, dash *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse { func applyTapNotifyClientWS(link *managedSerial, dash *wsHub, tapCtl *tapNotifyCtl, clientID uint32, single, doubleTap, triple bool) tapNotifyAPIResponse {
@@ -626,18 +699,70 @@ func applyTapNotifyClientWS(link *managedSerial, dash *wsHub, tapCtl *tapNotifyC
return out return out
} }
func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) { func handleAccelWSCommand(sub *wsSubscriber, data []byte, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) {
var cmd accelWSCommand var cmd accelWSCommand
if err := json.Unmarshal(data, &cmd); err != nil { if err := json.Unmarshal(data, &cmd); err != nil {
writeStreamStatus(conn, StreamStatusMessage{Type: "stream_status", Error: "invalid JSON"}) writeStreamStatus(sub, StreamStatusMessage{Type: "stream_status", Error: "invalid JSON"})
return return
} }
switch cmd.Type { switch cmd.Type {
case "get_host":
writeHostStatus(sub, hostStatusFromLink(link, true, ""))
case "list_serial_ports":
ports, err := listSerialPorts()
msg := SerialPortsMessage{Type: "serial_ports", Ports: ports, Success: err == nil}
if err != nil {
msg.Error = err.Error()
msg.Ports = []string{}
}
writeWSJSON(sub, msg)
case "ble_scan":
timeout := time.Duration(cmd.TimeoutS * float64(time.Second))
if timeout <= 0 {
timeout = 5 * time.Second
}
if link.Transport() == hostTransportBLE {
_ = link.Disconnect()
}
devs, err := scanBLEDevices(timeout, cmd.NameFilter)
msg := BleScanResultMessage{Type: "ble_scan_result", Devices: devs, Success: err == nil}
if err != nil {
msg.Error = err.Error()
msg.Devices = []bleScanDevice{}
}
writeWSJSON(sub, msg)
case "host_connect":
var err error
switch cmd.Transport {
case hostTransportUART:
err = link.ConnectUART(cmd.Port, cmd.Baud)
case hostTransportBLE:
err = link.ConnectBLE(cmd.Address, cmd.Name)
default:
writeHostStatus(sub, HostStatusMessage{
Type: "host_status",
Error: "transport must be uart or ble",
})
return
}
if err != nil {
writeHostStatus(sub, hostStatusFromLink(link, false, err.Error()))
return
}
writeHostStatus(sub, hostStatusFromLink(link, true, ""))
case "host_disconnect":
_ = link.Disconnect()
writeHostStatus(sub, hostStatusFromLink(link, true, ""))
case "list_clients": case "list_clients":
clients, err := link.listClientsPoll() clients, err := link.listClientsPoll()
if err != nil { if err != nil {
writeClientList(conn, ClientListMessage{ writeClientList(sub, ClientListMessage{
Type: "client_list", Type: "client_list",
Error: err.Error(), Error: err.Error(),
}) })
@@ -647,7 +772,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
for _, c := range clients { for _, c := range clients {
out = append(out, clientInfoToAPI(c)) out = append(out, clientInfoToAPI(c))
} }
writeClientList(conn, ClientListMessage{ writeClientList(sub, ClientListMessage{
Type: "client_list", Type: "client_list",
Success: true, Success: true,
Clients: out, Clients: out,
@@ -655,34 +780,34 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
case "set_stream": case "set_stream":
if cmd.Enable == nil { if cmd.Enable == nil {
writeStreamStatus(conn, StreamStatusMessage{ writeStreamStatus(sub, StreamStatusMessage{
Type: "stream_status", Type: "stream_status",
Error: "enable required", Error: "enable required",
}) })
return return
} }
writeStreamStatus(conn, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs, cmd.PreFetch)) writeStreamStatus(sub, hub.setStream(sub, *cmd.Enable, cmd.IntervalMs, cmd.PreFetch))
case "get_stream": case "get_stream":
writeStreamStatus(conn, hub.getStream(sub)) writeStreamStatus(sub, hub.getStream(sub))
case "set_input_stream": case "set_input_stream":
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"}) writeInputStreamStatus(sub, accelStreamAPIResponse{Error: "client_id required"})
return return
} }
if cmd.Enable == nil { if cmd.Enable == nil {
writeInputStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(sub, accelStreamAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: "enable required", Error: "enable required",
}) })
return return
} }
writeInputStreamStatus(conn, applyAccelStreamClient(link, dash, ctl, cmd.ClientID, *cmd.Enable)) writeInputStreamStatus(sub, applyAccelStreamClient(link, dash, ctl, cmd.ClientID, *cmd.Enable))
case "get_input_stream": case "get_input_stream":
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeInputStreamStatus(conn, accelStreamAPIResponse{Error: "client_id required"}) writeInputStreamStatus(sub, accelStreamAPIResponse{Error: "client_id required"})
return return
} }
resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{ resp, err := link.AccelStreamPoll(&pb.AccelStreamRequest{
@@ -690,7 +815,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
ClientId: cmd.ClientID, ClientId: cmd.ClientID,
}) })
if err != nil { if err != nil {
writeInputStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(sub, accelStreamAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: err.Error(), Error: err.Error(),
}) })
@@ -699,7 +824,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
if ctl != nil { if ctl != nil {
ctl.Set(cmd.ClientID, resp.GetEnabled()) ctl.Set(cmd.ClientID, resp.GetEnabled())
} }
writeInputStreamStatus(conn, accelStreamAPIResponse{ writeInputStreamStatus(sub, accelStreamAPIResponse{
Enabled: resp.GetEnabled(), Enabled: resp.GetEnabled(),
ClientID: resp.GetClientId(), ClientID: resp.GetClientId(),
Success: resp.GetSuccess(), Success: resp.GetSuccess(),
@@ -708,15 +833,15 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
case "set_tap_notify": case "set_tap_notify":
if cmd.AllClients { if cmd.AllClients {
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil { if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "single, double_tap, triple required"}) writeTapNotifyStatus(sub, tapNotifyAPIResponse{Error: "single, double_tap, triple required"})
return return
} }
updated, err := applyTapNotifyAll(link, dash, tapCtl, *cmd.Single, *cmd.DoubleTap, *cmd.Triple) updated, err := applyTapNotifyAll(link, dash, tapCtl, *cmd.Single, *cmd.DoubleTap, *cmd.Triple)
if err != nil { if err != nil {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: err.Error()}) writeTapNotifyStatus(sub, tapNotifyAPIResponse{Error: err.Error()})
return return
} }
writeTapNotifyStatus(conn, tapNotifyAPIResponse{ writeTapNotifyStatus(sub, tapNotifyAPIResponse{
Success: updated > 0, Success: updated > 0,
SlavesUpdated: updated, SlavesUpdated: updated,
Single: *cmd.Single, Single: *cmd.Single,
@@ -726,21 +851,21 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
return return
} }
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"}) writeTapNotifyStatus(sub, tapNotifyAPIResponse{Error: "client_id required"})
return return
} }
if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil { if cmd.Single == nil || cmd.DoubleTap == nil || cmd.Triple == nil {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{ writeTapNotifyStatus(sub, tapNotifyAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: "single, double_tap, triple required", Error: "single, double_tap, triple required",
}) })
return return
} }
writeTapNotifyStatus(conn, applyTapNotifyClientWS(link, dash, tapCtl, cmd.ClientID, *cmd.Single, *cmd.DoubleTap, *cmd.Triple)) writeTapNotifyStatus(sub, applyTapNotifyClientWS(link, dash, tapCtl, cmd.ClientID, *cmd.Single, *cmd.DoubleTap, *cmd.Triple))
case "get_tap_notify": case "get_tap_notify":
if cmd.ClientID == 0 { if cmd.ClientID == 0 {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{Error: "client_id required"}) writeTapNotifyStatus(sub, tapNotifyAPIResponse{Error: "client_id required"})
return return
} }
resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{ resp, err := link.TapNotifyPoll(&pb.TapNotifyRequest{
@@ -748,7 +873,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
ClientId: cmd.ClientID, ClientId: cmd.ClientID,
}) })
if err != nil { if err != nil {
writeTapNotifyStatus(conn, tapNotifyAPIResponse{ writeTapNotifyStatus(sub, tapNotifyAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Error: err.Error(), Error: err.Error(),
}) })
@@ -757,7 +882,7 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
if tapCtl != nil { if tapCtl != nil {
tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple()) tapCtl.Set(cmd.ClientID, resp.GetSingle(), resp.GetDoubleTap(), resp.GetTriple())
} }
writeTapNotifyStatus(conn, tapNotifyAPIResponse{ writeTapNotifyStatus(sub, tapNotifyAPIResponse{
ClientID: cmd.ClientID, ClientID: cmd.ClientID,
Success: resp.GetSuccess(), Success: resp.GetSuccess(),
Single: resp.GetSingle(), Single: resp.GetSingle(),
@@ -768,36 +893,57 @@ func handleAccelWSCommand(conn *websocket.Conn, sub *wsSubscriber, data []byte,
case "set_led_ring": case "set_led_ring":
var body ledRingAPIRequest var body ledRingAPIRequest
if err := json.Unmarshal(data, &body); err != nil { if err := json.Unmarshal(data, &body); err != nil {
writeLedRingStatus(conn, ledRingAPIResponse{Error: "invalid JSON"}) writeLedRingStatus(sub, ledRingAPIResponse{Error: "invalid JSON"})
return return
} }
if body.Mode == "" { if body.Mode == "" {
writeLedRingStatus(conn, ledRingAPIResponse{Error: "mode required"}) writeLedRingStatus(sub, ledRingAPIResponse{Error: "mode required"})
return return
} }
writeLedRingStatus(conn, applyLedRing(link, body)) writeLedRingStatus(sub, applyLedRing(link, body))
case "get_battery": case "get_battery":
var body batteryAPIRequest var body batteryAPIRequest
if err := json.Unmarshal(data, &body); err != nil { if err := json.Unmarshal(data, &body); err != nil {
writeBatteryStatus(conn, batteryAPIResponse{Error: "invalid JSON"}) writeBatteryStatus(sub, batteryAPIResponse{Error: "invalid JSON"})
return return
} }
if !body.AllClients && body.ClientID == 0 { if !body.AllClients && body.ClientID == 0 {
body.AllClients = true body.AllClients = true
} }
writeBatteryStatus(conn, applyBatteryStatus(link, body)) writeBatteryStatus(sub, applyBatteryStatus(link, body))
default: default:
writeStreamStatus(conn, StreamStatusMessage{ writeStreamStatus(sub, StreamStatusMessage{
Type: "stream_status", Type: "stream_status",
Error: "unknown type (list_clients, set_stream, get_stream, set_input_stream, get_input_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)", Error: "unknown type (get_host, list_serial_ports, ble_scan, host_connect, host_disconnect, list_clients, set_stream, get_stream, set_input_stream, get_input_stream, set_tap_notify, get_tap_notify, set_led_ring, get_battery)",
}) })
} }
} }
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, portName string, hub *accelStreamHub) { func hostStatusFromLink(link *managedSerial, success bool, errMsg string) HostStatusMessage {
sub := hub.register(conn, portName) st := link.HostStatus()
return HostStatusMessage{
Type: "host_status",
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
BleName: st["ble_name"].(string),
Success: success && errMsg == "",
Error: errMsg,
}
}
func writeHostStatus(sub *wsSubscriber, msg HostStatusMessage) {
sub.writeJSON(msg)
}
func writeWSJSON(sub *wsSubscriber, msg any) {
sub.writeJSON(msg)
}
func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl, hub *accelStreamHub) {
sub := hub.register(conn, link)
defer hub.unregister(conn) defer hub.unregister(conn)
defer conn.Close() defer conn.Close()
@@ -806,11 +952,12 @@ func serveExternalWS(conn *websocket.Conn, link *managedSerial, dash *wsHub, ctl
if err != nil { if err != nil {
return return
} }
handleAccelWSCommand(conn, sub, data, link, dash, ctl, tapCtl, hub) handleAccelWSCommand(sub, data, link, dash, ctl, tapCtl, hub)
} }
} }
func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) { func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.Duration, hub *accelStreamHub, link *managedSerial, dash *wsHub, ctl *accelStreamCtl, tapCtl *tapNotifyCtl) {
_ = portName
defMs := int(defaultInterval / time.Millisecond) defMs := int(defaultInterval / time.Millisecond)
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" && r.URL.Path != "/api/v1" && r.URL.Path != "/api/v1/" { if r.URL.Path != "/" && r.URL.Path != "/api/v1" && r.URL.Path != "/api/v1/" {
@@ -821,17 +968,21 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return return
} }
st := link.HostStatus()
writeJSON(w, http.StatusOK, APIInfoResponse{ writeJSON(w, http.StatusOK, APIInfoResponse{
Name: "powerpod-external-api", Name: "powerpod-external-api",
Version: "1", Version: "1",
SerialPort: portName, SerialPort: st["endpoint"].(string),
Transport: st["transport"].(string),
Endpoint: st["endpoint"].(string),
Connected: st["connected"].(bool),
WebSocket: "/ws", WebSocket: "/ws",
DefaultIntervalMs: defMs, DefaultIntervalMs: defMs,
DefaultPreFetchMs: defaultPreFetchMs, DefaultPreFetchMs: defaultPreFetchMs,
MinIntervalMs: int(minAPIStreamInterval / time.Millisecond), MinIntervalMs: int(minAPIStreamInterval / time.Millisecond),
MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond), MaxIntervalMs: int(maxAPIStreamInterval / time.Millisecond),
TapDisplayMinMs: apiTapDisplayMinMs, TapDisplayMinMs: apiTapDisplayMinMs,
Description: "WebSocket: set_input_stream + set_stream for input (accel + tap); set_tap_notify configures slave tap kinds", Description: "WebSocket: host_connect/ble_scan + set_input_stream + set_stream for input (accel + tap)",
}) })
}) })
@@ -841,7 +992,7 @@ func mountExternalAPI(mux *http.ServeMux, portName string, defaultInterval time.
log.Printf("api websocket upgrade: %v", err) log.Printf("api websocket upgrade: %v", err)
return return
} }
serveExternalWS(conn, link, dash, ctl, tapCtl, portName, hub) serveExternalWS(conn, link, dash, ctl, tapCtl, hub)
}) })
} }
+279
View File
@@ -0,0 +1,279 @@
package main
import (
"fmt"
"log"
"sync"
"time"
"tinygo.org/x/bluetooth"
pb "powerpod/gotool/pb"
uartframe "powerpod/gotool/uart"
)
const (
bleServiceUUID = "6e400001-b5a3-f393-e0a9-e50e24dcca9e"
bleCharUUID = "6e400002-b5a3-f393-e0a9-e50e24dcca9e"
bleDefaultName = "Powerpod"
)
type bleLink struct {
device bluetooth.Device
char bluetooth.DeviceCharacteristic
mu sync.Mutex
parser *uartframe.Parser
respCh chan []byte
streamCh chan []byte
quiet bool
}
func openBLE(name string, settle time.Duration) (*bleLink, error) {
if name == "" {
name = bleDefaultName
}
adapter := bluetooth.DefaultAdapter
if err := adapter.Enable(); err != nil {
return nil, fmt.Errorf("enable bluetooth adapter: %w (check BlueZ / permissions)", err)
}
log.Printf("BLE scanning for %q…", name)
var foundAddr bluetooth.Address
var found bool
go func() {
time.Sleep(15 * time.Second)
_ = adapter.StopScan()
}()
err := adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.LocalName() != name {
return
}
foundAddr = result.Address
found = true
_ = a.StopScan()
})
if err != nil {
return nil, fmt.Errorf("scan: %w", err)
}
if !found {
return nil, fmt.Errorf("device %q not found (check DIP8=BLE and advertising)", name)
}
return connectBLEDevice(foundAddr, settle)
}
func openBLEAddress(addrStr string, settle time.Duration) (*bleLink, error) {
adapter := bluetooth.DefaultAdapter
if err := adapter.Enable(); err != nil {
return nil, fmt.Errorf("enable bluetooth adapter: %w (check BlueZ / permissions)", err)
}
if mac, err := bluetooth.ParseMAC(addrStr); err == nil {
var addr bluetooth.Address
addr.Set(mac.String())
return connectBLEDevice(addr, settle)
}
// Fallback: scan until address string matches
log.Printf("BLE resolving address %s via scan…", addrStr)
var foundAddr bluetooth.Address
var found bool
go func() {
time.Sleep(15 * time.Second)
_ = adapter.StopScan()
}()
err := adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
if result.Address.String() != addrStr {
return
}
foundAddr = result.Address
found = true
_ = a.StopScan()
})
if err != nil {
return nil, fmt.Errorf("scan for address: %w", err)
}
if !found {
return nil, fmt.Errorf("BLE address %s not found", addrStr)
}
return connectBLEDevice(foundAddr, settle)
}
func connectBLEDevice(addr bluetooth.Address, settle time.Duration) (*bleLink, error) {
svcUUID, err := bluetooth.ParseUUID(bleServiceUUID)
if err != nil {
return nil, err
}
chrUUID, err := bluetooth.ParseUUID(bleCharUUID)
if err != nil {
return nil, err
}
adapter := bluetooth.DefaultAdapter
log.Printf("BLE connecting to %s…", addr.String())
device, err := adapter.Connect(addr, bluetooth.ConnectionParams{})
if err != nil {
return nil, fmt.Errorf("connect: %w", err)
}
services, err := device.DiscoverServices([]bluetooth.UUID{svcUUID})
if err != nil || len(services) == 0 {
_ = device.Disconnect()
return nil, fmt.Errorf("discover service: %v", err)
}
chars, err := services[0].DiscoverCharacteristics([]bluetooth.UUID{chrUUID})
if err != nil || len(chars) == 0 {
_ = device.Disconnect()
return nil, fmt.Errorf("discover characteristic: %v", err)
}
char := chars[0]
if mtu, err := char.GetMTU(); err == nil {
log.Printf("BLE ATT MTU=%d", mtu)
if mtu < 203 {
log.Printf("warning: MTU %d < 203 — large frames/stream ticks may fail", mtu)
}
}
link := &bleLink{
device: device,
char: char,
parser: uartframe.NewParser(),
respCh: make(chan []byte, 8),
streamCh: make(chan []byte, 64),
}
if err := char.EnableNotifications(func(buf []byte) {
link.onNotify(buf)
}); err != nil {
_ = device.Disconnect()
return nil, fmt.Errorf("enable notifications: %w (CCCD required before commands)", err)
}
if settle <= 0 {
settle = time.Second
}
log.Printf("BLE connected; settling %v…", settle)
time.Sleep(settle)
return link, nil
}
func (b *bleLink) onNotify(buf []byte) {
for _, by := range buf {
payload, ok, err := b.parser.Feed(by)
if err != nil || !ok {
continue
}
if len(payload) == 0 {
continue
}
if payload[0] == byte(pb.MessageType_BLE_STREAM_TICK) {
select {
case b.streamCh <- payload:
default:
}
continue
}
select {
case b.respCh <- payload:
default:
// Prefer dropping the oldest queued frame so a late/stale reply
// does not block the matching response for the in-flight command.
select {
case <-b.respCh:
default:
}
select {
case b.respCh <- payload:
default:
log.Printf("BLE: response channel full, dropping frame type=0x%02x", payload[0])
}
}
}
}
func (b *bleLink) Close() error {
return b.device.Disconnect()
}
func (b *bleLink) exchange(cmdID byte, cmdName string) ([]byte, error) {
return b.exchangePayload([]byte{cmdID}, cmdName)
}
func (b *bleLink) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return b.exchangePayloadTimeout(payload, cmdName, readTimeout)
}
func (b *bleLink) exchangePayloadTimeout(payload []byte, cmdName string, timeout time.Duration) ([]byte, error) {
b.mu.Lock()
defer b.mu.Unlock()
if len(payload) == 0 {
return nil, fmt.Errorf("empty payload")
}
expect := payload[0]
if timeout <= 0 {
timeout = readTimeout
}
frame, err := uartframe.EncodeFrame(payload)
if err != nil {
return nil, fmt.Errorf("encode frame: %w", err)
}
if !b.quiet {
log.Printf("BLE sending %s (%d bytes)", cmdName, len(frame))
}
for {
select {
case <-b.respCh:
default:
goto sent
}
}
sent:
if _, err := b.char.WriteWithoutResponse(frame); err != nil {
return nil, fmt.Errorf("BLE write: %w (check DIP8=BLE)", err)
}
deadline := time.Now().Add(timeout)
for {
remain := time.Until(deadline)
if remain <= 0 {
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
}
timer := time.NewTimer(remain)
select {
case resp := <-b.respCh:
timer.Stop()
if len(resp) == 0 {
continue
}
if resp[0] != expect {
// Stale reply (e.g. late BATTERY_STATUS after a short timeout).
if !b.quiet {
log.Printf("BLE: discard type=0x%02x while waiting for 0x%02x (%s)", resp[0], expect, cmdName)
}
continue
}
if !b.quiet {
log.Printf("BLE response (%d bytes)", len(resp))
}
return resp, nil
case <-timer.C:
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
}
}
}
func (b *bleLink) NextStreamTick(timeout time.Duration) ([]byte, error) {
if timeout <= 0 {
timeout = 5 * time.Second
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case p := <-b.streamCh:
return p, nil
case <-timer.C:
return nil, fmt.Errorf("BLE stream tick timeout")
}
}
+224
View File
@@ -0,0 +1,224 @@
package main
import (
"fmt"
"math"
"sort"
"time"
pb "powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
type latencyStats struct {
N int `json:"n"`
MinMs float64 `json:"min_ms"`
MedMs float64 `json:"median_ms"`
MeanMs float64 `json:"mean_ms"`
MaxMs float64 `json:"max_ms"`
}
type bleRttResult struct {
Success bool `json:"success"`
Count int `json:"count"`
Warmup int `json:"warmup"`
Stats latencyStats `json:"stats"`
Error string `json:"error,omitempty"`
}
type bleStreamResult struct {
Success bool `json:"success"`
Received int `json:"received"`
Requested int `json:"requested"`
PeriodMs uint32 `json:"period_ms"`
TolMs float64 `json:"tol_ms"`
OutsideTol int `json:"outside_tol"`
WindowOK bool `json:"window_ok"`
DTEsp latencyStats `json:"dt_esp_ms"`
DRPc latencyStats `json:"dr_pc_ms"`
ExtraVsFirst latencyStats `json:"extra_vs_first_ms"`
Error string `json:"error,omitempty"`
}
func measureBleRtt(h hostConn, count, warmup int, settle time.Duration) bleRttResult {
out := bleRttResult{Count: count, Warmup: warmup}
if count < 1 {
out.Error = "count must be >= 1"
return out
}
if warmup < 0 {
out.Error = "warmup must be >= 0"
return out
}
if settle > 0 {
time.Sleep(settle)
}
samples := make([]float64, 0, count)
total := warmup + count
for i := 0; i < total; i++ {
t0 := time.Now()
if _, err := hostGetVersion(h); err != nil {
out.Error = fmt.Sprintf("VERSION round-trip %d: %v", i, err)
return out
}
ms := float64(time.Since(t0).Microseconds()) / 1000.0
if i >= warmup {
samples = append(samples, ms)
}
}
out.Stats = makeLatencyStats(samples)
out.Success = true
return out
}
func measureBleStream(h hostConn, clientID, periodMs uint32, count int, tolMs, timeoutSec float64) bleStreamResult {
out := bleStreamResult{
Requested: count,
PeriodMs: periodMs,
TolMs: tolMs,
}
if periodMs < 8 || periodMs > 16 {
out.Error = "period_ms must be 8..16"
return out
}
if count < 2 {
out.Error = "count must be >= 2"
return out
}
sc, ok := h.(hostStreamConn)
if !ok {
out.Error = "BLE stream ticks require BLE host"
return out
}
req := &pb.AccelStreamRequest{
Write: true,
Enable: true,
PeriodMs: periodMs,
}
if clientID == 0 {
req.AllClients = true
} else {
req.ClientId = clientID
}
resp, err := hostAccelStream(h, req)
if err != nil {
out.Error = err.Error()
return out
}
if !resp.GetSuccess() {
out.Error = "accel stream enable failed"
return out
}
type sample struct {
espUs uint64
pcT float64
}
samples := make([]sample, 0, count)
deadline := time.Now().Add(time.Duration(timeoutSec * float64(time.Second)))
if timeoutSec <= 0 {
deadline = time.Now().Add(15 * time.Second)
}
for len(samples) < count {
remain := time.Until(deadline)
if remain <= 0 {
break
}
payload, err := sc.NextStreamTick(remain)
if err != nil {
break
}
pcT := time.Now().UnixMicro()
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
continue
}
tick := msg.GetBleStreamTick()
if tick == nil {
continue
}
samples = append(samples, sample{espUs: tick.GetEspTimestampUs(), pcT: float64(pcT)})
}
off := &pb.AccelStreamRequest{Write: true, Enable: false, AllClients: req.AllClients, ClientId: req.ClientId}
_, _ = hostAccelStream(h, off)
out.Received = len(samples)
if len(samples) < 2 {
out.Error = fmt.Sprintf("need >=2 ticks, got %d (stream running / DIP8=BLE?)", len(samples))
return out
}
var dT, dR, extra []float64
esp0 := samples[0].espUs
pc0 := samples[0].pcT
for i := 1; i < len(samples); i++ {
dT = append(dT, float64(samples[i].espUs-samples[i-1].espUs)/1000.0)
dR = append(dR, (samples[i].pcT-samples[i-1].pcT)/1000.0)
relEsp := float64(samples[i].espUs-esp0) / 1000.0
relPc := (samples[i].pcT - pc0) / 1000.0
extra = append(extra, relPc-relEsp)
}
period := float64(periodMs)
outCount := 0
for _, dt := range dT {
if math.Abs(dt-period) > tolMs {
outCount++
}
}
out.OutsideTol = outCount
out.WindowOK = outCount*2 <= len(dT)
out.DTEsp = makeLatencyStats(dT)
out.DRPc = makeLatencyStats(dR)
out.ExtraVsFirst = makeLatencyStats(extra)
out.Success = true
return out
}
func makeLatencyStats(v []float64) latencyStats {
if len(v) == 0 {
return latencyStats{}
}
s := append([]float64(nil), v...)
sort.Float64s(s)
return latencyStats{
N: len(s),
MinMs: s[0],
MedMs: percentile(s, 0.5),
MeanMs: mean(s),
MaxMs: s[len(s)-1],
}
}
func mean(v []float64) float64 {
if len(v) == 0 {
return math.NaN()
}
var s float64
for _, x := range v {
s += x
}
return s / float64(len(v))
}
func percentile(sorted []float64, p float64) float64 {
if len(sorted) == 0 {
return math.NaN()
}
if len(sorted) == 1 {
return sorted[0]
}
idx := p * float64(len(sorted)-1)
lo := int(idx)
hi := lo + 1
if hi >= len(sorted) {
return sorted[lo]
}
frac := idx - float64(lo)
return sorted[lo]*(1-frac) + sorted[hi]*frac
}
+35 -1
View File
@@ -1,6 +1,12 @@
package main package main
import "powerpod/gotool/pb" import (
"fmt"
"powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
// accelSamplesFromCacheStatus maps combined CACHE_STATUS entries to AccelSample // accelSamplesFromCacheStatus maps combined CACHE_STATUS entries to AccelSample
// (for dashboard / WebSocket accel push). // (for dashboard / WebSocket accel push).
@@ -47,3 +53,31 @@ func tapEventsFromCacheStatus(r *pb.CacheStatusResponse) []*pb.TapEvent {
} }
return out return out
} }
// cacheStatusFromBLEStreamPayload parses a framed BLE_STREAM_TICK payload
// (type byte + protobuf UartMessage) into a CacheStatusResponse-shaped view.
func cacheStatusFromBLEStreamPayload(payload []byte) (*pb.CacheStatusResponse, error) {
tick, err := parseBleStreamTickPayload(payload)
if err != nil {
return nil, err
}
return &pb.CacheStatusResponse{Clients: tick.GetClients()}, nil
}
func parseBleStreamTickPayload(payload []byte) (*pb.BleStreamTick, error) {
if len(payload) < 2 {
return nil, fmt.Errorf("BLE stream tick too short")
}
if payload[0] != byte(pb.MessageType_BLE_STREAM_TICK) {
return nil, fmt.Errorf("unexpected BLE tick type 0x%02x", payload[0])
}
var msg pb.UartMessage
if err := proto.Unmarshal(payload[1:], &msg); err != nil {
return nil, err
}
tick := msg.GetBleStreamTick()
if tick == nil {
return nil, fmt.Errorf("missing ble_stream_tick")
}
return tick, nil
}
+161 -105
View File
@@ -64,16 +64,14 @@ func decodeBatteryStatusPayload(payload []byte) (*pb.BatteryStatusResponse, erro
} }
func (m *managedSerial) BatteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) { func (m *managedSerial) BatteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
var resp *pb.BatteryStatusResponse return m.batteryStatusExchange(false, req)
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.batteryStatus(req)
return e
})
return resp, err
} }
func (m *managedSerial) BatteryStatusPoll(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) { func (m *managedSerial) BatteryStatusPoll(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
return m.batteryStatusExchange(true, req)
}
func (m *managedSerial) batteryStatusExchange(poll bool, req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
msg := &pb.UartMessage{ msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_STATUS, Type: pb.MessageType_BATTERY_STATUS,
Payload: &pb.UartMessage_BatteryStatusRequest{ Payload: &pb.UartMessage_BatteryStatusRequest{
@@ -85,23 +83,18 @@ func (m *managedSerial) BatteryStatusPoll(req *pb.BatteryStatusRequest) (*pb.Bat
return nil, fmt.Errorf("encode: %w", err) return nil, fmt.Errorf("encode: %w", err)
} }
payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...) payload := append([]byte{byte(pb.MessageType_BATTERY_STATUS)}, body...)
respPayload, err := m.batteryStatusPayloadPoll(payload) var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadMode(true, payload, "BATTERY_STATUS", batteryReadTimeout)
} else {
respPayload, err = m.exchangePayloadMode(false, payload, "BATTERY_STATUS", batteryReadTimeout)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
return decodeBatteryStatusPayload(respPayload) return decodeBatteryStatusPayload(respPayload)
} }
func (m *managedSerial) batteryStatusPayloadPoll(payload []byte) ([]byte, error) {
var resp []byte
err := m.withPortPoll(func(sp *serialPort) error {
var e error
resp, e = sp.exchangePayloadForBattery(payload, "BATTERY_STATUS")
return e
})
return resp, err
}
func (s *serialPort) batteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) { func (s *serialPort) batteryStatus(req *pb.BatteryStatusRequest) (*pb.BatteryStatusResponse, error) {
msg := &pb.UartMessage{ msg := &pb.UartMessage{
Type: pb.MessageType_BATTERY_STATUS, Type: pb.MessageType_BATTERY_STATUS,
@@ -122,11 +115,11 @@ func (s *serialPort) batteryStatus(req *pb.BatteryStatusRequest) (*pb.BatterySta
} }
func (m *managedSerial) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) { func (m *managedSerial) AccelStream(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
return m.accelStreamVia(m.withPort, req) return m.accelStreamExchange(false, req)
} }
func (m *managedSerial) AccelStreamPoll(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) { func (m *managedSerial) AccelStreamPoll(req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
return m.accelStreamVia(m.withPortPoll, req) return m.accelStreamExchange(true, req)
} }
// SetAccelStream enables or disables the ESP-NOW accel stream for one slave (master UART). // SetAccelStream enables or disables the ESP-NOW accel stream for one slave (master UART).
@@ -154,24 +147,41 @@ func (m *managedSerial) GetAccelStream(clientID uint32) (bool, error) {
} }
func (m *managedSerial) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) { func (m *managedSerial) TapNotify(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
return m.tapNotifyVia(m.withPort, req) return m.tapNotifyExchange(false, req)
} }
func (m *managedSerial) TapNotifyPoll(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) { func (m *managedSerial) TapNotifyPoll(req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
return m.tapNotifyVia(m.withPortPoll, req) return m.tapNotifyExchange(true, req)
} }
func (m *managedSerial) tapNotifyVia( func (m *managedSerial) tapNotifyExchange(poll bool, req *pb.TapNotifyRequest) (*pb.TapNotifyResponse, error) {
portFn func(func(*serialPort) error) error, msg := &pb.UartMessage{
req *pb.TapNotifyRequest, Type: pb.MessageType_TAP_NOTIFY,
) (*pb.TapNotifyResponse, error) { Payload: &pb.UartMessage_TapNotifyRequest{TapNotifyRequest: req},
var resp *pb.TapNotifyResponse }
err := portFn(func(sp *serialPort) error { body, err := proto.Marshal(msg)
var e error if err != nil {
resp, e = sp.TapNotify(req) return nil, fmt.Errorf("encode: %w", err)
return e }
}) payload := append([]byte{byte(pb.MessageType_TAP_NOTIFY)}, body...)
return resp, err var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadPoll(payload, "TAP_NOTIFY")
} else {
respPayload, err = m.exchangePayload(payload, "TAP_NOTIFY")
}
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetTapNotifyResponse()
if r == nil {
return nil, fmt.Errorf("missing tap_notify_response")
}
return r, nil
} }
func (m *managedSerial) readCacheStatusPoll() (*pb.CacheStatusResponse, error) { func (m *managedSerial) readCacheStatusPoll() (*pb.CacheStatusResponse, error) {
@@ -204,69 +214,138 @@ func decodeCacheStatusPayload(payload []byte) (*pb.CacheStatusResponse, error) {
return r, nil return r, nil
} }
func (m *managedSerial) accelStreamVia( func (m *managedSerial) accelStreamExchange(poll bool, req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
portFn func(func(*serialPort) error) error, msg := &pb.UartMessage{
req *pb.AccelStreamRequest, Type: pb.MessageType_ACCEL_STREAM,
) (*pb.AccelStreamResponse, error) { Payload: &pb.UartMessage_AccelStreamRequest{AccelStreamRequest: req},
var resp *pb.AccelStreamResponse }
err := portFn(func(sp *serialPort) error { body, err := proto.Marshal(msg)
var e error if err != nil {
resp, e = sp.AccelStream(req) return nil, fmt.Errorf("encode: %w", err)
return e }
}) payload := append([]byte{byte(pb.MessageType_ACCEL_STREAM)}, body...)
return resp, err var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadPoll(payload, "ACCEL_STREAM")
} else {
respPayload, err = m.exchangePayload(payload, "ACCEL_STREAM")
}
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetAccelStreamResponse()
if r == nil {
return nil, fmt.Errorf("missing accel_stream_response")
}
return r, nil
} }
func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) { func (m *managedSerial) AccelDeadzone(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
return m.accelDeadzoneVia(m.withPort, req) return m.accelDeadzoneExchange(false, req)
} }
func (m *managedSerial) AccelDeadzonePoll(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) { func (m *managedSerial) AccelDeadzonePoll(req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
return m.accelDeadzoneVia(m.withPortPoll, req) return m.accelDeadzoneExchange(true, req)
} }
func (m *managedSerial) TapConfig(req *pb.TapConfigRequest) (*pb.TapConfigResponse, error) { func (m *managedSerial) TapConfig(req *pb.TapConfigRequest) (*pb.TapConfigResponse, error) {
return m.tapConfigVia(m.withPort, req) return m.tapConfigExchange(false, req)
} }
func (m *managedSerial) TapConfigPoll(req *pb.TapConfigRequest) (*pb.TapConfigResponse, error) { func (m *managedSerial) TapConfigPoll(req *pb.TapConfigRequest) (*pb.TapConfigResponse, error) {
return m.tapConfigVia(m.withPortPoll, req) return m.tapConfigExchange(true, req)
} }
func (m *managedSerial) tapConfigVia( func (m *managedSerial) tapConfigExchange(poll bool, req *pb.TapConfigRequest) (*pb.TapConfigResponse, error) {
portFn func(func(*serialPort) error) error, msg := &pb.UartMessage{
req *pb.TapConfigRequest, Type: pb.MessageType_TAP_CONFIG,
) (*pb.TapConfigResponse, error) { Payload: &pb.UartMessage_TapConfigRequest{TapConfigRequest: req},
var resp *pb.TapConfigResponse }
err := portFn(func(sp *serialPort) error { body, err := proto.Marshal(msg)
var e error if err != nil {
resp, e = sp.TapConfig(req) return nil, fmt.Errorf("encode: %w", err)
return e }
}) payload := append([]byte{byte(pb.MessageType_TAP_CONFIG)}, body...)
return resp, err var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadPoll(payload, "TAP_CONFIG")
} else {
respPayload, err = m.exchangePayload(payload, "TAP_CONFIG")
}
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.GetTapConfigResponse()
if r == nil {
return nil, fmt.Errorf("missing tap_config_response")
}
return r, nil
} }
func (m *managedSerial) accelDeadzoneVia( func (m *managedSerial) accelDeadzoneExchange(poll bool, req *pb.AccelDeadzoneRequest) (*pb.AccelDeadzoneResponse, error) {
portFn func(func(*serialPort) error) error, msg := &pb.UartMessage{
req *pb.AccelDeadzoneRequest, Type: pb.MessageType_ACCEL_DEADZONE,
) (*pb.AccelDeadzoneResponse, error) { Payload: &pb.UartMessage_AccelDeadzoneRequest{AccelDeadzoneRequest: req},
var resp *pb.AccelDeadzoneResponse }
err := portFn(func(sp *serialPort) error { body, err := proto.Marshal(msg)
var e error if err != nil {
resp, e = sp.AccelDeadzone(req) return nil, fmt.Errorf("encode: %w", err)
return e }
}) payload := append([]byte{byte(pb.MessageType_ACCEL_DEADZONE)}, body...)
return resp, err var respPayload []byte
if poll {
respPayload, err = m.exchangePayloadPoll(payload, "ACCEL_DEADZONE")
} else {
respPayload, err = m.exchangePayload(payload, "ACCEL_DEADZONE")
}
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.GetAccelDeadzoneResponse()
if r == nil {
return nil, fmt.Errorf("missing accel_deadzone_response")
}
return r, nil
} }
func (m *managedSerial) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) { func (m *managedSerial) EspnowUnicastTest(clientID, seq uint32) (*pb.EspNowUnicastTestResponse, error) {
var resp *pb.EspNowUnicastTestResponse req := &pb.EspNowUnicastTestRequest{ClientId: clientID, Seq: seq}
err := m.withPort(func(sp *serialPort) error { msg := &pb.UartMessage{
var e error Type: pb.MessageType_ESPNOW_UNICAST_TEST,
resp, e = sp.EspnowUnicastTest(clientID, seq) Payload: &pb.UartMessage_EspnowUnicastTestRequest{
return e EspnowUnicastTestRequest: req,
}) },
return resp, err }
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ESPNOW_UNICAST_TEST)}, body...)
respPayload, err := m.exchangePayload(payload, "ESPNOW_UNICAST_TEST")
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.GetEspnowUnicastTestResponse()
if r == nil {
return nil, fmt.Errorf("missing espnow_unicast_test_response")
}
return r, nil
} }
func decodeVersionPayload(payload []byte) (*pb.VersionResponse, error) { func decodeVersionPayload(payload []byte) (*pb.VersionResponse, error) {
@@ -520,26 +599,15 @@ func (s *serialPort) echoPing(clientID uint32) (*EchoPingResult, error) {
} }
func (m *managedSerial) FindMe(clientID uint32) error { func (m *managedSerial) FindMe(clientID uint32) error {
return m.withPort(func(sp *serialPort) error { return hostFindMe(m, clientID)
return runFindMeClient(sp, clientID)
})
} }
func (m *managedSerial) SetLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) { func (m *managedSerial) SetLogLevel(write bool, level uint32) (*pb.SetLogLevelResponse, error) {
var resp *pb.SetLogLevelResponse return hostSetLogLevel(m, write, level)
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = runSetLogLevelClient(sp, write, level)
return e
})
return resp, err
} }
func (m *managedSerial) Restart(clientID uint32) error { func (m *managedSerial) Restart(clientID uint32) error {
err := m.withPort(func(sp *serialPort) error { if err := hostRestart(m, clientID); err != nil {
return runRestartClient(sp, clientID)
})
if err != nil {
return err return err
} }
if clientID == 0 { if clientID == 0 {
@@ -618,13 +686,7 @@ func (s *serialPort) EchoPing(clientID uint32) (*EchoPingResult, error) {
} }
func (m *managedSerial) EchoPing(clientID uint32) (*EchoPingResult, error) { func (m *managedSerial) EchoPing(clientID uint32) (*EchoPingResult, error) {
var result *EchoPingResult return hostEchoPing(m, clientID)
err := m.withPort(func(sp *serialPort) error {
var e error
result, e = sp.echoPing(clientID)
return e
})
return result, err
} }
func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) { func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
@@ -632,13 +694,7 @@ func (s *serialPort) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgres
} }
func (m *managedSerial) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) { func (m *managedSerial) LedRing(req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
var resp *pb.LedRingProgressResponse return hostLedRing(m, req)
err := m.withPort(func(sp *serialPort) error {
var e error
resp, e = sp.LedRing(req)
return e
})
return resp, err
} }
func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) { func (s *serialPort) FindMe(clientID uint32) (*pb.EspNowFindMeResponse, error) {
+44
View File
@@ -0,0 +1,44 @@
package main
import (
"flag"
"fmt"
pb "powerpod/gotool/pb"
)
func runAccelStreamCmd(h hostConn, args []string) error {
fs := flag.NewFlagSet("accel-stream", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id (0 with -all)")
all := fs.Bool("all", false, "all registered clients")
enable := fs.Bool("enable", true, "enable stream")
disable := fs.Bool("disable", false, "disable stream")
period := fs.Uint("period", 0, "BLE push period_ms 8–16 (0=leave/default)")
if err := fs.Parse(args); err != nil {
return err
}
on := *enable
if *disable {
on = false
}
if !*all && *clientID == 0 {
return fmt.Errorf("need -client ID or -all")
}
req := &pb.AccelStreamRequest{
Write: true,
Enable: on,
PeriodMs: uint32(*period),
}
if *all {
req.AllClients = true
} else {
req.ClientId = uint32(*clientID)
}
r, err := hostAccelStream(h, req)
if err != nil {
return err
}
fmt.Printf("accel-stream: success=%v enabled=%v client_id=%d slaves_updated=%d\n",
r.GetSuccess(), r.GetEnabled(), r.GetClientId(), r.GetSlavesUpdated())
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package main
import (
"flag"
"fmt"
"time"
)
func runBleRtt(h hostConn, args []string) error {
fs := flag.NewFlagSet("ble-rtt", flag.ExitOnError)
count := fs.Int("count", 50, "number of measured VERSION round-trips")
settle := fs.Float64("settle", 0, "extra settle seconds before measurement (0 = already settled on connect)")
warmup := fs.Int("warmup", 5, "discarded warm-up round-trips")
if err := fs.Parse(args); err != nil {
return err
}
out := measureBleRtt(h, *count, *warmup, time.Duration(*settle*float64(time.Second)))
if !out.Success {
return fmt.Errorf("%s", out.Error)
}
fmt.Printf("ble-rtt (VERSION): n=%d warmup=%d\n", out.Stats.N, out.Warmup)
fmt.Printf(" min=%.3f median=%.3f mean=%.3f max=%.3f ms\n",
out.Stats.MinMs, out.Stats.MedMs, out.Stats.MeanMs, out.Stats.MaxMs)
return nil
}
+40
View File
@@ -0,0 +1,40 @@
package main
import (
"flag"
"fmt"
)
func runBleStream(h hostConn, args []string) error {
fs := flag.NewFlagSet("ble-stream", flag.ExitOnError)
clientID := fs.Uint("client", 0, "slave client id (0 = all_clients)")
periodMs := fs.Uint("period", 16, "BLE push period_ms (8–16)")
count := fs.Int("count", 50, "number of BleStreamTick samples")
tolMs := fs.Float64("tol", 2.0, "tolerance ms around period for window_ok")
duration := fs.Float64("timeout", 15.0, "max seconds waiting for ticks")
if err := fs.Parse(args); err != nil {
return err
}
fmt.Printf("accel stream on; period_ms=%d; collecting %d ticks…\n", *periodMs, *count)
out := measureBleStream(h, uint32(*clientID), uint32(*periodMs), *count, *tolMs, *duration)
if !out.Success {
return fmt.Errorf("%s", out.Error)
}
fmt.Printf("received=%d/%d period_ms=%d tol=±%.1f ms\n", out.Received, out.Requested, out.PeriodMs, out.TolMs)
printStat("dT ESP inter-depart", out.DTEsp)
printStat("dR PC inter-arrive", out.DRPc)
printStat("extra vs first", out.ExtraVsFirst)
fmt.Printf("outside_tol=%d/%d window_ok=%s\n", out.OutsideTol, out.DTEsp.N,
map[bool]string{true: "yes", false: "no"}[out.WindowOK])
return nil
}
func printStat(name string, s latencyStats) {
if s.N == 0 {
fmt.Printf("%s: (none)\n", name)
return
}
fmt.Printf("%s: n=%d min=%.3f median=%.3f mean=%.3f max=%.3f ms\n",
name, s.N, s.MinMs, s.MedMs, s.MeanMs, s.MaxMs)
}
+40 -10
View File
@@ -20,21 +20,48 @@ var wsUpgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true }, CheckOrigin: func(r *http.Request) bool { return true },
} }
func runServe(portName string, baud int, args []string) error { type serveOpts struct {
portName string
baud int
useBLE bool
bleName string
settle time.Duration
}
func runServe(opts serveOpts, args []string) error {
serveFlags := flag.NewFlagSet("serve", flag.ExitOnError) serveFlags := flag.NewFlagSet("serve", flag.ExitOnError)
addr := serveFlags.String("addr", ":8080", "dashboard HTTP listen address") addr := serveFlags.String("addr", ":8080", "dashboard HTTP listen address")
apiAddr := serveFlags.String("api-addr", ":8081", "external API HTTP listen address (empty to disable)") apiAddr := serveFlags.String("api-addr", ":8081", "external API HTTP listen address (empty to disable)")
accelInterval := serveFlags.Duration("accel-interval", defaultAccelStreamInterval, "accel WebSocket sample period on API server") accelInterval := serveFlags.Duration("accel-interval", defaultAccelStreamInterval, "accel WebSocket sample period on API server")
interval := serveFlags.Duration("interval", 2*time.Second, "UART poll interval") interval := serveFlags.Duration("interval", 2*time.Second, "host poll interval")
if err := serveFlags.Parse(args); err != nil { if err := serveFlags.Parse(args); err != nil {
return err return err
} }
if portName == "" { if opts.useBLE && opts.portName != "" {
return fmt.Errorf("serve requires -port (master UART)") return fmt.Errorf("use either -port or -ble, not both")
} }
link := newManagedSerial(portName, baud) link := newManagedHost(opts.baud)
link.quiet = true link.quiet = true
link.settle = opts.settle
if opts.settle <= 0 {
link.settle = time.Second
}
switch {
case opts.portName != "":
if err := link.ConnectUART(opts.portName, opts.baud); err != nil {
log.Printf("initial UART connect failed (UI can retry): %v", err)
}
case opts.useBLE:
name := opts.bleName
if name == "" {
name = bleDefaultName
}
if err := link.ConnectBLE("", name); err != nil {
log.Printf("initial BLE connect failed (UI can scan/retry): %v", err)
}
}
hub := newWSHub() hub := newWSHub()
streamCtl := newAccelStreamCtl() streamCtl := newAccelStreamCtl()
@@ -48,16 +75,17 @@ func runServe(portName string, baud int, args []string) error {
shutdownHTTPServer(dashSrv) shutdownHTTPServer(dashSrv)
shutdownAPIServer(apiSrv) shutdownAPIServer(apiSrv)
if err := link.Close(); err != nil { if err := link.Close(); err != nil {
log.Printf("UART close: %v", err) log.Printf("host close: %v", err)
} }
}) })
go runPoller(link, portName, hub, streamCtl, tapCtl, *interval, stop) go runPoller(link, link.Endpoint(), hub, streamCtl, tapCtl, *interval, stop)
go runBatteryPoller(link, hub, 5*time.Second, stop) go runBatteryPoller(link, hub, 5*time.Second, stop)
go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop) go runCacheStatusDashboardPoller(link, hub, *accelInterval, stop)
go runBLEStreamDashboardPoller(link, hub, streamCtl, stop)
if *apiAddr != "" { if *apiAddr != "" {
apiSrv = runAPIServer(portName, link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop) apiSrv = runAPIServer(link.Endpoint(), link, *apiAddr, *accelInterval, hub, streamCtl, tapCtl, stop)
} }
mux := http.NewServeMux() mux := http.NewServeMux()
@@ -85,10 +113,12 @@ func runServe(portName string, baud int, args []string) error {
} }
mux.Handle("/", http.FileServer(http.FS(ui))) mux.Handle("/", http.FileServer(http.FS(ui)))
log.Printf("dashboard http://localhost%s (UART %s @ %d baud, poll %s, live-stream %s, auto-reconnect)", log.Printf("dashboard http://localhost%s (transport=%s endpoint=%q poll %s, live-stream %s)",
*addr, portName, baud, interval.String(), accelInterval.String()) *addr, link.Transport(), link.Endpoint(), interval.String(), accelInterval.String())
if *apiAddr == "" { if *apiAddr == "" {
log.Printf("external API disabled (-api-addr \"\")") log.Printf("external API disabled (-api-addr \"\")")
} else {
log.Printf("external API http://localhost%s ws://localhost%s/ws", *apiAddr, *apiAddr)
} }
dashSrv = &http.Server{Addr: *addr, Handler: mux} dashSrv = &http.Server{Addr: *addr, Handler: mux}
+196 -16
View File
@@ -68,20 +68,89 @@ type ClientView struct {
type DashboardState struct { type DashboardState struct {
UpdatedAt string `json:"updated_at"` UpdatedAt string `json:"updated_at"`
SerialPort string `json:"serial_port"` SerialPort string `json:"serial_port"`
/** Alias of host_endpoint for older UI. */
HostEndpoint string `json:"host_endpoint"`
HostTransport string `json:"host_transport"`
HostConnected bool `json:"host_connected"`
UARTConnected bool `json:"uart_connected"` UARTConnected bool `json:"uart_connected"`
SerialOK bool `json:"serial_ok"` SerialOK bool `json:"serial_ok"`
SerialError string `json:"serial_error,omitempty"` SerialError string `json:"serial_error,omitempty"`
/** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */ /** Host: fast CACHE_STATUS poll (~16 ms) for accel + tap. */
LiveStream bool `json:"live_stream"` LiveStream bool `json:"live_stream"`
/** BLE live path: inter-tick stats from BleStreamTick.esp_timestamp_us. */
BleTick *BleTickView `json:"ble_tick,omitempty"`
Master MasterView `json:"master"` Master MasterView `json:"master"`
Clients []ClientView `json:"clients"` Clients []ClientView `json:"clients"`
} }
// BleTickView is median/mean inter-departure of BLE_STREAM_TICK (ESP timer).
type BleTickView struct {
PeriodMs uint32 `json:"period_ms"`
MedianMs float64 `json:"median_ms"`
MeanMs float64 `json:"mean_ms"`
MinMs float64 `json:"min_ms"`
MaxMs float64 `json:"max_ms"`
Samples int `json:"samples"`
LastEspUs uint64 `json:"last_esp_us,omitempty"`
}
const bleTickDeltaCap = 64
type bleTickTracker struct {
lastEspUs uint64
haveLast bool
periodMs uint32
deltas [bleTickDeltaCap]float64
next int
count int
}
func (t *bleTickTracker) reset() {
*t = bleTickTracker{}
}
func (t *bleTickTracker) add(espUs uint64, periodMs uint32) BleTickView {
if periodMs != 0 {
t.periodMs = periodMs
}
if t.haveLast && espUs > t.lastEspUs {
dt := float64(espUs-t.lastEspUs) / 1000.0
t.deltas[t.next] = dt
t.next = (t.next + 1) % bleTickDeltaCap
if t.count < bleTickDeltaCap {
t.count++
}
}
t.lastEspUs = espUs
t.haveLast = true
samples := make([]float64, 0, t.count)
if t.count < bleTickDeltaCap {
samples = append(samples, t.deltas[:t.count]...)
} else {
samples = append(samples, t.deltas[t.next:]...)
samples = append(samples, t.deltas[:t.next]...)
}
st := makeLatencyStats(samples)
return BleTickView{
PeriodMs: t.periodMs,
MedianMs: st.MedMs,
MeanMs: st.MeanMs,
MinMs: st.MinMs,
MaxMs: st.MaxMs,
Samples: st.N,
LastEspUs: espUs,
}
}
type wsHub struct { type wsHub struct {
mu sync.RWMutex mu sync.RWMutex
writeMu sync.Mutex // gorilla websocket: one writer at a time
clients map[*websocket.Conn]struct{} clients map[*websocket.Conn]struct{}
state DashboardState state DashboardState
liveStream bool liveStream bool
bleTicks bleTickTracker
lastPush time.Time // throttle BLE tick WS pushes
} }
func newWSHub() *wsHub { func newWSHub() *wsHub {
@@ -92,6 +161,10 @@ func (h *wsHub) setState(st DashboardState) {
h.mu.Lock() h.mu.Lock()
prev := h.state prev := h.state
st.LiveStream = prev.LiveStream st.LiveStream = prev.LiveStream
if st.BleTick == nil && prev.BleTick != nil && st.LiveStream {
cp := *prev.BleTick
st.BleTick = &cp
}
st.Clients = preserveClientAccel(st.Clients, prev.Clients, st.LiveStream) st.Clients = preserveClientAccel(st.Clients, prev.Clients, st.LiveStream)
st.Clients = preserveClientBattery(st.Clients, prev.Clients) st.Clients = preserveClientBattery(st.Clients, prev.Clients)
st.Clients = preserveClientTap(st.Clients, prev.Clients) st.Clients = preserveClientTap(st.Clients, prev.Clients)
@@ -136,15 +209,25 @@ func (h *wsHub) unregister(c *websocket.Conn) {
h.mu.Unlock() h.mu.Unlock()
} }
// writeJSON sends to one client; removes it on error or panic (closed connection). // dropClient removes the client and closes the socket so the browser reconnects.
func (h *wsHub) dropClient(c *websocket.Conn) {
h.unregister(c)
_ = c.Close()
}
// writeJSON sends to one client; removes+closes it on error or panic.
func (h *wsHub) writeJSON(c *websocket.Conn, data []byte) { func (h *wsHub) writeJSON(c *websocket.Conn, data []byte) {
defer func() { defer func() {
if recover() != nil { if recover() != nil {
h.unregister(c) h.dropClient(c)
} }
}() }()
if err := c.WriteMessage(websocket.TextMessage, data); err != nil { h.writeMu.Lock()
h.unregister(c) _ = c.SetWriteDeadline(time.Now().Add(2 * time.Second))
err := c.WriteMessage(websocket.TextMessage, data)
h.writeMu.Unlock()
if err != nil {
h.dropClient(c)
} }
} }
@@ -413,6 +496,8 @@ func (h *wsHub) patchLiveStream(enabled bool) {
st := h.state st := h.state
st.LiveStream = enabled st.LiveStream = enabled
if !enabled { if !enabled {
h.bleTicks.reset()
st.BleTick = nil
for i := range st.Clients { for i := range st.Clients {
st.Clients[i].AccelValid = false st.Clients[i].AccelValid = false
st.Clients[i].AccelX = 0 st.Clients[i].AccelX = 0
@@ -497,6 +582,49 @@ func (h *wsHub) mergeAccel(samples []*pb.AccelSample) {
} }
} }
// mergeBleStreamTick applies accel/tap from a BLE tick and updates inter-tick stats.
// State is updated every tick; WebSocket push is throttled (~50 Hz) so concurrent
// writers (poller/battery) cannot flood or race the gorilla conn.
func (h *wsHub) mergeBleStreamTick(tick *pb.BleStreamTick) {
if tick == nil || !h.liveStreamEnabled() {
return
}
cache := &pb.CacheStatusResponse{Clients: tick.GetClients()}
const minPushGap = 20 * time.Millisecond
h.mu.Lock()
view := h.bleTicks.add(tick.GetEspTimestampUs(), tick.GetPeriodMs())
st := h.state
st.Clients = applyAccelSamples(st.Clients, accelSamplesFromCacheStatus(cache))
st.Clients = applyTapEvents(st.Clients, tapEventsFromCacheStatus(cache))
st.BleTick = &view
st.UpdatedAt = time.Now().Format(time.RFC3339)
h.state = st
push := time.Since(h.lastPush) >= minPushGap
if push {
h.lastPush = time.Now()
}
var conns []*websocket.Conn
if push {
conns = make([]*websocket.Conn, 0, len(h.clients))
for c := range h.clients {
conns = append(conns, c)
}
}
h.mu.Unlock()
if !push {
return
}
data, err := json.Marshal(st)
if err != nil {
return
}
for _, c := range conns {
h.writeJSON(c, data)
}
}
func (h *wsHub) mergeTap(events []*pb.TapEvent) { func (h *wsHub) mergeTap(events []*pb.TapEvent) {
if len(events) == 0 || !h.liveStreamEnabled() { if len(events) == 0 || !h.liveStreamEnabled() {
return return
@@ -530,20 +658,35 @@ func (h *wsHub) broadcastRaw(v any) {
} }
func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState { func pollDashboard(link *managedSerial, portName string, last *DashboardState, streamCtl *accelStreamCtl, tapCtl *tapNotifyCtl) DashboardState {
ep := link.Endpoint()
if ep == "" {
ep = portName
}
tr := link.Transport()
st := DashboardState{ st := DashboardState{
UpdatedAt: time.Now().Format(time.RFC3339), UpdatedAt: time.Now().Format(time.RFC3339),
SerialPort: portName, SerialPort: ep,
HostEndpoint: ep,
HostTransport: tr,
Clients: []ClientView{}, Clients: []ClientView{},
} }
if !link.IsConnected() && tr == hostTransportNone {
st.SerialError = "not connected — use UART or BLE connect"
return st
}
ver, err := link.getVersionPoll() ver, err := link.getVersionPoll()
if errors.Is(err, errUARTBusy) { if errors.Is(err, errUARTBusy) {
return pausedPollState(portName, last) return pausedPollState(ep, last)
} }
if err != nil { if err != nil {
return disconnectedState(portName, err) return disconnectedState(ep, err)
} }
st.UARTConnected = true st.UARTConnected = true
st.HostConnected = true
st.HostTransport = link.Transport()
st.HostEndpoint = link.Endpoint()
st.SerialPort = st.HostEndpoint
st.SerialOK = true st.SerialOK = true
st.Master = MasterView{ st.Master = MasterView{
Version: ver.GetVersion(), Version: ver.GetVersion(),
@@ -565,7 +708,8 @@ func pollDashboard(link *managedSerial, portName string, last *DashboardState, s
} }
st.SerialOK = false st.SerialOK = false
st.SerialError = err.Error() st.SerialError = err.Error()
st.UARTConnected = link.IsConnected() st.HostConnected = link.IsConnected()
st.UARTConnected = st.HostConnected
return st return st
} }
@@ -653,7 +797,7 @@ func runBatteryPoller(link *managedSerial, hub *wsHub, interval time.Duration, s
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
if !link.IsConnected() { if !link.IsConnected() || link.PollPaused() {
continue continue
} }
bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true}) bat, err := link.BatteryStatusPoll(&pb.BatteryStatusRequest{AllClients: true})
@@ -674,7 +818,11 @@ func runCacheStatusDashboardPoller(link *managedSerial, hub *wsHub, interval tim
case <-stop: case <-stop:
return return
case <-ticker.C: case <-ticker.C:
if !hub.liveStreamEnabled() { if !hub.liveStreamEnabled() || link.PollPaused() {
continue
}
// BLE live path prefers BLE_STREAM_TICK pushes.
if link.Transport() == hostTransportBLE {
continue continue
} }
cache, err := link.readCacheStatusPoll() cache, err := link.readCacheStatusPoll()
@@ -687,6 +835,35 @@ func runCacheStatusDashboardPoller(link *managedSerial, hub *wsHub, interval tim
} }
} }
// runBLEStreamDashboardPoller feeds live accel/tap from BLE_STREAM_TICK when live_stream is on.
func runBLEStreamDashboardPoller(link *managedSerial, hub *wsHub, streamCtl *accelStreamCtl, stop <-chan struct{}) {
_ = streamCtl
for {
select {
case <-stop:
return
default:
}
if link.Transport() != hostTransportBLE || !hub.liveStreamEnabled() || link.PollPaused() {
select {
case <-stop:
return
case <-time.After(50 * time.Millisecond):
}
continue
}
payload, err := link.NextStreamTick(200 * time.Millisecond)
if err != nil {
continue
}
tick, err := parseBleStreamTickPayload(payload)
if err != nil {
continue
}
hub.mergeBleStreamTick(tick)
}
}
func (h *wsHub) clientCount() int { func (h *wsHub) clientCount() int {
h.mu.RLock() h.mu.RLock()
n := len(h.clients) n := len(h.clients)
@@ -695,11 +872,14 @@ func (h *wsHub) clientCount() int {
} }
func pausedPollState(portName string, last *DashboardState) DashboardState { func pausedPollState(portName string, last *DashboardState) DashboardState {
if last != nil && last.UARTConnected { if last != nil && (last.HostConnected || last.UARTConnected) {
st := *last st := *last
st.UpdatedAt = time.Now().Format(time.RFC3339) st.UpdatedAt = time.Now().Format(time.RFC3339)
st.SerialPort = portName st.SerialPort = portName
st.HostEndpoint = portName
st.SerialOK = true st.SerialOK = true
st.HostConnected = true
st.UARTConnected = true
st.SerialError = "Live-Polling pausiert (OTA läuft)" st.SerialError = "Live-Polling pausiert (OTA läuft)"
return st return st
} }
@@ -774,20 +954,20 @@ func runPoller(link *managedSerial, portName string, hub *wsHub, streamCtl *acce
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
uartUp := false hostUp := false
var lastGood DashboardState var lastGood DashboardState
publish := func() { publish := func() {
st := pollDashboard(link, portName, &lastGood, streamCtl, tapCtl) st := pollDashboard(link, portName, &lastGood, streamCtl, tapCtl)
hub.setState(st) hub.setState(st)
if st.UARTConnected && st.SerialOK { if st.HostConnected && st.SerialOK {
hub.mu.RLock() hub.mu.RLock()
lastGood = hub.state lastGood = hub.state
hub.mu.RUnlock() hub.mu.RUnlock()
} }
if st.UARTConnected && !uartUp { if st.HostConnected && !hostUp {
log.Printf("UART %s connected", portName) log.Printf("host %s connected (%s)", st.HostEndpoint, st.HostTransport)
} }
uartUp = st.UARTConnected hostUp = st.HostConnected
} }
publish() publish()
+46 -2
View File
@@ -1,6 +1,6 @@
# REST API # REST API
`go run . -port /dev/ttyUSB0 serve` starts two HTTP servers on the same UART link: `go run . serve` (optional `-port` / `-ble`) starts two HTTP servers on the same `managedHost` (UART or BLE):
| Base URL | Flag | Used by | | Base URL | Flag | Used by |
|----------|------|---------| |----------|------|---------|
@@ -27,6 +27,9 @@ GET /api/v1/
"name": "powerpod-external-api", "name": "powerpod-external-api",
"version": "1", "version": "1",
"serial_port": "/dev/ttyUSB0", "serial_port": "/dev/ttyUSB0",
"transport": "uart",
"endpoint": "/dev/ttyUSB0",
"connected": true,
"websocket": "/ws", "websocket": "/ws",
"default_interval_ms": 16, "default_interval_ms": 16,
"min_interval_ms": 1, "min_interval_ms": 1,
@@ -110,7 +113,48 @@ WebSocket: `set_led_ring` with the same fields plus `"type":"set_led_ring"` →
Used by the web UI; safe for scripts that drive the same features. Used by the web UI; safe for scripts that drive the same features.
### Live stream (host `CACHE_STATUS` poll ~16 ms) ### Host link (UART / BLE)
```http
GET /api/host
→ {"transport":"none|uart|ble","endpoint":"…","connected":false,"ble_name":""}
GET /api/serial/ports
→ {"ports":["/dev/ttyUSB0",…]}
POST /api/host/connect
Content-Type: application/json
{"transport":"uart","port":"/dev/ttyUSB0"}
{"transport":"ble","address":"AA:BB:…","name":"Powerpod"}
POST /api/host/disconnect
POST /api/ble/scan
{"timeout_s":5,"name_filter":"Powerpod"}
→ {"devices":[{"address":"…","name":"Powerpod","rssi":-55}]}
```
`name_filter` is optional substring match (case-insensitive); omit to list all LE advertisements. Active BLE is disconnected before scan (BlueZ adapter).
Dashboard WebSocket state includes `host_connected`, `host_transport`, `host_endpoint` (aliases `uart_connected` / `serial_port` kept).
**OTA** (`POST /api/ota`) returns **400** unless `transport==uart` and connected.
### BLE latency tests (BLE host only)
```http
POST /api/ble-rtt
{"count":50,"warmup":5,"settle_s":0}
→ {"success":true,"count":50,"warmup":5,"stats":{"n":50,"min_ms":…,"median_ms":…,"mean_ms":…,"max_ms":…}}
POST /api/ble-stream
{"count":50,"period_ms":16,"tol_ms":2,"timeout_s":15,"client_id":0}
→ {"success":true,"received":50,"requested":50,"window_ok":true,"dt_esp_ms":{…},"dr_pc_ms":{…},…}
```
Pauses dashboard/API polling while running. Requires active BLE host (`DIP8=1`).
### Live stream (host `CACHE_STATUS` poll ~16 ms; BLE uses `BLE_STREAM_TICK`)
```http ```http
GET /api/live-stream GET /api/live-stream
+30 -6
View File
@@ -2,15 +2,16 @@
External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string). External API: `ws://localhost:8081/ws` (default `-api-addr`, disable with empty string).
Start with `go run . -port /dev/ttyUSB0 serve`. Start with `go run . serve` (optional `-port` / `-ble` to pre-connect). Host can be opened later via `host_connect` / dashboard REST.
--- ---
## Connection flow ## Connection flow
1. Connect → server sends `hello` (push off; defaults and command list). 1. Connect → server sends `hello` (host status, push off; defaults and command list).
2. Send JSON commands → one reply per message (`*_status` or `client_list`). 2. Optionally `ble_scan` / `list_serial_ports` then `host_connect` if not already linked.
3. After `set_stream` with `enable: true`, server may push `input` messages without a prior command. 3. Send JSON commands → one reply per message (`*_status`, `client_list`, `ble_scan_result`, …).
4. After `set_stream` with `enable: true`, server may push `input` messages without a prior command.
Commands and pushes share one socket — always branch on `type`. Commands and pushes share one socket — always branch on `type`.
@@ -27,7 +28,9 @@ On disconnect, `set_stream` state for that socket is dropped. Firmware settings
| This connection | `set_stream` | Whether you receive push JSON on this socket | | This connection | `set_stream` | Whether you receive push JSON on this socket |
UART polling runs only when at least one connection has `receive_input: true` **and** at least one slave streams input or has tap notify enabled. `set_tap_notify` alone does not enable push — you still need `set_stream`. Host polling / BLE ticks run 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`.
On **BLE**, live `input` prefers firmware `BLE_STREAM_TICK` pushes; otherwise falls back to `CACHE_STATUS` like UART.
### Push timing (per connection) ### Push timing (per connection)
@@ -38,7 +41,7 @@ UART polling runs only when at least one connection has `receive_input: true` **
| `pre_fetch` | `set_stream`, `stream_status` | Ms before each push when the host starts the UART cache read | | `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. Global host poll interval = minimum `interval_ms` among all connections with push enabled.
Typical sequence: Typical sequence:
@@ -115,10 +118,14 @@ One JSON object per message; field `type` selects the command.
{ {
"type": "hello", "type": "hello",
"serial_port": "/dev/ttyUSB0", "serial_port": "/dev/ttyUSB0",
"transport": "uart",
"endpoint": "/dev/ttyUSB0",
"connected": true,
"interval_ms": 16, "interval_ms": 16,
"pre_fetch_ms": 2, "pre_fetch_ms": 2,
"tap_display_min_ms": 2000, "tap_display_min_ms": 2000,
"commands": [ "commands": [
"get_host", "list_serial_ports", "ble_scan", "host_connect", "host_disconnect",
"list_clients", "list_clients",
"set_stream", "get_stream", "set_stream", "get_stream",
"set_input_stream", "get_input_stream", "set_input_stream", "get_input_stream",
@@ -128,6 +135,23 @@ One JSON object per message; field `type` selects the command.
} }
``` ```
### Host link
| Client `type` | Reply `type` | Notes |
|---------------|--------------|--------|
| `get_host` | `host_status` | `transport`, `endpoint`, `connected` |
| `list_serial_ports` | `serial_ports` | `{ports:[…]}` |
| `ble_scan` | `ble_scan_result` | optional `timeout_s`, `name_filter`; disconnects active BLE first |
| `host_connect` | `host_status` | `transport:"uart","port"` or `transport:"ble","address","name?"` |
| `host_disconnect` | `host_status` | |
```json
{"type":"host_connect","transport":"ble","address":"AA:BB:CC:DD:EE:FF","name":"Powerpod"}
{"type":"host_status","transport":"ble","endpoint":"AA:BB:…","connected":true,"success":true}
```
If the host is not connected, other commands that need the master return their usual error in the status reply.
### `list_clients` ### `list_clients`
Request: `{"type":"list_clients"}` Request: `{"type":"list_clients"}`
+10
View File
@@ -6,9 +6,19 @@ require (
github.com/gorilla/websocket v1.5.3 github.com/gorilla/websocket v1.5.3
go.bug.st/serial v1.6.4 go.bug.st/serial v1.6.4
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
tinygo.org/x/bluetooth v0.13.0
) )
require ( require (
github.com/creack/goselect v0.1.2 // indirect github.com/creack/goselect v0.1.2 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af // indirect
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 // indirect
github.com/tinygo-org/cbgo v0.0.4 // indirect
github.com/tinygo-org/pio v0.2.0 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/sys v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect
) )
+31
View File
@@ -1,20 +1,51 @@
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b h1:du3zG5fd8snsFN6RBoLA7fpaYV9ZQIsyH9snlk2Zvik=
github.com/saltosystems/winrt-go v0.0.0-20240509164145-4f7860a3bd2b/go.mod h1:CIltaIm7qaANUIvzr0Vmz71lmQMAIbGJ7cvgzX7FMfA=
github.com/sirupsen/logrus v1.5.0/go.mod h1:+F7Ogzej0PZc/94MaYx/nvG9jOFMD2osvC3s+Squfpo=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af h1:ZfFq94aH/BCSWWKd9RPUgdHOdgGKCnfl2VdvU9UksTA=
github.com/soypat/cyw43439 v0.0.0-20250505012923-830110c8f4af/go.mod h1:MUaGO5m6X7xrkHrPDmnaxCEcuCCFN/0ZFh9oie+exbU=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710 h1:Y9fBuiR/urFY/m76+SAZTxk2xAOS2n85f+H1CugajeA=
github.com/soypat/seqs v0.0.0-20250124201400-0d65bc7c1710/go.mod h1:oCVCNGCHMKoBj97Zp9znLbQ1nHxpkmOY9X+UAGzOxc8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tinygo-org/cbgo v0.0.4 h1:3D76CRYbH03Rudi8sEgs/YO0x3JIMdyq8jlQtk/44fU=
github.com/tinygo-org/cbgo v0.0.4/go.mod h1:7+HgWIHd4nbAz0ESjGlJ1/v9LDU1Ox8MGzP9mah/fLk=
github.com/tinygo-org/pio v0.2.0 h1:vo3xa6xDZ2rVtxrks/KcTZHF3qq4lyWOntvEvl2pOhU=
github.com/tinygo-org/pio v0.2.0/go.mod h1:LU7Dw00NJ+N86QkeTGjMLNkYcEYMor6wTDpTCu0EaH8=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
tinygo.org/x/bluetooth v0.13.0 h1:3pkTMcfqv71HoAxG4DBTm2n+1bm6Nqqz8eoHjSW9+5g=
tinygo.org/x/bluetooth v0.13.0/go.mod h1:YnyJRVX09i+wkFeHpXut0b+qHq+T2WwKBRRiF/scANA=
+239
View File
@@ -0,0 +1,239 @@
package main
import (
"fmt"
"time"
pb "powerpod/gotool/pb"
"google.golang.org/protobuf/proto"
)
// hostConn is the UART or BLE host transport used by CLI commands.
type hostConn interface {
exchangePayload(payload []byte, cmdName string) ([]byte, error)
exchange(cmdID byte, cmdName string) ([]byte, error)
Close() error
}
// optional stream source (BLE only)
type hostStreamConn interface {
hostConn
// NextStreamTick waits for a BLE_STREAM_TICK payload (type byte + protobuf).
NextStreamTick(timeout time.Duration) ([]byte, error)
}
func hostGetVersion(h hostConn) (*pb.VersionResponse, error) {
payload, err := h.exchange(byte(pb.MessageType_VERSION), "VERSION")
if err != nil {
return nil, err
}
return decodeVersionPayload(payload)
}
func hostListClients(h hostConn) ([]*pb.ClientInfo, error) {
payload, err := h.exchange(byte(pb.MessageType_CLIENT_INFO), "CLIENT_INFO")
if err != nil {
return nil, err
}
return decodeClientsPayload(payload)
}
func hostReadCacheStatus(h hostConn) (*pb.CacheStatusResponse, error) {
payload, err := h.exchange(byte(pb.MessageType_CACHE_STATUS), "CACHE_STATUS")
if err != nil {
return nil, err
}
return decodeCacheStatusPayload(payload)
}
func hostAccelStream(h hostConn, req *pb.AccelStreamRequest) (*pb.AccelStreamResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_ACCEL_STREAM,
Payload: &pb.UartMessage_AccelStreamRequest{
AccelStreamRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_ACCEL_STREAM)}, body...)
respPayload, err := h.exchangePayload(payload, "ACCEL_STREAM")
if err != nil {
return nil, err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return nil, fmt.Errorf("decode: %w", err)
}
r := respMsg.GetAccelStreamResponse()
if r == nil {
return nil, fmt.Errorf("missing accel_stream_response")
}
return r, nil
}
func hostFindMe(h hostConn, clientID uint32) error {
msg := &pb.UartMessage{
Type: pb.MessageType_FIND_ME,
Payload: &pb.UartMessage_EspnowFindMeRequest{
EspnowFindMeRequest: &pb.EspNowFindMeRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_FIND_ME)}, body...)
respPayload, err := h.exchangePayload(payload, "FIND_ME")
if err != nil {
return err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return fmt.Errorf("decode: %w", err)
}
r := respMsg.GetEspnowFindMeResponse()
if r == nil {
return fmt.Errorf("missing espnow_find_me_response")
}
if !r.GetSuccess() {
return fmt.Errorf("find-me rejected (client_id=%d)", r.GetClientId())
}
return nil
}
func hostRestart(h hostConn, clientID uint32) error {
msg := &pb.UartMessage{
Type: pb.MessageType_RESTART,
Payload: &pb.UartMessage_RestartRequest{
RestartRequest: &pb.RestartRequest{ClientId: clientID},
},
}
body, err := proto.Marshal(msg)
if err != nil {
return fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_RESTART)}, body...)
respPayload, err := h.exchangePayload(payload, "RESTART")
if err != nil {
return err
}
var respMsg pb.UartMessage
if err := proto.Unmarshal(respPayload[1:], &respMsg); err != nil {
return fmt.Errorf("decode: %w", err)
}
r := respMsg.GetRestartResponse()
if r == nil {
return fmt.Errorf("missing restart_response")
}
if !r.GetSuccess() {
return fmt.Errorf("restart rejected (client_id=%d)", r.GetClientId())
}
return nil
}
func hostSetLogLevel(h hostConn, 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 := h.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
}
func hostLedRing(h hostConn, req *pb.LedRingProgressRequest) (*pb.LedRingProgressResponse, error) {
msg := &pb.UartMessage{
Type: pb.MessageType_LED_RING,
Payload: &pb.UartMessage_LedRingProgressRequest{
LedRingProgressRequest: req,
},
}
body, err := proto.Marshal(msg)
if err != nil {
return nil, fmt.Errorf("encode: %w", err)
}
payload := append([]byte{byte(pb.MessageType_LED_RING)}, body...)
respPayload, err := h.exchangePayload(payload, "LED_RING")
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.GetLedRingProgressResponse()
if r == nil {
return nil, fmt.Errorf("missing led_ring_progress_response")
}
return r, nil
}
func hostEchoPing(h hostConn, 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 := h.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
}
+491
View File
@@ -0,0 +1,491 @@
package main
import (
"fmt"
"log"
"strings"
"sync"
"time"
"go.bug.st/serial"
"tinygo.org/x/bluetooth"
)
const (
hostTransportNone = "none"
hostTransportUART = "uart"
hostTransportBLE = "ble"
)
// errHostBusy is returned when the link is held for OTA (UART only).
var errHostBusy = errUARTBusy
// managedHost is a reconnecting UART or BLE host link for serve/CLI.
type managedHost struct {
quiet bool
baud int
settle time.Duration
mu sync.Mutex
transport string // none|uart|ble
endpoint string // port path or BLE address string
bleName string // last connected BLE local name (optional)
sp *serialPort
ble *bleLink
otaActive bool
pausePoll bool // true while BLE latency tests run (pollers yield)
uartPort string // remembered UART port for reconnect
}
func newManagedHost(baud int) *managedHost {
if baud <= 0 {
baud = defaultBaud
}
return &managedHost{
baud: baud,
transport: hostTransportNone,
settle: time.Second,
}
}
// newManagedSerial keeps the old constructor name for call sites during migrate.
func newManagedSerial(portName string, baud int) *managedHost {
m := newManagedHost(baud)
if portName != "" {
m.uartPort = portName
m.endpoint = portName
m.transport = hostTransportUART
}
return m
}
func (m *managedHost) Transport() string {
m.mu.Lock()
defer m.mu.Unlock()
if m.transport == "" {
return hostTransportNone
}
return m.transport
}
func (m *managedHost) Endpoint() string {
m.mu.Lock()
defer m.mu.Unlock()
return m.endpoint
}
func (m *managedHost) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.disconnectLocked()
}
func (m *managedHost) IsConnected() bool {
m.mu.Lock()
defer m.mu.Unlock()
switch m.transport {
case hostTransportUART:
return m.sp != nil
case hostTransportBLE:
return m.ble != nil
default:
return false
}
}
func (m *managedHost) disconnectLocked() error {
var err error
if m.sp != nil {
err = m.sp.port.Close()
m.sp = nil
}
if m.ble != nil {
e := m.ble.Close()
if err == nil {
err = e
}
m.ble = nil
}
m.transport = hostTransportNone
return err
}
func (m *managedHost) Disconnect() error {
m.mu.Lock()
defer m.mu.Unlock()
ep := m.endpoint
err := m.disconnectLocked()
if !m.quiet && ep != "" {
log.Printf("host disconnected (%s)", ep)
}
return err
}
func (m *managedHost) ConnectUART(port string, baud int) error {
if port == "" {
return fmt.Errorf("UART port required")
}
if baud <= 0 {
baud = m.baud
}
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive {
return errHostBusy
}
_ = m.disconnectLocked()
m.baud = baud
m.uartPort = port
m.endpoint = port
m.transport = hostTransportUART
if err := m.openUARTLocked(); err != nil {
m.transport = hostTransportNone
m.endpoint = ""
return err
}
return nil
}
func (m *managedHost) ConnectBLE(address string, name string) error {
if address == "" && name == "" {
return fmt.Errorf("BLE address or name required")
}
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive {
return errHostBusy
}
_ = m.disconnectLocked()
var link *bleLink
var err error
settle := m.settle
m.mu.Unlock()
if address != "" {
link, err = openBLEAddress(address, settle)
} else {
link, err = openBLE(name, settle)
}
m.mu.Lock()
if err != nil {
return err
}
link.quiet = m.quiet
m.ble = link
m.transport = hostTransportBLE
if address != "" {
m.endpoint = address
} else {
m.endpoint = name
}
m.bleName = name
if !m.quiet {
log.Printf("BLE host connected (%s)", m.endpoint)
}
return nil
}
func (m *managedHost) openUARTLocked() error {
sp, err := openSerial(m.uartPort, m.baud)
if err != nil {
return err
}
sp.quiet = m.quiet
m.sp = sp
if !m.quiet {
log.Printf("UART %s connected (%d baud)", m.uartPort, m.baud)
}
return nil
}
func (m *managedHost) openLocked() error {
return m.openUARTLocked()
}
func (m *managedHost) closeLocked() error {
return m.disconnectLocked()
}
func (m *managedHost) invalidateLocked(reason error) {
if m.sp == nil && m.ble == nil {
return
}
if !m.quiet {
log.Printf("host %s disconnected: %v", m.endpoint, reason)
}
_ = m.disconnectLocked()
// remember transport intent for UART reconnect
if m.uartPort != "" && m.transport == hostTransportNone {
// after invalidate, leave as none; poller will try Connect again via open
m.transport = hostTransportUART
m.endpoint = m.uartPort
}
}
func (m *managedHost) withPort(fn func(*serialPort) error) error {
return m.withPortLocked(false, fn)
}
func (m *managedHost) withPortPoll(fn func(*serialPort) error) error {
return m.withPortLocked(true, fn)
}
func (m *managedHost) withPortLocked(poll bool, fn func(*serialPort) error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive {
return errHostBusy
}
if m.transport == hostTransportBLE {
return fmt.Errorf("operation requires UART host (DIP8=0)")
}
if m.transport != hostTransportUART && m.uartPort == "" {
return fmt.Errorf("not connected")
}
if m.transport == hostTransportNone && m.uartPort != "" {
m.transport = hostTransportUART
m.endpoint = m.uartPort
}
if m.sp == nil {
if err := m.openUARTLocked(); err != nil {
return fmt.Errorf("%s: %w", m.uartPort, err)
}
}
err := fn(m.sp)
if err != nil {
m.invalidateLocked(err)
}
return err
}
func (m *managedHost) recoverAfterMasterRestart() {
const bootWait = 6 * time.Second
m.mu.Lock()
tr := m.transport
m.disconnectLocked()
if tr == hostTransportUART {
m.transport = hostTransportUART
m.endpoint = m.uartPort
}
m.mu.Unlock()
log.Printf("host: master restart — waiting %s for boot", bootWait)
time.Sleep(bootWait)
m.mu.Lock()
defer m.mu.Unlock()
if m.transport == hostTransportUART {
if err := m.openUARTLocked(); err != nil {
log.Printf("UART reconnect after master restart: %v", err)
return
}
flushSerialInput(m.sp)
log.Printf("UART %s ready after master restart", m.uartPort)
}
}
func (m *managedHost) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadMode(false, payload, cmdName, readTimeout)
}
func (m *managedHost) exchangePayloadPoll(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadMode(true, payload, cmdName, readTimeout)
}
func (m *managedHost) beginPausePoll() bool {
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive || m.pausePoll {
return false
}
m.pausePoll = true
return true
}
func (m *managedHost) endPausePoll() {
m.mu.Lock()
m.pausePoll = false
m.mu.Unlock()
}
func (m *managedHost) PollPaused() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.pausePoll || m.otaActive
}
func (m *managedHost) exchangePayloadMode(poll bool, payload []byte, cmdName string, timeout time.Duration) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive || (poll && m.pausePoll) {
return nil, errHostBusy
}
switch m.transport {
case hostTransportBLE:
if m.ble == nil {
return nil, fmt.Errorf("BLE not connected")
}
resp, err := m.ble.exchangePayloadTimeout(payload, cmdName, timeout)
if err != nil {
m.invalidateLocked(err)
}
return resp, err
case hostTransportUART, hostTransportNone:
if m.uartPort == "" && m.sp == nil {
return nil, fmt.Errorf("not connected")
}
if m.transport == hostTransportNone {
m.transport = hostTransportUART
m.endpoint = m.uartPort
}
if m.sp == nil {
if err := m.openUARTLocked(); err != nil {
return nil, fmt.Errorf("%s: %w", m.uartPort, err)
}
}
resp, err := m.sp.exchangePayloadLocked(payload, cmdName, timeout)
if err != nil {
m.invalidateLocked(err)
}
return resp, err
default:
return nil, fmt.Errorf("not connected")
}
}
func (m *managedHost) exchange(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangePayload([]byte{cmdID}, cmdName)
}
func (m *managedHost) exchangePoll(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangePayloadPoll([]byte{cmdID}, cmdName)
}
func (m *managedHost) exchangePayloadVia(
portFn func(func(*serialPort) error) error,
payload []byte, cmdName string,
) ([]byte, error) {
// Prefer transport-aware path (works for BLE too).
_ = portFn
return m.exchangePayload(payload, cmdName)
}
func (m *managedHost) exchangeVia(
portFn func(func(*serialPort) error) error,
cmdID byte, cmdName string,
) ([]byte, error) {
_ = portFn
return m.exchange(cmdID, cmdName)
}
func (m *managedHost) NextStreamTick(timeout time.Duration) ([]byte, error) {
m.mu.Lock()
ble := m.ble
tr := m.transport
m.mu.Unlock()
if tr != hostTransportBLE || ble == nil {
return nil, fmt.Errorf("BLE stream ticks require BLE host")
}
return ble.NextStreamTick(timeout)
}
func (m *managedHost) HostStatus() map[string]any {
m.mu.Lock()
defer m.mu.Unlock()
connected := false
switch m.transport {
case hostTransportUART:
connected = m.sp != nil
case hostTransportBLE:
connected = m.ble != nil
}
return map[string]any{
"transport": m.transportOrNone(),
"endpoint": m.endpoint,
"connected": connected,
"ble_name": m.bleName,
}
}
func (m *managedHost) transportOrNone() string {
if m.transport == "" {
return hostTransportNone
}
return m.transport
}
func listSerialPorts() ([]string, error) {
ports, err := serial.GetPortsList()
if err != nil {
return nil, err
}
if ports == nil {
return []string{}, nil
}
return ports, nil
}
type bleScanDevice struct {
Address string `json:"address"`
Name string `json:"name"`
RSSI int `json:"rssi"`
}
func scanBLEDevices(timeout time.Duration, nameFilter string) ([]bleScanDevice, error) {
if timeout <= 0 {
timeout = 5 * time.Second
}
adapter := bluetooth.DefaultAdapter
if err := adapter.Enable(); err != nil {
return nil, fmt.Errorf("enable bluetooth: %w", err)
}
seen := map[string]bleScanDevice{}
var mu sync.Mutex
go func() {
time.Sleep(timeout)
_ = adapter.StopScan()
}()
filter := strings.TrimSpace(nameFilter)
_ = adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
name := result.LocalName()
if filter != "" && !strings.Contains(strings.ToLower(name), strings.ToLower(filter)) {
return
}
addr := result.Address.String()
mu.Lock()
seen[addr] = bleScanDevice{
Address: addr,
Name: name,
RSSI: int(result.RSSI),
}
mu.Unlock()
})
// StopScan ends Scan; ignore error and return whatever was found.
mu.Lock()
defer mu.Unlock()
out := make([]bleScanDevice, 0, len(seen))
for _, d := range seen {
out = append(out, d)
}
return out, nil
}
func disconnectedState(portName string, err error) DashboardState {
msg := "host disconnected"
if err != nil {
msg = err.Error()
}
return DashboardState{
UpdatedAt: time.Now().Format(time.RFC3339),
SerialPort: portName,
HostEndpoint: portName,
UARTConnected: false,
HostConnected: false,
HostTransport: hostTransportNone,
SerialOK: false,
SerialError: msg,
Master: MasterView{OK: false, Error: msg},
Clients: []ClientView{},
}
}
+180 -20
View File
@@ -5,12 +5,14 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"time"
) )
const defaultBaud = 921600 const defaultBaud = 921600
func usage() { func usage() {
fmt.Fprintf(os.Stderr, "usage: gotool [-port /dev/ttyUSB0] <command>\n") fmt.Fprintf(os.Stderr, "usage: gotool [-port /dev/ttyUSB0 | -ble] <command>\n")
fmt.Fprintf(os.Stderr, " DIP8 on master: 0=UART (-port), 1=BLE (-ble); reboot after DIP change\n")
fmt.Fprintf(os.Stderr, " test uses uart.master from bench config when -port is omitted\n\n") fmt.Fprintf(os.Stderr, " test uses uart.master from bench config when -port is omitted\n\n")
fmt.Fprintf(os.Stderr, "commands:\n") fmt.Fprintf(os.Stderr, "commands:\n")
fmt.Fprintf(os.Stderr, " version firmware version and git hash\n") fmt.Fprintf(os.Stderr, " version firmware version and git hash\n")
@@ -18,12 +20,15 @@ func usage() {
fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n") fmt.Fprintf(os.Stderr, " deadzone get/set accelerometer deadzone (LSB)\n")
fmt.Fprintf(os.Stderr, " tap-config get/set BMA456 tap detection parameters\n") fmt.Fprintf(os.Stderr, " tap-config get/set BMA456 tap detection parameters\n")
fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n") fmt.Fprintf(os.Stderr, " tap-notify get/set which tap kinds notify via ESP-NOW\n")
fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one UART round-trip)\n") fmt.Fprintf(os.Stderr, " cache-status subscribed accel + tap cache (one round-trip)\n")
fmt.Fprintf(os.Stderr, " accel-stream enable/disable ESP-NOW accel stream (+ BLE period)\n")
fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n") fmt.Fprintf(os.Stderr, " unicast-test send ESP-NOW unicast test to one slave\n")
fmt.Fprintf(os.Stderr, " echo-ping ESP-NOW timestamp echo round-trip to one slave\n") fmt.Fprintf(os.Stderr, " echo-ping ESP-NOW timestamp echo round-trip to one slave\n")
fmt.Fprintf(os.Stderr, " ble-rtt Host↔Master RTT via VERSION (N samples)\n")
fmt.Fprintf(os.Stderr, " ble-stream enable stream + measure BleStreamTick window\n")
fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n") fmt.Fprintf(os.Stderr, " test run automated scenario (see testdata/)\n")
fmt.Fprintf(os.Stderr, " serve web dashboard (Bootstrap + WebSocket)\n") fmt.Fprintf(os.Stderr, " serve web dashboard (optional -port / -ble; connect from UI)\n")
fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions)\n") fmt.Fprintf(os.Stderr, " ota UART OTA upload (A/B partitions; UART host only)\n")
fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n") fmt.Fprintf(os.Stderr, " ota-progress query per-slave ESP-NOW OTA progress on master\n")
fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n") fmt.Fprintf(os.Stderr, " led-ring set LED ring progress bar (0–100%%, rgb, intensity)\n")
fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n") fmt.Fprintf(os.Stderr, " find-me blink LED ring red/green/blue (3× each, full brightness)\n")
@@ -33,8 +38,11 @@ func usage() {
} }
func main() { func main() {
portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0)") portName := flag.String("port", "", "serial port (e.g. /dev/ttyUSB0); XOR with -ble")
baud := flag.Int("baud", defaultBaud, "UART baud rate") baud := flag.Int("baud", defaultBaud, "UART baud rate")
useBLE := flag.Bool("ble", false, "use BLE host transport (DIP8=1); XOR with -port")
bleName := flag.String("ble-name", bleDefaultName, "BLE advertised name")
bleSettle := flag.Float64("ble-settle", 1.0, "seconds to settle after BLE connect")
flag.Parse() flag.Parse()
if flag.NArg() < 1 { if flag.NArg() < 1 {
@@ -47,55 +55,134 @@ func main() {
var runErr error var runErr error
switch cmd { switch cmd {
case "test", "autotest": case "test", "autotest":
if *useBLE {
runErr = fmt.Errorf("autotest requires -port (UART)")
break
}
runErr = runTest(*portName, *baud, flag.Args()[1:]) runErr = runTest(*portName, *baud, flag.Args()[1:])
case "serve", "web", "dashboard": case "serve", "web", "dashboard":
if *portName == "" { if *useBLE && *portName != "" {
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd) fmt.Fprintf(os.Stderr, "use either -port or -ble, not both\n")
os.Exit(2)
}
runErr = runServe(serveOpts{
portName: *portName,
baud: *baud,
useBLE: *useBLE,
bleName: *bleName,
settle: time.Duration(*bleSettle * float64(time.Second)),
}, flag.Args()[1:])
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-config", "tap_config", "tap-notify", "tap_notify", "cache-status", "cache_status", "accel-stream", "accel_stream", "unicast-test", "unicast_test", "echo-ping", "echo_ping", "ble-rtt", "ble_rtt", "ble-stream", "ble_stream", "led-ring", "led_ring", "find-me", "find_me", "restart", "log-level", "log_level", "ota", "ota-progress", "ota_progress":
if *useBLE && *portName != "" {
fmt.Fprintf(os.Stderr, "use either -port or -ble, not both\n")
os.Exit(2)
}
if !*useBLE && *portName == "" {
fmt.Fprintf(os.Stderr, "command %q requires -port or -ble\n\n", cmd)
usage() usage()
os.Exit(2) os.Exit(2)
} }
runErr = runServe(*portName, *baud, flag.Args()[1:]) if cmd == "ota" || cmd == "ota-progress" || cmd == "ota_progress" {
case "version", "clients", "client-info", "deadzone", "accel-deadzone", "tap-config", "tap_config", "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 *useBLE {
if *portName == "" { fmt.Fprintf(os.Stderr, "OTA requires UART host (DIP8=0, -port)\n")
fmt.Fprintf(os.Stderr, "command %q requires -port\n\n", cmd)
usage()
os.Exit(2) os.Exit(2)
} }
}
var h hostConn
if *useBLE {
bl, err := openBLE(*bleName, time.Duration(*bleSettle*float64(time.Second)))
if err != nil {
log.Fatalf("open BLE: %v", err)
}
h = bl
} else {
sp, err := openSerial(*portName, *baud) sp, err := openSerial(*portName, *baud)
if err != nil { if err != nil {
log.Fatalf("open serial: %v", err) log.Fatalf("open serial: %v", err)
} }
registerShutdown(func() { _ = sp.Close() }) h = sp
}
registerShutdown(func() { _ = h.Close() })
enableShutdownOnInterrupt() enableShutdownOnInterrupt()
defer sp.Close() defer h.Close()
switch cmd { switch cmd {
case "version": case "version":
runErr = runVersion(sp) runErr = runVersionHost(h)
case "clients", "client-info": case "clients", "client-info":
runErr = runClients(sp) runErr = runClientsHost(h)
case "cache-status", "cache_status":
runErr = runCacheStatusHost(h)
case "accel-stream", "accel_stream":
runErr = runAccelStreamCmd(h, flag.Args()[1:])
case "echo-ping", "echo_ping":
runErr = runEchoPingHost(h, flag.Args()[1:])
case "ble-rtt", "ble_rtt":
runErr = runBleRtt(h, flag.Args()[1:])
case "ble-stream", "ble_stream":
runErr = runBleStream(h, flag.Args()[1:])
case "deadzone", "accel-deadzone": case "deadzone", "accel-deadzone":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runDeadzone(sp, flag.Args()[1:]) runErr = runDeadzone(sp, flag.Args()[1:])
case "tap-config", "tap_config": case "tap-config", "tap_config":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runTapConfig(sp, flag.Args()[1:]) runErr = runTapConfig(sp, flag.Args()[1:])
case "tap-notify", "tap_notify": case "tap-notify", "tap_notify":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runTapNotify(sp, flag.Args()[1:]) runErr = runTapNotify(sp, flag.Args()[1:])
case "cache-status", "cache_status":
runErr = runCacheStatus(sp)
case "unicast-test", "unicast_test": case "unicast-test", "unicast_test":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runUnicastTest(sp, flag.Args()[1:]) runErr = runUnicastTest(sp, flag.Args()[1:])
case "echo-ping", "echo_ping":
runErr = runEchoPing(sp, flag.Args()[1:])
case "led-ring", "led_ring": case "led-ring", "led_ring":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runLedRing(sp, flag.Args()[1:]) runErr = runLedRing(sp, flag.Args()[1:])
case "find-me", "find_me": case "find-me", "find_me":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runFindMe(sp, flag.Args()[1:]) runErr = runFindMe(sp, flag.Args()[1:])
case "restart": case "restart":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runRestart(sp, flag.Args()[1:]) runErr = runRestart(sp, flag.Args()[1:])
case "log-level", "log_level": case "log-level", "log_level":
sp, ok := h.(*serialPort)
if !ok {
runErr = fmt.Errorf("%s over BLE not wired yet; use -port", cmd)
break
}
runErr = runLogLevel(sp, flag.Args()[1:]) runErr = runLogLevel(sp, flag.Args()[1:])
case "ota": case "ota":
sp := h.(*serialPort)
runErr = runOTA(sp, flag.Args()[1:]) runErr = runOTA(sp, flag.Args()[1:])
case "ota-progress", "ota_progress": case "ota-progress", "ota_progress":
sp := h.(*serialPort)
runErr = runOtaProgress(sp, flag.Args()[1:]) runErr = runOtaProgress(sp, flag.Args()[1:])
} }
default: default:
@@ -108,3 +195,76 @@ func main() {
log.Fatal(runErr) log.Fatal(runErr)
} }
} }
func runVersionHost(h hostConn) error {
ver, err := hostGetVersion(h)
if err != nil {
return err
}
fmt.Printf("version: %d\n", ver.GetVersion())
fmt.Printf("git_hash: %s\n", ver.GetGitHash())
return nil
}
func runClientsHost(h hostConn) error {
clients, err := hostListClients(h)
if err != nil {
return err
}
if len(clients) == 0 {
fmt.Println("no clients registered")
return nil
}
fmt.Printf("clients (%d):\n", len(clients))
for i, c := range clients {
fmt.Printf(" [%d] id=%d ver=%d available=%v used=%v\n",
i, c.GetId(), c.GetVersion(), c.GetAvailable(), c.GetUsed())
}
return nil
}
func runCacheStatusHost(h hostConn) error {
r, err := hostReadCacheStatus(h)
if err != nil {
return err
}
clients := r.GetClients()
if len(clients) == 0 {
fmt.Println("(no slaves with accel stream or tap notify enabled)")
return nil
}
for _, c := range clients {
id := c.GetClientId()
if a := c.GetAccel(); a != nil {
if !a.GetValid() {
fmt.Printf("client %d accel: no sample yet\n", id)
} else {
fmt.Printf("client %d accel: x=%d y=%d z=%d (age %d ms)\n",
id, a.GetX(), a.GetY(), a.GetZ(), a.GetAgeMs())
}
}
if t := c.GetTap(); t != nil {
fmt.Printf("client %d tap: %s (age %d ms)\n",
id, tapKindLabel(t.GetKind()), t.GetAgeMs())
}
}
return nil
}
func runEchoPingHost(h hostConn, 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 := hostEchoPing(h, uint32(*clientID))
if err != nil {
return err
}
fmt.Printf("echo ping: success=%v client_id=%d rtt_ms=%.3f (host↔master↔slave) esp_rtt_us=%d (ESP-NOW only)\n",
r.Success, r.ClientID, r.RttMs, r.EspRttUs)
return nil
}
+230 -129
View File
@@ -52,6 +52,8 @@ const (
MessageType_SET_LOG_LEVEL MessageType = 31 MessageType_SET_LOG_LEVEL MessageType = 31
// * Host → master: get/set BMA456H multitap detection parameters. // * Host → master: get/set BMA456H multitap detection parameters.
MessageType_TAP_CONFIG MessageType = 32 MessageType_TAP_CONFIG MessageType = 32
// * Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp.
MessageType_BLE_STREAM_TICK MessageType = 33
) )
// Enum value maps for MessageType. // Enum value maps for MessageType.
@@ -81,6 +83,7 @@ var (
30: "ESPNOW_ECHO_PING", 30: "ESPNOW_ECHO_PING",
31: "SET_LOG_LEVEL", 31: "SET_LOG_LEVEL",
32: "TAP_CONFIG", 32: "TAP_CONFIG",
33: "BLE_STREAM_TICK",
} }
MessageType_value = map[string]int32{ MessageType_value = map[string]int32{
"UNKNOWN": 0, "UNKNOWN": 0,
@@ -107,6 +110,7 @@ var (
"ESPNOW_ECHO_PING": 30, "ESPNOW_ECHO_PING": 30,
"SET_LOG_LEVEL": 31, "SET_LOG_LEVEL": 31,
"TAP_CONFIG": 32, "TAP_CONFIG": 32,
"BLE_STREAM_TICK": 33,
} }
) )
@@ -229,6 +233,7 @@ type UartMessage struct {
// *UartMessage_SetLogLevelResponse // *UartMessage_SetLogLevelResponse
// *UartMessage_TapConfigRequest // *UartMessage_TapConfigRequest
// *UartMessage_TapConfigResponse // *UartMessage_TapConfigResponse
// *UartMessage_BleStreamTick
Payload isUartMessage_Payload `protobuf_oneof:"payload"` Payload isUartMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
@@ -593,6 +598,15 @@ func (x *UartMessage) GetTapConfigResponse() *TapConfigResponse {
return nil return nil
} }
func (x *UartMessage) GetBleStreamTick() *BleStreamTick {
if x != nil {
if x, ok := x.Payload.(*UartMessage_BleStreamTick); ok {
return x.BleStreamTick
}
}
return nil
}
type isUartMessage_Payload interface { type isUartMessage_Payload interface {
isUartMessage_Payload() isUartMessage_Payload()
} }
@@ -737,6 +751,10 @@ type UartMessage_TapConfigResponse struct {
TapConfigResponse *TapConfigResponse `protobuf:"bytes,40,opt,name=tap_config_response,json=tapConfigResponse,proto3,oneof"` TapConfigResponse *TapConfigResponse `protobuf:"bytes,40,opt,name=tap_config_response,json=tapConfigResponse,proto3,oneof"`
} }
type UartMessage_BleStreamTick struct {
BleStreamTick *BleStreamTick `protobuf:"bytes,41,opt,name=ble_stream_tick,json=bleStreamTick,proto3,oneof"`
}
func (*UartMessage_AckPayload) isUartMessage_Payload() {} func (*UartMessage_AckPayload) isUartMessage_Payload() {}
func (*UartMessage_EchoPayload) isUartMessage_Payload() {} func (*UartMessage_EchoPayload) isUartMessage_Payload() {}
@@ -807,6 +825,8 @@ func (*UartMessage_TapConfigRequest) isUartMessage_Payload() {}
func (*UartMessage_TapConfigResponse) isUartMessage_Payload() {} func (*UartMessage_TapConfigResponse) isUartMessage_Payload() {}
func (*UartMessage_BleStreamTick) isUartMessage_Payload() {}
type Ack struct { type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
@@ -1371,12 +1391,14 @@ func (x *AccelDeadzoneResponse) GetSlavesUpdated() uint32 {
// Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave). // Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
// write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients). // write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
// period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16).
type AccelStreamRequest struct { type AccelStreamRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Write bool `protobuf:"varint,1,opt,name=write,proto3" json:"write,omitempty"` Write bool `protobuf:"varint,1,opt,name=write,proto3" json:"write,omitempty"`
Enable bool `protobuf:"varint,2,opt,name=enable,proto3" json:"enable,omitempty"` Enable bool `protobuf:"varint,2,opt,name=enable,proto3" json:"enable,omitempty"`
ClientId uint32 `protobuf:"varint,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` ClientId uint32 `protobuf:"varint,3,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
AllClients bool `protobuf:"varint,4,opt,name=all_clients,json=allClients,proto3" json:"all_clients,omitempty"` AllClients bool `protobuf:"varint,4,opt,name=all_clients,json=allClients,proto3" json:"all_clients,omitempty"`
PeriodMs uint32 `protobuf:"varint,5,opt,name=period_ms,json=periodMs,proto3" json:"period_ms,omitempty"`
unknownFields protoimpl.UnknownFields unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache sizeCache protoimpl.SizeCache
} }
@@ -1439,6 +1461,13 @@ func (x *AccelStreamRequest) GetAllClients() bool {
return false return false
} }
func (x *AccelStreamRequest) GetPeriodMs() uint32 {
if x != nil {
return x.PeriodMs
}
return 0
}
type AccelStreamResponse struct { type AccelStreamResponse struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"` Enabled bool `protobuf:"varint,1,opt,name=enabled,proto3" json:"enabled,omitempty"`
@@ -2333,6 +2362,67 @@ func (x *CacheStatusResponse) GetClients() []*CacheClientStatus {
return nil return nil
} }
// * Master → host over BLE notify: aggregated cache + ESP timer timestamp.
type BleStreamTick struct {
state protoimpl.MessageState `protogen:"open.v1"`
EspTimestampUs uint64 `protobuf:"varint,1,opt,name=esp_timestamp_us,json=espTimestampUs,proto3" json:"esp_timestamp_us,omitempty"`
PeriodMs uint32 `protobuf:"varint,2,opt,name=period_ms,json=periodMs,proto3" json:"period_ms,omitempty"`
Clients []*CacheClientStatus `protobuf:"bytes,3,rep,name=clients,proto3" json:"clients,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *BleStreamTick) Reset() {
*x = BleStreamTick{}
mi := &file_uart_messages_proto_msgTypes[25]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *BleStreamTick) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*BleStreamTick) ProtoMessage() {}
func (x *BleStreamTick) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[25]
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 BleStreamTick.ProtoReflect.Descriptor instead.
func (*BleStreamTick) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{25}
}
func (x *BleStreamTick) GetEspTimestampUs() uint64 {
if x != nil {
return x.EspTimestampUs
}
return 0
}
func (x *BleStreamTick) GetPeriodMs() uint32 {
if x != nil {
return x.PeriodMs
}
return 0
}
func (x *BleStreamTick) GetClients() []*CacheClientStatus {
if x != nil {
return x.Clients
}
return nil
}
type EspNowUnicastTestRequest struct { type EspNowUnicastTestRequest struct {
state protoimpl.MessageState `protogen:"open.v1"` state protoimpl.MessageState `protogen:"open.v1"`
ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` ClientId uint32 `protobuf:"varint,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"`
@@ -2343,7 +2433,7 @@ type EspNowUnicastTestRequest struct {
func (x *EspNowUnicastTestRequest) Reset() { func (x *EspNowUnicastTestRequest) Reset() {
*x = EspNowUnicastTestRequest{} *x = EspNowUnicastTestRequest{}
mi := &file_uart_messages_proto_msgTypes[25] mi := &file_uart_messages_proto_msgTypes[26]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2355,7 +2445,7 @@ func (x *EspNowUnicastTestRequest) String() string {
func (*EspNowUnicastTestRequest) ProtoMessage() {} func (*EspNowUnicastTestRequest) ProtoMessage() {}
func (x *EspNowUnicastTestRequest) ProtoReflect() protoreflect.Message { func (x *EspNowUnicastTestRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[25] mi := &file_uart_messages_proto_msgTypes[26]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2368,7 +2458,7 @@ func (x *EspNowUnicastTestRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowUnicastTestRequest.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowUnicastTestRequest.ProtoReflect.Descriptor instead.
func (*EspNowUnicastTestRequest) Descriptor() ([]byte, []int) { func (*EspNowUnicastTestRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{25} return file_uart_messages_proto_rawDescGZIP(), []int{26}
} }
func (x *EspNowUnicastTestRequest) GetClientId() uint32 { func (x *EspNowUnicastTestRequest) GetClientId() uint32 {
@@ -2395,7 +2485,7 @@ type EspNowUnicastTestResponse struct {
func (x *EspNowUnicastTestResponse) Reset() { func (x *EspNowUnicastTestResponse) Reset() {
*x = EspNowUnicastTestResponse{} *x = EspNowUnicastTestResponse{}
mi := &file_uart_messages_proto_msgTypes[26] mi := &file_uart_messages_proto_msgTypes[27]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2407,7 +2497,7 @@ func (x *EspNowUnicastTestResponse) String() string {
func (*EspNowUnicastTestResponse) ProtoMessage() {} func (*EspNowUnicastTestResponse) ProtoMessage() {}
func (x *EspNowUnicastTestResponse) ProtoReflect() protoreflect.Message { func (x *EspNowUnicastTestResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[26] mi := &file_uart_messages_proto_msgTypes[27]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2420,7 +2510,7 @@ func (x *EspNowUnicastTestResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowUnicastTestResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowUnicastTestResponse.ProtoReflect.Descriptor instead.
func (*EspNowUnicastTestResponse) Descriptor() ([]byte, []int) { func (*EspNowUnicastTestResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{26} return file_uart_messages_proto_rawDescGZIP(), []int{27}
} }
func (x *EspNowUnicastTestResponse) GetSuccess() bool { func (x *EspNowUnicastTestResponse) GetSuccess() bool {
@@ -2449,7 +2539,7 @@ type EspNowEchoPingRequest struct {
func (x *EspNowEchoPingRequest) Reset() { func (x *EspNowEchoPingRequest) Reset() {
*x = EspNowEchoPingRequest{} *x = EspNowEchoPingRequest{}
mi := &file_uart_messages_proto_msgTypes[27] mi := &file_uart_messages_proto_msgTypes[28]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2461,7 +2551,7 @@ func (x *EspNowEchoPingRequest) String() string {
func (*EspNowEchoPingRequest) ProtoMessage() {} func (*EspNowEchoPingRequest) ProtoMessage() {}
func (x *EspNowEchoPingRequest) ProtoReflect() protoreflect.Message { func (x *EspNowEchoPingRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[27] mi := &file_uart_messages_proto_msgTypes[28]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2474,7 +2564,7 @@ func (x *EspNowEchoPingRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowEchoPingRequest.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowEchoPingRequest.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingRequest) Descriptor() ([]byte, []int) { func (*EspNowEchoPingRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{27} return file_uart_messages_proto_rawDescGZIP(), []int{28}
} }
func (x *EspNowEchoPingRequest) GetClientId() uint32 { func (x *EspNowEchoPingRequest) GetClientId() uint32 {
@@ -2505,7 +2595,7 @@ type EspNowEchoPingResponse struct {
func (x *EspNowEchoPingResponse) Reset() { func (x *EspNowEchoPingResponse) Reset() {
*x = EspNowEchoPingResponse{} *x = EspNowEchoPingResponse{}
mi := &file_uart_messages_proto_msgTypes[28] mi := &file_uart_messages_proto_msgTypes[29]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2517,7 +2607,7 @@ func (x *EspNowEchoPingResponse) String() string {
func (*EspNowEchoPingResponse) ProtoMessage() {} func (*EspNowEchoPingResponse) ProtoMessage() {}
func (x *EspNowEchoPingResponse) ProtoReflect() protoreflect.Message { func (x *EspNowEchoPingResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[28] mi := &file_uart_messages_proto_msgTypes[29]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2530,7 +2620,7 @@ func (x *EspNowEchoPingResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowEchoPingResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowEchoPingResponse.ProtoReflect.Descriptor instead.
func (*EspNowEchoPingResponse) Descriptor() ([]byte, []int) { func (*EspNowEchoPingResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{28} return file_uart_messages_proto_rawDescGZIP(), []int{29}
} }
func (x *EspNowEchoPingResponse) GetSuccess() bool { func (x *EspNowEchoPingResponse) GetSuccess() bool {
@@ -2591,7 +2681,7 @@ type LedRingProgressRequest struct {
func (x *LedRingProgressRequest) Reset() { func (x *LedRingProgressRequest) Reset() {
*x = LedRingProgressRequest{} *x = LedRingProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[29] mi := &file_uart_messages_proto_msgTypes[30]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2603,7 +2693,7 @@ func (x *LedRingProgressRequest) String() string {
func (*LedRingProgressRequest) ProtoMessage() {} func (*LedRingProgressRequest) ProtoMessage() {}
func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message { func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[29] mi := &file_uart_messages_proto_msgTypes[30]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2616,7 +2706,7 @@ func (x *LedRingProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressRequest.ProtoReflect.Descriptor instead. // Deprecated: Use LedRingProgressRequest.ProtoReflect.Descriptor instead.
func (*LedRingProgressRequest) Descriptor() ([]byte, []int) { func (*LedRingProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{29} return file_uart_messages_proto_rawDescGZIP(), []int{30}
} }
func (x *LedRingProgressRequest) GetMode() uint32 { func (x *LedRingProgressRequest) GetMode() uint32 {
@@ -2717,7 +2807,7 @@ type LedRingProgressResponse struct {
func (x *LedRingProgressResponse) Reset() { func (x *LedRingProgressResponse) Reset() {
*x = LedRingProgressResponse{} *x = LedRingProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[30] mi := &file_uart_messages_proto_msgTypes[31]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2729,7 +2819,7 @@ func (x *LedRingProgressResponse) String() string {
func (*LedRingProgressResponse) ProtoMessage() {} func (*LedRingProgressResponse) ProtoMessage() {}
func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message { func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[30] mi := &file_uart_messages_proto_msgTypes[31]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2742,7 +2832,7 @@ func (x *LedRingProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use LedRingProgressResponse.ProtoReflect.Descriptor instead. // Deprecated: Use LedRingProgressResponse.ProtoReflect.Descriptor instead.
func (*LedRingProgressResponse) Descriptor() ([]byte, []int) { func (*LedRingProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{30} return file_uart_messages_proto_rawDescGZIP(), []int{31}
} }
func (x *LedRingProgressResponse) GetSuccess() bool { func (x *LedRingProgressResponse) GetSuccess() bool {
@@ -2797,7 +2887,7 @@ type EspNowFindMeRequest struct {
func (x *EspNowFindMeRequest) Reset() { func (x *EspNowFindMeRequest) Reset() {
*x = EspNowFindMeRequest{} *x = EspNowFindMeRequest{}
mi := &file_uart_messages_proto_msgTypes[31] mi := &file_uart_messages_proto_msgTypes[32]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2809,7 +2899,7 @@ func (x *EspNowFindMeRequest) String() string {
func (*EspNowFindMeRequest) ProtoMessage() {} func (*EspNowFindMeRequest) ProtoMessage() {}
func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message { func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[31] mi := &file_uart_messages_proto_msgTypes[32]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2822,7 +2912,7 @@ func (x *EspNowFindMeRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowFindMeRequest.ProtoReflect.Descriptor instead.
func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) { func (*EspNowFindMeRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{31} return file_uart_messages_proto_rawDescGZIP(), []int{32}
} }
func (x *EspNowFindMeRequest) GetClientId() uint32 { func (x *EspNowFindMeRequest) GetClientId() uint32 {
@@ -2842,7 +2932,7 @@ type EspNowFindMeResponse struct {
func (x *EspNowFindMeResponse) Reset() { func (x *EspNowFindMeResponse) Reset() {
*x = EspNowFindMeResponse{} *x = EspNowFindMeResponse{}
mi := &file_uart_messages_proto_msgTypes[32] mi := &file_uart_messages_proto_msgTypes[33]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2854,7 +2944,7 @@ func (x *EspNowFindMeResponse) String() string {
func (*EspNowFindMeResponse) ProtoMessage() {} func (*EspNowFindMeResponse) ProtoMessage() {}
func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message { func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[32] mi := &file_uart_messages_proto_msgTypes[33]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2867,7 +2957,7 @@ func (x *EspNowFindMeResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead. // Deprecated: Use EspNowFindMeResponse.ProtoReflect.Descriptor instead.
func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) { func (*EspNowFindMeResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{32} return file_uart_messages_proto_rawDescGZIP(), []int{33}
} }
func (x *EspNowFindMeResponse) GetSuccess() bool { func (x *EspNowFindMeResponse) GetSuccess() bool {
@@ -2894,7 +2984,7 @@ type RestartRequest struct {
func (x *RestartRequest) Reset() { func (x *RestartRequest) Reset() {
*x = RestartRequest{} *x = RestartRequest{}
mi := &file_uart_messages_proto_msgTypes[33] mi := &file_uart_messages_proto_msgTypes[34]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2906,7 +2996,7 @@ func (x *RestartRequest) String() string {
func (*RestartRequest) ProtoMessage() {} func (*RestartRequest) ProtoMessage() {}
func (x *RestartRequest) ProtoReflect() protoreflect.Message { func (x *RestartRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[33] mi := &file_uart_messages_proto_msgTypes[34]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2919,7 +3009,7 @@ func (x *RestartRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartRequest.ProtoReflect.Descriptor instead. // Deprecated: Use RestartRequest.ProtoReflect.Descriptor instead.
func (*RestartRequest) Descriptor() ([]byte, []int) { func (*RestartRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{33} return file_uart_messages_proto_rawDescGZIP(), []int{34}
} }
func (x *RestartRequest) GetClientId() uint32 { func (x *RestartRequest) GetClientId() uint32 {
@@ -2939,7 +3029,7 @@ type RestartResponse struct {
func (x *RestartResponse) Reset() { func (x *RestartResponse) Reset() {
*x = RestartResponse{} *x = RestartResponse{}
mi := &file_uart_messages_proto_msgTypes[34] mi := &file_uart_messages_proto_msgTypes[35]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -2951,7 +3041,7 @@ func (x *RestartResponse) String() string {
func (*RestartResponse) ProtoMessage() {} func (*RestartResponse) ProtoMessage() {}
func (x *RestartResponse) ProtoReflect() protoreflect.Message { func (x *RestartResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[34] mi := &file_uart_messages_proto_msgTypes[35]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -2964,7 +3054,7 @@ func (x *RestartResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use RestartResponse.ProtoReflect.Descriptor instead. // Deprecated: Use RestartResponse.ProtoReflect.Descriptor instead.
func (*RestartResponse) Descriptor() ([]byte, []int) { func (*RestartResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{34} return file_uart_messages_proto_rawDescGZIP(), []int{35}
} }
func (x *RestartResponse) GetSuccess() bool { func (x *RestartResponse) GetSuccess() bool {
@@ -2993,7 +3083,7 @@ type SetLogLevelRequest struct {
func (x *SetLogLevelRequest) Reset() { func (x *SetLogLevelRequest) Reset() {
*x = SetLogLevelRequest{} *x = SetLogLevelRequest{}
mi := &file_uart_messages_proto_msgTypes[35] mi := &file_uart_messages_proto_msgTypes[36]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3005,7 +3095,7 @@ func (x *SetLogLevelRequest) String() string {
func (*SetLogLevelRequest) ProtoMessage() {} func (*SetLogLevelRequest) ProtoMessage() {}
func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message { func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[35] mi := &file_uart_messages_proto_msgTypes[36]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3018,7 +3108,7 @@ func (x *SetLogLevelRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead. // Deprecated: Use SetLogLevelRequest.ProtoReflect.Descriptor instead.
func (*SetLogLevelRequest) Descriptor() ([]byte, []int) { func (*SetLogLevelRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{35} return file_uart_messages_proto_rawDescGZIP(), []int{36}
} }
func (x *SetLogLevelRequest) GetWrite() bool { func (x *SetLogLevelRequest) GetWrite() bool {
@@ -3045,7 +3135,7 @@ type SetLogLevelResponse struct {
func (x *SetLogLevelResponse) Reset() { func (x *SetLogLevelResponse) Reset() {
*x = SetLogLevelResponse{} *x = SetLogLevelResponse{}
mi := &file_uart_messages_proto_msgTypes[36] mi := &file_uart_messages_proto_msgTypes[37]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3057,7 +3147,7 @@ func (x *SetLogLevelResponse) String() string {
func (*SetLogLevelResponse) ProtoMessage() {} func (*SetLogLevelResponse) ProtoMessage() {}
func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message { func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[36] mi := &file_uart_messages_proto_msgTypes[37]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3070,7 +3160,7 @@ func (x *SetLogLevelResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead. // Deprecated: Use SetLogLevelResponse.ProtoReflect.Descriptor instead.
func (*SetLogLevelResponse) Descriptor() ([]byte, []int) { func (*SetLogLevelResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{36} return file_uart_messages_proto_rawDescGZIP(), []int{37}
} }
func (x *SetLogLevelResponse) GetSuccess() bool { func (x *SetLogLevelResponse) GetSuccess() bool {
@@ -3109,7 +3199,7 @@ type TapConfigRequest struct {
func (x *TapConfigRequest) Reset() { func (x *TapConfigRequest) Reset() {
*x = TapConfigRequest{} *x = TapConfigRequest{}
mi := &file_uart_messages_proto_msgTypes[37] mi := &file_uart_messages_proto_msgTypes[38]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3121,7 +3211,7 @@ func (x *TapConfigRequest) String() string {
func (*TapConfigRequest) ProtoMessage() {} func (*TapConfigRequest) ProtoMessage() {}
func (x *TapConfigRequest) ProtoReflect() protoreflect.Message { func (x *TapConfigRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[37] mi := &file_uart_messages_proto_msgTypes[38]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3134,7 +3224,7 @@ func (x *TapConfigRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use TapConfigRequest.ProtoReflect.Descriptor instead. // Deprecated: Use TapConfigRequest.ProtoReflect.Descriptor instead.
func (*TapConfigRequest) Descriptor() ([]byte, []int) { func (*TapConfigRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{37} return file_uart_messages_proto_rawDescGZIP(), []int{38}
} }
func (x *TapConfigRequest) GetWrite() bool { func (x *TapConfigRequest) GetWrite() bool {
@@ -3241,7 +3331,7 @@ type TapConfigResponse struct {
func (x *TapConfigResponse) Reset() { func (x *TapConfigResponse) Reset() {
*x = TapConfigResponse{} *x = TapConfigResponse{}
mi := &file_uart_messages_proto_msgTypes[38] mi := &file_uart_messages_proto_msgTypes[39]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3253,7 +3343,7 @@ func (x *TapConfigResponse) String() string {
func (*TapConfigResponse) ProtoMessage() {} func (*TapConfigResponse) ProtoMessage() {}
func (x *TapConfigResponse) ProtoReflect() protoreflect.Message { func (x *TapConfigResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[38] mi := &file_uart_messages_proto_msgTypes[39]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3266,7 +3356,7 @@ func (x *TapConfigResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use TapConfigResponse.ProtoReflect.Descriptor instead. // Deprecated: Use TapConfigResponse.ProtoReflect.Descriptor instead.
func (*TapConfigResponse) Descriptor() ([]byte, []int) { func (*TapConfigResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{38} return file_uart_messages_proto_rawDescGZIP(), []int{39}
} }
func (x *TapConfigResponse) GetClientId() uint32 { func (x *TapConfigResponse) GetClientId() uint32 {
@@ -3363,7 +3453,7 @@ type OtaStartPayload struct {
func (x *OtaStartPayload) Reset() { func (x *OtaStartPayload) Reset() {
*x = OtaStartPayload{} *x = OtaStartPayload{}
mi := &file_uart_messages_proto_msgTypes[39] mi := &file_uart_messages_proto_msgTypes[40]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3375,7 +3465,7 @@ func (x *OtaStartPayload) String() string {
func (*OtaStartPayload) ProtoMessage() {} func (*OtaStartPayload) ProtoMessage() {}
func (x *OtaStartPayload) ProtoReflect() protoreflect.Message { func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[39] mi := &file_uart_messages_proto_msgTypes[40]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3388,7 +3478,7 @@ func (x *OtaStartPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaStartPayload.ProtoReflect.Descriptor instead.
func (*OtaStartPayload) Descriptor() ([]byte, []int) { func (*OtaStartPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{39} return file_uart_messages_proto_rawDescGZIP(), []int{40}
} }
func (x *OtaStartPayload) GetTotalSize() uint32 { func (x *OtaStartPayload) GetTotalSize() uint32 {
@@ -3409,7 +3499,7 @@ type OtaPayload struct {
func (x *OtaPayload) Reset() { func (x *OtaPayload) Reset() {
*x = OtaPayload{} *x = OtaPayload{}
mi := &file_uart_messages_proto_msgTypes[40] mi := &file_uart_messages_proto_msgTypes[41]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3421,7 +3511,7 @@ func (x *OtaPayload) String() string {
func (*OtaPayload) ProtoMessage() {} func (*OtaPayload) ProtoMessage() {}
func (x *OtaPayload) ProtoReflect() protoreflect.Message { func (x *OtaPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[40] mi := &file_uart_messages_proto_msgTypes[41]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3434,7 +3524,7 @@ func (x *OtaPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaPayload.ProtoReflect.Descriptor instead.
func (*OtaPayload) Descriptor() ([]byte, []int) { func (*OtaPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{40} return file_uart_messages_proto_rawDescGZIP(), []int{41}
} }
func (x *OtaPayload) GetSeq() uint32 { func (x *OtaPayload) GetSeq() uint32 {
@@ -3460,7 +3550,7 @@ type OtaEndPayload struct {
func (x *OtaEndPayload) Reset() { func (x *OtaEndPayload) Reset() {
*x = OtaEndPayload{} *x = OtaEndPayload{}
mi := &file_uart_messages_proto_msgTypes[41] mi := &file_uart_messages_proto_msgTypes[42]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3472,7 +3562,7 @@ func (x *OtaEndPayload) String() string {
func (*OtaEndPayload) ProtoMessage() {} func (*OtaEndPayload) ProtoMessage() {}
func (x *OtaEndPayload) ProtoReflect() protoreflect.Message { func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[41] mi := &file_uart_messages_proto_msgTypes[42]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3485,7 +3575,7 @@ func (x *OtaEndPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaEndPayload.ProtoReflect.Descriptor instead.
func (*OtaEndPayload) Descriptor() ([]byte, []int) { func (*OtaEndPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{41} return file_uart_messages_proto_rawDescGZIP(), []int{42}
} }
// Device → host status (also used as ACK after each 4 KiB written). // Device → host status (also used as ACK after each 4 KiB written).
@@ -3502,7 +3592,7 @@ type OtaStatusPayload struct {
func (x *OtaStatusPayload) Reset() { func (x *OtaStatusPayload) Reset() {
*x = OtaStatusPayload{} *x = OtaStatusPayload{}
mi := &file_uart_messages_proto_msgTypes[42] mi := &file_uart_messages_proto_msgTypes[43]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3514,7 +3604,7 @@ func (x *OtaStatusPayload) String() string {
func (*OtaStatusPayload) ProtoMessage() {} func (*OtaStatusPayload) ProtoMessage() {}
func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message { func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[42] mi := &file_uart_messages_proto_msgTypes[43]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3527,7 +3617,7 @@ func (x *OtaStatusPayload) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead. // Deprecated: Use OtaStatusPayload.ProtoReflect.Descriptor instead.
func (*OtaStatusPayload) Descriptor() ([]byte, []int) { func (*OtaStatusPayload) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{42} return file_uart_messages_proto_rawDescGZIP(), []int{43}
} }
func (x *OtaStatusPayload) GetStatus() uint32 { func (x *OtaStatusPayload) GetStatus() uint32 {
@@ -3568,7 +3658,7 @@ type OtaSlaveProgressRequest struct {
func (x *OtaSlaveProgressRequest) Reset() { func (x *OtaSlaveProgressRequest) Reset() {
*x = OtaSlaveProgressRequest{} *x = OtaSlaveProgressRequest{}
mi := &file_uart_messages_proto_msgTypes[43] mi := &file_uart_messages_proto_msgTypes[44]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3580,7 +3670,7 @@ func (x *OtaSlaveProgressRequest) String() string {
func (*OtaSlaveProgressRequest) ProtoMessage() {} func (*OtaSlaveProgressRequest) ProtoMessage() {}
func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[43] mi := &file_uart_messages_proto_msgTypes[44]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3593,7 +3683,7 @@ func (x *OtaSlaveProgressRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressRequest.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressRequest) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{43} return file_uart_messages_proto_rawDescGZIP(), []int{44}
} }
func (x *OtaSlaveProgressRequest) GetClientId() uint32 { func (x *OtaSlaveProgressRequest) GetClientId() uint32 {
@@ -3617,7 +3707,7 @@ type OtaSlaveProgressEntry struct {
func (x *OtaSlaveProgressEntry) Reset() { func (x *OtaSlaveProgressEntry) Reset() {
*x = OtaSlaveProgressEntry{} *x = OtaSlaveProgressEntry{}
mi := &file_uart_messages_proto_msgTypes[44] mi := &file_uart_messages_proto_msgTypes[45]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3629,7 +3719,7 @@ func (x *OtaSlaveProgressEntry) String() string {
func (*OtaSlaveProgressEntry) ProtoMessage() {} func (*OtaSlaveProgressEntry) ProtoMessage() {}
func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[44] mi := &file_uart_messages_proto_msgTypes[45]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3642,7 +3732,7 @@ func (x *OtaSlaveProgressEntry) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressEntry.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressEntry) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{44} return file_uart_messages_proto_rawDescGZIP(), []int{45}
} }
func (x *OtaSlaveProgressEntry) GetClientId() uint32 { func (x *OtaSlaveProgressEntry) GetClientId() uint32 {
@@ -3693,7 +3783,7 @@ type OtaSlaveProgressResponse struct {
func (x *OtaSlaveProgressResponse) Reset() { func (x *OtaSlaveProgressResponse) Reset() {
*x = OtaSlaveProgressResponse{} *x = OtaSlaveProgressResponse{}
mi := &file_uart_messages_proto_msgTypes[45] mi := &file_uart_messages_proto_msgTypes[46]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi) ms.StoreMessageInfo(mi)
} }
@@ -3705,7 +3795,7 @@ func (x *OtaSlaveProgressResponse) String() string {
func (*OtaSlaveProgressResponse) ProtoMessage() {} func (*OtaSlaveProgressResponse) ProtoMessage() {}
func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message { func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
mi := &file_uart_messages_proto_msgTypes[45] mi := &file_uart_messages_proto_msgTypes[46]
if x != nil { if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil { if ms.LoadMessageInfo() == nil {
@@ -3718,7 +3808,7 @@ func (x *OtaSlaveProgressResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead. // Deprecated: Use OtaSlaveProgressResponse.ProtoReflect.Descriptor instead.
func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) { func (*OtaSlaveProgressResponse) Descriptor() ([]byte, []int) {
return file_uart_messages_proto_rawDescGZIP(), []int{45} return file_uart_messages_proto_rawDescGZIP(), []int{46}
} }
func (x *OtaSlaveProgressResponse) GetActive() bool { func (x *OtaSlaveProgressResponse) GetActive() bool {
@@ -3760,7 +3850,7 @@ var File_uart_messages_proto protoreflect.FileDescriptor
const file_uart_messages_proto_rawDesc = "" + const file_uart_messages_proto_rawDesc = "" +
"\n" + "\n" +
"\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\xd3\x15\n" + "\x13uart_messages.proto\x12\x04alox\x1a\fnanopb.proto\"\x92\x16\n" +
"\vUartMessage\x12%\n" + "\vUartMessage\x12%\n" +
"\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" + "\x04type\x18\x01 \x01(\x0e2\x11.alox.MessageTypeR\x04type\x12,\n" +
"\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" + "\vack_payload\x18\x02 \x01(\v2\t.alox.AckH\x00R\n" +
@@ -3801,7 +3891,8 @@ const file_uart_messages_proto_rawDesc = "" +
"\x15set_log_level_request\x18% \x01(\v2\x18.alox.SetLogLevelRequestH\x00R\x12setLogLevelRequest\x12P\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\x13setLogLevelResponse\x12F\n" + "\x16set_log_level_response\x18& \x01(\v2\x19.alox.SetLogLevelResponseH\x00R\x13setLogLevelResponse\x12F\n" +
"\x12tap_config_request\x18' \x01(\v2\x16.alox.TapConfigRequestH\x00R\x10tapConfigRequest\x12I\n" + "\x12tap_config_request\x18' \x01(\v2\x16.alox.TapConfigRequestH\x00R\x10tapConfigRequest\x12I\n" +
"\x13tap_config_response\x18( \x01(\v2\x17.alox.TapConfigResponseH\x00R\x11tapConfigResponseB\t\n" + "\x13tap_config_response\x18( \x01(\v2\x17.alox.TapConfigResponseH\x00R\x11tapConfigResponse\x12=\n" +
"\x0fble_stream_tick\x18) \x01(\v2\x13.alox.BleStreamTickH\x00R\rbleStreamTickB\t\n" +
"\apayload\"\x05\n" + "\apayload\"\x05\n" +
"\x03Ack\"!\n" + "\x03Ack\"!\n" +
"\vEchoPayload\x12\x12\n" + "\vEchoPayload\x12\x12\n" +
@@ -3843,13 +3934,14 @@ const file_uart_messages_proto_rawDesc = "" +
"\bdeadzone\x18\x01 \x01(\rR\bdeadzone\x12\x1b\n" + "\bdeadzone\x18\x01 \x01(\rR\bdeadzone\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\x12\x18\n" + "\tclient_id\x18\x02 \x01(\rR\bclientId\x12\x18\n" +
"\asuccess\x18\x03 \x01(\bR\asuccess\x12%\n" + "\asuccess\x18\x03 \x01(\bR\asuccess\x12%\n" +
"\x0eslaves_updated\x18\x04 \x01(\rR\rslavesUpdated\"\x80\x01\n" + "\x0eslaves_updated\x18\x04 \x01(\rR\rslavesUpdated\"\x9d\x01\n" +
"\x12AccelStreamRequest\x12\x14\n" + "\x12AccelStreamRequest\x12\x14\n" +
"\x05write\x18\x01 \x01(\bR\x05write\x12\x16\n" + "\x05write\x18\x01 \x01(\bR\x05write\x12\x16\n" +
"\x06enable\x18\x02 \x01(\bR\x06enable\x12\x1b\n" + "\x06enable\x18\x02 \x01(\bR\x06enable\x12\x1b\n" +
"\tclient_id\x18\x03 \x01(\rR\bclientId\x12\x1f\n" + "\tclient_id\x18\x03 \x01(\rR\bclientId\x12\x1f\n" +
"\vall_clients\x18\x04 \x01(\bR\n" + "\vall_clients\x18\x04 \x01(\bR\n" +
"allClients\"\x8d\x01\n" + "allClients\x12\x1b\n" +
"\tperiod_ms\x18\x05 \x01(\rR\bperiodMs\"\x8d\x01\n" +
"\x13AccelStreamResponse\x12\x18\n" + "\x13AccelStreamResponse\x12\x18\n" +
"\aenabled\x18\x01 \x01(\bR\aenabled\x12\x1b\n" + "\aenabled\x18\x01 \x01(\bR\aenabled\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\rR\bclientId\x12\x18\n" + "\tclient_id\x18\x02 \x01(\rR\bclientId\x12\x18\n" +
@@ -3915,7 +4007,11 @@ const file_uart_messages_proto_rawDesc = "" +
"\x05accel\x18\x02 \x01(\v2\x16.alox.CacheClientAccelR\x05accel\x12&\n" + "\x05accel\x18\x02 \x01(\v2\x16.alox.CacheClientAccelR\x05accel\x12&\n" +
"\x03tap\x18\x03 \x01(\v2\x14.alox.CacheClientTapR\x03tap\"O\n" + "\x03tap\x18\x03 \x01(\v2\x14.alox.CacheClientTapR\x03tap\"O\n" +
"\x13CacheStatusResponse\x128\n" + "\x13CacheStatusResponse\x128\n" +
"\aclients\x18\x01 \x03(\v2\x17.alox.CacheClientStatusB\x05\x92?\x02\x10\x10R\aclients\"I\n" + "\aclients\x18\x01 \x03(\v2\x17.alox.CacheClientStatusB\x05\x92?\x02\x10\x10R\aclients\"\x90\x01\n" +
"\rBleStreamTick\x12(\n" +
"\x10esp_timestamp_us\x18\x01 \x01(\x04R\x0eespTimestampUs\x12\x1b\n" +
"\tperiod_ms\x18\x02 \x01(\rR\bperiodMs\x128\n" +
"\aclients\x18\x03 \x03(\v2\x17.alox.CacheClientStatusB\x05\x92?\x02\x10\x10R\aclients\"I\n" +
"\x18EspNowUnicastTestRequest\x12\x1b\n" + "\x18EspNowUnicastTestRequest\x12\x1b\n" +
"\tclient_id\x18\x01 \x01(\rR\bclientId\x12\x10\n" + "\tclient_id\x18\x01 \x01(\rR\bclientId\x12\x10\n" +
"\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" + "\x03seq\x18\x02 \x01(\rR\x03seq\"G\n" +
@@ -4032,7 +4128,7 @@ const file_uart_messages_proto_rawDesc = "" +
"\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" + "\x0faggregate_bytes\x18\x03 \x01(\rR\x0eaggregateBytes\x12\x1f\n" +
"\vslave_count\x18\x04 \x01(\rR\n" + "\vslave_count\x18\x04 \x01(\rR\n" +
"slaveCount\x12:\n" + "slaveCount\x12:\n" +
"\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xaa\x03\n" + "\x06slaves\x18\x05 \x03(\v2\x1b.alox.OtaSlaveProgressEntryB\x05\x92?\x02\x10\x10R\x06slaves*\xbf\x03\n" +
"\vMessageType\x12\v\n" + "\vMessageType\x12\v\n" +
"\aUNKNOWN\x10\x00\x12\a\n" + "\aUNKNOWN\x10\x00\x12\a\n" +
"\x03ACK\x10\x01\x12\b\n" + "\x03ACK\x10\x01\x12\b\n" +
@@ -4060,7 +4156,8 @@ const file_uart_messages_proto_rawDesc = "" +
"\x10ESPNOW_ECHO_PING\x10\x1e\x12\x11\n" + "\x10ESPNOW_ECHO_PING\x10\x1e\x12\x11\n" +
"\rSET_LOG_LEVEL\x10\x1f\x12\x0e\n" + "\rSET_LOG_LEVEL\x10\x1f\x12\x0e\n" +
"\n" + "\n" +
"TAP_CONFIG\x10 \"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" + "TAP_CONFIG\x10 \x12\x13\n" +
"\x0fBLE_STREAM_TICK\x10!\"\x04\b\x18\x10\x18\"\x04\b\x1c\x10\x1c*G\n" +
"\aTapKind\x12\f\n" + "\aTapKind\x12\f\n" +
"\bTAP_NONE\x10\x00\x12\x0e\n" + "\bTAP_NONE\x10\x00\x12\x0e\n" +
"\n" + "\n" +
@@ -4083,7 +4180,7 @@ func file_uart_messages_proto_rawDescGZIP() []byte {
} }
var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2) var file_uart_messages_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 46) var file_uart_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 47)
var file_uart_messages_proto_goTypes = []any{ var file_uart_messages_proto_goTypes = []any{
(MessageType)(0), // 0: alox.MessageType (MessageType)(0), // 0: alox.MessageType
(TapKind)(0), // 1: alox.TapKind (TapKind)(0), // 1: alox.TapKind
@@ -4112,27 +4209,28 @@ var file_uart_messages_proto_goTypes = []any{
(*CacheClientTap)(nil), // 24: alox.CacheClientTap (*CacheClientTap)(nil), // 24: alox.CacheClientTap
(*CacheClientStatus)(nil), // 25: alox.CacheClientStatus (*CacheClientStatus)(nil), // 25: alox.CacheClientStatus
(*CacheStatusResponse)(nil), // 26: alox.CacheStatusResponse (*CacheStatusResponse)(nil), // 26: alox.CacheStatusResponse
(*EspNowUnicastTestRequest)(nil), // 27: alox.EspNowUnicastTestRequest (*BleStreamTick)(nil), // 27: alox.BleStreamTick
(*EspNowUnicastTestResponse)(nil), // 28: alox.EspNowUnicastTestResponse (*EspNowUnicastTestRequest)(nil), // 28: alox.EspNowUnicastTestRequest
(*EspNowEchoPingRequest)(nil), // 29: alox.EspNowEchoPingRequest (*EspNowUnicastTestResponse)(nil), // 29: alox.EspNowUnicastTestResponse
(*EspNowEchoPingResponse)(nil), // 30: alox.EspNowEchoPingResponse (*EspNowEchoPingRequest)(nil), // 30: alox.EspNowEchoPingRequest
(*LedRingProgressRequest)(nil), // 31: alox.LedRingProgressRequest (*EspNowEchoPingResponse)(nil), // 31: alox.EspNowEchoPingResponse
(*LedRingProgressResponse)(nil), // 32: alox.LedRingProgressResponse (*LedRingProgressRequest)(nil), // 32: alox.LedRingProgressRequest
(*EspNowFindMeRequest)(nil), // 33: alox.EspNowFindMeRequest (*LedRingProgressResponse)(nil), // 33: alox.LedRingProgressResponse
(*EspNowFindMeResponse)(nil), // 34: alox.EspNowFindMeResponse (*EspNowFindMeRequest)(nil), // 34: alox.EspNowFindMeRequest
(*RestartRequest)(nil), // 35: alox.RestartRequest (*EspNowFindMeResponse)(nil), // 35: alox.EspNowFindMeResponse
(*RestartResponse)(nil), // 36: alox.RestartResponse (*RestartRequest)(nil), // 36: alox.RestartRequest
(*SetLogLevelRequest)(nil), // 37: alox.SetLogLevelRequest (*RestartResponse)(nil), // 37: alox.RestartResponse
(*SetLogLevelResponse)(nil), // 38: alox.SetLogLevelResponse (*SetLogLevelRequest)(nil), // 38: alox.SetLogLevelRequest
(*TapConfigRequest)(nil), // 39: alox.TapConfigRequest (*SetLogLevelResponse)(nil), // 39: alox.SetLogLevelResponse
(*TapConfigResponse)(nil), // 40: alox.TapConfigResponse (*TapConfigRequest)(nil), // 40: alox.TapConfigRequest
(*OtaStartPayload)(nil), // 41: alox.OtaStartPayload (*TapConfigResponse)(nil), // 41: alox.TapConfigResponse
(*OtaPayload)(nil), // 42: alox.OtaPayload (*OtaStartPayload)(nil), // 42: alox.OtaStartPayload
(*OtaEndPayload)(nil), // 43: alox.OtaEndPayload (*OtaPayload)(nil), // 43: alox.OtaPayload
(*OtaStatusPayload)(nil), // 44: alox.OtaStatusPayload (*OtaEndPayload)(nil), // 44: alox.OtaEndPayload
(*OtaSlaveProgressRequest)(nil), // 45: alox.OtaSlaveProgressRequest (*OtaStatusPayload)(nil), // 45: alox.OtaStatusPayload
(*OtaSlaveProgressEntry)(nil), // 46: alox.OtaSlaveProgressEntry (*OtaSlaveProgressRequest)(nil), // 46: alox.OtaSlaveProgressRequest
(*OtaSlaveProgressResponse)(nil), // 47: alox.OtaSlaveProgressResponse (*OtaSlaveProgressEntry)(nil), // 47: alox.OtaSlaveProgressEntry
(*OtaSlaveProgressResponse)(nil), // 48: alox.OtaSlaveProgressResponse
} }
var file_uart_messages_proto_depIdxs = []int32{ var file_uart_messages_proto_depIdxs = []int32{
0, // 0: alox.UartMessage.type:type_name -> alox.MessageType 0, // 0: alox.UartMessage.type:type_name -> alox.MessageType
@@ -4141,22 +4239,22 @@ var file_uart_messages_proto_depIdxs = []int32{
5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse 5, // 3: alox.UartMessage.version_response:type_name -> alox.VersionResponse
7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse 7, // 4: alox.UartMessage.client_info_response:type_name -> alox.ClientInfoResponse
9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse 9, // 5: alox.UartMessage.client_input_response:type_name -> alox.ClientInputResponse
41, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload 42, // 6: alox.UartMessage.ota_start:type_name -> alox.OtaStartPayload
42, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload 43, // 7: alox.UartMessage.ota_payload:type_name -> alox.OtaPayload
43, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload 44, // 8: alox.UartMessage.ota_end:type_name -> alox.OtaEndPayload
44, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload 45, // 9: alox.UartMessage.ota_status:type_name -> alox.OtaStatusPayload
10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest 10, // 10: alox.UartMessage.accel_deadzone_request:type_name -> alox.AccelDeadzoneRequest
11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse 11, // 11: alox.UartMessage.accel_deadzone_response:type_name -> alox.AccelDeadzoneResponse
27, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest 28, // 12: alox.UartMessage.espnow_unicast_test_request:type_name -> alox.EspNowUnicastTestRequest
28, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse 29, // 13: alox.UartMessage.espnow_unicast_test_response:type_name -> alox.EspNowUnicastTestResponse
45, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest 46, // 14: alox.UartMessage.ota_slave_progress_request:type_name -> alox.OtaSlaveProgressRequest
47, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse 48, // 15: alox.UartMessage.ota_slave_progress_response:type_name -> alox.OtaSlaveProgressResponse
31, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest 32, // 16: alox.UartMessage.led_ring_progress_request:type_name -> alox.LedRingProgressRequest
32, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse 33, // 17: alox.UartMessage.led_ring_progress_response:type_name -> alox.LedRingProgressResponse
33, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest 34, // 18: alox.UartMessage.espnow_find_me_request:type_name -> alox.EspNowFindMeRequest
34, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse 35, // 19: alox.UartMessage.espnow_find_me_response:type_name -> alox.EspNowFindMeResponse
35, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest 36, // 20: alox.UartMessage.restart_request:type_name -> alox.RestartRequest
36, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse 37, // 21: alox.UartMessage.restart_response:type_name -> alox.RestartResponse
12, // 22: alox.UartMessage.accel_stream_request:type_name -> alox.AccelStreamRequest 12, // 22: alox.UartMessage.accel_stream_request:type_name -> alox.AccelStreamRequest
13, // 23: alox.UartMessage.accel_stream_response:type_name -> alox.AccelStreamResponse 13, // 23: alox.UartMessage.accel_stream_response:type_name -> alox.AccelStreamResponse
14, // 24: alox.UartMessage.battery_status_request:type_name -> alox.BatteryStatusRequest 14, // 24: alox.UartMessage.battery_status_request:type_name -> alox.BatteryStatusRequest
@@ -4165,28 +4263,30 @@ var file_uart_messages_proto_depIdxs = []int32{
20, // 27: alox.UartMessage.tap_notify_response:type_name -> alox.TapNotifyResponse 20, // 27: alox.UartMessage.tap_notify_response:type_name -> alox.TapNotifyResponse
22, // 28: alox.UartMessage.cache_status_request:type_name -> alox.CacheStatusRequest 22, // 28: alox.UartMessage.cache_status_request:type_name -> alox.CacheStatusRequest
26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse 26, // 29: alox.UartMessage.cache_status_response:type_name -> alox.CacheStatusResponse
29, // 30: alox.UartMessage.espnow_echo_ping_request:type_name -> alox.EspNowEchoPingRequest 30, // 30: alox.UartMessage.espnow_echo_ping_request:type_name -> alox.EspNowEchoPingRequest
30, // 31: alox.UartMessage.espnow_echo_ping_response:type_name -> alox.EspNowEchoPingResponse 31, // 31: alox.UartMessage.espnow_echo_ping_response:type_name -> alox.EspNowEchoPingResponse
37, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest 38, // 32: alox.UartMessage.set_log_level_request:type_name -> alox.SetLogLevelRequest
38, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse 39, // 33: alox.UartMessage.set_log_level_response:type_name -> alox.SetLogLevelResponse
39, // 34: alox.UartMessage.tap_config_request:type_name -> alox.TapConfigRequest 40, // 34: alox.UartMessage.tap_config_request:type_name -> alox.TapConfigRequest
40, // 35: alox.UartMessage.tap_config_response:type_name -> alox.TapConfigResponse 41, // 35: alox.UartMessage.tap_config_response:type_name -> alox.TapConfigResponse
6, // 36: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo 27, // 36: alox.UartMessage.ble_stream_tick:type_name -> alox.BleStreamTick
8, // 37: alox.ClientInputResponse.clients:type_name -> alox.ClientInput 6, // 37: alox.ClientInfoResponse.clients:type_name -> alox.ClientInfo
15, // 38: alox.BatterySample.lipo1:type_name -> alox.LipoReading 8, // 38: alox.ClientInputResponse.clients:type_name -> alox.ClientInput
15, // 39: alox.BatterySample.lipo2:type_name -> alox.LipoReading 15, // 39: alox.BatterySample.lipo1:type_name -> alox.LipoReading
16, // 40: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample 15, // 40: alox.BatterySample.lipo2:type_name -> alox.LipoReading
1, // 41: alox.TapEvent.kind:type_name -> alox.TapKind 16, // 41: alox.BatteryStatusResponse.samples:type_name -> alox.BatterySample
1, // 42: alox.CacheClientTap.kind:type_name -> alox.TapKind 1, // 42: alox.TapEvent.kind:type_name -> alox.TapKind
23, // 43: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel 1, // 43: alox.CacheClientTap.kind:type_name -> alox.TapKind
24, // 44: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap 23, // 44: alox.CacheClientStatus.accel:type_name -> alox.CacheClientAccel
25, // 45: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus 24, // 45: alox.CacheClientStatus.tap:type_name -> alox.CacheClientTap
46, // 46: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry 25, // 46: alox.CacheStatusResponse.clients:type_name -> alox.CacheClientStatus
47, // [47:47] is the sub-list for method output_type 25, // 47: alox.BleStreamTick.clients:type_name -> alox.CacheClientStatus
47, // [47:47] is the sub-list for method input_type 47, // 48: alox.OtaSlaveProgressResponse.slaves:type_name -> alox.OtaSlaveProgressEntry
47, // [47:47] is the sub-list for extension type_name 49, // [49:49] is the sub-list for method output_type
47, // [47:47] is the sub-list for extension extendee 49, // [49:49] is the sub-list for method input_type
0, // [0:47] is the sub-list for field type_name 49, // [49:49] is the sub-list for extension type_name
49, // [49:49] is the sub-list for extension extendee
0, // [0:49] is the sub-list for field type_name
} }
func init() { file_uart_messages_proto_init() } func init() { file_uart_messages_proto_init() }
@@ -4230,6 +4330,7 @@ func file_uart_messages_proto_init() {
(*UartMessage_SetLogLevelResponse)(nil), (*UartMessage_SetLogLevelResponse)(nil),
(*UartMessage_TapConfigRequest)(nil), (*UartMessage_TapConfigRequest)(nil),
(*UartMessage_TapConfigResponse)(nil), (*UartMessage_TapConfigResponse)(nil),
(*UartMessage_BleStreamTick)(nil),
} }
type x struct{} type x struct{}
out := protoimpl.TypeBuilder{ out := protoimpl.TypeBuilder{
@@ -4237,7 +4338,7 @@ func file_uart_messages_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(), GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)), RawDescriptor: unsafe.Slice(unsafe.StringData(file_uart_messages_proto_rawDesc), len(file_uart_messages_proto_rawDesc)),
NumEnums: 2, NumEnums: 2,
NumMessages: 46, NumMessages: 47,
NumExtensions: 0, NumExtensions: 0,
NumServices: 0, NumServices: 0,
}, },
+3 -175
View File
@@ -1,12 +1,6 @@
package main package main
import ( import "errors"
"errors"
"fmt"
"log"
"sync"
"time"
)
// errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug). // errUARTBusy is returned when the port is held for OTA (poller should not treat as unplug).
var errUARTBusy = errors.New("uart busy (OTA in progress)") var errUARTBusy = errors.New("uart busy (OTA in progress)")
@@ -14,171 +8,5 @@ var errUARTBusy = errors.New("uart busy (OTA in progress)")
// errOTAInProgress is returned when a second OTA upload is attempted while one is running. // errOTAInProgress is returned when a second OTA upload is attempted while one is running.
var errOTAInProgress = errors.New("OTA upload already in progress") var errOTAInProgress = errors.New("OTA upload already in progress")
// managedSerial keeps the UART open and reconnects after I/O failures or unplug. // managedSerial is an alias for managedHost (UART-first history).
type managedSerial struct { type managedSerial = managedHost
portName string
baud int
quiet bool
mu sync.Mutex
sp *serialPort
otaActive bool // UART held for firmware upload; poll/API must not interleave
}
func newManagedSerial(portName string, baud int) *managedSerial {
return &managedSerial{
portName: portName,
baud: baud,
}
}
func (m *managedSerial) Close() error {
m.mu.Lock()
defer m.mu.Unlock()
return m.closeLocked()
}
func (m *managedSerial) IsConnected() bool {
m.mu.Lock()
defer m.mu.Unlock()
return m.sp != nil
}
func (m *managedSerial) openLocked() error {
sp, err := openSerial(m.portName, m.baud)
if err != nil {
return err
}
sp.quiet = m.quiet
m.sp = sp
if !m.quiet {
log.Printf("UART %s connected (%d baud)", m.portName, m.baud)
}
return nil
}
func (m *managedSerial) closeLocked() error {
if m.sp == nil {
return nil
}
err := m.sp.port.Close()
m.sp = nil
return err
}
func (m *managedSerial) invalidateLocked(reason error) {
if m.sp == nil {
return
}
if !m.quiet {
log.Printf("UART %s disconnected: %v", m.portName, reason)
}
_ = m.closeLocked()
}
func (m *managedSerial) withPort(fn func(*serialPort) error) error {
return m.withPortLocked(false, fn)
}
// withPortPoll is like withPort but returns errUARTBusy during OTA (no TryLock race).
func (m *managedSerial) withPortPoll(fn func(*serialPort) error) error {
return m.withPortLocked(true, fn)
}
func (m *managedSerial) withPortLocked(poll bool, fn func(*serialPort) error) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.otaActive {
return errUARTBusy
}
if m.sp == nil {
if err := m.openLocked(); err != nil {
return fmt.Errorf("%s: %w", m.portName, err)
}
}
err := fn(m.sp)
if err != nil {
m.invalidateLocked(err)
}
return err
}
func (m *managedSerial) recoverAfterMasterRestart() {
const bootWait = 6 * time.Second
m.mu.Lock()
m.closeLocked()
m.mu.Unlock()
log.Printf("UART: master restart — waiting %s for boot", bootWait)
time.Sleep(bootWait)
m.mu.Lock()
defer m.mu.Unlock()
if err := m.openLocked(); err != nil {
log.Printf("UART reconnect after master restart: %v", err)
return
}
flushSerialInput(m.sp)
log.Printf("UART %s ready after master restart", m.portName)
}
func (m *managedSerial) exchangePayload(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPort, payload, cmdName)
}
func (m *managedSerial) exchangePayloadPoll(payload []byte, cmdName string) ([]byte, error) {
return m.exchangePayloadVia(m.withPortPoll, payload, cmdName)
}
func (m *managedSerial) exchangePayloadVia(
portFn func(func(*serialPort) error) error,
payload []byte, cmdName string,
) ([]byte, error) {
var resp []byte
err := portFn(func(sp *serialPort) error {
var e error
resp, e = sp.exchangePayloadLocked(payload, cmdName, readTimeout)
return e
})
return resp, err
}
func (m *managedSerial) exchange(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangeVia(m.withPort, cmdID, cmdName)
}
func (m *managedSerial) exchangePoll(cmdID byte, cmdName string) ([]byte, error) {
return m.exchangeVia(m.withPortPoll, cmdID, cmdName)
}
func (m *managedSerial) exchangeVia(
portFn func(func(*serialPort) error) error,
cmdID byte, cmdName string,
) ([]byte, error) {
var resp []byte
err := portFn(func(sp *serialPort) error {
var e error
resp, e = sp.exchangeLocked(cmdID, cmdName)
return e
})
return resp, err
}
func disconnectedState(portName string, err error) DashboardState {
msg := "UART disconnected"
if err != nil {
msg = err.Error()
}
return DashboardState{
UpdatedAt: time.Now().Format(time.RFC3339),
SerialPort: portName,
UARTConnected: false,
SerialOK: false,
SerialError: msg,
Master: MasterView{OK: false, Error: msg},
Clients: []ClientView{},
}
}
+433 -57
View File
@@ -179,8 +179,8 @@
<span class="navbar-brand mb-0 h1">Powerpod</span> <span class="navbar-brand mb-0 h1">Powerpod</span>
<span class="d-flex align-items-center gap-2"> <span class="d-flex align-items-center gap-2">
<span class="badge rounded-pill me-1" <span class="badge rounded-pill me-1"
:class="state.uart_connected ? 'badge-online' : 'badge-offline'" :class="hostUp() ? 'badge-online' : 'badge-offline'"
x-text="state.uart_connected ? 'UART' : 'UART off'"></span> x-text="hostBadge()"></span>
<span class="badge rounded-pill" :class="wsConnected ? 'badge-online' : 'badge-offline'" <span class="badge rounded-pill" :class="wsConnected ? 'badge-online' : 'badge-offline'"
x-text="wsConnected ? 'WS' : 'WS off'"></span> x-text="wsConnected ? 'WS' : 'WS off'"></span>
<small class="text-muted ms-2" x-text="state.updated_at || '—'"></small> <small class="text-muted ms-2" x-text="state.updated_at || '—'"></small>
@@ -195,19 +195,177 @@
x-text="configMsg"></div> x-text="configMsg"></div>
</template> </template>
<section class="card mb-4">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
<span>Host-Verbindung</span>
<span class="text-muted small" x-text="hostEndpointLabel()"></span>
</div>
<div class="card-body">
<ul class="nav nav-pills mb-3 gap-1">
<li class="nav-item">
<button type="button" class="nav-link"
:class="connTab === 'uart' ? 'active' : ''"
@click="connTab = 'uart'">UART</button>
</li>
<li class="nav-item">
<button type="button" class="nav-link"
:class="connTab === 'ble' ? 'active' : ''"
@click="connTab = 'ble'">BLE</button>
</li>
</ul>
<div x-show="connTab === 'uart'" class="d-flex flex-wrap gap-2 align-items-end">
<div>
<label class="form-label text-muted small mb-1" for="uart-port">Port</label>
<select id="uart-port" class="form-control form-control-sm" style="min-width:12rem"
x-model="uartPort" :disabled="hostBusy">
<option value="">— Port wählen —</option>
<template x-for="p in serialPorts" :key="p">
<option :value="p" x-text="p"></option>
</template>
</select>
</div>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="refreshSerialPorts()" :disabled="hostBusy">Aktualisieren</button>
<button type="button" class="btn btn-primary btn-sm"
@click="connectUART()" :disabled="hostBusy || !uartPort">Connect</button>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="disconnectHost()" :disabled="hostBusy || !hostUp()">Disconnect</button>
</div>
<div x-show="connTab === 'ble'">
<div class="d-flex flex-wrap gap-2 align-items-center mb-2">
<button type="button" class="btn btn-primary btn-sm"
@click="scanBLE()" :disabled="hostBusy || bleScanning"
x-text="bleScanning ? 'Scan…' : 'Scan'"></button>
<button type="button" class="btn btn-outline-secondary btn-sm"
@click="disconnectHost()" :disabled="hostBusy || !hostUp()">Disconnect</button>
<span class="text-muted small" x-show="bleScanErr" x-text="bleScanErr"></span>
</div>
<div class="table-responsive" x-show="bleDevices.length">
<table class="table table-sm pp-table mb-0">
<thead>
<tr><th>Name</th><th>Adresse</th><th>RSSI</th><th></th></tr>
</thead>
<tbody>
<template x-for="d in bleDevices" :key="d.address">
<tr>
<td x-text="d.name || '—'"></td>
<td class="mac" x-text="d.address"></td>
<td x-text="d.rssi"></td>
<td class="text-end">
<button type="button" class="btn btn-sm btn-primary"
@click="connectBLE(d)" :disabled="hostBusy">Connect</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
<p class="text-muted small mb-0" x-show="!bleDevices.length && !bleScanning">
Scan startet eine kurze Suche (trennt aktive BLE-Verbindung).
</p>
</div>
</div>
</section>
<section class="card mb-4" x-show="isBLEHost()">
<div class="card-header">BLE-Tests</div>
<div class="card-body">
<p class="text-muted small mb-3">
Host↔Master-Latenz und Stream-Tick-Fenster (wie CLI <code>ble-rtt</code> / <code>ble-stream</code>).
Dashboard-Polling pausiert während des Laufs.
</p>
<div class="row g-3">
<div class="col-md-6">
<h6 class="text-secondary mb-2">RTT (VERSION)</h6>
<div class="d-flex flex-wrap gap-2 align-items-end mb-2">
<div>
<label class="form-label text-muted small mb-1" for="ble-rtt-count">Samples</label>
<input id="ble-rtt-count" type="number" class="form-control form-control-sm config-input"
min="1" max="500" x-model.number="bleRtt.count" :disabled="bleTestBusy">
</div>
<div>
<label class="form-label text-muted small mb-1" for="ble-rtt-warmup">Warmup</label>
<input id="ble-rtt-warmup" type="number" class="form-control form-control-sm config-input"
min="0" max="100" x-model.number="bleRtt.warmup" :disabled="bleTestBusy">
</div>
<button type="button" class="btn btn-primary btn-sm"
@click="runBleRtt()" :disabled="bleTestBusy || busy"
x-text="bleTestBusy === 'rtt' ? 'Läuft…' : 'Start RTT'"></button>
</div>
<template x-if="bleRtt.result">
<dl class="row small mb-0">
<dt class="col-5 text-muted">n / warmup</dt>
<dd class="col-7" x-text="bleRtt.result.stats?.n + ' / ' + bleRtt.result.warmup"></dd>
<dt class="col-5 text-muted">min / median</dt>
<dd class="col-7" x-text="fmtMs(bleRtt.result.stats?.min_ms) + ' / ' + fmtMs(bleRtt.result.stats?.median_ms)"></dd>
<dt class="col-5 text-muted">mean / max</dt>
<dd class="col-7" x-text="fmtMs(bleRtt.result.stats?.mean_ms) + ' / ' + fmtMs(bleRtt.result.stats?.max_ms)"></dd>
<dt class="col-5 text-muted" x-show="bleRtt.result.error">Fehler</dt>
<dd class="col-7 text-danger" x-show="bleRtt.result.error" x-text="bleRtt.result.error"></dd>
</dl>
</template>
</div>
<div class="col-md-6">
<h6 class="text-secondary mb-2">Stream-Ticks</h6>
<div class="d-flex flex-wrap gap-2 align-items-end mb-2">
<div>
<label class="form-label text-muted small mb-1" for="ble-stream-count">Ticks</label>
<input id="ble-stream-count" type="number" class="form-control form-control-sm config-input"
min="2" max="500" x-model.number="bleStream.count" :disabled="bleTestBusy">
</div>
<div>
<label class="form-label text-muted small mb-1" for="ble-stream-period">Period ms</label>
<input id="ble-stream-period" type="number" class="form-control form-control-sm config-input"
min="8" max="16" x-model.number="bleStream.period_ms" :disabled="bleTestBusy">
</div>
<div>
<label class="form-label text-muted small mb-1" for="ble-stream-tol">Tol ms</label>
<input id="ble-stream-tol" type="number" class="form-control form-control-sm config-input"
min="0" max="20" step="0.5" x-model.number="bleStream.tol_ms" :disabled="bleTestBusy">
</div>
<button type="button" class="btn btn-primary btn-sm"
@click="runBleStream()" :disabled="bleTestBusy || busy"
x-text="bleTestBusy === 'stream' ? 'Läuft…' : 'Start Stream'"></button>
</div>
<template x-if="bleStream.result">
<dl class="row small mb-0">
<dt class="col-5 text-muted">received</dt>
<dd class="col-7" x-text="bleStream.result.received + ' / ' + bleStream.result.requested"></dd>
<dt class="col-5 text-muted">window_ok</dt>
<dd class="col-7"
:class="bleStream.result.window_ok ? 'text-success' : 'text-warning'"
x-text="bleStream.result.window_ok ? 'yes' : 'no'"></dd>
<dt class="col-5 text-muted">dT ESP</dt>
<dd class="col-7" x-text="fmtStats(bleStream.result.dt_esp_ms)"></dd>
<dt class="col-5 text-muted">dR PC</dt>
<dd class="col-7" x-text="fmtStats(bleStream.result.dr_pc_ms)"></dd>
<dt class="col-5 text-muted">outside tol</dt>
<dd class="col-7" x-text="bleStream.result.outside_tol + ' / ' + (bleStream.result.dt_esp_ms?.n || 0)"></dd>
<dt class="col-5 text-muted" x-show="bleStream.result.error">Fehler</dt>
<dd class="col-7 text-danger" x-show="bleStream.result.error" x-text="bleStream.result.error"></dd>
</dl>
</template>
</div>
</div>
</div>
</section>
<div class="row g-4"> <div class="row g-4">
<section class="col-lg-4"> <section class="col-lg-4">
<div class="card h-100"> <div class="card h-100">
<div class="card-header">Master (BMA456 lokal)</div> <div class="card-header">Master (BMA456 lokal)</div>
<div class="card-body"> <div class="card-body">
<p class="text-muted small mb-2" x-text="'UART ' + (state.serial_port || '—')"></p> <p class="text-muted small mb-2"
x-text="(state.host_transport || 'none').toUpperCase() + ' ' + (state.host_endpoint || state.serial_port || '—')"></p>
<p class="text-muted small mb-3">Deadzone nur am Master — Slaves bleiben unverändert.</p> <p class="text-muted small mb-3">Deadzone nur am Master — Slaves bleiben unverändert.</p>
<template x-if="!state.uart_connected"> <template x-if="!hostUp()">
<div class="alert alert-warning py-2 mb-2" <div class="alert alert-warning py-2 mb-2"
x-text="state.serial_error || 'UART disconnected — reconnecting…'"></div> x-text="state.serial_error || 'Host nicht verbunden — oben UART oder BLE wählen.'"></div>
</template> </template>
<template x-if="state.uart_connected && !state.serial_ok"> <template x-if="hostUp() && !state.serial_ok">
<div class="alert alert-danger py-2" x-text="state.serial_error || 'Serial error'"></div> <div class="alert alert-danger py-2" x-text="state.serial_error || 'Host error'"></div>
</template> </template>
<template x-if="state.master?.ok"> <template x-if="state.master?.ok">
<dl class="row mb-0"> <dl class="row mb-0">
@@ -242,26 +400,26 @@
min="0" max="4095" step="1" min="0" max="4095" step="1"
placeholder="z. B. 100" placeholder="z. B. 100"
x-model.number="masterDz" x-model.number="masterDz"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="readMasterDeadzone()" @click="readMasterDeadzone()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Lesen Lesen
</button> </button>
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="setMasterDeadzone()" @click="setMasterDeadzone()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Setzen Setzen
</button> </button>
<button type="button" class="btn btn-outline-warning btn-sm" <button type="button" class="btn btn-outline-warning btn-sm"
@click="findMe()" @click="findMe()"
:disabled="busy || !state.uart_connected" :disabled="busy || !hostUp()"
title="LED-Ring: Rot/Grün/Blau je 3×"> title="LED-Ring: Rot/Grün/Blau je 3×">
Find me Find me
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="restart(0)" @click="restart(0)"
:disabled="busy || !state.uart_connected" :disabled="busy || !hostUp()"
title="Master neu starten"> title="Master neu starten">
Restart Restart
</button> </button>
@@ -272,7 +430,7 @@
<div class="d-flex flex-wrap gap-2 align-items-end"> <div class="d-flex flex-wrap gap-2 align-items-end">
<select id="master-log-level" class="form-select form-select-sm config-input" <select id="master-log-level" class="form-select form-select-sm config-input"
x-model.number="masterLogLevel" x-model.number="masterLogLevel"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
<option value="0">None (aus)</option> <option value="0">None (aus)</option>
<option value="1">Error</option> <option value="1">Error</option>
<option value="2">Warn</option> <option value="2">Warn</option>
@@ -282,12 +440,12 @@
</select> </select>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="readMasterLogLevel()" @click="readMasterLogLevel()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Lesen Lesen
</button> </button>
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="setMasterLogLevel()" @click="setMasterLogLevel()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Setzen Setzen
</button> </button>
</div> </div>
@@ -297,65 +455,65 @@
<label class="form-label text-muted mb-0" for="tap-sens">sens (0–15)</label> <label class="form-label text-muted mb-0" for="tap-sens">sens (0–15)</label>
<input id="tap-sens" type="number" class="form-control form-control-sm" <input id="tap-sens" type="number" class="form-control form-control-sm"
min="0" max="15" x-model.number="masterTapCfg.tap_sens_thres" min="0" max="15" x-model.number="masterTapCfg.tap_sens_thres"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-max-gest">max_gest (×5ms)</label> <label class="form-label text-muted mb-0" for="tap-max-gest">max_gest (×5ms)</label>
<input id="tap-max-gest" type="number" class="form-control form-control-sm" <input id="tap-max-gest" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.max_gest_dur" min="0" max="65535" x-model.number="masterTapCfg.max_gest_dur"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-shock">shock (×5ms)</label> <label class="form-label text-muted mb-0" for="tap-shock">shock (×5ms)</label>
<input id="tap-shock" type="number" class="form-control form-control-sm" <input id="tap-shock" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.tap_shock_dur" min="0" max="65535" x-model.number="masterTapCfg.tap_shock_dur"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-quiet">quiet (×5ms)</label> <label class="form-label text-muted mb-0" for="tap-quiet">quiet (×5ms)</label>
<input id="tap-quiet" type="number" class="form-control form-control-sm" <input id="tap-quiet" type="number" class="form-control form-control-sm"
min="0" max="65535" x-model.number="masterTapCfg.quite_time_after_gest" min="0" max="65535" x-model.number="masterTapCfg.quite_time_after_gest"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-wait">wait (0|1)</label> <label class="form-label text-muted mb-0" for="tap-wait">wait (0|1)</label>
<input id="tap-wait" type="number" class="form-control form-control-sm" <input id="tap-wait" type="number" class="form-control form-control-sm"
min="0" max="1" x-model.number="masterTapCfg.wait_for_timeout" min="0" max="1" x-model.number="masterTapCfg.wait_for_timeout"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
<div class="col-6 col-md-4"> <div class="col-6 col-md-4">
<label class="form-label text-muted mb-0" for="tap-axis">axis (0=X…2=Z)</label> <label class="form-label text-muted mb-0" for="tap-axis">axis (0=X…2=Z)</label>
<input id="tap-axis" type="number" class="form-control form-control-sm" <input id="tap-axis" type="number" class="form-control form-control-sm"
min="0" max="2" x-model.number="masterTapCfg.axis_sel" min="0" max="2" x-model.number="masterTapCfg.axis_sel"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
</div> </div>
</div> </div>
<div class="d-flex flex-wrap gap-3 align-items-center mt-2 small"> <div class="d-flex flex-wrap gap-3 align-items-center mt-2 small">
<label class="tap-toggle mb-0"> <label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_single" <input type="checkbox" x-model="masterTapCfg.enable_single"
:disabled="busy || !state.uart_connected"> Single :disabled="busy || !hostUp()"> Single
</label> </label>
<label class="tap-toggle mb-0"> <label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_double" <input type="checkbox" x-model="masterTapCfg.enable_double"
:disabled="busy || !state.uart_connected"> Double :disabled="busy || !hostUp()"> Double
</label> </label>
<label class="tap-toggle mb-0"> <label class="tap-toggle mb-0">
<input type="checkbox" x-model="masterTapCfg.enable_triple" <input type="checkbox" x-model="masterTapCfg.enable_triple"
:disabled="busy || !state.uart_connected"> Triple :disabled="busy || !hostUp()"> Triple
</label> </label>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="readMasterTapConfig()" @click="readMasterTapConfig()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Lesen Lesen
</button> </button>
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="setMasterTapConfig()" @click="setMasterTapConfig()"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Setzen Setzen
</button> </button>
</div> </div>
<p class="text-muted small mt-2 mb-0" x-show="!state.uart_connected"> <p class="text-muted small mt-2 mb-0" x-show="!hostUp()">
UART nicht verbunden — Eingabe gesperrt. Host nicht verbunden — Eingabe gesperrt.
</p> </p>
</div> </div>
</div> </div>
@@ -373,12 +531,12 @@
:disabled="busy"> :disabled="busy">
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="setDeadzoneAll(allDz)" @click="setDeadzoneAll(allDz)"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Alle Slaves Alle Slaves
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="setTapConfigAll()" @click="setTapConfigAll()"
:disabled="busy || !state.uart_connected" :disabled="busy || !hostUp()"
title="Master-Tap-Config an alle Slaves (ESP-NOW)"> title="Master-Tap-Config an alle Slaves (ESP-NOW)">
Tap-Cfg alle Tap-Cfg alle
</button> </button>
@@ -386,24 +544,27 @@
</div> </div>
</div> </div>
<p class="text-muted small px-3 pt-2 mb-0"> <p class="text-muted small px-3 pt-2 mb-0">
<strong>Live-Stream</strong> startet die schnelle <code>CACHE_STATUS</code>-Abfrage (~16&nbsp;ms). <strong>Live-Stream</strong>: UART ≈ <code>CACHE_STATUS</code>-Poll (~16&nbsp;ms); BLE ≈
Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Accel-Stream; Tap-Notify (S/D/T) steuert <code>BLE_STREAM_TICK</code>-Push. Pro Slave <strong>Accel</strong> aktiviert den ESP-NOW-Stream;
die Tap-Arten auf dem Slave. Tap-Notify (S/D/T) steuert die Tap-Arten auf dem Slave.
</p> </p>
<div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center"> <div class="px-3 pb-2 d-flex flex-wrap gap-2 align-items-center">
<button type="button" <button type="button"
class="btn btn-sm" class="btn btn-sm"
:class="state.live_stream ? 'btn-warning' : 'btn-success'" :class="state.live_stream ? 'btn-warning' : 'btn-success'"
@click="setLiveStream(!state.live_stream)" @click="setLiveStream(!state.live_stream)"
:disabled="busy || !state.uart_connected || !(state.clients || []).length" :disabled="busy || !hostUp() || !(state.clients || []).length"
x-text="state.live_stream ? 'Live-Stream aus' : 'Live-Stream an'"></button> x-text="state.live_stream ? 'Live-Stream aus' : 'Live-Stream an'"></button>
<span class="text-muted small font-monospace" x-show="isBLEHost() && state.live_stream"
x-text="bleTickLabel()"
title="Median Δt zwischen BLE_STREAM_TICK (esp_timestamp_us)"></span>
<span class="text-muted small">Tap alle Slaves:</span> <span class="text-muted small">Tap alle Slaves:</span>
<label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label> <label class="tap-toggle"><input type="checkbox" x-model="allTapSingle" :disabled="busy"> S</label>
<label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label> <label class="tap-toggle"><input type="checkbox" x-model="allTapDouble" :disabled="busy"> D</label>
<label class="tap-toggle"><input type="checkbox" x-model="allTapTriple" :disabled="busy"> T</label> <label class="tap-toggle"><input type="checkbox" x-model="allTapTriple" :disabled="busy"> T</label>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="setTapNotifyAll(allTapSingle, allTapDouble, allTapTriple)" @click="setTapNotifyAll(allTapSingle, allTapDouble, allTapTriple)"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Tap setzen Tap setzen
</button> </button>
</div> </div>
@@ -445,7 +606,7 @@
<span x-text="formatTapCfg(c.tap_config)" :title="tapCfgTitle(c.tap_config)"></span> <span x-text="formatTapCfg(c.tap_config)" :title="tapCfgTitle(c.tap_config)"></span>
<button type="button" class="btn btn-outline-primary btn-sm ms-1 py-0 px-1" <button type="button" class="btn btn-outline-primary btn-sm ms-1 py-0 px-1"
@click="setTapConfig(c.id, masterTapCfg)" @click="setTapConfig(c.id, masterTapCfg)"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="Master-Tap-Config an diesen Slave"> title="Master-Tap-Config an diesen Slave">
Set Set
</button> </button>
@@ -462,7 +623,7 @@
class="btn btn-sm" class="btn btn-sm"
:class="c.accel_stream ? 'btn-warning' : 'btn-outline-success'" :class="c.accel_stream ? 'btn-warning' : 'btn-outline-success'"
@click="setAccelStream(c.id, !c.accel_stream)" @click="setAccelStream(c.id, !c.accel_stream)"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
x-text="c.accel_stream ? 'Aus' : 'An'" x-text="c.accel_stream ? 'Aus' : 'An'"
title="ESP-NOW Accel-Stream auf Slave"></button> title="ESP-NOW Accel-Stream auf Slave"></button>
</td> </td>
@@ -472,19 +633,19 @@
<input type="checkbox" <input type="checkbox"
:checked="c.tap_notify_single" :checked="c.tap_notify_single"
@change="setTapNotify(c.id, $event.target.checked, c.tap_notify_double, c.tap_notify_triple)" @change="setTapNotify(c.id, $event.target.checked, c.tap_notify_double, c.tap_notify_triple)"
:disabled="busy || !state.uart_connected || !c.available"> S :disabled="busy || !hostUp() || !c.available"> S
</label> </label>
<label class="tap-toggle" title="Double tap"> <label class="tap-toggle" title="Double tap">
<input type="checkbox" <input type="checkbox"
:checked="c.tap_notify_double" :checked="c.tap_notify_double"
@change="setTapNotify(c.id, c.tap_notify_single, $event.target.checked, c.tap_notify_triple)" @change="setTapNotify(c.id, c.tap_notify_single, $event.target.checked, c.tap_notify_triple)"
:disabled="busy || !state.uart_connected || !c.available"> D :disabled="busy || !hostUp() || !c.available"> D
</label> </label>
<label class="tap-toggle" title="Triple tap"> <label class="tap-toggle" title="Triple tap">
<input type="checkbox" <input type="checkbox"
:checked="c.tap_notify_triple" :checked="c.tap_notify_triple"
@change="setTapNotify(c.id, c.tap_notify_single, c.tap_notify_double, $event.target.checked)" @change="setTapNotify(c.id, c.tap_notify_single, c.tap_notify_double, $event.target.checked)"
:disabled="busy || !state.uart_connected || !c.available"> T :disabled="busy || !hostUp() || !c.available"> T
</label> </label>
</div> </div>
</td> </td>
@@ -500,36 +661,36 @@
:disabled="busy"> :disabled="busy">
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="setDeadzone(c.id, slaveDz[c.id] ?? c.deadzone ?? 100)" @click="setDeadzone(c.id, slaveDz[c.id] ?? c.deadzone ?? 100)"
:disabled="busy || !state.uart_connected || !c.available"> :disabled="busy || !hostUp() || !c.available">
Setzen Setzen
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="unicastTest(c.id)" @click="unicastTest(c.id)"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="ESP-NOW Unicast-Test"> title="ESP-NOW Unicast-Test">
Test Test
</button> </button>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-info btn-sm"
@click="echoPing(c.id)" @click="echoPing(c.id)"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="ESP-NOW Timestamp-Echo (Round-Trip-Latenz)"> title="ESP-NOW Timestamp-Echo (Round-Trip-Latenz)">
Ping Ping
</button> </button>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-info btn-sm"
@click="ledRing({ clientId: c.id })" @click="ledRing({ clientId: c.id })"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="LED-Ring (aktueller Modus)"> title="LED-Ring (aktueller Modus)">
LED LED
</button> </button>
<button type="button" class="btn btn-outline-warning btn-sm" <button type="button" class="btn btn-outline-warning btn-sm"
@click="ledRing({ clientId: c.id, mode: 'find-me' })" @click="ledRing({ clientId: c.id, mode: 'find-me' })"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="Find me"> title="Find me">
Find Find
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="restart(c.id)" @click="restart(c.id)"
:disabled="busy || !state.uart_connected || !c.available" :disabled="busy || !hostUp() || !c.available"
title="Neustart per ESP-NOW"> title="Neustart per ESP-NOW">
Restart Restart
</button> </button>
@@ -602,17 +763,17 @@
<div class="col-md-4 d-flex flex-wrap gap-2"> <div class="col-md-4 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="ledRing({ clientId: 0 })" @click="ledRing({ clientId: 0 })"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Master Master
</button> </button>
<button type="button" class="btn btn-outline-primary btn-sm" <button type="button" class="btn btn-outline-primary btn-sm"
@click="ledRing({ allClients: true, slavesOnly: true })" @click="ledRing({ allClients: true, slavesOnly: true })"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Alle Slaves Alle Slaves
</button> </button>
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="ledRing({ allClients: true })" @click="ledRing({ allClients: true })"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Alle + Master Alle + Master
</button> </button>
</div> </div>
@@ -621,23 +782,23 @@
</div> </div>
</section> </section>
<section class="col-12"> <section class="col-12" x-show="isUARTHost() || !hostUp()">
<div class="card"> <div class="card">
<div class="card-header">Firmware OTA (A/B)</div> <div class="card-header">Firmware OTA (A/B)</div>
<div class="card-body"> <div class="card-body">
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
Lädt eine <code>.bin</code> auf den Master (UART), danach verteilt die Firmware automatisch per ESP-NOW an alle verfügbaren Slaves. Lädt eine <code>.bin</code> auf den Master (nur UART-Host, DIP8=0), danach verteilt die Firmware automatisch per ESP-NOW an alle verfügbaren Slaves.
Während des Uploads pausiert das Live-Polling. Während des Uploads pausiert das Live-Polling. Bei BLE-Host ist OTA nicht verfügbar.
</p> </p>
<div class="mb-3"> <div class="mb-3">
<input type="file" class="form-control form-control-sm" accept=".bin,application/octet-stream" <input type="file" class="form-control form-control-sm" accept=".bin,application/octet-stream"
@change="otaFile = $event.target.files[0]" @change="otaFile = $event.target.files[0]"
:disabled="ota.active || busy || !state.uart_connected"> :disabled="ota.active || busy || !isUARTHost()">
</div> </div>
<div class="d-flex flex-wrap gap-2 align-items-center mb-2"> <div class="d-flex flex-wrap gap-2 align-items-center mb-2">
<button type="button" class="btn btn-primary btn-sm" <button type="button" class="btn btn-primary btn-sm"
@click="uploadOTA()" @click="uploadOTA()"
:disabled="ota.active || busy || !state.uart_connected || !otaFile"> :disabled="ota.active || busy || !isUARTHost() || !otaFile">
Firmware hochladen Firmware hochladen
</button> </button>
<span class="text-muted small" x-show="otaFile" <span class="text-muted small" x-show="otaFile"
@@ -666,7 +827,7 @@
<td class="ota-restart-col text-end"> <td class="ota-restart-col text-end">
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="restart(0)" @click="restart(0)"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Restart Restart
</button> </button>
</td> </td>
@@ -703,7 +864,7 @@
<td class="ota-restart-col text-end"> <td class="ota-restart-col text-end">
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
@click="restart(row.id)" @click="restart(row.id)"
:disabled="busy || !state.uart_connected"> :disabled="busy || !hostUp()">
Restart Restart
</button> </button>
</td> </td>
@@ -733,6 +894,16 @@
state: { master: {}, clients: [] }, state: { master: {}, clients: [] },
ws: null, ws: null,
wsConnected: false, wsConnected: false,
connTab: 'uart',
uartPort: '',
serialPorts: [],
bleDevices: [],
bleScanning: false,
bleScanErr: '',
hostBusy: false,
bleTestBusy: false,
bleRtt: { count: 50, warmup: 5, result: null },
bleStream: { count: 50, period_ms: 16, tol_ms: 2, result: null },
masterDz: 100, masterDz: 100,
masterDzSeeded: false, masterDzSeeded: false,
masterLogLevel: 0, masterLogLevel: 0,
@@ -779,18 +950,212 @@
blinkMs: 350, blinkMs: 350,
blinkCount: 1 blinkCount: 1
}, },
hostUp() {
return !!(this.state?.host_connected || this.state?.uart_connected);
},
isUARTHost() {
return this.hostUp() && (this.state?.host_transport || 'uart') === 'uart';
},
isBLEHost() {
return this.hostUp() && this.state?.host_transport === 'ble';
},
bleTickLabel() {
const t = this.state?.ble_tick;
if (!t || !t.samples) return 'BLE tick: …';
const med = Number(t.median_ms).toFixed(2);
const period = t.period_ms != null ? t.period_ms : '—';
return `BLE tick median ${med} ms (n=${t.samples}, period ${period} ms)`;
},
fmtMs(v) {
if (v == null || Number.isNaN(v)) return '—';
return Number(v).toFixed(3) + ' ms';
},
fmtStats(s) {
if (!s?.n) return '—';
return `min ${this.fmtMs(s.min_ms)} · med ${this.fmtMs(s.median_ms)} · mean ${this.fmtMs(s.mean_ms)} · max ${this.fmtMs(s.max_ms)}`;
},
async runBleRtt() {
this.bleTestBusy = 'rtt';
this.bleRtt.result = null;
try {
const r = await fetch('/api/ble-rtt', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
count: this.bleRtt.count,
warmup: this.bleRtt.warmup,
}),
});
const data = await r.json();
this.bleRtt.result = data;
if (!data.success) {
this.flash(data.error || 'BLE RTT fehlgeschlagen', false);
} else {
this.flash(
`BLE RTT: mean ${Number(data.stats?.mean_ms).toFixed(3)} ms (n=${data.stats?.n})`,
true
);
}
} catch (e) {
this.flash(String(e), false);
} finally {
this.bleTestBusy = false;
}
},
async runBleStream() {
this.bleTestBusy = 'stream';
this.bleStream.result = null;
try {
const r = await fetch('/api/ble-stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
count: this.bleStream.count,
period_ms: this.bleStream.period_ms,
tol_ms: this.bleStream.tol_ms,
timeout_s: 20,
}),
});
const data = await r.json();
this.bleStream.result = data;
if (!data.success) {
this.flash(data.error || 'BLE Stream-Test fehlgeschlagen', false);
} else {
this.flash(
`BLE Stream: ${data.received}/${data.requested} ticks, window_ok=${data.window_ok ? 'yes' : 'no'}`,
true
);
}
} catch (e) {
this.flash(String(e), false);
} finally {
this.bleTestBusy = false;
}
},
hostBadge() {
const t = this.state?.host_transport || 'none';
if (!this.hostUp()) return 'off';
if (t === 'ble') return 'BLE';
if (t === 'uart') return 'UART';
return 'on';
},
hostEndpointLabel() {
const ep = this.state?.host_endpoint || this.state?.serial_port || '';
const t = this.state?.host_transport || 'none';
return ep ? `${t}: ${ep}` : t;
},
async refreshSerialPorts() {
try {
const r = await fetch('/api/serial/ports');
const data = await r.json();
this.serialPorts = data.ports || [];
if (!this.uartPort && this.serialPorts.length) {
this.uartPort = this.serialPorts[0];
}
} catch (_) {
this.serialPorts = [];
}
},
async connectUART() {
if (!this.uartPort) return;
this.hostBusy = true;
this.bleScanErr = '';
try {
const r = await fetch('/api/host/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ transport: 'uart', port: this.uartPort }),
});
const data = await r.json();
if (!r.ok || data.error) {
this.flash(data.error || 'UART connect failed', false);
} else {
this.flash('UART verbunden: ' + (data.endpoint || this.uartPort), true);
}
} catch (e) {
this.flash(String(e), false);
} finally {
this.hostBusy = false;
}
},
async scanBLE() {
this.bleScanning = true;
this.bleScanErr = '';
this.bleDevices = [];
this.hostBusy = true;
try {
const r = await fetch('/api/ble/scan', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ timeout_s: 5 }),
});
const data = await r.json();
if (data.error) this.bleScanErr = data.error;
this.bleDevices = (data.devices || []).slice().sort((a, b) => (b.rssi || 0) - (a.rssi || 0));
} catch (e) {
this.bleScanErr = String(e);
} finally {
this.bleScanning = false;
this.hostBusy = false;
}
},
async connectBLE(d) {
if (!d?.address) return;
this.hostBusy = true;
this.bleScanErr = '';
try {
const r = await fetch('/api/host/connect', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
transport: 'ble',
address: d.address,
name: d.name || '',
}),
});
const data = await r.json();
if (!r.ok || data.error) {
this.flash(data.error || 'BLE connect failed', false);
} else {
this.flash('BLE verbunden: ' + (data.endpoint || d.address), true);
this.connTab = 'ble';
}
} catch (e) {
this.flash(String(e), false);
} finally {
this.hostBusy = false;
}
},
async disconnectHost() {
this.hostBusy = true;
try {
await fetch('/api/host/disconnect', { method: 'POST' });
this.flash('Host getrennt', true);
} catch (e) {
this.flash(String(e), false);
} finally {
this.hostBusy = false;
}
},
connect() { connect() {
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
const url = proto + '//' + location.host + '/ws'; const url = proto + '//' + location.host + '/ws';
this.refreshSerialPorts();
if (this._batteryTimer) clearInterval(this._batteryTimer); if (this._batteryTimer) clearInterval(this._batteryTimer);
this._batteryTimer = setInterval(() => this.refreshBattery(), 5000); this._batteryTimer = setInterval(() => this.refreshBattery(), 5000);
if (this._tapTimer) clearInterval(this._tapTimer); if (this._tapTimer) clearInterval(this._tapTimer);
this._tapTimer = setInterval(() => { this._tapClock++; }, 250); this._tapTimer = setInterval(() => { this._tapClock++; }, 250);
const connect = () => { const connect = () => {
if (this.ws && (this.ws.readyState === WebSocket.OPEN ||
this.ws.readyState === WebSocket.CONNECTING)) {
try { this.ws.close(); } catch (_) {}
}
this.ws = new WebSocket(url); this.ws = new WebSocket(url);
this.ws.onopen = () => { this.ws.onopen = () => {
this.wsConnected = true; this.wsConnected = true;
this._lastWsAt = Date.now();
this.refreshBattery(); this.refreshBattery();
this.refreshSerialPorts();
}; };
this.ws.onclose = () => { this.ws.onclose = () => {
this.wsConnected = false; this.wsConnected = false;
@@ -798,6 +1163,7 @@
}; };
this.ws.onmessage = (e) => { this.ws.onmessage = (e) => {
try { try {
this._lastWsAt = Date.now();
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === 'ota_progress') { if (msg.type === 'ota_progress') {
this.applyOTAProgress(msg); this.applyOTAProgress(msg);
@@ -809,6 +1175,8 @@
} }
const prev = this.state; const prev = this.state;
this.state = msg; this.state = msg;
if (msg.host_transport === 'ble') this.connTab = 'ble';
else if (msg.host_transport === 'uart') this.connTab = 'uart';
this.preserveBatteryInState(prev, this.state); this.preserveBatteryInState(prev, this.state);
this.syncTapDisplay(msg.clients || []); this.syncTapDisplay(msg.clients || []);
if (!this.masterDzSeeded && msg.master?.deadzone != null) { if (!this.masterDzSeeded && msg.master?.deadzone != null) {
@@ -827,6 +1195,14 @@
} catch (_) {} } catch (_) {}
}; };
}; };
if (this._wsWatch) clearInterval(this._wsWatch);
// If server dropped us from the hub without closing, force reconnect.
this._wsWatch = setInterval(() => {
if (!this.wsConnected || !this.state?.live_stream) return;
if (Date.now() - (this._lastWsAt || 0) > 4000) {
try { this.ws?.close(); } catch (_) {}
}
}, 2000);
connect(); connect();
}, },
preserveBatteryInState(prev, next) { preserveBatteryInState(prev, next) {
@@ -898,7 +1274,7 @@
}; };
}, },
async refreshBattery() { async refreshBattery() {
if (!this.state?.uart_connected) return; if (!this.hostUp()) return;
try { try {
const r = await fetch('/api/battery?all_clients=1'); const r = await fetch('/api/battery?all_clients=1');
if (!r.ok) return; if (!r.ok) return;
+4
View File
@@ -12,6 +12,9 @@ idf_component_register(
"powerpod.c" "powerpod.c"
"led_ring.c" "led_ring.c"
"uart.c" "uart.c"
"host_transport.c"
"ble_host.c"
"ble_stream.c"
"uart_proto.c" "uart_proto.c"
"uart_cmd.c" "uart_cmd.c"
"cmd/cmd_handler.c" "cmd/cmd_handler.c"
@@ -59,6 +62,7 @@ idf_component_register(
esp_netif esp_netif
esp_event esp_event
nvs_flash nvs_flash
bt
PRIV_REQUIRES PRIV_REQUIRES
esp_driver_gpio esp_driver_gpio
esp_driver_uart esp_driver_uart
+2
View File
@@ -9,6 +9,8 @@
typedef struct { typedef struct {
bool master; bool master;
uint8_t network; uint8_t network;
/** false = UART host, true = BLE host (DIP8; mutual exclusive, boot-only). */
bool host_ble;
char running_partition[APP_RUNNING_PARTITION_LABEL_MAX]; char running_partition[APP_RUNNING_PARTITION_LABEL_MAX];
} app_config_t; } app_config_t;
+415
View File
@@ -0,0 +1,415 @@
#include "ble_host.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "host_transport.h"
#include "nvs_flash.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nimble/nimble_port.h"
#include "nimble/nimble_port_freertos.h"
#include "host/ble_att.h"
#include "host/ble_hs.h"
#include "host/util/util.h"
#include "services/gap/ble_svc_gap.h"
#include "services/gatt/ble_svc_gatt.h"
#include <string.h>
static const char *TAG = "[BLE_HOST]";
static const char *DEVICE_NAME = "Powerpod";
#define BLE_CONN_ITVL_MIN_UNITS 6 /* 7.5 ms */
#define BLE_CONN_ITVL_MAX_UNITS 6
#define BLE_CONN_TUNE_MAX_TRIES 6
/*
* Nordic UART style:
* Service: 6e400001-b5a3-f393-e0a9-e50e24dcca9e
* Characteristic: 6e400002-b5a3-f393-e0a9-e50e24dcca9e
* Write = host→ESP (framed), Notify = ESP→host (framed).
*/
static const ble_uuid128_t ble_svc_uuid =
BLE_UUID128_INIT(0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, 0x93, 0xf3,
0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e);
static const ble_uuid128_t ble_chr_uuid =
BLE_UUID128_INIT(0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0, 0x93, 0xf3,
0xa3, 0xb5, 0x02, 0x00, 0x40, 0x6e);
static uint8_t own_addr_type;
static uint16_t conn_handle = BLE_HS_CONN_HANDLE_NONE;
static uint16_t ble_chr_val_handle;
static bool notify_enabled;
static uint16_t att_mtu = 23;
static esp_timer_handle_t conn_tune_timer;
static uint8_t conn_tune_tries;
static int ble_gap_event(struct ble_gap_event *event, void *arg);
static void ble_request_fast_conn_params(uint16_t handle);
static void ble_optimize_link(uint16_t handle);
bool ble_host_ready(void) {
return notify_enabled && conn_handle != BLE_HS_CONN_HANDLE_NONE;
}
uint16_t ble_host_att_mtu(void) { return att_mtu; }
uint16_t ble_host_max_payload(void) {
if (att_mtu < 4) {
return 20;
}
return (uint16_t)(att_mtu - 3);
}
static esp_err_t ble_host_tx(const uint8_t *frame, size_t frame_len) {
struct os_mbuf *om;
int rc;
if (!ble_host_ready()) {
return ESP_ERR_INVALID_STATE;
}
if (frame_len == 0 || frame_len > ble_host_max_payload()) {
return ESP_ERR_INVALID_SIZE;
}
om = ble_hs_mbuf_from_flat(frame, (uint16_t)frame_len);
if (om == NULL) {
return ESP_ERR_NO_MEM;
}
rc = ble_gatts_notify_custom(conn_handle, ble_chr_val_handle, om);
if (rc != 0) {
os_mbuf_free_chain(om);
return ESP_FAIL;
}
return ESP_OK;
}
static void ble_conn_tune_timer_cb(void *arg) {
struct ble_gap_conn_desc desc;
(void)arg;
if (conn_handle == BLE_HS_CONN_HANDLE_NONE) {
return;
}
if (ble_gap_conn_find(conn_handle, &desc) != 0) {
return;
}
if (desc.conn_itvl <= BLE_CONN_ITVL_MAX_UNITS) {
ESP_LOGI(TAG, "conn already at interval_us=%lu",
(unsigned long)desc.conn_itvl * 1250UL);
return;
}
if (conn_tune_tries >= BLE_CONN_TUNE_MAX_TRIES) {
ESP_LOGW(TAG, "central kept interval_us=%lu (ESP requested 7500)",
(unsigned long)desc.conn_itvl * 1250UL);
return;
}
conn_tune_tries++;
ESP_LOGI(TAG, "retry fast conn params (%u/%u); current interval_us=%lu",
conn_tune_tries, BLE_CONN_TUNE_MAX_TRIES,
(unsigned long)desc.conn_itvl * 1250UL);
ble_request_fast_conn_params(conn_handle);
esp_timer_start_once(conn_tune_timer, 750 * 1000);
}
static void ble_schedule_conn_tune(void) {
conn_tune_tries = 0;
esp_timer_stop(conn_tune_timer);
esp_timer_start_once(conn_tune_timer, 300 * 1000);
}
static void ble_request_fast_conn_params(uint16_t handle) {
struct ble_gap_upd_params params = {
.itvl_min = BLE_CONN_ITVL_MIN_UNITS,
.itvl_max = BLE_CONN_ITVL_MAX_UNITS,
.latency = 0,
.supervision_timeout = 400,
.min_ce_len = 0,
.max_ce_len = 24,
};
int rc = ble_gap_update_params(handle, &params);
if (rc != 0) {
ESP_LOGW(TAG, "conn param update request failed: %d", rc);
} else {
ESP_LOGI(TAG, "requested conn interval 7.5 ms");
}
}
static void ble_optimize_link(uint16_t handle) {
int rc;
/*
* Do NOT request 2M PHY here. With ESP-NOW/WiFi coexistence the controller
* can assert in llc_phy_upd (e.g. "BLE assert llc_phy_upd.c …") and the
* host link dies — no further responses. Stay on default 1M.
*/
ble_request_fast_conn_params(handle);
rc = ble_gap_set_data_len(handle, 251, 2120);
if (rc != 0) {
ESP_LOGW(TAG, "data len request failed: %d", rc);
}
ble_schedule_conn_tune();
}
static void ble_advertise(void) {
struct ble_gap_adv_params adv_params;
struct ble_hs_adv_fields fields;
int rc;
memset(&fields, 0, sizeof(fields));
fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP;
fields.tx_pwr_lvl_is_present = 1;
fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO;
fields.name = (uint8_t *)DEVICE_NAME;
fields.name_len = strlen(DEVICE_NAME);
fields.name_is_complete = 1;
rc = ble_gap_adv_set_fields(&fields);
if (rc != 0) {
ESP_LOGE(TAG, "adv set fields failed: %d", rc);
return;
}
memset(&adv_params, 0, sizeof(adv_params));
adv_params.conn_mode = BLE_GAP_CONN_MODE_UND;
adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN;
adv_params.itvl_min = 0x20;
adv_params.itvl_max = 0x30;
rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, &adv_params,
ble_gap_event, NULL);
if (rc != 0) {
ESP_LOGE(TAG, "adv start failed: %d", rc);
return;
}
ESP_LOGI(TAG, "advertising as \"%s\"", DEVICE_NAME);
}
static int ble_chr_access(uint16_t conn_handle_arg, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
(void)conn_handle_arg;
(void)attr_handle;
(void)arg;
switch (ctxt->op) {
case BLE_GATT_ACCESS_OP_READ_CHR:
return 0;
case BLE_GATT_ACCESS_OP_WRITE_CHR: {
uint16_t om_len = OS_MBUF_PKTLEN(ctxt->om);
uint8_t buf[512];
uint16_t copied = 0;
int rc;
if (om_len == 0 || om_len > sizeof(buf)) {
return BLE_ATT_ERR_INVALID_ATTR_VALUE_LEN;
}
rc = ble_hs_mbuf_to_flat(ctxt->om, buf, sizeof(buf), &copied);
if (rc != 0) {
return BLE_ATT_ERR_UNLIKELY;
}
host_rx_bytes(buf, copied);
return 0;
}
default:
return BLE_ATT_ERR_UNLIKELY;
}
}
static const struct ble_gatt_svc_def gatt_svcs[] = {
{
.type = BLE_GATT_SVC_TYPE_PRIMARY,
.uuid = &ble_svc_uuid.u,
.characteristics =
(struct ble_gatt_chr_def[]){
{
.uuid = &ble_chr_uuid.u,
.access_cb = ble_chr_access,
.val_handle = &ble_chr_val_handle,
.flags = BLE_GATT_CHR_F_READ | BLE_GATT_CHR_F_WRITE |
BLE_GATT_CHR_F_WRITE_NO_RSP | BLE_GATT_CHR_F_NOTIFY,
},
{0},
},
},
{0},
};
static int gatt_svr_init(void) {
int rc;
ble_svc_gap_init();
ble_svc_gatt_init();
rc = ble_gatts_count_cfg(gatt_svcs);
if (rc != 0) {
return rc;
}
return ble_gatts_add_svcs(gatt_svcs);
}
static int ble_gap_event(struct ble_gap_event *event, void *arg) {
(void)arg;
switch (event->type) {
case BLE_GAP_EVENT_CONNECT:
ESP_LOGI(TAG, "connection %s; status=%d handle=%d",
event->connect.status == 0 ? "established" : "failed",
event->connect.status, event->connect.conn_handle);
if (event->connect.status != 0) {
ble_advertise();
} else {
conn_handle = event->connect.conn_handle;
ble_optimize_link(conn_handle);
}
return 0;
case BLE_GAP_EVENT_DISCONNECT:
ESP_LOGI(TAG, "disconnect; reason=%d", event->disconnect.reason);
esp_timer_stop(conn_tune_timer);
conn_tune_tries = 0;
conn_handle = BLE_HS_CONN_HANDLE_NONE;
notify_enabled = false;
att_mtu = 23;
ble_advertise();
return 0;
case BLE_GAP_EVENT_CONN_UPDATE: {
struct ble_gap_conn_desc desc;
if (ble_gap_conn_find(event->conn_update.conn_handle, &desc) == 0) {
ESP_LOGI(TAG, "conn updated; status=%d interval_us=%lu latency=%u",
event->conn_update.status,
(unsigned long)desc.conn_itvl * 1250UL, desc.conn_latency);
if (event->conn_update.status == 0 &&
desc.conn_itvl > BLE_CONN_ITVL_MAX_UNITS &&
conn_tune_tries < BLE_CONN_TUNE_MAX_TRIES) {
esp_timer_stop(conn_tune_timer);
esp_timer_start_once(conn_tune_timer, 500 * 1000);
}
}
return 0;
}
case BLE_GAP_EVENT_PHY_UPDATE_COMPLETE:
ESP_LOGI(TAG, "phy update; status=%d tx=%u rx=%u",
event->phy_updated.status, event->phy_updated.tx_phy,
event->phy_updated.rx_phy);
return 0;
case BLE_GAP_EVENT_ADV_COMPLETE:
ESP_LOGI(TAG, "advertise complete; reason=%d", event->adv_complete.reason);
ble_advertise();
return 0;
case BLE_GAP_EVENT_SUBSCRIBE:
if (event->subscribe.attr_handle == ble_chr_val_handle) {
notify_enabled = event->subscribe.cur_notify;
ESP_LOGI(TAG, "notify %s", notify_enabled ? "enabled" : "disabled");
if (notify_enabled && conn_handle != BLE_HS_CONN_HANDLE_NONE) {
ble_request_fast_conn_params(conn_handle);
}
}
return 0;
case BLE_GAP_EVENT_MTU:
att_mtu = event->mtu.value;
ESP_LOGI(TAG, "mtu update; handle=%d mtu=%d", event->mtu.conn_handle,
event->mtu.value);
return 0;
default:
return 0;
}
}
static void ble_on_sync(void) {
int rc;
rc = ble_hs_id_infer_auto(0, &own_addr_type);
if (rc != 0) {
ESP_LOGE(TAG, "ble_hs_id_infer_auto failed: %d", rc);
return;
}
uint8_t addr_val[6] = {0};
ble_hs_id_copy_addr(own_addr_type, addr_val, NULL);
ESP_LOGI(TAG, "device address: %02x:%02x:%02x:%02x:%02x:%02x", addr_val[5],
addr_val[4], addr_val[3], addr_val[2], addr_val[1], addr_val[0]);
ble_advertise();
}
static void ble_on_reset(int reason) {
ESP_LOGE(TAG, "nimble reset; reason=%d", reason);
}
static void ble_host_task(void *param) {
(void)param;
ESP_LOGI(TAG, "nimble host task started");
nimble_port_run();
nimble_port_freertos_deinit();
}
esp_err_t init_ble_host(QueueHandle_t cmd_queue) {
esp_err_t ret;
host_transport_set_cmd_queue(cmd_queue);
host_transport_set_tx(ble_host_tx);
/* NVS may already be init via pod_settings; ignore ALREADY_INIT. */
ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) {
ESP_LOGE(TAG, "nvs_flash_init failed: %s", esp_err_to_name(ret));
return ret;
}
const esp_timer_create_args_t tune_args = {
.callback = &ble_conn_tune_timer_cb,
.name = "ble_conn_tune",
};
ESP_ERROR_CHECK(esp_timer_create(&tune_args, &conn_tune_timer));
ret = nimble_port_init();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "nimble_port_init failed: %s", esp_err_to_name(ret));
return ret;
}
ble_hs_cfg.sync_cb = ble_on_sync;
ble_hs_cfg.reset_cb = ble_on_reset;
int rc = gatt_svr_init();
if (rc != 0) {
ESP_LOGE(TAG, "gatt_svr_init failed: %d", rc);
return ESP_FAIL;
}
rc = ble_svc_gap_device_name_set(DEVICE_NAME);
if (rc != 0) {
ESP_LOGE(TAG, "device name set failed: %d", rc);
return ESP_FAIL;
}
rc = ble_att_set_preferred_mtu(247);
if (rc != 0) {
ESP_LOGW(TAG, "preferred MTU set failed: %d", rc);
}
nimble_port_freertos_init(ble_host_task);
ESP_LOGI(TAG, "BLE host transport started");
return ESP_OK;
}
+22
View File
@@ -0,0 +1,22 @@
#ifndef BLE_HOST_H
#define BLE_HOST_H
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <stddef.h>
#include <stdint.h>
/** Start NimBLE host transport (Master only). Uses same framing as UART. */
esp_err_t init_ble_host(QueueHandle_t cmd_queue);
/** True when a central is connected and CCCD notify is enabled. */
bool ble_host_ready(void);
/** Negotiated ATT MTU (default 23 until exchange). */
uint16_t ble_host_att_mtu(void);
/** Max notify/write payload bytes (ATT MTU - 3). */
uint16_t ble_host_max_payload(void);
#endif
+201
View File
@@ -0,0 +1,201 @@
#include "ble_stream.h"
#include "app_config.h"
#include "ble_host.h"
#include "client_registry.h"
#include "esp_log.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "pb_encode.h"
#include "uart.h"
#include "uart_cmd.h"
#include "uart_messages.pb.h"
#include <string.h>
static const char *TAG = "[BLE_STREAM]";
#define BLE_STREAM_PERIOD_MIN_MS 8u
#define BLE_STREAM_PERIOD_MAX_MS 16u
#define BLE_STREAM_PERIOD_DEFAULT_MS 16u
static uint32_t s_period_ms = BLE_STREAM_PERIOD_DEFAULT_MS;
static TaskHandle_t s_task;
static volatile bool s_running;
static bool tap_notify_any(const client_info_t *client) {
return client != NULL &&
(client->tap_notify_single || client->tap_notify_double ||
client->tap_notify_triple);
}
static alox_TapKind tap_kind_from_registry(uint32_t kind) {
switch (kind) {
case 1:
return alox_TapKind_TAP_SINGLE;
case 2:
return alox_TapKind_TAP_DOUBLE;
case 3:
return alox_TapKind_TAP_TRIPLE;
default:
return alox_TapKind_TAP_NONE;
}
}
static void fill_clients(alox_BleStreamTick *tick) {
tick->clients_count = 0;
size_t count = client_registry_count();
for (size_t i = 0; i < count; i++) {
const client_info_t *client = client_registry_at(i);
if (client == NULL) {
continue;
}
const bool want_accel = client->accel_stream_enabled;
const bool want_tap = tap_notify_any(client);
if (!want_accel && !want_tap) {
continue;
}
if (tick->clients_count >=
sizeof(tick->clients) / sizeof(tick->clients[0])) {
break;
}
alox_CacheClientStatus *entry = &tick->clients[tick->clients_count++];
entry->client_id = client->id;
entry->has_accel = false;
entry->has_tap = false;
if (want_accel) {
entry->has_accel = true;
entry->accel.valid = client->accel_valid;
if (client->accel_valid) {
entry->accel.x = client->accel_x;
entry->accel.y = client->accel_y;
entry->accel.z = client->accel_z;
entry->accel.age_ms =
client_registry_ms_since(client->accel_updated_at);
}
}
if (want_tap) {
uint32_t kind = 0;
uint32_t age_ms = 0;
if (client_registry_take_tap(client->id, &kind, &age_ms)) {
entry->has_tap = true;
entry->tap.kind = tap_kind_from_registry(kind);
entry->tap.age_ms = age_ms;
}
}
}
}
static bool any_accel_stream(void) {
size_t count = client_registry_count();
for (size_t i = 0; i < count; i++) {
const client_info_t *client = client_registry_at(i);
if (client != NULL && client->accel_stream_enabled) {
return true;
}
}
return false;
}
static esp_err_t send_tick_truncated(alox_UartMessage *msg) {
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
while (true) {
stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
return ESP_FAIL;
}
size_t payload_len = stream.bytes_written + 1;
uint16_t max_pay = ble_host_max_payload();
/* Framed size = payload + 4; notify must fit ATT MTU-3. */
size_t frame_len = payload_len + 4;
if (payload_len <= MAX_PAYLOAD_SIZE && frame_len <= max_pay) {
uint8_t payload[MAX_PAYLOAD_SIZE];
payload[0] = (uint8_t)msg->type;
memcpy(&payload[1], pb_buf, stream.bytes_written);
return uart_send_framed(payload, payload_len);
}
if (msg->payload.ble_stream_tick.clients_count == 0) {
ESP_LOGW(TAG, "tick too large even with 0 clients (need %u max %u)",
(unsigned)frame_len, (unsigned)max_pay);
return ESP_ERR_NO_MEM;
}
msg->payload.ble_stream_tick.clients_count--;
ESP_LOGW(TAG, "truncating tick clients to %u for MTU",
(unsigned)msg->payload.ble_stream_tick.clients_count);
}
}
static void ble_stream_task(void *param) {
(void)param;
ESP_LOGI(TAG, "BLE stream task started period_ms=%lu",
(unsigned long)s_period_ms);
while (s_running) {
uint32_t period = s_period_ms;
if (period < BLE_STREAM_PERIOD_MIN_MS) {
period = BLE_STREAM_PERIOD_MIN_MS;
}
if (period > BLE_STREAM_PERIOD_MAX_MS) {
period = BLE_STREAM_PERIOD_MAX_MS;
}
if (ble_host_ready() && any_accel_stream()) {
alox_UartMessage msg = alox_UartMessage_init_zero;
uart_cmd_init_response(&msg, alox_MessageType_BLE_STREAM_TICK,
alox_UartMessage_ble_stream_tick_tag);
msg.payload.ble_stream_tick.esp_timestamp_us =
(uint64_t)esp_timer_get_time();
msg.payload.ble_stream_tick.period_ms = period;
fill_clients(&msg.payload.ble_stream_tick);
esp_err_t err = send_tick_truncated(&msg);
if (err != ESP_OK) {
ESP_LOGW(TAG, "tick send failed: %s", esp_err_to_name(err));
}
}
vTaskDelay(pdMS_TO_TICKS(period));
}
s_task = NULL;
vTaskDelete(NULL);
}
void ble_stream_set_period_ms(uint32_t period_ms) {
if (period_ms == 0) {
period_ms = BLE_STREAM_PERIOD_DEFAULT_MS;
}
if (period_ms < BLE_STREAM_PERIOD_MIN_MS) {
period_ms = BLE_STREAM_PERIOD_MIN_MS;
}
if (period_ms > BLE_STREAM_PERIOD_MAX_MS) {
period_ms = BLE_STREAM_PERIOD_MAX_MS;
}
s_period_ms = period_ms;
}
uint32_t ble_stream_period_ms(void) { return s_period_ms; }
void ble_stream_update(void) {
const app_config_t *cfg = app_config_get();
bool want = cfg != NULL && cfg->host_ble && any_accel_stream();
if (want && !s_running) {
s_running = true;
if (xTaskCreate(ble_stream_task, "ble_stream", 4096, NULL, 5, &s_task) !=
pdPASS) {
s_running = false;
s_task = NULL;
ESP_LOGE(TAG, "failed to create ble_stream task");
}
} else if (!want && s_running) {
s_running = false;
}
}
+18
View File
@@ -0,0 +1,18 @@
#ifndef BLE_STREAM_H
#define BLE_STREAM_H
#include <stdbool.h>
#include <stdint.h>
/** Clamp and store BLE push period (8–16 ms). */
void ble_stream_set_period_ms(uint32_t period_ms);
uint32_t ble_stream_period_ms(void);
/**
* Re-evaluate whether the BLE push task should run (host_ble + any accel
* stream enabled). Safe to call from accel-stream handlers.
*/
void ble_stream_update(void);
#endif
+10 -2
View File
@@ -1,5 +1,6 @@
#include "client_registry.h" #include "client_registry.h"
#include "cmd_accel_stream.h" #include "cmd_accel_stream.h"
#include "ble_stream.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_now_comm.h" #include "esp_now_comm.h"
#include "uart_cmd.h" #include "uart_cmd.h"
@@ -50,6 +51,10 @@ static void handle_accel_stream(const uint8_t *data, size_t len) {
} }
if (req.write) { if (req.write) {
if (req.period_ms != 0) {
ble_stream_set_period_ms(req.period_ms);
}
if (req.all_clients) { if (req.all_clients) {
size_t n = client_registry_set_accel_stream_all(req.enable); size_t n = client_registry_set_accel_stream_all(req.enable);
uint32_t sent = 0; uint32_t sent = 0;
@@ -65,8 +70,10 @@ static void handle_accel_stream(const uint8_t *data, size_t len) {
} }
} }
ESP_LOGI(TAG, "accel stream %s for %u/%u slaves", ESP_LOGI(TAG, "accel stream %s for %u/%u slaves period_ms=%lu",
req.enable ? "on" : "off", (unsigned)sent, (unsigned)n); req.enable ? "on" : "off", (unsigned)sent, (unsigned)n,
(unsigned long)ble_stream_period_ms());
ble_stream_update();
reply(req.enable, 0, sent > 0, sent); reply(req.enable, 0, sent > 0, sent);
return; return;
} }
@@ -85,6 +92,7 @@ static void handle_accel_stream(const uint8_t *data, size_t len) {
} }
esp_err_t err = push_stream_to_slave(client, req.enable); esp_err_t err = push_stream_to_slave(client, req.enable);
ble_stream_update();
reply(req.enable, req.client_id, err == ESP_OK, err == ESP_OK ? 1u : 0u); reply(req.enable, req.client_id, err == ESP_OK, err == ESP_OK ? 1u : 0u);
return; return;
} }
+19
View File
@@ -1,4 +1,5 @@
#include "cmd_ota.h" #include "cmd_ota.h"
#include "app_config.h"
#include "led_ring.h" #include "led_ring.h"
#include "ota_espnow.h" #include "ota_espnow.h"
#include "ota_uart.h" #include "ota_uart.h"
@@ -103,10 +104,24 @@ static void ota_prepare_task(void *param) {
vTaskDelete(NULL); vTaskDelete(NULL);
} }
static bool ota_host_is_uart(void) {
const app_config_t *cfg = app_config_get();
if (cfg != NULL && cfg->host_ble) {
ESP_LOGW(TAG, "OTA requires UART host (DIP8=0); BLE host active");
return false;
}
return true;
}
static void handle_ota_start(const uint8_t *data, size_t len) { static void handle_ota_start(const uint8_t *data, size_t len) {
alox_UartMessage uart_msg; alox_UartMessage uart_msg;
alox_OtaStartPayload req = alox_OtaStartPayload_init_zero; alox_OtaStartPayload req = alox_OtaStartPayload_init_zero;
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) { if (uart_cmd_decode(data, len, &uart_msg) != ESP_OK) {
send_ota_failed( 2); send_ota_failed( 2);
return; return;
@@ -334,6 +349,10 @@ static void ota_start_espnow_task(void *param) {
} }
static void handle_ota_start_espnow(const uint8_t *data, size_t len) { static void handle_ota_start_espnow(const uint8_t *data, size_t len) {
if (!ota_host_is_uart()) {
send_ota_failed(20);
return;
}
(void)data; (void)data;
(void)len; (void)len;
+4 -2
View File
@@ -435,11 +435,13 @@ static void handle_accel_sample(const uint8_t mac[CLIENT_MAC_LEN],
esp_err_t err = client_registry_update_accel( esp_err_t err = client_registry_update_accel(
mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y, mac, sample->slave_id, (int16_t)sample->x, (int16_t)sample->y,
(int16_t)sample->z); (int16_t)sample->z);
if (err == ESP_ERR_NOT_FOUND) { if (err == ESP_ERR_NOT_FOUND || err == ESP_ERR_INVALID_STATE) {
// Unknown MAC, or stream already off (in-flight samples after disable).
return; return;
} }
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x", mac[0], mac[5]); ESP_LOGW(TAG, "accel sample id mismatch from %02x:…:%02x (slave_id=%lu)",
mac[0], mac[5], (unsigned long)sample->slave_id);
} }
} }
+97
View File
@@ -0,0 +1,97 @@
#include "host_transport.h"
#include "cmd_handler.h"
#include "esp_log.h"
#include "uart.h"
#include <stdlib.h>
#include <string.h>
static const char *TAG = "[HOST]";
static host_tx_fn_t s_tx_fn;
static QueueHandle_t s_cmd_queue;
static uart_packet_t s_rx_packet = {.state = STATE_START};
static TickType_t s_last_byte_ticks;
static const TickType_t s_rx_timeout_ticks = pdMS_TO_TICKS(50);
void host_transport_set_tx(host_tx_fn_t tx_fn) { s_tx_fn = tx_fn; }
void host_transport_set_cmd_queue(QueueHandle_t cmd_queue) {
s_cmd_queue = cmd_queue;
}
QueueHandle_t host_transport_cmd_queue(void) { return s_cmd_queue; }
bool host_enqueue_packet(const uint8_t *payload, uint8_t len) {
if (s_cmd_queue == NULL || payload == NULL || len == 0) {
return false;
}
generic_msg_t msg = {
.msg_id = payload[0],
.len = len > 1 ? (size_t)(len - 1) : 0,
.payload = NULL,
};
if (msg.len > 0) {
msg.payload = malloc(msg.len);
if (msg.payload == NULL) {
ESP_LOGE(TAG, "failed to allocate command payload");
return false;
}
memcpy(msg.payload, &payload[1], msg.len);
}
if (xQueueSend(s_cmd_queue, &msg, pdMS_TO_TICKS(500)) != pdPASS) {
free(msg.payload);
ESP_LOGW(TAG, "command queue full (cmd 0x%02x)", (unsigned)msg.msg_id);
return false;
}
return true;
}
esp_err_t host_send_framed(const uint8_t *payload, size_t len) {
if (payload == NULL || len == 0 || len > MAX_PAYLOAD_SIZE) {
return ESP_ERR_INVALID_ARG;
}
if (s_tx_fn == NULL) {
return ESP_ERR_INVALID_STATE;
}
uint8_t checksum = 0;
for (size_t i = 0; i < len; i++) {
checksum ^= payload[i];
}
uint8_t frame[4 + MAX_PAYLOAD_SIZE];
size_t pos = 0;
frame[pos++] = START_MARKER;
frame[pos++] = (uint8_t)len;
memcpy(&frame[pos], payload, len);
pos += len;
frame[pos++] = checksum;
frame[pos++] = STOP_MARKER;
return s_tx_fn(frame, pos);
}
void host_rx_bytes(const uint8_t *data, size_t len) {
if (data == NULL || len == 0) {
return;
}
TickType_t now = xTaskGetTickCount();
if (s_rx_packet.state != STATE_START &&
(now - s_last_byte_ticks) > s_rx_timeout_ticks) {
s_rx_packet.state = STATE_START;
}
for (size_t i = 0; i < len; i++) {
if (parse_uart_byte(data[i], &s_rx_packet)) {
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u",
s_rx_packet.len > 0 ? s_rx_packet.payload[0] : 0,
(unsigned)s_rx_packet.len);
host_enqueue_packet(s_rx_packet.payload, s_rx_packet.len);
}
}
s_last_byte_ticks = now;
}
+36
View File
@@ -0,0 +1,36 @@
#ifndef HOST_TRANSPORT_H
#define HOST_TRANSPORT_H
#include "esp_err.h"
#include "freertos/FreeRTOS.h"
#include "freertos/queue.h"
#include <stddef.h>
#include <stdint.h>
/** TX callback: send a complete framed packet (0xAA … 0xCC). */
typedef esp_err_t (*host_tx_fn_t)(const uint8_t *frame, size_t frame_len);
/** Set the active host TX backend (UART write or BLE notify). */
void host_transport_set_tx(host_tx_fn_t tx_fn);
/** Bind the shared cmd_queue used by uart_enqueue_packet / host RX. */
void host_transport_set_cmd_queue(QueueHandle_t cmd_queue);
QueueHandle_t host_transport_cmd_queue(void);
/**
* Build AA|len|payload|xor|CC and send via the registered TX backend.
* Same semantics as uart_send_framed.
*/
esp_err_t host_send_framed(const uint8_t *payload, size_t len);
/**
* Feed raw host RX bytes into the shared frame parser and enqueue complete
* packets onto the cmd_queue.
*/
void host_rx_bytes(const uint8_t *data, size_t len);
/** Enqueue a completed uart_packet_t (payload[0]=msg type). */
bool host_enqueue_packet(const uint8_t *payload, uint8_t len);
#endif
+27 -6
View File
@@ -32,8 +32,10 @@
#include "led_ring.h" #include "led_ring.h"
#include "pod_settings.h" #include "pod_settings.h"
#include "uart.h" #include "uart.h"
#include "ble_host.h"
#include "battery_uv.h" #include "battery_uv.h"
#include <stdint.h> #include <stdint.h>
#include <string.h>
enum MASTER_STATES { enum MASTER_STATES {
MSTATE_INIT, MSTATE_INIT,
@@ -94,7 +96,8 @@ void app_main(void) {
gpio_reset_pin(DIP_MASTER); gpio_reset_pin(DIP_MASTER);
gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT); gpio_set_direction(DIP_MASTER, GPIO_MODE_INPUT);
uint8_t master = gpio_get_level(DIP_MASTER) ? 0 : 1; uint8_t master = gpio_get_level(DIP_MASTER) ? 0 : 1;
uint8_t network = 0; uint8_t network = 1;
bool host_ble = false;
esp_err_t err; esp_err_t err;
@@ -137,12 +140,20 @@ void app_main(void) {
-1); -1);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGI(TAG, "Could not read IO Expander input %d", err); ESP_LOGI(TAG, "Could not read IO Expander input %d", err);
} else {
/* High nibble physically reversed on PCB; DIP1..8 after reverse+invert. */
uint8_t bits = (uint8_t)(~reverse_high_nibble_lut(read_val));
/* DIP8 (bit7) = host transport: 1 = BLE, 0 = UART */
host_ble = (bits & 0x80u) != 0;
uint8_t net_bits = (uint8_t)(bits & 0x7Fu);
if (net_bits == 0 || (net_bits & (net_bits - 1u)) != 0) {
ESP_LOGW(TAG, "invalid network DIP one-hot=0x%02x; fallback network=1",
net_bits);
network = 1;
} else {
network = (uint8_t)(__builtin_ctz(net_bits) + 1); /* 1..7 */
}
} }
// Build NetworkNumber because Pin 5-8 are reversed
network = reverse_high_nibble_lut(read_val);
network = ~network; // reverse bits
network = __builtin_ctz(network) +
1; // map network to 0-8, 0 is not a valid network
} }
if (bus_handle != NULL) { if (bus_handle != NULL) {
@@ -167,6 +178,7 @@ void app_main(void) {
app_config.master = (master != 0); app_config.master = (master != 0);
app_config.network = network; app_config.network = network;
app_config.host_ble = host_ble;
if (running != NULL) { if (running != NULL) {
memcpy(app_config.running_partition, running->label, memcpy(app_config.running_partition, running->label,
sizeof(app_config.running_partition)); sizeof(app_config.running_partition));
@@ -177,6 +189,8 @@ void app_main(void) {
ESP_LOGI(TAG, "RUNNING CONFIG:"); ESP_LOGI(TAG, "RUNNING CONFIG:");
ESP_LOGI(TAG, "Master: %d", app_config.master); ESP_LOGI(TAG, "Master: %d", app_config.master);
ESP_LOGI(TAG, "Network: %d", app_config.network); ESP_LOGI(TAG, "Network: %d", app_config.network);
ESP_LOGI(TAG, "Host: %s (DIP8; reboot to switch)",
app_config.host_ble ? "BLE" : "UART");
ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)", ESP_LOGI(TAG, "Running Partition: %s (OTA slot %d)",
app_config.running_partition, ota_slot); app_config.running_partition, ota_slot);
@@ -193,7 +207,14 @@ void app_main(void) {
if (app_config.master) { if (app_config.master) {
cmd_queue = xQueueCreate(64, sizeof(generic_msg_t)); cmd_queue = xQueueCreate(64, sizeof(generic_msg_t));
init_cmdHandler(cmd_queue); init_cmdHandler(cmd_queue);
if (app_config.host_ble) {
err = init_ble_host(cmd_queue);
if (err != ESP_OK) {
ESP_LOGE(TAG, "BLE host init failed: %s", esp_err_to_name(err));
}
} else {
init_uart(cmd_queue); init_uart(cmd_queue);
}
cmd_version_register(); cmd_version_register();
cmd_client_info_register(); cmd_client_info_register();
cmd_accel_deadzone_register(); cmd_accel_deadzone_register();
+3
View File
@@ -81,6 +81,9 @@ PB_BIND(alox_CacheClientStatus, alox_CacheClientStatus, AUTO)
PB_BIND(alox_CacheStatusResponse, alox_CacheStatusResponse, 2) PB_BIND(alox_CacheStatusResponse, alox_CacheStatusResponse, 2)
PB_BIND(alox_BleStreamTick, alox_BleStreamTick, 2)
PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO) PB_BIND(alox_EspNowUnicastTestRequest, alox_EspNowUnicastTestRequest, AUTO)
+45 -10
View File
@@ -38,7 +38,9 @@ typedef enum _alox_MessageType {
/* * Host → master: get/set ESP-IDF log level for tag "*" (global). */ /* * Host → master: get/set ESP-IDF log level for tag "*" (global). */
alox_MessageType_SET_LOG_LEVEL = 31, alox_MessageType_SET_LOG_LEVEL = 31,
/* * Host → master: get/set BMA456H multitap detection parameters. */ /* * Host → master: get/set BMA456H multitap detection parameters. */
alox_MessageType_TAP_CONFIG = 32 alox_MessageType_TAP_CONFIG = 32,
/* * Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp. */
alox_MessageType_BLE_STREAM_TICK = 33
} alox_MessageType; } alox_MessageType;
typedef enum _alox_TapKind { typedef enum _alox_TapKind {
@@ -113,12 +115,14 @@ typedef struct _alox_AccelDeadzoneResponse {
} alox_AccelDeadzoneResponse; } alox_AccelDeadzoneResponse;
/* Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave). /* Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients). */ write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16). */
typedef struct _alox_AccelStreamRequest { typedef struct _alox_AccelStreamRequest {
bool write; bool write;
bool enable; bool enable;
uint32_t client_id; uint32_t client_id;
bool all_clients; bool all_clients;
uint32_t period_ms;
} alox_AccelStreamRequest; } alox_AccelStreamRequest;
typedef struct _alox_AccelStreamResponse { typedef struct _alox_AccelStreamResponse {
@@ -231,6 +235,14 @@ typedef struct _alox_CacheStatusResponse {
alox_CacheClientStatus clients[16]; alox_CacheClientStatus clients[16];
} alox_CacheStatusResponse; } alox_CacheStatusResponse;
/* * Master → host over BLE notify: aggregated cache + ESP timer timestamp. */
typedef struct _alox_BleStreamTick {
uint64_t esp_timestamp_us;
uint32_t period_ms;
pb_size_t clients_count;
alox_CacheClientStatus clients[16];
} alox_BleStreamTick;
typedef struct _alox_EspNowUnicastTestRequest { typedef struct _alox_EspNowUnicastTestRequest {
uint32_t client_id; uint32_t client_id;
uint32_t seq; uint32_t seq;
@@ -443,6 +455,7 @@ typedef struct _alox_UartMessage {
alox_SetLogLevelResponse set_log_level_response; alox_SetLogLevelResponse set_log_level_response;
alox_TapConfigRequest tap_config_request; alox_TapConfigRequest tap_config_request;
alox_TapConfigResponse tap_config_response; alox_TapConfigResponse tap_config_response;
alox_BleStreamTick ble_stream_tick;
} payload; } payload;
} alox_UartMessage; } alox_UartMessage;
@@ -453,8 +466,8 @@ extern "C" {
/* Helper constants for enums */ /* Helper constants for enums */
#define _alox_MessageType_MIN alox_MessageType_UNKNOWN #define _alox_MessageType_MIN alox_MessageType_UNKNOWN
#define _alox_MessageType_MAX alox_MessageType_TAP_CONFIG #define _alox_MessageType_MAX alox_MessageType_BLE_STREAM_TICK
#define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_TAP_CONFIG+1)) #define _alox_MessageType_ARRAYSIZE ((alox_MessageType)(alox_MessageType_BLE_STREAM_TICK+1))
#define _alox_TapKind_MIN alox_TapKind_TAP_NONE #define _alox_TapKind_MIN alox_TapKind_TAP_NONE
#define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE #define _alox_TapKind_MAX alox_TapKind_TAP_TRIPLE
@@ -508,6 +521,7 @@ extern "C" {
/* Initializer values for message structs */ /* Initializer values for message structs */
@@ -521,7 +535,7 @@ extern "C" {
#define alox_ClientInputResponse_init_default {{{NULL}, NULL}} #define alox_ClientInputResponse_init_default {{{NULL}, NULL}}
#define alox_AccelDeadzoneRequest_init_default {0, 0, 0, 0} #define alox_AccelDeadzoneRequest_init_default {0, 0, 0, 0}
#define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0} #define alox_AccelDeadzoneResponse_init_default {0, 0, 0, 0}
#define alox_AccelStreamRequest_init_default {0, 0, 0, 0} #define alox_AccelStreamRequest_init_default {0, 0, 0, 0, 0}
#define alox_AccelStreamResponse_init_default {0, 0, 0, 0} #define alox_AccelStreamResponse_init_default {0, 0, 0, 0}
#define alox_BatteryStatusRequest_init_default {0, 0} #define alox_BatteryStatusRequest_init_default {0, 0}
#define alox_LipoReading_init_default {0, 0} #define alox_LipoReading_init_default {0, 0}
@@ -536,6 +550,7 @@ extern "C" {
#define alox_CacheClientTap_init_default {_alox_TapKind_MIN, 0} #define alox_CacheClientTap_init_default {_alox_TapKind_MIN, 0}
#define alox_CacheClientStatus_init_default {0, false, alox_CacheClientAccel_init_default, false, alox_CacheClientTap_init_default} #define alox_CacheClientStatus_init_default {0, false, alox_CacheClientAccel_init_default, false, alox_CacheClientTap_init_default}
#define alox_CacheStatusResponse_init_default {0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}} #define alox_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_BleStreamTick_init_default {0, 0, 0, {alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default, alox_CacheClientStatus_init_default}}
#define alox_EspNowUnicastTestRequest_init_default {0, 0} #define alox_EspNowUnicastTestRequest_init_default {0, 0}
#define alox_EspNowUnicastTestResponse_init_default {0, 0} #define alox_EspNowUnicastTestResponse_init_default {0, 0}
#define alox_EspNowEchoPingRequest_init_default {0, 0} #define alox_EspNowEchoPingRequest_init_default {0, 0}
@@ -567,7 +582,7 @@ extern "C" {
#define alox_ClientInputResponse_init_zero {{{NULL}, NULL}} #define alox_ClientInputResponse_init_zero {{{NULL}, NULL}}
#define alox_AccelDeadzoneRequest_init_zero {0, 0, 0, 0} #define alox_AccelDeadzoneRequest_init_zero {0, 0, 0, 0}
#define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0} #define alox_AccelDeadzoneResponse_init_zero {0, 0, 0, 0}
#define alox_AccelStreamRequest_init_zero {0, 0, 0, 0} #define alox_AccelStreamRequest_init_zero {0, 0, 0, 0, 0}
#define alox_AccelStreamResponse_init_zero {0, 0, 0, 0} #define alox_AccelStreamResponse_init_zero {0, 0, 0, 0}
#define alox_BatteryStatusRequest_init_zero {0, 0} #define alox_BatteryStatusRequest_init_zero {0, 0}
#define alox_LipoReading_init_zero {0, 0} #define alox_LipoReading_init_zero {0, 0}
@@ -582,6 +597,7 @@ extern "C" {
#define alox_CacheClientTap_init_zero {_alox_TapKind_MIN, 0} #define alox_CacheClientTap_init_zero {_alox_TapKind_MIN, 0}
#define alox_CacheClientStatus_init_zero {0, false, alox_CacheClientAccel_init_zero, false, alox_CacheClientTap_init_zero} #define alox_CacheClientStatus_init_zero {0, false, alox_CacheClientAccel_init_zero, false, alox_CacheClientTap_init_zero}
#define alox_CacheStatusResponse_init_zero {0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}} #define alox_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_BleStreamTick_init_zero {0, 0, 0, {alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero, alox_CacheClientStatus_init_zero}}
#define alox_EspNowUnicastTestRequest_init_zero {0, 0} #define alox_EspNowUnicastTestRequest_init_zero {0, 0}
#define alox_EspNowUnicastTestResponse_init_zero {0, 0} #define alox_EspNowUnicastTestResponse_init_zero {0, 0}
#define alox_EspNowEchoPingRequest_init_zero {0, 0} #define alox_EspNowEchoPingRequest_init_zero {0, 0}
@@ -638,6 +654,7 @@ extern "C" {
#define alox_AccelStreamRequest_enable_tag 2 #define alox_AccelStreamRequest_enable_tag 2
#define alox_AccelStreamRequest_client_id_tag 3 #define alox_AccelStreamRequest_client_id_tag 3
#define alox_AccelStreamRequest_all_clients_tag 4 #define alox_AccelStreamRequest_all_clients_tag 4
#define alox_AccelStreamRequest_period_ms_tag 5
#define alox_AccelStreamResponse_enabled_tag 1 #define alox_AccelStreamResponse_enabled_tag 1
#define alox_AccelStreamResponse_client_id_tag 2 #define alox_AccelStreamResponse_client_id_tag 2
#define alox_AccelStreamResponse_success_tag 3 #define alox_AccelStreamResponse_success_tag 3
@@ -685,6 +702,9 @@ extern "C" {
#define alox_CacheClientStatus_accel_tag 2 #define alox_CacheClientStatus_accel_tag 2
#define alox_CacheClientStatus_tap_tag 3 #define alox_CacheClientStatus_tap_tag 3
#define alox_CacheStatusResponse_clients_tag 1 #define alox_CacheStatusResponse_clients_tag 1
#define alox_BleStreamTick_esp_timestamp_us_tag 1
#define alox_BleStreamTick_period_ms_tag 2
#define alox_BleStreamTick_clients_tag 3
#define alox_EspNowUnicastTestRequest_client_id_tag 1 #define alox_EspNowUnicastTestRequest_client_id_tag 1
#define alox_EspNowUnicastTestRequest_seq_tag 2 #define alox_EspNowUnicastTestRequest_seq_tag 2
#define alox_EspNowUnicastTestResponse_success_tag 1 #define alox_EspNowUnicastTestResponse_success_tag 1
@@ -801,6 +821,7 @@ extern "C" {
#define alox_UartMessage_set_log_level_response_tag 38 #define alox_UartMessage_set_log_level_response_tag 38
#define alox_UartMessage_tap_config_request_tag 39 #define alox_UartMessage_tap_config_request_tag 39
#define alox_UartMessage_tap_config_response_tag 40 #define alox_UartMessage_tap_config_response_tag 40
#define alox_UartMessage_ble_stream_tick_tag 41
/* Struct field encoding specification for nanopb */ /* Struct field encoding specification for nanopb */
#define alox_UartMessage_FIELDLIST(X, a) \ #define alox_UartMessage_FIELDLIST(X, a) \
@@ -839,7 +860,8 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,espnow_echo_ping_response,payload.es
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_request,payload.set_log_level_request), 37) \
X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_response,payload.set_log_level_response), 38) \ X(a, STATIC, ONEOF, MESSAGE, (payload,set_log_level_response,payload.set_log_level_response), 38) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_request,payload.tap_config_request), 39) \ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_request,payload.tap_config_request), 39) \
X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_response,payload.tap_config_response), 40) X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_response,payload.tap_config_response), 40) \
X(a, STATIC, ONEOF, MESSAGE, (payload,ble_stream_tick,payload.ble_stream_tick), 41)
#define alox_UartMessage_CALLBACK NULL #define alox_UartMessage_CALLBACK NULL
#define alox_UartMessage_DEFAULT NULL #define alox_UartMessage_DEFAULT NULL
#define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack #define alox_UartMessage_payload_ack_payload_MSGTYPE alox_Ack
@@ -877,6 +899,7 @@ X(a, STATIC, ONEOF, MESSAGE, (payload,tap_config_response,payload.tap_conf
#define alox_UartMessage_payload_set_log_level_response_MSGTYPE alox_SetLogLevelResponse #define alox_UartMessage_payload_set_log_level_response_MSGTYPE alox_SetLogLevelResponse
#define alox_UartMessage_payload_tap_config_request_MSGTYPE alox_TapConfigRequest #define alox_UartMessage_payload_tap_config_request_MSGTYPE alox_TapConfigRequest
#define alox_UartMessage_payload_tap_config_response_MSGTYPE alox_TapConfigResponse #define alox_UartMessage_payload_tap_config_response_MSGTYPE alox_TapConfigResponse
#define alox_UartMessage_payload_ble_stream_tick_MSGTYPE alox_BleStreamTick
#define alox_Ack_FIELDLIST(X, a) \ #define alox_Ack_FIELDLIST(X, a) \
@@ -950,7 +973,8 @@ X(a, STATIC, SINGULAR, UINT32, slaves_updated, 4)
X(a, STATIC, SINGULAR, BOOL, write, 1) \ X(a, STATIC, SINGULAR, BOOL, write, 1) \
X(a, STATIC, SINGULAR, BOOL, enable, 2) \ X(a, STATIC, SINGULAR, BOOL, enable, 2) \
X(a, STATIC, SINGULAR, UINT32, client_id, 3) \ X(a, STATIC, SINGULAR, UINT32, client_id, 3) \
X(a, STATIC, SINGULAR, BOOL, all_clients, 4) X(a, STATIC, SINGULAR, BOOL, all_clients, 4) \
X(a, STATIC, SINGULAR, UINT32, period_ms, 5)
#define alox_AccelStreamRequest_CALLBACK NULL #define alox_AccelStreamRequest_CALLBACK NULL
#define alox_AccelStreamRequest_DEFAULT NULL #define alox_AccelStreamRequest_DEFAULT NULL
@@ -1064,6 +1088,14 @@ X(a, STATIC, REPEATED, MESSAGE, clients, 1)
#define alox_CacheStatusResponse_DEFAULT NULL #define alox_CacheStatusResponse_DEFAULT NULL
#define alox_CacheStatusResponse_clients_MSGTYPE alox_CacheClientStatus #define alox_CacheStatusResponse_clients_MSGTYPE alox_CacheClientStatus
#define alox_BleStreamTick_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT64, esp_timestamp_us, 1) \
X(a, STATIC, SINGULAR, UINT32, period_ms, 2) \
X(a, STATIC, REPEATED, MESSAGE, clients, 3)
#define alox_BleStreamTick_CALLBACK NULL
#define alox_BleStreamTick_DEFAULT NULL
#define alox_BleStreamTick_clients_MSGTYPE alox_CacheClientStatus
#define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \ #define alox_EspNowUnicastTestRequest_FIELDLIST(X, a) \
X(a, STATIC, SINGULAR, UINT32, client_id, 1) \ X(a, STATIC, SINGULAR, UINT32, client_id, 1) \
X(a, STATIC, SINGULAR, UINT32, seq, 2) X(a, STATIC, SINGULAR, UINT32, seq, 2)
@@ -1255,6 +1287,7 @@ extern const pb_msgdesc_t alox_CacheClientAccel_msg;
extern const pb_msgdesc_t alox_CacheClientTap_msg; extern const pb_msgdesc_t alox_CacheClientTap_msg;
extern const pb_msgdesc_t alox_CacheClientStatus_msg; extern const pb_msgdesc_t alox_CacheClientStatus_msg;
extern const pb_msgdesc_t alox_CacheStatusResponse_msg; extern const pb_msgdesc_t alox_CacheStatusResponse_msg;
extern const pb_msgdesc_t alox_BleStreamTick_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestRequest_msg;
extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg; extern const pb_msgdesc_t alox_EspNowUnicastTestResponse_msg;
extern const pb_msgdesc_t alox_EspNowEchoPingRequest_msg; extern const pb_msgdesc_t alox_EspNowEchoPingRequest_msg;
@@ -1303,6 +1336,7 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
#define alox_CacheClientTap_fields &alox_CacheClientTap_msg #define alox_CacheClientTap_fields &alox_CacheClientTap_msg
#define alox_CacheClientStatus_fields &alox_CacheClientStatus_msg #define alox_CacheClientStatus_fields &alox_CacheClientStatus_msg
#define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg #define alox_CacheStatusResponse_fields &alox_CacheStatusResponse_msg
#define alox_BleStreamTick_fields &alox_BleStreamTick_msg
#define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg #define alox_EspNowUnicastTestRequest_fields &alox_EspNowUnicastTestRequest_msg
#define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg #define alox_EspNowUnicastTestResponse_fields &alox_EspNowUnicastTestResponse_msg
#define alox_EspNowEchoPingRequest_fields &alox_EspNowEchoPingRequest_msg #define alox_EspNowEchoPingRequest_fields &alox_EspNowEchoPingRequest_msg
@@ -1332,16 +1366,17 @@ extern const pb_msgdesc_t alox_OtaSlaveProgressResponse_msg;
/* alox_ClientInfo_size depends on runtime parameters */ /* alox_ClientInfo_size depends on runtime parameters */
/* alox_ClientInfoResponse_size depends on runtime parameters */ /* alox_ClientInfoResponse_size depends on runtime parameters */
/* alox_ClientInputResponse_size depends on runtime parameters */ /* alox_ClientInputResponse_size depends on runtime parameters */
#define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_CacheStatusResponse_size #define ALOX_UART_MESSAGES_PB_H_MAX_SIZE alox_BleStreamTick_size
#define alox_AccelDeadzoneRequest_size 16 #define alox_AccelDeadzoneRequest_size 16
#define alox_AccelDeadzoneResponse_size 20 #define alox_AccelDeadzoneResponse_size 20
#define alox_AccelSample_size 32 #define alox_AccelSample_size 32
#define alox_AccelStreamRequest_size 12 #define alox_AccelStreamRequest_size 18
#define alox_AccelStreamResponse_size 16 #define alox_AccelStreamResponse_size 16
#define alox_Ack_size 0 #define alox_Ack_size 0
#define alox_BatterySample_size 32 #define alox_BatterySample_size 32
#define alox_BatteryStatusRequest_size 8 #define alox_BatteryStatusRequest_size 8
#define alox_BatteryStatusResponse_size 580 #define alox_BatteryStatusResponse_size 580
#define alox_BleStreamTick_size 753
#define alox_CacheClientAccel_size 26 #define alox_CacheClientAccel_size 26
#define alox_CacheClientStatus_size 44 #define alox_CacheClientStatus_size 44
#define alox_CacheClientTap_size 8 #define alox_CacheClientTap_size 8
+12
View File
@@ -35,6 +35,8 @@ enum MessageType {
SET_LOG_LEVEL = 31; SET_LOG_LEVEL = 31;
/** Host → master: get/set BMA456H multitap detection parameters. */ /** Host → master: get/set BMA456H multitap detection parameters. */
TAP_CONFIG = 32; TAP_CONFIG = 32;
/** Master → host (BLE only): periodic pushed cache snapshot with ESP timestamp. */
BLE_STREAM_TICK = 33;
} }
message UartMessage { message UartMessage {
@@ -75,6 +77,7 @@ message UartMessage {
SetLogLevelResponse set_log_level_response = 38; SetLogLevelResponse set_log_level_response = 38;
TapConfigRequest tap_config_request = 39; TapConfigRequest tap_config_request = 39;
TapConfigResponse tap_config_response = 40; TapConfigResponse tap_config_response = 40;
BleStreamTick ble_stream_tick = 41;
} }
} }
@@ -141,11 +144,13 @@ message AccelDeadzoneResponse {
// Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave). // Host → master: enable/disable slave accel ESP-NOW stream (~16 ms per slave).
// write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients). // write=false: read; write=true: apply. client_id 0 invalid for write (use >0 or all_clients).
// period_ms (8–16): BLE host push interval for BleStreamTick; ignored on UART (default 16).
message AccelStreamRequest { message AccelStreamRequest {
bool write = 1; bool write = 1;
bool enable = 2; bool enable = 2;
uint32 client_id = 3; uint32 client_id = 3;
bool all_clients = 4; bool all_clients = 4;
uint32 period_ms = 5;
} }
message AccelStreamResponse { message AccelStreamResponse {
@@ -257,6 +262,13 @@ message CacheStatusResponse {
repeated CacheClientStatus clients = 1 [(nanopb).max_count = 16]; repeated CacheClientStatus clients = 1 [(nanopb).max_count = 16];
} }
/** Master → host over BLE notify: aggregated cache + ESP timer timestamp. */
message BleStreamTick {
uint64 esp_timestamp_us = 1;
uint32 period_ms = 2;
repeated CacheClientStatus clients = 3 [(nanopb).max_count = 16];
}
message EspNowUnicastTestRequest { message EspNowUnicastTestRequest {
uint32 client_id = 1; uint32 client_id = 1;
uint32 seq = 2; uint32 seq = 2;
+12 -69
View File
@@ -4,46 +4,28 @@
#include "esp_log.h" #include "esp_log.h"
#include "freertos/idf_additions.h" #include "freertos/idf_additions.h"
#include "hal/uart_types.h" #include "hal/uart_types.h"
#include "host_transport.h"
#include "uart.h" #include "uart.h"
#include <stdbool.h> #include <stdbool.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
static const char *TAG = "[UART]"; static const char *TAG = "[UART]";
static QueueHandle_t uart_cmd_queue;
static bool uart_enqueue_packet(const uart_packet_t *packet) { static esp_err_t uart_host_tx(const uint8_t *frame, size_t frame_len) {
if (packet->len == 0) { int written = uart_write_bytes(UART_NUM, frame, frame_len);
return false; if (written < 0 || (size_t)written != frame_len) {
return ESP_FAIL;
} }
return ESP_OK;
generic_msg_t msg = {
.msg_id = packet->payload[0],
.len = packet->len > 1 ? packet->len - 1 : 0,
.payload = NULL,
};
if (msg.len > 0) {
msg.payload = malloc(msg.len);
if (msg.payload == NULL) {
ESP_LOGE(TAG, "failed to allocate command payload");
return false;
}
memcpy(msg.payload, &packet->payload[1], msg.len);
}
if (xQueueSend(uart_cmd_queue, &msg, pdMS_TO_TICKS(500)) != pdPASS) {
free(msg.payload);
ESP_LOGW(TAG, "command queue full (cmd 0x%02x)", (unsigned)msg.msg_id);
return false;
}
return true;
} }
void init_uart(QueueHandle_t cmd_queue) { void init_uart(QueueHandle_t cmd_queue) {
esp_err_t err; esp_err_t err;
uart_cmd_queue = cmd_queue; host_transport_set_cmd_queue(cmd_queue);
host_transport_set_tx(uart_host_tx);
uart_config_t uart_config = { uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE, .baud_rate = UART_BAUD_RATE,
.data_bits = UART_DATA_8_BITS, .data_bits = UART_DATA_8_BITS,
@@ -85,27 +67,10 @@ void uart_read_task(void *param) {
return; return;
} }
int len = 0;
uart_packet_t packet = {.state = STATE_START};
TickType_t last_byte_time = xTaskGetTickCount();
const TickType_t timeout_ticks = pdMS_TO_TICKS(50);
while (1) { while (1) {
len = uart_read_bytes(UART_NUM, data, UART_BUF_SIZE, pdMS_TO_TICKS(20)); int len = uart_read_bytes(UART_NUM, data, UART_BUF_SIZE, pdMS_TO_TICKS(20));
if (len > 0) { if (len > 0) {
for (int i = 0; i < len; ++i) { host_rx_bytes(data, (size_t)len);
if (parse_uart_byte(data[i], &packet)) {
ESP_LOGI(TAG, "received message cmd=0x%02x len=%u",
packet.len > 0 ? packet.payload[0] : 0, (unsigned)packet.len);
uart_enqueue_packet(&packet);
}
}
last_byte_time = xTaskGetTickCount();
} else if (packet.state != STATE_START) {
TickType_t now = xTaskGetTickCount();
if ((now - last_byte_time) > timeout_ticks) {
packet.state = STATE_START;
}
} }
} }
} }
@@ -156,27 +121,5 @@ bool parse_uart_byte(uint8_t byte, uart_packet_t *p) {
} }
esp_err_t uart_send_framed(const uint8_t *payload, size_t len) { esp_err_t uart_send_framed(const uint8_t *payload, size_t len) {
if (payload == NULL || len == 0 || len > MAX_PAYLOAD_SIZE) { return host_send_framed(payload, len);
return ESP_ERR_INVALID_ARG;
}
uint8_t checksum = 0;
for (size_t i = 0; i < len; i++) {
checksum ^= payload[i];
}
uint8_t frame[4 + MAX_PAYLOAD_SIZE];
size_t pos = 0;
frame[pos++] = START_MARKER;
frame[pos++] = (uint8_t)len;
memcpy(&frame[pos], payload, len);
pos += len;
frame[pos++] = checksum;
frame[pos++] = STOP_MARKER;
int written = uart_write_bytes(UART_NUM, frame, pos);
if (written < 0 || (size_t)written != pos) {
return ESP_FAIL;
}
return ESP_OK;
} }
+330 -4
View File
@@ -650,16 +650,296 @@ CONFIG_APPTRACE_LOCK_ENABLE=y
# #
# Bluetooth # Bluetooth
# #
# CONFIG_BT_ENABLED is not set CONFIG_BT_ENABLED=y
# CONFIG_BT_BLUEDROID_ENABLED is not set
CONFIG_BT_NIMBLE_ENABLED=y
# CONFIG_BT_CONTROLLER_ONLY is not set
CONFIG_BT_CONTROLLER_ENABLED=y
# CONFIG_BT_CONTROLLER_DISABLED is not set
#
# NimBLE Options
#
CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_BT_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_NONE is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_ERROR is not set
# CONFIG_BT_NIMBLE_LOG_LEVEL_WARNING is not set
CONFIG_BT_NIMBLE_LOG_LEVEL_INFO=y
# CONFIG_BT_NIMBLE_LOG_LEVEL_DEBUG is not set
CONFIG_BT_NIMBLE_LOG_LEVEL=1
CONFIG_BT_NIMBLE_MAX_CONNECTIONS=3
CONFIG_BT_NIMBLE_MAX_BONDS=3
CONFIG_BT_NIMBLE_MAX_CCCDS=8
CONFIG_BT_NIMBLE_L2CAP_COC_MAX_NUM=0
# CONFIG_BT_NIMBLE_PINNED_TO_CORE_0 is not set
CONFIG_BT_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_BT_NIMBLE_PINNED_TO_CORE=1
CONFIG_BT_NIMBLE_HOST_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_ROLE_CENTRAL=y
CONFIG_BT_NIMBLE_ROLE_PERIPHERAL=y
CONFIG_BT_NIMBLE_ROLE_BROADCASTER=y
CONFIG_BT_NIMBLE_ROLE_OBSERVER=y
CONFIG_BT_NIMBLE_GATT_CLIENT=y
CONFIG_BT_NIMBLE_GATT_SERVER=y
# CONFIG_BT_NIMBLE_NVS_PERSIST is not set
# CONFIG_BT_NIMBLE_SMP_ID_RESET is not set
CONFIG_BT_NIMBLE_SECURITY_ENABLE=y
CONFIG_BT_NIMBLE_SM_LEGACY=y
CONFIG_BT_NIMBLE_SM_SC=y
# CONFIG_BT_NIMBLE_SM_SC_DEBUG_KEYS is not set
CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_ENCRYPTION=y
CONFIG_BT_NIMBLE_SM_LVL=0
CONFIG_BT_NIMBLE_SM_SC_ONLY=0
CONFIG_BT_NIMBLE_PRINT_ERR_NAME=y
# CONFIG_BT_NIMBLE_DEBUG is not set
# CONFIG_BT_NIMBLE_DYNAMIC_SERVICE is not set
CONFIG_BT_NIMBLE_SVC_GAP_DEVICE_NAME="nimble"
CONFIG_BT_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31
CONFIG_BT_NIMBLE_ATT_PREFERRED_MTU=256
CONFIG_BT_NIMBLE_ATT_MAX_PREP_ENTRIES=64
CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=0
#
# Memory Settings
#
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_MSYS_1_BLOCK_SIZE=256
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_COUNT=24
CONFIG_BT_NIMBLE_MSYS_2_BLOCK_SIZE=320
CONFIG_BT_NIMBLE_TRANSPORT_ACL_FROM_LL_COUNT=24
CONFIG_BT_NIMBLE_TRANSPORT_ACL_SIZE=255
CONFIG_BT_NIMBLE_TRANSPORT_EVT_SIZE=70
CONFIG_BT_NIMBLE_TRANSPORT_EVT_COUNT=30
CONFIG_BT_NIMBLE_TRANSPORT_EVT_DISCARD_COUNT=8
CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT=1
# end of Memory Settings
CONFIG_BT_NIMBLE_GATT_MAX_PROCS=4
# CONFIG_BT_NIMBLE_HS_FLOW_CTRL is not set
CONFIG_BT_NIMBLE_RPA_TIMEOUT=900
# CONFIG_BT_NIMBLE_MESH is not set
CONFIG_BT_NIMBLE_CRYPTO_STACK_MBEDTLS=y
CONFIG_BT_NIMBLE_HS_STOP_TIMEOUT_MS=2000
CONFIG_BT_NIMBLE_ENABLE_CONN_REATTEMPT=y
CONFIG_BT_NIMBLE_MAX_CONN_REATTEMPT=3
# CONFIG_BT_NIMBLE_HANDLE_REPEAT_PAIRING_DELETION is not set
CONFIG_BT_NIMBLE_50_FEATURE_SUPPORT=y
CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_2M_PHY=y
CONFIG_BT_NIMBLE_LL_CFG_FEAT_LE_CODED_PHY=y
# CONFIG_BT_NIMBLE_EXT_ADV is not set
CONFIG_BT_NIMBLE_EXT_SCAN=y
CONFIG_BT_NIMBLE_ENABLE_PERIODIC_SYNC=y
CONFIG_BT_NIMBLE_MAX_PERIODIC_SYNCS=0
# CONFIG_BT_NIMBLE_GATT_CACHING is not set
# CONFIG_BT_NIMBLE_INCL_SVC_DISCOVERY is not set
CONFIG_BT_NIMBLE_WHITELIST_SIZE=12
# CONFIG_BT_NIMBLE_TEST_THROUGHPUT_TEST is not set
# CONFIG_BT_NIMBLE_BLUFI_ENABLE is not set
CONFIG_BT_NIMBLE_USE_ESP_TIMER=y
CONFIG_BT_NIMBLE_LEGACY_VHCI_ENABLE=y
# CONFIG_BT_NIMBLE_BLE_GATT_BLOB_TRANSFER is not set
#
# BLE Services
#
CONFIG_BT_NIMBLE_PROX_SERVICE=y
CONFIG_BT_NIMBLE_ANS_SERVICE=y
CONFIG_BT_NIMBLE_CTS_SERVICE=y
CONFIG_BT_NIMBLE_HTP_SERVICE=y
CONFIG_BT_NIMBLE_IPSS_SERVICE=y
CONFIG_BT_NIMBLE_TPS_SERVICE=y
CONFIG_BT_NIMBLE_IAS_SERVICE=y
CONFIG_BT_NIMBLE_LLS_SERVICE=y
CONFIG_BT_NIMBLE_SPS_SERVICE=y
CONFIG_BT_NIMBLE_HR_SERVICE=y
# CONFIG_BT_NIMBLE_HID_SERVICE is not set
CONFIG_BT_NIMBLE_BAS_SERVICE=y
# CONFIG_BT_NIMBLE_SVC_BAS_BATTERY_LEVEL_NOTIFY is not set
CONFIG_BT_NIMBLE_DIS_SERVICE=y
# CONFIG_BT_NIMBLE_SVC_DIS_MANUFACTURER_NAME is not set
# CONFIG_BT_NIMBLE_SVC_DIS_SERIAL_NUMBER is not set
# CONFIG_BT_NIMBLE_SVC_DIS_HARDWARE_REVISION is not set
# CONFIG_BT_NIMBLE_SVC_DIS_FIRMWARE_REVISION is not set
# CONFIG_BT_NIMBLE_SVC_DIS_SOFTWARE_REVISION is not set
# CONFIG_BT_NIMBLE_SVC_DIS_SYSTEM_ID is not set
# CONFIG_BT_NIMBLE_SVC_DIS_PNP_ID is not set
# CONFIG_BT_NIMBLE_SVC_DIS_INCLUDED is not set
CONFIG_BT_NIMBLE_GAP_SERVICE=y
#
# GAP Appearance write permissions
#
# CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE is not set
CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM=0
CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM_ENC=0
CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM_ATHN=0
CONFIG_BT_NIMBLE_SVC_GAP_APPEAR_WRITE_PERM_ATHR=0
# end of GAP Appearance write permissions
CONFIG_BT_NIMBLE_SVC_GAP_CAR_CHAR_NOT_SUPP=y
# CONFIG_BT_NIMBLE_SVC_GAP_CAR_NOT_SUPP is not set
# CONFIG_BT_NIMBLE_SVC_GAP_CAR_SUPP is not set
CONFIG_BT_NIMBLE_SVC_GAP_CENT_ADDR_RESOLUTION=-1
#
# GAP device name write permissions
#
# CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE is not set
# end of GAP device name write permissions
#
# Peripheral Preferred Connection Parameters (PPCP) settings
#
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MAX_CONN_INTERVAL=0
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_MIN_CONN_INTERVAL=0
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SLAVE_LATENCY=0
CONFIG_BT_NIMBLE_SVC_GAP_PPCP_SUPERVISION_TMO=0
# end of Peripheral Preferred Connection Parameters (PPCP) settings
CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM=0
CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_ENC=0
CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHEN=0
CONFIG_BT_NIMBLE_SVC_GAP_NAME_WRITE_PERM_AUTHOR=0
# CONFIG_BT_NIMBLE_SVC_GAP_GATT_SECURITY_LEVEL is not set
# CONFIG_BT_NIMBLE_SVC_GAP_RPA_ONLY is not set
# end of BLE Services
# CONFIG_BT_NIMBLE_VS_SUPPORT is not set
# CONFIG_BT_NIMBLE_ENC_ADV_DATA is not set
# CONFIG_BT_NIMBLE_HIGH_DUTY_ADV_ITVL is not set
# CONFIG_BT_NIMBLE_HOST_ALLOW_CONNECT_WITH_SCAN is not set
# CONFIG_BT_NIMBLE_HOST_QUEUE_CONG_CHECK is not set
# CONFIG_BT_NIMBLE_GATTC_PROC_PREEMPTION_PROTECT is not set
# CONFIG_BT_NIMBLE_GATTC_AUTO_PAIR is not set
#
# Host-controller Transport
#
CONFIG_UART_HW_FLOWCTRL_DISABLE=y
# CONFIG_UART_HW_FLOWCTRL_CTS_RTS is not set
CONFIG_BT_NIMBLE_HCI_UART_FLOW_CTRL=0
CONFIG_BT_NIMBLE_HCI_UART_RTS_PIN=19
CONFIG_BT_NIMBLE_HCI_UART_CTS_PIN=23
# end of Host-controller Transport
CONFIG_BT_NIMBLE_EATT_CHAN_NUM=0
# CONFIG_BT_NIMBLE_SUBRATE is not set
# end of NimBLE Options
#
# Controller Options
#
CONFIG_BT_CTRL_MODE_EFF=1
CONFIG_BT_CTRL_BLE_MAX_ACT=6
CONFIG_BT_CTRL_BLE_MAX_ACT_EFF=6
CONFIG_BT_CTRL_BLE_STATIC_ACL_TX_BUF_NB=0
CONFIG_BT_CTRL_PINNED_TO_CORE_0=y
# CONFIG_BT_CTRL_PINNED_TO_CORE_1 is not set
CONFIG_BT_CTRL_PINNED_TO_CORE=0
CONFIG_BT_CTRL_HCI_MODE_VHCI=y
# CONFIG_BT_CTRL_HCI_MODE_UART_H4 is not set
CONFIG_BT_CTRL_HCI_TL=1
CONFIG_BT_CTRL_ADV_DUP_FILT_MAX=30
CONFIG_BT_BLE_CCA_MODE_NONE=y
# CONFIG_BT_BLE_CCA_MODE_HW is not set
# CONFIG_BT_BLE_CCA_MODE_SW is not set
CONFIG_BT_BLE_CCA_MODE=0
CONFIG_BT_CTRL_HW_CCA_VAL=75
CONFIG_BT_CTRL_HW_CCA_EFF=0
CONFIG_BT_CTRL_CE_LENGTH_TYPE_ORIG=y
# CONFIG_BT_CTRL_CE_LENGTH_TYPE_CE is not set
# CONFIG_BT_CTRL_CE_LENGTH_TYPE_SD is not set
CONFIG_BT_CTRL_CE_LENGTH_TYPE_EFF=0
CONFIG_BT_CTRL_TX_ANTENNA_INDEX_0=y
# CONFIG_BT_CTRL_TX_ANTENNA_INDEX_1 is not set
CONFIG_BT_CTRL_TX_ANTENNA_INDEX_EFF=0
CONFIG_BT_CTRL_RX_ANTENNA_INDEX_0=y
# CONFIG_BT_CTRL_RX_ANTENNA_INDEX_1 is not set
CONFIG_BT_CTRL_RX_ANTENNA_INDEX_EFF=0
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N24 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N21 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N18 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N15 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N12 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N9 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N6 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N3 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_N0 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P3 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P6 is not set
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P9=y
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P12 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P15 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P18 is not set
# CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_P20 is not set
CONFIG_BT_CTRL_DFT_TX_POWER_LEVEL_EFF=11
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_SUPP=y
CONFIG_BT_CTRL_BLE_ADV_REPORT_FLOW_CTRL_NUM=100
CONFIG_BT_CTRL_BLE_ADV_REPORT_DISCARD_THRSHOLD=20
CONFIG_BT_CTRL_BLE_SCAN_DUPL=y
CONFIG_BT_CTRL_SCAN_DUPL_TYPE_DEVICE=y
# CONFIG_BT_CTRL_SCAN_DUPL_TYPE_DATA is not set
# CONFIG_BT_CTRL_SCAN_DUPL_TYPE_DATA_DEVICE is not set
CONFIG_BT_CTRL_SCAN_DUPL_TYPE=0
CONFIG_BT_CTRL_SCAN_DUPL_CACHE_SIZE=100
CONFIG_BT_CTRL_DUPL_SCAN_CACHE_REFRESH_PERIOD=0
# CONFIG_BT_CTRL_BLE_MESH_SCAN_DUPL_EN is not set
# CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EN is not set
CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_DIS=y
CONFIG_BT_CTRL_COEX_PHY_CODED_TX_RX_TLIM_EFF=0
#
# MODEM SLEEP Options
#
# CONFIG_BT_CTRL_MODEM_SLEEP is not set
# end of MODEM SLEEP Options
CONFIG_BT_CTRL_SLEEP_MODE_EFF=0
CONFIG_BT_CTRL_SLEEP_CLOCK_EFF=0
CONFIG_BT_CTRL_HCI_TL_EFF=1
# CONFIG_BT_CTRL_AGC_RECORRECT_EN is not set
# CONFIG_BT_CTRL_SCAN_BACKOFF_UPPERLIMITMAX is not set
# CONFIG_BT_BLE_ADV_DATA_LENGTH_ZERO_AUX is not set
CONFIG_BT_CTRL_CHAN_ASS_EN=y
CONFIG_BT_CTRL_LE_PING_EN=y
#
# BLE disconnects when Instant Passed (0x28) occurs
#
# CONFIG_BT_CTRL_BLE_LLCP_CONN_UPDATE is not set
# CONFIG_BT_CTRL_BLE_LLCP_CHAN_MAP_UPDATE is not set
# CONFIG_BT_CTRL_BLE_LLCP_PHY_UPDATE is not set
# end of BLE disconnects when Instant Passed (0x28) occurs
# CONFIG_BT_CTRL_RUN_IN_FLASH_ONLY is not set
CONFIG_BT_CTRL_DTM_ENABLE=y
CONFIG_BT_CTRL_BLE_MASTER=y
# CONFIG_BT_CTRL_BLE_TEST is not set
CONFIG_BT_CTRL_BLE_SCAN=y
CONFIG_BT_CTRL_BLE_SECURITY_ENABLE=y
CONFIG_BT_CTRL_BLE_ADV=y
# CONFIG_BT_CTRL_CHECK_CONNECT_IND_ACCESS_ADDRESS is not set
#
# Controller debug log Options (Experimental)
#
# end of Controller debug log Options (Experimental)
# end of Controller Options
# #
# Common Options # Common Options
# #
CONFIG_BT_ALARM_MAX_NUM=50
# CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set # CONFIG_BT_BLE_LOG_SPI_OUT_ENABLED is not set
# CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set # CONFIG_BT_BLE_LOG_UHCI_OUT_ENABLED is not set
# end of Common Options # end of Common Options
# CONFIG_BT_HCI_LOG_DEBUG_EN is not set
# end of Bluetooth # end of Bluetooth
# CONFIG_BLE_MESH is not set
# #
# Console Library # Console Library
# #
@@ -789,7 +1069,8 @@ CONFIG_ESP_TLS_DYN_BUF_STRATEGY_SUPPORTED=y
# Wireless Coexistence # Wireless Coexistence
# #
CONFIG_ESP_COEX_ENABLED=y CONFIG_ESP_COEX_ENABLED=y
# CONFIG_ESP_COEX_EXTERNAL_COEXIST_ENABLE is not set CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
# CONFIG_ESP_COEX_POWER_MANAGEMENT is not set
# CONFIG_ESP_COEX_GPIO_DEBUG is not set # CONFIG_ESP_COEX_GPIO_DEBUG is not set
# end of Wireless Coexistence # end of Wireless Coexistence
@@ -2233,6 +2514,11 @@ CONFIG_WL_SECTOR_SIZE=4096
# #
CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16 CONFIG_WIFI_PROV_SCAN_MAX_ENTRIES=16
CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30 CONFIG_WIFI_PROV_AUTOSTOP_TIMEOUT=30
# CONFIG_WIFI_PROV_BLE_BONDING is not set
CONFIG_WIFI_PROV_BLE_SEC_CONN=y
# CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION is not set
# CONFIG_WIFI_PROV_BLE_NOTIFY is not set
# CONFIG_WIFI_PROV_KEEP_BLE_ON_AFTER_PROV is not set
CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y CONFIG_WIFI_PROV_STA_ALL_CHANNEL_SCAN=y
# CONFIG_WIFI_PROV_STA_FAST_SCAN is not set # CONFIG_WIFI_PROV_STA_FAST_SCAN is not set
# end of Wi-Fi Provisioning Manager # end of Wi-Fi Provisioning Manager
@@ -2275,8 +2561,48 @@ CONFIG_STACK_CHECK_NONE=y
# CONFIG_ESP32_APPTRACE_DEST_TRAX is not set # CONFIG_ESP32_APPTRACE_DEST_TRAX is not set
CONFIG_ESP32_APPTRACE_DEST_NONE=y CONFIG_ESP32_APPTRACE_DEST_NONE=y
CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y CONFIG_ESP32_APPTRACE_LOCK_ENABLE=y
# CONFIG_EXTERNAL_COEX_ENABLE is not set # CONFIG_BLUEDROID_ENABLED is not set
# CONFIG_ESP_WIFI_EXTERNAL_COEXIST_ENABLE is not set CONFIG_NIMBLE_ENABLED=y
CONFIG_NIMBLE_MEM_ALLOC_MODE_INTERNAL=y
# CONFIG_NIMBLE_MEM_ALLOC_MODE_DEFAULT is not set
CONFIG_NIMBLE_MAX_CONNECTIONS=3
CONFIG_NIMBLE_MAX_BONDS=3
CONFIG_NIMBLE_MAX_CCCDS=8
CONFIG_NIMBLE_L2CAP_COC_MAX_NUM=0
# CONFIG_NIMBLE_PINNED_TO_CORE_0 is not set
CONFIG_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_NIMBLE_PINNED_TO_CORE=1
CONFIG_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_BT_NIMBLE_TASK_STACK_SIZE=4096
CONFIG_NIMBLE_ROLE_CENTRAL=y
CONFIG_NIMBLE_ROLE_PERIPHERAL=y
CONFIG_NIMBLE_ROLE_BROADCASTER=y
CONFIG_NIMBLE_ROLE_OBSERVER=y
# CONFIG_NIMBLE_NVS_PERSIST is not set
CONFIG_NIMBLE_SM_LEGACY=y
CONFIG_NIMBLE_SM_SC=y
# CONFIG_NIMBLE_SM_SC_DEBUG_KEYS is not set
CONFIG_BT_NIMBLE_SM_SC_LVL=0
# CONFIG_NIMBLE_DEBUG is not set
CONFIG_NIMBLE_SVC_GAP_DEVICE_NAME="nimble"
CONFIG_NIMBLE_GAP_DEVICE_NAME_MAX_LEN=31
CONFIG_NIMBLE_ATT_PREFERRED_MTU=256
CONFIG_NIMBLE_SVC_GAP_APPEARANCE=0
CONFIG_BT_NIMBLE_MSYS1_BLOCK_COUNT=12
CONFIG_BT_NIMBLE_ACL_BUF_COUNT=24
CONFIG_BT_NIMBLE_ACL_BUF_SIZE=255
CONFIG_BT_NIMBLE_HCI_EVT_BUF_SIZE=70
CONFIG_BT_NIMBLE_HCI_EVT_HI_BUF_COUNT=30
CONFIG_BT_NIMBLE_HCI_EVT_LO_BUF_COUNT=8
# CONFIG_NIMBLE_HS_FLOW_CTRL is not set
CONFIG_NIMBLE_RPA_TIMEOUT=900
# CONFIG_NIMBLE_MESH is not set
CONFIG_NIMBLE_CRYPTO_STACK_MBEDTLS=y
# CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_EN is not set
CONFIG_BT_NIMBLE_COEX_PHY_CODED_TX_RX_TLIM_DIS=y
CONFIG_SW_COEXIST_ENABLE=y
CONFIG_ESP32_WIFI_SW_COEXIST_ENABLE=y
CONFIG_ESP_WIFI_SW_COEXIST_ENABLE=y
# CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set # CONFIG_CAM_CTLR_DVP_CAM_ISR_IRAM_SAFE is not set
# CONFIG_GPTIMER_ISR_IRAM_SAFE is not set # CONFIG_GPTIMER_ISR_IRAM_SAFE is not set
# CONFIG_MCPWM_ISR_IRAM_SAFE is not set # CONFIG_MCPWM_ISR_IRAM_SAFE is not set
+8
View File
@@ -0,0 +1,8 @@
# BLE host transport (NimBLE) + WiFi/ESP-NOW coexistence
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_ESP_COEX_SW_COEXIST_ENABLE=y
# Pin stacks to different cores (WiFi stays on core 0)
CONFIG_BT_NIMBLE_PINNED_TO_CORE_1=y
CONFIG_ESP_WIFI_TASK_PINNED_TO_CORE_0=y