Enable host OTA over BLE and harden ESP-NOW slave distribution.

Allow the same staged firmware upload path on BLE as UART, fix distribute stack/event-group crashes, and recover sticky slave OTA sessions instead of failing with err=4.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-11-01 20:42:14 +01:00
co-authored by Cursor
parent 567dab6d0a
commit ae8ef40820
16 changed files with 400 additions and 208 deletions
+79 -4
View File
@@ -26,6 +26,7 @@ type bleLink struct {
respCh chan []byte
streamCh chan []byte
quiet bool
attMTU int // negotiated ATT MTU (0 if unknown)
}
func openBLE(name string, settle time.Duration) (*bleLink, error) {
@@ -125,19 +126,23 @@ func connectBLEDevice(addr bluetooth.Address, settle time.Duration) (*bleLink, e
}
char := chars[0]
attMTU := 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)
attMTU = int(mtu)
log.Printf("BLE ATT MTU=%d", attMTU)
if attMTU < otaMinATTMTU {
log.Printf("warning: MTU %d < %d — OTA (200 B chunks) and large stream ticks may fail",
attMTU, otaMinATTMTU)
}
}
link := &bleLink{
device: device,
device: device,
char: char,
parser: uartframe.NewParser(),
respCh: make(chan []byte, 8),
streamCh: make(chan []byte, 64),
attMTU: attMTU,
}
if err := char.EnableNotifications(func(buf []byte) {
@@ -277,3 +282,73 @@ func (b *bleLink) NextStreamTick(timeout time.Duration) ([]byte, error) {
return nil, fmt.Errorf("BLE stream tick timeout")
}
}
// ensureOTAMTU rejects OTA when the negotiated ATT MTU cannot carry 200 B chunks.
func (b *bleLink) ensureOTAMTU() error {
mtu := b.attMTU
if mtu <= 0 {
if v, err := b.char.GetMTU(); err == nil {
mtu = int(v)
b.attMTU = mtu
}
}
if mtu < otaMinATTMTU {
return fmt.Errorf("BLE ATT MTU %d < %d required for OTA (negotiate larger MTU)", mtu, otaMinATTMTU)
}
maxWrite := mtu - 3
if maxWrite < 216 {
return fmt.Errorf("BLE max write %d < 216 needed for 200 B OTA frames", maxWrite)
}
return nil
}
func (b *bleLink) writeMsg(msg *pb.UartMessage) error {
b.mu.Lock()
defer b.mu.Unlock()
frame, err := encodeUartMessage(msg)
if err != nil {
return err
}
if b.attMTU > 0 && len(frame) > b.attMTU-3 {
return fmt.Errorf("OTA frame %d exceeds BLE max write %d (MTU=%d)",
len(frame), b.attMTU-3, b.attMTU)
}
if _, err := b.char.WriteWithoutResponse(frame); err != nil {
return fmt.Errorf("BLE write: %w", err)
}
return nil
}
func (b *bleLink) readFrame(timeout time.Duration) ([]byte, error) {
if timeout <= 0 {
timeout = readTimeout
}
timer := time.NewTimer(timeout)
defer timer.Stop()
select {
case p := <-b.respCh:
return p, nil
case <-timer.C:
return nil, fmt.Errorf("BLE read timeout")
}
}
func (b *bleLink) flushInput() {
for {
select {
case <-b.respCh:
default:
goto streams
}
}
streams:
for {
select {
case <-b.streamCh:
default:
return
}
}
}
func (b *bleLink) chunkPace() time.Duration { return otaHostChunkPaceBLE }