feat(ble_esl): split PAwR APIs and harden OTS writes

Rename start/stop to PAwR-only calls, type persisted ESL addresses,
report failed image writes, and post L2CAP credits atomically.
This commit is contained in:
jiminxiang
2026-09-03 20:55:25 +08:00
parent 0b286c61f3
commit e1d9911c3f
8 changed files with 102 additions and 78 deletions

View File

@@ -96,7 +96,7 @@ typedef struct {
*/
typedef enum {
BLE_ESL_EVT_STATE_CHANGED = 0, /*!< State transition occurred */
BLE_ESL_EVT_IMAGE_WRITE, /*!< Image data received via OTS */
BLE_ESL_EVT_IMAGE_WRITE, /*!< OTS image chunk or write completion */
BLE_ESL_EVT_DISPLAY_IMAGE, /*!< Display a stored image */
BLE_ESL_EVT_REFRESH_DISPLAY, /*!< Refresh the current display image */
BLE_ESL_EVT_SENSOR_READ, /*!< Read sensor data request */
@@ -117,11 +117,16 @@ typedef struct {
/**
* @brief Event data for BLE_ESL_EVT_IMAGE_WRITE
*
* Fired once per OTS fragment and once when the write transfer ends:
* - `data != NULL`: one fragment; the pointer is valid only during the callback
* - `data == NULL && length > 0`: write succeeded; `length` is the total size
* - `data == NULL && length == 0`: write failed or empty; the slot is incomplete
*/
typedef struct {
uint8_t image_index; /*!< Image storage index (0 to Max_Image_Index) */
const uint8_t *data; /*!< Pointer to received image data */
uint32_t length; /*!< Length of image data in bytes */
const uint8_t *data; /*!< Fragment bytes, or NULL on completion */
uint32_t length; /*!< Fragment size, total size, or 0 on failure */
uint32_t offset; /*!< Write offset within the image object */
} ble_esl_image_write_evt_param_t;
@@ -331,8 +336,10 @@ bool ble_esl_image_is_complete(uint8_t image_index);
/**
* @brief Atomically snapshot a completed image into a caller buffer.
*
* Marks the slot incomplete while a concurrent write is in progress so that
* Display Image never observes a torn frame.
* Copies only when the slot is marked complete. Concurrent OTS writes
* mark the slot incomplete via BLE_OTS_SERVER_EVT_DATA_WRITE so Display
* Image never observes a torn frame. This function does not change the
* complete flag itself.
*
* @param[in] image_index Image index
* @param[out] dst Destination buffer

View File

@@ -371,17 +371,26 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config);
esp_err_t ble_esl_ap_deinit(void);
/**
* @brief Start AP operation (PAwR broadcasting; scan idle until scan_start)
* @brief Start PAwR broadcasting
*
* Starts PAwR broadcasting. GAP discovery is not started here; call
* ble_esl_ap_start_scan() when the phone sends scan_start.
* Does not start GAP discovery. Call ble_esl_ap_start_scan() when the
* application wants to discover ESLs. Existing ACL connections are not
* affected.
*
* @note This function does not automatically initiate connections. The caller must handle
* scan results and call ble_esl_ap_connect() to establish connections.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized or already started
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not initialized
* or PAwR is already started
*/
esp_err_t ble_esl_ap_start(void);
esp_err_t ble_esl_ap_start_pawr(void);
/**
* @brief Stop PAwR broadcasting
*
* Does not stop GAP discovery. Call ble_esl_ap_stop_scan() first if
* discovery is running. Existing ACL connections are not affected.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_stop_pawr(void);
/**
* @brief Start GAP discovery if it is not already running
@@ -390,7 +399,7 @@ esp_err_t ble_esl_ap_start(void);
* cancelling the current scan. Clears scan_suppressed so discovery can
* auto-resume after connections end. PAwR broadcasting is not affected.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if AP not started
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_start_scan(void);
@@ -400,19 +409,10 @@ esp_err_t ble_esl_ap_start_scan(void);
* Sets scan_suppressed so discovery is not auto-resumed after connections
* end. Call ble_esl_ap_start_scan() to scan again.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if AP not started
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if PAwR is not started
*/
esp_err_t ble_esl_ap_stop_scan(void);
/**
* @brief Stop AP operation (scanning + PAwR broadcasting)
*
* Existing ACL connections are not affected.
*
* @return ESP_OK on success; ESP_ERR_INVALID_STATE if not started
*/
esp_err_t ble_esl_ap_stop(void);
/**
* @brief Initiate ACL connection to an ESL
*
@@ -547,12 +547,18 @@ bool ble_esl_ap_synchronize_in_progress(void);
esp_err_t ble_esl_ap_write_absolute_time(uint16_t conn_handle);
/**
* @brief Persisted association used to seed AP tracking after reboot
* @brief In-memory association snapshot used to seed AP tracking after reboot
*
* Not an NVS/wire blob. Call after ble_esl_ap_init(); may be invoked
* multiple times (once per ESL), including after ble_esl_ap_start_pawr().
* Key material is not checked for all-zeros — the application must
* quarantine corrupt records. The AP Sync Key of the last successful
* restore is installed as the current PAwR key.
*/
typedef struct __attribute__((packed)) {
uint16_t esl_addr;
uint8_t ble_addr[6];
uint8_t ble_addr_type;
typedef struct {
ble_esl_address_t esl_address; /*!< ESL Address (ESL_ID + Group_ID) */
uint8_t ble_addr[6]; /*!< Identity address of the ESL */
uint8_t ble_addr_type; /*!< BLE address type */
ble_esl_key_material_t ap_sync_key;
ble_esl_key_material_t resp_key;
} ble_esl_ap_persisted_esl_t;

View File

@@ -269,7 +269,7 @@ esp_err_t ble_esl_ap_init(const ble_esl_ap_config_t *config)
g_esl_ap->app_cb = config->callback;
g_esl_ap->pawr_config = config->pawr_config;
g_esl_ap->initialized = true;
g_esl_ap->started = false;
g_esl_ap->pawr_started = false;
g_esl_ap->pawr_active = false;
/* Initialize connection table */
@@ -388,9 +388,10 @@ esp_err_t ble_esl_ap_deinit(void)
return ESP_ERR_INVALID_STATE;
}
/* Stop scanning and PAwR if active */
if (g_esl_ap->started) {
ble_esl_ap_stop();
/* Stop discovery then PAwR if the public start_pawr() path ran */
if (g_esl_ap->pawr_started) {
(void)ble_esl_ap_stop_scan();
(void)ble_esl_ap_stop_pawr();
}
/* Mark the module as no longer operational before touching connections:
@@ -461,27 +462,26 @@ esp_err_t ble_esl_ap_deinit(void)
return ESP_OK;
}
esp_err_t ble_esl_ap_start(void)
esp_err_t ble_esl_ap_start_pawr(void)
{
if (!g_esl_ap || !g_esl_ap->initialized) {
return ESP_ERR_INVALID_STATE;
}
if (g_esl_ap->started) {
ESP_LOGW(TAG, "Already started");
if (g_esl_ap->pawr_started) {
ESP_LOGW(TAG, "PAwR already started");
return ESP_ERR_INVALID_STATE;
}
g_esl_ap->scan_suppressed = true;
/* Start PAwR broadcasting; GAP discovery waits for explicit scan_start */
esp_err_t ret = ble_esl_ap_pawr_start();
if (ret != ESP_OK) {
ESP_LOGE(TAG, "Failed to start PAwR: %s", esp_err_to_name(ret));
return ret;
}
g_esl_ap->started = true;
ESP_LOGI(TAG, "ESL AP started (PAwR active, scan idle until scan_start)");
g_esl_ap->pawr_started = true;
ESP_LOGI(TAG, "PAwR started (scan idle until start_scan)");
return ESP_OK;
}
@@ -520,7 +520,7 @@ static esp_err_t ble_esl_ap_start_discovery(void)
static void ble_esl_ap_maybe_resume_discovery(void)
{
if (!g_esl_ap || !g_esl_ap->started || g_esl_ap->scan_suppressed) {
if (!g_esl_ap || !g_esl_ap->pawr_started || g_esl_ap->scan_suppressed) {
return;
}
if (ble_gap_disc_active()) {
@@ -531,7 +531,7 @@ static void ble_esl_ap_maybe_resume_discovery(void)
esp_err_t ble_esl_ap_start_scan(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
@@ -545,7 +545,7 @@ esp_err_t ble_esl_ap_start_scan(void)
esp_err_t ble_esl_ap_stop_scan(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
@@ -561,31 +561,19 @@ esp_err_t ble_esl_ap_stop_scan(void)
return ESP_OK;
}
esp_err_t ble_esl_ap_stop(void)
esp_err_t ble_esl_ap_stop_pawr(void)
{
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->started) {
if (!g_esl_ap || !g_esl_ap->initialized || !g_esl_ap->pawr_started) {
return ESP_ERR_INVALID_STATE;
}
/* Cancel any pending (not yet established) connection attempts */
ble_gap_conn_cancel();
/* Stop scanning */
if (ble_gap_disc_active()) {
int rc = ble_gap_disc_cancel();
if (rc != 0 && rc != BLE_HS_EALREADY) {
ESP_LOGW(TAG, "Failed to cancel discovery; rc=%d", rc);
}
}
/* Stop PAwR broadcasting */
esp_err_t ret = ble_esl_ap_pawr_stop();
if (ret != ESP_OK) {
ESP_LOGW(TAG, "Failed to stop PAwR: %s", esp_err_to_name(ret));
}
g_esl_ap->started = false;
ESP_LOGI(TAG, "ESL AP stopped");
g_esl_ap->pawr_started = false;
ESP_LOGI(TAG, "PAwR stopped");
return ESP_OK;
}

View File

@@ -181,6 +181,7 @@ static TaskHandle_t s_lifecycle_holder;
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "sync key packed 24");
_Static_assert(sizeof(ble_esl_key_material_t) == 24, "resp key packed 24");
/* In-memory compactness only — not an NVS/wire layout. */
_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, ap_sync_key) == 9, "flat key block start");
_Static_assert(offsetof(ble_esl_ap_persisted_esl_t, resp_key) == 9 + 24, "resp key follows sync");
@@ -1710,17 +1711,19 @@ esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *inf
return ESP_ERR_INVALID_ARG;
}
if (info->esl_addr == 0) {
uint16_t esl_addr = BLE_ESL_AP_ADDR_PACK(info->esl_address);
if (esl_addr == 0) {
ESP_LOGI(TAG, "restore: esl_addr=0x0000 (esl_id=0 group_id=0)");
}
if (BLE_ESL_AP_ADDR_ESL_ID(info->esl_addr) == BLE_ESL_BROADCAST_ADDRESS) {
if (info->esl_address.esl_id == BLE_ESL_BROADCAST_ADDRESS) {
ESP_LOGE(TAG, "restore: broadcast esl_id not allowed");
return ESP_ERR_INVALID_ARG;
}
ble_esl_ap_tracking_lock();
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(info->esl_addr);
ble_esl_ap_esl_entry_t *esl = ble_esl_ap_find_esl(esl_addr);
if (esl == NULL) {
esl = ble_esl_ap_find_esl_by_ble_addr(info->ble_addr, info->ble_addr_type);
}
@@ -1734,7 +1737,7 @@ esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *inf
}
esl->in_use = true;
esl->esl_addr = info->esl_addr;
esl->esl_addr = esl_addr;
memcpy(esl->ble_addr, info->ble_addr, 6);
esl->ble_addr_type = info->ble_addr_type;
esl->conn_handle = BLE_ESL_AP_CONN_HANDLE_INVALID;
@@ -1746,22 +1749,22 @@ esp_err_t ble_esl_ap_restore_persisted_esl(const ble_esl_ap_persisted_esl_t *inf
ble_esl_ap_pawr_set_sync_key(&info->ap_sync_key);
esp_err_t ret = ble_esl_ap_pawr_set_response_key(info->esl_addr,
esp_err_t ret = ble_esl_ap_pawr_set_response_key(esl_addr,
&info->resp_key);
if (ret != ESP_OK) {
ESP_LOGW(TAG, "restore: set response key for 0x%04X failed: %s",
info->esl_addr, esp_err_to_name(ret));
esl_addr, esp_err_to_name(ret));
}
ret = ble_esl_ap_update_esl_state(info->esl_addr,
ret = ble_esl_ap_update_esl_state(esl_addr,
BLE_ESL_STATE_UNSYNCHRONIZED);
if (ret != ESP_OK) {
ESP_LOGE(TAG, "restore: failed to mark ESL 0x%04X unsynchronized: %s",
info->esl_addr, esp_err_to_name(ret));
esl_addr, esp_err_to_name(ret));
return ret;
}
ESP_LOGI(TAG, "Restored ESL 0x%04X from persistence (awaiting resync)",
info->esl_addr);
esl_addr);
return ESP_OK;
}

View File

@@ -230,7 +230,7 @@ typedef struct {
*/
typedef struct {
bool initialized; /*!< Module has been initialized */
bool started; /*!< Scanning + PAwR broadcasting active */
bool pawr_started; /*!< ble_esl_ap_start_pawr() has succeeded */
bool scan_suppressed; /*!< User/app requested scan stop; do not auto-resume */
/* Application callback */

View File

@@ -720,7 +720,7 @@ static void esl_ots_write_cb(ble_ots_server_event_t event,
.image_write = {
.image_index = image_index,
.data = NULL,
.length = param->write_complete.bytes_received,
.length = success ? param->write_complete.bytes_received : 0,
.offset = param->write_complete.offset,
}
};

View File

@@ -105,9 +105,9 @@ typedef enum {
BLE_OTS_SERVER_EVT_EXECUTE, /*!< OACP Execute on current object */
BLE_OTS_SERVER_EVT_CHECKSUM_REQUEST, /*!< OACP Calculate Checksum completed */
BLE_OTS_SERVER_EVT_READ_COMPLETE, /*!< Read transfer completed */
BLE_OTS_SERVER_EVT_DATA_WRITE, /*!< Object data chunk received during write transfer */
BLE_OTS_SERVER_EVT_WRITE_COMPLETE, /*!< Write transfer completed */
BLE_OTS_SERVER_EVT_METADATA_WRITTEN, /*!< Client wrote a metadata characteristic */
BLE_OTS_SERVER_EVT_DATA_WRITE, /*!< Object data chunk received during write transfer */
} ble_ots_server_event_t;
/*****************************************************************************

View File

@@ -68,7 +68,8 @@ void ble_ots_server_unlock(void)
/*****************************************************************************
* L2CAP OTC MTU
*****************************************************************************/
#define OTS_L2CAP_COC_MTU 1024
#define OTS_L2CAP_COC_MTU 1024
#define OTS_L2CAP_RX_WIN_MAX 8
/*****************************************************************************
* Directory Listing Object name
@@ -1516,17 +1517,36 @@ static int ots_l2cap_event_handle(struct ble_l2cap_event *event)
}
/* Pre-post the configured receive window. Each buffer contributes one
* peer credit; posting only one turns a bulk write into stop-and-wait. */
for (int i = 0; i < CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT; i++) {
struct os_mbuf *sdu_rx = os_msys_get_pkthdr(OTS_L2CAP_COC_MTU, 0);
if (!sdu_rx) {
ESP_LOGE(TAG, "L2CAP accept: failed to allocate SDU rx buffer %d", i);
* peer credit; posting only one (the Kconfig default) is stop-and-wait.
* Raise CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT and msys block size
* to actually pipeline a 1024-byte CoC write. Allocate all SDUs first
* so a later ENOMEM does not leave a half-posted window. */
int win = CONFIG_BT_NIMBLE_L2CAP_COC_SDU_BUFF_COUNT;
if (win < 1) {
win = 1;
} else if (win > OTS_L2CAP_RX_WIN_MAX) {
win = OTS_L2CAP_RX_WIN_MAX;
}
struct os_mbuf *sdu_bufs[OTS_L2CAP_RX_WIN_MAX];
int got = 0;
for (; got < win; got++) {
sdu_bufs[got] = os_msys_get_pkthdr(OTS_L2CAP_COC_MTU, 0);
if (!sdu_bufs[got]) {
ESP_LOGE(TAG, "L2CAP accept: failed to allocate SDU rx buffer %d", got);
while (got-- > 0) {
os_mbuf_free_chain(sdu_bufs[got]);
}
return BLE_HS_ENOMEM;
}
int rc = ble_l2cap_recv_ready(event->accept.chan, sdu_rx);
}
for (int i = 0; i < win; i++) {
int rc = ble_l2cap_recv_ready(event->accept.chan, sdu_bufs[i]);
if (rc != 0) {
ESP_LOGE(TAG, "L2CAP accept: recv_ready failed rc=%d", rc);
os_mbuf_free_chain(sdu_rx);
os_mbuf_free_chain(sdu_bufs[i]);
for (int j = i + 1; j < win; j++) {
os_mbuf_free_chain(sdu_bufs[j]);
}
return rc;
}
}