fix(esp_http_server): merge release/v6.0 to resolve backport conflict

Merge branch 'release/v6.0' into backport-48352-resolve to resolve a
conflict in esp_httpd_priv.h between this backport's sdkconfig.h
include and the ctrl-socket semaphore's freertos/semphr.h include.
This commit is contained in:
Hrushikesh Bhosale
2026-07-02 11:41:38 +05:30
1121 changed files with 36331 additions and 10307 deletions
+26 -6
View File
@@ -16,6 +16,14 @@ examples/bluetooth:
disable:
- if: SOC_BT_SUPPORTED != 1
examples/bluetooth/ble_uart_service:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_filepatterns:
- examples/bluetooth/common/ble_uart/**/*
- examples/bluetooth/ble_uart_service/**/*
examples/bluetooth/bluedroid/ble:
<<: *bt_default_depends
disable:
@@ -428,14 +436,10 @@ examples/bluetooth/nimble/throughput_app:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_components+:
- esp_driver_gpio
- esp_driver_uart
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/*
examples/bluetooth/nimble/throughput_app/blecent_throughput:
examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
@@ -444,4 +448,20 @@ examples/bluetooth/nimble/throughput_app/blecent_throughput:
- esp_driver_uart
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/blecent_throughput/components/**/*
- examples/bluetooth/nimble/throughput_app/gatt/blecent_throughput/components/**/*
examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_cent/**/*
examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph:
<<: *bt_default_depends
disable:
- if: SOC_BLE_SUPPORTED != 1
depends_filepatterns:
- examples/bluetooth/nimble/common/**/*
- examples/bluetooth/nimble/throughput_app/l2cap_coc/l2cap_coc_prph/**/*
@@ -169,9 +169,6 @@ The characteristic is binded with `led_chr_access` callback function, in which t
``` C
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
/* Local variables */
int rc;
/* Handle access events */
/* Note: LED characteristic is write only */
switch (ctxt->op) {
@@ -203,7 +200,7 @@ static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
} else {
goto error;
}
return rc;
return 0;
}
goto error;
@@ -114,9 +114,6 @@ error:
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
/* Local variables */
int rc = 0;
/* Handle access events */
/* Note: LED characteristic is write only */
switch (ctxt->op) {
@@ -148,7 +145,7 @@ static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
} else {
goto error;
}
return rc;
return 0;
}
goto error;
@@ -117,9 +117,6 @@ error:
static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
struct ble_gatt_access_ctxt *ctxt, void *arg) {
/* Local variables */
int rc = 0;
/* Handle access events */
/* Note: LED characteristic is write only */
switch (ctxt->op) {
@@ -151,7 +148,7 @@ static int led_chr_access(uint16_t conn_handle, uint16_t attr_handle,
} else {
goto error;
}
return rc;
return 0;
}
goto error;
@@ -0,0 +1,470 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# Building an OpenCode Companion with ESP-BLE-UART and ESP-VoCat
## Introduction
This document describes how to build a physical companion device for OpenCode using ESP-BLE-UART and ESP-VoCat. The companion device reflects the current session state on a display, presents permission requests for user approval, and returns permission decisions to OpenCode via single-key input. BLE UART serves as the transport layer between the device and the host-side editor session.
The tutorial is organized in two parts. Part 1 uses **ESP-BLE-UART Console** with the `ble_uart_service` Echo Server (this example) to verify that the host can discover, connect to, and exchange data with a BLE UART device. Part 2 introduces the `ble_uart_service` example firmware for the ESP-VoCat board (maintained in [esp-iot-solution](https://github.com/espressif/esp-iot-solution)), the **ESP-BLE-UART Daemon**, and the **OpenCode Plugin**, which together enable the device to receive session status updates and return `once` / `reject` permission decisions to OpenCode.
<p align="center">
<img src="assets/ESP-VoCat-Working-With-OpenCode.png" alt="ESP-VoCat Working With OpenCode" width="80%">
<br><em>ESP-VoCat Working With OpenCode</em>
</p>
## Learning Objectives
- Understand the BLE UART service and its GATT convention
- Learn how to build and flash the ESP-BLE-UART Echo Server
- Understand the JSON Lines protocol used over BLE UART
- Learn how to configure the ESP-BLE-UART Daemon and OpenCode Plugin
## Prerequisites
- A host machine with a Bluetooth adapter and scan/connect permissions.
- ESP-IDF environment exported.
- Any target supported by `ble_uart_service` for the Console echo-server smoke test.
- The full OpenCode UI demo requires:
- An [ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) development board (based on ESP32-S3) with a circular touch display and single-key input. The BLE UART transport is reusable, but the display/touch/emote UI in this example is board-specific. The example is maintained in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository at `examples/bluetooth/ble_uart_service`; see its README for supported boards, dependency versions, and build instructions.
- The first CMake configuration of the `ble_uart_service` example requires network access to download `emote_assets.bin`. For offline or intranet environments, set `EMOTE_ASSETS_BIN` to a local path to override the download.
- OpenCode installed to run the plugin demo.
Install the host-side ESP-BLE-UART Bridge dependencies:
```bash
cd $IDF_PATH
. ./export.sh
python -m pip install -r tools/ble/ble_uart_bridge/requirements.txt
```
On Windows, use `export.bat` or `export.ps1` from the ESP-IDF root instead of `. ./export.sh`.
## Part 1: ESP-BLE-UART Console
### What BLE UART Is
Bluetooth LE does not have a real UART peripheral in the classic serial-port sense. A BLE UART service is a GATT convention: one characteristic serves as the host-to-device RX channel, another as the device-to-host TX channel. The Echo Server in `ble_uart_service` uses Nordic UART Service-style UUIDs and sends received bytes back through TX notifications, which makes it suitable for verifying the host-side Console path.
The transport layer only moves bytes. In Part 1, those bytes are simple echoed text. In Part 2, the `ble_uart_service` example firmware running on ESP-VoCat puts a JSONL protocol on top of the same BLE UART channel.
### Build and Flash the ESP-BLE-UART Echo Server
```bash
cd $IDF_PATH/examples/bluetooth/ble_uart_service
idf.py set-target esp32s3 # or another supported target
idf.py build flash monitor
```
Keep the monitor open during pairing. If the central asks for a passkey, use the six-digit value printed by the firmware log. The firmware console output should resemble the following log (the address and device name suffix will vary):
```
I (548) ble_uart: BLE host task started
I (548) ble_uart: registered service 0x1800 handle=1
I (548) ble_uart: registered chr 0x2a00 def=2 val=3
I (548) ble_uart: registered chr 0x2a01 def=4 val=5
I (558) ble_uart: registered service 0x1801 handle=6
I (558) ble_uart: registered chr 0x2a05 def=7 val=8
I (568) ble_uart: registered chr 0x2b3a def=10 val=11
I (568) ble_uart: registered chr 0x2b29 def=12 val=13
I (578) ble_uart: registered service 6e400001-b5a3-f393-e0a9-e50e24dcca9e handle=14
I (578) ble_uart: registered chr 6e400002-b5a3-f393-e0a9-e50e24dcca9e def=15 val=16
I (588) ble_uart: registered chr 6e400003-b5a3-f393-e0a9-e50e24dcca9e def=17 val=18
I (608) NimBLE: GAP procedure initiated: stop advertising.
I (608) NimBLE: GAP procedure initiated: stop advertising.
I (608) ble_uart: addr=74:4d:bd:a9:ed:72
I (608) NimBLE: GAP procedure initiated: advertise;
I (618) NimBLE: disc_mode=2
I (618) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0
I (628) NimBLE:
I (628) ble_uart: advertising as 'BleUart-ED72'
I (628) main_task: Returned from app_main()
```
The `ble_uart: addr=74:4d:bd:a9:ed:72` line shows the device Bluetooth MAC address (`74:4D:BD:A9:ED:72`). The device advertises under the name shown in the last `ble_uart: advertising as 'BleUart-XXXX'` line.
When the central initiates a connection, the firmware logs a pairing passkey prompt. If you are prompted for a passkey by the system Bluetooth dialog or the `connection-check` command, enter the six-digit number shown in the monitor:
```
W (19298) ble_uart: +-----------------------------+
W (19298) ble_uart: | BLE PAIRING PASSKEY: |
W (19298) ble_uart: | 617138 |
W (19298) ble_uart: +-----------------------------+
```
### Find the BLE UART Device
Open a second terminal:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py list-devices
```
Use the printed device identifier as `DEVICE_ID`. You can check whether the target device has been discovered by matching the MAC address or device name in the output. On Linux, the device MAC address is printed directly:
```
> python main.py list-devices
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 74:4D:BD:A9:ED:72, with name BleUart-ED72, rssi=-46
```
The `74:4D:BD:A9:ED:72` MAC address and `BleUart-ED72` device name in this output match the firmware log above.
On macOS, system restrictions prevent the tool from displaying the real Bluetooth MAC address. Instead, macOS assigns a CoreBluetooth UUID as the device identifier. Match the device name (`BleUart-ED72` in this example) in the `list-devices` output with the name shown in the firmware log to find the corresponding UUID:
```
> python main.py list-devices
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5, with name BleUart-ED72, rssi=-46
```
The `5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5` string in this example is the CoreBluetooth UUID to use as `DEVICE_ID` on macOS.
### Check the Bluetooth LE Link Before Opening Console
```bash
python main.py connection-check "<DEVICE_ID>"
```
This command connects, discovers the BLE UART service and characteristics, then disconnects. On Linux or Windows, pass the device MAC address as `DEVICE_ID`:
```
> python main.py connection-check 74:4D:BD:A9:ED:72
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 74:4D:BD:A9:ED:72...
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 74:4D:BD:A9:ED:72!
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 74:4D:BD:A9:ED:72...
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 74:4D:BD:A9:ED:72
```
On macOS, use the CoreBluetooth UUID instead:
```
> python main.py connection-check 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5!
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
```
If this step fails, resolve scanning, pairing, permissions, or advertising issues before proceeding to the daemon or OpenCode integration.
### Open ESP-BLE-UART Console
```bash
python main.py console "<DEVICE_ID>" --terminator lf
```
In the Console, type a short line and press Enter:
```
hello from console
```
Expected result:
```
[INFO] Connected to 68:B6:B3:55:41:76
[TX] hello from console
[RX] hello from console
```
- The Bluetooth LE address varies by device.
- Console shows `[TX]` lines for the input.
- The ESP-BLE-UART example echoes the same bytes back as `[RX]` output.
At this point Bluetooth LE discovery, connection, host-to-device writes, and device-to-host notifications all work. The JSONL protocol (used by the ESP-VoCat example), daemon, and OpenCode Plugin are application layers on top of this path; they do not replace it.
For more Console options such as hex mode, write-with-response, and alternate line endings, see [`tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md`](../../../tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md).
## Part 2: ESP-VoCat OpenCode Companion
### About ESP-VoCat
[ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) is an intelligent AI development kit based on the ESP32-S3 module, featuring a circular touch display and single-key input.
The [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository contains a `ble_uart_service` example (at `examples/bluetooth/ble_uart_service`) that runs on the ESP-VoCat development board. This example firmware renders session status as emote expressions and presents permission requests for physical approval. See the example README in esp-iot-solution for supported boards, required component versions, and build details.
### Why JSON Lines
Bluetooth LE writes are packetized by the ATT MTU, not by application messages. The ESP-VoCat OpenCode flow uses JSON Lines (JSONL) on top of BLE UART. JSONL works here because it is readable in logs, easy to type into Console for manual testing, parsable with cJSON on firmware, and covers both request/response and fire-and-forget patterns.
### Architecture
The ESP-BLE-UART Bridge tools and OpenCode demo plugin are included in ESP-IDF master and release branches starting from `release/v5.2` under `tools/ble/ble_uart_bridge/`. The `ble_uart_service` example implements the device side of the protocol and is available in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository.
```mermaid
flowchart LR
OC[OpenCode] -->|session.status / permission.asked| Plugin[OpenCode Plugin]
Plugin -->|POST /notify| Daemon[ESP-BLE-UART Daemon]
Plugin -->|POST /request| Daemon
Daemon -->|Bluetooth LE write: NUS RX JSONL| ESP[ble_uart_service example]
ESP -->|Bluetooth LE notify: NUS TX JSONL| Daemon
Daemon -->|HTTP response| Plugin
Plugin -->|permission reply| OC
ESP --> Display[Emote + Tip Text]
ESP --> Key[Single Key: once / reject]
```
Each layer can be replaced independently:
- Console verifies the raw BLE UART path.
- Daemon keeps one Bluetooth LE connection open and exposes local HTTP endpoints.
- The OpenCode Plugin translates editor events into daemon requests.
- The `ble_uart_service` example renders status and permission prompts on the ESP-VoCat device.
### Start the ESP-BLE-UART Daemon
First flash the `ble_uart_service` example from the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository onto the ESP-VoCat board. This is a different application from the Console Echo Server:
```bash
# Clone esp-iot-solution if not already available
git clone https://github.com/espressif/esp-iot-solution.git
cd esp-iot-solution/examples/bluetooth/ble_uart_service
idf.py set-target esp32s3
idf.py build flash monitor
```
See the example README in esp-iot-solution for dependency versions and board-specific configuration.
Then scan again and use the ESP-VoCat device identifier as `VOCAT_DEVICE_ID`:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py list-devices
python main.py connection-check "<VOCAT_DEVICE_ID>"
```
Start the daemon with the ESP-VoCat device:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py daemon "<VOCAT_DEVICE_ID>" --host 127.0.0.1 --port 8888
```
> **Note:** The daemon HTTP endpoints are unauthenticated. Keep the daemon bound to `127.0.0.1` unless you add your own access control.
In another terminal, check daemon status:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py daemon-status
```
### Verify ESP-VoCat Through the Daemon Before OpenCode
Do not use a generic `echo` request for ESP-VoCat validation; this firmware does not implement an echo op. Use the operations defined in the [esp-iot-solution example's json_format.md](https://github.com/espressif/esp-iot-solution/blob/master/examples/bluetooth/ble_uart_service/json_format.md).
Session status smoke test:
```bash
python main.py daemon-notify --op session.status --json '{
"v": 1,
"kind": "session.status",
"event_id": "evt_manual",
"session_id": "ses_manual",
"requires_reply": false,
"payload": {
"type": "busy"
}
}'
python main.py daemon-notify --op session.status --json '{
"v": 1,
"kind": "session.status",
"event_id": "evt_manual",
"session_id": "ses_manual",
"requires_reply": false,
"payload": {
"type": "idle"
}
}'
```
The CLI wraps each JSON object as the daemon envelope `data` field with `op: "session.status"` and `id: ""`. The firmware receives a complete JSONL envelope over Bluetooth LE and updates the display without replying.
Permission request smoke test:
```bash
python main.py daemon-send --op permission.request --timeout 35 --json '{
"v": 1,
"kind": "permission.request",
"event_id": "evt_manual",
"session_id": "ses_manual",
"permission_id": "perm_manual",
"requires_reply": true,
"payload": {
"id": "perm_manual",
"sessionID": "ses_manual",
"type": "bash",
"title": "Run idf.py build",
"metadata": {
"command": "idf.py build"
}
}
}'
```
The ESP-VoCat device should display a permission prompt:
| ESP-VoCat input | Device reply |
|-----------------|-----------------|
| Single click | `decision: "once"` |
| Long press | `decision: "reject"` |
| 30s timeout | `decision: "reject"` |
This manual daemon test exercises the same request/response path that the OpenCode Plugin uses.
### Install the OpenCode Demo Plugin
The OpenCode demo plugin is included in ESP-IDF under `tools/ble/ble_uart_bridge/demos/opencode`.
Project-local install:
```bash
mkdir -p <proj-path>/.opencode/plugins/opencode-ble-uart-bridge
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
<proj-path>/.opencode/plugins/opencode-ble-uart-bridge/
```
User-level install:
```bash
mkdir -p ~/.config/opencode/plugins/opencode-ble-uart-bridge
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
~/.config/opencode/plugins/opencode-ble-uart-bridge/
```
Then configure OpenCode. For a project-local install, put the following in `<proj-path>/opencode.json` or merge it into an existing config:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
".opencode/plugins/opencode-ble-uart-bridge/opencode-ble-uart-bridge.ts"
],
"permission": {
"edit": "ask"
}
}
```
For a user-level install, point `plugin` at the installed file under `~/.config/opencode/plugins/opencode-ble-uart-bridge/`. Use an absolute home path if the config loader does not expand `~`.
Useful plugin environment variables:
```bash
export OPENCODE_BLE_DAEMON_URL="http://127.0.0.1:8888"
export OPENCODE_BLE_DECISION_TIMEOUT_SECONDS=60
export OPENCODE_BLE_DEBUG=1
```
Restart OpenCode after changing plugin files, `opencode.json`, or these environment variables.
### Run the OpenCode Demo
1. Keep the firmware running and advertising/connected.
2. Keep the ESP-BLE-UART Daemon running on `127.0.0.1:8888`.
3. Start OpenCode in the project where the plugin is configured.
4. Trigger a permission prompt, for example an edit operation when `permission.edit` is set to `ask`.
Expected behavior:
- OpenCode session status is forwarded as best-effort `session.status` updates.
- ESP-VoCat shows busy/idle/retry expressions.
- Permission prompts appear on ESP-VoCat with compact metadata such as command, path, or URL.
- Single click returns `once` to OpenCode.
- Long press or timeout returns `reject`.
To demonstrate bash or tool execution permissions, ensure the OpenCode permission config is set to prompt for that tool category. Otherwise, use an edit permission as the primary trigger.
<p align="center">
<img src="assets/ESP-VoCat-Asking-For-Permission.png" alt="ESP-VoCat Asking For Permission" width="80%">
<br><em>ESP-VoCat Asking For Permission</em>
</p>
> **Note:** For a comprehensive understanding of Bluetooth Low Energy, see the [Bluetooth LE Overview](../../../docs/en/api-guides/ble/overview.rst). For Bluetooth LE connection management and data exchange, refer to the [Bluetooth LE Multi-Connection Guide](../../../docs/en/api-guides/ble/ble-multiconnection-guide.rst).
## Protocol Reference
The firmware protocol is documented in the `ble_uart_service` example's `json_format.md` in the [esp-iot-solution](https://github.com/espressif/esp-iot-solution) repository (`examples/bluetooth/ble_uart_service/json_format.md`). The outer daemon envelope has the following format:
```
{"v":1,"id":"<bridge-request-id>","op":"<operation>","data":{}}
```
- `id` is non-empty for request/response operations such as `permission.request`.
- `id` is empty for fire-and-forget notifications such as `session.status` and `permission.cancel`.
- Device replies echo the same non-empty `id` and return either `ok/data` or `ok:false/error`.
Example permission request over JSONL on Bluetooth LE:
```json
{
"v": 1,
"id": "perm-001",
"op": "permission.request",
"data": {
"v": 1,
"kind": "permission.request",
"event_id": "evt_...",
"session_id": "ses_...",
"permission_id": "perm_...",
"requires_reply": true,
"payload": {
"id": "perm_...",
"sessionID": "ses_...",
"type": "bash",
"title": "Run idf.py build",
"metadata": {
"command": "idf.py build"
}
}
}
}
```
Example device response:
```json
{
"v": 1,
"id": "perm-001",
"ok": true,
"data": {
"decision": "once",
"message": "Approved from BLE device"
}
}
```
`permission.cancel` clears a stale prompt without sending a later decision. This covers the case where the user answers from the OpenCode TUI before interacting with ESP-VoCat.
## Troubleshooting
- **No devices found:** confirm host Bluetooth access, firmware advertising, and proximity. Start with `list-devices` and `connection-check`.
- **Console works but daemon does not:** ensure Console is closed; this firmware accepts only one Bluetooth LE connection at a time.
- **Daemon disconnects:** the daemon does not run a background reconnect loop. When the next `/request` or `/notify` HTTP call arrives, it attempts an on-demand reconnect. If the device is unreachable for several consecutive attempts, the daemon exits automatically. Use `daemon-status` to check the current connection state and reconnect failure count.
- **OpenCode does not forward events:** confirm `OPENCODE_BLE_DAEMON_URL`, run `daemon-status`, and restart OpenCode after config changes.
- **Permission request times out:** confirm the device received a non-empty request `id`, no older prompt is pending, and the key was pressed before the timeout.
- **Unexpected rejections:** the demo is designed to fail closed. If the Bluetooth LE link, daemon, plugin, or device decision handling fails, the OpenCode side rejects rather than silently approves.
- **Pairing fails:** check the passkey printed in firmware logs and confirm the same value on the central.
- **Insufficient authentication:** if the connection or characteristic access fails with an authentication error, pair the device through the system Bluetooth settings first and enter the six-digit passkey shown in the firmware monitor log. Some desktop Bluetooth LE stacks require explicit system-level pairing before GATT operations succeed.
## Extension Ideas
- Add more input gestures for `always`, `edit`, or `deny for session`.
- Add richer display layouts for command/path/URL metadata.
- Add device-side settings for prompt timeout.
- Add an allowlist for low-risk commands.
- Add integration tests with a mocked daemon and simulated firmware replies.
- Replace JSONL with a compact binary protocol if a production product requires lower overhead.
## Summary
Each layer in this demo is small and independently testable: Console validates the raw BLE UART path, the daemon turns one Bluetooth LE connection into a local HTTP bridge, the OpenCode Plugin maps editor events to daemon requests, and ESP-VoCat provides the physical UI. Any layer can be replaced without affecting the others.
@@ -0,0 +1,472 @@
<!-- SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD -->
<!-- SPDX-License-Identifier: Apache-2.0 -->
# 使用 ESP-BLE-UART 与 ESP-VoCat 构建 OpenCode 伴侣设备
> [English](OPENCODE_COMPANION.md)
## 介绍
本文档介绍如何使用 ESP-BLE-UART 和 ESP-VoCat 构建一个 OpenCode 的物理伴侣设备。该设备在显示屏上反映当前会话 (Session) 状态,呈现权限请求 (Permission Request) 供用户审批,并通过单键输入将权限决策返回给 OpenCode。BLE UART 作为设备与主机侧编辑器会话之间的传输层。
本教程分为两个部分。第一部分使用 **ESP-BLE-UART 控制台 (Console)** 搭配 `ble_uart_service` 回显服务器 (Echo Server),验证主机是否能够发现、连接 BLE UART 设备并完成数据交换。第二部分引入 `ble_uart_service` 示例固件(运行于 ESP-VoCat 开发板)、**ESP-BLE-UART 守护进程 (Daemon)** 和 **OpenCode 插件 (Plugin)**,使设备能够接收会话状态更新,并将 `once` / `reject` 权限决策返回给 OpenCode。
<p align="center">
<img src="assets/ESP-VoCat-Working-With-OpenCode.png" alt="ESP-VoCat 与 OpenCode 协同工作" width="80%">
<br><em>ESP-VoCat 与 OpenCode 协同工作</em>
</p>
## 学习目标
- 了解 BLE UART 服务及其 GATT 约定
- 掌握构建和烧录 ESP-BLE-UART 回显服务器的方法
- 理解在 BLE UART 上运行的 JSON Lines 协议
- 掌握 ESP-BLE-UART 守护进程和 OpenCode 插件的配置方法
## 前置条件
- 主机具备可用的蓝牙适配器和扫描/连接权限。
- ESP-IDF 环境已导出。
- 回显服务器冒烟测试可使用 `ble_uart_service` 支持的任意目标芯片 (Target)。
- 完整 OpenCode UI 演示需要以下环境:
- [ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) 开发板(基于 ESP32-S3),配备圆形触摸显示屏和单键输入。BLE UART 传输层可以复用,但显示、触摸和表情 UI 为该示例的板级特性。该示例维护在 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库的 `examples/bluetooth/ble_uart_service` 路径下,支持的板型、依赖版本和构建说明请参考示例 README。
- `ble_uart_service` 示例首次编译配置时需要联网下载 `emote_assets.bin`;离线或内网环境下,请将 `EMOTE_ASSETS_BIN` 设置为本地路径以覆盖下载。
- 安装 OpenCode 以运行插件演示。
安装主机侧 ESP-BLE-UART 桥接工具 (Bridge) 依赖:
```bash
cd $IDF_PATH
. ./export.sh
python -m pip install -r tools/ble/ble_uart_bridge/requirements.txt
```
Windows 下请使用 ESP-IDF 根目录中的 `export.bat` 或 `export.ps1`,不要使用 `. ./export.sh`。
## 第一部分:ESP-BLE-UART 控制台验证
### BLE UART 简介
Bluetooth LE 协议中并没有传统串口意义上的 UART 外设。BLE UART 服务 (BLE UART Service) 是一种 GATT 约定:一个特征值 (Characteristic) 作为主机写入设备的 RX 通道,另一个特征值作为设备通知 (Notify) 给主机的 TX 通道。`ble_uart_service` 中的回显服务器使用 Nordic UART Service 风格的 UUID,将收到的字节通过 TX Notify 原样发回,适合用于验证主机侧控制台链路。
传输层只负责搬运字节。第一部分中,这些字节为普通回显文本;第二部分中,运行在 ESP-VoCat 上的 `ble_uart_service` 示例固件会在同一条 BLE UART 通道上叠加 JSONL 协议。
### 构建并烧录 ESP-BLE-UART 回显服务器
```bash
cd $IDF_PATH/examples/bluetooth/ble_uart_service
idf.py set-target esp32s3 # 或其他支持的 target
idf.py build flash monitor
```
配对期间请保持串口监视器打开。中央设备 (Central) 提示输入配对密钥 (Passkey) 时,输入固件日志中打印的六位数字即可。固件控制台输出应类似以下日志(地址和设备名后缀会有所不同):
```
I (548) ble_uart: BLE host task started
I (548) ble_uart: registered service 0x1800 handle=1
I (548) ble_uart: registered chr 0x2a00 def=2 val=3
I (548) ble_uart: registered chr 0x2a01 def=4 val=5
I (558) ble_uart: registered service 0x1801 handle=6
I (558) ble_uart: registered chr 0x2a05 def=7 val=8
I (568) ble_uart: registered chr 0x2b3a def=10 val=11
I (568) ble_uart: registered chr 0x2b29 def=12 val=13
I (578) ble_uart: registered service 6e400001-b5a3-f393-e0a9-e50e24dcca9e handle=14
I (578) ble_uart: registered chr 6e400002-b5a3-f393-e0a9-e50e24dcca9e def=15 val=16
I (588) ble_uart: registered chr 6e400003-b5a3-f393-e0a9-e50e24dcca9e def=17 val=18
I (608) NimBLE: GAP procedure initiated: stop advertising.
I (608) NimBLE: GAP procedure initiated: stop advertising.
I (608) ble_uart: addr=74:4d:bd:a9:ed:72
I (608) NimBLE: GAP procedure initiated: advertise;
I (618) NimBLE: disc_mode=2
I (618) NimBLE: adv_channel_map=0 own_addr_type=0 adv_filter_policy=0 adv_itvl_min=0 adv_itvl_max=0
I (628) NimBLE:
I (628) ble_uart: advertising as 'BleUart-ED72'
I (628) main_task: Returned from app_main()
```
`ble_uart: addr=74:4d:bd:a9:ed:72` 这条日志指示了设备的蓝牙 MAC 地址为 `74:4D:BD:A9:ED:72`。设备以最后一行 `ble_uart: advertising as 'BleUart-XXXX'` 中显示的名称广播。
当中央设备发起连接时,固件会输出配对密钥提示。如果系统蓝牙对话框或 `connection-check` 命令要求输入配对密钥,请输入监视器中显示的六位数字:
```
W (19298) ble_uart: +-----------------------------+
W (19298) ble_uart: | BLE PAIRING PASSKEY: |
W (19298) ble_uart: | 617138 |
W (19298) ble_uart: +-----------------------------+
```
### 扫描 BLE UART 设备
打开第二个终端:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py list-devices
```
将输出中的设备标识记为 `DEVICE_ID`。可以通过输出中的 MAC 地址或设备名来判断是否扫描到了目标设备。在 Linux 上,设备 MAC 地址会直接显示:
```
> python main.py list-devices
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 74:4D:BD:A9:ED:72, with name BleUart-ED72, rssi=-46
```
此输出中的 MAC 地址 `74:4D:BD:A9:ED:72` 和设备名 `BleUart-ED72` 均与上述固件日志一致。
在 macOS 上,由于系统限制,工具无法显示设备的真实蓝牙 MAC 地址,而是分配一个 CoreBluetooth UUID 作为设备标识。需要通过匹配 `list-devices` 输出中的设备名(此例中为 `BleUart-ED72`)与固件日志中的广播名,找到对应的 UUID:
```
> python main.py list-devices
2026-06-05 11:19:56.728 | INFO | src.core.scanner:scan_devices:42 - Scanning for nearby BLE devices in 5.0s...
2026-06-05 11:19:57.108 | SUCCESS | src.core.scanner:on_detect:39 - Found: 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5, with name BleUart-ED72, rssi=-46
```
此例中的 `5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5` 即 macOS 下用作 `DEVICE_ID` 的 CoreBluetooth UUID。
### 打开控制台前检查连接
```bash
python main.py connection-check "<DEVICE_ID>"
```
该命令会连接设备、发现 BLE UART 服务和特征值,然后断开。在 Linux 或 Windows 上,将设备 MAC 地址作为 `DEVICE_ID` 传入:
```
> python main.py connection-check 74:4D:BD:A9:ED:72
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 74:4D:BD:A9:ED:72...
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 74:4D:BD:A9:ED:72!
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 74:4D:BD:A9:ED:72...
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 74:4D:BD:A9:ED:72
```
在 macOS 上,改用 CoreBluetooth UUID:
```
> python main.py connection-check 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
2026-06-05 12:06:27.252 | INFO | src.core.bridge:connect:139 - Connecting to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
2026-06-05 12:06:37.460 | SUCCESS | src.core.bridge:connect:206 - Succeeded to connect to 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5!
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_disconnect_locked:86 - Disconnecting from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5...
2026-06-05 12:06:37.461 | INFO | src.core.bridge:_handle_disconnect:120 - Disconnected from 5BA2476C-CDD2-BF3F-F98C-252CFA45F8B5
```
如果此步骤失败,请先解决扫描、配对、权限或广播问题,再继续使用守护进程或 OpenCode。
### 打开 ESP-BLE-UART 控制台
```bash
python main.py console "<DEVICE_ID>" --terminator lf
```
在控制台中输入一行短文本并按 Enter:
```
hello from console
```
预期结果:
```
[INFO] Connected to 68:B6:B3:55:41:76
[TX] hello from console
[RX] hello from console
```
- Bluetooth LE 地址因设备而异。
- 控制台中显示输入内容对应的 `[TX]` 行。
- ESP-BLE-UART 示例将相同字节回显,并显示为 `[RX]` 输出。
至此,Bluetooth LE 扫描、连接、主机到设备写入、设备到主机通知 (Notify) 均已验证通过。JSONL 协议(用于 ESP-VoCat 示例)、守护进程和 OpenCode 插件是叠加在该链路之上的应用层,不替代该链路本身。
更多控制台选项(如十六进制模式、带响应写入 (Write with Response)、不同换行符)请参考 [`tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md`](../../../tools/ble/ble_uart_bridge/docs/Quick-Start-BLE-UART-Console.md)。
## 第二部分:ESP-VoCat OpenCode 伴侣设备
### ESP-VoCat 简介
[ESP-VoCat](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32s3/esp-vocat/index.html) 是基于 ESP32-S3 模组的智能 AI 开发套件,配备圆形触摸显示屏和单键输入。
[esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库中包含一个 `ble_uart_service` 示例(位于 `examples/bluetooth/ble_uart_service`),该示例运行在 ESP-VoCat 开发板上。此示例固件将会话状态渲染为表情动画,并将权限请求呈现到屏幕上供用户物理审批。支持的板型、所需组件版本和构建说明请参考 esp-iot-solution 中的示例 README。
### 选择 JSON Lines 的原因
Bluetooth LE 写入按 ATT MTU 分包,不等同于应用层消息边界。ESP-VoCat 的 OpenCode 流程在 BLE UART 上使用 JSON Lines (JSONL)。选择 JSONL 的原因:日志和控制台中可直接阅读,便于手动测试时输入,固件侧可用 cJSON 解析,且同时支持请求/响应和即发即弃两种消息模式。
### 架构概述
ESP-BLE-UART 桥接工具和 OpenCode 演示插件已包含在 ESP-IDF 的 master 及 `release/v5.2` 及以上 release 分支中,位于 `tools/ble/ble_uart_bridge/` 目录下。`ble_uart_service` 示例实现设备侧协议,源码位于 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库。
```mermaid
flowchart LR
OC[OpenCode] -->|session.status / permission.asked| Plugin[OpenCode Plugin]
Plugin -->|POST /notify| Daemon[ESP-BLE-UART Daemon]
Plugin -->|POST /request| Daemon
Daemon -->|Bluetooth LE write: NUS RX JSONL| ESP[ble_uart_service 示例]
ESP -->|Bluetooth LE notify: NUS TX JSONL| Daemon
Daemon -->|HTTP response| Plugin
Plugin -->|permission reply| OC
ESP --> Display[Emote + Tip Text]
ESP --> Key[Single Key: once / reject]
```
各层均可独立替换:
- 控制台验证原始 BLE UART 链路;
- 守护进程维持一个 Bluetooth LE 连接并提供本地 HTTP API;
- OpenCode 插件将编辑器事件转换为守护进程请求/通知;
- `ble_uart_service` 示例在 ESP-VoCat 设备上显示状态和权限提示 (Permission Prompt)。
### 启动 ESP-BLE-UART 守护进程
首先从 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库将 `ble_uart_service` 示例烧录到 ESP-VoCat 开发板上,该示例与第一部分的控制台回显服务器为不同应用:
```bash
# 如尚未克隆 esp-iot-solution,先执行克隆
git clone https://github.com/espressif/esp-iot-solution.git
cd esp-iot-solution/examples/bluetooth/ble_uart_service
idf.py set-target esp32s3
idf.py build flash monitor
```
依赖版本和板级配置详见 esp-iot-solution 中的示例 README。
然后重新扫描设备,并将 ESP-VoCat 的设备标识记为 `VOCAT_DEVICE_ID`:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py list-devices
python main.py connection-check "<VOCAT_DEVICE_ID>"
```
使用该 ESP-VoCat 设备启动守护进程:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py daemon "<VOCAT_DEVICE_ID>" --host 127.0.0.1 --port 8888
```
> **Note:** 守护进程 HTTP API 未内置认证机制。除非自行添加访问控制,否则请保持绑定在 `127.0.0.1`。
在另一个终端检查守护进程状态:
```bash
cd $IDF_PATH/tools/ble/ble_uart_bridge
python main.py daemon-status
```
### 在接入 OpenCode 前通过守护进程验证 ESP-VoCat
不要使用通用 `echo` 请求验证 ESP-VoCat,该固件未实现 echo 操作。请使用 [esp-iot-solution 示例中的 json_format.md](https://github.com/espressif/esp-iot-solution/blob/master/examples/bluetooth/ble_uart_service/json_format.md) 中定义的操作。
会话状态 (Session Status) 冒烟测试:
```bash
python main.py daemon-notify --op session.status --json '{
"v": 1,
"kind": "session.status",
"event_id": "evt_manual",
"session_id": "ses_manual",
"requires_reply": false,
"payload": {
"type": "busy"
}
}'
python main.py daemon-notify --op session.status --json '{
"v": 1,
"kind": "session.status",
"event_id": "evt_manual",
"session_id": "ses_manual",
"requires_reply": false,
"payload": {
"type": "idle"
}
}'
```
CLI 将每个 JSON 对象作为守护进程信封 (Envelope) 的 `data` 字段发送,并设置 `op: "session.status"` 和 `id: ""`。固件通过 Bluetooth LE 收到完整 JSONL 信封后,更新显示但不返回响应。
权限请求 (Permission Request) 冒烟测试:
```bash
python main.py daemon-send --op permission.request --timeout 35 --json '{
"v": 1,
"kind": "permission.request",
"event_id": "evt_manual",
"session_id": "ses_manual",
"permission_id": "perm_manual",
"requires_reply": true,
"payload": {
"id": "perm_manual",
"sessionID": "ses_manual",
"type": "bash",
"title": "Run idf.py build",
"metadata": {
"command": "idf.py build"
}
}
}'
```
ESP-VoCat 设备应显示一个权限提示:
| ESP-VoCat 输入 | 设备响应 |
|----------------|---------------------|
| 单击 | `decision: "once"` |
| 长按 | `decision: "reject"` |
| 30 秒无输入 | `decision: "reject"` |
该手动守护进程测试验证的是 OpenCode 插件后续使用的同一条请求/响应链路。
### 安装 OpenCode 演示插件
OpenCode 演示插件已包含在 ESP-IDF 中,位于 `tools/ble/ble_uart_bridge/demos/opencode`。
项目级安装:
```bash
mkdir -p <proj-path>/.opencode/plugins/opencode-ble-uart-bridge
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
<proj-path>/.opencode/plugins/opencode-ble-uart-bridge/
```
用户级安装:
```bash
mkdir -p ~/.config/opencode/plugins/opencode-ble-uart-bridge
cp $IDF_PATH/tools/ble/ble_uart_bridge/demos/opencode/src/*.ts \
~/.config/opencode/plugins/opencode-ble-uart-bridge/
```
然后配置 OpenCode。项目级安装时,将以下内容放入 `<proj-path>/opencode.json`,或合并到已有配置中:
```json
{
"$schema": "https://opencode.ai/config.json",
"plugin": [
".opencode/plugins/opencode-ble-uart-bridge/opencode-ble-uart-bridge.ts"
],
"permission": {
"edit": "ask"
}
}
```
用户级安装时,将 `plugin` 指向 `~/.config/opencode/plugins/opencode-ble-uart-bridge/` 下的入口文件。如果配置加载器不展开 `~`,请使用绝对路径。
常用插件环境变量:
```bash
export OPENCODE_BLE_DAEMON_URL="http://127.0.0.1:8888"
export OPENCODE_BLE_DECISION_TIMEOUT_SECONDS=60
export OPENCODE_BLE_DEBUG=1
```
修改插件文件、`opencode.json` 或上述环境变量后,需要重启 OpenCode。
### 运行 OpenCode 演示
1. 保持固件运行,并处于广播或已连接状态。
2. 保持 ESP-BLE-UART 守护进程运行在 `127.0.0.1:8888`。
3. 在已配置插件的项目中启动 OpenCode。
4. 触发一次权限提示,例如当 `permission.edit` 设置为 `ask` 时执行编辑操作。
预期结果:
- OpenCode 会话状态以尽力传递 (Best-effort) 方式转发为 `session.status`;
- ESP-VoCat 显示 busy/idle/retry 表情;
- 权限提示显示到 ESP-VoCat,附带命令、路径、URL 等紧凑元数据 (Metadata);
- 单击返回 `once` 给 OpenCode;
- 长按或超时返回 `reject`。
如需演示 bash 命令或工具执行权限,请确认 OpenCode 权限配置确实会对该工具类别发起询问;否则建议使用编辑权限 (Edit Permission) 作为触发路径。
<p align="center">
<img src="assets/ESP-VoCat-Asking-For-Permission.png" alt="ESP-VoCat 请求权限" width="80%">
<br><em>ESP-VoCat 请求权限</em>
</p>
> **Note:** 如需全面了解 Bluetooth Low Energy,请参见 [Bluetooth LE 概览](../../../docs/zh_CN/api-guides/ble/overview.rst)。关于 Bluetooth LE 连接管理和数据交换,请参考 [Bluetooth LE 多连接指南](../../../docs/zh_CN/api-guides/ble/ble-multiconnection-guide.rst)。
## 协议参考
固件协议详见 `ble_uart_service` 示例中的 `json_format.md`,位于 [esp-iot-solution](https://github.com/espressif/esp-iot-solution) 仓库 (`examples/bluetooth/ble_uart_service/json_format.md`)。外层守护进程信封格式如下:
```
{"v":1,"id":"<bridge-request-id>","op":"<operation>","data":{}}
```
- `id` 非空表示请求/响应操作,例如 `permission.request`;
- `id` 为空表示即发即弃通知,例如 `session.status` 和 `permission.cancel`;
- 设备响应会带回相同的非空 `id`,并返回 `ok/data` 或 `ok:false/error`。
Bluetooth LE 上的 JSONL 权限请求示例:
```json
{
"v": 1,
"id": "perm-001",
"op": "permission.request",
"data": {
"v": 1,
"kind": "permission.request",
"event_id": "evt_...",
"session_id": "ses_...",
"permission_id": "perm_...",
"requires_reply": true,
"payload": {
"id": "perm_...",
"sessionID": "ses_...",
"type": "bash",
"title": "Run idf.py build",
"metadata": {
"command": "idf.py build"
}
}
}
}
```
设备响应示例:
```json
{
"v": 1,
"id": "perm-001",
"ok": true,
"data": {
"decision": "once",
"message": "Approved from BLE device"
}
}
```
`permission.cancel` 用于清理过期的权限提示,不再发送迟到的决策。适用于用户已在 OpenCode 终端界面 (TUI) 中处理了权限请求、但 ESP-VoCat 仍在显示权限提示的情况。
## 故障排查
- **扫描不到设备:** 确认主机蓝牙权限、固件正在广播、设备距离足够近。先用 `list-devices` 和 `connection-check` 排查。
- **控制台可用但守护进程不可用:** 确认控制台已关闭;该固件同一时间只接受一个 Bluetooth LE 连接。
- **守护进程断开连接:** 守护进程没有后台自动重连循环。当下一次 `/request` 或 `/notify` HTTP 请求到达时,守护进程会尝试按需重连。如果设备连续多次不可达,守护进程将自动退出。可使用 `daemon-status` 查看当前连接状态和重连失败计数。
- **OpenCode 未转发事件:** 确认 `OPENCODE_BLE_DAEMON_URL`,运行 `daemon-status`,修改配置后重启 OpenCode。
- **权限请求超时:** 确认设备收到的是带非空 `id` 的 `permission.request`,没有旧的权限提示仍在等待中,且用户在超时前已按下按键。
- **出现意外拒绝:** 演示采用失败即关闭 (Fail-closed) 设计。Bluetooth LE 链路、守护进程、插件或设备决策处理中任一环节失败,OpenCode 侧都会拒绝而非静默允许。
- **配对失败:** 检查固件日志中打印的配对密钥 (Passkey),确认中央设备端输入的是同一个值。
- **认证不足 (Insufficient Authentication):** 如果连接或特征值访问时报认证错误,请先通过系统蓝牙设置完成设备配对,并输入固件监视器日志中显示的六位数配对密钥。部分桌面 Bluetooth LE 协议栈要求在系统层面完成显式配对后,GATT 操作才能成功。
## 扩展方向
- 增加更多输入手势,支持 `always`、`edit` 或"本会话拒绝"。
- 为命令/路径/URL 元数据设计更丰富的显示布局。
- 在设备侧增加权限提示超时设置。
- 为低风险命令增加允许列表 (Allowlist)。
- 使用模拟守护进程和模拟固件响应进行集成测试。
- 如需更低开销,将 JSONL 替换为紧凑二进制协议。
## 总结
本演示中的每一层均保持简单且可独立测试:控制台验证原始 BLE UART 链路,守护进程将一个 Bluetooth LE 连接转换为本地 HTTP 桥接,OpenCode 插件将编辑器事件映射为守护进程请求,ESP-VoCat 提供物理 UI。任意一层均可单独替换而不影响其余部分。
+169 -25
View File
@@ -1,4 +1,4 @@
# BLE UART Service Example — NimBLE / Bluedroid
# ESP-BLE-UART Example — NimBLE / Bluedroid
| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-S3 |
| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | -------- |
@@ -24,12 +24,19 @@ ble_uart_install(&cfg); // NimBLE host + BLE UART GATT service
ble_uart_open(); // start advertising + auto-encrypt
```
…and two matching tear-down calls if your app ever needs to power
BLE off at runtime:
If your app powers BLE off at runtime, use **one** of the release paths
in [PORTING.md §5.3](../common/ble_uart/PORTING.md#53-lifecycle--bring-up-and-release)
(this example uses Path A from `app_main`):
| Path | When | Calls |
| --- | --- | --- |
| **A — sync** (default) | Shutdown from a normal task (button, Wi-Fi, `app_main`) | `ble_uart_close()` → `ble_uart_uninstall()` |
| **B — async** | Shutdown triggered inside `on_event` / `on_rx` | `close_async()` in callback → `CLOSED` sets flag → **`uninstall()` on a separate app task** (not inside `CLOSED`) |
```c
ble_uart_close(); // stop advertising / disconnect / halt host
ble_uart_uninstall(); // free the NimBLE port + reset state
/* Path A — this example style */
ble_uart_close();
ble_uart_uninstall();
```
When a central connects, the firmware automatically initiates LE Secure
@@ -47,49 +54,135 @@ back with `ble_uart_tx()`.
| TX (out) | `6e400003-b5a3-f393-e0a9-e50e24dcca9e` | Notify (auto-CCCD) | encrypted, authenticated |
The `_ENC | _AUTHEN` flags are turned on only when `cfg.encrypted = true`
(the default in this example).
(the default in this example). The two flags can be controlled
independently via `cfg.security.mitm` (drops `_AUTHEN`) and the
combined `cfg.security.{sc,bonding,mitm}` set (all OFF drops `_ENC`
too) — see PORTING.md §5.6.
## Files
| File | Lines | Role |
| --- | ---: | --- |
| `main/main.c` | ~70 | NVS init, MAC-derived device name, install + open, RX echo handler. Identical for both backends. |
| `main/main.c` | ~200 | NVS init, install + open with `<prefix>-XXXX` device name (Kconfig prefix + BT MAC suffix), RX echo handler, lifecycle/link-state event sink, bonded-peer dump on boot. Identical for both backends. |
| `main/Kconfig.projbuild` | ~50 | Example-local `EXAMPLE_CUSTOM_ADV_DATA` switch — toggles the `ble_uart_config_t::adv_data` demo path in `main.c`. |
| `CMakeLists.txt` (root) | ~15 | `list(APPEND EXTRA_COMPONENT_DIRS .../common/ble_uart)` before `project()` so `main` can `REQUIRES ble_uart`. |
| `../common/ble_uart/ble_uart.h` | ~155 | Stack-agnostic public API: 3-field config + 4 lifecycle functions + TX/status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
| `../common/ble_uart/ble_uart_nimble.c` | ~650 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising, pairing, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1020 | Bluedroid backend: controller + host enable, BLE UART GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising, pairing, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
| `../common/ble_uart/Kconfig` | ~30 | Device-name prefix + RX scratch size (`menuconfig → Component configuration → BLE UART library`). |
| `../common/ble_uart/PORTING.md` | ~724 | Porting and API guide (integration, CMake, sdkconfig, thread safety). |
| `../common/ble_uart/ble_uart.h` | ~640 | Stack-agnostic public API: configuration struct (preset + per-feature security overrides + custom adv payload + RX/event callbacks) + lifecycle (install/open/close/close_async/uninstall) + TX + pairing replies + bond-management + status + UUID + `BLE_UART_E*` return codes. No NimBLE / Bluedroid types leak through. |
| `../common/ble_uart/ble_uart_nimble.c` | ~1290 | NimBLE backend: host bring-up, BLE UART GATT service via `ble_gatts_add_svcs`, advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, install/open/close/uninstall. Active when `CONFIG_BT_NIMBLE_ENABLED=y`. |
| `../common/ble_uart/ble_uart_bluedroid.c` | ~1660 | Bluedroid backend: controller + host enable, BLE UART GATT service via `esp_ble_gatts_create_attr_tab` (service-table API), advertising (default + raw), pairing (incl. Passkey Entry / Numeric Comparison), bond store, async close, full PREP/EXEC long-write reassembly, install/open/close/uninstall. Active when `CONFIG_BT_BLUEDROID_ENABLED=y`. |
| `../common/ble_uart/Kconfig` | ~30 | Device name prefix + RX scratch size (`menuconfig → Component configuration → ESP-BLE-UART library`). |
| `../common/ble_uart/PORTING.md` | ~1300 | Porting and API guide (integration, CMake, sdkconfig, security model, custom advertising, bond management, thread safety). |
| `sdkconfig.defaults` | — | Default: NimBLE backend, MTU 512, SC + bonding + persistent NVS. |
| `sdkconfig.bluedroid` | — | Overlay: switch to Bluedroid backend (used via `-D SDKCONFIG_DEFAULTS=...`, see "Choosing the host stack" below). |
## Public API
```c
typedef void (*ble_uart_rx_cb_t)(const uint8_t *data, size_t len);
typedef void (*ble_uart_rx_cb_t) (const uint8_t *data, size_t len);
typedef void (*ble_uart_evt_cb_t)(const ble_uart_evt_t *evt);
typedef struct {
bool encrypted; /* SC + Bonding + MITM in one knob */
const char *device_name;
ble_uart_rx_cb_t ble_uart_on_rx;
ble_uart_sec_t sc; /* AUTO / OFF / ON */
ble_uart_sec_t bonding;
ble_uart_sec_t mitm;
ble_uart_io_cap_t io_cap; /* AUTO / NO_INPUT_OUTPUT / DISPLAY_ONLY /
KEYBOARD_ONLY / DISPLAY_YES_NO /
KEYBOARD_DISPLAY */
} ble_uart_security_t;
typedef struct {
bool encrypted; /* preset: SC + Bonding + MITM + DisplayOnly */
ble_uart_security_t security; /* per-feature overrides; see PORTING.md §5.6 */
const char *device_name; /* ≤ BLE_UART_DEVICE_NAME_MAX (26) */
/* Optional: raw advertising / scan-response bytes (NULL → defaults).
* Limits: adv_data_len ≤ BLE_UART_ADV_DATA_MAX (28),
* scan_rsp_data_len ≤ BLE_UART_SCAN_RSP_DATA_MAX (31).
* The 3-byte Flags AD element is prepended automatically — don't
* include it in adv_data. See PORTING.md §5.9 for examples. */
const uint8_t *adv_data;
size_t adv_data_len;
const uint8_t *scan_rsp_data;
size_t scan_rsp_data_len;
ble_uart_rx_cb_t ble_uart_on_rx;
ble_uart_evt_cb_t on_event; /* lifecycle / link-state events; NULL drops */
} ble_uart_config_t;
typedef struct {
uint8_t bytes[6]; /* big-endian: bytes[0] is the MSB (AA:BB:CC:DD:EE:FF) */
uint8_t type; /* BLE_UART_ADDR_TYPE_PUBLIC | _RANDOM */
} ble_uart_addr_t;
/* Lifecycle */
int ble_uart_install(const ble_uart_config_t *cfg); /* NimBLE host + GATT */
int ble_uart_install(const ble_uart_config_t *cfg); /* host + GATT */
int ble_uart_open(void); /* host task + advertising */
int ble_uart_close(void); /* stop adv / disconnect / halt host */
int ble_uart_uninstall(void); /* free NimBLE port + reset state */
int ble_uart_close_async(void); /* same, fire-and-forget; safe from inside on_event/on_rx */
int ble_uart_uninstall(void); /* free port + reset state */
/* Data path */
int ble_uart_tx(const uint8_t *data, size_t len);
/* Pairing replies (call from on_event for input-capable IO caps) */
int ble_uart_passkey_reply(uint32_t passkey); /* answer PASSKEY_REQUEST */
int ble_uart_compare_reply(bool match); /* answer NUMERIC_COMPARE */
/* Status (best-effort snapshot) */
bool ble_uart_is_connected(void);
bool ble_uart_is_subscribed(void);
/* Bond management (works after install()) */
int ble_uart_get_bond_count(size_t *out_count);
int ble_uart_get_bonded_peers(ble_uart_addr_t *out, size_t cap, size_t *out_count);
int ble_uart_remove_peer(const ble_uart_addr_t *peer);
int ble_uart_clear_bonds(void);
extern const ble_uart_uuid128_t ble_uart_service_uuid;
```
### Event callback
`on_event` is invoked on the BLE host task (same context as `ble_uart_on_rx`)
with a tagged `ble_uart_evt_t`. Use `LINK_SECURE` — not `is_connected()` —
to gate any application logic that requires the channel to be encrypted /
authenticated:
| `evt->id` | Payload | Fires when |
| ------------------------------- | ------------------------------------------------------- | ---------- |
| `BLE_UART_EVT_CONNECTED` | `connected.peer` | Physical link up |
| `BLE_UART_EVT_DISCONNECTED` | `disconnected.reason` (int, stack-specific) | Physical link down — Bluedroid: `esp_gatt_conn_reason_t`; NimBLE: BLE host return code (`BLE_HS_HCI_ERR()` for HCI) |
| `BLE_UART_EVT_SUBSCRIBED` | `subscribed.subscribed` | Central writes CCCD on TX (edge-triggered) |
| `BLE_UART_EVT_LINK_SECURE` | `link_secure.{encrypted,authenticated,bonded,key_size}` | Pairing or bonded reconnect succeeds |
| `BLE_UART_EVT_PASSKEY_DISPLAY` | `passkey.passkey` (0..999999) | SM asks the device to show a passkey |
| `BLE_UART_EVT_PASSKEY_REQUEST` | — | SM asks the user to enter a passkey shown by the central — answer with `ble_uart_passkey_reply()` |
| `BLE_UART_EVT_NUMERIC_COMPARE` | `numeric_compare.passkey` (0..999999) | SM asks the user to confirm both sides display the same value — answer with `ble_uart_compare_reply()` |
| `BLE_UART_EVT_PAIRING_FAILED` | `pairing_failed.reason` (stack-specific) | Pairing rejected or timed out |
| `BLE_UART_EVT_CLOSED` | `closed.status` (`BLE_UART_*`) | `ble_uart_close_async()` worker has finished; `BLE_UART_OK` means tear-down succeeded |
The default passkey UART banner still prints; the callback is additive so
log-scraping tests stay compatible. Don't block in the callback.
**Callback rules:**
- Do **not** call `ble_uart_close()` or `ble_uart_uninstall()` from
`on_event` / `on_rx` (host task — deadlocks).
- To start teardown from a callback, call `ble_uart_close_async()` only.
- Call `ble_uart_uninstall()` from a **normal app task** after
`BLE_UART_EVT_CLOSED` with `closed.status == BLE_UART_OK` (see
[PORTING.md §5.3.2](../common/ble_uart/PORTING.md#532-path-b--release-after-a-ble-event-close_async)).
Path B sketch (full code in PORTING.md):
```c
case BLE_UART_EVT_PAIRING_FAILED:
ble_uart_close_async();
break;
case BLE_UART_EVT_CLOSED:
if (e->closed.status == BLE_UART_OK) {
s_ble_closed_ok = true; /* app task calls uninstall */
}
break;
```
## Choosing the host stack
The same `ble_uart.h` API is implemented twice — once on top of NimBLE
@@ -133,9 +226,11 @@ When neither is enabled the build fails up-front with a clear error.
```bash
idf.py set-target esp32c3 # or esp32, esp32s3, esp32c6, esp32h2 ...
idf.py menuconfig # optional
# Component configuration -> BLE UART library
# - BLE device name prefix (default: BleUart)
# Component configuration -> ESP-BLE-UART library
# - BLE device name prefix (default: BleUart; example appends -XXXX from BT MAC)
# - RX scratch buffer size (default: 1024 bytes)
# BLE UART service example
# - Use custom advertising data (default: off)
```
Those `BLE_UART_*` options are defined in **`../common/ble_uart/Kconfig`**
@@ -143,6 +238,27 @@ Those `BLE_UART_*` options are defined in **`../common/ble_uart/Kconfig`**
build (this example pulls it in via `EXTRA_COMPONENT_DIRS` in the root
`CMakeLists.txt`).
`EXAMPLE_CUSTOM_ADV_DATA` is example-local (`main/Kconfig.projbuild`)
and demonstrates `ble_uart_config_t::adv_data` — the field that lets
the application fully control the over-the-air advertising payload
instead of using the library default.
When the option is on, `app_main` hands a static byte array
(`example_adv_payload[]`, top of `main.c`) to `ble_uart_install()`.
The array is just a sequence of `[length][AD type][value]` triplets;
edit it directly to advertise whatever you want — a different Local
Name, Manufacturer Specific Data, custom Service Data, additional
Service UUIDs, etc. The only hard rule is total length ≤
`BLE_UART_ADV_DATA_MAX` (28); the 3-byte Flags AD is added by the
library and does not count against that budget.
The GAP-service Device Name (set via `device_name` in the same
config struct) is independent and is what connected centrals read
post-pair, regardless of `adv_data`.
With the option off the library default is used (Complete Local Name
in the primary packet, 128-bit Service UUID in the scan response).
The two security knobs are set in `sdkconfig.defaults`:
```ini
@@ -154,6 +270,17 @@ Disable `cfg.encrypted` in `main.c` (set it to `false`) for plaintext
operation in the lab — the GATT characteristics drop their `_ENC`
flags accordingly. Production firmware should keep encryption on.
For finer control without going all-or-nothing — e.g. a displayless
gateway that wants encryption + bonding but no passkey UI, or a
device with a keypad that wants Passkey Entry / Numeric Comparison —
keep `cfg.encrypted = true` and override individual bits via
`cfg.security.{sc,bonding,mitm,io_cap}`. The input-capable IO caps
(`KEYBOARD_ONLY`, `DISPLAY_YES_NO`, `KEYBOARD_DISPLAY`) require an
`on_event` handler that wires `BLE_UART_EVT_PASSKEY_REQUEST` /
`NUMERIC_COMPARE` to `ble_uart_passkey_reply()` /
`ble_uart_compare_reply()`. See PORTING.md §5.6 for the full matrix
and worked examples.
### Build & flash
```bash
@@ -170,7 +297,7 @@ I (xxx) ble_uart: registered chr 6e400002-... def=15 val=16
I (xxx) ble_uart: registered chr 6e400003-... def=17 val=18
I (xxx) ble_uart: addr=80:7d:3a:11:22:33
I (xxx) ble_uart: BLE host task started
I (xxx) ble_uart: advertising as 'BleUart-XXXX'
I (xxx) ble_uart: advertising as 'BleUart-2233'
```
Expected boot log (Bluedroid backend):
@@ -186,8 +313,10 @@ I (xxx) ble_uart: advertising started
1. On a phone, install **a BLE GATT client app** that supports scanning,
pairing, characteristic write, and notify/CCCD (many mobile “BLE tools”
or serial-over-BLE utilities qualify).
2. Scan, tap **Connect** on `BleUart-XXXX`. The phone prompts for a
6-digit code.
2. Scan, tap **Connect** on `BleUart-XXXX` (prefix from
`CONFIG_BLE_UART_DEVICE_NAME_PREFIX`, `XXXX` = last two BT MAC
bytes). The phone prompts for a 6-digit
code.
3. The device prints a fresh code in a banner on UART:
```
@@ -204,8 +333,13 @@ I (xxx) ble_uart: advertising started
6. Disconnect and reconnect: no passkey prompt — the bond resumes
automatically.
To wipe the bond and force a fresh passkey, run `idf.py erase-flash`
and re-flash.
To wipe the bond and force a fresh passkey there are three options:
- Call `ble_uart_clear_bonds()` from your app (preserves the rest of NVS)
- Call `ble_uart_remove_peer(&addr)` to drop one peer (use the address
reported in `BLE_UART_EVT_CONNECTED`, or any address you happen to
have stored — Bluedroid matches by address only, NimBLE by identity)
- Run `idf.py erase-flash` and re-flash (also wipes WiFi creds, NVS, etc.)
## Adapting to your application
@@ -249,6 +383,16 @@ ble_uart_open();
That's it — encrypted serial-over-BLE in 4 lines.
## OpenCode Companion
This example serves as the transport layer for the [OpenCode Companion tutorial](OPENCODE_COMPANION.md), which walks through building a physical companion device for OpenCode using ESP-BLE-UART and ESP-VoCat. The tutorial covers:
- **Part 1:** Using ESP-BLE-UART Console to verify the BLE UART data path (Echo Server mode).
- **Part 2:** Using the `ble_uart_service` example on ESP-VoCat with the ESP-BLE-UART Daemon and OpenCode Plugin for session status display and physical permission approval.
See the full guide in English: [OPENCODE_COMPANION.md](OPENCODE_COMPANION.md)
Chinese version: [OPENCODE_COMPANION_CN.md](OPENCODE_COMPANION_CN.md)
## Troubleshooting
- **Phone shows "pairing failed"** — the central asked for "Just Works"
Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

@@ -0,0 +1,47 @@
menu "BLE UART service example"
config EXAMPLE_CUSTOM_ADV_DATA
bool "Use custom advertising data"
default n
help
Demonstrates `ble_uart_config_t::adv_data` — the field that
lets the application fully control the advertising payload
instead of relying on the library default.
When enabled, the example passes a static byte array
(`example_adv_payload[]` defined at the top of `main.c`) to
`ble_uart_install()`. Edit that array to broadcast anything
you want: a different Local Name, Manufacturer Specific
Data, custom Service Data, multiple Service UUIDs, etc.
Format
The array is a sequence of standard Bluetooth Core "AD
structure" triplets:
[length(1)] [AD type(1)] [value(length-1)]
See the Bluetooth Assigned Numbers (Generic Access
Profile) document for the full type list.
Length budget
Total bytes in the array must be
≤ BLE_UART_ADV_DATA_MAX (28). The 3-byte mandatory
Flags AD element is prepended automatically by
ble_uart and does NOT count against this budget. An
oversized buffer makes `ble_uart_install()` fail with
BLE_UART_EINVAL.
Scope
Only affects the over-the-air advertising payload.
The GAP-service Device Name (UUID 0x2A00, set via
`device_name` in the same struct) is independent and
stays whatever the application configured — connected
centrals read that name regardless of what is in
`adv_data`.
Default value
Off. The library default is used (Complete Local Name
in the primary packet, 128-bit Service UUID in the
scan response).
endmenu
+151 -2
View File
@@ -3,11 +3,12 @@
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*
* BLE UART Service example. Backend (NimBLE / Bluedroid) is picked
* ESP-BLE-UART example. Backend (NimBLE / Bluedroid) is picked
* by the host-stack Kconfig at compile time. Whatever the central
* writes to the RX characteristic is echoed back over TX.
*/
#include <inttypes.h>
#include <stdio.h>
#include "esp_log.h"
@@ -17,6 +18,41 @@
#include "ble_uart.h"
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
/* Sample advertising payload demonstrating ble_uart_config_t::adv_data.
* Replace these bytes with whatever your product needs (a different
* Local Name, Manufacturer Specific Data, custom Service Data,
* additional Service UUIDs, ...) — ble_uart broadcasts them verbatim.
*
* Format: a sequence of standard BT Core "AD structure" triplets,
* [length(1)] [AD type(1)] [value(length-1)].
*
* Length budget: total ≤ BLE_UART_ADV_DATA_MAX (28). The mandatory
* 3-byte Flags AD is prepended by ble_uart and does NOT count against
* this budget; oversize fails ble_uart_install() with EINVAL.
*
* The current contents (purely illustrative — edit freely):
*
* Layout bytes
* -------------------------------------- -----
* Complete Local Name AD "BleUart" 1 + 1 + 7 = 9
* Complete 128-bit UUID AD 1 + 1 + 16 = 18
* -------------------------------------- -----
* total 27 (≤ 28)
*/
static const uint8_t example_adv_payload[] = {
/* AD type 0x09: Complete Local Name */
0x08, 0x09, 'B', 'l', 'e', 'U', 'a', 'r', 't',
/* AD type 0x07: Complete List of 128-bit Service UUIDs.
* UUID bytes are in over-the-air (little-endian) order, matching
* ble_uart_service_uuid.bytes[]. */
0x11, 0x07,
0x9e, 0xca, 0xdc, 0x24, 0x0e, 0xe5, 0xa9, 0xe0,
0x93, 0xf3, 0xa3, 0xb5, 0x01, 0x00, 0x40, 0x6e,
};
#endif
static const char *TAG = "app";
static void ble_uart_on_rx(const uint8_t *data, size_t len)
@@ -29,6 +65,83 @@ static void ble_uart_on_rx(const uint8_t *data, size_t len)
ble_uart_tx(data, len); /* echo back */
}
/* Lifecycle / link-state event sink. Runs on the BLE host task —
* keep it short, never call ble_uart_close()/uninstall() from here.
*
* For production code: gate any sensitive TX on
* BLE_UART_EVT_LINK_SECURE (encrypted+authenticated) instead of just
* "connected"; ble_uart_is_connected() returns true while the link is
* still plaintext during the pairing window. */
static void ble_uart_on_event(const ble_uart_evt_t *e)
{
switch (e->id) {
case BLE_UART_EVT_CONNECTED: {
const uint8_t *b = e->connected.peer.bytes;
ESP_LOGI(TAG,
"evt: connected peer=%02x:%02x:%02x:%02x:%02x:%02x type=%u",
b[0], b[1], b[2], b[3], b[4], b[5], e->connected.peer.type);
break;
}
case BLE_UART_EVT_DISCONNECTED:
ESP_LOGI(TAG, "evt: disconnected reason=0x%x",
e->disconnected.reason);
break;
case BLE_UART_EVT_SUBSCRIBED:
ESP_LOGI(TAG, "evt: %ssubscribed",
e->subscribed.subscribed ? "" : "un");
break;
case BLE_UART_EVT_LINK_SECURE:
ESP_LOGI(TAG, "evt: link_secure enc=%d auth=%d bond=%d ks=%u",
e->link_secure.encrypted, e->link_secure.authenticated,
e->link_secure.bonded, e->link_secure.key_size);
break;
case BLE_UART_EVT_PASSKEY_DISPLAY:
ESP_LOGI(TAG, "evt: passkey=%06" PRIu32, e->passkey.passkey);
break;
case BLE_UART_EVT_PASSKEY_REQUEST:
/* Fires only when cfg.security.io_cap is KEYBOARD_ONLY or
* KEYBOARD_DISPLAY (this example leaves io_cap at AUTO →
* DisplayOnly, so it should not fire). For a real keypad
* product, prompt the user for the 6 digits the central
* displayed and feed them in:
*
* ble_uart_passkey_reply(digits);
*
* See PORTING.md §5.6.1 for the full pattern. */
ESP_LOGW(TAG, "evt: passkey entry requested — no UI wired in this "
"example (see PORTING.md §5.6.1)");
break;
case BLE_UART_EVT_NUMERIC_COMPARE:
/* Fires only when cfg.security.io_cap is DISPLAY_YES_NO or
* KEYBOARD_DISPLAY (likewise dormant in this example). For a
* product with a yes/no control, surface the digits to the
* user and resolve the comparison:
*
* ble_uart_compare_reply(user_says_match);
*
* See PORTING.md §5.6.1. */
ESP_LOGW(TAG, "evt: numeric compare %06" PRIu32
" — no yes/no UI wired (see PORTING.md §5.6.1)",
e->numeric_compare.passkey);
break;
case BLE_UART_EVT_PAIRING_FAILED:
ESP_LOGW(TAG, "evt: pairing failed reason=0x%x",
e->pairing_failed.reason);
break;
case BLE_UART_EVT_CLOSED:
/* Only after ble_uart_close_async(). This example does not use
* close_async; do not ble_uart_uninstall() here — defer to an
* app task (PORTING.md §5.3.2). Kept for -Wswitch. */
if (e->closed.status == BLE_UART_OK) {
ESP_LOGI(TAG, "evt: closed (async-close succeeded)");
} else {
ESP_LOGW(TAG, "evt: closed async-close failed status=%d",
e->closed.status);
}
break;
}
}
void app_main(void)
{
/* NVS is required by the BT controller (PHY calibration) and the
@@ -49,15 +162,51 @@ void app_main(void)
ESP_LOGW(TAG, "esp_read_mac(BT) failed (%s); device name suffix will be 0000",
esp_err_to_name(mac_err));
}
char name[24];
char name[BLE_UART_DEVICE_NAME_MAX + 1];
snprintf(name, sizeof(name), "%s-%02X%02X",
CONFIG_BLE_UART_DEVICE_NAME_PREFIX, mac[4], mac[5]);
ESP_ERROR_CHECK(ble_uart_install(&(ble_uart_config_t){
.encrypted = true,
.device_name = name,
#if CONFIG_EXAMPLE_CUSTOM_ADV_DATA
/* Hand the application-defined bytes to ble_uart. Whatever
* the array contains is broadcast verbatim; what `device_name`
* (above) holds is exposed via the GAP service for connected
* centrals to read — independent paths. */
.adv_data = example_adv_payload,
.adv_data_len = sizeof(example_adv_payload),
/* scan_rsp_data is left at its default (NULL) → ble_uart still
* sends its built-in scan response. Override it the same way
* if you want to control those bytes too. */
#endif
.ble_uart_on_rx = ble_uart_on_rx,
.on_event = ble_uart_on_event,
}));
/* Demonstrate the bond-management API: list every bonded peer
* already on flash. Replace the log with `ble_uart_clear_bonds()`
* to wipe them at boot (e.g. when a "factory reset" GPIO is held);
* use `ble_uart_remove_peer(&list[i])` to target one specifically. */
size_t total = 0;
ble_uart_addr_t list[8];
int rc = ble_uart_get_bonded_peers(list, sizeof(list) / sizeof(list[0]),
&total);
if (rc == 0) {
ESP_LOGI(TAG, "%u peer(s) currently bonded", (unsigned)total);
size_t shown = total < sizeof(list) / sizeof(list[0])
? total : sizeof(list) / sizeof(list[0]);
for (size_t i = 0; i < shown; i++) {
const uint8_t *b = list[i].bytes;
ESP_LOGI(TAG, " [%u] %02x:%02x:%02x:%02x:%02x:%02x type=%u",
(unsigned)i,
b[0], b[1], b[2], b[3], b[4], b[5], list[i].type);
}
if (total > shown) {
ESP_LOGI(TAG, " (%u more not shown)",
(unsigned)(total - shown));
}
}
ESP_ERROR_CHECK(ble_uart_open());
}
@@ -0,0 +1,24 @@
# CI build overlay: Bluedroid host (sdkconfig.defaults selects NimBLE).
# Mirrors sdkconfig.bluedroid; kept in sync for idf-build-apps CONFIG_NAME=bluedroid.
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_ENABLED=y
CONFIG_BT_NIMBLE_ENABLED=n
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_SMP_ENABLE=y
CONFIG_BT_GATTS_ENABLE=y
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
CONFIG_BT_BLE_42_ADV_EN=y
# CONFIG_BT_BLE_42_SCAN_EN is not set
@@ -0,0 +1,7 @@
# CI build overlay: NimBLE host (sdkconfig.defaults is NimBLE-first).
# Explicit config so idf-build-apps builds both nimble and bluedroid in CI.
CONFIG_BT_NIMBLE_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=n
CONFIG_BT_NIMBLE_SM_SC=y
CONFIG_BT_NIMBLE_NVS_PERSIST=y
@@ -46,6 +46,7 @@ typedef struct {
/* Latency test functions */
void latency_test_init(uint16_t conn_id, uint16_t char_handle);
void latency_test_start(void);
void latency_test_stop(void);
void latency_test_handle_notify(uint8_t *data, uint16_t len);
void latency_test_print_results(void);
@@ -132,7 +132,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
}
ESP_LOGI(GATTC_TAG, "discover service complete conn_id %d", param->dis_srvc_cmpl.conn_id);
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
esp_ble_gattc_search_service(gattc_if, param->dis_srvc_cmpl.conn_id, &remote_filter_service_uuid);
break;
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
@@ -311,6 +311,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_DISCONNECT_EVT:
connect = false;
get_server = false;
latency_test_stop();
ESP_LOGI(GATTC_TAG, "ESP_GATTC_DISCONNECT_EVT, reason = %d", p_data->disconnect.reason);
break;
default:
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -22,9 +22,10 @@ static latency_record_t records[TEST_PACKET_COUNT];
static uint16_t test_conn_id = 0;
static uint16_t char_handle = 0;
static test_packet_t send_packets[TEST_PACKET_COUNT];
static bool test_running = false;
/* Polled by latency_test_task and written by latency_test_stop() from another context. */
static volatile bool test_running = false;
static bool test_initialized = false;
static esp_gatt_if_t test_gattc_if = 0;
static esp_gatt_if_t test_gattc_if = ESP_GATT_IF_NONE;
/**
* Fill random data
@@ -45,7 +46,6 @@ latency_test_init(uint16_t conn_id, uint16_t handle)
{
test_conn_id = conn_id;
char_handle = handle;
test_running = false;
test_initialized = true;
memset(records, 0, sizeof(records));
@@ -69,7 +69,7 @@ latency_test_set_gattc_if(esp_gatt_if_t gattc_if)
static int
send_test_packet(uint16_t seq)
{
if (char_handle == 0 || test_gattc_if == 0) {
if (char_handle == 0 || test_gattc_if == ESP_GATT_IF_NONE) {
ESP_LOGE(TAG, "Test not initialized");
return -1;
}
@@ -113,21 +113,19 @@ latency_test_task(void *arg)
test_running = true;
/* Send all test packets */
for (int i = 0; i < TEST_PACKET_COUNT; i++) {
/* Send all test packets, abort early if latency_test_stop() is called */
for (int i = 0; i < TEST_PACKET_COUNT && test_running; i++) {
int rc = send_test_packet(i);
if (rc != 0) {
ESP_LOGW(TAG, "Send failed for seq=%d", i);
}
/* Wait interval */
vTaskDelay(pdMS_TO_TICKS(TEST_PACKET_INTERVAL_MS));
}
ESP_LOGI(TAG, "All packets sent, waiting for responses...");
/* Wait for all responses */
vTaskDelay(pdMS_TO_TICKS(2000));
if (test_running) {
ESP_LOGI(TAG, "All packets sent, waiting for responses...");
vTaskDelay(pdMS_TO_TICKS(2000));
}
/* Print results */
latency_test_print_results();
@@ -152,9 +150,26 @@ latency_test_start(void)
return;
}
if (test_gattc_if == ESP_GATT_IF_NONE) {
ESP_LOGE(TAG, "GATT client interface not set");
return;
}
xTaskCreate(latency_test_task, "latency_test", 4096, NULL, 5, NULL);
}
/**
* Stop latency test
*
* Only raises the stop flag; the task observes it at the next loop iteration
* (or vTaskDelay boundary) and self-terminates via vTaskDelete(NULL).
*/
void
latency_test_stop(void)
{
test_running = false;
}
/**
* Handle notification
*/
@@ -189,6 +204,11 @@ latency_test_handle_notify(uint8_t *data, uint16_t len)
return;
}
if (records[seq].received) {
ESP_LOGD(TAG, "Duplicate notify for seq=%d, ignored", seq);
return;
}
/* Record receive time */
records[seq].recv_time_us = recv_time;
records[seq].received = true;
@@ -5,3 +5,10 @@ CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT client only example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -7,6 +7,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
@@ -18,6 +19,7 @@
#include "esp_gatts_api.h"
#include "esp_bt_main.h"
#include "esp_gatt_common_api.h"
#include "esp_gatt_defs.h"
#include "gatts_latency.h"
#define GATTS_TAG "BLE_ACL_LATENCY_PERIPH"
@@ -191,16 +193,18 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
}
esp_err_t ret = esp_ble_gap_config_adv_data(&adv_data);
if (ret){
if (ret != ESP_OK) {
ESP_LOGE(GATTS_TAG, "Config adv data failed, error code = %x", ret);
} else {
adv_config_done |= adv_config_flag;
}
adv_config_done |= adv_config_flag;
ret = esp_ble_gap_config_adv_data(&scan_rsp_data);
if (ret){
if (ret != ESP_OK) {
ESP_LOGE(GATTS_TAG, "Config scan response data failed, error code = %x", ret);
} else {
adv_config_done |= scan_rsp_config_flag;
}
adv_config_done |= scan_rsp_config_flag;
esp_err_t create_attr_ret = esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, LATENCY_IDX_NB, 0);
if (create_attr_ret){
@@ -209,7 +213,41 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
}
break;
case ESP_GATTS_READ_EVT:
ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT");
ESP_LOGD(GATTS_TAG, "ESP_GATTS_READ_EVT, handle=%d, need_rsp=%d", param->read.handle, param->read.need_rsp);
if (!param->read.need_rsp) {
break;
}
{
esp_gatt_rsp_t rsp;
memset(&rsp, 0, sizeof(rsp));
rsp.attr_value.handle = param->read.handle;
rsp.attr_value.offset = param->read.offset;
uint16_t vlen = 0;
const uint8_t *v = NULL;
esp_gatt_status_t gst = esp_ble_gatts_get_attr_value(param->read.handle, &vlen, &v);
if (gst != ESP_GATT_OK || v == NULL) {
rsp.attr_value.len = 0;
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
ESP_GATT_ERROR, &rsp);
break;
}
if (param->read.offset > vlen) {
rsp.attr_value.len = 0;
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id,
ESP_GATT_INVALID_OFFSET, &rsp);
break;
}
uint16_t remain = vlen - param->read.offset;
uint16_t copy_len = remain > ESP_GATT_MAX_ATTR_LEN ? ESP_GATT_MAX_ATTR_LEN : remain;
if (copy_len > 0) {
memcpy(rsp.attr_value.value, v + param->read.offset, copy_len);
}
rsp.attr_value.len = copy_len;
esp_ble_gatts_send_response(gatts_if, param->read.conn_id, param->read.trans_id, ESP_GATT_OK, &rsp);
}
break;
case ESP_GATTS_WRITE_EVT:
if (!param->write.is_prep){
@@ -220,9 +258,35 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
esp_ble_gatts_send_indicate(gatts_if, param->write.conn_id, handle_table[LATENCY_IDX_CHAR_VAL],
param->write.len, param->write.value, false);
}
if (param->write.need_rsp) {
esp_err_t sr = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id,
ESP_GATT_OK, NULL);
if (sr != ESP_OK) {
ESP_LOGE(GATTS_TAG, "send_response failed: %s", esp_err_to_name(sr));
}
}
} else {
/* This latency demo does not support Prepare/Long/Reliable Write.
* Reply with ATT Error 0x06 (Request Not Supported) instead of leaving
* the request unanswered, otherwise the peer would hit an ATT timeout.
*/
ESP_LOGW(GATTS_TAG, "Prepare write not supported, handle=%d, offset=%d, len=%d",
param->write.handle, param->write.offset, param->write.len);
if (param->write.need_rsp) {
esp_err_t sr = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id,
ESP_GATT_REQ_NOT_SUPPORTED, NULL);
if (sr != ESP_OK) {
ESP_LOGE(GATTS_TAG, "send_response (prep reject) failed: %s", esp_err_to_name(sr));
}
}
}
break;
case ESP_GATTS_EXEC_WRITE_EVT:
ESP_LOGD(GATTS_TAG, "ESP_GATTS_EXEC_WRITE_EVT, conn_id=%d, trans_id=%" PRIu32 ", flag=0x%02x",
param->exec_write.conn_id, param->exec_write.trans_id, param->exec_write.exec_write_flag);
esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id,
ESP_GATT_OK, NULL);
break;
case ESP_GATTS_MTU_EVT:
ESP_LOGI(GATTS_TAG, "MTU exchange, MTU=%d", param->mtu.mtu);
break;
@@ -5,3 +5,10 @@ CONFIG_BT_ENABLED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -29,9 +29,9 @@ A GATT notification delivered through the Notification Source characteristic con
Control Point characteristic to interact with the iOS notification.
*/
char *EventID_to_String(uint8_t EventID)
const char *EventID_to_String(uint8_t EventID)
{
char *str = NULL;
const char *str = NULL;
switch (EventID)
{
case EventIDNotificationAdded:
@@ -50,9 +50,9 @@ char *EventID_to_String(uint8_t EventID)
return str;
}
char *CategoryID_to_String(uint8_t CategoryID)
const char *CategoryID_to_String(uint8_t CategoryID)
{
char *Cidstr = NULL;
const char *Cidstr = NULL;
switch(CategoryID) {
case CategoryIDOther:
Cidstr = "Other";
@@ -103,17 +103,20 @@ char *CategoryID_to_String(uint8_t CategoryID)
void esp_receive_apple_notification_source(uint8_t *message, uint16_t message_len)
{
if (!message || message_len < 5) {
if (!message || message_len < 8) {
return;
}
uint8_t EventID = message[0];
char *EventIDS = EventID_to_String(EventID);
const char *EventIDS = EventID_to_String(EventID);
uint8_t EventFlags = message[1];
uint8_t CategoryID = message[2];
char *Cidstr = CategoryID_to_String(CategoryID);
const char *Cidstr = CategoryID_to_String(CategoryID);
uint8_t CategoryCount = message[3];
uint32_t NotificationUID = (message[4]) | (message[5]<< 8) | (message[6]<< 16) | (message[7] << 24);
uint32_t NotificationUID = (uint32_t)message[4]
| ((uint32_t)message[5] << 8)
| ((uint32_t)message[6] << 16)
| ((uint32_t)message[7] << 24);
ESP_LOGI(BLE_ANCS_TAG, "EventID:%s EventFlags:0x%x CategoryID:%s CategoryCount:%d NotificationUID:%" PRIu32, EventIDS, EventFlags, Cidstr, CategoryCount, NotificationUID);
}
@@ -131,7 +134,10 @@ void esp_receive_apple_data_source(uint8_t *message, uint16_t message_len)
ESP_LOGE(BLE_ANCS_TAG, "Message too short for NotificationAttributes");
break;
}
uint32_t NotificationUID = (message[1]) | (message[2]<< 8) | (message[3]<< 16) | (message[4] << 24);
uint32_t NotificationUID = (uint32_t)message[1]
| ((uint32_t)message[2] << 8)
| ((uint32_t)message[3] << 16)
| ((uint32_t)message[4] << 24);
uint32_t remian_attr_len = message_len - 5;
uint8_t *attrs = &message[5];
ESP_LOGI(BLE_ANCS_TAG, "recevice Notification Attributes response Command_id %d NotificationUID %" PRIu32, Command_id, NotificationUID);
@@ -142,7 +148,7 @@ void esp_receive_apple_data_source(uint8_t *message, uint16_t message_len)
break;
}
uint8_t AttributeID = attrs[0];
uint16_t len = attrs[1] | (attrs[2] << 8);
uint16_t len = (uint16_t)attrs[1] | ((uint16_t)attrs[2] << 8);
if(len > (remian_attr_len - 3)) {
ESP_LOGE(BLE_ANCS_TAG, "data error");
break;
@@ -197,9 +203,9 @@ void esp_receive_apple_data_source(uint8_t *message, uint16_t message_len)
}
}
char *Errcode_to_String(uint16_t status)
const char *Errcode_to_String(uint16_t status)
{
char *Errstr = NULL;
const char *Errstr = NULL;
switch (status) {
case Unknown_command:
Errstr = "Unknown_command";
@@ -213,6 +219,9 @@ char *Errcode_to_String(uint16_t status)
case Action_failed:
Errstr = "Action_failed";
break;
case Internal_error:
Errstr = "Internal_error";
break;
default:
Errstr = "unknown_failed";
break;
@@ -95,6 +95,7 @@ typedef enum {
Invalid_command = (0xA1), //The command was improperly formatted.
Invalid_parameter = (0xA2), // One of the parameters (for example, the NotificationUID) does not refer to an existing object on the NP.
Action_failed = (0xA3), //The action was not performed
Internal_error = (0xA4), //An internal error occurred on the NP (Apple ANCS specification).
} esp_error_code;
typedef enum {
@@ -111,8 +112,8 @@ typedef enum {
#define ESP_NOTIFICATIONUID_LEN 4
char *EventID_to_String(uint8_t EventID);
char *CategoryID_to_String(uint8_t CategoryID);
const char *EventID_to_String(uint8_t EventID);
const char *CategoryID_to_String(uint8_t CategoryID);
void esp_receive_apple_notification_source(uint8_t *message, uint16_t message_len);
void esp_receive_apple_data_source(uint8_t *message, uint16_t message_len);
char *Errcode_to_String(uint16_t status);
const char *Errcode_to_String(uint16_t status);
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -8,6 +8,7 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/event_groups.h"
#include "freertos/semphr.h"
#include "esp_system.h"
#include "esp_log.h"
#include "nvs_flash.h"
@@ -52,6 +53,7 @@ struct data_source_buffer {
};
static struct data_source_buffer data_buffer = {0};
static SemaphoreHandle_t data_buffer_mux;
//In its basic form, the ANCS exposes three characteristics:
// service UUID: 7905F431-B5CE-4E99-A40F-4B1E122D00D0
@@ -190,8 +192,8 @@ void esp_get_notification_attributes(uint8_t *notificationUID, uint8_t num_attr,
ESP_LOGE(BLE_ANCS_TAG, "Command buffer overflow in get_notification_attributes");
return;
}
cmd[index ++] = p_attr->attribute_len;
cmd[index ++] = (p_attr->attribute_len << 8);
cmd[index ++] = p_attr->attribute_len & 0xFF;
cmd[index ++] = (p_attr->attribute_len >> 8) & 0xFF;
}
p_attr ++;
num_attr --;
@@ -254,12 +256,23 @@ void esp_perform_notification_action(uint8_t *notificationUID, uint8_t ActionID)
static void periodic_timer_callback(void* arg)
{
esp_timer_stop(periodic_timer);
if (data_buffer_mux == NULL) {
return;
}
if (xSemaphoreTake(data_buffer_mux, 0) != pdTRUE) {
/* Mutex held by GATT handler; retry soon without blocking esp_timer task */
esp_err_t tr = esp_timer_start_once(periodic_timer, 50000);
if (tr != ESP_OK) {
ESP_LOGE(BLE_ANCS_TAG, "Data source idle timer retry failed: %s", esp_err_to_name(tr));
}
return;
}
if (data_buffer.len > 0) {
esp_receive_apple_data_source(data_buffer.buffer, data_buffer.len);
memset(data_buffer.buffer, 0, data_buffer.len);
data_buffer.len = 0;
}
xSemaphoreGive(data_buffer_mux);
}
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
@@ -525,6 +538,10 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_NOTIFY_EVT:
if (param->notify.handle == gl_profile_tab[PROFILE_A_APP_ID].notification_source_handle) {
if (!param->notify.value || param->notify.value_len < 8) {
ESP_LOGW(BLE_ANCS_TAG, "Notification source too short (%u), need 8 bytes", param->notify.value_len);
break;
}
esp_receive_apple_notification_source(param->notify.value, param->notify.value_len);
uint8_t *notificationUID = &param->notify.value[4];
if (param->notify.value[0] == EventIDNotificationAdded && param->notify.value[2] == CategoryIDIncomingCall) {
@@ -537,23 +554,35 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_get_notification_attributes(notificationUID, sizeof(p_attr)/sizeof(esp_noti_attr_list_t), p_attr);
}
} else if (param->notify.handle == gl_profile_tab[PROFILE_A_APP_ID].data_source_handle) {
if (data_buffer_mux == NULL) {
break;
}
if (xSemaphoreTake(data_buffer_mux, portMAX_DELAY) != pdTRUE) {
break;
}
if ((data_buffer.len + param->notify.value_len) > sizeof(data_buffer.buffer)) {
ESP_LOGE(BLE_ANCS_TAG, "Data source buffer overflow detected, discarding data");
memset(data_buffer.buffer, 0, sizeof(data_buffer.buffer));
data_buffer.len = 0;
xSemaphoreGive(data_buffer_mux);
break;
}
memcpy(&data_buffer.buffer[data_buffer.len], param->notify.value, param->notify.value_len);
data_buffer.len += param->notify.value_len;
if (param->notify.value_len == (gl_profile_tab[PROFILE_A_APP_ID].MTU_size - 3)) {
// copy and wait next packet, start timer 500ms
esp_timer_start_periodic(periodic_timer, 500000);
/* Retriggerable idle timeout: stop then one-shot so each full fragment resets 500 ms */
esp_timer_stop(periodic_timer);
esp_err_t tr = esp_timer_start_once(periodic_timer, 500000);
if (tr != ESP_OK) {
ESP_LOGE(BLE_ANCS_TAG, "Data source idle timer start failed: %s", esp_err_to_name(tr));
}
} else {
esp_timer_stop(periodic_timer);
esp_receive_apple_data_source(data_buffer.buffer, data_buffer.len);
memset(data_buffer.buffer, 0, data_buffer.len);
data_buffer.len = 0;
}
xSemaphoreGive(data_buffer_mux);
} else {
ESP_LOGI(BLE_ANCS_TAG, "unknown handle, receive notify value:");
}
@@ -572,7 +601,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_WRITE_CHAR_EVT:
if (param->write.status != ESP_GATT_OK) {
char *Errstr = Errcode_to_String(param->write.status);
const char *Errstr = Errcode_to_String(param->write.status);
if (Errstr) {
ESP_LOGE(BLE_ANCS_TAG, "write control point error %s", Errstr);
}
@@ -582,6 +611,18 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGI(BLE_ANCS_TAG, "ESP_GATTC_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason);
if (data_buffer_mux != NULL && xSemaphoreTake(data_buffer_mux, portMAX_DELAY) == pdTRUE) {
esp_timer_stop(periodic_timer);
memset(data_buffer.buffer, 0, sizeof(data_buffer.buffer));
data_buffer.len = 0;
xSemaphoreGive(data_buffer_mux);
} else {
/* Mutex unavailable: only stop the timer (esp_timer_stop is thread-safe).
* Skip buffer reset to avoid an unsynchronized write that could race with
* NOTIFY_EVT / periodic_timer_callback if the mutex were ever held elsewhere. */
ESP_LOGE(BLE_ANCS_TAG, "data_buffer_mux unavailable on disconnect, skip buffer reset");
esp_timer_stop(periodic_timer);
}
get_service = false;
esp_ble_gap_start_advertising(&adv_params);
break;
@@ -639,6 +680,8 @@ static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp
void init_timer(void)
{
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
data_buffer_mux = xSemaphoreCreateMutex();
ESP_ERROR_CHECK(data_buffer_mux != NULL ? ESP_OK : ESP_ERR_NO_MEM);
}
void app_main(void)
@@ -6,3 +6,10 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
CONFIG_BT_LE_50_FEATURE_SUPPORT=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for ANCS example
# (advertises as peripheral and acts as GATT client to ANCS service)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -7,6 +7,7 @@
/********************************************************************************
*
* This file is for gatt server. It can send adv data, and get connected by client.
* Only one BLE ACL connection is supported (see sdkconfig.defaults: CONFIG_BT_ACL_CONNECTIONS=1).
*
*********************************************************************************/
@@ -65,6 +66,7 @@ typedef struct {
int prepare_len;
} prepare_type_env_t;
/* This demo targets one connected client; a single prepare-write buffer is enough. */
static prepare_type_env_t prepare_write_env;
//#define CONFIG_SET_RAW_ADV_DATA
@@ -441,7 +443,11 @@ void example_prepare_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
/* Extent of prepared value: max(offset+len), not sum(len) — overlaps/retries must not inflate length. */
uint32_t span_end = (uint32_t)param->write.offset + (uint32_t)param->write.len;
if (span_end > (uint32_t)prepare_write_env->prepare_len) {
prepare_write_env->prepare_len = (int)span_end;
}
}
uint8_t long_write[16] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF};
@@ -562,7 +568,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTS_EXEC_WRITE_EVT:
// the length of gattc prepare write data must be less than GATTS_EXAMPLE_CHAR_VAL_LEN_MAX.
ESP_LOGI(EXAMPLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT, Length=%d", prepare_write_env.prepare_len);
ESP_LOGI(EXAMPLE_TAG, "ESP_GATTS_EXEC_WRITE_EVT, Length=%d", prepare_write_env.prepare_len);
example_exec_write_event_env(&prepare_write_env, param);
break;
case ESP_GATTS_MTU_EVT:
@@ -581,6 +587,11 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(EXAMPLE_TAG, "ESP_GATTS_DISCONNECT_EVT, reason = %d", param->disconnect.reason);
if (prepare_write_env.prepare_buf) {
free(prepare_write_env.prepare_buf);
prepare_write_env.prepare_buf = NULL;
}
prepare_write_env.prepare_len = 0;
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
@@ -6,3 +6,9 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=n
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
CONFIG_BT_LE_50_FEATURE_SUPPORT=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for compatibility test (server with SMP)
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -6,3 +6,11 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for Eddystone receiver (scanning only)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -77,22 +77,24 @@ static void eddystone_send_raw(const esp_eddystone_result_t *res)
}
case EDDYSTONE_FRAME_TYPE_URL: {
size_t url_len = strlen((char*)res->inform.url.encoded_url); //encoded url length
if(url_len > EDDYSTONE_URL_MAX_LEN){
url_len = EDDYSTONE_URL_MAX_LEN;
size_t url_len = strlen((char*)res->inform.url.encoded_url); // encoded url length
/* Eddystone: max 17 bytes encoded URL after scheme byte; also raw_adv_data[31] leaves
* only 20 bytes at index 11 (3 header + url_len), so url_len <= 17. */
if (url_len > EDDYSTONE_URL_ENCODED_MAX_LEN) {
url_len = EDDYSTONE_URL_ENCODED_MAX_LEN;
}
raw_adv_data[index++] = url_len+6; //length
raw_adv_data[index++] = url_len + 6; // length
raw_adv_data[index++] = ESP_BLE_AD_TYPE_SERVICE_DATA;
raw_adv_data[index++] = 0xAA;
raw_adv_data[index++] = 0xFE;
uint8_t service_data[EDDYSTONE_URL_MAX_LEN+2] = {0};
uint8_t service_data[EDDYSTONE_URL_ENCODED_MAX_LEN + 3] = {0};
service_data[0] = EDDYSTONE_FRAME_TYPE_URL;
service_data[1] = res->inform.url.tx_power;
service_data[2] = res->inform.url.url_scheme;
memcpy(&service_data[3], res->inform.url.encoded_url, url_len);
memcpy(&raw_adv_data[index], service_data, url_len+3);
index += url_len+3;
memcpy(&raw_adv_data[index], service_data, url_len + 3);
index += url_len + 3;
break;
}
@@ -107,7 +109,7 @@ static void eddystone_send_raw(const esp_eddystone_result_t *res)
service_data[2] = (res->inform.tlm.battery_voltage >> 8) & 0xFF;
service_data[3] = res->inform.tlm.battery_voltage & 0xFF;
service_data[4] = (res->inform.tlm.temperature >> 8) & 0xFF;
service_data[4] = res->inform.tlm.temperature & 0xFF;
service_data[5] = res->inform.tlm.temperature & 0xFF;
service_data[6] = (res->inform.tlm.adv_count >> 24) & 0xFF;
service_data[7] = (res->inform.tlm.adv_count >> 16) & 0xFF;
service_data[8] = (res->inform.tlm.adv_count >> 8) & 0xFF;
@@ -167,8 +169,8 @@ void esp_eddystone_appRegister(void)
void esp_eddystone_init(void)
{
esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
esp_bluedroid_init_with_cfg(&cfg);
esp_bluedroid_enable();
ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(&cfg));
ESP_ERROR_CHECK(esp_bluedroid_enable());
esp_eddystone_appRegister();
}
@@ -177,8 +179,8 @@ void app_main(void)
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT));
esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
esp_bt_controller_init(&bt_cfg);
esp_bt_controller_enable(ESP_BT_MODE_BLE);
ESP_ERROR_CHECK(esp_bt_controller_init(&bt_cfg));
ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE));
esp_eddystone_init();
esp_eddystone_result_t eddystone_result;
@@ -9,3 +9,11 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
CONFIG_EXMAPLE_EDDYSTONE_SEND_UID=y
CONFIG_EXMAPLE_EDDYSTONE_SEND_URL=n
CONFIG_EXMAPLE_EDDYSTONE_SEND_TLM=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for Eddystone sender (advertising only)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -190,7 +190,8 @@ static int ble_aes_ccm_encrypt(const uint8_t *key, const uint8_t *nonce,
static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
const uint8_t *ciphertext, size_t ciphertext_len,
const uint8_t *aad, size_t aad_len,
uint8_t *plaintext, size_t tag_len)
uint8_t *plaintext, size_t tag_len,
size_t plaintext_capacity)
{
#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT)
struct tc_aes_key_sched_struct sched;
@@ -213,6 +214,11 @@ static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
plaintext_len = ciphertext_len - tag_len;
if (plaintext_len > plaintext_capacity) {
ESP_LOGE(TAG, "plaintext_len (%zu) > plaintext_capacity (%zu)", plaintext_len, plaintext_capacity);
return -1;
}
/* Set AES encryption key */
ret = tc_aes128_set_encrypt_key(&sched, key);
if (ret != TC_CRYPTO_SUCCESS) {
@@ -275,6 +281,11 @@ static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
plaintext_len = ciphertext_len - tag_len;
if (plaintext_len > plaintext_capacity) {
ESP_LOGE(TAG, "plaintext_len (%zu) > plaintext_capacity (%zu)", plaintext_len, plaintext_capacity);
return -1;
}
/* Set key attributes */
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, alg);
@@ -369,13 +380,14 @@ int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
const uint8_t iv[BLE_EAD_IV_SIZE],
const uint8_t *encrypted_payload, size_t encrypted_payload_size,
uint8_t *payload)
uint8_t *payload, size_t payload_capacity)
{
int ret;
uint8_t nonce[BLE_EAD_NONCE_SIZE];
const uint8_t *randomizer;
const uint8_t *ciphertext;
size_t ciphertext_len;
size_t expected_plaintext_len;
if (session_key == NULL) {
ESP_LOGE(TAG, "session_key is NULL");
@@ -402,6 +414,13 @@ int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
return -1;
}
expected_plaintext_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size);
if (expected_plaintext_len > payload_capacity) {
ESP_LOGE(TAG, "EAD plaintext length %zu exceeds payload buffer %zu",
expected_plaintext_len, payload_capacity);
return -1;
}
/* Extract randomizer from the start of encrypted payload */
randomizer = encrypted_payload;
@@ -420,7 +439,8 @@ int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
ret = ble_aes_ccm_decrypt(session_key, nonce,
ciphertext, ciphertext_len,
ble_ead_aad, BLE_EAD_AAD_SIZE,
payload, BLE_EAD_MIC_SIZE);
payload, BLE_EAD_MIC_SIZE,
payload_capacity);
return ret;
}
@@ -79,14 +79,15 @@ int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
* @param encrypted_payload Encrypted advertising data (includes randomizer and MIC)
* @param encrypted_payload_size Size of encrypted data
* @param payload Output buffer for decrypted data
* Size must be at least BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size)
* @param payload_capacity Size of @a payload in bytes; must be >=
* BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size)
*
* @return 0 on success, negative error code on failure
*/
int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
const uint8_t iv[BLE_EAD_IV_SIZE],
const uint8_t *encrypted_payload, size_t encrypted_payload_size,
uint8_t *payload);
uint8_t *payload, size_t payload_capacity);
#ifdef __cplusplus
}
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -19,6 +19,7 @@
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include "nvs.h"
#include "nvs_flash.h"
#include "esp_bt.h"
@@ -57,12 +58,14 @@ static peer_info_t peers[MAX_PEERS] = {0};
/* GATT client state */
static bool is_connected = false;
static bool connect_pending = false; /* enh_open issued; CONNECT_EVT not yet received */
static bool get_server = false;
static uint16_t conn_id_stored = 0;
static uint16_t service_start_handle = 0;
static uint16_t service_end_handle = 0;
static uint16_t key_material_char_handle = INVALID_HANDLE;
static esp_bd_addr_t current_peer_addr = {0};
/* BDA for the active GATT connection; set in CONNECT_EVT, cleared on disconnect */
static esp_bd_addr_t gattc_remote_bda = {0};
/* GATT interface */
static esp_gatt_if_t gattc_if_stored = ESP_GATT_IF_NONE;
@@ -146,27 +149,54 @@ static void decrypt_enc_adv_data(const uint8_t *adv_data, uint8_t adv_len, const
uint8_t dec_data[32]; /* Buffer for decrypted data */
size_t dec_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(enc_data_len);
if (dec_len > sizeof(dec_data)) {
ESP_LOGW(TAG, "Encrypted AD would yield %zu plaintext bytes; example buffer is %zu — skip",
dec_len, sizeof(dec_data));
break;
}
int rc = ble_ead_decrypt(
peers[peer_idx].key_material.session_key,
peers[peer_idx].key_material.iv,
enc_data, enc_data_len,
dec_data);
dec_data, sizeof(dec_data));
if (rc == 0) {
size_t safe_dec_len = dec_len;
if (safe_dec_len > sizeof(dec_data)) {
ESP_LOGW(TAG, "dec_len %zu > buffer %zu, clamping for log/parse",
dec_len, sizeof(dec_data));
safe_dec_len = sizeof(dec_data);
}
ESP_LOGI(TAG, "Decryption successful!");
ESP_LOGI(TAG, "Decrypted data:");
ESP_LOG_BUFFER_HEX(TAG, dec_data, dec_len);
ESP_LOG_BUFFER_HEX(TAG, dec_data, safe_dec_len);
/* Parse decrypted advertising structure */
if (dec_len >= 2) {
uint8_t dec_type = dec_data[1];
if (dec_type == ESP_BLE_AD_TYPE_NAME_CMPL || dec_type == ESP_BLE_AD_TYPE_NAME_SHORT) {
char name[32] = {0};
size_t name_len = dec_data[0] - 1;
if (name_len < sizeof(name)) {
memcpy(name, &dec_data[2], name_len);
ESP_LOGI(TAG, "Decrypted device name: %s", name);
/* Parse decrypted advertising structure (do not trust length octet past plaintext) */
if (safe_dec_len >= 2) {
if (dec_data[0] == 0) {
ESP_LOGW(TAG, "Malformed decrypted AD: zero inner length");
} else {
/* BLE: octet 0 is L = len(type+data); element occupies 1+L octets */
const size_t inner_total = 1U + (size_t)dec_data[0];
if (inner_total > safe_dec_len) {
ESP_LOGW(TAG, "Malformed decrypted AD: inner len claims %zu octets, have %zu",
inner_total, safe_dec_len);
} else {
uint8_t dec_type = dec_data[1];
if (dec_type == ESP_BLE_AD_TYPE_NAME_CMPL ||
dec_type == ESP_BLE_AD_TYPE_NAME_SHORT) {
char name[32] = {0};
size_t name_len = (size_t)dec_data[0] - 1U;
/* Name in dec_data[2 .. name_copy_end); last index is name_copy_end - 1 */
const size_t name_copy_end = 2U + name_len;
if (name_len < sizeof(name) &&
name_copy_end <= sizeof(dec_data) &&
name_copy_end <= safe_dec_len) {
memcpy(name, &dec_data[2], name_len);
ESP_LOGI(TAG, "Decrypted device name: %s", name);
}
}
}
}
}
@@ -193,11 +223,14 @@ static bool should_connect(const uint8_t *adv_data, uint8_t adv_len)
uint8_t type = adv_data[offset + 1];
if (type == ESP_BLE_AD_TYPE_16SRV_CMPL || type == ESP_BLE_AD_TYPE_16SRV_PART) {
/* Check for GAP service UUID */
for (int i = 0; i < len - 1; i += 2) {
uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8);
if (uuid == GAP_SERVICE_UUID) {
return true;
/* Octets after AD type = len - 1; each 16-bit UUID needs 2 payload bytes */
int payload_len = (int)len - 1;
if (payload_len >= 2) {
for (int i = 0; i + 1 < payload_len; i += 2) {
uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8);
if (uuid == GAP_SERVICE_UUID) {
return true;
}
}
}
}
@@ -250,26 +283,33 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
decrypt_enc_adv_data(adv_data, adv_len, scan_result->scan_rst.bda);
} else {
/* Need to connect and get key */
if (!is_connected) {
ESP_LOGI(TAG, "Connecting to get key material...");
add_peer(scan_result->scan_rst.bda);
memcpy(current_peer_addr, scan_result->scan_rst.bda, sizeof(esp_bd_addr_t));
if (!is_connected && !connect_pending) {
int peer_slot = add_peer(scan_result->scan_rst.bda);
if (peer_slot < 0) {
ESP_LOGE(TAG, "Peer table full (max %d); cannot track key for "
ESP_BD_ADDR_STR " — skip connection (increase "
"MAX_PEERS or free a slot)",
MAX_PEERS, ESP_BD_ADDR_HEX(scan_result->scan_rst.bda));
} else {
ESP_LOGI(TAG, "Connecting to get key material...");
esp_ble_gap_stop_scanning();
esp_ble_gap_stop_scanning();
esp_ble_gatt_creat_conn_params_t conn_params = {0};
memcpy(conn_params.remote_bda, scan_result->scan_rst.bda, ESP_BD_ADDR_LEN);
conn_params.remote_addr_type = scan_result->scan_rst.ble_addr_type;
conn_params.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
conn_params.is_direct = true;
conn_params.is_aux = false;
esp_ble_gattc_enh_open(gattc_if_stored, &conn_params);
esp_ble_gatt_creat_conn_params_t conn_params = {0};
memcpy(conn_params.remote_bda, scan_result->scan_rst.bda,
ESP_BD_ADDR_LEN);
conn_params.remote_addr_type = scan_result->scan_rst.ble_addr_type;
conn_params.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
conn_params.is_direct = true;
conn_params.is_aux = false;
connect_pending = true;
esp_ble_gattc_enh_open(gattc_if_stored, &conn_params);
}
}
}
}
} else if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
ESP_LOGI(TAG, "Scan complete");
if (!is_connected) {
if (!is_connected && !connect_pending) {
start_scan(); /* Restart scanning */
}
}
@@ -314,6 +354,8 @@ static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_CONNECT_EVT:
ESP_LOGI(TAG, "Connected, conn_id %d", param->connect.conn_id);
conn_id_stored = param->connect.conn_id;
connect_pending = false;
memcpy(gattc_remote_bda, param->connect.remote_bda, sizeof(esp_bd_addr_t));
is_connected = true;
/* Request MTU exchange */
@@ -324,6 +366,8 @@ static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
if (param->open.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "Open failed: %d", param->open.status);
is_connected = false;
connect_pending = false;
memset(gattc_remote_bda, 0, sizeof(gattc_remote_bda));
start_scan();
}
break;
@@ -360,25 +404,29 @@ static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
if (get_server) {
/* Get characteristics */
uint16_t count = 0;
esp_ble_gattc_get_attr_count(gattc_if, conn_id_stored,
ESP_GATT_DB_CHARACTERISTIC,
service_start_handle,
service_end_handle,
INVALID_HANDLE, &count);
esp_gatt_status_t gc_st = esp_ble_gattc_get_attr_count(gattc_if, conn_id_stored,
ESP_GATT_DB_CHARACTERISTIC,
service_start_handle,
service_end_handle,
INVALID_HANDLE, &count);
if (count > 0) {
esp_gattc_char_elem_t *char_elem = malloc(sizeof(esp_gattc_char_elem_t) * count);
if (gc_st != ESP_GATT_OK) {
ESP_LOGE(TAG, "get_attr_count failed: %d", gc_st);
} else if (count > 0) {
esp_gattc_char_elem_t *char_elem = calloc(count, sizeof(esp_gattc_char_elem_t));
if (char_elem) {
esp_bt_uuid_t km_uuid = {
.len = ESP_UUID_LEN_16,
.uuid = {.uuid16 = KEY_MATERIAL_CHAR_UUID},
};
esp_ble_gattc_get_char_by_uuid(gattc_if, conn_id_stored,
service_start_handle,
service_end_handle,
km_uuid, char_elem, &count);
gc_st = esp_ble_gattc_get_char_by_uuid(gattc_if, conn_id_stored,
service_start_handle,
service_end_handle,
km_uuid, char_elem, &count);
if (count > 0) {
if (gc_st != ESP_GATT_OK) {
ESP_LOGE(TAG, "get_char_by_uuid failed: %d", gc_st);
} else if (count > 0) {
key_material_char_handle = char_elem[0].char_handle;
ESP_LOGI(TAG, "Key Material characteristic found, handle %d", key_material_char_handle);
@@ -402,7 +450,7 @@ static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
if (param->read.handle == key_material_char_handle &&
param->read.value_len == sizeof(ble_ead_key_material_t)) {
/* Store key material */
int peer_idx = find_peer(current_peer_addr);
int peer_idx = find_peer(gattc_remote_bda);
if (peer_idx >= 0) {
memcpy(&peers[peer_idx].key_material, param->read.value,
sizeof(ble_ead_key_material_t));
@@ -424,6 +472,8 @@ static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGI(TAG, "Disconnected, reason 0x%02x", param->disconnect.reason);
is_connected = false;
connect_pending = false;
memset(gattc_remote_bda, 0, sizeof(gattc_remote_bda));
get_server = false;
key_material_char_handle = INVALID_HANDLE;
start_scan();
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -67,10 +67,14 @@ static bool is_target_device(const uint8_t *adv_data, uint8_t adv_len)
uint8_t type = adv_data[offset + 1];
if (type == ESP_BLE_AD_TYPE_16SRV_CMPL || type == ESP_BLE_AD_TYPE_16SRV_PART) {
for (int i = 0; i < len - 1; i += 2) {
uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8);
if (uuid == CUSTOM_SERVICE_UUID) {
return true;
/* Octets after AD type = len - 1; each 16-bit UUID needs 2 payload bytes */
int payload_len = (int)len - 1;
if (payload_len >= 2) {
for (int i = 0; i + 1 < payload_len; i += 2) {
uint16_t uuid = adv_data[offset + 2 + i] | (adv_data[offset + 3 + i] << 8);
if (uuid == CUSTOM_SERVICE_UUID) {
return true;
}
}
}
}
@@ -116,30 +120,53 @@ static void decrypt_adv_data_no_connect(const uint8_t *adv_data, uint8_t adv_len
/* Decrypt using pre-shared key */
uint8_t dec_data[32];
size_t dec_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(enc_data_len);
if (dec_len > sizeof(dec_data)) {
ESP_LOGW(TAG, "Encrypted AD would yield %zu plaintext bytes; example buffer is %zu — skip",
dec_len, sizeof(dec_data));
return;
}
int rc = ble_ead_decrypt(
pre_shared_key.session_key,
pre_shared_key.iv,
enc_data, enc_data_len,
dec_data);
dec_data, sizeof(dec_data));
if (rc == 0) {
size_t safe_dec_len = dec_len;
if (safe_dec_len > sizeof(dec_data)) {
ESP_LOGW(TAG, "dec_len %zu > buffer %zu, clamping for log/parse",
dec_len, sizeof(dec_data));
safe_dec_len = sizeof(dec_data);
}
ESP_LOGI(TAG, "✅ Decryption successful (no connection needed!)");
ESP_LOGI(TAG, "Decrypted data (%d bytes):", dec_len);
ESP_LOG_BUFFER_HEX(TAG, dec_data, dec_len);
ESP_LOGI(TAG, "Decrypted data (%zu bytes):", safe_dec_len);
ESP_LOG_BUFFER_HEX(TAG, dec_data, safe_dec_len);
/* Parse the decrypted advertising structure */
if (dec_len >= 2) {
uint8_t inner_len = dec_data[0];
uint8_t inner_type = dec_data[1];
if (inner_type == ESP_BLE_AD_TYPE_NAME_CMPL ||
inner_type == ESP_BLE_AD_TYPE_NAME_SHORT) {
char name[32] = {0};
size_t name_len = inner_len - 1;
if (name_len < sizeof(name) && name_len <= dec_len - 2) {
memcpy(name, &dec_data[2], name_len);
ESP_LOGI(TAG, "📛 Decrypted device name: \"%s\"", name);
/* Parse decrypted AD (do not trust length octet past plaintext) */
if (safe_dec_len >= 2) {
if (dec_data[0] == 0) {
ESP_LOGW(TAG, "Malformed decrypted AD: zero inner length");
} else {
const size_t inner_total = 1U + (size_t)dec_data[0];
if (inner_total > safe_dec_len) {
ESP_LOGW(TAG,
"Malformed decrypted AD: inner len claims %zu octets, have %zu",
inner_total, safe_dec_len);
} else {
uint8_t inner_type = dec_data[1];
if (inner_type == ESP_BLE_AD_TYPE_NAME_CMPL ||
inner_type == ESP_BLE_AD_TYPE_NAME_SHORT) {
char name[32] = {0};
size_t name_len = (size_t)dec_data[0] - 1U;
const size_t name_copy_end = 2U + name_len;
if (name_len < sizeof(name) &&
name_copy_end <= sizeof(dec_data) &&
name_copy_end <= safe_dec_len) {
memcpy(name, &dec_data[2], name_len);
ESP_LOGI(TAG, "📛 Decrypted device name: \"%s\"", name);
}
}
}
}
}
@@ -11,3 +11,9 @@ CONFIG_BT_BLE_SMP_ENABLE=y
# Select crypto library for EAD (Encrypted Advertising Data)
# Options: CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS
CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for encrypted adv data central (client with SMP)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -184,7 +184,8 @@ static int ble_aes_ccm_encrypt(const uint8_t *key, const uint8_t *nonce,
static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
const uint8_t *ciphertext, size_t ciphertext_len,
const uint8_t *aad, size_t aad_len,
uint8_t *plaintext, size_t tag_len)
uint8_t *plaintext, size_t tag_len,
size_t plaintext_capacity)
{
#if defined(CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT)
struct tc_aes_key_sched_struct sched;
@@ -207,6 +208,11 @@ static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
plaintext_len = ciphertext_len - tag_len;
if (plaintext_len > plaintext_capacity) {
ESP_LOGE(TAG, "plaintext_len (%zu) > plaintext_capacity (%zu)", plaintext_len, plaintext_capacity);
return -1;
}
/* Set AES encryption key */
ret = tc_aes128_set_encrypt_key(&sched, key);
if (ret != TC_CRYPTO_SUCCESS) {
@@ -269,6 +275,11 @@ static int ble_aes_ccm_decrypt(const uint8_t *key, const uint8_t *nonce,
plaintext_len = ciphertext_len - tag_len;
if (plaintext_len > plaintext_capacity) {
ESP_LOGE(TAG, "plaintext_len (%zu) > plaintext_capacity (%zu)", plaintext_len, plaintext_capacity);
return -1;
}
/* Set key attributes */
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
psa_set_key_algorithm(&attributes, alg);
@@ -362,13 +373,14 @@ int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
const uint8_t iv[BLE_EAD_IV_SIZE],
const uint8_t *encrypted_payload, size_t encrypted_payload_size,
uint8_t *payload)
uint8_t *payload, size_t payload_capacity)
{
int ret;
uint8_t nonce[BLE_EAD_NONCE_SIZE];
const uint8_t *randomizer;
const uint8_t *ciphertext;
size_t ciphertext_len;
size_t expected_plaintext_len;
if (session_key == NULL) {
ESP_LOGE(TAG, "session_key is NULL");
@@ -395,6 +407,13 @@ int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
return -1;
}
expected_plaintext_len = BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size);
if (expected_plaintext_len > payload_capacity) {
ESP_LOGE(TAG, "EAD plaintext length %zu exceeds payload buffer %zu",
expected_plaintext_len, payload_capacity);
return -1;
}
/* Extract randomizer from the start of encrypted payload */
randomizer = encrypted_payload;
@@ -413,7 +432,8 @@ int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
ret = ble_aes_ccm_decrypt(session_key, nonce,
ciphertext, ciphertext_len,
ble_ead_aad, BLE_EAD_AAD_SIZE,
payload, BLE_EAD_MIC_SIZE);
payload, BLE_EAD_MIC_SIZE,
payload_capacity);
return ret;
}
@@ -79,14 +79,15 @@ int ble_ead_encrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
* @param encrypted_payload Encrypted advertising data (includes randomizer and MIC)
* @param encrypted_payload_size Size of encrypted data
* @param payload Output buffer for decrypted data
* Size must be at least BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size)
* @param payload_capacity Size of @a payload in bytes; must be >=
* BLE_EAD_DECRYPTED_PAYLOAD_SIZE(encrypted_payload_size)
*
* @return 0 on success, negative error code on failure
*/
int ble_ead_decrypt(const uint8_t session_key[BLE_EAD_KEY_SIZE],
const uint8_t iv[BLE_EAD_IV_SIZE],
const uint8_t *encrypted_payload, size_t encrypted_payload_size,
uint8_t *payload);
uint8_t *payload, size_t payload_capacity);
#ifdef __cplusplus
}
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -59,7 +59,6 @@ static ble_ead_key_material_t key_material = {
/* GATT state */
static esp_gatt_if_t gatts_if_stored = ESP_GATT_IF_NONE;
static uint16_t conn_id_stored = 0;
static bool is_connected = false;
/* Advertising parameters */
static esp_ble_adv_params_t adv_params = {
@@ -158,8 +157,13 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
{
switch (event) {
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
ESP_LOGI(TAG, "Raw advertising data set complete");
start_advertising();
if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(TAG, "Raw advertising data set failed: %d",
param->adv_data_raw_cmpl.status);
} else {
ESP_LOGI(TAG, "Raw advertising data set complete");
start_advertising();
}
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
@@ -236,7 +240,6 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
ESP_LOGI(TAG, "Connected, conn_id %d, remote "ESP_BD_ADDR_STR"",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
conn_id_stored = param->connect.conn_id;
is_connected = true;
/* Update connection parameters */
esp_ble_conn_update_params_t conn_params = {0};
@@ -251,7 +254,6 @@ static void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
is_connected = false;
/* Re-encrypt and restart advertising with new randomizer */
set_encrypted_adv_data();
@@ -11,3 +11,9 @@ CONFIG_BT_BLE_SMP_ENABLE=y
# Select crypto library for EAD (Encrypted Advertising Data)
# Options: CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT or CONFIG_BT_SMP_CRYPTO_STACK_MBEDTLS
CONFIG_BT_SMP_CRYPTO_STACK_TINYCRYPT=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for encrypted adv data peripheral (server with SMP)
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -37,9 +37,11 @@ esp_err_t esp_hidd_register_callbacks(esp_hidd_event_cb_t callbacks)
return hidd_status;
}
esp_ble_gatts_app_register(BATTRAY_APP_ID);
if ((hidd_status = esp_ble_gatts_app_register(BATTRAY_APP_ID)) != ESP_OK) {
return hidd_status;
}
if((hidd_status = esp_ble_gatts_app_register(HIDD_APP_ID)) != ESP_OK) {
if ((hidd_status = esp_ble_gatts_app_register(HIDD_APP_ID)) != ESP_OK) {
return hidd_status;
}
@@ -52,8 +54,10 @@ esp_err_t esp_hidd_profile_init(void)
ESP_LOGE(HID_LE_PRF_TAG, "HID device profile already initialized");
return ESP_FAIL;
}
// Reset the hid device target environment
/* Reset the hid device target environment */
memset(&hidd_le_env, 0, sizeof(hidd_le_env_t));
hidd_le_env.gatt_if = ESP_GATT_IF_NONE;
hidd_le_env.bat_gatt_if = ESP_GATT_IF_NONE;
hidd_le_env.enabled = true;
return ESP_OK;
}
@@ -62,20 +66,32 @@ esp_err_t esp_hidd_profile_deinit(void)
{
uint16_t hidd_svc_hdl = hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC];
if (!hidd_le_env.enabled) {
ESP_LOGE(HID_LE_PRF_TAG, "HID device profile already initialized");
ESP_LOGW(HID_LE_PRF_TAG, "HID device profile not initialized, deinit skipped");
return ESP_OK;
}
if(hidd_svc_hdl != 0) {
esp_ble_gatts_stop_service(hidd_svc_hdl);
esp_ble_gatts_delete_service(hidd_svc_hdl);
if (hidd_svc_hdl != 0) {
esp_ble_gatts_stop_service(hidd_svc_hdl);
esp_ble_gatts_delete_service(hidd_svc_hdl);
} else {
return ESP_FAIL;
}
ESP_LOGW(HID_LE_PRF_TAG, "HID service handle unset, skip stop/delete");
}
/* register the HID device profile to the BTA_GATTS module*/
esp_ble_gatts_app_unregister(hidd_le_env.gatt_if);
/* Release both GATTS apps (battery registered first, then HID) */
if (hidd_le_env.bat_gatt_if != ESP_GATT_IF_NONE) {
esp_ble_gatts_app_unregister(hidd_le_env.bat_gatt_if);
hidd_le_env.bat_gatt_if = ESP_GATT_IF_NONE;
} else {
ESP_LOGW(HID_LE_PRF_TAG, "Battery gatt_if invalid, app_unregister skipped (possible stack slot leak)");
}
if (hidd_le_env.gatt_if != ESP_GATT_IF_NONE) {
esp_ble_gatts_app_unregister(hidd_le_env.gatt_if);
hidd_le_env.gatt_if = ESP_GATT_IF_NONE;
} else {
ESP_LOGW(HID_LE_PRF_TAG, "HID gatt_if invalid, app_unregister skipped (possible stack slot leak)");
}
hidd_le_env.enabled = false;
return ESP_OK;
}
@@ -112,7 +128,7 @@ void esp_hidd_send_keyboard_value(uint16_t conn_id, key_mask_t special_key_mask,
buffer[i+2] = keyboard_cmd[i];
}
ESP_LOGD(HID_LE_PRF_TAG, "the key vaule = %d,%d,%d, %d, %d, %d,%d, %d", buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7]);
ESP_LOGD(HID_LE_PRF_TAG, "the key value = %d,%d,%d, %d, %d, %d,%d, %d", buffer[0], buffer[1], buffer[2], buffer[3], buffer[4], buffer[5], buffer[6], buffer[7]);
hid_dev_send_report(hidd_le_env.gatt_if, conn_id,
HID_RPT_ID_KEY_IN, HID_REPORT_TYPE_INPUT, HID_KEYBOARD_IN_RPT_LEN, buffer);
return;
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -170,7 +170,7 @@ static const uint8_t hidReportMap[] = {
0x09, 0xA6, // Usage(Vendor defined)
0x09, 0xA9, // Usage(Vendor defined)
0x75, 0x08, // Report Size
0x95, 0x7F, // Report Count = 127 Btyes
0x95, 0x7F, // Report Count = 127 Bytes
0x91, 0x02, // Output(Data, Variable, Absolute)
0xC0, // End Collection
#endif
@@ -285,7 +285,7 @@ static const uint8_t char_prop_read_notify = ESP_GATT_CHAR_PROP_BIT_READ|ESP_GAT
static const uint8_t char_prop_read_write_notify = ESP_GATT_CHAR_PROP_BIT_READ|ESP_GATT_CHAR_PROP_BIT_WRITE|ESP_GATT_CHAR_PROP_BIT_NOTIFY;
static const uint8_t char_prop_read_write_write_nr = ESP_GATT_CHAR_PROP_BIT_READ|ESP_GATT_CHAR_PROP_BIT_WRITE|ESP_GATT_CHAR_PROP_BIT_WRITE_NR;
/// battary Service
/// battery Service
static const uint16_t battary_svc = ESP_GATT_UUID_BATTERY_SERVICE_SVC;
static const uint16_t bat_lev_uuid = ESP_GATT_UUID_BATTERY_LEVEL;
@@ -296,23 +296,23 @@ static uint8_t battary_lev = 50;
/// Full HRS Database Description - Used to add attributes into the database
static const esp_gatts_attr_db_t bas_att_db[BAS_IDX_NB] =
{
// Battary Service Declaration
// Battery Service Declaration
[BAS_IDX_SVC] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&primary_service_uuid, ESP_GATT_PERM_READ,
sizeof(uint16_t), sizeof(battary_svc), (uint8_t *)&battary_svc}},
// Battary level Characteristic Declaration
// Battery level Characteristic Declaration
[BAS_IDX_BATT_LVL_CHAR] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_declaration_uuid, ESP_GATT_PERM_READ,
CHAR_DECLARATION_SIZE,CHAR_DECLARATION_SIZE, (uint8_t *)&char_prop_read_notify}},
// Battary level Characteristic Value
// Battery level Characteristic Value
[BAS_IDX_BATT_LVL_VAL] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&bat_lev_uuid, ESP_GATT_PERM_READ,
sizeof(uint8_t),sizeof(uint8_t), &battary_lev}},
// Battary level Characteristic - Client Characteristic Configuration Descriptor
// Battery level Characteristic - Client Characteristic Configuration Descriptor
[BAS_IDX_BATT_LVL_NTF_CFG] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&character_client_config_uuid, ESP_GATT_PERM_READ|ESP_GATT_PERM_WRITE,
sizeof(uint16_t),sizeof(bat_lev_ccc), (uint8_t *)bat_lev_ccc}},
// Battary level report Characteristic Declaration
// Battery level report Characteristic Declaration
[BAS_IDX_BATT_LVL_PRES_FMT] = {{ESP_GATT_AUTO_RSP}, {ESP_UUID_LEN_16, (uint8_t *)&char_format_uuid, ESP_GATT_PERM_READ,
sizeof(struct prf_char_pres_fmt), 0, NULL}},
};
@@ -550,6 +550,7 @@ void esp_hidd_prf_cb_hdl(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
}
}
if(param->reg.app_id == BATTRAY_APP_ID) {
hidd_le_env.bat_gatt_if = gatts_if;
hidd_param.init_finish.gatts_if = gatts_if;
if(hidd_le_env.hidd_cb != NULL) {
(hidd_le_env.hidd_cb)(ESP_BAT_EVENT_REG, &hidd_param);
@@ -587,7 +588,8 @@ void esp_hidd_prf_cb_hdl(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
break;
case ESP_GATTS_WRITE_EVT: {
esp_hidd_cb_param_t cb_param = {0};
if (param->write.handle == hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_REPORT_LED_OUT_VAL]) {
if (hidd_le_env.hidd_cb != NULL &&
param->write.handle == hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_REPORT_LED_OUT_VAL]) {
cb_param.led_write.conn_id = param->write.conn_id;
cb_param.led_write.report_id = HID_RPT_ID_LED_OUT;
cb_param.led_write.length = param->write.len;
@@ -595,8 +597,8 @@ void esp_hidd_prf_cb_hdl(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
(hidd_le_env.hidd_cb)(ESP_HIDD_EVENT_BLE_LED_REPORT_WRITE_EVT, &cb_param);
}
#if (SUPPORT_REPORT_VENDOR == true)
if (param->write.handle == hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_REPORT_VENDOR_OUT_VAL] &&
hidd_le_env.hidd_cb != NULL) {
if (hidd_le_env.hidd_cb != NULL &&
param->write.handle == hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_REPORT_VENDOR_OUT_VAL]) {
cb_param.vendor_write.conn_id = param->write.conn_id;
cb_param.vendor_write.report_id = HID_RPT_ID_VENDOR_OUT;
cb_param.vendor_write.length = param->write.len;
@@ -607,24 +609,26 @@ void esp_hidd_prf_cb_hdl(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
break;
}
case ESP_GATTS_CREAT_ATTR_TAB_EVT: {
if (param->add_attr_tab.num_handle == BAS_IDX_NB &&
param->add_attr_tab.svc_uuid.uuid.uuid16 == ESP_GATT_UUID_BATTERY_SERVICE_SVC &&
param->add_attr_tab.status == ESP_GATT_OK) {
if (param->add_attr_tab.status != ESP_GATT_OK) {
ESP_LOGE(HID_LE_PRF_TAG, "ATTR_TAB_EVT failed: status=%d num_handle=%d",
param->add_attr_tab.status, param->add_attr_tab.num_handle);
} else if (param->add_attr_tab.num_handle == BAS_IDX_NB &&
param->add_attr_tab.svc_uuid.uuid.uuid16 == ESP_GATT_UUID_BATTERY_SERVICE_SVC) {
incl_svc.start_hdl = param->add_attr_tab.handles[BAS_IDX_SVC];
incl_svc.end_hdl = incl_svc.start_hdl + BAS_IDX_NB -1;
incl_svc.end_hdl = incl_svc.start_hdl + BAS_IDX_NB - 1;
ESP_LOGI(HID_LE_PRF_TAG, "%s(), start added the hid service to the stack database. incl_handle = %d",
__func__, incl_svc.start_hdl);
esp_ble_gatts_start_service(param->add_attr_tab.handles[BAS_IDX_SVC]);
esp_ble_gatts_create_attr_tab(hidd_le_gatt_db, gatts_if, HIDD_LE_IDX_NB, 0);
}
if (param->add_attr_tab.num_handle == HIDD_LE_IDX_NB &&
param->add_attr_tab.status == ESP_GATT_OK) {
} else if (param->add_attr_tab.num_handle == HIDD_LE_IDX_NB) {
memcpy(hidd_le_env.hidd_inst.att_tbl, param->add_attr_tab.handles,
HIDD_LE_IDX_NB*sizeof(uint16_t));
ESP_LOGI(HID_LE_PRF_TAG, "hid svc handle = %x",hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC]);
HIDD_LE_IDX_NB * sizeof(uint16_t));
ESP_LOGI(HID_LE_PRF_TAG, "hid svc handle = %x", hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC]);
hid_add_id_tbl();
esp_ble_gatts_start_service(hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC]);
esp_ble_gatts_start_service(hidd_le_env.hidd_inst.att_tbl[HIDD_LE_IDX_SVC]);
} else {
esp_ble_gatts_start_service(param->add_attr_tab.handles[0]);
ESP_LOGW(HID_LE_PRF_TAG, "ATTR_TAB_EVT unexpected num_handle=%d",
param->add_attr_tab.num_handle);
}
break;
}
@@ -645,8 +649,10 @@ void hidd_le_create_service(esp_gatt_if_t gatts_if)
void hidd_le_init(void)
{
// Reset the hid device target environment
/* Reset the hid device target environment */
memset(&hidd_le_env, 0, sizeof(hidd_le_env_t));
hidd_le_env.gatt_if = ESP_GATT_IF_NONE;
hidd_le_env.bat_gatt_if = ESP_GATT_IF_NONE;
}
void hidd_clcb_alloc (uint16_t conn_id, esp_bd_addr_t bda)
@@ -666,16 +672,15 @@ void hidd_clcb_alloc (uint16_t conn_id, esp_bd_addr_t bda)
return;
}
bool hidd_clcb_dealloc (uint16_t conn_id)
bool hidd_clcb_dealloc(uint16_t conn_id)
{
uint8_t i_clcb = 0;
hidd_clcb_t *p_clcb = NULL;
for (i_clcb = 0, p_clcb= hidd_le_env.hidd_clcb; i_clcb < HID_MAX_APPS; i_clcb++, p_clcb++) {
for (uint8_t i_clcb = 0; i_clcb < HID_MAX_APPS; i_clcb++) {
hidd_clcb_t *p_clcb = &hidd_le_env.hidd_clcb[i_clcb];
if (p_clcb->in_use && p_clcb->conn_id == conn_id) {
memset(p_clcb, 0, sizeof(hidd_clcb_t));
return true;
}
}
return false;
}
@@ -735,12 +740,12 @@ void hidd_set_attr_value(uint16_t handle, uint16_t val_len, const uint8_t *value
return;
}
void hidd_get_attr_value(uint16_t handle, uint16_t *length, uint8_t **value)
void hidd_get_attr_value(uint16_t handle, uint16_t *length, const uint8_t **value)
{
hidd_inst_t *hidd_inst = &hidd_le_env.hidd_inst;
if(hidd_inst->att_tbl[HIDD_LE_IDX_HID_INFO_VAL] <= handle &&
hidd_inst->att_tbl[HIDD_LE_IDX_REPORT_REP_REF] >= handle){
esp_ble_gatts_get_attr_value(handle, length, (const uint8_t **)value);
esp_ble_gatts_get_attr_value(handle, length, value);
} else {
ESP_LOGE(HID_LE_PRF_TAG, "%s error:Invalid handle value.", __func__);
}
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -183,7 +183,7 @@ enum {
HIDD_LE_CHAR_MAX //= HIDD_LE_REPORT_CHAR + HIDD_LE_NB_REPORT_INST_MAX,
};
///att read event table Indexs
///att read event table Indexes
enum {
HIDD_LE_READ_INFO_EVT,
HIDD_LE_READ_CTNL_PT_EVT,
@@ -306,7 +306,8 @@ typedef struct
/* service engine control block */
typedef struct {
hidd_clcb_t hidd_clcb[HID_MAX_APPS]; /* connection link*/
esp_gatt_if_t gatt_if;
esp_gatt_if_t gatt_if; /* HIDD_APP_ID */
esp_gatt_if_t bat_gatt_if; /* BATTRAY_APP_ID */
bool enabled;
bool is_take;
bool is_primery;
@@ -327,7 +328,7 @@ void hidd_le_create_service(esp_gatt_if_t gatts_if);
void hidd_set_attr_value(uint16_t handle, uint16_t val_len, const uint8_t *value);
void hidd_get_attr_value(uint16_t handle, uint16_t *length, uint8_t **value);
void hidd_get_attr_value(uint16_t handle, uint16_t *length, const uint8_t **value);
esp_err_t hidd_register_cb(void);
@@ -6,3 +6,9 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for HID device example (server with SMP)
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -6,3 +6,9 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
# Disable unused Bluedroid host features for iBeacon (advertising/scanning only, no GATT/SMP)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -219,6 +219,22 @@ static const esp_gatts_attr_db_t gatt_db[HRS_IDX_NB] =
};
static uint16_t profile_handle_table[HRS_IDX_NB];
/** After a prepared write is executed, read committed value from GATT DB and relay to peers. */
static void relay_demo_char_value_to_peers(void)
{
uint16_t len = 0;
const uint8_t *value = NULL;
uint16_t h = profile_handle_table[IDX_CHAR_VAL_A];
if (h == 0) {
return;
}
if (esp_ble_gatts_get_attr_value(h, &len, &value) != ESP_GATT_OK || value == NULL || len == 0) {
return;
}
traverse_send_peer(len, (uint8_t *)value);
}
#if (BLE50_SUPPORTED == 1)
static esp_ble_gap_ext_adv_t ext_adv[1] = {
[0] = {ADV_HANDLE_INST, ADV_DURATION, ADV_MAX_EVTS},
@@ -238,8 +254,9 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTC_CONNECT_EVT:
if (param->connect.link_role == 0) {
++multi_conn_num;
ESP_LOGI(DEMO_TAG, "Connected, conn_id %d, remote "ESP_BD_ADDR_STR", total %u", param->connect.conn_id,
ESP_BD_ADDR_HEX(param->connect.remote_bda), ++multi_conn_num);
ESP_BD_ADDR_HEX(param->connect.remote_bda), (unsigned)multi_conn_num);
Peer new_peer;
new_peer.conn_id = param->connect.conn_id;
new_peer.conn_handle = param->connect.conn_handle;
@@ -261,20 +278,33 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
}
break;
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGI(DEMO_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x, total %u",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason,
(multi_conn_num ? --multi_conn_num : multi_conn_num));
if (param->disconnect.conn_id != prph_conn_id) {
Peer *peer = find_peer(param->disconnect.conn_id);
if (peer) {
peer_remove(peer->conn_id);
case ESP_GATTC_DISCONNECT_EVT: {
/* Only central-role connections are added to the peer list (see ESP_GATTC_CONNECT_EVT);
* peripheral-role connections are tracked separately via prph_conn_id and cleaned up
* entirely in ESP_GATTS_DISCONNECT_EVT. Use peer_lookup() as the role oracle here:
* a non-NULL result is the unambiguous proof that this disconnect belongs to a
* central-role peer, regardless of GATTC/GATTS event ordering.
*
* Note: Bluedroid posts ESP_GATTS_DISCONNECT_EVT to the BTC queue before
* ESP_GATTC_DISCONNECT_EVT (the GATTS path is delivered directly, while the GATTC
* path is relayed through the BTA queue), so by the time we get here for a
* peripheral disconnect, prph_conn_id has already been reset to 0xFFFF. Relying on
* (conn_id != prph_conn_id) would therefore mis-classify the peripheral disconnect
* as a central one and incorrectly decrement multi_conn_num / give restart_scan_sem.
* peer_lookup() is used instead of find_peer() so peripheral disconnects don't
* trigger a spurious "peer not found" ERROR log. */
Peer *peer = peer_lookup(param->disconnect.conn_id);
if (peer) {
peer_remove(peer->conn_id);
if (multi_conn_num) {
--multi_conn_num;
}
ESP_LOGI(DEMO_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x, total %u",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason, (unsigned)multi_conn_num);
xSemaphoreGive(restart_scan_sem);
} else {
prph_conn_id = 0xFFFF;
}
break;
}
case ESP_GATTC_OPEN_EVT:
ESP_LOGI(DEMO_TAG, "Open, conn_id %d, status %d", param->open.conn_id, param->open.status);
break;
@@ -340,14 +370,27 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
esp_ble_gatts_create_attr_tab(gatt_db, gatts_if, HRS_IDX_NB, SVC_INST_ID);
break;
case ESP_GATTS_WRITE_EVT:
ESP_LOGI(DEMO_TAG, "Characteristic write received, conn_id %u, value", param->write.conn_id);
ESP_LOGI(DEMO_TAG, "Characteristic write received, conn_id %u, prep=%d", param->write.conn_id,
(int)param->write.is_prep);
ESP_LOG_BUFFER_HEX(DEMO_TAG, param->write.value, param->write.len);
traverse_send_peer(param->write.len, param->write.value);
/* Prepared writes must be relayed only after ESP_GATTS_EXEC_WRITE_EVT (EXEC). */
if (param->write.is_prep) {
break;
}
if (param->write.handle == profile_handle_table[IDX_CHAR_VAL_A]) {
traverse_send_peer(param->write.len, param->write.value);
}
break;
case ESP_GATTS_EXEC_WRITE_EVT:
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
relay_demo_char_value_to_peers();
}
break;
case ESP_GATTS_CONNECT_EVT:
if (param->connect.link_role == 1) {
++multi_conn_num;
ESP_LOGI(DEMO_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR", total %u",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda), ++multi_conn_num);
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda), (unsigned)multi_conn_num);
prph_conn_id = param->connect.conn_id;
advertising_state = DISABLED;
#if (BLE50_SUPPORTED == 1)
@@ -361,9 +404,17 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTS_DISCONNECT_EVT:
if (param->disconnect.conn_id == prph_conn_id) {
unsigned total_after;
if (multi_conn_num) {
--multi_conn_num;
total_after = (unsigned)multi_conn_num;
} else {
total_after = 0;
}
ESP_LOGI(DEMO_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%x, total %u",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason,
(multi_conn_num ? --multi_conn_num : multi_conn_num));
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason, total_after);
/* Clear before SET_STATIC_RAND_ADDR_EVT: esp_gap_cb requires 0xFFFF to restart advertising */
prph_conn_id = 0xFFFF;
advertising_state = PENDING;
#if (BLE50_SUPPORTED == 1)
esp_ble_gap_addr_create_static(adv_rand_addr);
@@ -376,7 +427,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTS_CREAT_ATTR_TAB_EVT:
ESP_LOGI(DEMO_TAG, "The number handle = %x", param->add_attr_tab.num_handle);
if (param->create.status == ESP_GATT_OK) {
if (param->add_attr_tab.status == ESP_GATT_OK) {
if (param->add_attr_tab.num_handle == HRS_IDX_NB) {
memcpy(profile_handle_table, param->add_attr_tab.handles,
sizeof(profile_handle_table));
@@ -386,7 +437,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
param->add_attr_tab.num_handle, HRS_IDX_NB);
}
} else {
ESP_LOGE(DEMO_TAG, " Create attribute table failed, status %x", param->create.status);
ESP_LOGE(DEMO_TAG, "Create attribute table failed, status %x", param->add_attr_tab.status);
}
break;
default:
@@ -484,7 +535,7 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
// ESP_LOGI(DEMO_TAG, "Scan result, device "ESP_BD_ADDR_STR", name len %u", ESP_BD_ADDR_HEX(param->ext_adv_report.params.addr), adv_name_len);
// ESP_LOG_BUFFER_CHAR(DEMO_TAG, adv_name, adv_name_len);
if (strlen(remote_target_name) == adv_name_len && strncmp((char *)adv_name, remote_target_name, adv_name_len) == 0)
if (adv_name != NULL && strlen(remote_target_name) == adv_name_len && strncmp((char *)adv_name, remote_target_name, adv_name_len) == 0)
{
esp_ble_gap_stop_ext_scan();
scan_state = DISABLED;
@@ -532,13 +583,17 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
if (scan_result->scan_rst.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
uint8_t *adv_name = NULL;
uint8_t adv_name_len = 0;
const unsigned buf_cap = sizeof(scan_result->scan_rst.ble_adv);
unsigned comb = (unsigned)scan_result->scan_rst.adv_data_len +
(unsigned)scan_result->scan_rst.scan_rsp_len;
uint16_t safe_adv_len = (comb > buf_cap) ? (uint16_t)buf_cap : (uint16_t)comb;
adv_name = esp_ble_resolve_adv_data_by_type(scan_result->scan_rst.ble_adv,
scan_result->scan_rst.adv_data_len + scan_result->scan_rst.scan_rsp_len,
safe_adv_len,
ESP_BLE_AD_TYPE_NAME_CMPL,
&adv_name_len);
// ESP_LOGI(DEMO_TAG, "Scan result, device "ESP_BD_ADDR_STR", name len %u", ESP_BD_ADDR_HEX(scan_result->scan_rst.bda), adv_name_len);
// ESP_LOG_BUFFER_CHAR(DEMO_TAG, adv_name, adv_name_len);
if (strlen(remote_target_name) == adv_name_len && strncmp((char *)adv_name, remote_target_name, adv_name_len) == 0) {
if (adv_name != NULL && strlen(remote_target_name) == adv_name_len && strncmp((char *)adv_name, remote_target_name, adv_name_len) == 0) {
esp_ble_gap_stop_scanning();
scan_state = DISABLED;
@@ -712,10 +767,12 @@ void app_main(void)
if (multi_conn_num < BLE_PEER_MAX_NUM && scan_state == DISABLED) {
scan_state = PENDING;
#if (BLE50_SUPPORTED == 0)
/* Legacy: cannot set scan random addr until adv stops; completion continues in
* ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT. Do not break out of while(1) or app_main exits. */
if (advertising_state != DISABLED) {
advertising_state = DISABLED;
esp_ble_gap_stop_advertising();
break;
continue;
}
#endif
esp_ble_gap_addr_create_static(new_rand_addr);
@@ -66,4 +66,6 @@ esp_err_t peer_remove(uint16_t conn_id);
Peer *find_peer(uint16_t conn_id);
Peer *peer_lookup(uint16_t conn_id);
#endif
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -8,6 +8,19 @@
static Peer remote_peer_lst[MAX_CONN_NUM];
/* Silent lookup: returns NULL without logging if conn_id is not in the peer list.
* Use this when the caller legitimately treats a miss as "not a central peer"
* (e.g. disconnect path where peripheral-role conn_ids are intentionally absent). */
Peer *peer_lookup(uint16_t conn_id)
{
for (int i = 0; i < MAX_CONN_NUM; i++) {
if (remote_peer_lst[i].conn_id == conn_id) {
return &remote_peer_lst[i];
}
}
return NULL;
}
void peer_manager_init(void)
{
for (int i = 0; i < MAX_CONN_NUM; i++) {
@@ -20,6 +33,18 @@ void peer_manager_init(void)
esp_err_t peer_add(Peer *peer)
{
if (peer == NULL) {
return ESP_ERR_INVALID_ARG;
}
Peer *existing = peer_lookup(peer->conn_id);
if (existing != NULL) {
/* Same conn_id already tracked — refresh metadata, do not consume a second slot */
existing->conn_handle = peer->conn_handle;
existing->gattc_if = peer->gattc_if;
memcpy(&existing->peer_addr, &peer->peer_addr, sizeof(esp_bd_addr_t));
ESP_LOGW(PEER_MANAGER_TAG, "peer_add: conn_id %u already in list, updated", peer->conn_id);
return ESP_OK;
}
for (int i = 0; i < MAX_CONN_NUM; i++) {
if (remote_peer_lst[i].conn_id == 0xFFFF) {
remote_peer_lst[i].char_handle = 0xFFFF;
@@ -51,14 +76,11 @@ esp_err_t peer_remove(uint16_t conn_id)
Peer *find_peer(uint16_t conn_id)
{
for (int i = 0; i < MAX_CONN_NUM; i++) {
if (remote_peer_lst[i].conn_id == conn_id) {
return &remote_peer_lst[i];
}
Peer *p = peer_lookup(conn_id);
if (p == NULL) {
ESP_LOGE(PEER_MANAGER_TAG, "peer not found in list, conn_id %d", conn_id);
}
ESP_LOGE(PEER_MANAGER_TAG, "peer not found in list, conn_id %d", conn_id);
return NULL;
return p;
}
void traverse_send_peer(uint16_t len, uint8_t *value)
@@ -6,3 +6,9 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set
CONFIG_BT_MULTI_CONNECTION_ENBALE=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for multi-connection central
# (acts as both GATT server and GATT client, but no SMP)
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -139,10 +139,14 @@ static void ble_prph_set_new_adv(void)
is_advertising = true;
esp_ble_gap_addr_create_static(new_rand_addr);
#if (BLE50_SUPPORTED == 1)
esp_ble_gap_ext_adv_set_rand_addr(EXT_ADV_HANDLE, new_rand_addr);
esp_err_t err = esp_ble_gap_ext_adv_set_rand_addr(EXT_ADV_HANDLE, new_rand_addr);
#else
esp_ble_gap_set_rand_addr(new_rand_addr);
esp_err_t err = esp_ble_gap_set_rand_addr(new_rand_addr);
#endif
if (err != ESP_OK) {
is_advertising = false;
ESP_LOGE(DEMO_TAG, "Set random addr failed: %s", esp_err_to_name(err));
}
}
}
@@ -178,41 +182,67 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
case ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Extended adv random address set, status %d, "ESP_BD_ADDR_STR"",
param->ext_adv_set_rand_addr.status, ESP_BD_ADDR_HEX(new_rand_addr));
if (param->ext_adv_set_rand_addr.status != ESP_BT_STATUS_SUCCESS) {
is_advertising = false;
break;
}
esp_ble_gap_ext_adv_start(NUM_EXT_ADV_SET, &ext_adv[0]);
break;
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Extended advertising params set, status %d", param->ext_adv_set_params.status);
if (param->ext_adv_set_params.status != ESP_BT_STATUS_SUCCESS) {
break;
}
esp_ble_gap_config_ext_adv_data_raw(EXT_ADV_HANDLE, sizeof(adv_data_raw), &adv_data_raw[0]);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Extended advertising data set, status %d", param->ext_adv_data_set.status);
if (param->ext_adv_data_set.status != ESP_BT_STATUS_SUCCESS) {
break;
}
ble_prph_restart_adv();
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Extended advertising start, status %d", param->ext_adv_start.status);
is_advertising = true;
if (param->ext_adv_start.status == ESP_BT_STATUS_SUCCESS) {
is_advertising = true;
} else {
is_advertising = false;
}
break;
case ESP_GAP_BLE_ADV_TERMINATED_EVT:
ESP_LOGI(DEMO_TAG, "Extended advertising terminated, status = %d", param->adv_terminate.status);
if (param->adv_terminate.status == 0x00) {
ESP_LOGI(DEMO_TAG, "Advertising successfully ended with a connection being created");
is_advertising = false;
}
/* Any terminate reason means advertising has stopped; clear flag so restart can run */
is_advertising = false;
break;
#else
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Advertising data set, status %d", param->adv_data_raw_cmpl.status);
if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
break;
}
esp_ble_gap_addr_create_static(new_rand_addr);
esp_ble_gap_set_rand_addr(new_rand_addr);
break;
case ESP_GAP_BLE_SET_STATIC_RAND_ADDR_EVT:
ESP_LOGI(DEMO_TAG, "Random address set, status %d, addr "ESP_BD_ADDR_STR"",
param->set_rand_addr_cmpl.status, ESP_BD_ADDR_HEX(new_rand_addr));
if (param->set_rand_addr_cmpl.status != ESP_BT_STATUS_SUCCESS) {
is_advertising = false;
break;
}
esp_ble_gap_start_advertising(&legacy_adv_params);
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Advertising start, status %d", param->adv_start_cmpl.status);
is_advertising = true;
if (param->adv_start_cmpl.status == ESP_BT_STATUS_SUCCESS) {
is_advertising = true;
} else {
is_advertising = false;
}
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
ESP_LOGI(DEMO_TAG, "Advertising stop, status %d", param->adv_stop_cmpl.status);
@@ -248,16 +278,20 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
ESP_LOG_BUFFER_HEX(DEMO_TAG, param->write.value, param->write.len);
break;
case ESP_GATTS_CONNECT_EVT:
prph_conn_num++;
ESP_LOGI(DEMO_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR", total %u",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda), ++prph_conn_num);
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda), prph_conn_num);
is_advertising = false;
#if CONFIG_EXAMPLE_RESTART_ADV_AFTER_CONNECTED
ble_prph_restart_adv();
#endif
break;
case ESP_GATTS_DISCONNECT_EVT:
if (prph_conn_num > 0) {
prph_conn_num--;
}
ESP_LOGI(DEMO_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%x, total %d",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason, (prph_conn_num ? --prph_conn_num : prph_conn_num));
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason, prph_conn_num);
/* start advertising again when disconnected */
ble_prph_restart_adv();
break;
@@ -274,7 +308,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
case ESP_GATTS_CREAT_ATTR_TAB_EVT:
{
ESP_LOGI(DEMO_TAG, "The number handle = %x", param->add_attr_tab.num_handle);
if (param->create.status == ESP_GATT_OK) {
if (param->add_attr_tab.status == ESP_GATT_OK) {
if (param->add_attr_tab.num_handle == HRS_IDX_NB) {
memcpy(profile_handle_table, param->add_attr_tab.handles,
sizeof(profile_handle_table));
@@ -289,7 +323,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
param->add_attr_tab.num_handle, HRS_IDX_NB);
}
} else {
ESP_LOGE(DEMO_TAG, " Create attribute table failed, error code = %x", param->create.status);
ESP_LOGE(DEMO_TAG, " Create attribute table failed, error code = %x", param->add_attr_tab.status);
}
break;
}
@@ -8,3 +8,10 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
CONFIG_BT_ACL_CONNECTIONS=50
CONFIG_BT_ALARM_MAX_NUM=150
CONFIG_BT_MULTI_CONNECTION_ENBALE=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -17,6 +17,7 @@
#include "driver/uart.h"
#include "esp_bt.h"
#include "esp_err.h"
#include "nvs_flash.h"
#include "esp_bt_device.h"
#include "esp_gap_ble_api.h"
@@ -110,6 +111,7 @@ static bool connect = false;
static char * notify_value_p = NULL;
static int notify_value_offset = 0;
static int notify_value_count = 0;
static size_t notify_value_alloc_size = 0;
static bool start = false;
static uint64_t notify_len = 0;
static uint64_t start_time = 0;
@@ -125,60 +127,71 @@ static esp_bt_uuid_t spp_service_uuid = {
.uuid = {.uuid16 = ESP_GATT_SPP_SERVICE_UUID,},
};
/* Drop any in-flight notify reassembly buffer/counters. Used both on errors and on disconnect. */
static void reset_notify_reasm(void)
{
if (notify_value_p != NULL) {
free(notify_value_p);
notify_value_p = NULL;
}
notify_value_offset = 0;
notify_value_count = 0;
notify_value_alloc_size = 0;
}
static void notify_event_handler(esp_ble_gattc_cb_param_t * p_data)
{
uint8_t handle = 0;
uint16_t handle = p_data->notify.handle;
handle = p_data->notify.handle;
if (db == NULL) {
ESP_LOGE(GATTC_TAG, " %s db is NULL", __func__);
return;
}
if (handle == db[SPP_IDX_SPP_DATA_NTY_VAL].attribute_handle) {
if ((p_data->notify.value[0] == '#') && (p_data->notify.value[1] == '#')) {
if ((++notify_value_count) != p_data->notify.value[3]) {
if(notify_value_p != NULL){
free(notify_value_p);
}
notify_value_count = 0;
notify_value_p = NULL;
notify_value_offset = 0;
ESP_LOGE(GATTC_TAG,"notify value count is not continuous, %s", __func__);
/* Fragment header is 4 bytes: "##", total_frags, seq. */
if (p_data->notify.value_len >= 4 &&
p_data->notify.value[0] == '#' && p_data->notify.value[1] == '#') {
uint8_t total_frags = p_data->notify.value[2];
uint8_t seq = p_data->notify.value[3];
uint16_t payload_len = p_data->notify.value_len - 4;
if ((++notify_value_count) != seq) {
ESP_LOGE(GATTC_TAG, "notify value count is not continuous, %s", __func__);
reset_notify_reasm();
return;
}
if (p_data->notify.value[3] == 1) {
notify_value_p = (char *)malloc(((spp_mtu_size-7)*(p_data->notify.value[2]))*sizeof(char));
if (seq == 1) {
notify_value_alloc_size = (size_t)(spp_mtu_size - 7) * total_frags;
notify_value_p = (char *)malloc(notify_value_alloc_size);
if (notify_value_p == NULL) {
ESP_LOGE(GATTC_TAG, "malloc failed, %s L#%d", __func__, __LINE__);
notify_value_count = 0;
reset_notify_reasm();
return;
}
memcpy((notify_value_p + notify_value_offset), (p_data->notify.value + 4), (p_data->notify.value_len - 4));
if (p_data->notify.value[2] == p_data->notify.value[3]) {
uart_write_bytes(UART_NUM_0, (char *)(notify_value_p), (p_data->notify.value_len - 4 + notify_value_offset));
free(notify_value_p);
notify_value_p = NULL;
notify_value_offset = 0;
return;
}
notify_value_offset += (p_data->notify.value_len - 4);
} else if (p_data->notify.value[3] <= p_data->notify.value[2]) {
memcpy((notify_value_p + notify_value_offset), (p_data->notify.value + 4), (p_data->notify.value_len - 4));
if (p_data->notify.value[3] == p_data->notify.value[2]) {
uart_write_bytes(UART_NUM_0, (char *)(notify_value_p), (p_data->notify.value_len - 4 + notify_value_offset));
free(notify_value_p);
notify_value_count = 0;
notify_value_p = NULL;
notify_value_offset = 0;
return;
}
notify_value_offset += (p_data->notify.value_len - 4);
} else if (notify_value_p == NULL) {
ESP_LOGE(GATTC_TAG, "fragment %u without start, %s", seq, __func__);
reset_notify_reasm();
return;
}
/* Bound the write against the actually allocated reasm buffer to defeat
* malicious peers that send total_frags=0 or grow total_frags mid-stream. */
if ((size_t)notify_value_offset + payload_len > notify_value_alloc_size) {
ESP_LOGE(GATTC_TAG, "fragment payload would overflow reasm buffer, %s", __func__);
reset_notify_reasm();
return;
}
memcpy(notify_value_p + notify_value_offset, p_data->notify.value + 4, payload_len);
if (seq == total_frags) {
uart_write_bytes(UART_NUM_0, notify_value_p, payload_len + notify_value_offset);
reset_notify_reasm();
return;
}
notify_value_offset += payload_len;
} else {
uart_write_bytes(UART_NUM_0, (char *)(p_data->notify.value), p_data->notify.value_len);
}
} else if (handle == ((db+SPP_IDX_SPP_STATUS_VAL)->attribute_handle)) {
} else if (handle == db[SPP_IDX_SPP_STATUS_VAL].attribute_handle) {
ESP_LOG_BUFFER_CHAR(GATTC_TAG, (char *)p_data->notify.value, p_data->notify.value_len);
//TODO:server notify status characteristic
} else {
@@ -196,13 +209,42 @@ static void free_gattc_srv_db(void)
cmd = 0;
spp_srv_start_handle = 0;
spp_srv_end_handle = 0;
notify_value_p = NULL;
notify_value_offset = 0;
notify_value_count = 0;
reset_notify_reasm();
if (db) {
free(db);
db = NULL;
}
count = SPP_IDX_NB;
}
/**
* Resolve the CCCD handle for a known notify-capable characteristic value index by looking up the
* adjacent ESP_GATT_UUID_CHAR_CLIENT_CONFIG descriptor. Returns 0 on mismatch.
*
* This is safer than `(db+cmd+1)->attribute_handle` because the descriptor slot is verified.
*/
static uint16_t spp_get_cccd_handle(uint16_t char_val_idx)
{
size_t cfg_idx;
if (db == NULL) {
return 0;
}
switch (char_val_idx) {
case SPP_IDX_SPP_DATA_NTY_VAL: cfg_idx = SPP_IDX_SPP_DATA_NTF_CFG; break;
case SPP_IDX_SPP_STATUS_VAL: cfg_idx = SPP_IDX_SPP_STATUS_CFG; break;
#ifdef SUPPORT_HEARTBEAT
case SPP_IDX_SPP_HEARTBEAT_VAL: cfg_idx = SPP_IDX_SPP_HEARTBEAT_CFG; break;
#endif
default: return 0;
}
const esp_gattc_db_elem_t *d = &db[cfg_idx];
if (d->type != ESP_GATT_DB_DESCRIPTOR ||
d->uuid.len != ESP_UUID_LEN_16 ||
d->uuid.uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
return 0;
}
return d->attribute_handle;
}
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
@@ -246,7 +288,9 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
ESP_BLE_AD_TYPE_NAME_CMPL, &adv_name_len);
ESP_LOGI(GATTC_TAG, "Scan result, device "ESP_BD_ADDR_STR", name len %u", ESP_BD_ADDR_HEX(scan_result->scan_rst.bda), adv_name_len);
ESP_LOG_BUFFER_CHAR(GATTC_TAG, adv_name, adv_name_len);
if (adv_name != NULL && strncmp((char *)adv_name, device_name, adv_name_len) == 0) {
/* Full-name match (strncmp(adv_name, device_name, adv_name_len) would also accept prefixes). */
if (adv_name != NULL && adv_name_len == (sizeof(device_name) - 1) &&
memcmp(adv_name, device_name, adv_name_len) == 0) {
if (connect == false) {
connect = true;
esp_ble_gap_stop_scanning();
@@ -373,6 +417,11 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (p_data->reg_for_notify.status != ESP_GATT_OK) {
break;
}
uint16_t descr_handle = spp_get_cccd_handle(cmd);
if (descr_handle == 0) {
ESP_LOGW(GATTC_TAG, "CCCD handle not resolved for char index %u", (unsigned)cmd);
break;
}
uint16_t notify_en = 0x01;
#ifdef CONFIG_EXAMPLE_SPP_RELIABLE
if (cmd == SPP_IDX_SPP_DATA_NTY_VAL) {
@@ -382,7 +431,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
esp_ble_gattc_write_char_descr(
spp_gattc_if,
spp_conn_id,
(db+cmd+1)->attribute_handle,
descr_handle,
sizeof(notify_en),
(uint8_t *)&notify_en,
ESP_GATT_WRITE_TYPE_RSP,
@@ -438,26 +487,35 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTC_CFG_MTU_EVT:
ESP_LOGI(GATTC_TAG, "MTU exchange, status %d, MTU %d", param->cfg_mtu.status, param->cfg_mtu.mtu);
if(p_data->cfg_mtu.status != ESP_OK){
if (param->cfg_mtu.status != ESP_GATT_OK) {
break;
}
spp_mtu_size = p_data->cfg_mtu.mtu;
spp_mtu_size = param->cfg_mtu.mtu;
db = (esp_gattc_db_elem_t *)malloc(count*sizeof(esp_gattc_db_elem_t));
if(db == NULL){
if (db != NULL) {
free(db);
db = NULL;
}
count = SPP_IDX_NB;
db = (esp_gattc_db_elem_t *)malloc(count * sizeof(esp_gattc_db_elem_t));
if (db == NULL) {
ESP_LOGE(GATTC_TAG, "Malloc db failed");
break;
}
if(esp_ble_gattc_get_db(spp_gattc_if, spp_conn_id, spp_srv_start_handle, spp_srv_end_handle, db, &count) != ESP_GATT_OK){
if (esp_ble_gattc_get_db(spp_gattc_if, spp_conn_id, spp_srv_start_handle, spp_srv_end_handle, db, &count) != ESP_GATT_OK) {
ESP_LOGE(GATTC_TAG, "Get db failed");
free(db);
db = NULL;
break;
}
if(count != SPP_IDX_NB){
if (count != SPP_IDX_NB) {
ESP_LOGE(GATTC_TAG, "Get db count != SPP_IDX_NB, count = %d, SPP_IDX_NB = %d", count, SPP_IDX_NB);
free(db);
db = NULL;
break;
}
for(int i = 0;i < SPP_IDX_NB;i++){
switch((db+i)->type){
for (int i = 0; i < SPP_IDX_NB; i++) {
switch ((db + i)->type) {
case ESP_GATT_DB_PRIMARY_SERVICE:
ESP_LOGI(GATTC_TAG, "PRIMARY_SERVICE, attribute_handle %d, start_handle %d, end_handle %d, properties 0x%x, uuid 0x%04x",
(db+i)->attribute_handle, (db+i)->start_handle, (db+i)->end_handle, (db+i)->properties, (db+i)->uuid.uuid.uuid16);
@@ -572,11 +630,11 @@ void ble_client_appRegister(void)
ESP_LOGE(GATTC_TAG, "set local MTU failed: %s", esp_err_to_name_r(local_mtu_ret, err_msg, sizeof(err_msg)));
}
cmd_reg_queue = xQueueCreate(10, sizeof(uint32_t));
cmd_reg_queue = xQueueCreate(10, sizeof(uint16_t));
xTaskCreate(spp_client_reg_task, "spp_client_reg_task", 2048, NULL, 10, NULL);
#ifdef SUPPORT_HEARTBEAT
cmd_heartbeat_queue = xQueueCreate(10, sizeof(uint32_t));
cmd_heartbeat_queue = xQueueCreate(10, sizeof(uint16_t));
xTaskCreate(spp_heart_beat_task, "spp_heart_beat_task", 2048, NULL, 10, NULL);
#endif
esp_ble_gattc_app_register(PROFILE_APP_ID);
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT client only example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -59,7 +59,7 @@ static const uint8_t spp_adv_data[23] = {
0x0F, ESP_BLE_AD_TYPE_NAME_CMPL, 'E', 'S', 'P', '_', 'S', 'P', 'P', '_', 'S', 'E', 'R','V', 'E', 'R'
};
static uint16_t spp_mtu_size = SPP_GATT_MTU_SIZE;
static uint16_t spp_mtu_size = 23;
static uint16_t spp_conn_id = 0xffff;
static esp_gatt_if_t spp_gatts_if = 0xff;
QueueHandle_t spp_uart_queue = NULL;
@@ -108,19 +108,25 @@ typedef struct spp_receive_data_node{
struct spp_receive_data_node * next_node;
}spp_receive_data_node_t;
static spp_receive_data_node_t * temp_spp_recv_data_node_p1 = NULL;
static spp_receive_data_node_t * temp_spp_recv_data_node_p2 = NULL;
typedef struct spp_receive_data_buff{
int32_t node_num;
int32_t buff_size;
spp_receive_data_node_t * first_node;
spp_receive_data_node_t *first_node;
spp_receive_data_node_t *last_node;
}spp_receive_data_buff_t;
static spp_receive_data_buff_t SppRecvDataBuff = {
/* Command queue carries (data, len); we cannot use strlen() on the payload because
* BLE writes are not NUL-terminated. */
typedef struct {
uint16_t len;
uint8_t *data;
} spp_cmd_queue_msg_t;
static spp_receive_data_buff_t spp_prep_wr_buff = {
.node_num = 0,
.buff_size = 0,
.first_node = NULL
.first_node = NULL,
.last_node = NULL,
};
static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@@ -271,67 +277,74 @@ static uint8_t find_char_and_desr_index(uint16_t handle)
static bool store_wr_buffer(esp_ble_gatts_cb_param_t *p_data)
{
temp_spp_recv_data_node_p1 = (spp_receive_data_node_t *)malloc(sizeof(spp_receive_data_node_t));
if(temp_spp_recv_data_node_p1 == NULL){
ESP_LOGI(GATTS_TABLE_TAG, "malloc error %s %d", __func__, __LINE__);
if (p_data == NULL) {
return false;
}
temp_spp_recv_data_node_p1->len = p_data->write.len;
temp_spp_recv_data_node_p1->next_node = NULL;
temp_spp_recv_data_node_p1->node_buff = (uint8_t *)malloc(p_data->write.len);
if (temp_spp_recv_data_node_p1->node_buff == NULL) {
ESP_LOGI(GATTS_TABLE_TAG, "malloc error %s %d\n", __func__, __LINE__);
// Security fix: Free the node and return false to prevent memory leak
free(temp_spp_recv_data_node_p1);
temp_spp_recv_data_node_p1 = NULL;
/* Per Bluetooth Core Spec (Vol 3, Part F, 3.4.6.1) the Part Attribute Value
* in ATT_PREPARE_WRITE_REQ may be 0..(ATT_MTU-5) bytes, so a zero-length
* fragment is a valid request that carries no payload. Skip allocating an
* empty node here: malloc(0) is implementation-defined in C and on
* ESP-IDF's multi_heap returns NULL, which would otherwise be reported as
* a (false) allocation failure. */
if (p_data->write.len == 0) {
return true;
}
spp_receive_data_node_t *node = (spp_receive_data_node_t *)malloc(sizeof(spp_receive_data_node_t));
if (node == NULL) {
ESP_LOGE(GATTS_TABLE_TAG, "malloc error %s %d", __func__, __LINE__);
return false;
}
memcpy(temp_spp_recv_data_node_p1->node_buff, p_data->write.value, p_data->write.len);
// Security fix: Link to list only after successful allocation
if(temp_spp_recv_data_node_p2 != NULL){
temp_spp_recv_data_node_p2->next_node = temp_spp_recv_data_node_p1;
node->len = p_data->write.len;
node->next_node = NULL;
node->node_buff = (uint8_t *)malloc(p_data->write.len);
if (node->node_buff == NULL) {
ESP_LOGE(GATTS_TABLE_TAG, "malloc error %s %d", __func__, __LINE__);
free(node);
return false;
}
temp_spp_recv_data_node_p2 = temp_spp_recv_data_node_p1;
SppRecvDataBuff.buff_size += p_data->write.len;
memcpy(node->node_buff, p_data->write.value, p_data->write.len);
if(SppRecvDataBuff.node_num == 0){
SppRecvDataBuff.first_node = temp_spp_recv_data_node_p1;
SppRecvDataBuff.node_num++;
}else{
SppRecvDataBuff.node_num++;
if (spp_prep_wr_buff.last_node != NULL) {
spp_prep_wr_buff.last_node->next_node = node;
} else {
spp_prep_wr_buff.first_node = node;
}
spp_prep_wr_buff.last_node = node;
spp_prep_wr_buff.buff_size += p_data->write.len;
spp_prep_wr_buff.node_num++;
return true;
}
static void free_write_buffer(void)
static void free_prep_wr_buffer(void)
{
temp_spp_recv_data_node_p1 = SppRecvDataBuff.first_node;
spp_receive_data_node_t *cur = spp_prep_wr_buff.first_node;
while(temp_spp_recv_data_node_p1 != NULL){
temp_spp_recv_data_node_p2 = temp_spp_recv_data_node_p1->next_node;
if (temp_spp_recv_data_node_p1->node_buff) {
free(temp_spp_recv_data_node_p1->node_buff);
while (cur != NULL) {
spp_receive_data_node_t *next = cur->next_node;
if (cur->node_buff) {
free(cur->node_buff);
}
free(temp_spp_recv_data_node_p1);
temp_spp_recv_data_node_p1 = temp_spp_recv_data_node_p2;
free(cur);
cur = next;
}
SppRecvDataBuff.node_num = 0;
SppRecvDataBuff.buff_size = 0;
SppRecvDataBuff.first_node = NULL;
spp_prep_wr_buff.node_num = 0;
spp_prep_wr_buff.buff_size = 0;
spp_prep_wr_buff.first_node = NULL;
spp_prep_wr_buff.last_node = NULL;
}
static void print_write_buffer(void)
static void print_prep_wr_buffer(void)
{
temp_spp_recv_data_node_p1 = SppRecvDataBuff.first_node;
spp_receive_data_node_t *cur = spp_prep_wr_buff.first_node;
while (temp_spp_recv_data_node_p1 != NULL) {
uart_write_bytes(UART_NUM_0, (char *)(temp_spp_recv_data_node_p1->node_buff), temp_spp_recv_data_node_p1->len);
temp_spp_recv_data_node_p1 = temp_spp_recv_data_node_p1->next_node;
while (cur != NULL) {
uart_write_bytes(UART_NUM_0, (char *)(cur->node_buff), cur->len);
cur = cur->next_node;
}
}
@@ -449,22 +462,26 @@ static void spp_uart_init(void)
#ifdef SUPPORT_HEARTBEAT
void spp_heartbeat_task(void * arg)
{
uint16_t cmd_id;
uint32_t cmd_id;
for(;;) {
vTaskDelay(50 / portTICK_PERIOD_MS);
if(xQueueReceive(cmd_heartbeat_queue, &cmd_id, portMAX_DELAY)) {
while(1){
heartbeat_count_num++;
vTaskDelay(5000/ portTICK_PERIOD_MS);
if((heartbeat_count_num >3)&&(is_connected)){
esp_ble_gap_disconnect(spp_remote_bda);
}
if(is_connected && enable_heart_ntf){
esp_ble_gatts_send_indicate(spp_gatts_if, spp_conn_id, spp_handle_table[SPP_IDX_SPP_HEARTBEAT_VAL],sizeof(heartbeat_s), heartbeat_s, false);
}else if(!is_connected){
heartbeat_count_num = 0;
while (1) {
if (!is_connected) {
break;
}
vTaskDelay(5000 / portTICK_PERIOD_MS);
heartbeat_count_num++;
if ((heartbeat_count_num > 3) && is_connected) {
esp_ble_gap_disconnect(spp_remote_bda);
break;
}
if (is_connected && enable_heart_ntf) {
esp_ble_gatts_send_indicate(spp_gatts_if, spp_conn_id, spp_handle_table[SPP_IDX_SPP_HEARTBEAT_VAL],
sizeof(heartbeat_s), heartbeat_s, false);
}
}
}
}
@@ -474,13 +491,15 @@ void spp_heartbeat_task(void * arg)
void spp_cmd_task(void * arg)
{
uint8_t * cmd_id;
spp_cmd_queue_msg_t msg;
for (;;) {
vTaskDelay(50 / portTICK_PERIOD_MS);
if(xQueueReceive(cmd_cmd_queue, &cmd_id, portMAX_DELAY)) {
ESP_LOG_BUFFER_CHAR(GATTS_TABLE_TAG, (char *)(cmd_id), strlen((char *)cmd_id));
free(cmd_id);
if (xQueueReceive(cmd_cmd_queue, &msg, portMAX_DELAY)) {
if (msg.data != NULL) {
ESP_LOG_BUFFER_CHAR(GATTS_TABLE_TAG, (char *)msg.data, msg.len);
free(msg.data);
}
}
}
vTaskDelete(NULL);
@@ -499,7 +518,7 @@ static void spp_task_init(void)
xTaskCreate(spp_heartbeat_task, "spp_heartbeat_task", 2048, NULL, 10, NULL);
#endif
cmd_cmd_queue = xQueueCreate(10, sizeof(uint32_t));
cmd_cmd_queue = xQueueCreate(10, sizeof(spp_cmd_queue_msg_t));
xTaskCreate(spp_cmd_task, "spp_cmd_task", 4096, NULL, 10, NULL);
}
@@ -518,7 +537,7 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
ESP_LOGI(GATTS_TABLE_TAG, "Advertising start successfully");
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TABLE_TAG, "Advertising stop failed, status %d", param->adv_stop_cmpl.status);
break;
}
@@ -556,15 +575,17 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
res = find_char_and_desr_index(p_data->write.handle);
if (p_data->write.is_prep == false) {
if (res == SPP_IDX_SPP_COMMAND_VAL) {
uint8_t * spp_cmd_buff = NULL;
spp_cmd_buff = (uint8_t *)malloc((spp_mtu_size - 3) * sizeof(uint8_t));
if(spp_cmd_buff == NULL){
uint8_t *spp_cmd_buff = (uint8_t *)malloc(p_data->write.len);
if (spp_cmd_buff == NULL) {
ESP_LOGE(GATTS_TABLE_TAG, "%s malloc failed", __func__);
break;
}
memset(spp_cmd_buff, 0x0, (spp_mtu_size - 3));
memcpy(spp_cmd_buff, p_data->write.value, p_data->write.len);
xQueueSend(cmd_cmd_queue, &spp_cmd_buff, 10/portTICK_PERIOD_MS);
spp_cmd_queue_msg_t msg = { .len = p_data->write.len, .data = spp_cmd_buff };
if (xQueueSend(cmd_cmd_queue, &msg, 10 / portTICK_PERIOD_MS) != pdTRUE) {
ESP_LOGE(GATTS_TABLE_TAG, "%s cmd_cmd_queue send failed", __func__);
free(spp_cmd_buff);
}
} else if (res == SPP_IDX_SPP_DATA_NTF_CFG) {
if ((p_data->write.len == 2) && (p_data->write.value[0] == 0x01) && (p_data->write.value[1] == 0x00)) {
ESP_LOGI(GATTS_TABLE_TAG, "SPP data notification enable");
@@ -588,9 +609,11 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
if ((p_data->write.len == 2) && (p_data->write.value[0] == 0x01) && (p_data->write.value[1] == 0x00)) {
ESP_LOGI(GATTS_TABLE_TAG, "SPP heartbeat notification enable");
enable_heart_ntf = true;
heartbeat_count_num = 0;
} else if ((p_data->write.len == 2) && (p_data->write.value[0] == 0x00) && (p_data->write.value[1] == 0x00)) {
ESP_LOGI(GATTS_TABLE_TAG, "SPP heartbeat notification disable");
enable_heart_ntf = false;
heartbeat_count_num = 0;
}
} else if (res == SPP_IDX_SPP_HEARTBEAT_VAL) {
if ((p_data->write.len == sizeof(heartbeat_s)) && (memcmp(heartbeat_s, p_data->write.value, sizeof(heartbeat_s)) == 0)) {
@@ -612,14 +635,15 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
}
break;
}
case ESP_GATTS_EXEC_WRITE_EVT: {
ESP_LOGI(GATTS_TABLE_TAG, "Execute write");
if (p_data->exec_write.exec_write_flag) {
print_write_buffer();
free_write_buffer();
}
break;
}
case ESP_GATTS_EXEC_WRITE_EVT:
/* End of prepared-write transaction: print on EXEC, then always release queued chunks
* (master only freed on EXEC, which leaked on CANCEL). */
ESP_LOGI(GATTS_TABLE_TAG, "Execute write flag 0x%02x", p_data->exec_write.exec_write_flag);
if (p_data->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
print_prep_wr_buffer();
}
free_prep_wr_buffer();
break;
case ESP_GATTS_RESPONSE_EVT:
break;
case ESP_GATTS_MTU_EVT:
@@ -644,18 +668,20 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
case ESP_GATTS_CONNECT_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
free_prep_wr_buffer();
spp_conn_id = p_data->connect.conn_id;
spp_gatts_if = gatts_if;
is_connected = true;
memcpy(&spp_remote_bda,&p_data->connect.remote_bda,sizeof(esp_bd_addr_t));
#ifdef SUPPORT_HEARTBEAT
uint16_t cmd = 0;
xQueueSend(cmd_heartbeat_queue,&cmd,10/portTICK_PERIOD_MS);
uint32_t cmd = 0;
xQueueSend(cmd_heartbeat_queue, &cmd, 10 / portTICK_PERIOD_MS);
#endif
break;
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
free_prep_wr_buffer();
spp_mtu_size = 23;
is_connected = false;
enable_data_ntf = false;
@@ -670,6 +696,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
case ESP_GATTS_CANCEL_OPEN_EVT:
break;
case ESP_GATTS_CLOSE_EVT:
free_prep_wr_buffer();
break;
case ESP_GATTS_LISTEN_EVT:
break;
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -126,6 +126,9 @@ static uint8_t check_sum(uint8_t *addr, uint16_t count)
if (addr == NULL || count == 0) {
return 0;
}
if (count > (ESP_GATT_MAX_MTU_SIZE - 3U)) {
return 0;
}
for(int i = 0; i < count; i++) {
sum = sum + addr[i];
@@ -288,6 +291,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
/* free descr_elem_result */
free(descr_elem_result);
descr_elem_result = NULL;
}
}
else{
@@ -299,10 +303,23 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
case ESP_GATTC_NOTIFY_EVT: {
#if (CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT)
if (p_data->notify.is_notify &&
(p_data->notify.value[p_data->notify.value_len - 1] ==
check_sum(p_data->notify.value, p_data->notify.value_len - 1))){
notify_len += p_data->notify.value_len;
if (p_data->notify.is_notify) {
uint16_t vlen = p_data->notify.value_len;
uint8_t *val = p_data->notify.value;
const uint16_t max_notify_len = (uint16_t)(ESP_GATT_MAX_MTU_SIZE - 3U);
if (val == NULL) {
ESP_LOGW(GATTC_TAG, "notify ignored: null value");
} else if (vlen == 0) {
ESP_LOGW(GATTC_TAG, "notify ignored: zero length");
} else if (vlen < 2) {
ESP_LOGW(GATTC_TAG, "notify ignored: length too short for payload+checksum");
} else if (vlen > max_notify_len) {
ESP_LOGW(GATTC_TAG, "notify ignored: length exceeds bound");
} else if (val[vlen - 1] == check_sum(val, (uint16_t)(vlen - 1U))) {
notify_len += vlen;
} else {
ESP_LOGE(GATTC_TAG, "notify checksum mismatch");
}
} else {
ESP_LOGE(GATTC_TAG, "Indication received, value:");
}
@@ -347,6 +364,11 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
current_time = 0;
notify_len = 0;
#endif /* #if (CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT) */
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
/* Unblock throughput_client_task if it is waiting on gattc_semaphore while congested. */
can_send_write = true;
xSemaphoreGive(gattc_semaphore);
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
ESP_LOGI(GATTC_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(p_data->disconnect.remote_bda), p_data->disconnect.reason);
break;
@@ -405,7 +427,7 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
esp_ble_conn_params_t phy_1m_conn_params = {0};
#if(CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT && CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT)
phy_1m_conn_params.interval_max = 34;
phy_1m_conn_params.interval_min = 34;
phy_1m_conn_params.interval_max = 34;
#else
phy_1m_conn_params.interval_max = 32;
@@ -540,9 +562,14 @@ static void throughput_cal_task(void *param)
uint32_t bit_rate = 0;
if (start_time) {
current_time = esp_timer_get_time();
bit_rate = notify_len * SECOND_TO_USECOND / (current_time - start_time);
ESP_LOGI(GATTC_TAG, "Notify Bit rate = %" PRIu32 " Byte/s, = %" PRIu32 " bit/s, time = %ds",
bit_rate, bit_rate<<3, (int)((current_time - start_time) / SECOND_TO_USECOND));
uint64_t elapsed_us = current_time - start_time;
if (elapsed_us > 0) {
bit_rate = (uint32_t)(notify_len * SECOND_TO_USECOND / elapsed_us);
ESP_LOGI(GATTC_TAG, "Notify Bit rate = %" PRIu32 " Byte/s, = %" PRIu32 " bit/s, time = %ds",
bit_rate, bit_rate << 3, (int)(elapsed_us / SECOND_TO_USECOND));
} else {
ESP_LOGI(GATTC_TAG, "Notify Bit rate = 0 Byte/s, = 0 bit/s (elapsed 0 us)");
}
} else {
ESP_LOGI(GATTC_TAG, "Notify Bit rate = 0 Byte/s, = 0 bit/s");
}
@@ -601,6 +628,17 @@ void app_main(void)
return;
}
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
/* Create the semaphore before registering the GATTC callback so that any
* xSemaphoreGive() invoked from the callback is guaranteed to see a valid handle.
*/
gattc_semaphore = xSemaphoreCreateBinary();
if (!gattc_semaphore) {
ESP_LOGE(GATTC_TAG, "%s, init fail, the gattc semaphore create fail.", __func__);
return;
}
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
//register the callback function to the gattc module
ret = esp_ble_gattc_register_callback(esp_gattc_cb);
if(ret){
@@ -626,12 +664,4 @@ void app_main(void)
#if (CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT)
xTaskCreatePinnedToCore(&throughput_cal_task, "throughput_cal_task", 4096, NULL, 9, NULL, BLUETOOTH_TASK_PINNED_TO_CORE);
#endif
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
gattc_semaphore = xSemaphoreCreateBinary();
if (!gattc_semaphore) {
ESP_LOGE(GATTC_TAG, "%s, init fail, the gattc semaphore create fail.", __func__);
return;
}
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
}
@@ -8,3 +8,10 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT=y
CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT client only example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -59,6 +59,7 @@ static bool start = false;
static uint64_t write_len = 0;
static uint64_t start_time = 0;
static uint64_t current_time = 0;
static portMUX_TYPE s_write_throughput_stats_mux = portMUX_INITIALIZER_UNLOCKED;
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
static bool is_connect = false;
@@ -204,6 +205,18 @@ static prepare_type_env_t a_prepare_write_env;
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
static void prepare_write_env_clear(prepare_type_env_t *env)
{
if (env == NULL) {
return;
}
if (env->prepare_buf != NULL) {
free(env->prepare_buf);
env->prepare_buf = NULL;
}
env->prepare_len = 0;
}
static uint8_t check_sum(uint8_t *addr, uint16_t count)
{
uint32_t sum = 0;
@@ -211,6 +224,9 @@ static uint8_t check_sum(uint8_t *addr, uint16_t count)
if (addr == NULL || count == 0) {
return 0;
}
if (count > (ESP_GATT_MAX_MTU_SIZE - 3U)) {
return 0;
}
for(int i = 0; i < count; i++) {
sum = sum + addr[i];
@@ -334,14 +350,14 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
}
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (prepare_write_env == NULL) {
ESP_LOGE(GATTS_TAG, "exec_write: prepare_write_env is NULL");
return;
}
if (param->exec_write.exec_write_flag != ESP_GATT_PREP_WRITE_EXEC){
ESP_LOGI(GATTS_TAG,"Prepare write cancel");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
prepare_write_env_clear(prepare_write_env);
}
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
@@ -442,17 +458,32 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
example_write_event_env(gatts_if, &a_prepare_write_env, param);
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].char_handle) {
// The last value byte is the checksum data, should used to check the data is received corrected or not.
if (param->write.value[param->write.len - 1] ==
check_sum(param->write.value, param->write.len - 1)) {
write_len += param->write.len;
uint16_t wlen = param->write.len;
uint8_t *wval = param->write.value;
/* len==0 makes (wlen-1) wrap; cap len before indexing or passing to check_sum. */
const uint16_t max_write_len = (uint16_t)(ESP_GATT_MAX_MTU_SIZE - 3U);
if (wval == NULL) {
ESP_LOGW(GATTS_TAG, "write ignored: null value");
} else if (wlen == 0) {
ESP_LOGW(GATTS_TAG, "write ignored: zero length");
} else if (wlen < 2) {
ESP_LOGW(GATTS_TAG, "write ignored: length too short for payload+checksum");
} else if (wlen > max_write_len) {
ESP_LOGW(GATTS_TAG, "write ignored: length exceeds bound");
} else if (wval[wlen - 1] == check_sum(wval, (uint16_t)(wlen - 1U))) {
portENTER_CRITICAL(&s_write_throughput_stats_mux);
write_len += wlen;
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
} else {
ESP_LOGE(GATTS_TAG, "write checksum mismatch");
}
portENTER_CRITICAL(&s_write_throughput_stats_mux);
if (start == false) {
start_time = esp_timer_get_time();
start = true;
break;
}
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
}
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
@@ -462,11 +493,13 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
ESP_LOGI(GATTS_TAG,"Execute write");
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_CANCEL) {
portENTER_CRITICAL(&s_write_throughput_stats_mux);
if (write_len > a_prepare_write_env.prepare_len) {
write_len -= a_prepare_write_env.prepare_len;
} else {
write_len = 0;
}
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
}
#endif /* #if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT) */
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, NULL);
@@ -538,10 +571,34 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
ESP_LOGI(GATTS_TAG, "Connected, conn_id %d, remote "ESP_BD_ADDR_STR"",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
prepare_write_env_clear(&a_prepare_write_env);
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
portENTER_CRITICAL(&s_write_throughput_stats_mux);
write_len = 0;
start_time = 0;
start = false;
current_time = 0;
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
#endif
#if (CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT)
can_send_notify = false;
#endif
break;
}
case ESP_GATTS_DISCONNECT_EVT:
is_connect = false;
prepare_write_env_clear(&a_prepare_write_env);
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
portENTER_CRITICAL(&s_write_throughput_stats_mux);
write_len = 0;
start_time = 0;
start = false;
current_time = 0;
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
#endif
#if (CONFIG_EXAMPLE_GATTS_NOTIFY_THROUGHPUT)
can_send_notify = false;
#endif
ESP_LOGI(GATTS_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
esp_ble_gap_start_advertising(&adv_params);
@@ -637,11 +694,22 @@ void throughput_cal_task(void *param)
{
uint32_t bit_rate = 0;
vTaskDelay(2000 / portTICK_PERIOD_MS);
if (is_connect && start_time) {
current_time = esp_timer_get_time();
bit_rate = write_len * SECOND_TO_USECOND / (current_time - start_time);
ESP_LOGI(GATTS_TAG, "GATTC write Bit rate = %" PRIu32 " Byte/s, = %" PRIu32 " bit/s, time %d",
bit_rate, bit_rate<<3, (int)((current_time - start_time) / SECOND_TO_USECOND));
if (is_connect) {
uint64_t snap_write_len;
uint64_t snap_start_time;
portENTER_CRITICAL(&s_write_throughput_stats_mux);
snap_start_time = start_time;
snap_write_len = write_len;
portEXIT_CRITICAL(&s_write_throughput_stats_mux);
if (snap_start_time != 0) {
current_time = esp_timer_get_time();
uint64_t elapsed_us = current_time - snap_start_time;
if (elapsed_us > 0) {
bit_rate = (uint32_t)(snap_write_len * SECOND_TO_USECOND / elapsed_us);
ESP_LOGI(GATTS_TAG, "GATTC write Bit rate = %" PRIu32 " Byte/s, = %" PRIu32 " bit/s, time %d",
bit_rate, bit_rate << 3, (int)(elapsed_us / SECOND_TO_USECOND));
}
}
}
}
@@ -8,3 +8,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -204,6 +204,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
/* free char_elem_result */
free(char_elem_result);
char_elem_result = NULL;
}else{
ESP_LOGE(GATTC_TAG, "no char found");
}
@@ -262,6 +263,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
/* free descr_elem_result */
free(descr_elem_result);
descr_elem_result = NULL;
}
}
else{
@@ -488,7 +490,7 @@ void app_main(void)
}
esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
ret = esp_bluedroid_init_with_cfg(&cfg);;
ret = esp_bluedroid_init_with_cfg(&cfg);
if (ret) {
ESP_LOGE(GATTC_TAG, "%s init bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT client only example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -15,6 +15,7 @@
****************************************************************************/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdio.h>
@@ -177,6 +178,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT:
if (param->open.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "Open failed, status %x", p_data->open.status);
connect = false;
break;
}
ESP_LOGI(GATTC_TAG, "Open successfully, MTU %d", p_data->open.mtu);
@@ -6,3 +6,9 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT security client example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -178,9 +178,9 @@ static const esp_gatts_attr_db_t heart_rate_gatt_db[HRS_IDX_NB] =
sizeof(uint8_t), sizeof(heart_ctrl_point), (uint8_t *)heart_ctrl_point}},
};
static char *esp_key_type_to_str(esp_ble_key_type_t key_type)
static const char *esp_key_type_to_str(esp_ble_key_type_t key_type)
{
char *key_str = NULL;
const char *key_str = NULL;
switch(key_type) {
case ESP_LE_KEY_NONE:
key_str = "ESP_LE_KEY_NONE";
@@ -257,12 +257,16 @@ static char *esp_auth_req_to_str(esp_ble_auth_req_t auth_req)
static void show_bonded_devices(void)
{
int dev_num = esp_ble_get_bond_device_num();
if (dev_num < 0) {
ESP_LOGE(GATTS_TABLE_TAG, "Get bond device num failed (stack may be disabled), ret %d", dev_num);
return;
}
if (dev_num == 0) {
ESP_LOGI(GATTS_TABLE_TAG, "Bonded devices number zero\n");
return;
}
esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * dev_num);
esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * (size_t)dev_num);
if (!dev_list) {
ESP_LOGI(GATTS_TABLE_TAG, "malloc failed, return\n");
return;
@@ -280,12 +284,16 @@ static void show_bonded_devices(void)
static void __attribute__((unused)) remove_all_bonded_devices(void)
{
int dev_num = esp_ble_get_bond_device_num();
if (dev_num < 0) {
ESP_LOGE(GATTS_TABLE_TAG, "Get bond device num failed (stack may be disabled), ret %d", dev_num);
return;
}
if (dev_num == 0) {
ESP_LOGI(GATTS_TABLE_TAG, "Bonded devices number zero\n");
return;
}
esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * dev_num);
esp_ble_bond_dev_t *dev_list = (esp_ble_bond_dev_t *)malloc(sizeof(esp_ble_bond_dev_t) * (size_t)dev_num);
if (!dev_list) {
ESP_LOGI(GATTS_TABLE_TAG, "malloc failed, return\n");
return;
@@ -461,7 +469,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
case ESP_GATTS_CONGEST_EVT:
break;
case ESP_GATTS_CREAT_ATTR_TAB_EVT: {
if (param->create.status == ESP_GATT_OK){
if (param->add_attr_tab.status == ESP_GATT_OK){
if(param->add_attr_tab.num_handle == HRS_IDX_NB) {
ESP_LOGI(GATTS_TABLE_TAG, "Attribute table create successfully, num_handle %x", param->add_attr_tab.num_handle);
memcpy(heart_rate_handle_table, param->add_attr_tab.handles,
@@ -472,7 +480,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
param->add_attr_tab.num_handle, HRS_IDX_NB);
}
}else{
ESP_LOGE(GATTS_TABLE_TAG, "Attribute table create failed, error code = %x", param->create.status);
ESP_LOGE(GATTS_TABLE_TAG, "Attribute table create failed, error code = %x", param->add_attr_tab.status);
}
break;
}
@@ -6,3 +6,9 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT security server example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -198,6 +198,18 @@ typedef struct {
static prepare_type_env_t a_prepare_write_env;
static prepare_type_env_t b_prepare_write_env;
static void prepare_write_env_clear(prepare_type_env_t *env)
{
if (env == NULL) {
return;
}
if (env->prepare_buf != NULL) {
free(env->prepare_buf);
env->prepare_buf = NULL;
}
env->prepare_len = 0;
}
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
@@ -206,27 +218,43 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
switch (event) {
#ifdef CONFIG_EXAMPLE_SET_RAW_ADV_DATA
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
if (param->adv_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Raw adv data set failed, status %d", param->adv_data_raw_cmpl.status);
break;
}
adv_config_done &= (~adv_config_flag);
if (adv_config_done==0){
if (adv_config_done == 0) {
esp_ble_gap_start_advertising(&adv_params);
}
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_RAW_SET_COMPLETE_EVT:
if (param->scan_rsp_data_raw_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Raw scan rsp data set failed, status %d", param->scan_rsp_data_raw_cmpl.status);
break;
}
adv_config_done &= (~scan_rsp_config_flag);
if (adv_config_done==0){
if (adv_config_done == 0) {
esp_ble_gap_start_advertising(&adv_params);
}
break;
#else
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
if (param->adv_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Adv data set failed, status %d", param->adv_data_cmpl.status);
break;
}
adv_config_done &= (~adv_config_flag);
if (adv_config_done == 0){
if (adv_config_done == 0) {
esp_ble_gap_start_advertising(&adv_params);
}
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
if (param->scan_rsp_data_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Scan rsp data set failed, status %d", param->scan_rsp_data_cmpl.status);
break;
}
adv_config_done &= (~scan_rsp_config_flag);
if (adv_config_done == 0){
if (adv_config_done == 0) {
esp_ble_gap_start_advertising(&adv_params);
}
break;
@@ -240,7 +268,7 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
ESP_LOGI(GATTS_TAG, "Advertising start successfully");
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) {
if (param->adv_stop_cmpl.status != ESP_BT_STATUS_SUCCESS) {
ESP_LOGE(GATTS_TAG, "Advertising stop failed, status %d", param->adv_stop_cmpl.status);
break;
}
@@ -274,7 +302,7 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
status = ESP_GATT_INVALID_ATTR_LEN;
}
if (status == ESP_GATT_OK && prepare_write_env->prepare_buf == NULL) {
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE*sizeof(uint8_t));
prepare_write_env->prepare_buf = (uint8_t *)calloc(PREPARE_BUF_MAX_SIZE, sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TAG, "Gatt_server prep no mem");
@@ -305,7 +333,14 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
/* Extent is max(end of this fragment), not sum(len): same offset overwrites, not appends. */
int frag_end = (int)param->write.offset + (int)param->write.len;
if (frag_end > prepare_write_env->prepare_len) {
prepare_write_env->prepare_len = frag_end;
}
if (prepare_write_env->prepare_len > PREPARE_BUF_MAX_SIZE) {
prepare_write_env->prepare_len = PREPARE_BUF_MAX_SIZE;
}
}else{
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, NULL);
@@ -315,15 +350,19 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC){
ESP_LOG_BUFFER_HEX(GATTS_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
int log_len = prepare_write_env->prepare_len;
if (log_len < 0) {
log_len = 0;
} else if (log_len > PREPARE_BUF_MAX_SIZE) {
log_len = PREPARE_BUF_MAX_SIZE;
}
if (prepare_write_env->prepare_buf != NULL && log_len > 0) {
ESP_LOG_BUFFER_HEX(GATTS_TAG, prepare_write_env->prepare_buf, (size_t)log_len);
}
}else{
ESP_LOGI(GATTS_TAG,"Prepare write cancel");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
prepare_write_env_clear(prepare_write_env);
}
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
@@ -546,12 +585,13 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(GATTS_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
prepare_write_env_clear(&a_prepare_write_env);
esp_ble_gap_start_advertising(&adv_params);
local_mtu = 23; // Reset MTU for a single connection
break;
case ESP_GATTS_CONF_EVT:
ESP_LOGI(GATTS_TAG, "Confirm receive, status %d, attr_handle %d", param->conf.status, param->conf.handle);
if (param->conf.status != ESP_GATT_OK){
if (param->conf.status == ESP_GATT_OK && param->conf.value != NULL && param->conf.len > 0) {
ESP_LOG_BUFFER_HEX(GATTS_TAG, param->conf.value, param->conf.len);
}
break;
@@ -692,11 +732,13 @@ static void gatts_profile_b_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
break;
case ESP_GATTS_CONF_EVT:
ESP_LOGI(GATTS_TAG, "Confirm receive, status %d, attr_handle %d", param->conf.status, param->conf.handle);
if (param->conf.status != ESP_GATT_OK){
if (param->conf.status == ESP_GATT_OK && param->conf.value != NULL && param->conf.len > 0) {
ESP_LOG_BUFFER_HEX(GATTS_TAG, param->conf.value, param->conf.len);
}
break;
break;
case ESP_GATTS_DISCONNECT_EVT:
prepare_write_env_clear(&b_prepare_write_env);
break;
case ESP_GATTS_OPEN_EVT:
case ESP_GATTS_CANCEL_OPEN_EVT:
case ESP_GATTS_CLOSE_EVT:
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -14,6 +14,7 @@
*
****************************************************************************/
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -57,8 +58,21 @@ typedef struct {
int prepare_len;
} prepare_type_env_t;
/* Single-connection demo: one prepare-write buffer for the lone GATT link. */
static prepare_type_env_t prepare_write_env;
static void prepare_write_env_clear(prepare_type_env_t *env)
{
if (env == NULL) {
return;
}
if (env->prepare_buf != NULL) {
free(env->prepare_buf);
env->prepare_buf = NULL;
}
env->prepare_len = 0;
}
#define CONFIG_SET_RAW_ADV_DATA
#ifdef CONFIG_SET_RAW_ADV_DATA
static uint8_t raw_adv_data[] = {
@@ -288,7 +302,7 @@ void example_prepare_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t
status = ESP_GATT_INVALID_ATTR_LEN;
}
if (status == ESP_GATT_OK && prepare_write_env->prepare_buf == NULL) {
prepare_write_env->prepare_buf = (uint8_t *)malloc(PREPARE_BUF_MAX_SIZE * sizeof(uint8_t));
prepare_write_env->prepare_buf = (uint8_t *)calloc(PREPARE_BUF_MAX_SIZE, sizeof(uint8_t));
prepare_write_env->prepare_len = 0;
if (prepare_write_env->prepare_buf == NULL) {
ESP_LOGE(GATTS_TABLE_TAG, "%s, Gatt_server prep no mem", __func__);
@@ -321,21 +335,32 @@ void example_prepare_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t
memcpy(prepare_write_env->prepare_buf + param->write.offset,
param->write.value,
param->write.len);
prepare_write_env->prepare_len += param->write.len;
/* Extent is max(end of fragment), not sum(len); cap to allocated size. */
int frag_end = (int)param->write.offset + (int)param->write.len;
if (frag_end > prepare_write_env->prepare_len) {
prepare_write_env->prepare_len = frag_end;
}
if (prepare_write_env->prepare_len > PREPARE_BUF_MAX_SIZE) {
prepare_write_env->prepare_len = PREPARE_BUF_MAX_SIZE;
}
}
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param){
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC && prepare_write_env->prepare_buf){
ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, prepare_write_env->prepare_buf, prepare_write_env->prepare_len);
int log_len = prepare_write_env->prepare_len;
if (log_len < 0) {
log_len = 0;
} else if (log_len > PREPARE_BUF_MAX_SIZE) {
log_len = PREPARE_BUF_MAX_SIZE;
}
if (log_len > 0) {
ESP_LOG_BUFFER_HEX(GATTS_TABLE_TAG, prepare_write_env->prepare_buf, (size_t)log_len);
}
}else{
ESP_LOGI(GATTS_TABLE_TAG,"ESP_GATT_PREP_WRITE_CANCEL");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
prepare_write_env_clear(prepare_write_env);
}
static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param)
@@ -459,6 +484,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "ESP_GATTS_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason);
prepare_write_env_clear(&prepare_write_env);
esp_ble_gap_start_advertising(&adv_params);
break;
case ESP_GATTS_CREAT_ATTR_TAB_EVT:{
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT server only example
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_SCAN_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -125,10 +125,11 @@ static struct gattc_profile_inst gl_profile_tab[PROFILE_NUM] = {
};
/* Restarts scanning; does not clear Isconnecting — that flag is tied to enh_open/OPEN_EVT only
* so unrelated events (e.g. another profile disconnect) cannot drop the guard mid–connect attempt. */
static void start_scan(void)
{
stop_scan_done = false;
Isconnecting = false;
uint32_t duration = 30;
esp_ble_gap_start_scanning(duration);
}
@@ -154,7 +155,8 @@ static void gattc_profile_a_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
//open failed, ignore the first device, connect the second device
ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
conn_device_a = false;
//start_scan();
Isconnecting = false;
start_scan();
break;
}
memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, p_data->open.remote_bda, 6);
@@ -166,6 +168,7 @@ static void gattc_profile_a_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
if (mtu_ret){
ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
Isconnecting = false;
break;
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
@@ -267,6 +270,9 @@ static void gattc_profile_a_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
&count);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_descr_by_char_handle error");
free(descr_elem_result_a);
descr_elem_result_a = NULL;
break;
}
/* Every char has only one descriptor in our 'ESP_GATTS_DEMO' demo, so we used first 'descr_elem_result' */
@@ -286,6 +292,7 @@ static void gattc_profile_a_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
/* free descr_elem_result */
free(descr_elem_result_a);
descr_elem_result_a = NULL;
}
}
else{
@@ -327,8 +334,8 @@ static void gattc_profile_a_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
case ESP_GATTC_SRVC_CHG_EVT: {
esp_bd_addr_t bda;
memcpy(bda, p_data->srvc_chg.remote_bda, sizeof(esp_bd_addr_t));
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%08x%04x",(bda[0] << 24) + (bda[1] << 16) + (bda[2] << 8) + bda[3],
(bda[4] << 8) + bda[5]);
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%02x:%02x:%02x:%02x:%02x:%02x",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
break;
}
case ESP_GATTC_DISCONNECT_EVT:
@@ -360,7 +367,8 @@ static void gattc_profile_b_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
//open failed, ignore the second device, connect the third device
ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
conn_device_b = false;
//start_scan();
Isconnecting = false;
start_scan();
break;
}
memcpy(gl_profile_tab[PROFILE_B_APP_ID].remote_bda, p_data->open.remote_bda, 6);
@@ -372,6 +380,7 @@ static void gattc_profile_b_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
if (mtu_ret){
ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
Isconnecting = false;
break;
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
@@ -538,8 +547,8 @@ static void gattc_profile_b_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
case ESP_GATTC_SRVC_CHG_EVT: {
esp_bd_addr_t bda;
memcpy(bda, p_data->srvc_chg.remote_bda, sizeof(esp_bd_addr_t));
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%08x%04x",(bda[0] << 24) + (bda[1] << 16) + (bda[2] << 8) + bda[3],
(bda[4] << 8) + bda[5]);
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%02x:%02x:%02x:%02x:%02x:%02x",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
break;
}
case ESP_GATTC_DISCONNECT_EVT:
@@ -568,7 +577,8 @@ static void gattc_profile_c_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
if (p_data->open.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "connect device failed, status %d", p_data->open.status);
conn_device_c = false;
//start_scan();
Isconnecting = false;
start_scan();
break;
}
memcpy(gl_profile_tab[PROFILE_C_APP_ID].remote_bda, p_data->open.remote_bda, 6);
@@ -580,6 +590,7 @@ static void gattc_profile_c_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
if (mtu_ret){
ESP_LOGE(GATTC_TAG, "config MTU error, error code = %x", mtu_ret);
}
Isconnecting = false;
break;
case ESP_GATTC_CFG_MTU_EVT:
if (param->cfg_mtu.status != ESP_GATT_OK){
@@ -746,8 +757,8 @@ static void gattc_profile_c_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
case ESP_GATTC_SRVC_CHG_EVT: {
esp_bd_addr_t bda;
memcpy(bda, p_data->srvc_chg.remote_bda, sizeof(esp_bd_addr_t));
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%08x%04x",(bda[0] << 24) + (bda[1] << 16) + (bda[2] << 8) + bda[3],
(bda[4] << 8) + bda[5]);
ESP_LOGI(GATTC_TAG, "ESP_GATTC_SRVC_CHG_EVT, bd_addr:%02x:%02x:%02x:%02x:%02x:%02x",
bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
break;
}
case ESP_GATTC_DISCONNECT_EVT:
@@ -6,3 +6,10 @@ CONFIG_BT_ENABLED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not set
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
# Disable unused Bluedroid host features for GATT client only example
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_42_ADV_EN is not set
# CONFIG_BT_BLE_42_DTM_TEST_EN is not set
@@ -2,3 +2,7 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
# Disable unused Bluedroid host features (GATT security client, extended scan only)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -287,7 +287,7 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
esp_ble_gap_ext_adv_start(NUM_EXT_ADV_SET, &ext_adv[0]);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "Extended advertising start, status %d", param->ext_adv_data_set.status);
ESP_LOGI(GATTS_TABLE_TAG, "Extended advertising start, status %d", param->ext_adv_start.status);
break;
case ESP_GAP_BLE_ADV_TERMINATED_EVT:
ESP_LOGI(GATTS_TABLE_TAG, "Extended advertising terminated, status %d", param->adv_terminate.status);
@@ -2,3 +2,7 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
# Disable unused Bluedroid host features (GATT security server, extended adv only)
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -163,6 +163,9 @@ static uint8_t check_sum(uint8_t *addr, uint16_t count)
if (addr == NULL || count == 0) {
return 0;
}
if (count > (ESP_GATT_MAX_MTU_SIZE - 3U)) {
return 0;
}
for(int i = 0; i < count; i++) {
sum = sum + addr[i];
@@ -175,6 +178,15 @@ static uint8_t check_sum(uint8_t *addr, uint16_t count)
return (uint8_t)~sum;
}
static void throughput_client_resume_ext_scan(void)
{
connect = false;
esp_err_t err = esp_ble_gap_start_ext_scan(EXT_SCAN_DURATION, EXT_SCAN_PERIOD);
if (err != ESP_OK) {
ESP_LOGE(GATTC_TAG, "start_ext_scan failed: %s", esp_err_to_name(err));
}
}
static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
{
esp_ble_gattc_cb_param_t *p_data = (esp_ble_gattc_cb_param_t *)param;
@@ -202,6 +214,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT:
if (param->open.status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "Open failed, status %d", p_data->open.status);
throughput_client_resume_ext_scan();
break;
}
ESP_LOGI(GATTC_TAG, "Open successfully, MTU %u", param->open.mtu);
@@ -293,12 +306,13 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
&count);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_attr_count error");
}
if (count > 0){
} else if (count == 0) {
ESP_LOGE(GATTC_TAG, "decsr not found");
} else {
descr_elem_result = malloc(sizeof(esp_gattc_descr_elem_t) * count);
if (!descr_elem_result){
if (descr_elem_result == NULL){
ESP_LOGE(GATTC_TAG, "malloc error, gattc no mem");
}else{
} else {
ret_status = esp_ble_gattc_get_descr_by_char_handle( gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
p_data->reg_for_notify.handle,
@@ -307,10 +321,10 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
&count);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_get_descr_by_char_handle error");
}
/* Every char has only one descriptor in our 'throughput_server' demo, so we use first 'descr_elem_result' */
if (count > 0 && descr_elem_result[0].uuid.len == ESP_UUID_LEN_16 && descr_elem_result[0].uuid.uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG){
} else if (count > 0 &&
descr_elem_result[0].uuid.len == ESP_UUID_LEN_16 &&
descr_elem_result[0].uuid.uuid.uuid16 == ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
/* Every char has only one descriptor in our 'throughput_server' demo, so we use first 'descr_elem_result' */
ret_status = esp_ble_gattc_write_char_descr( gattc_if,
gl_profile_tab[PROFILE_A_APP_ID].conn_id,
descr_elem_result[0].handle,
@@ -318,29 +332,37 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
(uint8_t *)&notify_en,
ESP_GATT_WRITE_TYPE_RSP,
ESP_GATT_AUTH_REQ_NONE);
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_write_char_descr error");
}
}
if (ret_status != ESP_GATT_OK){
ESP_LOGE(GATTC_TAG, "esp_ble_gattc_write_char_descr error");
}
/* free descr_elem_result */
free(descr_elem_result);
descr_elem_result = NULL;
}
}
else{
ESP_LOGE(GATTC_TAG, "decsr not found");
}
}
break;
}
case ESP_GATTC_NOTIFY_EVT: {
#if (CONFIG_GATTS_NOTIFY_THROUGHPUT)
if (p_data->notify.is_notify &&
(p_data->notify.value[p_data->notify.value_len - 1] ==
check_sum(p_data->notify.value, p_data->notify.value_len - 1))){
notify_len += p_data->notify.value_len;
if (p_data->notify.is_notify) {
uint16_t vlen = p_data->notify.value_len;
uint8_t *val = p_data->notify.value;
/* value_len == 0 makes (vlen - 1) wrap; never index or pass to check_sum before validating. */
const uint16_t max_notify_len = (uint16_t)(ESP_GATT_MAX_MTU_SIZE - 3U);
if (val == NULL) {
ESP_LOGW(GATTC_TAG, "notify ignored: null value");
} else if (vlen == 0) {
ESP_LOGW(GATTC_TAG, "notify ignored: zero length");
} else if (vlen < 2) {
ESP_LOGW(GATTC_TAG, "notify ignored: length too short for payload+checksum");
} else if (vlen > max_notify_len) {
ESP_LOGW(GATTC_TAG, "notify ignored: length exceeds bound");
} else if (val[vlen - 1] == check_sum(val, (uint16_t)(vlen - 1U))) {
notify_len += vlen;
} else {
ESP_LOGE(GATTC_TAG, "notify checksum mismatch");
}
} else {
ESP_LOGE(GATTC_TAG, "Indication received, value:");
}
@@ -385,8 +407,14 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
current_time = 0;
notify_len = 0;
#endif /* #if (CONFIG_GATTS_NOTIFY_THROUGHPUT) */
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
/* Unblock throughput_client_task if it is waiting on gattc_semaphore while congested. */
can_send_write = true;
xSemaphoreGive(gattc_semaphore);
#endif /* #if (CONFIG_GATTC_WRITE_THROUGHPUT) */
ESP_LOGI(GATTC_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(p_data->disconnect.remote_bda), p_data->disconnect.reason);
throughput_client_resume_ext_scan();
break;
case ESP_GATTC_CONGEST_EVT:
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
@@ -517,7 +545,6 @@ static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
static void throughput_client_task(void *param)
{
vTaskDelay(2000 / portTICK_PERIOD_MS);
uint8_t sum = check_sum(write_data, sizeof(write_data) - 1);
write_data[GATTC_WRITE_LEN - 1] = sum;
@@ -619,6 +646,17 @@ void app_main(void)
return;
}
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
/* Create the semaphore before registering the GATTC callback so that any
* xSemaphoreGive() invoked from the callback is guaranteed to see a valid handle.
*/
gattc_semaphore = xSemaphoreCreateBinary();
if (gattc_semaphore == NULL) {
ESP_LOGE(GATTC_TAG, "%s: gattc semaphore create failed", __func__);
return;
}
#endif /* #if (CONFIG_GATTC_WRITE_THROUGHPUT) */
//register the callback function to the gattc module
ret = esp_ble_gattc_register_callback(esp_gattc_cb);
if(ret){
@@ -636,20 +674,11 @@ void app_main(void)
ESP_LOGE(GATTC_TAG, "set local MTU failed, error code = %x", local_mtu_ret);
}
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
// The task is only created on the CPU core that Bluetooth is working on,
// preventing the sending task from using the un-updated Bluetooth state on another CPU.
/* Create the task only after the semaphore exists; never rely on priority or delays. */
xTaskCreatePinnedToCore(&throughput_client_task, "throughput_client_task", 4096, NULL, 10, NULL, BLUETOOTH_TASK_PINNED_TO_CORE);
#endif
#if (CONFIG_GATTS_NOTIFY_THROUGHPUT)
xTaskCreatePinnedToCore(&throughput_cal_task, "throughput_cal_task", 4096, NULL, 9, NULL, BLUETOOTH_TASK_PINNED_TO_CORE);
#endif
#if (CONFIG_GATTC_WRITE_THROUGHPUT)
gattc_semaphore = xSemaphoreCreateBinary();
if (!gattc_semaphore) {
ESP_LOGE(GATTC_TAG, "%s, init fail, the gattc semaphore create fail.", __func__);
return;
}
#endif /* #if (CONFIG_GATTC_WRITE_THROUGHPUT) */
}
@@ -8,3 +8,8 @@ CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n
# CONFIG_BT_LE_50_FEATURE_SUPPORT=n
CONFIG_GATTS_NOTIFY_THROUGHPUT=y
CONFIG_GATTC_WRITE_THROUGHPUT=n
# Disable unused Bluedroid host features (GATT client only)
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -164,6 +164,18 @@ typedef struct {
static prepare_type_env_t a_prepare_write_env;
static void prepare_write_env_clear(prepare_type_env_t *env)
{
if (env == NULL) {
return;
}
if (env->prepare_buf != NULL) {
free(env->prepare_buf);
env->prepare_buf = NULL;
}
env->prepare_len = 0;
}
extern void esp_ble_switch_phy_coded(bool phy_500k);
void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble_gatts_cb_param_t *param);
@@ -175,6 +187,9 @@ static uint8_t check_sum(uint8_t *addr, uint16_t count)
if (addr == NULL || count == 0) {
return 0;
}
if (count > (ESP_GATT_MAX_MTU_SIZE - 3U)) {
return 0;
}
for(int i = 0; i < count; i++) {
sum = sum + addr[i];
@@ -228,6 +243,8 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
status = ESP_GATT_INVALID_OFFSET;
} else if ((param->write.offset + param->write.len) > PREPARE_BUF_MAX_SIZE) {
status = ESP_GATT_INVALID_ATTR_LEN;
} else if (param->write.len > ESP_GATT_MAX_ATTR_LEN) {
status = ESP_GATT_INVALID_ATTR_LEN;
}
if (status == ESP_GATT_OK && prepare_write_env->prepare_buf == NULL) {
@@ -239,13 +256,19 @@ void example_write_event_env(esp_gatt_if_t gatts_if, prepare_type_env_t *prepare
}
}
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)malloc(sizeof(esp_gatt_rsp_t));
esp_gatt_rsp_t *gatt_rsp = (esp_gatt_rsp_t *)calloc(1, sizeof(esp_gatt_rsp_t));
if (gatt_rsp) {
gatt_rsp->attr_value.len = param->write.len;
gatt_rsp->attr_value.handle = param->write.handle;
gatt_rsp->attr_value.offset = param->write.offset;
gatt_rsp->attr_value.auth_req = ESP_GATT_AUTH_REQ_NONE;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
if (status == ESP_GATT_OK) {
if (param->write.value == NULL) {
status = ESP_GATT_INVALID_ATTR_LEN;
} else {
gatt_rsp->attr_value.len = param->write.len;
memcpy(gatt_rsp->attr_value.value, param->write.value, param->write.len);
}
}
esp_err_t response_err = esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, gatt_rsp);
if (response_err != ESP_OK) {
@@ -275,11 +298,7 @@ void example_exec_write_event_env(prepare_type_env_t *prepare_write_env, esp_ble
if (param->exec_write.exec_write_flag != ESP_GATT_PREP_WRITE_EXEC){
ESP_LOGI(GATTS_TAG,"Prepare write cancel");
}
if (prepare_write_env->prepare_buf) {
free(prepare_write_env->prepare_buf);
prepare_write_env->prepare_buf = NULL;
}
prepare_write_env->prepare_len = 0;
prepare_write_env_clear(prepare_write_env);
}
static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param) {
@@ -356,9 +375,22 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
#if (CONFIG_EXAMPLE_GATTC_WRITE_THROUGHPUT)
if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].char_handle) {
// The last value byte is the checksum data, should used to check the data is received corrected or not.
if (param->write.value[param->write.len - 1] ==
check_sum(param->write.value, param->write.len - 1)) {
write_len += param->write.len;
uint16_t wlen = param->write.len;
uint8_t *wval = param->write.value;
/* len==0 makes (wlen-1) wrap; cap len before indexing or passing to check_sum. */
const uint16_t max_write_len = (uint16_t)(ESP_GATT_MAX_MTU_SIZE - 3U);
if (wval == NULL) {
ESP_LOGW(GATTS_TAG, "write ignored: null value");
} else if (wlen == 0) {
ESP_LOGW(GATTS_TAG, "write ignored: zero length");
} else if (wlen < 2) {
ESP_LOGW(GATTS_TAG, "write ignored: length too short for payload+checksum");
} else if (wlen > max_write_len) {
ESP_LOGW(GATTS_TAG, "write ignored: length exceeds bound");
} else if (wval[wlen - 1] == check_sum(wval, (uint16_t)(wlen - 1U))) {
write_len += wlen;
} else {
ESP_LOGE(GATTS_TAG, "write checksum mismatch");
}
if (start == false) {
@@ -451,10 +483,12 @@ static void gatts_profile_a_event_handler(esp_gatts_cb_event_t event, esp_gatt_i
ESP_LOGI(GATTS_TAG, "Connected, conn_id %u, remote "ESP_BD_ADDR_STR"",
param->connect.conn_id, ESP_BD_ADDR_HEX(param->connect.remote_bda));
gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->connect.conn_id;
prepare_write_env_clear(&a_prepare_write_env);
break;
}
case ESP_GATTS_DISCONNECT_EVT:
is_connect = false;
prepare_write_env_clear(&a_prepare_write_env);
ESP_LOGI(GATTS_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
esp_ble_gap_ext_adv_start(NUM_EXT_ADV_SET, &ext_adv[0]);
@@ -9,3 +9,8 @@ CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n
# CONFIG_BT_LE_50_FEATURE_SUPPORT is not used on ESP32, ESP32-C3 and ESP32-S3.
# CONFIG_BT_LE_50_FEATURE_SUPPORT=n
# Disable unused Bluedroid host features (GATT server only)
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -96,6 +96,16 @@ static struct gattc_profile_inst gl_profile_tab[PROFILE_NUM] = {
},
};
/** Clear connect-in-progress flag and restart indefinite extended scan (after failed open or disconnect). */
static void central_resume_ext_scan(void)
{
connect = false;
esp_err_t err = esp_ble_gap_start_ext_scan(0, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "start ext scan failed, error = 0x%x", err);
}
}
/**
* @brief GATT client event handler
*/
@@ -112,6 +122,10 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_REG_EVT:
ESP_LOGI(TAG, "GATT client register, status %d, app_id %d, gattc_if %d",
p_data->reg.status, p_data->reg.app_id, gattc_if);
if (p_data->reg.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "GATT client register failed, status %d", p_data->reg.status);
break;
}
gl_profile_tab[PROFILE_A_APP_ID].gattc_if = gattc_if;
// Set default subrate parameters
esp_ble_default_subrate_param_t default_subrate_params = {
@@ -148,11 +162,19 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
ESP_LOGI(TAG, "Subrate request sent successfully");
}
break;
case ESP_GATTC_OPEN_EVT:
if (p_data->open.status != ESP_GATT_OK) {
ESP_LOGE(TAG, "Open failed, status %d", p_data->open.status);
central_resume_ext_scan();
break;
}
ESP_LOGI(TAG, "GATT open OK, conn_id %d, MTU %u", p_data->open.conn_id, p_data->open.mtu);
break;
case ESP_GATTC_DISCONNECT_EVT:
connect = false;
g_conn_handle = 0xFFFF;
ESP_LOGI(TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(p_data->disconnect.remote_bda), p_data->disconnect.reason);
central_resume_ext_scan();
break;
default:
break;
@@ -208,10 +230,9 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
creat_conn_params.phy_1m_conn_params = &phy_1m_conn_params;
creat_conn_params.phy_2m_conn_params = &phy_2m_conn_params;
creat_conn_params.phy_coded_conn_params = &phy_coded_conn_params;
if (esp_ble_gattc_enh_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, &creat_conn_params) != ESP_OK)
{
connect = false;
if (esp_ble_gattc_enh_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, &creat_conn_params) != ESP_OK) {
ESP_LOGE(TAG, "Failed to open connection");
central_resume_ext_scan();
}
}
}
@@ -25,3 +25,4 @@ CONFIG_BT_BLE_50_EXTEND_ADV_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -25,3 +25,4 @@ CONFIG_BT_BLE_50_PERIODIC_ADV_EN=n
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -37,6 +37,7 @@
#define REMOTE_NOTIFY_UUID 0xFF01
#define EXT_SCAN_DURATION 0
#define EXT_SCAN_PERIOD 0
#define BLE_CONN_HDL_INVALID ((uint16_t)0xFFFF)
///Declare static functions
static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
@@ -95,7 +96,7 @@ const esp_ble_conn_params_t phy_coded_conn_params = {
static uint8_t antenna_ids[2] = {0x00, 0x01};
esp_ble_cte_recv_params_params_t cte_recv_params = {
.conn_handle = 0xff,
.conn_handle = BLE_CONN_HDL_INVALID,
.sampling_en = ESP_BLE_CTE_SAMPLING_ENABLE,
.slot_dur = ESP_BLE_CTE_SLOT_DURATION_2US,
.switching_pattern_len = sizeof(antenna_ids),
@@ -103,14 +104,14 @@ esp_ble_cte_recv_params_params_t cte_recv_params = {
};
static esp_ble_cte_req_en_params_t cte_conn_req_en = {
.conn_handle = 0xff,
.conn_handle = BLE_CONN_HDL_INVALID,
.enable = ESP_BLE_CTE_SAMPLING_ENABLE,
.cte_req_interval = 0x05,
.req_cte_len = ESP_BLE_CTE_MAX_REQUESTED_CTE_LENGTH,
.req_cte_Type = ESP_BLE_CTE_TYPE_AOA,
};
uint16_t cur_conn_hdl = 0xff;
uint16_t cur_conn_hdl = BLE_CONN_HDL_INVALID;
#define PROFILE_NUM 1
#define PROFILE_A_APP_ID 0
@@ -210,6 +211,31 @@ static char *esp_auth_req_to_str(esp_ble_auth_req_t auth_req)
return auth_str;
}
/** After a failed open or disconnect: clear connect flag and restart extended scan. */
static void cte_resume_ext_scan(void)
{
connect = false;
esp_err_t err = esp_ble_gap_start_ext_scan(EXT_SCAN_DURATION, EXT_SCAN_PERIOD);
if (err != ESP_OK) {
ESP_LOGE(LOG_TAG, "start ext scan failed, error = 0x%x", err);
}
}
/** Start CTE receive setup only after link is authenticated (see esp_gap_cb AUTH_CMPL). */
static void cte_enable_connection_receive_after_encrypted(void)
{
if (cur_conn_hdl == BLE_CONN_HDL_INVALID) {
ESP_LOGW(LOG_TAG, "Skip CTE receive params: no connection handle");
return;
}
ESP_LOGI(LOG_TAG, "Set CTE connection receive params after encryption, conn_handle %d", cur_conn_hdl);
cte_recv_params.conn_handle = cur_conn_hdl;
esp_err_t cte_ret = esp_ble_cte_set_connection_receive_params(&cte_recv_params);
if (cte_ret != ESP_OK) {
ESP_LOGE(LOG_TAG, "CTE set connection receive params failed, 0x%x", cte_ret);
}
}
static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param)
{
esp_ble_gattc_cb_param_t *p_data = (esp_ble_gattc_cb_param_t *)param;
@@ -227,6 +253,7 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_OPEN_EVT:
if (param->open.status != ESP_GATT_OK){
ESP_LOGE(LOG_TAG, "Open failed, status %x", p_data->open.status);
cte_resume_ext_scan();
break;
}
ESP_LOGI(LOG_TAG, "Open successfully, MTU %d", p_data->open.mtu);
@@ -239,19 +266,14 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
case ESP_GATTC_CFG_MTU_EVT:
ESP_LOGI(LOG_TAG, "MTU exchange, status %d, MTU %d, conn_id %d", param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id);
if (!param->cfg_mtu.status) {
ESP_LOGI(LOG_TAG, "Set CTE connection receive params, conn_handle %d", cur_conn_hdl);
cte_recv_params.conn_handle = cur_conn_hdl;
esp_ble_cte_set_connection_receive_params(&cte_recv_params);
}
break;
case ESP_GATTC_DIS_SRVC_CMPL_EVT:
if (param->dis_srvc_cmpl.status != ESP_GATT_OK){
ESP_LOGE(LOG_TAG, "Service discover failed, status %d", param->dis_srvc_cmpl.status);
break;
}
ESP_LOGI(LOG_TAG, "Service discover complete, conn_id %d", param->dis_srvc_cmpl.conn_id);
esp_ble_gattc_search_service(gattc_if, param->cfg_mtu.conn_id, &remote_filter_service_uuid);
ESP_LOGI(LOG_TAG, "Service discover complete, conn_id %d", p_data->dis_srvc_cmpl.conn_id);
esp_ble_gattc_search_service(gattc_if, p_data->dis_srvc_cmpl.conn_id, &remote_filter_service_uuid);
break;
case ESP_GATTC_SEARCH_RES_EVT: {
ESP_LOGI(LOG_TAG, "Service search result, conn_id %x, is primary service %d", p_data->search_res.conn_id, p_data->search_res.is_primary);
@@ -314,9 +336,9 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGI(LOG_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%02x",
ESP_BD_ADDR_HEX(p_data->disconnect.remote_bda), p_data->disconnect.reason);
connect = false;
get_service = false;
esp_ble_gap_start_ext_scan(EXT_SCAN_DURATION, EXT_SCAN_PERIOD);
cur_conn_hdl = BLE_CONN_HDL_INVALID;
cte_resume_ext_scan();
break;
default:
break;
@@ -397,8 +419,7 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
ESP_LOGI(LOG_TAG, "Pairing failed, reason 0x%x",param->ble_security.auth_cmpl.fail_reason);
} else {
ESP_LOGI(LOG_TAG, "Pairing successfully, auth mode %s",esp_auth_req_to_str(param->ble_security.auth_cmpl.auth_mode));
// Enable CTE
cte_enable_connection_receive_after_encrypted();
}
break;
}
@@ -429,7 +450,10 @@ static void esp_gap_cb(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *par
creat_conn_params.phy_1m_conn_params = &phy_1m_conn_params;
creat_conn_params.phy_2m_conn_params = &phy_2m_conn_params;
creat_conn_params.phy_coded_conn_params = &phy_coded_conn_params;
esp_ble_gattc_enh_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, &creat_conn_params);
if (esp_ble_gattc_enh_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, &creat_conn_params) != ESP_OK) {
ESP_LOGE(LOG_TAG, "Failed to open connection");
cte_resume_ext_scan();
}
}
break;
@@ -453,14 +477,17 @@ static void cte_event_handler(esp_ble_cte_cb_event_t event, esp_ble_cte_cb_param
case ESP_BLE_CTE_SET_CONN_TRANS_PARAMS_CMPL_EVT:
ESP_LOGI(LOG_TAG, "CTE set connection transmit params, status %d", param->conn_trans_params_cmpl.status);
break;
case ESP_BLE_CTE_SET_CONN_RECV_PARAMS_CMPL_EVT:
ESP_LOGI(LOG_TAG, "CTE set connection receive params, status %d", param->conn_recv_params_cmpl.status);
case ESP_BLE_CTE_SET_CONN_RECV_PARAMS_CMPL_EVT: {
uint16_t recv_cmpl_conn_hdl = param->conn_recv_params_cmpl.conn_handle;
ESP_LOGI(LOG_TAG, "CTE set connection receive params, status %d, conn_handle %d",
param->conn_recv_params_cmpl.status, recv_cmpl_conn_hdl);
if (!param->conn_recv_params_cmpl.status) {
cte_conn_req_en.conn_handle = cur_conn_hdl;
ESP_LOGI(LOG_TAG, "Enable CTE request, conn_handle %d", cur_conn_hdl);
cte_conn_req_en.conn_handle = recv_cmpl_conn_hdl;
ESP_LOGI(LOG_TAG, "Enable CTE request, conn_handle %d", recv_cmpl_conn_hdl);
esp_ble_cte_connection_cte_request_enable(&cte_conn_req_en);
}
break;
}
case ESP_BLE_CTE_SET_CONN_REQ_ENABLE_CMPL_EVT:
ESP_LOGI(LOG_TAG, "CTE set connection request enable, status %d", param->conn_req_en_cmpl.status);
break;
@@ -20,3 +20,5 @@ CONFIG_BT_BLE_50_EXTEND_ADV_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -35,6 +35,9 @@
#define GATTS_DEMO_CHAR_VAL_LEN_MAX 0x40
/* Connection_Handle is 12-bit (0x0000..0x0EFF); 0xFFFF is outside the valid range. */
#define BLE_CONN_HDL_INVALID ((uint16_t)0xFFFF)
#ifndef MIN
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#endif
@@ -68,18 +71,18 @@ esp_ble_gap_ext_adv_params_t ext_adv_params_2M = {
static uint8_t antenna_ids[2] = {0x00, 0x01};
static esp_ble_cte_conn_trans_params_t cte_conn_trans_params = {
.conn_handle = 0xff,
.conn_handle = BLE_CONN_HDL_INVALID,
.cte_types = ESP_BLE_CTE_TYPES_ALL,
.switching_pattern_len = sizeof(antenna_ids),
.antenna_ids = &antenna_ids[0],
};
static esp_ble_cte_rsp_en_params_t cte_conn_rsp_en = {
.conn_handle = 0xff,
.conn_handle = BLE_CONN_HDL_INVALID,
.enable = ESP_BLE_CTE_RESPONSE_FOR_CONNECTION_ENABLE,
};
uint16_t cur_conn_hdl = 0xff;
uint16_t cur_conn_hdl = BLE_CONN_HDL_INVALID;
struct gatts_profile_inst {
esp_gatts_cb_t gatts_cb;
@@ -315,10 +318,14 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
ESP_LOGI(LOG_TAG, "Pairing failed, reason 0x%x",param->ble_security.auth_cmpl.fail_reason);
} else {
ESP_LOGI(LOG_TAG, "Pairing successfully, auth_mode %s",esp_auth_req_to_str(param->ble_security.auth_cmpl.auth_mode));
// Setting CTE connection transmit parameters
cte_conn_trans_params.conn_handle = cur_conn_hdl;
ESP_LOGI(LOG_TAG, "Set CTE connection transmit params, conn_handle %d", cur_conn_hdl);
esp_ble_cte_set_connection_transmit_params(&cte_conn_trans_params);
if (cur_conn_hdl != BLE_CONN_HDL_INVALID) {
// Setting CTE connection transmit parameters
cte_conn_trans_params.conn_handle = cur_conn_hdl;
ESP_LOGI(LOG_TAG, "Set CTE connection transmit params, conn_handle %d", cur_conn_hdl);
esp_ble_cte_set_connection_transmit_params(&cte_conn_trans_params);
} else {
ESP_LOGW(LOG_TAG, "Skip CTE transmit params: no active connection handle");
}
}
break;
}
@@ -385,6 +392,7 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
case ESP_GATTS_DISCONNECT_EVT:
ESP_LOGI(LOG_TAG, "Disconnected, remote "ESP_BD_ADDR_STR", reason 0x%x",
ESP_BD_ADDR_HEX(param->disconnect.remote_bda), param->disconnect.reason);
cur_conn_hdl = BLE_CONN_HDL_INVALID;
/* start advertising again when missing the connect */
esp_ble_gap_ext_adv_start(NUM_EXT_ADV_SET, &ext_adv[0]);
break;
@@ -399,18 +407,18 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
case ESP_GATTS_CONGEST_EVT:
break;
case ESP_GATTS_CREAT_ATTR_TAB_EVT: {
if (param->create.status == ESP_GATT_OK){
if(param->add_attr_tab.num_handle == HRS_IDX_NB) {
if (param->add_attr_tab.status == ESP_GATT_OK) {
if (param->add_attr_tab.num_handle == HRS_IDX_NB) {
ESP_LOGI(LOG_TAG, "Attribute table create successfully, num_handle %x", param->add_attr_tab.num_handle);
memcpy(profile_handle_table, param->add_attr_tab.handles,
sizeof(profile_handle_table));
sizeof(profile_handle_table));
esp_ble_gatts_start_service(profile_handle_table[IDX_SVC]);
}else{
} else {
ESP_LOGE(LOG_TAG, "Attribute table create abnormally, num_handle (%d) doesn't equal to HRS_IDX_NB(%d)",
param->add_attr_tab.num_handle, HRS_IDX_NB);
param->add_attr_tab.num_handle, HRS_IDX_NB);
}
}else{
ESP_LOGE(LOG_TAG, "Attribute table create failed, status %x", param->create.status);
} else {
ESP_LOGE(LOG_TAG, "Attribute table create failed, status %x", param->add_attr_tab.status);
}
break;
}
@@ -422,15 +430,17 @@ static void gatts_profile_event_handler(esp_gatts_cb_event_t event,
static void cte_event_handler(esp_ble_cte_cb_event_t event, esp_ble_cte_cb_param_t *param)
{
switch (event) {
case ESP_BLE_CTE_SET_CONN_TRANS_PARAMS_CMPL_EVT:
ESP_LOGI(LOG_TAG, "CTE set connection transmit params, status %d", param->conn_trans_params_cmpl.status);
case ESP_BLE_CTE_SET_CONN_TRANS_PARAMS_CMPL_EVT: {
uint16_t trans_cmpl_conn_hdl = param->conn_trans_params_cmpl.conn_handle;
ESP_LOGI(LOG_TAG, "CTE set connection transmit params, status %d, conn_handle %d",
param->conn_trans_params_cmpl.status, trans_cmpl_conn_hdl);
if (!param->conn_trans_params_cmpl.status) {
ESP_LOGI(LOG_TAG, "Setting CTE connection response enable");
// Enable CTE response for connection
cte_conn_rsp_en.conn_handle = cur_conn_hdl;
ESP_LOGI(LOG_TAG, "Setting CTE connection response enable, conn_handle %d", trans_cmpl_conn_hdl);
cte_conn_rsp_en.conn_handle = trans_cmpl_conn_hdl;
esp_ble_cte_connection_cte_response_enable(&cte_conn_rsp_en);
}
break;
}
case ESP_BLE_CTE_SET_CONN_RECV_PARAMS_CMPL_EVT:
ESP_LOGI(LOG_TAG, "CTE set connection receive params, status %d", param->conn_recv_params_cmpl.status);
break;
@@ -23,3 +23,5 @@ CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -223,7 +223,11 @@ static void start_periodic_adv(void)
{
// Create static random address
esp_bd_addr_t rand_addr;
esp_ble_gap_addr_create_static(rand_addr);
esp_err_t addr_ret = esp_ble_gap_addr_create_static(rand_addr);
if (addr_ret != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gap_addr_create_static failed: %s", esp_err_to_name(addr_ret));
return;
}
ESP_LOG_BUFFER_HEX(TAG, rand_addr, ESP_BD_ADDR_LEN);
@@ -34,3 +34,5 @@ CONFIG_BT_GATTS_ENABLE=n
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -32,3 +32,5 @@ CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_BT_BLE_SMP_ENABLE=n
CONFIG_BT_GATTS_ENABLE=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -243,7 +243,9 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
param->ext_adv_report.params.adv_data_len,
ESP_BLE_AD_TYPE_NAME_CMPL,
&adv_name_len);
if ((adv_name != NULL) && (memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
if ((adv_name != NULL) && (adv_name_len > 0) &&
(adv_name_len == strlen(remote_device_name)) &&
(memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
// Note: If there are multiple devices with the same device name, the device may sync to an unintended one.
// It is recommended to change the default device name to ensure it is unique.
periodic_sync = true;
@@ -12,3 +12,6 @@ CONFIG_BT_GATTC_ENABLE=n
CONFIG_BT_BLE_SMP_ENABLE=n
CONFIG_BT_BLE_50_EXTEND_ADV_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_BT_BLE_50_DTM_TEST_EN=n
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -41,16 +41,21 @@
#define FUNC_SEND_WAIT_SEM(func, sem) do {\
esp_err_t __err_rc = (func);\
if (__err_rc != ESP_OK) { \
ESP_LOGE(LOG_TAG, "%s, message send fail, error = %d", __func__, __err_rc); \
ESP_LOGE(LOG_TAG, "%s failed: %s", #func, esp_err_to_name(__err_rc)); \
return; \
} \
xSemaphoreTake((sem), portMAX_DELAY); \
if (last_ble_async_status != ESP_BT_STATUS_SUCCESS) { \
ESP_LOGE(LOG_TAG, "Async completion after %s failed, status 0x%x", #func, last_ble_async_status); \
return; \
} \
xSemaphoreTake(sem, portMAX_DELAY); \
} while(0);
#define EXT_ADV_HANDLE 0
#define NUM_EXT_ADV 1
static SemaphoreHandle_t test_sem = NULL;
static esp_bt_status_t last_ble_async_status = ESP_BT_STATUS_SUCCESS;
uint8_t addr_2m[6] = {0xc0, 0xde, 0x52, 0x00, 0x00, 0x02};
@@ -113,44 +118,56 @@ static esp_ble_cte_trans_enable_params_t cte_trans_enable = {
static uint8_t periodic_adv_hdl = 0xff;
static void ble_async_complete_signal(esp_bt_status_t status)
{
last_ble_async_status = status;
if (test_sem != NULL) {
xSemaphoreGive(test_sem);
}
}
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->ext_adv_set_rand_addr.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT, status %d", param->ext_adv_set_rand_addr.status);
break;
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->ext_adv_set_params.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT, status %d", param->ext_adv_set_params.status);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->ext_adv_data_set.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT, status %d", param->ext_adv_data_set.status);
break;
case ESP_GAP_BLE_EXT_SCAN_RSP_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->scan_rsp_set.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_SCAN_RSP_DATA_SET_COMPLETE_EVT, status %d", param->scan_rsp_set.status);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->ext_adv_start.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT, status %d", param->ext_adv_start.status);
break;
case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->ext_adv_stop.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT, status %d", param->ext_adv_stop.status);
break;
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
periodic_adv_hdl = param->peroid_adv_set_params.instance;
xSemaphoreGive(test_sem);
if (param->peroid_adv_set_params.status == ESP_BT_STATUS_SUCCESS) {
periodic_adv_hdl = param->peroid_adv_set_params.instance;
} else {
ESP_LOGE(LOG_TAG, "periodic adv set params failed, not updating periodic_adv_hdl");
}
ble_async_complete_signal(param->peroid_adv_set_params.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT, status %d", param->peroid_adv_set_params.status);
break;
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->period_adv_data_set.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT, status %d", param->period_adv_data_set.status);
break;
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->period_adv_start.status);
ESP_LOGI(LOG_TAG, "ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT, status %d", param->period_adv_start.status);
break;
default:
@@ -162,11 +179,11 @@ static void cte_event_handler(esp_ble_cte_cb_event_t event, esp_ble_cte_cb_param
{
switch (event) {
case ESP_BLE_CTE_SET_CONNLESS_TRANS_PARAMS_CMPL_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->set_trans_params_cmpl.status);
ESP_LOGI(LOG_TAG, "ESP_BLE_CTE_SET_CONNLESS_TRANS_PARAMS_CMPL_EVT, status %d", param->set_trans_params_cmpl.status);
break;
case ESP_BLE_CTE_SET_CONNLESS_TRANS_ENABLE_CMPL_EVT:
xSemaphoreGive(test_sem);
ble_async_complete_signal(param->set_trans_enable_cmpl.status);
ESP_LOGI(LOG_TAG, "ESP_BLE_CTE_SET_CONNLESS_TRANS_ENABLE_CMPL_EVT, status %d", param->set_trans_enable_cmpl.status);
break;
case ESP_BLE_CTE_SET_CONNLESS_IQ_SAMPLING_ENABLE_CMPL_EVT:
@@ -24,3 +24,5 @@ CONFIG_BT_BLE_50_PERIODIC_ADV_EN=y
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -37,15 +37,16 @@
#include "esp_ble_cte_api.h"
#define LOG_TAG "PERIODIC_SYNC"
#define FUNC_SEND_WAIT_SEM(func, sem) do {\
esp_err_t __err_rc = (func);\
if (__err_rc != ESP_OK) { \
ESP_LOGE(LOG_TAG, "%s, message send fail, error = %d", __func__, __err_rc); \
ESP_LOGE(LOG_TAG, "%s failed: %s", #func, esp_err_to_name(__err_rc)); \
return; \
} \
xSemaphoreTake(sem, portMAX_DELAY); \
xSemaphoreTake((sem), portMAX_DELAY); \
} while(0);
#define LOG_TAG "PERIODIC_SYNC"
#define EXT_SCAN_DURATION 0
#define EXT_SCAN_PERIOD 0
@@ -136,13 +137,16 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
param->ext_adv_report.params.adv_data_len,
ESP_BLE_AD_TYPE_NAME_CMPL,
&adv_name_len);
if ((adv_name != NULL) && (memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
const size_t remote_cap = sizeof(remote_device_name);
size_t remote_len = strnlen(remote_device_name, remote_cap);
if ((adv_name != NULL) && (adv_name_len > 0) &&
(adv_name_len <= remote_cap) &&
(adv_name_len == remote_len) &&
(memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
// Note: If there are multiple devices with the same device name, the device may sync to an unintended one.
// It is recommended to change the default device name to ensure it is unique.
periodic_sync = true;
char adv_temp_name[30] = {'0'};
memcpy(adv_temp_name, adv_name, adv_name_len);
ESP_LOGI(LOG_TAG, "Create sync with the peer device %s", adv_temp_name);
ESP_LOGI(LOG_TAG, "Create sync with the peer device %.*s", (int)adv_name_len, (const char *)adv_name);
periodic_adv_sync_params.sid = param->ext_adv_report.params.sid;
periodic_adv_sync_params.addr_type = param->ext_adv_report.params.addr_type;
memcpy(periodic_adv_sync_params.addr, param->ext_adv_report.params.addr, sizeof(esp_bd_addr_t));
@@ -22,3 +22,5 @@ CONFIG_BT_BLE_50_EXTEND_ADV_EN=n
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=y
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2025-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -180,12 +180,12 @@ static void gattc_profile_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
}
break;
case ESP_GATTC_CONNECT_EVT:
ESP_LOGI(TAG, "Connected, conn_id %d, remote "ESP_BD_ADDR_STR"",
p_data->connect.conn_id, ESP_BD_ADDR_HEX(p_data->connect.remote_bda));
ESP_LOGI(TAG, "Connected, conn_id %d, hci_conn_handle %d, remote "ESP_BD_ADDR_STR"",
p_data->connect.conn_id, p_data->connect.conn_handle,
ESP_BD_ADDR_HEX(p_data->connect.remote_bda));
gl_profile_tab[PROFILE_A_APP_ID].conn_id = p_data->connect.conn_id;
memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, p_data->connect.remote_bda, sizeof(esp_bd_addr_t));
conn_handle = p_data->connect.conn_id;
// Initialize power control after connection
conn_handle = p_data->connect.conn_handle;
init_power_control(conn_handle);
break;
case ESP_GATTC_DISCONNECT_EVT:
@@ -29,3 +29,5 @@ CONFIG_BT_BLE_50_EXTEND_ADV_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -30,3 +30,5 @@ CONFIG_BT_BLE_50_EXTEND_SCAN_EN=y
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_BT_BLE_50_DTM_TEST_EN=n
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
@@ -2,4 +2,20 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n
# Multiple extended advertisers only
CONFIG_BT_LE_MAX_EXT_ADV_INSTANCES=4
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_BT_BLE_50_EXTEND_ADV_EN=y
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
CONFIG_BT_BLE_50_PERIODIC_ADV_EN=n
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2025 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -40,9 +40,10 @@
#define FUNC_SEND_WAIT_SEM(func, sem) do {\
esp_err_t __err_rc = (func);\
if (__err_rc != ESP_OK) { \
ESP_LOGE(LOG_TAG, "%s, message send fail, error = %d", __func__, __err_rc); \
ESP_LOGE(LOG_TAG, "%s failed: %s", #func, esp_err_to_name(__err_rc)); \
return; \
} \
xSemaphoreTake(sem, portMAX_DELAY); \
xSemaphoreTake((sem), portMAX_DELAY); \
} while(0);
#define EXT_ADV_HANDLE 0
@@ -95,43 +96,50 @@ static esp_ble_gap_ext_adv_t ext_adv[1] = {
[0] = {EXT_ADV_HANDLE, 0, 0},
};
static void periodic_adv_gap_sem_give(void)
{
if (test_sem != NULL) {
xSemaphoreGive(test_sem);
}
}
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param)
{
switch (event) {
case ESP_GAP_BLE_EXT_ADV_SET_RAND_ADDR_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising random address set, status %d, instance %d", param->ext_adv_set_rand_addr.status, param->ext_adv_set_rand_addr.instance);
break;
case ESP_GAP_BLE_EXT_ADV_SET_PARAMS_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising params set, status %d, instance %d", param->ext_adv_set_params.status, param->ext_adv_set_params.instance);
break;
case ESP_GAP_BLE_EXT_ADV_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising data set, status %d, instance %d", param->ext_adv_data_set.status, param->ext_adv_data_set.instance);
break;
case ESP_GAP_BLE_EXT_SCAN_RSP_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising scan response data set, status %d, instance %d", param->scan_rsp_set.status, param->scan_rsp_set.instance);
break;
case ESP_GAP_BLE_EXT_ADV_START_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ESP_LOGI(LOG_TAG, "Extended advertising start, status %d, instance numble %d", param->ext_adv_start.status, param->ext_adv_start.instance_num);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising start, status %d, instance number %d", param->ext_adv_start.status, param->ext_adv_start.instance_num);
break;
case ESP_GAP_BLE_EXT_ADV_STOP_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ESP_LOGI(LOG_TAG, "Extended advertising start, status %d, instance numble %d", param->ext_adv_stop.status, param->ext_adv_stop.instance_num);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Extended advertising stop, status %d, instance number %d", param->ext_adv_stop.status, param->ext_adv_stop.instance_num);
break;
case ESP_GAP_BLE_PERIODIC_ADV_SET_PARAMS_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Periodic advertising params set, status %d, instance %d", param->peroid_adv_set_params.status, param->peroid_adv_set_params.instance);
break;
case ESP_GAP_BLE_PERIODIC_ADV_DATA_SET_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Periodic advertising data set, status %d, instance %d", param->period_adv_data_set.status, param->period_adv_data_set.instance);
break;
case ESP_GAP_BLE_PERIODIC_ADV_START_COMPLETE_EVT:
xSemaphoreGive(test_sem);
periodic_adv_gap_sem_give();
ESP_LOGI(LOG_TAG, "Periodic advertising start, status %d, instance %d", param->period_adv_start.status, param->period_adv_start.instance);
break;
default:
@@ -185,9 +193,18 @@ void app_main(void)
ESP_LOGE(LOG_TAG, "%s enable bluetooth failed: %s", __func__, esp_err_to_name(ret));
return;
}
test_sem = xSemaphoreCreateBinary();
if (test_sem == NULL) {
ESP_LOGE(LOG_TAG, "Failed to create semaphore");
return;
}
ret = esp_ble_gap_register_callback(gap_event_handler);
if (ret){
ESP_LOGE(LOG_TAG, "gap register error, error code = %x", ret);
vSemaphoreDelete(test_sem);
test_sem = NULL;
return;
}
@@ -195,8 +212,6 @@ void app_main(void)
esp_bd_addr_t rand_addr;
esp_ble_gap_addr_create_static(rand_addr);
test_sem = xSemaphoreCreateBinary();
// 2M phy extend adv, Non-Connectable and Non-Scannable Undirected advertising
ESP_LOG_BUFFER_HEX(LOG_TAG, rand_addr, ESP_BD_ADDR_LEN);
FUNC_SEND_WAIT_SEM(esp_ble_gap_ext_adv_set_params(EXT_ADV_HANDLE, &ext_adv_params_2M), test_sem);
@@ -2,3 +2,19 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n
# Periodic extended advertiser only (no scan / sync / GATT)
CONFIG_BT_BLE_50_EXTEND_ADV_EN=y
CONFIG_BT_BLE_50_PERIODIC_ADV_EN=y
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=n
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=n
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2021-2023 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2021-2026 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -84,10 +84,13 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
break;
case ESP_GAP_BLE_EXT_SCAN_STOP_COMPLETE_EVT:
xSemaphoreGive(test_sem);
ESP_LOGI(LOG_TAG, "Extended scanning stop, status %d", param->period_adv_stop.status);
ESP_LOGI(LOG_TAG, "Extended scanning stop, status %d", param->ext_scan_stop.status);
break;
case ESP_GAP_BLE_PERIODIC_ADV_CREATE_SYNC_COMPLETE_EVT:
ESP_LOGI(LOG_TAG, "Periodic advertising create sync, status %d", param->period_adv_create_sync.status);
if (param->period_adv_create_sync.status != ESP_BT_STATUS_SUCCESS) {
periodic_sync = false;
}
break;
case ESP_GAP_BLE_PERIODIC_ADV_SYNC_CANCEL_COMPLETE_EVT:
ESP_LOGI(LOG_TAG, "Periodic advertising sync cancel, status %d", param->period_adv_sync_cancel.status);
@@ -97,9 +100,13 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
break;
case ESP_GAP_BLE_PERIODIC_ADV_SYNC_LOST_EVT:
ESP_LOGI(LOG_TAG, "Periodic advertising sync lost, sync handle %d", param->periodic_adv_sync_lost.sync_handle);
periodic_sync = false;
break;
case ESP_GAP_BLE_PERIODIC_ADV_SYNC_ESTAB_EVT:
ESP_LOGI(LOG_TAG, "Periodic advertising sync establish, status %d", param->periodic_adv_sync_estab.status);
if (param->periodic_adv_sync_estab.status != ESP_BT_STATUS_SUCCESS) {
periodic_sync = false;
}
ESP_LOGI(LOG_TAG, "address "ESP_BD_ADDR_STR"", ESP_BD_ADDR_HEX(param->periodic_adv_sync_estab.adv_addr));
ESP_LOGI(LOG_TAG, "sync handle %d sid %d perioic adv interval %d adv phy %d", param->periodic_adv_sync_estab.sync_handle,
param->periodic_adv_sync_estab.sid,
@@ -113,13 +120,16 @@ static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param
param->ext_adv_report.params.adv_data_len,
ESP_BLE_AD_TYPE_NAME_CMPL,
&adv_name_len);
if ((adv_name != NULL) && (memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
const size_t remote_cap = sizeof(remote_device_name);
size_t remote_len = strnlen(remote_device_name, remote_cap);
if ((adv_name != NULL) && (adv_name_len > 0) &&
(adv_name_len <= remote_cap) &&
(adv_name_len == remote_len) &&
(memcmp(adv_name, remote_device_name, adv_name_len) == 0) && !periodic_sync) {
// Note: If there are multiple devices with the same device name, the device may sync to an unintended one.
// It is recommended to change the default device name to ensure it is unique.
periodic_sync = true;
char adv_temp_name[30] = {'0'};
memcpy(adv_temp_name, adv_name, adv_name_len);
ESP_LOGI(LOG_TAG, "Create sync with the peer device %s", adv_temp_name);
ESP_LOGI(LOG_TAG, "Create sync with the peer device %.*s", (int)adv_name_len, (const char *)adv_name);
periodic_adv_sync_params.sid = param->ext_adv_report.params.sid;
periodic_adv_sync_params.addr_type = param->ext_adv_report.params.addr_type;
memcpy(periodic_adv_sync_params.addr, param->ext_adv_report.params.addr, sizeof(esp_bd_addr_t));
@@ -2,3 +2,19 @@
# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration
#
CONFIG_BT_ENABLED=y
CONFIG_PARTITION_TABLE_SINGLE_APP_LARGE=y
CONFIG_BT_BLUEDROID_ENABLED=y
CONFIG_BT_BLE_ENABLED=y
CONFIG_BT_BLE_50_FEATURES_SUPPORTED=y
CONFIG_BT_BLE_42_FEATURES_SUPPORTED=n
# Periodic advertising sync role: extended scan + sync only (no extended adv / no periodic adv in host)
CONFIG_BT_BLE_50_EXTEND_SCAN_EN=y
CONFIG_BT_BLE_50_EXTEND_SYNC_EN=y
# CONFIG_BT_BLE_50_EXTEND_ADV_EN is not set
# CONFIG_BT_BLE_50_PERIODIC_ADV_EN is not set
# CONFIG_BT_GATTS_ENABLE is not set
# CONFIG_BT_GATTC_ENABLE is not set
# CONFIG_BT_BLE_SMP_ENABLE is not set
# CONFIG_BT_BLE_50_DTM_TEST_EN is not set
@@ -127,7 +127,7 @@ bool bt_app_work_dispatch(bt_app_cb_t p_cback, uint16_t event, void *p_params, i
void bt_app_task_start_up(void)
{
s_bt_app_task_queue = xQueueCreate(10, sizeof(bt_app_msg_t));
xTaskCreate(bt_app_task_handler, "BtAppTask", 3072, NULL, 10, &s_bt_app_task_handle);
xTaskCreate(bt_app_task_handler, "BtAppTask", 4096, NULL, 10, &s_bt_app_task_handle);
}
void bt_app_task_shut_down(void)

Some files were not shown because too many files have changed in this diff Show More