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>
This commit is contained in:
2026-10-23 23:27:39 +02:00
co-authored by Cursor
parent aa67edfe94
commit af20635047
14 changed files with 893 additions and 293 deletions
+49 -10
View File
@@ -175,7 +175,17 @@ func (b *bleLink) onNotify(buf []byte) {
select {
case b.respCh <- payload:
default:
log.Printf("BLE: response channel full, dropping frame type=0x%02x", payload[0])
// 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])
}
}
}
}
@@ -189,9 +199,21 @@ func (b *bleLink) exchange(cmdID byte, cmdName string) ([]byte, error) {
}
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)
@@ -212,16 +234,33 @@ sent:
return nil, fmt.Errorf("BLE write: %w (check DIP8=BLE)", err)
}
timer := time.NewTimer(readTimeout)
defer timer.Stop()
select {
case resp := <-b.respCh:
if !b.quiet {
log.Printf("BLE response (%d bytes)", len(resp))
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)
}
return resp, nil
case <-timer.C:
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
}
}