From a8bb79b5b97759ddc1b1f23de5504ec580023e34 Mon Sep 17 00:00:00 2001 From: jiminxiang Date: Mon, 6 Jul 2026 11:05:45 +0800 Subject: [PATCH] feat(examples): add Bluedroid and NimBLE BLE AMS client examples Add Apple Media Service client demos that discover AMS on iOS, subscribe to media entity updates, and expose remote playback control helpers. --- docs/en/api-reference/bluetooth/esp_gattc.rst | 4 + .../bluedroid/ble/ble_ams/CMakeLists.txt | 8 + .../bluetooth/bluedroid/ble/ble_ams/README.md | 122 +++ .../bluedroid/ble/ble_ams/main/CMakeLists.txt | 3 + .../bluedroid/ble/ble_ams/main/ble_ams.c | 236 +++++ .../bluedroid/ble/ble_ams/main/ble_ams.h | 89 ++ .../bluedroid/ble/ble_ams/main/ble_ams_demo.c | 855 ++++++++++++++++ .../bluedroid/ble/ble_ams/sdkconfig.defaults | 14 + .../ble/ble_ams/sdkconfig.defaults.esp32c2 | 9 + .../ble/ble_ams/sdkconfig.defaults.esp32c3 | 7 + .../ble/ble_ams/sdkconfig.defaults.esp32s3 | 7 + .../bluetooth/nimble/ble_ams/CMakeLists.txt | 8 + examples/bluetooth/nimble/ble_ams/README.md | 123 +++ .../nimble/ble_ams/main/CMakeLists.txt | 5 + .../nimble/ble_ams/main/Kconfig.projbuild | 14 + .../bluetooth/nimble/ble_ams/main/ble_ams.c | 222 +++++ .../bluetooth/nimble/ble_ams/main/ble_ams.h | 75 ++ .../nimble/ble_ams/main/idf_component.yml | 3 + examples/bluetooth/nimble/ble_ams/main/main.c | 924 ++++++++++++++++++ .../nimble/ble_ams/sdkconfig.defaults | 14 + 20 files changed, 2742 insertions(+) create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/README.md create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/main/CMakeLists.txt create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.h create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams_demo.c create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c2 create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c3 create mode 100644 examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32s3 create mode 100644 examples/bluetooth/nimble/ble_ams/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_ams/README.md create mode 100644 examples/bluetooth/nimble/ble_ams/main/CMakeLists.txt create mode 100644 examples/bluetooth/nimble/ble_ams/main/Kconfig.projbuild create mode 100644 examples/bluetooth/nimble/ble_ams/main/ble_ams.c create mode 100644 examples/bluetooth/nimble/ble_ams/main/ble_ams.h create mode 100644 examples/bluetooth/nimble/ble_ams/main/idf_component.yml create mode 100644 examples/bluetooth/nimble/ble_ams/main/main.c create mode 100644 examples/bluetooth/nimble/ble_ams/sdkconfig.defaults diff --git a/docs/en/api-reference/bluetooth/esp_gattc.rst b/docs/en/api-reference/bluetooth/esp_gattc.rst index 5454148e2e8..f3cc049140c 100644 --- a/docs/en/api-reference/bluetooth/esp_gattc.rst +++ b/docs/en/api-reference/bluetooth/esp_gattc.rst @@ -10,6 +10,10 @@ Application Examples - :example:`bluetooth/bluedroid/coex/gattc_gatts_coex` demonstrates the coexistence of GATT client and GATT server by creating a GATT service, starting ADV, and exchanging data between a GATT client and device. +- :example:`bluetooth/bluedroid/ble/ble_ams` demonstrates how to act as an Apple Media Service (AMS) client on the Bluedroid stack, connecting to an iOS device to receive media metadata and send remote playback commands over GATT. + +- :example:`bluetooth/nimble/ble_ams` demonstrates how to act as an Apple Media Service (AMS) client on the NimBLE stack, connecting to an iOS device to receive media metadata and send remote playback commands over GATT. + API Reference ------------- diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_ams/CMakeLists.txt new file mode 100644 index 00000000000..24bb2d0bb68 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(ble_ams) diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/README.md b/examples/bluetooth/bluedroid/ble/ble_ams/README.md new file mode 100644 index 00000000000..b8c513609fe --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/README.md @@ -0,0 +1,122 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- | + +# ESP-IDF BLE AMS Example + +The purpose of the Apple Media Service (AMS) is to give Bluetooth accessories (that connect to iOS devices through a Bluetooth low-energy link) a simple and convenient way to control media apps and access information about the media state of the connected iOS device. + +## Flow Diagram + +``` + ┌──────────────┐ ┌──────────────┐ + │ ESP32 │ │ iOS Device │ + │ (AMS Client) │ │ (AMS Server) │ + └──────┬───────┘ └──────┬───────┘ + │ │ + │ ─────────── Connection ─────────── │ + │ │ + │ 1. Start Advertising │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 2. iOS connects & pairs │ + │ <─────────────────────────────────────────────── │ + │ │ + │ ─────────── Service Discovery ─────────── │ + │ │ + │ 3. Discover AMS Service │ + │ (UUID: 89D3502B-0F36-433A-8EF4-C502AD55F8DC) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 4. Subscribe to Remote Command │ + │ (UUID: 9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 5. Subscribe to Entity Update │ + │ (UUID: 2F7CABCE-808D-411F-9A0C-BB92BA96C102) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 6. Register entity/attribute pairs │ + │ (Write to Entity Update) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ ─────────── Media Update Flow ─────────── │ + │ │ + │ 7. Media state changes on iOS │ + │ (Track, Player, Queue updates) │ + │ <─────────────────────────────────────────────── │ + │ │ + │ 8. Optional: read full truncated value │ + │ (Write + Read Entity Attribute) │ + │ ───────────────────────────────────────────────> │ + │ │ + ┌──────┴───────┐ ┌──────┴───────┐ + │ ESP32 │ │ iOS Device │ + └──────────────┘ └──────────────┘ +``` + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using: + +```bash +idf.py set-target +``` + +The Apple Media Service is a primary service whose service UUID is: + +`89D3502B-0F36-433A-8EF4-C502AD55F8DC` + +Only one instance of the AMS may be present on an MS. Due to the nature of iOS, the AMS is not guaranteed to always be present. As a result, the MR should look for and subscribe to the Service Changed characteristic of the GATT service in order to monitor for the potential publishing and unpublishing of the AMS at any time. + +In its basic form, the AMS exposes three characteristics: + +* Remote Command: UUID `9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2` (writeable, notifiable) +* Entity Update: UUID `2F7CABCE-808D-411F-9A0C-BB92BA96C102` (writeable with response, notifiable) +* Entity Attribute: UUID `C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7` (readable, writeable) + +After pairing, start playing music on the iOS device. The example subscribes to track, player, and queue entity updates and prints media metadata to the console. + +The example uses legacy connectable advertising (`ESP_BLE_AMS`). + +Advertising data matches `ble_ancs`: HID appearance, 16-bit service UUID `0x1812`, and device name in the scan response. + +To send a remote command from application code, call `esp_send_remote_command()` with a value from `esp_remote_command_id_t`, for example `RemoteCommandIDTogglePlayPause`. + +### Hardware Required + +* A development board with ESP32/ESP32-C3/ESP32-H2/ESP32-C2/ESP32-S3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) +* A USB cable for power supply and programming + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (918) BLE_AMS: app_main init bluetooth +I (1018) BLE_AMS: REG_EVT +I (1058) BLE_AMS: advertising start success +I (5234) BLE_AMS: ESP_GATTC_OPEN_EVT +I (5456) BLE_AMS: ESP_GATTC_CFG_MTU_EVT, Status 0, MTU 517, conn_id 0 +I (5678) BLE_AMS: Found Remote Command characteristic +I (5678) BLE_AMS: Found Entity Update characteristic +I (5680) BLE_AMS: Found Entity Attribute characteristic +I (5720) BLE_AMS: Entity Update notifications enabled +I (5721) BLE_AMS: Subscribe track entity updates +I (5800) BLE_AMS: Supported remote commands (6): +I (5801) BLE_AMS: [0] Play +I (5802) BLE_AMS: [1] Pause +I (5900) BLE_AMS: Entity update: Track / Title, flags=0x00, len=12 +I (5901) BLE_AMS: Value: Example Song +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/main/CMakeLists.txt b/examples/bluetooth/bluedroid/ble/ble_ams/main/CMakeLists.txt new file mode 100644 index 00000000000..7749b1c3f88 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRCS "ble_ams_demo.c" "ble_ams.c" + PRIV_REQUIRES bt nvs_flash + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.c b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.c new file mode 100644 index 00000000000..5e9b16f5f32 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.c @@ -0,0 +1,236 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "esp_log.h" +#include "ble_ams.h" + +#define BLE_AMS_TAG "BLE_AMS" + +const char *RemoteCommandID_to_String(uint8_t command_id) +{ + switch (command_id) { + case RemoteCommandIDPlay: + return "Play"; + case RemoteCommandIDPause: + return "Pause"; + case RemoteCommandIDTogglePlayPause: + return "TogglePlayPause"; + case RemoteCommandIDNextTrack: + return "NextTrack"; + case RemoteCommandIDPreviousTrack: + return "PreviousTrack"; + case RemoteCommandIDVolumeUp: + return "VolumeUp"; + case RemoteCommandIDVolumeDown: + return "VolumeDown"; + case RemoteCommandIDAdvanceRepeatMode: + return "AdvanceRepeatMode"; + case RemoteCommandIDAdvanceShuffleMode: + return "AdvanceShuffleMode"; + case RemoteCommandIDSkipForward: + return "SkipForward"; + case RemoteCommandIDSkipBackward: + return "SkipBackward"; + case RemoteCommandIDLikeTrack: + return "LikeTrack"; + case RemoteCommandIDDislikeTrack: + return "DislikeTrack"; + case RemoteCommandIDBookmarkTrack: + return "BookmarkTrack"; + default: + return "Unknown RemoteCommandID"; + } +} + +const char *EntityID_to_String(uint8_t entity_id) +{ + switch (entity_id) { + case EntityIDPlayer: + return "Player"; + case EntityIDQueue: + return "Queue"; + case EntityIDTrack: + return "Track"; + default: + return "Unknown EntityID"; + } +} + +const char *AttributeID_to_String(uint8_t entity_id, uint8_t attribute_id) +{ + switch (entity_id) { + case EntityIDPlayer: + switch (attribute_id) { + case PlayerAttributeIDName: + return "PlayerName"; + case PlayerAttributeIDPlaybackInfo: + return "PlaybackInfo"; + case PlayerAttributeIDVolume: + return "Volume"; + default: + return "Unknown PlayerAttributeID"; + } + case EntityIDQueue: + switch (attribute_id) { + case QueueAttributeIDIndex: + return "QueueIndex"; + case QueueAttributeIDCount: + return "QueueCount"; + case QueueAttributeIDShuffleMode: + return "ShuffleMode"; + case QueueAttributeIDRepeatMode: + return "RepeatMode"; + default: + return "Unknown QueueAttributeID"; + } + case EntityIDTrack: + switch (attribute_id) { + case TrackAttributeIDArtist: + return "Artist"; + case TrackAttributeIDAlbum: + return "Album"; + case TrackAttributeIDTitle: + return "Title"; + case TrackAttributeIDDuration: + return "Duration"; + default: + return "Unknown TrackAttributeID"; + } + default: + return "Unknown AttributeID"; + } +} + +const char *PlaybackState_to_String(uint8_t playback_state) +{ + switch (playback_state) { + case PlaybackStatePaused: + return "Paused"; + case PlaybackStatePlaying: + return "Playing"; + case PlaybackStateRewinding: + return "Rewinding"; + case PlaybackStateFastForwarding: + return "FastForwarding"; + default: + return "Unknown PlaybackState"; + } +} + +static void log_playback_info(const uint8_t *value, uint16_t value_len) +{ + if (!value || value_len == 0) { + return; + } + + char buffer[256]; + uint16_t copy_len = value_len < (sizeof(buffer) - 1) ? value_len : (sizeof(buffer) - 1); + memcpy(buffer, value, copy_len); + buffer[copy_len] = '\0'; + + char *state = buffer; + char *rate = NULL; + char *elapsed = NULL; + char *separator = strchr(state, ','); + if (separator) { + *separator = '\0'; + rate = separator + 1; + separator = strchr(rate, ','); + if (separator) { + *separator = '\0'; + elapsed = separator + 1; + } + } + + if (state[0] != '\0') { + int playback_state = atoi(state); + ESP_LOGI(BLE_AMS_TAG, " PlaybackState: %s", PlaybackState_to_String((uint8_t)playback_state)); + } + if (rate && rate[0] != '\0') { + ESP_LOGI(BLE_AMS_TAG, " PlaybackRate: %s", rate); + } + if (elapsed && elapsed[0] != '\0') { + ESP_LOGI(BLE_AMS_TAG, " ElapsedTime: %s s", elapsed); + } +} + +void esp_receive_remote_command_notification(uint8_t *message, uint16_t message_len) +{ + if (!message || message_len == 0) { + return; + } + + ESP_LOGI(BLE_AMS_TAG, "Supported remote commands (%u):", message_len); + for (uint16_t i = 0; i < message_len; i++) { + ESP_LOGI(BLE_AMS_TAG, " [%u] %s", i, RemoteCommandID_to_String(message[i])); + } +} + +void esp_receive_entity_update(uint8_t *message, uint16_t message_len) +{ + if (!message || message_len < 3) { + return; + } + + uint8_t entity_id = message[0]; + uint8_t attribute_id = message[1]; + uint8_t flags = message[2]; + uint8_t *value = &message[3]; + uint16_t value_len = message_len - 3; + + ESP_LOGI(BLE_AMS_TAG, "Entity update: %s / %s, flags=0x%02x, len=%u", + EntityID_to_String(entity_id), + AttributeID_to_String(entity_id, attribute_id), + flags, + value_len); + + if (entity_id == EntityIDPlayer && attribute_id == PlayerAttributeIDPlaybackInfo) { + log_playback_info(value, value_len); + return; + } + + if (value_len > 0) { + ESP_LOG_BUFFER_CHAR("Value", value, value_len); + } + + if (flags & EntityUpdateFlagTruncated) { + ESP_LOGI(BLE_AMS_TAG, "Value truncated; read full value from Entity Attribute characteristic"); + } +} + +void esp_receive_entity_attribute_value(uint8_t entity_id, uint8_t attribute_id, + uint8_t *message, uint16_t message_len) +{ + ESP_LOGI(BLE_AMS_TAG, "Full entity attribute: %s / %s, len=%u", + EntityID_to_String(entity_id), + AttributeID_to_String(entity_id, attribute_id), + message_len); + + if (entity_id == EntityIDPlayer && attribute_id == PlayerAttributeIDPlaybackInfo) { + log_playback_info(message, message_len); + return; + } + + if (message && message_len > 0) { + ESP_LOG_BUFFER_CHAR("Value", message, message_len); + } +} + +const char *Errcode_to_String(uint16_t status) +{ + switch (status) { + case Invalid_state: + return "Invalid_state"; + case Invalid_command: + return "Invalid_command"; + case Absent_attribute: + return "Absent_attribute"; + default: + return "unknown_failed"; + } +} diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.h b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.h new file mode 100644 index 00000000000..f1383bc11fc --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams.h @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#include +#include "esp_err.h" + +// RemoteCommandID values (Table A-1) +typedef enum { + RemoteCommandIDPlay = 0, + RemoteCommandIDPause = 1, + RemoteCommandIDTogglePlayPause = 2, + RemoteCommandIDNextTrack = 3, + RemoteCommandIDPreviousTrack = 4, + RemoteCommandIDVolumeUp = 5, + RemoteCommandIDVolumeDown = 6, + RemoteCommandIDAdvanceRepeatMode = 7, + RemoteCommandIDAdvanceShuffleMode = 8, + RemoteCommandIDSkipForward = 9, + RemoteCommandIDSkipBackward = 10, + RemoteCommandIDLikeTrack = 11, + RemoteCommandIDDislikeTrack = 12, + RemoteCommandIDBookmarkTrack = 13, +} esp_remote_command_id_t; + +// EntityID values (Table A-2) +typedef enum { + EntityIDPlayer = 0, + EntityIDQueue = 1, + EntityIDTrack = 2, +} esp_entity_id_t; + +// EntityUpdateFlags (Table A-3) +typedef enum { + EntityUpdateFlagTruncated = (1 << 0), +} esp_entity_update_flag_t; + +// PlayerAttributeID values (Table A-4) +typedef enum { + PlayerAttributeIDName = 0, + PlayerAttributeIDPlaybackInfo = 1, + PlayerAttributeIDVolume = 2, +} esp_player_attribute_id_t; + +// QueueAttributeID values (Table A-5) +typedef enum { + QueueAttributeIDIndex = 0, + QueueAttributeIDCount = 1, + QueueAttributeIDShuffleMode = 2, + QueueAttributeIDRepeatMode = 3, +} esp_queue_attribute_id_t; + +// TrackAttributeID values (Table A-8) +typedef enum { + TrackAttributeIDArtist = 0, + TrackAttributeIDAlbum = 1, + TrackAttributeIDTitle = 2, + TrackAttributeIDDuration = 3, +} esp_track_attribute_id_t; + +typedef enum { + PlaybackStatePaused = 0, + PlaybackStatePlaying = 1, + PlaybackStateRewinding = 2, + PlaybackStateFastForwarding = 3, +} esp_playback_state_t; + +typedef enum { + Invalid_state = 0xA0, + Invalid_command = 0xA1, + Absent_attribute = 0xA2, +} esp_ams_error_code_t; + +const char *RemoteCommandID_to_String(uint8_t command_id); +const char *EntityID_to_String(uint8_t entity_id); +const char *AttributeID_to_String(uint8_t entity_id, uint8_t attribute_id); +const char *PlaybackState_to_String(uint8_t playback_state); +const char *Errcode_to_String(uint16_t status); + +void esp_receive_remote_command_notification(uint8_t *message, uint16_t message_len); +void esp_receive_entity_update(uint8_t *message, uint16_t message_len); +void esp_receive_entity_attribute_value(uint8_t entity_id, uint8_t attribute_id, + uint8_t *message, uint16_t message_len); + +esp_err_t esp_send_remote_command(uint8_t command_id); diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams_demo.c b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams_demo.c new file mode 100644 index 00000000000..13a518b0bb2 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/main/ble_ams_demo.c @@ -0,0 +1,855 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "esp_log.h" +#include "esp_timer.h" +#include "nvs_flash.h" +#include "esp_bt.h" + +#include "esp_gap_ble_api.h" +#include "esp_bt_defs.h" +#include "esp_bt_main.h" +#include "esp_bt_device.h" +#include "esp_gattc_api.h" +#include "esp_gatt_defs.h" +#include "esp_gatt_common_api.h" +#include "ble_ams.h" + +#define BLE_AMS_DEMO_TAG "BLE_AMS" +#define EXAMPLE_DEVICE_NAME "ESP_BLE_AMS" +#define PROFILE_A_APP_ID 0 +#define PROFILE_NUM 1 +#define ADV_CONFIG_FLAG (1 << 0) +#define SCAN_RSP_CONFIG_FLAG (1 << 1) +#define INVALID_HANDLE 0 +#define AMS_ENTITY_UPDATE_CCCD_DELAY_US (500 * 1000) +#define ENTITY_ATTR_REQ_QUEUE_LEN 4 +#define ENTITY_UPDATE_SUB_CMD_MAX_LEN 8 +#define ENTITY_UPDATE_SUB_QUEUE_LEN 3 + +static uint8_t adv_config_done = 0; +static bool get_service = false; +static bool entity_update_notify_enabled = false; +static esp_gattc_char_elem_t *char_elem_result = NULL; +static uint16_t remote_command_cccd_handle = 0; +static uint16_t entity_update_cccd_handle = 0; +static uint16_t pending_cccd_char_handle = 0; +static esp_gatt_if_t pending_entity_update_gattc_if = ESP_GATT_IF_NONE; +static esp_timer_handle_t entity_update_cccd_delay_timer; + +static esp_bt_uuid_t cccd_uuid = { + .len = ESP_UUID_LEN_16, + .uuid = { .uuid16 = ESP_GATT_UUID_CHAR_CLIENT_CONFIG }, +}; + +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); +static void ams_schedule_entity_update_notify(esp_gatt_if_t gattc_if); + +// Apple Media Service UUID: 89D3502B-0F36-433A-8EF4-C502AD55F8DC +static uint8_t Apple_MS_UUID[16] = {0xDC, 0xF8, 0x55, 0xAD, 0x02, 0xC5, 0xF4, 0x8E, 0x3A, 0x43, 0x36, 0x0F, 0x2B, 0x50, 0xD3, 0x89}; +// Remote Command UUID: 9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2 (writeable, notifiable) +static uint8_t remote_command_uuid[16] = {0xC2, 0x51, 0xCA, 0xF7, 0x56, 0x0E, 0xDF, 0xB8, 0x8A, 0x4A, 0xB1, 0x57, 0xD8, 0x81, 0x3C, 0x9B}; +// Entity Update UUID: 2F7CABCE-808D-411F-9A0C-BB92BA96C102 (writeable with response, notifiable) +static uint8_t entity_update_uuid[16] = {0x02, 0xC1, 0x96, 0xBA, 0x92, 0xBB, 0x0C, 0x9A, 0x1F, 0x41, 0x8D, 0x80, 0xCE, 0xAB, 0x7C, 0x2F}; +// Entity Attribute UUID: C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7 (readable, writeable) +static uint8_t entity_attribute_uuid[16] = {0xD7, 0xD5, 0xBB, 0x70, 0xA8, 0xA3, 0xAB, 0xA6, 0xD8, 0x46, 0xAB, 0x23, 0x8C, 0xF3, 0xB2, 0xC6}; + +static esp_bt_uuid_t apple_ms_uuid = { + .len = ESP_UUID_LEN_128, +}; + +static uint8_t hidd_service_uuid128[] = { + 0xfb, 0x34, 0x9b, 0x5f, 0x80, 0x00, 0x00, 0x80, 0x00, 0x10, 0x00, 0x00, 0x12, 0x18, 0x00, 0x00, +}; + +static esp_ble_adv_data_t adv_config = { + .set_scan_rsp = false, + .include_txpower = false, + .min_interval = ESP_BLE_GAP_CONN_ITVL_MS(7.5), + .max_interval = ESP_BLE_GAP_CONN_ITVL_MS(20), + .appearance = ESP_BLE_APPEARANCE_GENERIC_HID, + .service_uuid_len = sizeof(hidd_service_uuid128), + .p_service_uuid = hidd_service_uuid128, + .flag = (ESP_BLE_ADV_FLAG_GEN_DISC | ESP_BLE_ADV_FLAG_BREDR_NOT_SPT), +}; + +static esp_ble_adv_data_t scan_rsp_config = { + .set_scan_rsp = true, + .include_name = true, +}; + +static esp_ble_adv_params_t adv_params = { + .adv_int_min = ESP_BLE_GAP_ADV_ITVL_MS(160), + .adv_int_max = ESP_BLE_GAP_ADV_ITVL_MS(160), + .adv_type = ADV_TYPE_IND, + .own_addr_type = BLE_ADDR_TYPE_PUBLIC, + .channel_map = ADV_CHNL_ALL, + .adv_filter_policy = ADV_FILTER_ALLOW_SCAN_ANY_CON_ANY, +}; + +struct gattc_profile_inst { + esp_gattc_cb_t gattc_cb; + uint16_t gattc_if; + uint16_t app_id; + uint16_t conn_id; + uint16_t service_start_handle; + uint16_t service_end_handle; + uint16_t remote_command_handle; + uint16_t entity_update_handle; + uint16_t entity_attribute_handle; + esp_bd_addr_t remote_bda; + uint16_t MTU_size; +}; + +static struct gattc_profile_inst gl_profile_tab[PROFILE_NUM] = { + [PROFILE_A_APP_ID] = { + .gattc_cb = gattc_profile_event_handler, + .gattc_if = ESP_GATT_IF_NONE, + }, +}; + +typedef struct { + uint8_t entity_id; + uint8_t attribute_id; +} entity_attr_req_t; + +typedef struct { + uint8_t cmd_len; + uint8_t cmd[ENTITY_UPDATE_SUB_CMD_MAX_LEN]; +} entity_update_sub_req_t; + +static uint8_t pending_entity_id = 0; +static uint8_t pending_attribute_id = 0; +static bool entity_attr_req_in_flight = false; +static entity_attr_req_t entity_attr_req_queue[ENTITY_ATTR_REQ_QUEUE_LEN]; +static uint8_t entity_attr_req_head = 0; +static uint8_t entity_attr_req_tail = 0; +static uint8_t entity_attr_req_count = 0; + +static bool entity_update_sub_in_flight = false; +static entity_update_sub_req_t entity_update_sub_queue[ENTITY_UPDATE_SUB_QUEUE_LEN]; +static uint8_t entity_update_sub_head = 0; +static uint8_t entity_update_sub_tail = 0; +static uint8_t entity_update_sub_count = 0; + +static void entity_attr_req_queue_reset(void) +{ + entity_attr_req_head = 0; + entity_attr_req_tail = 0; + entity_attr_req_count = 0; + entity_attr_req_in_flight = false; + pending_entity_id = 0; + pending_attribute_id = 0; +} + +static void entity_update_sub_queue_reset(void) +{ + entity_update_sub_head = 0; + entity_update_sub_tail = 0; + entity_update_sub_count = 0; + entity_update_sub_in_flight = false; +} + +static bool entity_attr_req_queue_push(uint8_t entity_id, uint8_t attribute_id) +{ + if (entity_attr_req_count >= ENTITY_ATTR_REQ_QUEUE_LEN) { + return false; + } + + entity_attr_req_queue[entity_attr_req_tail].entity_id = entity_id; + entity_attr_req_queue[entity_attr_req_tail].attribute_id = attribute_id; + entity_attr_req_tail = (entity_attr_req_tail + 1) % ENTITY_ATTR_REQ_QUEUE_LEN; + entity_attr_req_count++; + return true; +} + +static bool entity_attr_req_queue_pop(entity_attr_req_t *req) +{ + if (!req || entity_attr_req_count == 0) { + return false; + } + + *req = entity_attr_req_queue[entity_attr_req_head]; + entity_attr_req_head = (entity_attr_req_head + 1) % ENTITY_ATTR_REQ_QUEUE_LEN; + entity_attr_req_count--; + return true; +} + +static void esp_process_next_entity_attribute_request(void) +{ + entity_attr_req_t req; + uint8_t cmd[2]; + esp_err_t ret; + + if (gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle == 0) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Entity Attribute handle not discovered"); + return; + } + + if (entity_attr_req_in_flight || !entity_attr_req_queue_pop(&req)) { + return; + } + + pending_entity_id = req.entity_id; + pending_attribute_id = req.attribute_id; + entity_attr_req_in_flight = true; + cmd[0] = req.entity_id; + cmd[1] = req.attribute_id; + + ret = esp_ble_gattc_write_char(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle, + sizeof(cmd), + cmd, + ESP_GATT_WRITE_TYPE_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "write entity attribute request failed, error = %x", ret); + entity_attr_req_in_flight = false; + esp_process_next_entity_attribute_request(); + } +} + +static bool entity_update_sub_queue_push(const uint8_t *cmd, uint8_t cmd_len) +{ + if (entity_update_sub_count >= ENTITY_UPDATE_SUB_QUEUE_LEN || + cmd_len > ENTITY_UPDATE_SUB_CMD_MAX_LEN) { + return false; + } + + entity_update_sub_queue[entity_update_sub_tail].cmd_len = cmd_len; + memcpy(entity_update_sub_queue[entity_update_sub_tail].cmd, cmd, cmd_len); + entity_update_sub_tail = (entity_update_sub_tail + 1) % ENTITY_UPDATE_SUB_QUEUE_LEN; + entity_update_sub_count++; + return true; +} + +static bool entity_update_sub_queue_pop(entity_update_sub_req_t *req) +{ + if (!req || entity_update_sub_count == 0) { + return false; + } + + *req = entity_update_sub_queue[entity_update_sub_head]; + entity_update_sub_head = (entity_update_sub_head + 1) % ENTITY_UPDATE_SUB_QUEUE_LEN; + entity_update_sub_count--; + return true; +} + +static void esp_process_next_entity_update_subscribe(void) +{ + entity_update_sub_req_t req; + esp_err_t ret; + + if (entity_update_sub_in_flight || !entity_update_sub_queue_pop(&req)) { + return; + } + + entity_update_sub_in_flight = true; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Subscribe %s entity updates", EntityID_to_String(req.cmd[0])); + + ret = esp_ble_gattc_write_char(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle, + req.cmd_len, + req.cmd, + ESP_GATT_WRITE_TYPE_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "write entity update subscribe request failed, error = %x", ret); + entity_update_sub_in_flight = false; + esp_process_next_entity_update_subscribe(); + } +} + +static void esp_subscribe_entity_updates(void) +{ + const uint8_t track_cmd[] = { + EntityIDTrack, + TrackAttributeIDArtist, + TrackAttributeIDAlbum, + TrackAttributeIDTitle, + TrackAttributeIDDuration, + }; + const uint8_t player_cmd[] = { + EntityIDPlayer, + PlayerAttributeIDName, + PlayerAttributeIDPlaybackInfo, + PlayerAttributeIDVolume, + }; + const uint8_t queue_cmd[] = { + EntityIDQueue, + QueueAttributeIDIndex, + QueueAttributeIDCount, + QueueAttributeIDShuffleMode, + QueueAttributeIDRepeatMode, + }; + + entity_update_sub_queue_reset(); + + if (!entity_update_sub_queue_push(track_cmd, sizeof(track_cmd)) || + !entity_update_sub_queue_push(player_cmd, sizeof(player_cmd)) || + !entity_update_sub_queue_push(queue_cmd, sizeof(queue_cmd))) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Failed to queue entity update subscribe requests"); + return; + } + + esp_process_next_entity_update_subscribe(); +} + +esp_err_t esp_send_remote_command(uint8_t command_id) +{ + if (gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle == 0) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Remote Command handle not discovered"); + return ESP_ERR_INVALID_STATE; + } + + esp_err_t ret = esp_ble_gattc_write_char(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle, + 1, + &command_id, + ESP_GATT_WRITE_TYPE_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "write remote command failed, error = %x", ret); + } + return ret; +} + +static void esp_request_entity_attribute(uint8_t entity_id, uint8_t attribute_id) +{ + if (!entity_attr_req_queue_push(entity_id, attribute_id)) { + ESP_LOGW(BLE_AMS_DEMO_TAG, + "Entity Attribute request dropped (entity=%u attr=%u): queue full (max=%u)", + entity_id, + attribute_id, + ENTITY_ATTR_REQ_QUEUE_LEN); + return; + } + + esp_process_next_entity_attribute_request(); +} + +static bool ams_find_cccd_handle(esp_gatt_if_t gattc_if, uint16_t char_handle, + uint16_t *cccd_handle, const char *char_name) +{ + esp_gattc_descr_elem_t descr; + uint16_t count = 1; + + esp_gatt_status_t status = esp_ble_gattc_get_descr_by_char_handle(gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + char_handle, + cccd_uuid, + &descr, + &count); + if (status != ESP_GATT_OK || count == 0) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "%s CCCD not found (char 0x%04x, status 0x%x)", + char_name, char_handle, status); + return false; + } + + if (descr.uuid.len != ESP_UUID_LEN_16 || + descr.uuid.uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "%s unexpected descriptor UUID (char 0x%04x)", char_name, char_handle); + return false; + } + + *cccd_handle = descr.handle; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Found %s CCCD handle 0x%04x (char 0x%04x)", + char_name, *cccd_handle, char_handle); + return true; +} + +static void ams_write_cccd(esp_gatt_if_t gattc_if, uint16_t cccd_handle, uint16_t char_handle) +{ + uint8_t notify_en[2] = {0x01, 0x00}; + esp_err_t ret; + + pending_cccd_char_handle = char_handle; + ret = esp_ble_gattc_write_char_descr(gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + cccd_handle, + sizeof(notify_en), + notify_en, + ESP_GATT_WRITE_TYPE_RSP, + ESP_GATT_AUTH_REQ_NONE); + if (ret != ESP_OK) { + pending_cccd_char_handle = 0; + ESP_LOGE(BLE_AMS_DEMO_TAG, "write cccd failed, error = %x", ret); + if (char_handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Remote Command CCCD write failed, continue Entity Update subscribe"); + ams_schedule_entity_update_notify(gattc_if); + } + } +} + +static void ams_start_remote_command_notify(esp_gatt_if_t gattc_if) +{ + struct gattc_profile_inst *profile = &gl_profile_tab[PROFILE_A_APP_ID]; + esp_err_t ret; + + if (profile->remote_command_handle == 0) { + return; + } + + if (!ams_find_cccd_handle(gattc_if, profile->remote_command_handle, + &remote_command_cccd_handle, "Remote Command")) { + ams_schedule_entity_update_notify(gattc_if); + return; + } + + ret = esp_ble_gattc_register_for_notify(gattc_if, profile->remote_bda, profile->remote_command_handle); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "register Remote Command notify failed, error = %x", ret); + ams_schedule_entity_update_notify(gattc_if); + } +} + +static void ams_start_entity_update_notify(esp_gatt_if_t gattc_if) +{ + struct gattc_profile_inst *profile = &gl_profile_tab[PROFILE_A_APP_ID]; + esp_err_t ret; + + if (profile->entity_update_handle == 0) { + return; + } + + if (!ams_find_cccd_handle(gattc_if, profile->entity_update_handle, + &entity_update_cccd_handle, "Entity Update")) { + return; + } + + ret = esp_ble_gattc_register_for_notify(gattc_if, profile->remote_bda, profile->entity_update_handle); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "register Entity Update notify failed, error = %x", ret); + } +} + +static void ams_entity_update_cccd_delay_timer_cb(void *arg) +{ + esp_gatt_if_t gattc_if = pending_entity_update_gattc_if; + + pending_entity_update_gattc_if = ESP_GATT_IF_NONE; + if (gattc_if == ESP_GATT_IF_NONE) { + return; + } + + ESP_LOGI(BLE_AMS_DEMO_TAG, "Starting Entity Update CCCD subscribe after delay"); + ams_start_entity_update_notify(gattc_if); +} + +static void ams_schedule_entity_update_notify(esp_gatt_if_t gattc_if) +{ + pending_entity_update_gattc_if = gattc_if; + esp_timer_stop(entity_update_cccd_delay_timer); + esp_timer_start_once(entity_update_cccd_delay_timer, AMS_ENTITY_UPDATE_CCCD_DELAY_US); +} + +static void ble_ams_start_adv_config(void) +{ + esp_err_t ret = esp_ble_gap_config_adv_data(&adv_config); + if (ret) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "config adv data failed, error code = %x", ret); + } else { + adv_config_done |= ADV_CONFIG_FLAG; + } + + ret = esp_ble_gap_config_adv_data(&scan_rsp_config); + if (ret) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "config scan rsp data failed, error code = %x", ret); + } else { + adv_config_done |= SCAN_RSP_CONFIG_FLAG; + } +} + +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_SCAN_RSP_DATA_SET_COMPLETE_EVT: + adv_config_done &= (~SCAN_RSP_CONFIG_FLAG); + if (adv_config_done == 0) { + esp_ble_gap_start_advertising(&adv_params); + } + break; + case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: + adv_config_done &= (~ADV_CONFIG_FLAG); + if (adv_config_done == 0) { + esp_ble_gap_start_advertising(&adv_params); + } + break; + case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: + if (param->adv_start_cmpl.status != ESP_BT_STATUS_SUCCESS) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "advertising start failed, error status = %x", param->adv_start_cmpl.status); + break; + } + ESP_LOGI(BLE_AMS_DEMO_TAG, "advertising start success"); + break; + case ESP_GAP_BLE_OOB_REQ_EVT: { + uint8_t tk[16] = {1}; + esp_ble_oob_req_reply(param->ble_security.ble_req.bd_addr, tk, sizeof(tk)); + break; + } + case ESP_GAP_BLE_NC_REQ_EVT: + esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true); + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GAP_BLE_NC_REQ_EVT, the passkey Notify number:%" PRIu32, + param->ble_security.key_notif.passkey); + break; + case ESP_GAP_BLE_SEC_REQ_EVT: + esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true); + break; + case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: + ESP_LOGI(BLE_AMS_DEMO_TAG, "The passkey Notify number:%06" PRIu32, param->ble_security.key_notif.passkey); + break; + case ESP_GAP_BLE_AUTH_CMPL_EVT: + ESP_LOG_BUFFER_HEX("addr", param->ble_security.auth_cmpl.bd_addr, ESP_BD_ADDR_LEN); + ESP_LOGI(BLE_AMS_DEMO_TAG, "pair status = %s", param->ble_security.auth_cmpl.success ? "success" : "fail"); + if (!param->ble_security.auth_cmpl.success) { + ESP_LOGI(BLE_AMS_DEMO_TAG, "fail reason = 0x%x", param->ble_security.auth_cmpl.fail_reason); + } + break; + default: + break; + } +} + +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) +{ + switch (event) { + case ESP_GATTC_REG_EVT: + ESP_LOGI(BLE_AMS_DEMO_TAG, "REG_EVT"); + esp_ble_gap_set_device_name(EXAMPLE_DEVICE_NAME); + esp_ble_gap_config_local_icon(ESP_BLE_APPEARANCE_GENERIC_WATCH); + ble_ams_start_adv_config(); + break; + case ESP_GATTC_OPEN_EVT: + if (param->open.status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "open failed, error status = %x", param->open.status); + break; + } + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_OPEN_EVT"); + gl_profile_tab[PROFILE_A_APP_ID].conn_id = param->open.conn_id; + esp_ble_set_encryption(param->open.remote_bda, ESP_BLE_SEC_ENCRYPT_MITM); + esp_err_t mtu_ret = esp_ble_gattc_send_mtu_req(gattc_if, param->open.conn_id); + if (mtu_ret) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "config MTU error, error code = %x", mtu_ret); + } + break; + case ESP_GATTC_CFG_MTU_EVT: + if (param->cfg_mtu.status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "config mtu failed, error status = %x", param->cfg_mtu.status); + } + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_CFG_MTU_EVT, Status %d, MTU %d, conn_id %d", + param->cfg_mtu.status, param->cfg_mtu.mtu, param->cfg_mtu.conn_id); + gl_profile_tab[PROFILE_A_APP_ID].MTU_size = param->cfg_mtu.mtu; + memcpy(apple_ms_uuid.uuid.uuid128, Apple_MS_UUID, 16); + esp_ble_gattc_search_service(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + &apple_ms_uuid); + break; + case ESP_GATTC_SEARCH_RES_EVT: + if (param->search_res.srvc_id.uuid.len == ESP_UUID_LEN_128) { + gl_profile_tab[PROFILE_A_APP_ID].service_start_handle = param->search_res.start_handle; + gl_profile_tab[PROFILE_A_APP_ID].service_end_handle = param->search_res.end_handle; + get_service = true; + } + break; + case ESP_GATTC_SEARCH_CMPL_EVT: + if (param->search_cmpl.status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "search service failed, error status = %x", param->search_cmpl.status); + break; + } + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_SEARCH_CMPL_EVT"); + if (!get_service) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "No Apple Media Service found"); + break; + } + + { + uint16_t count = 0; + uint16_t offset = 0; + esp_gatt_status_t ret_status = esp_ble_gattc_get_attr_count(gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + ESP_GATT_DB_CHARACTERISTIC, + gl_profile_tab[PROFILE_A_APP_ID].service_start_handle, + gl_profile_tab[PROFILE_A_APP_ID].service_end_handle, + INVALID_HANDLE, + &count); + if (ret_status != ESP_GATT_OK || count == 0) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "esp_ble_gattc_get_attr_count error"); + break; + } + + char_elem_result = (esp_gattc_char_elem_t *)malloc(sizeof(esp_gattc_char_elem_t) * count); + if (!char_elem_result) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "gattc no mem"); + break; + } + + memset(char_elem_result, 0xff, sizeof(esp_gattc_char_elem_t) * count); + ret_status = esp_ble_gattc_get_all_char(gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + gl_profile_tab[PROFILE_A_APP_ID].service_start_handle, + gl_profile_tab[PROFILE_A_APP_ID].service_end_handle, + char_elem_result, + &count, + offset); + if (ret_status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "esp_ble_gattc_get_all_char error"); + free(char_elem_result); + char_elem_result = NULL; + break; + } + + for (int i = 0; i < count; i++) { + if (char_elem_result[i].uuid.len != ESP_UUID_LEN_128) { + continue; + } + + if ((char_elem_result[i].properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY) && + memcmp(char_elem_result[i].uuid.uuid.uuid128, remote_command_uuid, 16) == 0) { + gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle = char_elem_result[i].char_handle; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Found Remote Command characteristic"); + } else if ((char_elem_result[i].properties & ESP_GATT_CHAR_PROP_BIT_NOTIFY) && + memcmp(char_elem_result[i].uuid.uuid.uuid128, entity_update_uuid, 16) == 0) { + gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle = char_elem_result[i].char_handle; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Found Entity Update characteristic"); + } else if (memcmp(char_elem_result[i].uuid.uuid.uuid128, entity_attribute_uuid, 16) == 0) { + gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle = char_elem_result[i].char_handle; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Found Entity Attribute characteristic"); + } + } + + if (gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle != 0) { + ams_start_remote_command_notify(gattc_if); + } else if (gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle != 0) { + ams_start_entity_update_notify(gattc_if); + } + + free(char_elem_result); + char_elem_result = NULL; + } + break; + case ESP_GATTC_REG_FOR_NOTIFY_EVT: + if (param->reg_for_notify.status != ESP_GATT_OK) { + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_REG_FOR_NOTIFY_EVT status %d", param->reg_for_notify.status); + if (param->reg_for_notify.handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle) { + ams_schedule_entity_update_notify(gattc_if); + } + break; + } + + if (param->reg_for_notify.handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle && + remote_command_cccd_handle != 0) { + ams_write_cccd(gattc_if, remote_command_cccd_handle, + gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle); + } else if (param->reg_for_notify.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle && + entity_update_cccd_handle != 0) { + ams_write_cccd(gattc_if, entity_update_cccd_handle, + gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle); + } + break; + case ESP_GATTC_WRITE_DESCR_EVT: + if (param->write.status != ESP_GATT_OK) { + bool remote_command_pending = (pending_cccd_char_handle != 0 && + pending_cccd_char_handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle); + ESP_LOGE(BLE_AMS_DEMO_TAG, "write descr failed, error status = %x", param->write.status); + pending_cccd_char_handle = 0; + if (remote_command_pending) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Remote Command CCCD callback failed, continue Entity Update subscribe"); + ams_schedule_entity_update_notify(gattc_if); + } + break; + } + + if (pending_cccd_char_handle != 0 && + pending_cccd_char_handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle) { + pending_cccd_char_handle = 0; + remote_command_cccd_handle = 0; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Remote Command notifications enabled, delay 500 ms before Entity Update CCCD"); + ams_schedule_entity_update_notify(gattc_if); + } else if (pending_cccd_char_handle != 0 && + pending_cccd_char_handle == gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle && + !entity_update_notify_enabled) { + pending_cccd_char_handle = 0; + entity_update_cccd_handle = 0; + entity_update_notify_enabled = true; + ESP_LOGI(BLE_AMS_DEMO_TAG, "Entity Update notifications enabled"); + esp_subscribe_entity_updates(); + } else { + pending_cccd_char_handle = 0; + } + break; + case ESP_GATTC_NOTIFY_EVT: + if (param->notify.handle == gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle) { + esp_receive_remote_command_notification(param->notify.value, param->notify.value_len); + } else if (param->notify.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle) { + if (!param->notify.value || param->notify.value_len < 3) { + ESP_LOGW(BLE_AMS_DEMO_TAG, "Entity update notification too short (%u)", param->notify.value_len); + break; + } + esp_receive_entity_update(param->notify.value, param->notify.value_len); + if (param->notify.value[2] & EntityUpdateFlagTruncated) { + esp_request_entity_attribute(param->notify.value[0], param->notify.value[1]); + } + } else { + ESP_LOGI(BLE_AMS_DEMO_TAG, "Unknown notify handle 0x%04x", param->notify.handle); + } + break; + case ESP_GATTC_WRITE_CHAR_EVT: + if (param->write.status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "write char error, status = 0x%x (%s)", + param->write.status, Errcode_to_String(param->write.status)); + if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle) { + entity_attr_req_in_flight = false; + esp_process_next_entity_attribute_request(); + } else if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle) { + entity_update_sub_in_flight = false; + esp_process_next_entity_update_subscribe(); + } + break; + } + + if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle) { + entity_update_sub_in_flight = false; + esp_process_next_entity_update_subscribe(); + break; + } + + if (param->write.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle) { + esp_err_t ret = esp_ble_gattc_read_char(gattc_if, + gl_profile_tab[PROFILE_A_APP_ID].conn_id, + gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle, + ESP_GATT_AUTH_REQ_NONE); + if (ret != ESP_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "read char request failed, error = %x", ret); + entity_attr_req_in_flight = false; + esp_process_next_entity_attribute_request(); + } + } + break; + case ESP_GATTC_READ_CHAR_EVT: + if (param->read.status != ESP_GATT_OK) { + ESP_LOGE(BLE_AMS_DEMO_TAG, "read char failed, error status = %x", param->read.status); + } + if (param->read.handle == gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle) { + if (param->read.status == ESP_GATT_OK) { + esp_receive_entity_attribute_value(pending_entity_id, + pending_attribute_id, + param->read.value, + param->read.value_len); + } + entity_attr_req_in_flight = false; + esp_process_next_entity_attribute_request(); + } + break; + case ESP_GATTC_DISCONNECT_EVT: + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_DISCONNECT_EVT, reason = 0x%x", param->disconnect.reason); + get_service = false; + entity_update_notify_enabled = false; + pending_cccd_char_handle = 0; + remote_command_cccd_handle = 0; + entity_update_cccd_handle = 0; + pending_entity_update_gattc_if = ESP_GATT_IF_NONE; + entity_attr_req_queue_reset(); + entity_update_sub_queue_reset(); + esp_timer_stop(entity_update_cccd_delay_timer); + gl_profile_tab[PROFILE_A_APP_ID].remote_command_handle = 0; + gl_profile_tab[PROFILE_A_APP_ID].entity_update_handle = 0; + gl_profile_tab[PROFILE_A_APP_ID].entity_attribute_handle = 0; + esp_ble_gap_start_advertising(&adv_params); + break; + case ESP_GATTC_CONNECT_EVT: + memcpy(gl_profile_tab[PROFILE_A_APP_ID].remote_bda, param->connect.remote_bda, ESP_BD_ADDR_LEN); + esp_ble_gatt_creat_conn_params_t creat_conn_params = {0}; + memcpy(&creat_conn_params.remote_bda, gl_profile_tab[PROFILE_A_APP_ID].remote_bda, ESP_BD_ADDR_LEN); + creat_conn_params.remote_addr_type = BLE_ADDR_TYPE_RANDOM; + creat_conn_params.own_addr_type = BLE_ADDR_TYPE_PUBLIC; + creat_conn_params.is_direct = true; + creat_conn_params.is_aux = false; + creat_conn_params.phy_mask = 0x0; + esp_ble_gattc_enh_open(gl_profile_tab[PROFILE_A_APP_ID].gattc_if, &creat_conn_params); + break; + case ESP_GATTC_SRVC_CHG_EVT: + ESP_LOGI(BLE_AMS_DEMO_TAG, "ESP_GATTC_SRVC_CHG_EVT"); + break; + default: + break; + } +} + +static void esp_gattc_cb(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) +{ + if (event == ESP_GATTC_REG_EVT) { + if (param->reg.status == ESP_GATT_OK) { + gl_profile_tab[param->reg.app_id].gattc_if = gattc_if; + } else { + ESP_LOGI(BLE_AMS_DEMO_TAG, "Reg app failed, app_id %04x, status %d", + param->reg.app_id, param->reg.status); + return; + } + } + + for (int idx = 0; idx < PROFILE_NUM; idx++) { + if (gattc_if == ESP_GATT_IF_NONE || gattc_if == gl_profile_tab[idx].gattc_if) { + if (gl_profile_tab[idx].gattc_cb) { + gl_profile_tab[idx].gattc_cb(event, gattc_if, param); + } + } + } +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + 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_ERROR_CHECK(esp_bt_controller_init(&bt_cfg)); + ESP_ERROR_CHECK(esp_bt_controller_enable(ESP_BT_MODE_BLE)); + + ESP_LOGI(BLE_AMS_DEMO_TAG, "%s init bluetooth", __func__); + + esp_bluedroid_config_t cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_bluedroid_init_with_cfg(&cfg)); + ESP_ERROR_CHECK(esp_bluedroid_enable()); + ESP_ERROR_CHECK(esp_ble_gattc_register_callback(esp_gattc_cb)); + ESP_ERROR_CHECK(esp_ble_gap_register_callback(gap_event_handler)); + ESP_ERROR_CHECK(esp_ble_gattc_app_register(PROFILE_A_APP_ID)); + ESP_ERROR_CHECK(esp_ble_gatt_set_local_mtu(500)); + + const esp_timer_create_args_t entity_update_cccd_delay_timer_args = { + .callback = ams_entity_update_cccd_delay_timer_cb, + .name = "ams_eu_cccd", + }; + ESP_ERROR_CHECK(esp_timer_create(&entity_update_cccd_delay_timer_args, &entity_update_cccd_delay_timer)); + + esp_ble_auth_req_t auth_req = ESP_LE_AUTH_REQ_SC_MITM_BOND; + esp_ble_io_cap_t iocap = ESP_IO_CAP_NONE; + uint8_t key_size = 16; + uint8_t init_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint8_t rsp_key = ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK; + uint32_t passkey = 123456; + uint8_t auth_option = ESP_BLE_ONLY_ACCEPT_SPECIFIED_AUTH_DISABLE; + uint8_t oob_support = ESP_BLE_OOB_DISABLE; + + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_STATIC_PASSKEY, &passkey, sizeof(uint32_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_AUTHEN_REQ_MODE, &auth_req, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_IOCAP_MODE, &iocap, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_MAX_KEY_SIZE, &key_size, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_ONLY_ACCEPT_SPECIFIED_SEC_AUTH, &auth_option, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_OOB_SUPPORT, &oob_support, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_INIT_KEY, &init_key, sizeof(uint8_t)); + esp_ble_gap_set_security_param(ESP_BLE_SM_SET_RSP_KEY, &rsp_key, sizeof(uint8_t)); +} diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults new file mode 100644 index 00000000000..18930b857e3 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults @@ -0,0 +1,14 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_BT_ENABLED=y +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 AMS example +# (advertises as peripheral and acts as GATT client to AMS service) +# CONFIG_BT_BLE_42_SCAN_EN is not set +# CONFIG_BT_BLE_42_DTM_TEST_EN is not set diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c2 b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c2 new file mode 100644 index 00000000000..298663d0a39 --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c2 @@ -0,0 +1,9 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32c2" +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_BT_LE_HCI_EVT_BUF_SIZE=257 diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c3 b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c3 new file mode 100644 index 00000000000..0ad7bbc0aeb --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32c3 @@ -0,0 +1,7 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32c3" +CONFIG_BT_ENABLED=y +# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y diff --git a/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32s3 b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32s3 new file mode 100644 index 00000000000..2a50f1657bf --- /dev/null +++ b/examples/bluetooth/bluedroid/ble/ble_ams/sdkconfig.defaults.esp32s3 @@ -0,0 +1,7 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_IDF_TARGET="esp32s3" +CONFIG_BT_ENABLED=y +# CONFIG_BT_BLE_50_FEATURES_SUPPORTED is not set +CONFIG_BT_BLE_42_FEATURES_SUPPORTED=y diff --git a/examples/bluetooth/nimble/ble_ams/CMakeLists.txt b/examples/bluetooth/nimble/ble_ams/CMakeLists.txt new file mode 100644 index 00000000000..7effdd0c06b --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/CMakeLists.txt @@ -0,0 +1,8 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.22) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +# "Trim" the build. Include the minimal set of components, main, and anything it depends on. +idf_build_set_property(MINIMAL_BUILD ON) +project(ble_ams) diff --git a/examples/bluetooth/nimble/ble_ams/README.md b/examples/bluetooth/nimble/ble_ams/README.md new file mode 100644 index 00000000000..0dc8f94811b --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/README.md @@ -0,0 +1,123 @@ +| Supported Targets | ESP32 | ESP32-C2 | ESP32-C3 | ESP32-C5 | ESP32-C6 | ESP32-C61 | ESP32-H2 | ESP32-H21 | ESP32-H4 | ESP32-S3 | ESP32-S31 | +| ----------------- | ----- | -------- | -------- | -------- | -------- | --------- | -------- | --------- | -------- | -------- | --------- | + +# ESP-IDF NimBLE AMS Example + +The purpose of the Apple Media Service (AMS) is to give Bluetooth accessories (that connect to iOS devices through a Bluetooth low-energy link) a simple and convenient way to control media apps and access information about the media state of the connected iOS device. + +## Flow Diagram + +``` + ┌──────────────┐ ┌──────────────┐ + │ ESP32 │ │ iOS Device │ + │ (AMS Client) │ │ (AMS Server) │ + └──────┬───────┘ └──────┬───────┘ + │ │ + │ ─────────── Connection ─────────── │ + │ │ + │ 1. Start Advertising │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 2. iOS connects & pairs │ + │ <─────────────────────────────────────────────── │ + │ │ + │ ─────────── Service Discovery ─────────── │ + │ │ + │ 3. Discover AMS Service │ + │ (UUID: 89D3502B-0F36-433A-8EF4-C502AD55F8DC) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 4. Subscribe to Remote Command │ + │ (UUID: 9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 5. Subscribe to Entity Update │ + │ (UUID: 2F7CABCE-808D-411F-9A0C-BB92BA96C102) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ 6. Register entity/attribute pairs │ + │ (Write to Entity Update) │ + │ ───────────────────────────────────────────────> │ + │ │ + │ ─────────── Media Update Flow ─────────── │ + │ │ + │ 7. Media state changes on iOS │ + │ (Track, Player, Queue updates) │ + │ <─────────────────────────────────────────────── │ + │ │ + │ 8. Optional: read full truncated value │ + │ (Write + Read Entity Attribute) │ + │ ───────────────────────────────────────────────> │ + │ │ + ┌──────┴───────┐ ┌──────┴───────┐ + │ ESP32 │ │ iOS Device │ + └──────────────┘ └──────────────┘ +``` + +## How to Use Example + +Before project configuration and build, be sure to set the correct chip target using: + +```bash +idf.py set-target +``` + +The Apple Media Service is a primary service whose service UUID is: + +`89D3502B-0F36-433A-8EF4-C502AD55F8DC` + +Only one instance of the AMS may be present on an MS. Due to the nature of iOS, the AMS is not guaranteed to always be present. As a result, the MR should look for and subscribe to the Service Changed characteristic of the GATT service in order to monitor for the potential publishing and unpublishing of the AMS at any time. + +In its basic form, the AMS exposes three characteristics: + +* Remote Command: UUID `9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2` (writeable, notifiable) +* Entity Update: UUID `2F7CABCE-808D-411F-9A0C-BB92BA96C102` (writeable with response, notifiable) +* Entity Attribute: UUID `C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7` (readable, writeable) + +After pairing, start playing music on the iOS device. The example subscribes to track, player, and queue entity updates and prints media metadata to the console. + +On targets with `SOC_ESP_NIMBLE_CONTROLLER`, extended advertising is enabled by default (`nimble-ams`); other targets use legacy connectable advertising. Toggle via `idf.py menuconfig` → Example Configuration → Enable Extended Adv. + +Advertising data matches `ble_ancs`: HID appearance, 16-bit service UUID `0x1812`, and device name in the primary advertising packet. + +To send a remote command from application code, call `ble_send_remote_command()` with a value from `ble_remote_command_id_t`, for example `RemoteCommandIDTogglePlayPause`. + +### Hardware Required + +* A development board with ESP32/ESP32-C3/ESP32-H2/ESP32-C2/ESP32-S3 SoC (e.g., ESP32-DevKitC, ESP-WROVER-KIT, etc.) +* A USB cable for power supply and programming + +See [Development Boards](https://www.espressif.com/en/products/devkits) for more information about it. + +### Build and Flash + +Run `idf.py -p PORT flash monitor` to build, flash and monitor the project. + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the [Getting Started Guide](https://idf.espressif.com/) for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +``` +I (466) NimBLE_AMS: BLE Host Task Started +I (476) NimBLE: Device Address: +I (476) NimBLE: 48:27:e2:e9:0e:a6 +I (476) NimBLE: GAP procedure initiated: advertise; +I (5234) NimBLE: connection established; status=0 +I (5456) NimBLE: encryption change event; status=0 +I (5678) NimBLE: Found Remote Command characteristic +I (5678) NimBLE: Found Entity Update characteristic +I (5680) NimBLE: Found Entity Attribute characteristic +I (5720) NimBLE: Entity Update notifications enabled +I (5721) NimBLE: Subscribe track entity updates +I (5800) NimBLE_AMS: Supported remote commands (6): +I (5801) NimBLE_AMS: [0] Play +I (5802) NimBLE_AMS: [1] Pause +I (5900) NimBLE_AMS: Entity update: Track / Title, flags=0x00, len=12 +I (5901) NimBLE_AMS: Value: Example Song +``` + +## Troubleshooting + +For any technical queries, please open an [issue](https://github.com/espressif/esp-idf/issues) on GitHub. We will get back to you soon. diff --git a/examples/bluetooth/nimble/ble_ams/main/CMakeLists.txt b/examples/bluetooth/nimble/ble_ams/main/CMakeLists.txt new file mode 100644 index 00000000000..46a2bf48388 --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/CMakeLists.txt @@ -0,0 +1,5 @@ +set(srcs "main.c" + "ble_ams.c") + +idf_component_register(SRCS "${srcs}" + INCLUDE_DIRS ".") diff --git a/examples/bluetooth/nimble/ble_ams/main/Kconfig.projbuild b/examples/bluetooth/nimble/ble_ams/main/Kconfig.projbuild new file mode 100644 index 00000000000..8e5ab9f250a --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/Kconfig.projbuild @@ -0,0 +1,14 @@ +menu "Example Configuration" + + config EXAMPLE_EXTENDED_ADV + bool + depends on SOC_BLE_50_SUPPORTED && BT_NIMBLE_50_FEATURE_SUPPORT + default y if SOC_ESP_NIMBLE_CONTROLLER + select BT_NIMBLE_EXT_ADV + prompt "Enable Extended Adv" + help + Use this option to enable extended advertising in the example. + If this option is disabled, ensure config BT_NIMBLE_EXT_ADV is + also disabled from Nimble stack menuconfig + +endmenu diff --git a/examples/bluetooth/nimble/ble_ams/main/ble_ams.c b/examples/bluetooth/nimble/ble_ams/main/ble_ams.c new file mode 100644 index 00000000000..c6866f80143 --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/ble_ams.c @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "esp_log.h" +#include "ble_ams.h" + +#define NimBLE_AMS_TAG "NimBLE_AMS" + +const char *RemoteCommandID_to_String(uint8_t command_id) +{ + switch (command_id) { + case RemoteCommandIDPlay: + return "Play"; + case RemoteCommandIDPause: + return "Pause"; + case RemoteCommandIDTogglePlayPause: + return "TogglePlayPause"; + case RemoteCommandIDNextTrack: + return "NextTrack"; + case RemoteCommandIDPreviousTrack: + return "PreviousTrack"; + case RemoteCommandIDVolumeUp: + return "VolumeUp"; + case RemoteCommandIDVolumeDown: + return "VolumeDown"; + case RemoteCommandIDAdvanceRepeatMode: + return "AdvanceRepeatMode"; + case RemoteCommandIDAdvanceShuffleMode: + return "AdvanceShuffleMode"; + case RemoteCommandIDSkipForward: + return "SkipForward"; + case RemoteCommandIDSkipBackward: + return "SkipBackward"; + case RemoteCommandIDLikeTrack: + return "LikeTrack"; + case RemoteCommandIDDislikeTrack: + return "DislikeTrack"; + case RemoteCommandIDBookmarkTrack: + return "BookmarkTrack"; + default: + return "Unknown RemoteCommandID"; + } +} + +const char *EntityID_to_String(uint8_t entity_id) +{ + switch (entity_id) { + case EntityIDPlayer: + return "Player"; + case EntityIDQueue: + return "Queue"; + case EntityIDTrack: + return "Track"; + default: + return "Unknown EntityID"; + } +} + +const char *AttributeID_to_String(uint8_t entity_id, uint8_t attribute_id) +{ + switch (entity_id) { + case EntityIDPlayer: + switch (attribute_id) { + case PlayerAttributeIDName: + return "PlayerName"; + case PlayerAttributeIDPlaybackInfo: + return "PlaybackInfo"; + case PlayerAttributeIDVolume: + return "Volume"; + default: + return "Unknown PlayerAttributeID"; + } + case EntityIDQueue: + switch (attribute_id) { + case QueueAttributeIDIndex: + return "QueueIndex"; + case QueueAttributeIDCount: + return "QueueCount"; + case QueueAttributeIDShuffleMode: + return "ShuffleMode"; + case QueueAttributeIDRepeatMode: + return "RepeatMode"; + default: + return "Unknown QueueAttributeID"; + } + case EntityIDTrack: + switch (attribute_id) { + case TrackAttributeIDArtist: + return "Artist"; + case TrackAttributeIDAlbum: + return "Album"; + case TrackAttributeIDTitle: + return "Title"; + case TrackAttributeIDDuration: + return "Duration"; + default: + return "Unknown TrackAttributeID"; + } + default: + return "Unknown AttributeID"; + } +} + +const char *PlaybackState_to_String(uint8_t playback_state) +{ + switch (playback_state) { + case PlaybackStatePaused: + return "Paused"; + case PlaybackStatePlaying: + return "Playing"; + case PlaybackStateRewinding: + return "Rewinding"; + case PlaybackStateFastForwarding: + return "FastForwarding"; + default: + return "Unknown PlaybackState"; + } +} + +static void log_playback_info(const uint8_t *value, uint16_t value_len) +{ + if (!value || value_len == 0) { + return; + } + + char buffer[256]; + uint16_t copy_len = value_len < (sizeof(buffer) - 1) ? value_len : (sizeof(buffer) - 1); + memcpy(buffer, value, copy_len); + buffer[copy_len] = '\0'; + + char *state = buffer; + char *rate = NULL; + char *elapsed = NULL; + char *separator = strchr(state, ','); + if (separator) { + *separator = '\0'; + rate = separator + 1; + separator = strchr(rate, ','); + if (separator) { + *separator = '\0'; + elapsed = separator + 1; + } + } + + if (state[0] != '\0') { + int playback_state = atoi(state); + ESP_LOGI(NimBLE_AMS_TAG, " PlaybackState: %s", PlaybackState_to_String((uint8_t)playback_state)); + } + if (rate && rate[0] != '\0') { + ESP_LOGI(NimBLE_AMS_TAG, " PlaybackRate: %s", rate); + } + if (elapsed && elapsed[0] != '\0') { + ESP_LOGI(NimBLE_AMS_TAG, " ElapsedTime: %s s", elapsed); + } +} + +void ble_receive_remote_command_notification(uint8_t *message, uint16_t message_len) +{ + if (!message || message_len == 0) { + return; + } + + ESP_LOGI(NimBLE_AMS_TAG, "Supported remote commands (%u):", message_len); + for (uint16_t i = 0; i < message_len; i++) { + ESP_LOGI(NimBLE_AMS_TAG, " [%u] %s", i, RemoteCommandID_to_String(message[i])); + } +} + +void ble_receive_entity_update(uint8_t *message, uint16_t message_len) +{ + if (!message || message_len < 3) { + return; + } + + uint8_t entity_id = message[0]; + uint8_t attribute_id = message[1]; + uint8_t flags = message[2]; + uint8_t *value = &message[3]; + uint16_t value_len = message_len - 3; + + ESP_LOGI(NimBLE_AMS_TAG, "Entity update: %s / %s, flags=0x%02x, len=%u", + EntityID_to_String(entity_id), + AttributeID_to_String(entity_id, attribute_id), + flags, + value_len); + + if (entity_id == EntityIDPlayer && attribute_id == PlayerAttributeIDPlaybackInfo) { + log_playback_info(value, value_len); + return; + } + + if (value_len > 0) { + ESP_LOG_BUFFER_CHAR("Value", value, value_len); + } + + if (flags & EntityUpdateFlagTruncated) { + ESP_LOGI(NimBLE_AMS_TAG, "Value truncated; read full value from Entity Attribute characteristic"); + } +} + +void ble_receive_entity_attribute_value(uint8_t entity_id, uint8_t attribute_id, + uint8_t *message, uint16_t message_len) +{ + ESP_LOGI(NimBLE_AMS_TAG, "Full entity attribute: %s / %s, len=%u", + EntityID_to_String(entity_id), + AttributeID_to_String(entity_id, attribute_id), + message_len); + + if (entity_id == EntityIDPlayer && attribute_id == PlayerAttributeIDPlaybackInfo) { + log_playback_info(message, message_len); + return; + } + + if (message && message_len > 0) { + ESP_LOG_BUFFER_CHAR("Value", message, message_len); + } +} diff --git a/examples/bluetooth/nimble/ble_ams/main/ble_ams.h b/examples/bluetooth/nimble/ble_ams/main/ble_ams.h new file mode 100644 index 00000000000..1bb3fd15edb --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/ble_ams.h @@ -0,0 +1,75 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#include + +typedef enum { + RemoteCommandIDPlay = 0, + RemoteCommandIDPause = 1, + RemoteCommandIDTogglePlayPause = 2, + RemoteCommandIDNextTrack = 3, + RemoteCommandIDPreviousTrack = 4, + RemoteCommandIDVolumeUp = 5, + RemoteCommandIDVolumeDown = 6, + RemoteCommandIDAdvanceRepeatMode = 7, + RemoteCommandIDAdvanceShuffleMode = 8, + RemoteCommandIDSkipForward = 9, + RemoteCommandIDSkipBackward = 10, + RemoteCommandIDLikeTrack = 11, + RemoteCommandIDDislikeTrack = 12, + RemoteCommandIDBookmarkTrack = 13, +} ble_remote_command_id_t; + +typedef enum { + EntityIDPlayer = 0, + EntityIDQueue = 1, + EntityIDTrack = 2, +} ble_entity_id_t; + +typedef enum { + EntityUpdateFlagTruncated = (1 << 0), +} ble_entity_update_flag_t; + +typedef enum { + PlayerAttributeIDName = 0, + PlayerAttributeIDPlaybackInfo = 1, + PlayerAttributeIDVolume = 2, +} ble_player_attribute_id_t; + +typedef enum { + QueueAttributeIDIndex = 0, + QueueAttributeIDCount = 1, + QueueAttributeIDShuffleMode = 2, + QueueAttributeIDRepeatMode = 3, +} ble_queue_attribute_id_t; + +typedef enum { + TrackAttributeIDArtist = 0, + TrackAttributeIDAlbum = 1, + TrackAttributeIDTitle = 2, + TrackAttributeIDDuration = 3, +} ble_track_attribute_id_t; + +typedef enum { + PlaybackStatePaused = 0, + PlaybackStatePlaying = 1, + PlaybackStateRewinding = 2, + PlaybackStateFastForwarding = 3, +} ble_playback_state_t; + +const char *RemoteCommandID_to_String(uint8_t command_id); +const char *EntityID_to_String(uint8_t entity_id); +const char *AttributeID_to_String(uint8_t entity_id, uint8_t attribute_id); +const char *PlaybackState_to_String(uint8_t playback_state); + +void ble_receive_remote_command_notification(uint8_t *message, uint16_t message_len); +void ble_receive_entity_update(uint8_t *message, uint16_t message_len); +void ble_receive_entity_attribute_value(uint8_t entity_id, uint8_t attribute_id, + uint8_t *message, uint16_t message_len); + +void ble_send_remote_command(uint16_t conn_handle, uint8_t command_id); diff --git a/examples/bluetooth/nimble/ble_ams/main/idf_component.yml b/examples/bluetooth/nimble/ble_ams/main/idf_component.yml new file mode 100644 index 00000000000..d6e735fe770 --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/idf_component.yml @@ -0,0 +1,3 @@ +dependencies: + nimble_peripheral_utils: + path: ${IDF_PATH}/examples/bluetooth/nimble/common/nimble_peripheral_utils diff --git a/examples/bluetooth/nimble/ble_ams/main/main.c b/examples/bluetooth/nimble/ble_ams/main/main.c new file mode 100644 index 00000000000..1580d48a1ee --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/main/main.c @@ -0,0 +1,924 @@ +/* + * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#include +#include +#include "esp_log.h" +#include "nvs_flash.h" +#include "nimble/nimble_port.h" +#include "nimble/nimble_port_freertos.h" +#include "host/ble_hs.h" +#include "host/ble_store.h" +#include "host/util/util.h" +#include "console/console.h" +#include "services/gap/ble_svc_gap.h" +#include "nimble/ble.h" +#include "esp_peripheral.h" +#include "ble_ams.h" + +static uint16_t remote_command_handle; +static uint16_t remote_command_def_handle; +static uint16_t entity_update_handle; +static uint16_t entity_update_def_handle; +static uint16_t entity_attribute_handle; +static uint16_t entity_attribute_def_handle; +static uint16_t svc_e_handle; +static bool entity_update_notify_enabled = false; +static uint8_t pending_entity_id = 0; +static uint8_t pending_attribute_id = 0; +#define ENTITY_ATTR_REQ_QUEUE_LEN 4 +#define ENTITY_UPDATE_SUB_CMD_MAX_LEN 8 +#define ENTITY_UPDATE_SUB_QUEUE_LEN 3 + +typedef struct { + uint16_t conn_handle; + uint8_t entity_id; + uint8_t attribute_id; +} entity_attr_req_t; + +typedef struct { + uint16_t conn_handle; + uint8_t cmd_len; + uint8_t cmd[ENTITY_UPDATE_SUB_CMD_MAX_LEN]; +} entity_update_sub_req_t; + +static bool entity_attr_req_in_flight; +static entity_attr_req_t entity_attr_req_queue[ENTITY_ATTR_REQ_QUEUE_LEN]; +static uint8_t entity_attr_req_head; +static uint8_t entity_attr_req_tail; +static uint8_t entity_attr_req_count; + +static bool entity_update_sub_in_flight; +static entity_update_sub_req_t entity_update_sub_queue[ENTITY_UPDATE_SUB_QUEUE_LEN]; +static uint8_t entity_update_sub_head; +static uint8_t entity_update_sub_tail; +static uint8_t entity_update_sub_count; + +// Apple Media Service UUID: 89D3502B-0F36-433A-8EF4-C502AD55F8DC +static ble_uuid128_t APPLE_MS_UUID = BLE_UUID128_INIT( + 0xDC, 0xF8, 0x55, 0xAD, 0x02, 0xC5, 0xF4, 0x8E, 0x3A, 0x43, 0x36, 0x0F, 0x2B, 0x50, 0xD3, 0x89); +// Remote Command UUID: 9B3C81D8-57B1-4A8A-B8DF-0E56F7CA51C2 +static ble_uuid128_t remote_command_uuid = BLE_UUID128_INIT( + 0xC2, 0x51, 0xCA, 0xF7, 0x56, 0x0E, 0xDF, 0xB8, 0x8A, 0x4A, 0xB1, 0x57, 0xD8, 0x81, 0x3C, 0x9B); +// Entity Update UUID: 2F7CABCE-808D-411F-9A0C-BB92BA96C102 +static ble_uuid128_t entity_update_uuid = BLE_UUID128_INIT( + 0x02, 0xC1, 0x96, 0xBA, 0x92, 0xBB, 0x0C, 0x9A, 0x1F, 0x41, 0x8D, 0x80, 0xCE, 0xAB, 0x7C, 0x2F); +// Entity Attribute UUID: C6B2F38C-23AB-46D8-A6AB-A3A870BBD5D7 +static ble_uuid128_t entity_attribute_uuid = BLE_UUID128_INIT( + 0xD7, 0xD5, 0xBB, 0x70, 0xA8, 0xA3, 0xAB, 0xA6, 0xD8, 0x46, 0xAB, 0x23, 0x8C, 0xF3, 0xB2, 0xC6); + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static uint8_t ext_adv_pattern_1[] = { + 0x02, BLE_HS_ADV_TYPE_FLAGS, 0x06, + 0x03, BLE_HS_ADV_TYPE_COMP_UUIDS16, 0xab, 0xcd, + 0x03, BLE_HS_ADV_TYPE_COMP_UUIDS16, 0x18, 0x11, + 0x0d, BLE_HS_ADV_TYPE_COMP_NAME, 'n', 'i', 'm', 'b', 'l', 'e', '-', 'a', 'm', 's', '-', 'e', +}; +#endif + +static const char *tag = "NimBLE_AMS"; +static int ble_ams_gap_event(struct ble_gap_event *event, void *arg); +static uint8_t own_addr_type; + +/* Match Bluedroid ble_ams / ble_ancs: helps iOS Settings list the device. */ +#define BLE_AMS_APPEARANCE BLE_SVC_GAP_APPEARANCE_GEN_HID + +#if !CONFIG_EXAMPLE_EXTENDED_ADV +#define BLE_AMS_ADV_SVC_UUID16 0x1812 /* HID Service */ + +static const ble_uuid16_t adv_uuids16[] = { + BLE_UUID16_INIT(BLE_AMS_ADV_SVC_UUID16), +}; +#endif + +void ble_store_config_init(void); + +static int ams_dsc_discovered_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc, + void *arg); +static void ble_process_next_entity_attribute_request(void); +static void ble_process_next_entity_update_subscribe(void); + +static void ams_discovery_state_reset(void) +{ + svc_e_handle = 0; + remote_command_handle = 0; + remote_command_def_handle = 0; + entity_update_handle = 0; + entity_update_def_handle = 0; + entity_attribute_handle = 0; + entity_attribute_def_handle = 0; + entity_update_notify_enabled = false; +} + +static void entity_attr_req_queue_reset(void) +{ + entity_attr_req_head = 0; + entity_attr_req_tail = 0; + entity_attr_req_count = 0; + entity_attr_req_in_flight = false; + pending_entity_id = 0; + pending_attribute_id = 0; +} + +static void entity_update_sub_queue_reset(void) +{ + entity_update_sub_head = 0; + entity_update_sub_tail = 0; + entity_update_sub_count = 0; + entity_update_sub_in_flight = false; +} + +static bool entity_attr_req_queue_push(uint16_t conn_handle, uint8_t entity_id, uint8_t attribute_id) +{ + if (entity_attr_req_count >= ENTITY_ATTR_REQ_QUEUE_LEN) { + return false; + } + + entity_attr_req_queue[entity_attr_req_tail].conn_handle = conn_handle; + entity_attr_req_queue[entity_attr_req_tail].entity_id = entity_id; + entity_attr_req_queue[entity_attr_req_tail].attribute_id = attribute_id; + entity_attr_req_tail = (entity_attr_req_tail + 1) % ENTITY_ATTR_REQ_QUEUE_LEN; + entity_attr_req_count++; + return true; +} + +static bool entity_attr_req_queue_pop(entity_attr_req_t *req) +{ + if (req == NULL || entity_attr_req_count == 0) { + return false; + } + + *req = entity_attr_req_queue[entity_attr_req_head]; + entity_attr_req_head = (entity_attr_req_head + 1) % ENTITY_ATTR_REQ_QUEUE_LEN; + entity_attr_req_count--; + return true; +} + +static uint16_t ams_chr_dsc_end_handle(uint16_t chr_def_handle) +{ + uint16_t end_handle = svc_e_handle; + + if (remote_command_def_handle > chr_def_handle && + remote_command_def_handle - 1 < end_handle) { + end_handle = remote_command_def_handle - 1; + } + if (entity_update_def_handle > chr_def_handle && + entity_update_def_handle - 1 < end_handle) { + end_handle = entity_update_def_handle - 1; + } + if (entity_attribute_def_handle > chr_def_handle && + entity_attribute_def_handle - 1 < end_handle) { + end_handle = entity_attribute_def_handle - 1; + } + + return end_handle; +} + +static int ams_discover_descriptors(uint16_t conn_handle, uint16_t chr_handle, + uint16_t chr_def_handle, const char *chr_name) +{ + int rc; + + if (chr_handle == 0 || chr_def_handle == 0) { + return 0; + } + + rc = ble_gattc_disc_all_dscs(conn_handle, + chr_handle, + ams_chr_dsc_end_handle(chr_def_handle), + ams_dsc_discovered_cb, + (void *)(uintptr_t)chr_handle); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to discover %s descriptors: rc=%d", chr_name, rc); + } + + return rc; +} + +static bool entity_update_sub_queue_push(uint16_t conn_handle, const uint8_t *cmd, uint8_t cmd_len) +{ + if (entity_update_sub_count >= ENTITY_UPDATE_SUB_QUEUE_LEN || + cmd_len > ENTITY_UPDATE_SUB_CMD_MAX_LEN) { + return false; + } + + entity_update_sub_queue[entity_update_sub_tail].conn_handle = conn_handle; + entity_update_sub_queue[entity_update_sub_tail].cmd_len = cmd_len; + memcpy(entity_update_sub_queue[entity_update_sub_tail].cmd, cmd, cmd_len); + entity_update_sub_tail = (entity_update_sub_tail + 1) % ENTITY_UPDATE_SUB_QUEUE_LEN; + entity_update_sub_count++; + return true; +} + +static bool entity_update_sub_queue_pop(entity_update_sub_req_t *req) +{ + if (req == NULL || entity_update_sub_count == 0) { + return false; + } + + *req = entity_update_sub_queue[entity_update_sub_head]; + entity_update_sub_head = (entity_update_sub_head + 1) % ENTITY_UPDATE_SUB_QUEUE_LEN; + entity_update_sub_count--; + return true; +} + +static int ams_entity_update_sub_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + if (error->status != 0) { + MODLOG_DFLT(WARN, "Entity update subscribe write failed; status=%d\n", error->status); + } + + entity_update_sub_in_flight = false; + ble_process_next_entity_update_subscribe(); + return 0; +} + +static void ble_process_next_entity_update_subscribe(void) +{ + entity_update_sub_req_t req; + int rc; + + if (entity_update_sub_in_flight || !entity_update_sub_queue_pop(&req)) { + return; + } + + entity_update_sub_in_flight = true; + MODLOG_DFLT(INFO, "Subscribe %s entity updates", EntityID_to_String(req.cmd[0])); + + rc = ble_gattc_write_flat(req.conn_handle, + entity_update_handle, + req.cmd, + req.cmd_len, + ams_entity_update_sub_write_cb, + NULL); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to write entity update request: rc=%d\n", rc); + entity_update_sub_in_flight = false; + ble_process_next_entity_update_subscribe(); + } +} + +static void ble_subscribe_entity_updates(uint16_t conn_handle) +{ + const uint8_t track_cmd[] = { + EntityIDTrack, + TrackAttributeIDArtist, + TrackAttributeIDAlbum, + TrackAttributeIDTitle, + TrackAttributeIDDuration, + }; + const uint8_t player_cmd[] = { + EntityIDPlayer, + PlayerAttributeIDName, + PlayerAttributeIDPlaybackInfo, + PlayerAttributeIDVolume, + }; + const uint8_t queue_cmd[] = { + EntityIDQueue, + QueueAttributeIDIndex, + QueueAttributeIDCount, + QueueAttributeIDShuffleMode, + QueueAttributeIDRepeatMode, + }; + + entity_update_sub_queue_reset(); + + if (!entity_update_sub_queue_push(conn_handle, track_cmd, sizeof(track_cmd)) || + !entity_update_sub_queue_push(conn_handle, player_cmd, sizeof(player_cmd)) || + !entity_update_sub_queue_push(conn_handle, queue_cmd, sizeof(queue_cmd))) { + MODLOG_DFLT(WARN, "Failed to queue entity update subscribe requests"); + return; + } + + ble_process_next_entity_update_subscribe(); +} + +void ble_send_remote_command(uint16_t conn_handle, uint8_t command_id) +{ + if (remote_command_handle == 0) { + MODLOG_DFLT(WARN, "Remote command handle not discovered\n"); + return; + } + + int rc = ble_gattc_write_flat(conn_handle, + remote_command_handle, + &command_id, + 1, + NULL, + NULL); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to write remote command: rc=%d\n", rc); + } +} + +static int ams_entity_attr_read_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + if (error->status == 0 && attr != NULL && attr->om != NULL) { + ble_receive_entity_attribute_value(pending_entity_id, + pending_attribute_id, + attr->om->om_data, + attr->om->om_len); + } else if (error->status != 0) { + MODLOG_DFLT(WARN, "Entity attribute read failed; status=%d\n", error->status); + } + + entity_attr_req_in_flight = false; + ble_process_next_entity_attribute_request(); + return 0; +} + +static int ams_entity_attr_write_cb(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + if (error->status != 0) { + MODLOG_DFLT(WARN, "Entity attribute write failed; status=%d\n", error->status); + entity_attr_req_in_flight = false; + ble_process_next_entity_attribute_request(); + return 0; + } + + int rc = ble_gattc_read(conn_handle, entity_attribute_handle, ams_entity_attr_read_cb, NULL); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to read entity attribute: rc=%d\n", rc); + entity_attr_req_in_flight = false; + ble_process_next_entity_attribute_request(); + } + return 0; +} + +static void ble_process_next_entity_attribute_request(void) +{ + entity_attr_req_t req; + uint8_t cmd[2]; + int rc; + + if (entity_attribute_handle == 0) { + MODLOG_DFLT(WARN, "Entity Attribute handle not discovered\n"); + return; + } + + if (entity_attr_req_in_flight || !entity_attr_req_queue_pop(&req)) { + return; + } + + pending_entity_id = req.entity_id; + pending_attribute_id = req.attribute_id; + entity_attr_req_in_flight = true; + cmd[0] = req.entity_id; + cmd[1] = req.attribute_id; + + rc = ble_gattc_write_flat(req.conn_handle, + entity_attribute_handle, + cmd, + sizeof(cmd), + ams_entity_attr_write_cb, + NULL); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to write entity attribute request: rc=%d\n", rc); + entity_attr_req_in_flight = false; + ble_process_next_entity_attribute_request(); + } +} + +static void ble_request_entity_attribute(uint16_t conn_handle, uint8_t entity_id, uint8_t attribute_id) +{ + if (!entity_attr_req_queue_push(conn_handle, entity_id, attribute_id)) { + MODLOG_DFLT(WARN, + "Entity Attribute request dropped (entity=%u attr=%u): queue full (max=%u)", + entity_id, + attribute_id, + ENTITY_ATTR_REQ_QUEUE_LEN); + return; + } + + ble_process_next_entity_attribute_request(); +} + +static int ams_on_notify_subscribe(uint16_t conn_handle, + const struct ble_gatt_error *error, + struct ble_gatt_attr *attr, + void *arg) +{ + uint16_t chr_handle = (uint16_t)(uintptr_t)arg; + + MODLOG_DFLT(INFO, + "Subscribe complete; status=%d conn_handle=%d chr_handle=0x%04x", + error->status, conn_handle, chr_handle); + + if (error->status != 0) { + if (chr_handle == remote_command_handle && entity_update_handle != 0) { + MODLOG_DFLT(WARN, "Remote Command subscribe failed; continue Entity Update setup"); + return ams_discover_descriptors(conn_handle, entity_update_handle, + entity_update_def_handle, "Entity Update"); + } + return 0; + } + + if (chr_handle == remote_command_handle) { + MODLOG_DFLT(INFO, "Remote Command notifications enabled"); + ams_discover_descriptors(conn_handle, entity_update_handle, + entity_update_def_handle, "Entity Update"); + return 0; + } + + if (chr_handle == entity_update_handle && !entity_update_notify_enabled) { + entity_update_notify_enabled = true; + MODLOG_DFLT(INFO, "Entity Update notifications enabled"); + ble_subscribe_entity_updates(conn_handle); + } + + return 0; +} + +static int ams_dsc_discovered_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + uint16_t chr_val_handle, const struct ble_gatt_dsc *dsc, + void *arg) +{ + int rc = 0; + uint8_t cccd_val[2] = {0x01, 0x00}; + + if (error->status == BLE_HS_EDONE) { + return 0; + } + + if (error->status != 0) { + return error->status; + } + + if (ble_uuid_cmp(&dsc->uuid.u, BLE_UUID16_DECLARE(BLE_GATT_DSC_CLT_CFG_UUID16)) == 0) { + rc = ble_gattc_write_flat(conn_handle, + dsc->handle, + cccd_val, + sizeof(cccd_val), + ams_on_notify_subscribe, + arg); + if (rc != 0) { + MODLOG_DFLT(WARN, "Failed to subscribe (rc=%d)", rc); + if (chr_val_handle == remote_command_handle && entity_update_handle != 0) { + return ams_discover_descriptors(conn_handle, entity_update_handle, + entity_update_def_handle, "Entity Update"); + } + return rc; + } + } + + return 0; +} + +static int ams_chr_discovered_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_chr *chr, void *arg) +{ + if (error->status == BLE_HS_EDONE) { + if (remote_command_handle != 0) { + return ams_discover_descriptors(conn_handle, remote_command_handle, + remote_command_def_handle, "Remote Command"); + } + if (entity_update_handle != 0) { + return ams_discover_descriptors(conn_handle, entity_update_handle, + entity_update_def_handle, "Entity Update"); + } + return 0; + } + + if (error->status != 0) { + return error->status; + } + + if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) && + ble_uuid_cmp(&chr->uuid.u, &remote_command_uuid.u) == 0) { + remote_command_handle = chr->val_handle; + remote_command_def_handle = chr->def_handle; + MODLOG_DFLT(INFO, "Found Remote Command characteristic"); + } else if ((chr->properties & BLE_GATT_CHR_PROP_NOTIFY) && + ble_uuid_cmp(&chr->uuid.u, &entity_update_uuid.u) == 0) { + entity_update_handle = chr->val_handle; + entity_update_def_handle = chr->def_handle; + MODLOG_DFLT(INFO, "Found Entity Update characteristic"); + } else if (ble_uuid_cmp(&chr->uuid.u, &entity_attribute_uuid.u) == 0) { + entity_attribute_handle = chr->val_handle; + entity_attribute_def_handle = chr->def_handle; + MODLOG_DFLT(INFO, "Found Entity Attribute characteristic"); + } + + return 0; +} + +static int ams_service_discovered_cb(uint16_t conn_handle, const struct ble_gatt_error *error, + const struct ble_gatt_svc *svc, void *arg) +{ + if (error->status == BLE_HS_EDONE) { + if (svc_e_handle == 0) { + MODLOG_DFLT(WARN, "AMS service not found"); + } + return 0; + } + + if (error->status != 0) { + MODLOG_DFLT(WARN, "AMS service discovery failed; status=%d", error->status); + return error->status; + } + + if (svc == NULL) { + MODLOG_DFLT(WARN, "AMS service callback missing service data"); + return BLE_HS_EUNKNOWN; + } + + svc_e_handle = svc->end_handle; + return ble_gattc_disc_all_chrs(conn_handle, svc->start_handle, svc->end_handle, + ams_chr_discovered_cb, NULL); +} + +#if NIMBLE_BLE_CONNECT +static void ble_ams_print_conn_desc(struct ble_gap_conn_desc *desc) +{ + MODLOG_DFLT(INFO, "handle=%d our_ota_addr_type=%d our_ota_addr=", + desc->conn_handle, desc->our_ota_addr.type); + print_addr(desc->our_ota_addr.val); + MODLOG_DFLT(INFO, " our_id_addr_type=%d our_id_addr=", + desc->our_id_addr.type); + print_addr(desc->our_id_addr.val); + MODLOG_DFLT(INFO, " peer_ota_addr_type=%d peer_ota_addr=", + desc->peer_ota_addr.type); + print_addr(desc->peer_ota_addr.val); + MODLOG_DFLT(INFO, " peer_id_addr_type=%d peer_id_addr=", + desc->peer_id_addr.type); + print_addr(desc->peer_id_addr.val); + MODLOG_DFLT(INFO, " conn_itvl=%d conn_latency=%d supervision_timeout=%d " + "encrypted=%d authenticated=%d bonded=%d\n", + desc->conn_itvl, desc->conn_latency, + desc->supervision_timeout, + desc->sec_state.encrypted, + desc->sec_state.authenticated, + desc->sec_state.bonded); +} +#endif + +#if CONFIG_EXAMPLE_EXTENDED_ADV +static void ext_ble_ams_advertise(void) +{ + struct ble_gap_ext_adv_params params; + struct os_mbuf *data; + uint8_t instance = 0; + int rc; + + if (ble_gap_ext_adv_active(instance)) { + return; + } + + memset(¶ms, 0, sizeof(params)); + params.connectable = 1; + params.own_addr_type = own_addr_type; + params.primary_phy = BLE_HCI_LE_PHY_1M; + params.secondary_phy = BLE_HCI_LE_PHY_2M; + params.tx_power = 127; + params.sid = 1; + params.itvl_min = BLE_GAP_ADV_FAST_INTERVAL1_MIN; + params.itvl_max = BLE_GAP_ADV_FAST_INTERVAL1_MIN; + + rc = ble_gap_ext_adv_configure(instance, ¶ms, NULL, ble_ams_gap_event, NULL); + assert(rc == 0); + + data = os_msys_get_pkthdr(sizeof(ext_adv_pattern_1), 0); + assert(data); + + rc = os_mbuf_append(data, ext_adv_pattern_1, sizeof(ext_adv_pattern_1)); + assert(rc == 0); + + rc = ble_gap_ext_adv_set_data(instance, data); + assert(rc == 0); + + rc = ble_gap_ext_adv_start(instance, 0, 0); + assert(rc == 0); +} +#else +static void ble_ams_advertise(void) +{ + struct ble_gap_adv_params adv_params; + struct ble_hs_adv_fields fields; + const char *name; + int rc; + + memset(&fields, 0, sizeof(fields)); + fields.flags = BLE_HS_ADV_F_DISC_GEN | BLE_HS_ADV_F_BREDR_UNSUP; + fields.tx_pwr_lvl_is_present = 1; + fields.tx_pwr_lvl = BLE_HS_ADV_TX_PWR_LVL_AUTO; + + name = ble_svc_gap_device_name(); + fields.name = (uint8_t *)name; + fields.name_len = strlen(name); + fields.name_is_complete = 1; + + fields.appearance = BLE_AMS_APPEARANCE; + fields.appearance_is_present = 1; + + fields.uuids16 = (ble_uuid16_t *)adv_uuids16; + fields.num_uuids16 = 1; + fields.uuids16_is_complete = 1; + + rc = ble_gap_adv_set_fields(&fields); + if (rc != 0) { + MODLOG_DFLT(ERROR, "error setting advertisement data; rc=%d\n", rc); + return; + } + + memset(&adv_params, 0, sizeof(adv_params)); + adv_params.conn_mode = BLE_GAP_CONN_MODE_UND; + adv_params.disc_mode = BLE_GAP_DISC_MODE_GEN; + rc = ble_gap_adv_start(own_addr_type, NULL, BLE_HS_FOREVER, + &adv_params, ble_ams_gap_event, NULL); + if (rc != 0) { + MODLOG_DFLT(ERROR, "error enabling advertisement; rc=%d\n", rc); + } +} +#endif + +static int ble_ams_gap_event(struct ble_gap_event *event, void *arg) +{ +#if NIMBLE_BLE_CONNECT + struct ble_gap_conn_desc desc; + int rc; +#endif + + switch (event->type) { +#if NIMBLE_BLE_CONNECT + case BLE_GAP_EVENT_CONNECT: + MODLOG_DFLT(INFO, "connection %s; status=%d ", + event->connect.status == 0 ? "established" : "failed", + event->connect.status); + if (event->connect.status == 0) { + rc = ble_gap_conn_find(event->connect.conn_handle, &desc); + assert(rc == 0); + ble_ams_print_conn_desc(&desc); + } + MODLOG_DFLT(INFO, "\n"); + + if (event->connect.status != 0) { +#if CONFIG_EXAMPLE_EXTENDED_ADV + ext_ble_ams_advertise(); +#else + ble_ams_advertise(); +#endif + return 0; + } + + rc = ble_gap_security_initiate(event->connect.conn_handle); + if (rc != 0) { + MODLOG_DFLT(INFO, "Security could not be initiated, rc = %d\n", rc); + return ble_gap_terminate(event->connect.conn_handle, BLE_ERR_REM_USER_CONN_TERM); + } + return 0; + + case BLE_GAP_EVENT_DISCONNECT: + MODLOG_DFLT(INFO, "disconnect; reason=%d ", event->disconnect.reason); + ble_ams_print_conn_desc(&event->disconnect.conn); + MODLOG_DFLT(INFO, "\n"); + + ams_discovery_state_reset(); + entity_attr_req_queue_reset(); + entity_update_sub_queue_reset(); + +#if CONFIG_EXAMPLE_EXTENDED_ADV + ext_ble_ams_advertise(); +#else + ble_ams_advertise(); +#endif + return 0; + + case BLE_GAP_EVENT_CONN_UPDATE: + MODLOG_DFLT(INFO, "connection updated; status=%d ", event->conn_update.status); + rc = ble_gap_conn_find(event->conn_update.conn_handle, &desc); + assert(rc == 0); + ble_ams_print_conn_desc(&desc); + MODLOG_DFLT(INFO, "\n"); + return 0; + + case BLE_GAP_EVENT_ADV_COMPLETE: + MODLOG_DFLT(INFO, "advertise complete; reason=%d", event->adv_complete.reason); +#if CONFIG_EXAMPLE_EXTENDED_ADV + ext_ble_ams_advertise(); +#else + ble_ams_advertise(); +#endif + return 0; + + case BLE_GAP_EVENT_ENC_CHANGE: + MODLOG_DFLT(INFO, "encryption change event; status=%d ", event->enc_change.status); + rc = ble_gap_conn_find(event->enc_change.conn_handle, &desc); + assert(rc == 0); + ble_ams_print_conn_desc(&desc); + MODLOG_DFLT(INFO, "\n"); + + if (event->enc_change.status != 0) { + MODLOG_DFLT(WARN, "Encryption failed; skip AMS service discovery"); + return 0; + } + + MODLOG_DFLT(INFO, "Connection secured\n"); + ams_discovery_state_reset(); + entity_attr_req_queue_reset(); + entity_update_sub_queue_reset(); + + rc = ble_gattc_disc_svc_by_uuid(event->enc_change.conn_handle, &APPLE_MS_UUID.u, + ams_service_discovered_cb, NULL); + if (rc != 0) { + return rc; + } + return 0; + + case BLE_GAP_EVENT_NOTIFY_TX: + MODLOG_DFLT(INFO, "notify_tx event; conn_handle=%d attr_handle=%d " + "status=%d is_indication=%d", + event->notify_tx.conn_handle, + event->notify_tx.attr_handle, + event->notify_tx.status, + event->notify_tx.indication); + return 0; + + case BLE_GAP_EVENT_NOTIFY_RX: + if (event->notify_rx.attr_handle == remote_command_handle) { + ble_receive_remote_command_notification(event->notify_rx.om->om_data, + event->notify_rx.om->om_len); + } else if (event->notify_rx.attr_handle == entity_update_handle) { + if (event->notify_rx.om->om_len < 3) { + MODLOG_DFLT(WARN, "Entity update notification too short (%d)", + event->notify_rx.om->om_len); + return 0; + } + ble_receive_entity_update(event->notify_rx.om->om_data, event->notify_rx.om->om_len); + if (event->notify_rx.om->om_data[2] & EntityUpdateFlagTruncated) { + ble_request_entity_attribute(event->notify_rx.conn_handle, + event->notify_rx.om->om_data[0], + event->notify_rx.om->om_data[1]); + } + } else { + MODLOG_DFLT(INFO, "unknown handle, receive notify value"); + } + return 0; + + case BLE_GAP_EVENT_SUBSCRIBE: + MODLOG_DFLT(INFO, "subscribe event; conn_handle=%d attr_handle=%d " + "reason=%d prevn=%d curn=%d previ=%d curi=%d\n", + event->subscribe.conn_handle, + event->subscribe.attr_handle, + event->subscribe.reason, + event->subscribe.prev_notify, + event->subscribe.cur_notify, + event->subscribe.prev_indicate, + event->subscribe.cur_indicate); + return 0; + + case BLE_GAP_EVENT_MTU: + MODLOG_DFLT(INFO, "mtu update event; conn_handle=%d cid=%d mtu=%d\n", + event->mtu.conn_handle, + event->mtu.channel_id, + event->mtu.value); + return 0; + + case BLE_GAP_EVENT_REPEAT_PAIRING: + rc = ble_gap_conn_find(event->repeat_pairing.conn_handle, &desc); + assert(rc == 0); + ble_store_util_delete_peer(&desc.peer_id_addr); + return BLE_GAP_REPEAT_PAIRING_RETRY; + + case BLE_GAP_EVENT_PASSKEY_ACTION: + ESP_LOGI(tag, "PASSKEY_ACTION_EVENT started"); + struct ble_sm_io pkey = {0}; + int key = 0; + + if (event->passkey.params.action == BLE_SM_IOACT_DISP) { + pkey.action = event->passkey.params.action; + pkey.passkey = 123456; + ESP_LOGI(tag, "Enter passkey %" PRIu32 " on the peer side", pkey.passkey); + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc); + } else if (event->passkey.params.action == BLE_SM_IOACT_NUMCMP) { + ESP_LOGI(tag, "Passkey on device's display: %" PRIu32, event->passkey.params.numcmp); + ESP_LOGI(tag, "Accept or reject the passkey through console in this format -> key Y or key N"); + pkey.action = event->passkey.params.action; + if (scli_receive_key(&key)) { + pkey.numcmp_accept = key; + } else { + pkey.numcmp_accept = 0; + ESP_LOGE(tag, "Timeout! Rejecting the key"); + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc); + } else if (event->passkey.params.action == BLE_SM_IOACT_OOB) { + static uint8_t tem_oob[16] = {0}; + pkey.action = event->passkey.params.action; + for (int i = 0; i < 16; i++) { + pkey.oob[i] = tem_oob[i]; + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc); + } else if (event->passkey.params.action == BLE_SM_IOACT_INPUT) { + ESP_LOGI(tag, "Enter the passkey through console in this format-> key 123456"); + pkey.action = event->passkey.params.action; + if (scli_receive_key(&key)) { + pkey.passkey = key; + } else { + pkey.passkey = 0; + ESP_LOGE(tag, "Timeout! Passing 0 as the key"); + } + rc = ble_sm_inject_io(event->passkey.conn_handle, &pkey); + ESP_LOGI(tag, "ble_sm_inject_io result: %d", rc); + } + return 0; +#endif + default: + break; + } + + return 0; +} + +static void ble_ams_on_reset(int reason) +{ + MODLOG_DFLT(ERROR, "Resetting state; reason=%d\n", reason); +} + +static void ble_ams_on_sync(void) +{ + int rc; + + rc = ble_hs_util_ensure_addr(0); + assert(rc == 0); + + rc = ble_hs_id_infer_auto(0, &own_addr_type); + if (rc != 0) { + MODLOG_DFLT(ERROR, "error determining address type; rc=%d\n", rc); + return; + } + + uint8_t addr_val[6] = {0}; + rc = ble_hs_id_copy_addr(own_addr_type, addr_val, NULL); + + MODLOG_DFLT(INFO, "Device Address: "); + print_addr(addr_val); + MODLOG_DFLT(INFO, "\n"); + +#if CONFIG_EXAMPLE_EXTENDED_ADV + ext_ble_ams_advertise(); +#else + ble_ams_advertise(); +#endif +} + +static void ble_ams_host_task(void *param) +{ + ESP_LOGI(tag, "BLE Host Task Started"); + nimble_port_run(); + nimble_port_freertos_deinit(); +} + +void app_main(void) +{ + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + ret = nimble_port_init(); + if (ret != ESP_OK) { + ESP_LOGE(tag, "Failed to init nimble %d", ret); + return; + } + + ble_hs_cfg.reset_cb = ble_ams_on_reset; + ble_hs_cfg.sync_cb = ble_ams_on_sync; + ble_hs_cfg.store_status_cb = ble_store_util_status_rr; + + ble_hs_cfg.sm_bonding = 1; + ble_hs_cfg.sm_our_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC; + ble_hs_cfg.sm_their_key_dist |= BLE_SM_PAIR_KEY_DIST_ENC; + ble_hs_cfg.sm_sc = 0; + + int rc = ble_svc_gap_device_name_set("nimble-ams"); + assert(rc == 0); + rc = ble_svc_gap_device_appearance_set(BLE_AMS_APPEARANCE); + assert(rc == 0); + + ble_store_config_init(); + + nimble_port_freertos_init(ble_ams_host_task); + + /* Initialize command line interface to accept input from user */ + rc = scli_init(); + if (rc != ESP_OK) { + ESP_LOGE(tag, "scli_init() failed"); + } +} diff --git a/examples/bluetooth/nimble/ble_ams/sdkconfig.defaults b/examples/bluetooth/nimble/ble_ams/sdkconfig.defaults new file mode 100644 index 00000000000..57da93a0ff2 --- /dev/null +++ b/examples/bluetooth/nimble/ble_ams/sdkconfig.defaults @@ -0,0 +1,14 @@ +# Override some defaults so BT stack is enabled +# in this example + +# +# BT config +# +CONFIG_BT_ENABLED=y +CONFIG_BTDM_CTRL_MODE_BLE_ONLY=y +CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=n +CONFIG_BTDM_CTRL_MODE_BTDM=n +CONFIG_BT_BLUEDROID_ENABLED=n +CONFIG_BT_NIMBLE_ENABLED=y +CONFIG_BT_NIMBLE_NVS_PERSIST=y +CONFIG_BT_NIMBLE_SVC_GAP_APPEARANCE=960