Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
141176a824 | ||
|
|
9046677460 |
@@ -109,7 +109,7 @@ sequenceDiagram
|
|||||||
└─────────────────────────────────────────────────────────────┘
|
└─────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
**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** über UART oder BLE (ATT MTU ≥ ~219). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (10–16 ms, Default 10; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
|
**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; Frames größer als ATT MTU-3 werden in aufeinanderfolgende Notifies gestückelt (Host-Parser reassembliert). `gotool serve` verbindet UART oder BLE über Dashboard/REST/WS (`managedHost`); **OTA** über UART oder BLE (ATT MTU ≥ ~219, preferred 517). Bei BLE-Host und aktivem Accel-Stream pusht `ble_stream` periodisch `BLE_STREAM_TICK` (10–16 ms, Default 10; pausiert während OTA) mit `esp_timestamp_us` (Live-Pfad bevorzugt Tick statt CACHE_STATUS-Poll).
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -225,12 +225,12 @@ Unabhängig von Protobuf — reines Bytestream-Framing:
|
|||||||
| Feld | Wert |
|
| Feld | Wert |
|
||||||
|------|------|
|
|------|------|
|
||||||
| Start | `0xAA` |
|
| Start | `0xAA` |
|
||||||
| Länge | 1 Byte (1–252) |
|
| Länge | 2 Bytes little-endian (1–1024) |
|
||||||
| Payload | `length` Bytes |
|
| Payload | `length` Bytes |
|
||||||
| Prüfsumme | XOR aller Payload-Bytes |
|
| Prüfsumme | XOR aller Payload-Bytes |
|
||||||
| Stopp | `0xCC` |
|
| Stopp | `0xCC` |
|
||||||
|
|
||||||
**Parameter:** `main/uart.h` — `UART_NUM_1`, `921600` Baud, TX GPIO **2**, RX GPIO **3**, `MAX_PAYLOAD_SIZE` 248.
|
**Parameter:** `main/uart.h` — `UART_NUM_1`, `921600` Baud, TX GPIO **2**, RX GPIO **3**, `MAX_PAYLOAD_SIZE` 1024.
|
||||||
|
|
||||||
### 5.2 Nutzlast (Anwendung)
|
### 5.2 Nutzlast (Anwendung)
|
||||||
|
|
||||||
|
|||||||
@@ -124,10 +124,10 @@ idf.py -p /dev/ttyUSB0 flash monitor
|
|||||||
| Byte | Inhalt |
|
| Byte | Inhalt |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| 0 | `0xAA` Start |
|
| 0 | `0xAA` Start |
|
||||||
| 1 | Länge N (1–252) |
|
| 1–2 | Länge N little-endian (1–1024) |
|
||||||
| 2…N+1 | Payload |
|
| 3…N+2 | Payload |
|
||||||
| N+2 | XOR-Checksum über Payload |
|
| N+3 | XOR-Checksum über Payload |
|
||||||
| N+3 | `0xCC` Stopp |
|
| N+4 | `0xCC` Stopp |
|
||||||
|
|
||||||
Implementierung: `uart.c` — `parse_uart_byte()`, `uart_send_framed()`.
|
Implementierung: `uart.c` — `parse_uart_byte()`, `uart_send_framed()`.
|
||||||
|
|
||||||
@@ -370,7 +370,7 @@ Implementierung: `ota_uart.c`, Steuerung `cmd/cmd_ota.c`. Gleiches Protokoll üb
|
|||||||
|
|
||||||
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
|
- Inaktive Partition: `esp_ota_get_next_update_partition()`.
|
||||||
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
|
- LED-Ring zeigt Fortschritt (blau Schreiben, grün Verteilung).
|
||||||
- BLE: ATT MTU ≥ ~219 (preferred 247); `BLE_STREAM_TICK` pausiert während `ota_session_busy()`.
|
- BLE: ATT MTU ≥ ~219 (preferred 517); große Antworten werden über mehrere Notifies gestückelt; `BLE_STREAM_TICK` pausiert während `ota_session_busy()`.
|
||||||
- **App-Rollback** (`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`): neues Image bootet als `PENDING_VERIFY`. `ota_confirm` markiert nach ~8 s stabilem Lauf `mark_app_valid`; Crash/Reset davor → Bootloader rollt auf die vorherige App zurück. Während `PENDING_VERIFY` ist ein neues `OTA_START` blockiert.
|
- **App-Rollback** (`CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE`): neues Image bootet als `PENDING_VERIFY`. `ota_confirm` markiert nach ~8 s stabilem Lauf `mark_app_valid`; Crash/Reset davor → Bootloader rollt auf die vorherige App zurück. Während `PENDING_VERIFY` ist ein neues `OTA_START` blockiert.
|
||||||
|
|
||||||
### 8.2 ESP-NOW-OTA (Master → Slaves)
|
### 8.2 ESP-NOW-OTA (Master → Slaves)
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ Entspricht `main/README.md`:
|
|||||||
|
|
||||||
| Byte | Inhalt |
|
| Byte | Inhalt |
|
||||||
|------|--------|
|
|------|--------|
|
||||||
| Frame | `0xAA`, Länge, Payload, XOR-Checksum, `0xCC` |
|
| Frame | `0xAA`, Länge (uint16 LE), Payload, XOR-Checksum, `0xCC` |
|
||||||
| Payload[0] | `MessageType` (z.B. `22` = FIND_ME) |
|
| Payload[0] | `MessageType` (z.B. `22` = FIND_ME) |
|
||||||
| Payload[1…] | Nanopb-`UartMessage` |
|
| Payload[1…] | Nanopb-`UartMessage` |
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type bleLink struct {
|
|||||||
device bluetooth.Device
|
device bluetooth.Device
|
||||||
char bluetooth.DeviceCharacteristic
|
char bluetooth.DeviceCharacteristic
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
|
parserMu sync.Mutex
|
||||||
parser *uartframe.Parser
|
parser *uartframe.Parser
|
||||||
respCh chan []byte
|
respCh chan []byte
|
||||||
streamCh chan []byte
|
streamCh chan []byte
|
||||||
@@ -162,6 +163,8 @@ func connectBLEDevice(addr bluetooth.Address, settle time.Duration) (*bleLink, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *bleLink) onNotify(buf []byte) {
|
func (b *bleLink) onNotify(buf []byte) {
|
||||||
|
b.parserMu.Lock()
|
||||||
|
defer b.parserMu.Unlock()
|
||||||
for _, by := range buf {
|
for _, by := range buf {
|
||||||
payload, ok, err := b.parser.Feed(by)
|
payload, ok, err := b.parser.Feed(by)
|
||||||
if err != nil || !ok {
|
if err != nil || !ok {
|
||||||
@@ -195,6 +198,12 @@ func (b *bleLink) onNotify(buf []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *bleLink) resetParser() {
|
||||||
|
b.parserMu.Lock()
|
||||||
|
b.parser = uartframe.NewParser()
|
||||||
|
b.parserMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
func (b *bleLink) Close() error {
|
func (b *bleLink) Close() error {
|
||||||
return b.device.Disconnect()
|
return b.device.Disconnect()
|
||||||
}
|
}
|
||||||
@@ -243,6 +252,9 @@ sent:
|
|||||||
for {
|
for {
|
||||||
remain := time.Until(deadline)
|
remain := time.Until(deadline)
|
||||||
if remain <= 0 {
|
if remain <= 0 {
|
||||||
|
// Drop a half-parsed frame (e.g. aborted multi-notify) so the next
|
||||||
|
// exchange does not treat a new 0xAA as payload data.
|
||||||
|
b.resetParser()
|
||||||
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
|
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
|
||||||
}
|
}
|
||||||
timer := time.NewTimer(remain)
|
timer := time.NewTimer(remain)
|
||||||
@@ -264,6 +276,7 @@ sent:
|
|||||||
}
|
}
|
||||||
return resp, nil
|
return resp, nil
|
||||||
case <-timer.C:
|
case <-timer.C:
|
||||||
|
b.resetParser()
|
||||||
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
|
return nil, fmt.Errorf("BLE read timeout for %s (check DIP8=BLE / -ble)", cmdName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-11
@@ -10,8 +10,10 @@ import (
|
|||||||
const (
|
const (
|
||||||
StartMarker = 0xAA
|
StartMarker = 0xAA
|
||||||
StopMarker = 0xCC
|
StopMarker = 0xCC
|
||||||
// Must match main/uart.h MAX_PAYLOAD_SIZE (MAX_BUF_SIZE - 4).
|
// Must match main/uart.h MAX_PAYLOAD_SIZE.
|
||||||
MaxPayload = 248
|
MaxPayload = 1024
|
||||||
|
// AA | len_lo | len_hi | payload | xor | CC
|
||||||
|
FrameOverhead = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -19,14 +21,15 @@ var (
|
|||||||
ErrTimeout = errors.New("read timeout")
|
ErrTimeout = errors.New("read timeout")
|
||||||
)
|
)
|
||||||
|
|
||||||
// EncodeFrame builds a framed packet: 0xAA len payload xor(payload) 0xCC.
|
// EncodeFrame builds a framed packet: 0xAA len_lo len_hi payload xor(payload) 0xCC.
|
||||||
func EncodeFrame(payload []byte) ([]byte, error) {
|
func EncodeFrame(payload []byte) ([]byte, error) {
|
||||||
if len(payload) == 0 || len(payload) > MaxPayload {
|
if len(payload) == 0 || len(payload) > MaxPayload {
|
||||||
return nil, fmt.Errorf("payload length %d out of range 1..%d", len(payload), MaxPayload)
|
return nil, fmt.Errorf("payload length %d out of range 1..%d", len(payload), MaxPayload)
|
||||||
}
|
}
|
||||||
|
|
||||||
frame := make([]byte, 0, 4+len(payload))
|
n := len(payload)
|
||||||
frame = append(frame, StartMarker, byte(len(payload)))
|
frame := make([]byte, 0, FrameOverhead+n)
|
||||||
|
frame = append(frame, StartMarker, byte(n&0xff), byte((n>>8)&0xff))
|
||||||
var checksum byte
|
var checksum byte
|
||||||
for _, b := range payload {
|
for _, b := range payload {
|
||||||
frame = append(frame, b)
|
frame = append(frame, b)
|
||||||
@@ -39,6 +42,7 @@ func EncodeFrame(payload []byte) ([]byte, error) {
|
|||||||
type Parser struct {
|
type Parser struct {
|
||||||
state int
|
state int
|
||||||
len int
|
len int
|
||||||
|
lenLo byte
|
||||||
payload []byte
|
payload []byte
|
||||||
index int
|
index int
|
||||||
checksum byte
|
checksum byte
|
||||||
@@ -46,7 +50,8 @@ type Parser struct {
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
stateStart = iota
|
stateStart = iota
|
||||||
stateLen
|
stateLenLo
|
||||||
|
stateLenHi
|
||||||
stateData
|
stateData
|
||||||
stateChecksum
|
stateChecksum
|
||||||
stateStop
|
stateStop
|
||||||
@@ -63,14 +68,19 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
|
|||||||
if b == StartMarker {
|
if b == StartMarker {
|
||||||
p.index = 0
|
p.index = 0
|
||||||
p.checksum = 0
|
p.checksum = 0
|
||||||
p.state = stateLen
|
p.state = stateLenLo
|
||||||
}
|
}
|
||||||
case stateLen:
|
case stateLenLo:
|
||||||
if b == 0 || b > MaxPayload {
|
p.lenLo = b
|
||||||
|
p.state = stateLenHi
|
||||||
|
case stateLenHi:
|
||||||
|
n := int(p.lenLo) | (int(b) << 8)
|
||||||
|
if n == 0 || n > MaxPayload {
|
||||||
p.state = stateStart
|
p.state = stateStart
|
||||||
} else {
|
} else {
|
||||||
p.len = int(b)
|
p.len = n
|
||||||
p.payload = make([]byte, p.len)
|
p.payload = make([]byte, p.len)
|
||||||
|
p.index = 0
|
||||||
p.state = stateData
|
p.state = stateData
|
||||||
}
|
}
|
||||||
case stateData:
|
case stateData:
|
||||||
@@ -102,7 +112,7 @@ func (p *Parser) Feed(b byte) (payload []byte, ok bool, err error) {
|
|||||||
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
|
// maxWait bounds total wait time; zero means no limit (serial read timeouts retry forever).
|
||||||
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
|
func ReadFrame(r io.Reader, buf []byte, maxWait time.Duration) ([]byte, error) {
|
||||||
if buf == nil {
|
if buf == nil {
|
||||||
buf = make([]byte, 256)
|
buf = make([]byte, 1024)
|
||||||
}
|
}
|
||||||
parser := NewParser()
|
parser := NewParser()
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -166,7 +166,7 @@ Hardware: **UART1**, **921600** baud, **TX = GPIO3**, **RX = GPIO2** (adapter TX
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|-------|--------|
|
|-------|--------|
|
||||||
| Start | `0xAA` |
|
| Start | `0xAA` |
|
||||||
| Length | 1 byte, payload size 1–252 |
|
| Length | 2 bytes little-endian, payload size 1–1024 |
|
||||||
| Payload | `length` bytes |
|
| Payload | `length` bytes |
|
||||||
| Checksum | XOR of all payload bytes |
|
| Checksum | XOR of all payload bytes |
|
||||||
| Stop | `0xCC` |
|
| Stop | `0xCC` |
|
||||||
@@ -178,7 +178,7 @@ Hardware: **UART1**, **921600** baud, **TX = GPIO3**, **RX = GPIO2** (adapter TX
|
|||||||
| 0 | Command ID (`MessageType` / `msg_id`) |
|
| 0 | Command ID (`MessageType` / `msg_id`) |
|
||||||
| 1… | Arguments (handler receives bytes after ID only) |
|
| 1… | Arguments (handler receives bytes after ID only) |
|
||||||
|
|
||||||
Example VERSION request: single-byte payload `03` → frame `AA 01 03 03 CC`.
|
Example VERSION request: single-byte payload `03` → frame `AA 01 00 03 03 CC`.
|
||||||
|
|
||||||
Logging:
|
Logging:
|
||||||
|
|
||||||
|
|||||||
+85
-14
@@ -4,6 +4,7 @@
|
|||||||
#include "host_transport.h"
|
#include "host_transport.h"
|
||||||
#include "nvs_flash.h"
|
#include "nvs_flash.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
|
#include "freertos/semphr.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
#include "nimble/nimble_port.h"
|
#include "nimble/nimble_port.h"
|
||||||
@@ -22,6 +23,12 @@ static const char *DEVICE_NAME = "Powerpod";
|
|||||||
#define BLE_CONN_ITVL_MIN_UNITS 6 /* 7.5 ms */
|
#define BLE_CONN_ITVL_MIN_UNITS 6 /* 7.5 ms */
|
||||||
#define BLE_CONN_ITVL_MAX_UNITS 6
|
#define BLE_CONN_ITVL_MAX_UNITS 6
|
||||||
#define BLE_CONN_TUNE_MAX_TRIES 6
|
#define BLE_CONN_TUNE_MAX_TRIES 6
|
||||||
|
/** Prefer large ATT MTU so CLIENT_INFO / BATTERY fit in fewer notify chunks. */
|
||||||
|
#define BLE_PREFERRED_ATT_MTU 517
|
||||||
|
#define BLE_NOTIFY_CHUNK_RETRIES 24
|
||||||
|
#define BLE_NOTIFY_CHUNK_RETRY_MS 5
|
||||||
|
/** Pace multi-chunk notifies so the central parser sees contiguous frame bytes. */
|
||||||
|
#define BLE_NOTIFY_CHUNK_GAP_MS 8
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Nordic UART style:
|
* Nordic UART style:
|
||||||
@@ -43,6 +50,7 @@ static bool notify_enabled;
|
|||||||
static uint16_t att_mtu = 23;
|
static uint16_t att_mtu = 23;
|
||||||
static esp_timer_handle_t conn_tune_timer;
|
static esp_timer_handle_t conn_tune_timer;
|
||||||
static uint8_t conn_tune_tries;
|
static uint8_t conn_tune_tries;
|
||||||
|
static SemaphoreHandle_t s_tx_mu;
|
||||||
|
|
||||||
static int ble_gap_event(struct ble_gap_event *event, void *arg);
|
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_request_fast_conn_params(uint16_t handle);
|
||||||
@@ -61,28 +69,83 @@ uint16_t ble_host_max_payload(void) {
|
|||||||
return (uint16_t)(att_mtu - 3);
|
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;
|
* One ATT notification (≤ MTU-3). Retries on transient NimBLE buffer pressure.
|
||||||
int rc;
|
*/
|
||||||
|
static esp_err_t ble_notify_chunk(const uint8_t *data, uint16_t len) {
|
||||||
|
for (int attempt = 0; attempt < BLE_NOTIFY_CHUNK_RETRIES; attempt++) {
|
||||||
|
struct os_mbuf *om = ble_hs_mbuf_from_flat(data, len);
|
||||||
|
if (om == NULL) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_RETRY_MS));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rc = ble_gatts_notify_custom(conn_handle, ble_chr_val_handle, om);
|
||||||
|
if (rc == 0) {
|
||||||
|
return ESP_OK;
|
||||||
|
}
|
||||||
|
/* Ownership stays with us on failure. */
|
||||||
|
os_mbuf_free_chain(om);
|
||||||
if (!ble_host_ready()) {
|
if (!ble_host_ready()) {
|
||||||
return ESP_ERR_INVALID_STATE;
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
if (frame_len == 0 || frame_len > ble_host_max_payload()) {
|
ESP_LOGD(TAG, "notify chunk rc=%d attempt=%d len=%u", rc, attempt,
|
||||||
return ESP_ERR_INVALID_SIZE;
|
(unsigned)len);
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_RETRY_MS));
|
||||||
}
|
}
|
||||||
|
ESP_LOGW(TAG, "notify chunk gave up after retries len=%u", (unsigned)len);
|
||||||
om = ble_hs_mbuf_from_flat(frame, (uint16_t)frame_len);
|
|
||||||
if (om == NULL) {
|
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = ble_gatts_notify_custom(conn_handle, ble_chr_val_handle, om);
|
/**
|
||||||
if (rc != 0) {
|
* Send a full host frame. If larger than ATT MTU-3, split across consecutive
|
||||||
os_mbuf_free_chain(om);
|
* notifies; goTool feeds every notify into the same byte parser, so the frame
|
||||||
return ESP_FAIL;
|
* reassembles without a protocol change. Mutex keeps stream ticks from
|
||||||
|
* interleaving mid-frame.
|
||||||
|
*/
|
||||||
|
static esp_err_t ble_host_tx(const uint8_t *frame, size_t frame_len) {
|
||||||
|
if (!ble_host_ready()) {
|
||||||
|
return ESP_ERR_INVALID_STATE;
|
||||||
}
|
}
|
||||||
return ESP_OK;
|
if (frame == NULL || frame_len == 0) {
|
||||||
|
return ESP_ERR_INVALID_ARG;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t max_chunk = ble_host_max_payload();
|
||||||
|
if (max_chunk == 0) {
|
||||||
|
return ESP_ERR_INVALID_SIZE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s_tx_mu == NULL ||
|
||||||
|
xSemaphoreTake(s_tx_mu, pdMS_TO_TICKS(2000)) != pdTRUE) {
|
||||||
|
return ESP_ERR_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
esp_err_t result = ESP_OK;
|
||||||
|
size_t off = 0;
|
||||||
|
while (off < frame_len) {
|
||||||
|
if (!ble_host_ready()) {
|
||||||
|
result = ESP_ERR_INVALID_STATE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
size_t remain = frame_len - off;
|
||||||
|
uint16_t n =
|
||||||
|
(uint16_t)(remain > max_chunk ? max_chunk : remain);
|
||||||
|
result = ble_notify_chunk(&frame[off], n);
|
||||||
|
if (result != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "TX abort at %u/%u (chunk %u, mtu_pay %u)",
|
||||||
|
(unsigned)off, (unsigned)frame_len, (unsigned)n,
|
||||||
|
(unsigned)max_chunk);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
off += n;
|
||||||
|
if (off < frame_len) {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(BLE_NOTIFY_CHUNK_GAP_MS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
xSemaphoreGive(s_tx_mu);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void ble_conn_tune_timer_cb(void *arg) {
|
static void ble_conn_tune_timer_cb(void *arg) {
|
||||||
@@ -365,6 +428,12 @@ esp_err_t init_ble_host(QueueHandle_t cmd_queue) {
|
|||||||
host_transport_set_cmd_queue(cmd_queue);
|
host_transport_set_cmd_queue(cmd_queue);
|
||||||
host_transport_set_tx(ble_host_tx);
|
host_transport_set_tx(ble_host_tx);
|
||||||
|
|
||||||
|
s_tx_mu = xSemaphoreCreateMutex();
|
||||||
|
if (s_tx_mu == NULL) {
|
||||||
|
ESP_LOGE(TAG, "failed to create TX mutex");
|
||||||
|
return ESP_ERR_NO_MEM;
|
||||||
|
}
|
||||||
|
|
||||||
/* NVS may already be init via pod_settings; ignore ALREADY_INIT. */
|
/* NVS may already be init via pod_settings; ignore ALREADY_INIT. */
|
||||||
ret = nvs_flash_init();
|
ret = nvs_flash_init();
|
||||||
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
|
if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
|
||||||
@@ -404,9 +473,11 @@ esp_err_t init_ble_host(QueueHandle_t cmd_queue) {
|
|||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
rc = ble_att_set_preferred_mtu(247);
|
rc = ble_att_set_preferred_mtu(BLE_PREFERRED_ATT_MTU);
|
||||||
if (rc != 0) {
|
if (rc != 0) {
|
||||||
ESP_LOGW(TAG, "preferred MTU set failed: %d", rc);
|
ESP_LOGW(TAG, "preferred MTU set failed: %d", rc);
|
||||||
|
} else {
|
||||||
|
ESP_LOGI(TAG, "preferred ATT MTU %u", (unsigned)BLE_PREFERRED_ATT_MTU);
|
||||||
}
|
}
|
||||||
|
|
||||||
nimble_port_freertos_init(ble_host_task);
|
nimble_port_freertos_init(ble_host_task);
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ bool ble_host_ready(void);
|
|||||||
/** Negotiated ATT MTU (default 23 until exchange). */
|
/** Negotiated ATT MTU (default 23 until exchange). */
|
||||||
uint16_t ble_host_att_mtu(void);
|
uint16_t ble_host_att_mtu(void);
|
||||||
|
|
||||||
/** Max notify/write payload bytes (ATT MTU - 3). */
|
/** Max bytes per ATT notify/write (ATT MTU - 3). Frames may span multiple notifies. */
|
||||||
uint16_t ble_host_max_payload(void);
|
uint16_t ble_host_max_payload(void);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+13
-26
@@ -7,10 +7,9 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
#include "ota_session.h"
|
#include "ota_session.h"
|
||||||
#include "pb_encode.h"
|
|
||||||
#include "uart.h"
|
|
||||||
#include "uart_cmd.h"
|
#include "uart_cmd.h"
|
||||||
#include "uart_messages.pb.h"
|
#include "uart_messages.pb.h"
|
||||||
|
#include "uart_proto.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
static const char *TAG = "[BLE_STREAM]";
|
static const char *TAG = "[BLE_STREAM]";
|
||||||
@@ -102,34 +101,22 @@ static bool any_accel_stream(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static esp_err_t send_tick_truncated(alox_UartMessage *msg) {
|
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));
|
* ble_host_tx splits frames larger than ATT MTU-3 across notifies. Only
|
||||||
|
* shrink the tick if it cannot encode into MAX_PAYLOAD_SIZE.
|
||||||
|
*/
|
||||||
while (true) {
|
while (true) {
|
||||||
stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
|
esp_err_t err = uart_send_uart_message(msg);
|
||||||
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
|
if (err == ESP_OK) {
|
||||||
return ESP_FAIL;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
if (msg->payload.ble_stream_tick.clients_count == 0) {
|
||||||
ESP_LOGW(TAG, "tick too large even with 0 clients (need %u max %u)",
|
ESP_LOGW(TAG, "tick too large even with 0 clients: %s",
|
||||||
(unsigned)frame_len, (unsigned)max_pay);
|
esp_err_to_name(err));
|
||||||
return ESP_ERR_NO_MEM;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
msg->payload.ble_stream_tick.clients_count--;
|
msg->payload.ble_stream_tick.clients_count--;
|
||||||
ESP_LOGW(TAG, "truncating tick clients to %u for MTU",
|
ESP_LOGW(TAG, "truncating tick clients to %u for encode buffer",
|
||||||
(unsigned)msg->payload.ble_stream_tick.clients_count);
|
(unsigned)msg->payload.ble_stream_tick.clients_count);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,7 +184,7 @@ void ble_stream_update(void) {
|
|||||||
|
|
||||||
if (want && !s_running) {
|
if (want && !s_running) {
|
||||||
s_running = true;
|
s_running = true;
|
||||||
if (xTaskCreate(ble_stream_task, "ble_stream", 4096, NULL, 5, &s_task) !=
|
if (xTaskCreate(ble_stream_task, "ble_stream", 8192, NULL, 5, &s_task) !=
|
||||||
pdPASS) {
|
pdPASS) {
|
||||||
s_running = false;
|
s_running = false;
|
||||||
s_task = NULL;
|
s_task = NULL;
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ void host_transport_set_cmd_queue(QueueHandle_t cmd_queue) {
|
|||||||
|
|
||||||
QueueHandle_t host_transport_cmd_queue(void) { return s_cmd_queue; }
|
QueueHandle_t host_transport_cmd_queue(void) { return s_cmd_queue; }
|
||||||
|
|
||||||
bool host_enqueue_packet(const uint8_t *payload, uint8_t len) {
|
bool host_enqueue_packet(const uint8_t *payload, size_t len) {
|
||||||
if (s_cmd_queue == NULL || payload == NULL || len == 0) {
|
if (s_cmd_queue == NULL || payload == NULL || len == 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
generic_msg_t msg = {
|
generic_msg_t msg = {
|
||||||
.msg_id = payload[0],
|
.msg_id = payload[0],
|
||||||
.len = len > 1 ? (size_t)(len - 1) : 0,
|
.len = len > 1 ? (len - 1) : 0,
|
||||||
.payload = NULL,
|
.payload = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -63,10 +63,11 @@ esp_err_t host_send_framed(const uint8_t *payload, size_t len) {
|
|||||||
checksum ^= payload[i];
|
checksum ^= payload[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t frame[4 + MAX_PAYLOAD_SIZE];
|
uint8_t frame[UART_FRAME_OVERHEAD + MAX_PAYLOAD_SIZE];
|
||||||
size_t pos = 0;
|
size_t pos = 0;
|
||||||
frame[pos++] = START_MARKER;
|
frame[pos++] = START_MARKER;
|
||||||
frame[pos++] = (uint8_t)len;
|
frame[pos++] = (uint8_t)(len & 0xff);
|
||||||
|
frame[pos++] = (uint8_t)((len >> 8) & 0xff);
|
||||||
memcpy(&frame[pos], payload, len);
|
memcpy(&frame[pos], payload, len);
|
||||||
pos += len;
|
pos += len;
|
||||||
frame[pos++] = checksum;
|
frame[pos++] = checksum;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ void host_transport_set_cmd_queue(QueueHandle_t cmd_queue);
|
|||||||
QueueHandle_t host_transport_cmd_queue(void);
|
QueueHandle_t host_transport_cmd_queue(void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build AA|len|payload|xor|CC and send via the registered TX backend.
|
* Build AA|len_lo|len_hi|payload|xor|CC and send via the registered TX backend.
|
||||||
* Same semantics as uart_send_framed.
|
* Same semantics as uart_send_framed.
|
||||||
*/
|
*/
|
||||||
esp_err_t host_send_framed(const uint8_t *payload, size_t len);
|
esp_err_t host_send_framed(const uint8_t *payload, size_t len);
|
||||||
@@ -31,6 +31,6 @@ esp_err_t host_send_framed(const uint8_t *payload, size_t len);
|
|||||||
void host_rx_bytes(const uint8_t *data, size_t len);
|
void host_rx_bytes(const uint8_t *data, size_t len);
|
||||||
|
|
||||||
/** Enqueue a completed uart_packet_t (payload[0]=msg type). */
|
/** Enqueue a completed uart_packet_t (payload[0]=msg type). */
|
||||||
bool host_enqueue_packet(const uint8_t *payload, uint8_t len);
|
bool host_enqueue_packet(const uint8_t *payload, size_t len);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+13
-4
@@ -81,18 +81,27 @@ bool parse_uart_byte(uint8_t byte, uart_packet_t *p) {
|
|||||||
if (byte == START_MARKER) {
|
if (byte == START_MARKER) {
|
||||||
p->index = 0;
|
p->index = 0;
|
||||||
p->checksum = 0;
|
p->checksum = 0;
|
||||||
p->state = STATE_LEN;
|
p->len = 0;
|
||||||
|
p->state = STATE_LEN_LO;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case STATE_LEN:
|
case STATE_LEN_LO:
|
||||||
if (byte > MAX_PAYLOAD_SIZE || byte == 0) {
|
p->len = byte;
|
||||||
|
p->state = STATE_LEN_HI;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case STATE_LEN_HI: {
|
||||||
|
uint16_t len = (uint16_t)(p->len | ((uint16_t)byte << 8));
|
||||||
|
if (len == 0 || len > MAX_PAYLOAD_SIZE) {
|
||||||
p->state = STATE_START;
|
p->state = STATE_START;
|
||||||
} else {
|
} else {
|
||||||
p->len = byte;
|
p->len = len;
|
||||||
|
p->index = 0;
|
||||||
p->state = STATE_DATA;
|
p->state = STATE_DATA;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case STATE_DATA:
|
case STATE_DATA:
|
||||||
p->payload[p->index++] = byte;
|
p->payload[p->index++] = byte;
|
||||||
|
|||||||
+11
-6
@@ -22,13 +22,18 @@
|
|||||||
#define UART_DRIVER_TX_BUF_SIZE 4096
|
#define UART_DRIVER_TX_BUF_SIZE 4096
|
||||||
#define START_MARKER 0xAA
|
#define START_MARKER 0xAA
|
||||||
#define STOP_MARKER 0xCC
|
#define STOP_MARKER 0xCC
|
||||||
#define MAX_BUF_SIZE 252
|
/** Frame: AA | len_lo | len_hi | payload[len] | xor | CC */
|
||||||
#define MAX_PAYLOAD_SIZE \
|
#define UART_FRAME_OVERHEAD 5
|
||||||
MAX_BUF_SIZE - 4 // Buffer overhead, Start, Len, CRC, End
|
/**
|
||||||
|
* Max application payload (type byte + protobuf). Sized for CLIENT_REGISTRY_MAX
|
||||||
|
* aggregations (CLIENT_INFO / BATTERY_STATUS with 16+ samples).
|
||||||
|
*/
|
||||||
|
#define MAX_PAYLOAD_SIZE 1024
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
STATE_START,
|
STATE_START,
|
||||||
STATE_LEN,
|
STATE_LEN_LO,
|
||||||
|
STATE_LEN_HI,
|
||||||
STATE_DATA,
|
STATE_DATA,
|
||||||
STATE_CHECKSUM,
|
STATE_CHECKSUM,
|
||||||
STATE_STOP
|
STATE_STOP
|
||||||
@@ -36,8 +41,8 @@ typedef enum {
|
|||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
uint8_t payload[MAX_PAYLOAD_SIZE];
|
uint8_t payload[MAX_PAYLOAD_SIZE];
|
||||||
uint8_t len;
|
uint16_t len;
|
||||||
uint8_t index;
|
uint16_t index;
|
||||||
uint8_t checksum;
|
uint8_t checksum;
|
||||||
parser_state_t state;
|
parser_state_t state;
|
||||||
} uart_packet_t;
|
} uart_packet_t;
|
||||||
|
|||||||
+2
-1
@@ -42,7 +42,8 @@ void uart_cmd_init_response(alox_UartMessage *msg, alox_MessageType type,
|
|||||||
esp_err_t uart_cmd_send(const alox_UartMessage *msg, const char *log_tag) {
|
esp_err_t uart_cmd_send(const alox_UartMessage *msg, const char *log_tag) {
|
||||||
esp_err_t err = uart_send_uart_message(msg);
|
esp_err_t err = uart_send_uart_message(msg);
|
||||||
if (err != ESP_OK && log_tag != NULL) {
|
if (err != ESP_OK && log_tag != NULL) {
|
||||||
ESP_LOGE(log_tag, "failed to send UART response");
|
ESP_LOGE(log_tag, "failed to send UART response: %s",
|
||||||
|
esp_err_to_name(err));
|
||||||
}
|
}
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,26 @@
|
|||||||
#include "uart_proto.h"
|
#include "uart_proto.h"
|
||||||
|
#include "esp_log.h"
|
||||||
#include "pb_encode.h"
|
#include "pb_encode.h"
|
||||||
#include "uart.h"
|
#include "uart.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
static const char *TAG = "[UART_PROTO]";
|
||||||
|
|
||||||
esp_err_t uart_send_uart_message(const alox_UartMessage *msg) {
|
esp_err_t uart_send_uart_message(const alox_UartMessage *msg) {
|
||||||
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
|
uint8_t pb_buf[MAX_PAYLOAD_SIZE];
|
||||||
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
|
pb_ostream_t stream = pb_ostream_from_buffer(pb_buf, sizeof(pb_buf));
|
||||||
|
|
||||||
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
|
if (!pb_encode(&stream, alox_UartMessage_fields, msg)) {
|
||||||
|
ESP_LOGE(TAG, "pb_encode failed type=%u (%s)", (unsigned)msg->type,
|
||||||
|
PB_GET_ERROR(&stream));
|
||||||
return ESP_FAIL;
|
return ESP_FAIL;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t payload[MAX_PAYLOAD_SIZE];
|
uint8_t payload[MAX_PAYLOAD_SIZE];
|
||||||
if (stream.bytes_written + 1 > sizeof(payload)) {
|
if (stream.bytes_written + 1 > sizeof(payload)) {
|
||||||
|
ESP_LOGE(TAG, "payload too large type=%u need=%u max=%u",
|
||||||
|
(unsigned)msg->type, (unsigned)(stream.bytes_written + 1),
|
||||||
|
(unsigned)sizeof(payload));
|
||||||
return ESP_ERR_NO_MEM;
|
return ESP_ERR_NO_MEM;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user