Design Specification
The following charts focus on the design specification of Bluetooth Audio Transceiver. It is written to help users easily and completely understand the code flow of the application. The following topics are included:
Firmware Initialization and Configuration (Includes flash and FTL).
Memory (Includes Global and Heap).
IO.
OTA.
DLPS.
Code Flow (Including main functions of typical scenarios, such as transmitter, receiver, transceiver, transmitter_mp3 and intergrated transceiver).
Before studying the content of this article, it is highly recommended to first study the following documents:
Software Architecture
Please refer to Software Architecture for more details.
Firmware Initialization and Configuration
Initialization and Main Application Loop
This procedure develops an application for Bluetooth/Bluetooth Low Energy audio on the RTL87x3E series platforms. The main
procedure is to be carried out in the following steps:
Call the
ram_config()
API to configure DSP to share 80KB RAM with MCU.Call the
log_module_trace_set()
API to disable some trace logs during system initialization.Call the
APP_PRINT_INFO2()
API to print the wake-up reason, compile time, and restart reason.Call the
os_msg_queue_create()
API to create a message and event queue for task communication.Call the
app_init_timer()
API to initialize APP timer.Call the
pm_cpu_freq_init()
API to initialize CPU frequency module.Call the
app_cfg_init()
API to initialize the configuration.Call the
board_init()
API to initialize PINMUX and pad settings.Call the
app_bt_gap_init()
API to initialize GAP parameters of Bluetooth and Bluetooth Low Energy.-
Call the
framework_init()
API to initialize the System Manager, Remote Manager, Bluetooth Manager and Audio Manager. This is done using the following steps:Call the
sys_mgr_init()
API to initialize system.Call the
remote_mgr_init()
API to initialize remote.Call the
bt_mgr_init()
API to initialize Bluetooth.Call the
audio_mgr_init()
API to initialize audio.
Call the
app_ota_init()
API to initialize OTA.-
Call the
driver_init()
API to initialize the drivers. This is done using the following steps:Call the
app_adp_init()
API to initialize adaptor.Call the
app_iap_cp_hw_init()
API to initialize I2C for CP chip.Call the
app_console_init()
API to initialize console.Call the
app_charger_init()
API to initialize charger.Call the
app_line_in_driver_init()
API to initialize Line-in.Call the
app_amp_init()
API to initialize AMP.Call the
app_qdec_driver_init()
API to initialize QDEC.Call the
app_dlps_system_wakeup_clear_rtc_int()
API to initialize RTC.Call the
ext_flash_init()
API to initialize external flash.
Call the
app_auto_power_off_init()
API to initialize the auto power off module.Call the
app_audio_init()
API to initialize audio, such as initializing EQ, setting VP language, setting volume, etc.Call the
app_mmi_init()
API to initialize MMI.Call the
app_test_init()
API to initialize test.Call the
app_gap_init()
API to initialize GAP.Call the
app_ble_gap_init()
API to initialize Bluetooth Low Energy GAP.Call the
app_bt_policy_init()
API to initialize Bluetooth policy.Call the
app_ble_client_init()
API to initialize LE client.Call the
gatt_svc_init()
API to initialize GATT service.-
Call the profile initialize APIs to initialize Bluetooth profiles. This is done using the following steps:
Call the
app_hfp_init()
API to initialize HFP.Call the
app_avrcp_init()
API to initialize AVRCP.Call the
app_a2dp_init()
API to initialize A2DP.Call the
app_sdp_init()
API to initialize SDP.Call the
app_spp_init()
API to initialize SPP.Call the
app_pbap_init()
API to initialize PBAP.Call the
app_hid_init()
API to initialize HID.Call the
app_iap_init()
API to initialize iAP.
-
Call the
app_ble_service_init()
APIs to initialize Bluetooth Low Energy services. This is done using the following steps:Call the
transmit_srv_add()
API to initialize transmit service.Call the
ota_add_service()
API to initialize OTA service.Call the
bas_add_service()
API to initialize battery service.
Call the
app_lea_acc_profile_init()
API to initialize LE Audio service and profiles.Call the
os_task_create()
API to create a new task and add it to the list of tasks that are ready to run.Call the
os_sched_start()
API to start the RTOS kernel scheduler.
Configuration
Some parameters are configured through the MCUConfig Tool, and once configured, they are saved in the APP Config image, the location and size of which is specified by the flash map file. Some parameters need to be saved to be used the next time the system boots up, and these configurations are saved in the FTL.
Configuration in Flash
The flash map file is located in sdk\bin\rtl87x3e\flash_map_config\4M\flash_4M\flash_map.h
.
The corresponding configurations are as follows.
#define BANK0_APP_CFG_ADDR 0x021F0000
#define BANK0_APP_CFG_SIZE 0x00002000 *//8K Bytes*
The APP Config image, which is 8KB (Defined as BANK0_APP_CFG_SIZE
), contains the following sections that are pre-allocated fields and cannot be modified.
#define APP_CONFIG_OFFSET 0
#define APP_CONFIG_SIZE 1024
#define APP_LED_OFFSET (APP_CONFIG_OFFSET + APP_CONFIG_SIZE)
#define APP_LED_SIZE 512
#define SYS_CONFIG_OFFSET (APP_LED_OFFSET + APP_LED_SIZE)
#define SYS_CONFIG_SIZE 512
#define APP_CONFIG2_OFFSET (SYS_CONFIG_OFFSET + SYS_CONFIG_SIZE)
#define APP_CONFIG2_SIZE 512
#define TONE_DATA_OFFSET 4096 //Rsv 4K for APP parameter for better flash control
#define TONE_DATA_SIZE 3072
The variables corresponding to APP_CONFIG_OFFSET
and APP_CONFIG_SIZE
are defined as follows.
Only device_name_legacy_default
and device_name_le_default
are generated through MCUConfig Tool configuration, while all other fields are configured and initialized through app_cfg_init()
.
typedef struct
{
uint32_t sync_word;
uint8_t device_name_legacy_default[DEVICE_NAME_MAX_LEN]; //this field offset is fixed, SHALL NOT modify
uint8_t device_name_le_default[DEVICE_NAME_MAX_LEN]; //this field offset is fixed, SHALL NOT modify
...
...
} T_APP_CFG_CONST;
Configuration in FTL
The areas used by FTL are defined as follows, and users can extend new areas according to their product needs.
//FTL start
#define APP_RW_DATA_ADDR 3072
#define APP_RW_DATA_SIZE 360
#define FACTORY_RESET_OFFSET 124
#define GCSS_ATT_TBL_INFO_ADDR (APP_RW_DATA_ADDR + APP_RW_DATA_SIZE)
#define GCSS_ATT_TBL_INFO_SIZE (30 * 16)
#define APP_FINDMY_INFO_ADDR (GCSS_ATT_TBL_INFO_ADDR + GCSS_ATT_TBL_INFO_SIZE)
#define APP_FINDMY_INFO_SIZE 320
//FTL end
The variables corresponding to APP_RW_DATA_ADDR
and APP_RW_DATA_SIZE
are defined as follows.
typedef struct
{
T_APP_CFG_NV_HDR hdr;
//offset: 8
uint8_t device_name_legacy[40];
uint8_t device_name_le[40];
uint8_t le_single_random_addr[6];
...
...
} T_APP_CFG_NV;
Memory
The available size of Global and Heap memory used by the application is configured in:
sdk\board\evb\bt_audio_trx\inc\rtl87x3e\mem_config.h
.
Users can adjust the parameters according to the product memory usage requirements.
MCU Memory
#define APP_RAM_TEXT_SIZE (5*1024)
#define APP_GLOBAL_SIZE (14*1024)
How to Check the Memory Usage?
The use of Global and TEXT RAM can be confirmed by the following file:
sdk\board\evb\bt_audio_trx\bin\rtl87x3e\flash_4M_dualbank\bank0\bt_audio_trx_bank0.map
.
Execution Region RAM_TEXT (Exec base: 0x00208800, Load base: 0x0214351c, Size: 0x000014a0, Max: 0x00002800, ABSOLUTE)
Execution Region RAM_GLOBAL (Exec base: 0x002c0000, Load base: 0x02139868, Size: 0x000021b8, Max: 0x00003800, ABSOLUTE)
Execution Region SHARE_RAM_DATA (Exec base: 0x00300000, Load base: 0x0213a0ec, Size: 0x00000000, Max: 0x00002800, ABSOLUTE)
The use of Heap RAM can be confirmed by the following log.
APP_PRINT_WARN1("app_task unused memory: %d", mem_peek());
Flash
APP Image in Flash Map
The flash map file is located in sdk\bin\rtl87x3e\flash_map_config\4M\flash_4M\flash_map.h
. The maximum size of the APP image is defined by BANK0_APP_SIZE
.
#define BANK0_APP_ADDR 0x02099000
#define BANK0_APP_SIZE 0x000CD000 *//820K Bytes*
How to Check APP Image Size Used?
The actual size of the APP image used can be confirmed by the following file:
sdk\board\evb\bt_audio_trx\bin\rtl87x3e\flash_4M_dualbank\bank0\bt_audio_trx_bank0.map
.
Load Region LOAD_FLASH **//Base: 0x0086e000, Size: 0x0008eeb8, Max: 0x000c8000, ABSOLUTE*
IO
IO Resource
GPIO
In some scenarios that do not need to play voice prompts and tones, some analog pins can be released to be used as
GPIO. Taking the LOUT_P
pin as an example, the following three steps are mainly required:
-
Remove the SPK setting.
Use the MCUConfig Tool to remove the SPK setting of the audio on the Audio Route page. This ensures the pad of
LOUT_P
will not be configured.Use GPIO Pin 1
-
Mute voice prompts and tones.
Open the macro of
F_APP_DISABLE_NOTIFICATION_SUPPORT
inapp_flags.h
.#define F_APP_USER_EQ_SUPPORT 1 //open #define F_APP_DISABLE_NOTIFICATION_SUPPORT 1 #define F_APP_MONITOR_MEMORY_AND_TIMER 0
-
Set AVCC driver.
Set AVCC driver active by clicking
.Use GPIO Pin 2
DMA
DMA Resources
Currently, there are a total of 9 DMA channels available for use. Some channels are already utilized or reserved for DSP and log UART purposes. There are 3 channels currently unoccupied. In addition to this, the DSP reserved channel 8 is not used by DSP and is also available for APP use.
Channel |
State |
---|---|
GDMA_Channel 0 |
Idle |
GDMA_Channel 1 |
Idle |
GDMA_Channel 2 |
DSP Reserved. |
GDMA_Channel 3 |
DSP Reserved. |
GDMA_Channel 4 |
DSP Reserved. |
GDMA_Channel 5 |
DSP Reserved. |
GDMA_Channel 6 |
Idle |
GDMA_Channel 7 |
Log UART |
GDMA_Channel 8 |
DSP reserved, can be used for APP. |
DMA Current Usage
-
UART with ACI Host CLI Tool.
For MP3 Transmitter Application (
F_APP_BT_AUDIO_TRANSMITTER_MP3_DEMO_SUPPORT
), due to substantial audio data stream, the UART will dynamically request 2 channels.static void app_console_uart_init(void) { ... console_uart_config.uart_rx_dma_enable = false; console_uart_config.uart_tx_dma_enable = false; #if F_APP_BT_AUDIO_TRANSMITTER_MP3_DEMO_SUPPORT console_uart_config.uart_tx_dma_enable = true; console_uart_config.uart_rx_dma_enable = true; #endif ... }
-
SPI.
In the case of the Transceiver Application (
F_APP_BT_AUDIO_TRANSCEIVER_DEMO_SUPPORT
), SPI TX dynamically requests 1 channel, while SPI RX utilizes channel 8, which is reserved for DMA purposes.// SPI TX void app_spi_master_init(void) { ... if (!GDMA_channel_request(&spi_tx_dma_ch_num, app_spi_master_tx_dma_handler, true)) { APP_PRINT_ERROR0("app_spi_master_init: tx channel request fail"); return; } ... }
// SPI RX static uint8_t spi_rx_dma_ch_num = 8; #define SPI_RX_DMA_CHANNEL_NUM spi_rx_dma_ch_num static void app_spi_master_dma_rx_init(void *readbuf) { ... GDMA_InitStruct.GDMA_ChannelNum = SPI_RX_DMA_CHANNEL_NUM; ... GDMA_INTConfig(SPI_RX_DMA_CHANNEL_NUM, GDMA_INT_Transfer, ENABLE); }
Key
The key module supports two types of keys: MFB and GPIO keys. On the EVB, it can accommodate up to 1 MFB key and 8 GPIO keys. Typically, MFB is utilized for power on and power off functionalities, while GPIO keys are mapped to MMI features.
The configuration for key module functionalities is defined in app_key_cfg.c
. Users have the flexibility to configure various aspects, including enabling or disabling key functionality, mapping keys to GPIO, and associating keys with MMI functions.
Refer to T_MMI_ACTION
in app_mmi.h
for a comprehensive list of all supported MMI interfaces. This enumeration provides details on the various MMI actions that can be utilized for configuring and mapping functionalities to different key scenarios.
typedef enum
{
MMI_NULL = 0x00,
MMI_HF_END_OUTGOING_CALL = 0x02,
...
MMI_TOTAL
} T_MMI_ACTION;
MFB
To enable the MFB functionality, set app_key_cfg.mfb_replace_key0
to 1.
void app_key_cfg_init(void)
{
...
app_key_cfg.mfb_replace_key0 = 1;
...
}
In the power off state, different durations of long-pressing the MFB key can achieve power on, enter pairing mode, and factory reset. Modify the following configurations for specific duration definitions (The unit is 100 ms).
void app_key_cfg_init(void)
{
...
app_key_cfg.key_power_on_interval = 25;
app_key_cfg.key_enter_pairing_interval = 60;
app_key_cfg.key_factory_reset_interval = 90;
...
}
In the power on state, a long-pressing of the MFB key allows for shutting down. The duration to trigger the shutdown key can be configured through key_power_off_interval
.
void app_key_cfg_init(void)
{
...
app_key_cfg.key_power_off_interval = 30;
...
}
GPIO Key
The GPIO key system allows for flexible configuration of keys mapped to specific GPIOs. Configuring these keys properly is essential for achieving desired hardware control effects.
Hardware Configurations
To utilize GPIO Keys, first, map the keys to the required GPIOs. This can be achieved by modifying key_pinmux[8]
, where these GPIOs can be configured for KEY0 to KEY7. Enable the keys through key_enable_mask
. For instance, configure the GPIO for KEY1 as P0_1
, KEY2 as P1_0
, and enable both KEY1 and KEY2.
void app_key_cfg_init(void)
{
...
uint8_t key_pinmux[8] = {0xFF, P0_1, P1_0, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
memcpy(app_key_cfg.key_pinmux, key_pinmux, sizeof(key_pinmux));
app_key_cfg.key_enable_mask = KEY1_MASK | KEY2_MASK;
...
}
It’s essential to note that keys are default low-active. Depending on the circuitry, if the key is high-active, include the corresponding key mask in key_high_active_mask
.
void app_key_cfg_init(void)
{
...
app_key_cfg.key_high_active_mask = KEY2_MASK | KEY4_MASK | KEY6_MASK;
...
}
Operation Modes of Keys
GPIO keys support various operation modes, categorized based on key scenarios into short click, long press, and multiple clicks. They are further distinguished by the number of keys as single key and combine key. Specifically, there are six operation modes:
Mode 1: Single key + short click.
Mode 2: Single key + long press.
Mode 3: Single key + multiple clicks (2/3/4/5 clicks).
Mode 4: Combine key + short click.
Mode 5: Combine key + long press.
Mode 6: Combine key + multiple clicks (2/3/4/5 clicks).
The trigger duration for a long press is configured through key_long_press_interval
, and the detection duration for multiple clicks is configured through key_multi_click_interval
.
void app_key_cfg_init(void)
{
...
app_key_cfg.key_long_press_interval = 20;
app_key_cfg.key_multi_click_interval = 3;
...
}
For operation mode 2, long repeat functionality is supported. This means that when holding down
the key for an extended period, the corresponding MMI function can be triggered multiple times. The
interval for triggering is configured through key_long_press_repeat_interval
. The corresponding
keys are configured through key_long_press_repeat_mask
.
void app_key_cfg_init(void)
{
...
app_key_cfg.key_long_press_repeat_interval = 4;
app_key_cfg.key_long_press_repeat_mask = KEY_NULL;
...
}
The mapping of different operation modes to MMI functions is maintained through tables. The modes 1 and 2 are part of the basic functionality maintained by the key table, while modes 2 to 6 are considered hybrid functionality and are maintained by the hybrid key table.
Key Table
The mapping of different operation modes to MMI functions is maintained through tables. The modes 1 and 2 are part of the basic functionality maintained by the key table, while modes 2 to 6 are considered hybrid functionality and are maintained by the hybrid key table.
As depicted in the figure, the key table is a multidimensional array of size 2 x 9 x 8. key_table[0]
corresponds to the short click table, and key_table[1]
corresponds to the long press table. Different MMI functions can be assigned to keys under different call statuses.

Key Table
There are a total of 9 call statuses, defined as follows.
enum key_table_call_status
{
CALL_IDLE,
VOICE_DIAL,
INCOMING_CALL,
OUT_GOING_CALL,
CALL_ACTIVE,
CALL_ACTIVE_WITH_CALL_WAITING,
CALL_ACTIVE_WITH_CALL_HOLD,
MULTI_CALL_ACTIVE_WITH_CALL_WAITING,
MULTI_CALL_ACTIVE_WITH_CALL_HOLD,
};
For example, MMI_DEV_MIC_VOL_UP
can be configured to be triggered by a short press of KEY1 in
the CALL_IDLE
status and MMI_DEV_MIC_VOL_DOWN
can be triggered by a long press of KEY1 in
the CALL_IDLE
status.
void app_key_cfg_init(void)
{
...
// sample of single key short click
app_key_cfg.key_table[SHORT_CLICK][CALL_IDLE][KEY_1] = MMI_DEV_MIC_VOL_UP;
// sample of single key long press
app_key_cfg.key_table[LONG_PRESS][CALL_IDLE][KEY_1] = MMI_DEV_MIC_VOL_DOWN;
...
}
Hybrid Key Table
As illustrated in the figure below, the hybrid key table is a multidimensional array of size 9 x 8. Different MMI functions can be assigned to different hybrid types under various call statuses.
Hybrid types consist of a hybrid mask and hybrid scenario. The hybrid mask defines the key
combination, and the hybrid scenario defines the operation mode. All hybrid types are defined
through hybrid_key_mapping
, supporting a maximum of 8 hybrid types.

Hybrid Key Table
For example, configure the following:
Trigger
MMI_DEV_MIC_VOL_DOWN
by operatingHYBRID_TYPE_0
(double-clicking KEY1) in theCALL_IDLE
status.Trigger
MMI_START_ROLESWAP
by operatingHYBRID_TYPE_1
(triple-clicking KEY1) in theCALL_IDLE
status.-
Trigger
MMI_DEV_MIC_VOL_DOWN
by simultaneously short pressing KEY1 and KEY2 (HYBRID_TYPE_2
) while in theCALL_IDLE
status.void app_key_cfg_init(void) { ... // sample of double click app_key_cfg.hybrid_key_table[CALL_IDLE][HYBRID_TYPE_0] = MMI_DEV_MIC_VOL_DOWN; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_0][HYBRID_MASK] = KEY1_MASK; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_0][HYBRID_SCENARIO] = HYBRID_KEY_2_CLICK; // sample of triple click app_key_cfg.hybrid_key_table[CALL_IDLE][HYBRID_TYPE_1] = MMI_START_ROLESWAP; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_1][HYBRID_MASK] = KEY1_MASK; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_1][HYBRID_SCENARIO] = HYBRID_KEY_3_CLICK; // sample of combine key single click app_key_cfg.hybrid_key_table[CALL_IDLE][HYBRID_TYPE_2] = MMI_DEV_MIC_VOL_DOWN; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_2][HYBRID_MASK] = KEY1_MASK | KEY2_MASK; app_key_cfg.hybrid_key_mapping[HYBRID_TYPE_2][HYBRID_SCENARIO] = HYBRID_KEY_SHORT_PRESS; ... }
OTA
Refer to OTA for details.
DLPS
Please refer to Low Power Mode for details.
Code Flow
Basic Function
Power On/Off and Factory Reset
Functions such as power on, power off, and factory reset are realized by transmitting MMI commands through UART. These commands are centrally processed in app_cmd_general.c
, and the used APIs are introduced as follows.
void app_cmd_general_cmd_handle(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path,
uint8_t app_idx, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
switch (cmd_id)
{
case CMD_MMI:
...
app_mmi_handle_action(cmd_ptr[3]);
...
}
break;
}
APP handles key messages in function app_mmi_handle_action()
. The user triggers the specific key process, then the device would perform the related action.
If the device performs power on, it will call sys_mgr_power_on()
after judging the device state under case MMI_DEV_POWER_ON
.
void app_mmi_handle_action(uint8_t action)
{
...
switch (action)
{
case MMI_DEV_POWER_ON:
{
if (app_db.device_state == APP_DEVICE_STATE_OFF_ING)
{
if (app_bt_policy_get_b2s_connected_num())
{
app_bt_policy_disconnect_all_link();
}
app_mmi_modify_reboot_check_times(REBOOT_CHECK_POWER_ON_MAX_TIMES);
}
else if (app_db.device_state == APP_DEVICE_STATE_OFF)
{
if (!app_db.is_long_press_power_on_play_vp && !mp_hci_test_mode_is_running())
{
app_audio_tone_type_play(TONE_POWER_ON, false, false);
}
app_db.is_long_press_power_on_play_vp = false;
sys_mgr_power_on();
}
}
break;
...
}
}
If the device performs power off, it will call app_mmi_power_off()
which encapsulates sys_mgr_power_off()
after judging device state under case MMI_DEV_POWER_OFF
.
case MMI_DEV_POWER_OFF:
{
...
if (app_db.device_state == APP_DEVICE_STATE_ON)
{
...
app_device_state_change(APP_DEVICE_STATE_OFF_ING);
app_dlps_disable(APP_DLPS_ENTER_CHECK_WAIT_RESET);
...
app_stop_timer(&timer_idx_reboot_check);
app_mmi_reboot_check_timer_start(500);
app_timer_register_pm_excluded(&timer_idx_reboot_check);
if (mp_hci_test_mode_is_running())
{
mp_hci_test_mmi_handle_action(action);
}
else
{
app_sniff_mode_disable_all();
//power off
app_mmi_power_off();
}
}
}
break;
If the device performs a factory reset, it will call sys_mgr_power_off()
after checking the factory reset behavior.
case MMI_DEV_FACTORY_RESET:
{
app_dlps_disable(APP_DLPS_ENTER_CHECK_WAIT_RESET);
app_mmi_check_factory_reset_behavior(action);
...
app_stop_timer(&timer_idx_reboot_check);
app_mmi_reboot_check_timer_start(500);
app_db.waiting_factory_reset = true;
if (app_db.device_state == APP_DEVICE_STATE_ON)
{
app_device_state_change(APP_DEVICE_STATE_OFF_ING);
app_sniff_mode_disable_all();
sys_mgr_power_off();
}
}
Bluetooth Connection and Linkback
The device utilizes CMD_BT_CREATE_CONNECTION
to establish a Bluetooth connection, and CMD_XM_USER_CFM_REQ
to confirm the connection request from another device. APIs that can be referred to are as follows.
The APIs called in app_cmd_br.c
are declared in br_cmd_handle()
, developers can enter the profiles to be connected in ACI Host CLI Tool (Please refer to Connection and Linkback Function). The specific profile mask can refer to app_link_util.h
, which can be adjusted according to needs.
void br_cmd_handle(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t app_idx,
uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
switch (cmd_id)
{
...
case CMD_BT_CREATE_CONNECTION:
{
struct
{
uint16_t cmd_id;
uint32_t profile_mask;
uint8_t addr[6];
} __attribute__((packed)) *CMD = (typeof(CMD))cmd_ptr;
...
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
linkback_todo_queue_insert_normal_node(CMD->addr, CMD->profile_mask,
app_cfg_const.timer_linkback_timeout * 1000, 0);
linkback_run();
}
break;
...
}
}
After inserting a normal linkback node, the linkback node information will be checked, then the profiles to be connected will be placed in
p_item->linkback_node.plan_profs
in linkback_todo_queue_all_node()
.
void linkback_todo_queue_insert_normal_node(uint8_t *bd_addr, uint32_t plan_profs,
uint32_t retry_timeout, bool is_group_member)
{
T_LINKBACK_NODE_ITEM *p_item;
p_item = linkback_todo_queue_find_node_item(bd_addr);
...
linkback_todo_queue_all_node();
}
The linkback process runs and checks the status of linkback_active_node
. After confirming that the node exists and the doing profile is not 0, linkback_profile_connect_start()
will continue to be executed.
Then, the linkback_active_node_step_suc_adjust_remain_profs()
or linkback_active_node_step_fail_adjust_remain_profs()
will be returned according to the result of linkback_profile_search_start()
.
void linkback_run(void)
{
T_LINKBACK_NODE node;
uint32_t profs;
...
if (b2s_connected_find_node(linkback_active_node.linkback_node.bd_addr, &profs))
{
if (profs & linkback_active_node.doing_prof)
{
ENGAGE_PRINT_TRACE1("linkback_run: prof 0x%08x, already connected",
linkback_active_node.doing_prof);
linkback_active_node_step_suc_adjust_remain_profs();
goto RETRY;
}
...
}
...
if (linkback_profile_is_need_search(linkback_active_node.doing_prof))
{
if (!linkback_profile_search_start(linkback_active_node.linkback_node.bd_addr,
linkback_active_node.doing_prof, linkback_active_node.linkback_node.is_special))
{
linkback_active_node_step_fail_adjust_remain_profs();
goto RETRY;
}
...
}
else
{
if (linkback_profile_connect_start(linkback_active_node.linkback_node.bd_addr,
linkback_active_node.doing_prof, &linkback_active_node.linkback_conn_param))
{
goto EXIT;
}
else
{
linkback_active_node_step_fail_adjust_remain_profs();
goto RETRY;
}
}
}
After the link is established, the Bluetooth stack will report BT_EVENT_LINK_USER_CONFIRMATION_REQ
, which requires the device to confirm. The device needs to enter CMD_XM_USER_CFM_REQ
to confirm the connection request in app_customer_bt_handle_cmd()
.
Note
When cfm
is 0x01, it means confirm request. When cfm
is 0x00, it means reject request.
void app_customer_bt_handle_cmd(uint8_t app_idx, T_CMD_PATH cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
switch (cmd_id)
{
...
case CMD_XM_USER_CFM_REQ:
{
...
cfm = cmd_ptr[3];
if (cfm == 0x00)
{
gap_br_user_cfm_req_cfm(app_db.br_link[app_index].bd_addr, GAP_CFM_CAUSE_REJECT);
...
}
else if (cfm == 0x01)
{
gap_br_user_cfm_req_cfm(app_db.br_link[app_index].bd_addr, GAP_CFM_CAUSE_ACCEPT);
}
}
break;
}
}
Bluetooth Low Energy
All Bluetooth Low Energy commands are processed in app_cmd_ble_handle
in app_cmd_ble.c
, while events are processed by a number of functions in app_rpt_ble.c
.
Advertising
Advertising is the first step to Bluetooth Low Energy connection (As a Peripheral). Furthermore, it can convey a bit of information by itself with scan response:
-
Create an advertisement.
Go through the command
CMD_XM_LE_ADV_CREATE
inapp_cmd_ble.c
, it calls static functionadv_create
. Theadv_create
function callsble_ext_adv_mgr_init_adv_params()
which creates an advertisement in the lower layer.It also calls
ble_ext_adv_mgr_register_callback()
to get advertisement states from the lower layer. In the end,adv_create
saves advertisement information inp_elem
toble.advs.q. Handle
is the most important member ofp_elem
which was always referenced as a parameter for other commands.static uint8_t adv_create(ADV_PARAMS *p_params) { ADV_Q_ELEM *p_elem = calloc(1, sizeof(*p_elem)); if (p_params->adv_data) { memcpy(p_elem->adv_data, p_params->adv_data, p_params->adv_data_len); p_elem->adv_data_len = p_params->adv_data_len; } if (p_params->scan_rsp) { memcpy(p_elem->scan_rsp, p_params->scan_rsp, p_params->scan_rsp_len); p_elem->scan_rsp_len = p_params->scan_rsp_len; } p_elem->state = BLE_EXT_ADV_MGR_ADV_DISABLED; ble_ext_adv_mgr_init_adv_params(&p_elem->handle, p_params->adv_prop, p_params->interval_min, p_params->interval_max, p_params->own_addr_type, p_params->peer_addr_type, p_params->peer_addr, GAP_ADV_FILTER_ANY, p_elem->adv_data_len, p_elem->adv_data, p_elem->scan_rsp_len, p_elem->scan_rsp, p_params->own_addr); APP_PRINT_TRACE2("app_customer_ble adv_create: adv_data %b, le_single_random_addr %s", TRACE_BINARY(sizeof(p_elem->adv_data), p_elem->adv_data), TRACE_BDADDR(app_cfg_nv.le_single_random_addr)); /* set adv event handle callback for further process*/ ble_ext_adv_mgr_register_callback(adv_callback, p_elem->handle); os_queue_in(&ble.advs.q, p_elem); //save adv information by p_elem in ble.advs.q return p_elem->handle; }
-
Start an advertisement.
Glance over the
CMD_XM_LE_START_ADVERTISING
command, and you will find a wrapper function calledadv_start
, which requires a handle and triggersble_ext_adv_mgr_enable
to enable advertising.static bool adv_start(uint8_t handle, uint16_t duration_10ms) { ADV_Q_ELEM *p_adv_elem = find_adv_elem_by_hdl(handle); if (p_adv_elem) { if (p_adv_elem->state == BLE_EXT_ADV_MGR_ADV_DISABLED) { if (ble_ext_adv_mgr_enable(handle, duration_10ms) == GAP_CAUSE_SUCCESS) { return true; } else { return false; } } else { APP_PRINT_TRACE0("customer_adv_start: Already started"); return true; } } return false; }
-
Check the state of an advertisement.
As the description above,
adv_callback
registered byble_ext_adv_mgr_register_callback()
will check advertisement states from the lower layer. In the case ofBLE_EXT_ADV_STATE_CHANGE
, it gets the advertising state fromcb_data.p_ble_state_change->state
. The enumeration definition is as follows.typedef enum { BLE_EXT_ADV_MGR_ADV_DISABLED,/**< when call api ble_ext_adv_mgr_disable, the application adv state will be set to BLE_EXT_ADV_MGR_ADV_DISABLED*/ BLE_EXT_ADV_MGR_ADV_ENABLED, /**< when call api ble_ext_adv_mgr_enable, the application adv state will be set to BLE_EXT_ADV_MGR_ADV_ENABLED*/ } T_BLE_EXT_ADV_MGR_STATE;
At last, the
adv_callback
function will update the state inp_elem
inble.advs.q
.if (p_adv_elem) { p_adv_elem->state = cb_data.p_ble_state_change->state; ...... }
-
Scan response.
Scan response is set as a parameter for
CMD_XM_LE_ADV_CREATE
.
Connection Establishment
The connection establishment process involves two flows, corresponding to the two roles: Peripheral and Central. The flows:
-
Connection establishments for Peripheral.
Peripheral means the device is advertising when creating a connection. The event
EVENT_XM_LE_CON_STATE
will report a connection state (LE_LINK_STATE
). The enumeration is described below. Besides, it also gives the connection parameters such asconn_interval
,conn_latency
andsup_tout
.typedef enum { LE_LINK_STATE_DISCONNECTED, LE_LINK_STATE_CONNECTING, LE_LINK_STATE_CONNECTED, LE_LINK_STATE_DISCONNECTING, } LE_LINK_STATE;
void app_rpt_ble_conn_cmpl(T_APP_LE_LINK *p_link) { ...... le_get_conn_param(GAP_PARAM_CONN_INTERVAL, &rpt.conn_interval, p_link->conn_id); le_get_conn_param(GAP_PARAM_CONN_LATENCY, &rpt.conn_latency, p_link->conn_id); le_get_conn_param(GAP_PARAM_CONN_TIMEOUT, &rpt.sup_tout, p_link->conn_id); app_report_event(CMD_PATH_UART, EVENT_XM_LE_CON_STATE, 0, (uint8_t *)&rpt, sizeof(rpt)); }
-
Scanning for Central.
Central means that the device should scan advertisements from Peripheral and then initiate connection creation.
Ble_scan_start
starts scanning for Peripheral’s advertisements with scan parameters and a callback to get scan information. The example code is as follows.void app_lea_ini_scan_start(void) { APP_PRINT_INFO0("app_ble_scan_start"); BLE_SCAN_PARAM param; param.own_addr_type = GAP_LOCAL_ADDR_LE_PUBLIC; param.phys = GAP_EXT_SCAN_PHYS_1M_BIT; param.ext_filter_policy = GAP_SCAN_FILTER_ANY; param.ext_filter_duplicate = GAP_SCAN_FILTER_DUPLICATE_ENABLE; param.scan_param_1m.scan_type = GAP_SCAN_MODE_PASSIVE; param.scan_param_1m.scan_interval = 0x140; param.scan_param_1m.scan_window = 0xD0; param.scan_param_coded.scan_type = GAP_SCAN_MODE_PASSIVE; param.scan_param_coded.scan_interval = 0x0050; param.scan_param_coded.scan_window = 0x0025; if (ble_scan_start(&app_lea_ini_scan_handle, app_lea_ini_scan_cb, ¶m, NULL)) { app_lea_clear_scan_dev(); } }
The scan results are reported in the callback registered by
ble_scan_start
. The scan information is defined inBLE_SCAN_EVT_DATA
.typedef union { T_LE_EXT_ADV_REPORT_INFO *report; } BLE_SCAN_EVT_DATA;
-
Connection establishments for Central.
Typically, a connection is initiated after obtaining the scan information of the desired device from the aforementioned scanning process.
Le_connect
inCMD_LE_CREATE_CONN
initiates a connection and the key parameter isbd_addr
received fromT_LE_EXT_ADV_REPORT_INFO
. The connection state reporting is the same as described in establishments for Peripheral.
Audio Tone Play
When configuring audio tone/VP playback, it is necessary to check the index in the MCUConfig Tool first, and initialize the index pointed to by VP in app_audio_tone_cfg_init()
. The tone index must correspond to the tone type parameters. The index position in the MCUConfig Tool is shown below.

Ringtone Index

VP Index
void app_audio_tone_cfg_init(void)
{
memset(&app_audio_tone_cfg, TONE_INVALID_INDEX, sizeof(T_APP_AUDIO_TONE_CFG));
app_audio_tone_cfg.tone_link_connected = 0x8c;
app_audio_tone_cfg.tone_hf_call_in = 0x8d;
app_audio_tone_cfg.tone_pairing = 0x90;
app_audio_tone_cfg.tone_link_disconnect = 0x97;
app_audio_tone_cfg.tone_power_off = 0x9b;
app_audio_tone_cfg.tone_power_on = 0x9c;
}
Users can play VP by calling app_audio_tone_type_play()
, where tone_type
corresponds to T_APP_AUDIO_TONE_TYPE
, and TONE_POWER_ON
can be selected as the power on tone. The tone_index
corresponds to the index in app_audio_tone_cfg
above. According to the size of VOICE_PROMPT_INDEX
and tone_index
, it will be judged to ringtone_play()
or voice_prompt_play()
. If TONE_POWER_ON
is selected, it will go to voice_prompt_play()
.
Note
If a new WAV file is placed under the voice prompt folder path (tool\MCUConfigTool-vx.xxx.xxx.x-Common_SDK\Voice Prompt
),
it may be inserted between the existing indexes, which will cause the index in the tool to change and not correspond to the
index in the code. Therefore, the code index needs to be modified based on the index in the tool.
typedef enum
{
TONE_POWER_ON, //0x00
TONE_POWER_OFF, //0x01
...
} T_APP_AUDIO_TONE_TYPE;
#define VOICE_PROMPT_INDEX 0x80
#define TONE_INVALID_INDEX 0xFF
bool app_audio_tone_type_play(T_APP_AUDIO_TONE_TYPE tone_type, bool flush, bool relay)
{
bool ret = false;
uint8_t tone_index = TONE_INVALID_INDEX;
int8_t check_result = 0;
...
tone_index = app_audio_get_tone_index(tone_type);
check_result = app_audio_tone_play_check(tone_type, tone_index);
APP_PRINT_INFO6("app_audio_tone_type_play: tone_type 0x%02x, tone_index 0x%02x, state=%d, index=0x%02x, flush=%d, check_result = %d",
tone_type,
tone_index,
app_db.tone_vp_status.state,
app_db.tone_vp_status.index,
flush,
check_result);
...
if (tone_index < VOICE_PROMPT_INDEX)
{
if (flush)
{
ringtone_cancel(tone_index, true);
}
ret = ringtone_play(tone_index, relay);
}
else if (tone_index < TONE_INVALID_INDEX)
{
if (flush)
{
voice_prompt_cancel(tone_index - VOICE_PROMPT_INDEX, true);
}
ret = voice_prompt_play(tone_index - VOICE_PROMPT_INDEX,
(T_VOICE_PROMPT_LANGUAGE_ID)app_cfg_nv.voice_prompt_language,
relay);
}
return ret;
}
For example, users can use it in app_mmi.c
. When using the mmi pwron CMD to boot, call app_audio_tone_type_play()
in case MMI_DEV_POWER_ON
, fill in the parameters with TONE_POWER_ON
, and then the VP can be played during the boot operation.
case MMI_DEV_POWER_ON:
{
if (app_db.device_state == APP_DEVICE_STATE_OFF_ING)
{
if (app_bt_policy_get_b2s_connected_num())
{
app_bt_policy_disconnect_all_link();
}
app_mmi_modify_reboot_check_times(REBOOT_CHECK_POWER_ON_MAX_TIMES);
}
else if (app_db.device_state == APP_DEVICE_STATE_OFF)
{
if (!app_db.is_long_press_power_on_play_vp && !mp_hci_test_mode_is_running())
{
app_audio_tone_type_play(TONE_POWER_ON, false, false);
}
app_db.is_long_press_power_on_play_vp = false;
sys_mgr_power_on();
}
}
break;
Bluetooth Audio Transmitter
Source Play
Source play function, which can transmit MIC or Line-in signal to BUDs through HFP, A2DP, or BIS. These two input methods and three output methods can be combined in various ways by set_route_in
and set_route_out
. The file path of the source play function is: src\sample\bt_audio_trx\source_play
.
The functions of each file are as follows:
app_src_play.c
includes selection and switching of source play input source and output source, and play/stop of output source.app_src_play_a2dp.c
includes processing when the output source is A2DP.app_src_play_hfp.c
includes processing when the output source is HFP.app_src_play_cmd.c
encapsulates the APIs mentioned above as commands.
The input and output paths are defined in the following structures in app_src_play.h
.
//input route
typedef enum
{
SOURCE_ROUTE_MIC = 1,
SOURCE_ROUTE_LINE_IN = 2,
SOURCE_ROUTE_USB = 3,
SOURCE_ROUTE_SD_CARD = 4,
SOURCE_ROUTE_INVALID = 0xFF
} T_SOURCE_ROUTE;
//output route
typedef enum
{
PLAY_ROUTE_A2DP = 1,
PLAY_ROUTE_HFP_AG = 2,
PLAY_ROUTE_BIS = 3,
PLAY_ROUTE_CIS = 4,
PLAY_ROUTE_LOCAL = 5,
PLAY_ROUTE_INVALID = 0xFF
} T_PLAY_ROUTE;
MIC/Line-in Source Play Input Route
The MIC/Line-in input adopts the record path, the input path can be set by modifying the device
parameter in audio_track_create()
.
The source_play
structure.
static struct
{
T_PLAY_ROUTE play_route;
struct
{
T_SOURCE_ROUTE src_route;
T_AUDIO_TRACK_HANDLE handle;
uint8_t default_volume;
} record;
} source_play
Set and get source input route.
void app_src_play_set_src_route(T_SOURCE_ROUTE src_route)
{
APP_PRINT_INFO1("app_src_play_set_src_route: src_route %d", src_route);
source_play.record.src_route = src_route;
}
//set and get input source route via CMD
void app_src_play_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
case CMD_SRC_PLAY_SET_SRC_ROUTE:
{
uint8_t src_route = cmd_ptr[2];
app_src_play_set_src_route((T_SOURCE_ROUTE)src_route);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_GET_SRC_ROUTE:
{
ack_pkt[2] = app_src_play_get_src_route();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
}
Modify device parameter of Audio Track due to src_route
.
static void record_start(void)
{
...
if (source_play.record.src_route == SOURCE_ROUTE_MIC)
{
device = AUDIO_DEVICE_IN_MIC;
}
else if (source_play.record.src_route == SOURCE_ROUTE_LINE_IN)
{
device = AUDIO_DEVICE_IN_AUX;
}
}
USB Source Play Input Route
The USB input adopts the pipe path, the input path can be set by modifying the device
parameter in audio_pipe_create()
.
The source_play
structure.
typedef struct
{
T_PLAY_ROUTE play_route;
struct
{
T_SOURCE_ROUTE src_route;
T_AUDIO_PIPE_HANDLE handle;
T_RING_BUFFER src_rbuf;
uint8_t *src_fill_buf;
uint16_t src_fill_len;
uint8_t *snk_drain_buf;
uint8_t default_volume;
uint16_t seq_num;
uint8_t state;
} pipe;
} T_SOURCE_PIPE_PLAY;
Set and get source input route.
void app_src_play_set_src_route(T_SOURCE_ROUTE src_route)
{
APP_PRINT_INFO1("app_src_play_set_src_route: src_route %d", src_route);
source_play.record.src_route = src_route;
}
//set and get input source route via CMD
void app_src_play_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
case CMD_SRC_PLAY_SET_SRC_ROUTE:
{
uint8_t src_route = cmd_ptr[2];
app_src_play_set_src_route((T_SOURCE_ROUTE)src_route);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_GET_SRC_ROUTE:
{
ack_pkt[2] = app_src_play_get_src_route();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
}
SD Card Source Play Input Route
Input format information from SD card that requires the use of file system to read audio files in app_src_play_sd_start(uint8_t play_route)
.
bool app_src_play_sd_start(uint8_t play_route)
{
uint8_t err_code = 0;
T_FILE_FORMAT_INFO file_format_info;
uint32_t playback_offset = 0;
uint16_t name_len = 0;
//select a file TEMP_FILE_NAME_STRING
if (sd.fs_handle == NULL)
{
// init file system and get sd.fs_handle
uint8_t res = app_src_playback_open_and_get_file_info(SINGLE_FILE, (uint8_t *)TEMP_FILE_NAME_STRING,
&name_len, &playback_offset);
if (res != PLAYBACK_SUCCESS)
{
err_code = 1;
goto ERR;
}
}
else
{
uint8_t *p_file_name = audio_fs_get_filename(sd.fs_handle);
name_len = audio_fs_get_filenameLen(sd.fs_handle);
// memcpy((uint8_t *)TEMP_FILE_NAME_STRING, p_file_name, name_len);
playback_offset = audio_fs_get_file_offset(sd.fs_handle);
}
app_src_sd_card_dlps_disable(APP_SD_DLPS_ENTER_CHECK_PLAYING);
app_src_sd_card_power_down_disable(APP_SD_POWER_DOWN_ENTER_CHECK_PLAYBACK);
// read file header to get format for
if (app_src_play_sd_get_file_format(&file_format_info) == false)
{
err_code = 2;
return false;
}
if (play_route == PLAY_ROUTE_INVALID)
{
err_code = 3;
return false;
}
else if (play_route == PLAY_ROUTE_LOCAL)
{
app_src_play_sd_local_start(&file_format_info);
}
else if (play_route == PLAY_ROUTE_BIS ||
play_route == PLAY_ROUTE_CIS)
{
app_src_play_sd_pipe_start(&file_format_info);
}
return true;
ERR:
APP_PRINT_ERROR1("app_src_play_sd_start: err_code %d", -err_code);
return false;
}
MIC/Line-in Source Play Output Route
Similar to input source, source play output route can be set as HFP, A2DP, or BIS.
Set and get source output route.
void app_src_play_set_play_route(T_PLAY_ROUTE play_route)
{
APP_PRINT_INFO1("app_src_play_set_play_route: play_route %d", play_route);
source_play.play_route = play_route;
}
//set and get output source route via CMD
void app_src_play_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
...
case CMD_SRC_PLAY_SET_PLAY_ROUTE:
{
uint8_t play_route = cmd_ptr[2];
app_src_play_set_play_route((T_PLAY_ROUTE)play_route);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_GET_PLAY_ROUTE:
{
ack_pkt[2] = app_src_play_get_play_route();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
}
Create and start audio_track
after configurations,
record_read_cb
will process the MIC/Line-in input source data to convert to the format specified in the source output route.
static void record_start(void)
{
APP_PRINT_TRACE0("record_start!!");
uint32_t device = 0;
T_AUDIO_FORMAT_INFO format_info;
if (source_play.play_route == PLAY_ROUTE_A2DP)
{
if (!app_src_play_get_a2dp_format((uint8_t *)&format_info))
{
APP_PRINT_ERROR0("record_start: a2dp format info does not exist");
return;
}
app_src_play_print_a2dp_format("record_start", format_info);
}
else if (source_play.play_route == PLAY_ROUTE_HFP_AG)
{
if (!app_src_play_get_hfp_format((uint8_t *)&format_info))
{
APP_PRINT_ERROR0("record_start: hfp format info does not exist");
return;
}
app_src_play_print_hfp_format("record_start", format_info);
}
else if (source_play.play_route == PLAY_ROUTE_BIS)
{
#if BAP_BROADCAST_SOURCE
if (!app_lea_get_data_format((uint8_t *)&format_info))
{
APP_PRINT_ERROR0("record_start: lc3 format info does not exist");
return;
}
#endif
// app_src_play_print_hfp_format("record_start", format_info);
}
else if (source_play.play_route == PLAY_ROUTE_CIS)
{
if (!app_lea_get_data_format((uint8_t *)&format_info))
{
APP_PRINT_ERROR0("record_start: lc3 format info does not exist");
return;
}
}
...
source_play.record.handle = audio_track_create(AUDIO_STREAM_TYPE_RECORD,
AUDIO_STREAM_MODE_NORMAL,
AUDIO_STREAM_USAGE_LOCAL,
format_info,
0,
source_play.record.default_volume,
device,
NULL,
record_read_cb);
if (source_play.record.handle == NULL)
{
APP_PRINT_ERROR0("app_customer_mic_record record_start: handle is NULL");
return;
}
audio_track_start(source_play.record.handle);
}
static bool record_read_cb(T_AUDIO_TRACK_HANDLE handle,
uint32_t *timestamp,
uint16_t *seq_num,
T_AUDIO_STREAM_STATUS *status,
uint8_t *frame_num,
void *buf,
uint16_t required_len,
uint16_t *actual_len)
{
{
mic_dump_record_data("app_mic_record_read_cb", buf, required_len);
uint8_t actual_frame_num = *frame_num;
if (source_play.record.handle)
{
if (source_play.play_route == PLAY_ROUTE_A2DP)
{
uint8_t res = app_src_play_a2dp_handle_data(buf, required_len, actual_frame_num);
}
else if (source_play.play_route == PLAY_ROUTE_HFP_AG)
{
app_src_play_hfp_send_sco(buf, required_len);
}
else if (source_play.play_route == PLAY_ROUTE_BIS ||
source_play.play_route == PLAY_ROUTE_CIS)
{
#if (BAP_BROADCAST_SOURCE || BAP_UNICAST_CLIENT)
app_lea_iso_data_send(buf, required_len, true, *timestamp, *seq_num);
#endif
}
}
}
*actual_len = required_len;
return true;
}
pipe_encode_cb
will process the USB input source data to convert to the format specified in the source output route.
static void usb_pipe_start(void)
{
T_AUDIO_FORMAT_INFO src_info;
T_AUDIO_FORMAT_INFO snk_info;
uint16_t src_len = 0;
if (source_play.play_route == PLAY_ROUTE_A2DP)
{
if (!app_src_play_get_a2dp_format((uint8_t *)&snk_info))
{
APP_PRINT_ERROR0("usb_pipe_start: a2dp format info does not exist");
return;
}
app_src_play_print_a2dp_format("usb_pipe_start", snk_info);
}
else if (source_play.play_route == PLAY_ROUTE_HFP_AG)
{
if (!app_src_play_get_hfp_format((uint8_t *)&snk_info))
{
APP_PRINT_ERROR0("usb_pipe_start: hfp format info does not exist");
return;
}
app_src_play_print_hfp_format("usb_pipe_start", snk_info);
}
else if (source_play.play_route == PLAY_ROUTE_BIS)
{
#if BAP_BROADCAST_SOURCE
if (!app_lea_get_data_format((uint8_t *)&snk_info))
{
APP_PRINT_ERROR0("usb_pipe_start: lc3 format info does not exist");
return;
}
#endif
// app_src_play_print_hfp_format("record_start", format_info);
}
else if (source_play.play_route == PLAY_ROUTE_CIS)
{
if (!app_lea_get_data_format((uint8_t *)&snk_info))
{
APP_PRINT_ERROR0("usb_pipe_start: lc3 format info does not exist");
return;
}
}
else
{
APP_PRINT_ERROR1("usb_pipe_start: play_route", source_pipe_play.play_route);
return;
}
if (source_pipe_play.pipe.handle != NULL)
{
APP_PRINT_ERROR0("usb_pipe_start: already started");
return;
}
src_info.type = AUDIO_FORMAT_TYPE_PCM;
src_info.frame_num = 1;
src_info.attr.pcm.sample_rate = 48000;
/*FIXME: frame_length due to snk_format*/
src_info.attr.pcm.chann_num = 2;
src_info.attr.pcm.bit_width = 16;
APP_PRINT_INFO1("usb_pipe_start: snk type %x", snk_info.type);
switch (snk_info.type)
{
case AUDIO_FORMAT_TYPE_LC3:
{
src_len = 512;
snk_info.frame_num = 1;
src_lea_db.lea_data_buf = calloc(1, snk_info.attr.lc3.frame_length * 2);
if (src_lea_db.lea_data_buf == NULL)
{
APP_PRINT_ERROR0("usb_pipe_start: lea_data_buf NULL");
return;
}
src_lea_db.frame_len = snk_info.attr.lc3.frame_length;
if (snk_info.attr.lc3.frame_duration == AUDIO_LC3_FRAME_DURATION_7_5_MS)
{
src_lea_db.frame_duration = 7500;
}
else
{
src_lea_db.frame_duration = 10000;
}
}
break;
case AUDIO_FORMAT_TYPE_SBC:
{
src_len = 512;
snk_info.frame_num = 1;
}
break;
case AUDIO_FORMAT_TYPE_MSBC:
{
src_len = 720;
snk_info.frame_num = 1;
}
break;
default:
return;
}
/*Frame length per channel in octets for encoding or decoding.*/
src_info.attr.pcm.frame_length = src_len / src_info.attr.pcm.chann_num;
source_pipe_play.pipe.src_fill_buf = calloc(1, src_len);
if (source_pipe_play.pipe.src_fill_buf == NULL)
{
APP_PRINT_ERROR0("usb_pipe_start: src_fill_buf NULL");
usb_pipe_buf_release();
return;
}
source_pipe_play.pipe.src_fill_len = src_len;
source_pipe_play.pipe.snk_drain_buf = calloc(1, PIPE_DRAIN_BUF_LEN);
if (source_pipe_play.pipe.snk_drain_buf == NULL)
{
APP_PRINT_ERROR0("usb_pipe_start: snk_drain_buf NULL");
usb_pipe_buf_release();
return;
}
source_pipe_play.pipe.handle = audio_pipe_create(src_info,
snk_info,
source_pipe_play.pipe.default_volume,
pipe_encode_cb);
if (!source_pipe_play.pipe.handle)
{
APP_PRINT_ERROR0("usb_pipe_start: pipe.handle NULL");
usb_pipe_buf_release();
return;
}
}
static bool pipe_encode_cb(T_AUDIO_PIPE_HANDLE handle, T_AUDIO_PIPE_EVENT event, uint32_t param)
{
if (handle != source_pipe_play.pipe.handle)
{
return true;
}
if ((event != AUDIO_PIPE_EVENT_DATA_IND) && (event != AUDIO_PIPE_EVENT_DATA_FILLED))
{
APP_PRINT_INFO2("pipe_encode_cb: handle %x event %x", handle, event);
}
switch (event)
{
case AUDIO_PIPE_EVENT_RELEASED:
{
source_pipe_play.pipe.state = PIPE_STATE_IDLE;
usb_pipe_buf_release();
source_pipe_play.pipe.handle = NULL;
}
break;
case AUDIO_PIPE_EVENT_CREATED:
{
source_pipe_play.pipe.state = PIPE_STATE_CREATED;
audio_pipe_start(source_pipe_play.pipe.handle);
}
break;
case AUDIO_PIPE_EVENT_STARTED:
{
source_pipe_play.pipe.state = PIPE_STATE_STARTED;
}
break;
case AUDIO_PIPE_EVENT_STOPPED:
{
source_pipe_play.pipe.state = PIPE_STATE_CREATED;
}
break;
case AUDIO_PIPE_EVENT_DATA_IND:
{
pipe_handle_data_ind();
}
break;
case AUDIO_PIPE_EVENT_DATA_FILLED:
{
pipe_handle_data_filled();
}
break;
case AUDIO_PIPE_EVENT_MIXED:
break;
case AUDIO_PIPE_EVENT_DEMIXED:
break;
default:
break;
}
return true;
}
When the play_route
is set to A2DP, record_read_cb
will call app_src_play_a2dp_handle_data
,
the A2DP output format will be saved by app_src_play_save_a2dp_format
when Bluetooth reports BT_EVENT_A2DP_CONFIG_CMPL
in app_audio_bt_cback
.
void app_src_play_save_a2dp_format(uint8_t *format_info)
{
if (!a2dp_play.a2dp_format_ready)
{
memcpy(&a2dp_play.a2dp_format, format_info, sizeof(T_AUDIO_FORMAT_INFO));
a2dp_play.a2dp_format.attr.sbc.bitpool = 0x22;
a2dp_play.a2dp_format_ready = true;
}
app_src_play_print_a2dp_format("app_src_play_save_a2dp_format: ",
a2dp_play.a2dp_format);
}
uint16_t app_src_play_a2dp_handle_data(uint8_t *p_data, uint16_t data_len, uint8_t frame_number)
{
uint16_t res = SRC_PLAY_A2DP_SUCCESS;
if (ring_buffer_write(&a2dp_play.ring_buf, p_data, data_len))
{
a2dp_play.num_frame_buf++;
}
else
{
res = SRC_PLAY_A2DP_ERR_RINGBUF;
APP_PRINT_ERROR0("xiaoai_process_voice_data: xiaoai_record.voice_buf is full, drop pkt");
}
if (a2dp_play.num_frame_buf == A2DP_PACKET_FRAME_NUM)
{
a2dp_play.num_frame_buf = 0;
uint16_t data_len_to_send = data_len * A2DP_PACKET_FRAME_NUM;
uint8_t *p_data_to_send = malloc(data_len_to_send);
if (p_data_to_send)
{
uint32_t actual_len = ring_buffer_read(&a2dp_play.ring_buf, data_len_to_send, p_data_to_send);
APP_PRINT_INFO1("app_src_play_a2dp_handle_data: actual_len %d sent", actual_len);
res = app_src_play_a2dp_send_data(p_data_to_send, data_len_to_send, A2DP_PACKET_FRAME_NUM);
free(p_data_to_send);
}
else
{
res = SRC_PLAY_A2DP_ERR_RAM;
}
}
return res;
}
When the play_route
is set to HFP, record_read_cb
will call app_src_play_hfp_send_sco
.
The HFP output format will be saved by app_src_play_save_hfp_format
when Bluetooth reports BT_EVENT_SCO_CONN_CMPL
in app_audio_bt_cback
.
void app_src_play_save_hfp_format(uint8_t *format_info)
{
if (!hfp_play.hfp_format_ready)
{
memcpy(&hfp_play.hfp_format, format_info, sizeof(T_AUDIO_FORMAT_INFO));
hfp_play.hfp_format_ready = true;
}
app_src_play_print_hfp_format("app_src_play_save_hfp_format: ",
hfp_play.hfp_format);
}
void app_src_play_hfp_send_sco(uint8_t *p_data, uint16_t len)
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(app_db.br_link[active_hf_idx].bd_addr);
if (p_link == NULL)
{
APP_PRINT_ERROR0("app_src_play_hfp_send_sco: no br link found");
return;
}
hfp_seq_num++;
if (p_link->sco.duplicate_fst_data)
{
p_link->sco.duplicate_fst_data = false;
bt_sco_data_send(p_link->bd_addr, hfp_seq_num - 1, p_data, len);
}
bt_sco_data_send(p_link->bd_addr, hfp_seq_num, p_data, len);
}
When the play_route
is set to BIS/CIS, record_read_cb
will call app_lea_iso_data_send
.
The BIS output format will be saved by app_lea_save_data_format
in app_lea_handle_bis_data_path_setup
.
The CIS output format will be saved by app_lea_save_data_format
in app_lea_handle_cis_data_path_setup
.
static void app_lea_save_data_format(T_APP_LEA_ISO_CHANN *p_iso_chann)
{
save_format.type = AUDIO_FORMAT_TYPE_LC3;
codec_max_sdu_len_get(&p_iso_chann->codec_data, &p_iso_chann->iso_sdu_len);
//always get two-channel data but send data according to bis/cis num
save_format.attr.lc3.chann_location = AUDIO_CHANNEL_LOCATION_SL | AUDIO_CHANNEL_LOCATION_SR;
save_format.attr.lc3.sample_rate = app_lea_get_sample_rate(p_iso_chann);
save_format.attr.lc3.frame_length = p_iso_chann->codec_data.octets_per_codec_frame;
if (p_iso_chann->codec_data.frame_duration == FRAME_DURATION_CFG_7_5_MS)
{
save_format.attr.lc3.frame_duration = AUDIO_LC3_FRAME_DURATION_7_5_MS;
}
else
{
save_format.attr.lc3.frame_duration = AUDIO_LC3_FRAME_DURATION_10_MS;
}
save_format.attr.lc3.presentation_delay = p_iso_chann->presentation_delay;
}
void app_lea_iso_data_send(uint8_t *p_data, uint16_t len, bool ext_flag, uint32_t ts, uint16_t seq)
{
T_GAP_CAUSE cause = GAP_CAUSE_SUCCESS;
T_APP_LEA_ISO_CHANN *p_iso_chann = NULL;
uint8_t chnl_cnt = 0;
uint32_t time_stamp = 0;
uint16_t seq_num = 0;
for (uint8_t i = 0; i < app_db.iso_input_queue.count; i++)
{
p_iso_chann = (T_APP_LEA_ISO_CHANN *)os_queue_peek(&app_db.iso_input_queue, i);
if (p_iso_chann->codec_data.audio_channel_allocation == AUDIO_LOCATION_MONO)
{
chnl_cnt = 1;
}
else
{
chnl_cnt = __builtin_popcount(p_iso_chann->codec_data.audio_channel_allocation);
}
if (ext_flag)
{
time_stamp = ts;
seq_num = seq;
}
else
{
time_stamp = (p_iso_chann->time_stamp + p_iso_chann->pkt_seq_num) *
(p_iso_chann->sdu_interval * 1000);
seq_num = p_iso_chann->pkt_seq_num;
}
if (chnl_cnt == 2)
{
cause = gap_iso_send_data(p_data,
p_iso_chann->iso_conn_handle,
len,
false,
time_stamp,
seq_num);
}
else if (chnl_cnt == 1)
{
if (p_iso_chann->codec_data.audio_channel_allocation & (AUDIO_LOCATION_FL |
AUDIO_LOCATION_SIL))
{
cause = gap_iso_send_data(p_data,
p_iso_chann->iso_conn_handle,
len / 2,
0,
time_stamp,
seq_num);
}
else
{
cause = gap_iso_send_data(p_data + (len / 2),
p_iso_chann->iso_conn_handle,
len / 2,
0,
time_stamp,
seq_num);
}
}
p_iso_chann->pkt_seq_num++;
if (cause != GAP_CAUSE_SUCCESS)
{
APP_PRINT_ERROR1("app_lea_iso_data_send: failed, cause 0x%x", cause);
}
}
return;
}
SD Card Source Play Output Route
Similar to the input source, the SD card source play output route can be set as local play, BIS, or CIS.
bool app_src_play_sd_start(uint8_t play_route)
{
...
if (play_route == PLAY_ROUTE_INVALID)
{
err_code = 3;
return false;
}
else if (play_route == PLAY_ROUTE_LOCAL)
{
app_src_play_sd_local_start(&file_format_info);
}
else if (play_route == PLAY_ROUTE_BIS ||
play_route == PLAY_ROUTE_CIS)
{
app_src_play_sd_pipe_start(&file_format_info);
}
}
If choosing the play route as PLAY_ROUTE_LOCAL
, the local start is set in app_src_play_sd_local_start()
.
static void app_src_play_sd_local_start(T_FILE_FORMAT_INFO *file_format)
{
if (sd.local_play.handle != NULL)
{
audio_track_release(sd.local_play.handle);
sd.local_play.handle = NULL;
}
T_LOCALPLAY_SET_INFO set_play_info;
app_src_play_sd_set_local_play_info(file_format, &set_play_info);
sd.local_play.play_monitor.put_data_time_ms = set_play_info.play_duration;
sd.local_play.play_monitor.preq_pkts = set_play_info.preq_pkts;
sd.local_play.handle = audio_track_create(AUDIO_STREAM_TYPE_PLAYBACK, //stream_type
AUDIO_STREAM_MODE_NORMAL, // mode
AUDIO_STREAM_USAGE_SNOOP, // usage
file_format->format_info, //format_info
sd.local_play.volume, //volume
0,
AUDIO_DEVICE_OUT_SPK, // device
NULL,
NULL);
if (sd.local_play.handle != NULL)
{
audio_track_latency_set(sd.local_play.handle, set_play_info.latency, true);
audio_track_threshold_set(sd.local_play.handle, set_play_info.upper_level,
set_play_info.lower_level);
sd.local_play.play_monitor.delay_stop_ms = set_play_info.latency;
}
sd.op_next_action = SD_STOPPED_IDLE_ACTION;
// app_src_playback_volume_set(sd.local_play.volume);
sd.local_play.play_state = SD_PLAY_STATE_PLAY;
sd.local_play.play_monitor.local_track_state = PLAYBACK_TRACK_STATE_CLEAR;
sd.local_play.play_monitor.sec_track_state = PLAYBACK_TRACK_STATE_CLEAR;
sd.local_play.play_monitor.buffer_state = PLAYBACK_BUF_NORMAL;
audio_track_start(sd.local_play.handle);
}
If choose to play the route as PLAY_ROUTE_BIS
or PLAY_ROUTE_CIS
, the route start is set in app_src_play_sd_pipe_start()
.
void app_src_play_sd_pipe_start(T_FILE_FORMAT_INFO *file_format)
{
T_AUDIO_FORMAT_INFO src_info = file_format->format_info;
T_AUDIO_FORMAT_INFO snk_info;
T_PLAY_ROUTE play_route = app_src_play_get_play_route();
if (play_route == PLAY_ROUTE_A2DP)
{
// TODO: Support A2DP TX
return;
}
else if (play_route == PLAY_ROUTE_HFP_AG)
{
// TODO: Support HFP TX
return;
}
#if (BAP_BROADCAST_SOURCE || BAP_UNICAST_CLIENT)
else if (play_route == PLAY_ROUTE_BIS || play_route == PLAY_ROUTE_CIS)
{
if (!app_lea_get_data_format(LEA_CODEC_DIR_ENCODE, &snk_info))
{
APP_PRINT_ERROR0("app_src_play_sd_pipe_start: lc3 format info does not exist");
return;
}
uint8_t chnl_cnt;
if (snk_info.attr.lc3.chann_location == AUDIO_LOCATION_MONO)
{
chnl_cnt = 1;
}
else
{
chnl_cnt = __builtin_popcount(snk_info.attr.lc3.chann_location);
}
lea_tx_mgr.pkt_len = snk_info.attr.lc3.frame_length * chnl_cnt;
lea_tx_mgr.p_lea_send_buf = calloc(1, lea_tx_mgr.pkt_len);
if (lea_tx_mgr.p_lea_send_buf == NULL)
{
APP_PRINT_ERROR0("app_src_play_sd_pipe_start: p_lea_send_buf malloc fail");
return;
}
lea_tx_mgr.target_threshold = 5; // frame_cnt
lea_tx_mgr.pre_fill_num = 4;
APP_PRINT_INFO2("app_src_play_sd_pipe_start: pkt_len %d, target_threshold %d",
lea_tx_mgr.pkt_len, lea_tx_mgr.target_threshold);
if (snk_info.attr.lc3.frame_duration == AUDIO_LC3_FRAME_DURATION_10_MS)
{
lea_tx_mgr.timer_duration = 10000;
}
else
{
lea_tx_mgr.timer_duration = 7500;
}
}
#endif
src_info.frame_num = 1;
snk_info.frame_num = 1;
if (sd.pipe_play.handle == NULL)
{
sd.pipe_play.handle = audio_pipe_create(AUDIO_STREAM_MODE_NORMAL,
src_info, snk_info,
sd.pipe_play.volume,
app_src_play_sd_pipe_cback);
}
sd.op_next_action = SD_STOPPED_IDLE_ACTION;
}
Audio Track/Pipe will be created after calling app_src_play_sd_local_start()
/ app_src_play_sd_pipe_start()
, they will be released in the calling of app_src_play_sd_local_stop()
and app_src_play_sd_pipe_stop()
.
void app_src_play_sd_stop(uint8_t play_route)
{
if (play_route == PLAY_ROUTE_LOCAL)
{
app_src_play_sd_local_stop();
}
else if (play_route == PLAY_ROUTE_BIS ||
play_route == PLAY_ROUTE_CIS)
{
app_src_play_sd_pipe_stop();
}
}
static void app_src_play_sd_local_stop(void)
{
uint8_t res = PLAYBACK_SUCCESS;
audio_fs_decode_deinit();
app_stop_timer(&timer_idx_sd_local_put_data);
sd.local_play.play_state = SD_PLAY_STATE_IDLE;
if (sd.local_play.handle != NULL)
{
sd.local_play.play_monitor.local_track_state = PLAYBACK_TRACK_STATE_CLEAR;
audio_track_release(sd.local_play.handle);
}
if (sd.fs_handle != NULL)
{
if (0 != audio_fs_close(sd.fs_handle))
{
// return;
}
sd.fs_handle = NULL;
}
app_src_sd_card_dlps_enable(APP_SD_DLPS_ENTER_CHECK_PLAYING);
// TODO: FIX ME
// app_sd_card_power_down_enable(APP_SD_POWER_DOWN_ENTER_CHECK_PLAYBACK);
}
static void app_src_play_sd_pipe_stop(void)
{
audio_fs_decode_deinit();
if (sd.pipe_play.handle != NULL)
{
audio_pipe_release(sd.pipe_play.handle);
}
if (sd.fs_handle != NULL)
{
if (0 != audio_fs_close(sd.fs_handle))
{
// return;
}
sd.fs_handle = NULL;
}
app_src_sd_card_dlps_enable(APP_SD_DLPS_ENTER_CHECK_PLAYING);
}
When the end of the file is reached, the file system also releases the handle created to read audio file information.
Source Play Start/Stop
Source play start/stop can be divided into two parts: Input route start/stop and output route start/stop.
When the source play input route needs to be activated, the app_src_play_route_in_start()
should be called.
bool app_src_play_route_in_start(void)
{
bool result = false;
if (source_play.src_route == SOURCE_ROUTE_USB)
{
if (usb_ds_attr.active)
{
result = usb_encode_pipe_start();
}
if (usb_us_attr.active)
{
result = usb_decode_pipe_start();
}
}
#if F_APP_SD_CARD_PLAY
else if (source_play.src_route == SOURCE_ROUTE_SD_CARD)
{
result = app_src_play_sd_start(source_play.play_route);
}
#endif
#if F_APP_INTEGRATED_TRANSCEIVER
else if (source_play.src_route == SOURCE_ROUTE_A2DP)
{
result = app_src_play_pipe_start(source_play.play_route);
}
#endif
else
{
result = record_start();
}
return result;
}
When the source play input route needs to be stopped, it means releasing the started audio_track
and freeing the record handle.
void app_src_play_route_in_stop(void)
{
if (source_play.src_route == SOURCE_ROUTE_USB)
{
if (usb_ds_attr.active)
{
usb_encode_pipe_stop();
}
if (usb_us_attr.active)
{
usb_decode_pipe_stop();
}
}
#if F_APP_SD_CARD_PLAY
else if (source_play.src_route == SOURCE_ROUTE_SD_CARD)
{
app_src_play_sd_stop(source_play.play_route);
}
#endif
#if F_APP_INTEGRATED_TRANSCEIVER
else if (source_play.src_route == SOURCE_ROUTE_A2DP)
{
app_src_play_pipe_stop();
}
#endif
else
{
record_stop();
}
}
When source play output route needs to be activated, depending on the output format, specific processing is required.
bool app_src_play_a2dp_start_req(void)
{
APP_PRINT_INFO0("app_src_play_a2dp_start_req");
return bt_a2dp_stream_start_req(a2dp_play.sink_addr);
}
bool app_src_play_hfp_start_req(void)
{
APP_PRINT_INFO0("app_src_play_hfp_start_req");
return bt_hfp_ag_audio_connect_req(hfp_play.hf_addr);
}
bool app_lea_bsrc_start(void)
{
if (app_db.bsrc_db.source_handle == NULL)
{
APP_PRINT_ERROR0("app_lea_bsrc_start: init bis firstly!");
return false;
}
app_db.bsrc_db.prefer_state = BROADCAST_SOURCE_STATE_STREAMING;
app_lea_bsrc_target_state();
return true;
}
bool app_lea_ini_cis_media_start(uint8_t group_idx)
{
bool ret = false;
if (group_idx < app_db.group_handle_queue.count)
{
T_APP_LEA_GROUP_INFO *p_group = (T_APP_LEA_GROUP_INFO *)os_queue_peek(&app_db.group_handle_queue,
group_idx);
if (p_group)
{
ret = app_lea_ini_start_media(p_group->group_handle);
}
}
return ret;
}
//start in cases
bool app_src_play_route_out_start(void)
{
bool result = false;
switch (source_play.play_route)
{
case PLAY_ROUTE_A2DP:
{
result = app_src_play_a2dp_start_req();
}
break;
case PLAY_ROUTE_HFP_AG:
{
result = app_src_play_hfp_start_req();
}
break;
#if BAP_BROADCAST_SOURCE
case PLAY_ROUTE_BIS:
{
result = app_lea_bsrc_start();
}
break;
#endif
#if BAP_UNICAST_CLIENT
case PLAY_ROUTE_CIS:
{
result = app_lea_ini_cis_media_start(0);
}
#endif
case PLAY_ROUTE_LOCAL:
{
// DO Nothing
result = true;
}
case PLAY_ROUTE_MULTI_A2DP:
{
result = app_src_play_a2dp_start_req();
}
break;
default:
break;
}
return result;
}
When the source play output route needs to be stopped, depending on the output format, specific processing is required.
void app_src_play_a2dp_stop(void)
{
APP_PRINT_TRACE0("app_src_play_a2dp_stop");
a2dp_play.num_frame_buf = 0;
ring_buffer_clear(&a2dp_play.ring_buf);
if (a2dp_play.a2dp_state != A2DP_STATE_STREAM_STOP)
{
bt_a2dp_stream_suspend_req(a2dp_play.sink_addr);
}
app_dlps_enable(APP_DLPS_ENTER_CHECK_PLAYBACK);
}
void app_src_play_hfp_stop(void)
{
APP_PRINT_TRACE0("app_src_play_hfp_stop");
bt_hfp_ag_audio_disconnect_req(hfp_play.hf_addr);
}
bool app_lea_bsrc_stop(bool release)
{
if (app_db.bsrc_db.source_handle == NULL)
{
return false;
}
if (release)
{
app_db.bsrc_db.prefer_state = BROADCAST_SOURCE_STATE_IDLE;
}
else
{
if (app_db.bsrc_db.prefer_state != BROADCAST_SOURCE_STATE_IDLE)
{
app_db.bsrc_db.prefer_state = BROADCAST_SOURCE_STATE_CONFIGURED;
}
}
app_lea_bsrc_target_state();
return true;
}
bool app_lea_ini_cis_stream_stop(uint8_t group_idx, bool release)
{
bool ret = false;
if (group_idx < app_db.group_handle_queue.count)
{
T_APP_LEA_GROUP_INFO *p_group = (T_APP_LEA_GROUP_INFO *)os_queue_peek(&app_db.group_handle_queue,
group_idx);
if (p_group)
{
if (release)
{
ret = app_lea_ini_unicast_audio_release(p_group->group_handle);
}
else
{
ret = app_lea_ini_unicast_audio_stop(p_group->group_handle, 0);
}
}
}
return ret;
}
//stop in cases
void app_src_play_route_out_stop(void)
{
switch (source_play.play_route)
{
case PLAY_ROUTE_A2DP:
{
app_src_play_a2dp_stop();
}
break;
case PLAY_ROUTE_HFP_AG:
{
app_src_play_hfp_stop();
}
break;
#if BAP_BROADCAST_SOURCE
case PLAY_ROUTE_BIS:
{
app_lea_bsrc_stop(true);
}
break;
#endif
#if BAP_UNICAST_CLIENT
case PLAY_ROUTE_CIS:
{
app_lea_ini_cis_stream_stop(0, true);
}
#endif
case PLAY_ROUTE_MULTI_A2DP:
{
result = app_src_play_a2dp_stop();
}
break;
default:
break;
}
}
The APIs mentioned above are called in app_src_play_handle_cmd_set
,
user can fill in the corresponding cmd_id
in the ACI Host CLI Tool
according to the following case to perform operations.
void app_src_play_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
...
case CMD_SRC_PLAY_ROUTE_IN_START:
{
app_src_play_route_in_start();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_ROUTE_IN_STOP:
{
app_src_play_route_in_stop();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_ROUTE_OUT_START:
{
if (app_src_play_route_out_start())
{
ack_pkt[2] = CMD_SET_STATUS_COMPLETE;
}
else
{
ack_pkt[2] = CMD_SET_STATUS_SCENARIO_ERROR;
}
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_SRC_PLAY_ROUTE_OUT_STOP:
{
app_src_play_route_out_stop();
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
default:
break;
}
}
LE Audio Initiator - BIS
Bluetooth Low Energy Audio CAP roles can be divided into Acceptor, Initiator, and Commander. In a transmitter application, the Initiator role is supported, which can transmit BIS or transmit and receive CIS.
In this chapter, the design specification for BIS will be presented.
Initialization
Initialization is performed through the function of app_lea_profile_init()
, including configuration of LE Audio parameters, initialization of BAP and CAP, as well as the necessary data processing setup.
//app_lea_ini_profile.c
void app_lea_profile_init(void)
{
T_BLE_AUDIO_PARAMS ble_audio_param = {0};
ble_audio_param.evt_queue_handle = audio_evt_queue_handle;
ble_audio_param.io_queue_handle = audio_io_queue_handle;
ble_audio_param.bt_gatt_client_init = (GATT_CLIENT_DISCOV_MODE_REG_SVC_BIT |
GATT_CLIENT_DISCOV_MODE_CCCD_STORAGE_BIT |
GATT_CLIENT_DISCOV_MODE_USE_EXT_CLIENT);
ble_audio_param.acl_link_num = MAX_BLE_LINK_NUM;
ble_audio_param.io_event_type = IO_MSG_TYPE_LE_AUDIO;
ble_audio_init(&ble_audio_param);
app_lea_ini_bap_init();
app_lea_ini_cap_init();
app_lea_audio_data_init();
}
Configure BIS
Before broadcasting data through BIS, it is necessary to initialize the broadcast source parameters. This can be achieved through the CMD_LEA_BSRC_INIT
command, specifying codec configuration, BIS number, and ULL mode, among others. For detailed explanations of the command, please refer to ACI_CMD_LEA_BSRC_INIT.
void app_lea_ini_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
...
switch (cmd_id)
{
...
case CMD_LEA_BSRC_INIT:
{
struct
{
uint16_t cmd_id;
uint8_t codec_cfg;
uint8_t bis_num;
bool encryption;
bool ull_mode;
uint16_t pd;
} __attribute__((packed)) *bis_param = (typeof(bis_param))cmd_ptr;
T_CODEC_CFG_ITEM codec_cfg_type = (T_CODEC_CFG_ITEM)bis_param->codec_cfg;
uint8_t bis_num = bis_param->bis_num;
bool encryption = bis_param->encryption;
app_db.bis_ull_mode = bis_param->ull_mode;
uint16_t presentation_delay = bis_param->pd;
APP_PRINT_TRACE1("app_lea_ini_handle_cmd_set: bis ull mode %d", app_db.cis_ull_mode);
T_QOS_CFG_TYPE qos_type = QOS_CFG_BIS_HIG_RELIABILITY;
if (app_db.bis_ull_mode)
{
qos_type = QOS_CFG_BIS_LOW_LATENCY;
}
app_lea_bsrc_init(codec_cfg_type,
qos_type,
bis_num,
GAP_LOCAL_ADDR_LE_PUBLIC,
encryption,
presentation_delay);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
}
Establish BIS
The broadcast source defines several states as below, among which STATE_CONFIGURED_STARTING
, STATE_CONFIGURED_STOPPING
, STATE_STREAMING_STARTING
, and STATE_STREAMING_STOPPING
are intermediate states, while STATE_IDLE
, STATE_CONFIGURED
, and STATE_STREAMING
can serve as target states.
typedef enum
{
BROADCAST_SOURCE_STATE_IDLE = 0x00,
BROADCAST_SOURCE_STATE_CONFIGURED_STARTING = 0x01,
BROADCAST_SOURCE_STATE_CONFIGURED = 0x02,
BROADCAST_SOURCE_STATE_CONFIGURED_STOPPING = 0x03,
BROADCAST_SOURCE_STATE_STREAMING_STARTING = 0x04,
BROADCAST_SOURCE_STATE_STREAMING = 0x05,
BROADCAST_SOURCE_STATE_STREAMING_STOPPING = 0x06,
} T_BROADCAST_SOURCE_STATE;
By calling the app_lea_bsrc_start()
API, the broadcast will attempt to transition its state to BROADCAST_SOURCE_STATE_STREAMING
.
bool app_lea_bsrc_start(void)
{
if (app_db.bsrc_db.source_handle == NULL)
{
APP_PRINT_ERROR0("app_lea_bsrc_start: init bis firstly!");
return false;
}
app_db.bsrc_db.prefer_state = BROADCAST_SOURCE_STATE_STREAMING;
app_lea_bsrc_target_state();
return true;
}
Specifically, the module will configure and enable the extended advertisement and the periodic
advertisement through app_lea_bsrc_config()
, and create BIG through app_lea_bsrc_establish()
.
When the broadcast source successfully transitions to the BROADCAST_SOURCE_STATE_STREAMING
state
(The state changed event could be notified in app_lea_bsrc_sm_cb()
through with
MSG_BROADCAST_SOURCE_STATE_CHANGE
message), the data path will be established by calling
broadcast_source_setup_data_path()
.
void app_lea_bsrc_sm_cb(T_BROADCAST_SOURCE_HANDLE handle, uint8_t cb_type, void *p_cb_data)
{
...
switch (cb_type)
{
case MSG_BROADCAST_SOURCE_STATE_CHANGE:
{
APP_PRINT_INFO2("MSG_BROADCAST_SOURCE_STATE_CHANGE: state %d, cause 0x%x",
p_sm_data->p_state_change->state,
p_sm_data->p_state_change->cause);
app_db.bsrc_db.state = p_sm_data->p_state_change->state;
app_lea_bsrc_target_state();
if (p_sm_data->p_state_change->state == BROADCAST_SOURCE_STATE_STREAMING &&
p_sm_data->p_state_change->cause == GAP_SUCCESS)
{
uint8_t codec_id[5] = {LC3_CODEC_ID, 0, 0, 0, 0};
for (uint8_t i = 0; i < app_db.bsrc_db.group1_bis_num; i++)
{
broadcast_source_setup_data_path(app_db.bsrc_db.source_handle, i + 1,
codec_id, 0, 0, NULL);
}
}
}
break;
...
}
}
Once the data path is successfully established, notifications will also be received in
app_lea_bsrc_sm_cb()
with the message of MSG_BROADCAST_SOURCE_SETUP_DATA_PATH
. In the
app_lea_handle_bis_data_path_setup()
function, data path information is processed and stored in
the form of T_APP_LEA_ISO_CHANN
. Other modules, such as the source play module, can utilize
this information to specify the format for creating Audio Tracks. This enables data from sources
like MIC, Line-in, or USB to be transmitted via BIS.
void app_lea_bsrc_sm_cb(T_BROADCAST_SOURCE_HANDLE handle, uint8_t cb_type, void *p_cb_data)
{
...
switch (cb_type)
{
case MSG_BROADCAST_SOURCE_SETUP_DATA_PATH:
{
APP_PRINT_INFO2("MSG_BROADCAST_SOURCE_SETUP_DATA_PATH: bis_idx %d, cause 0x%x",
p_sm_data->p_setup_data_path->bis_idx,
p_sm_data->p_setup_data_path->cause);
if (p_sm_data->p_setup_data_path->cause == GAP_SUCCESS)
{
T_LEA_SETUP_DATA_PATH data = {0};
data.iso_mode = BIG_ISO_MODE;
data.iso_conn_handle = p_sm_data->p_setup_data_path->bis_conn_handle;
data.path_direction = DATA_PATH_INPUT_FLAG;
data.presentation_delay = app_db.bsrc_db.prefer_qos.presentation_delay;
memcpy(&data.codec_parsed_data, &app_db.bsrc_db.codec_cfg, sizeof(T_CODEC_CFG));
if (app_db.bsrc_db.cfg_bis_num == 1)
{
data.codec_parsed_data.audio_channel_allocation = AUDIO_LOCATION_SIL | AUDIO_LOCATION_SIR;
}
else if (app_db.bsrc_db.cfg_bis_num == 2)
{
if (p_sm_data->p_setup_data_path->bis_idx == 1)
{
data.codec_parsed_data.audio_channel_allocation = AUDIO_LOCATION_SIL;
}
else if (p_sm_data->p_setup_data_path->bis_idx == 2)
{
data.codec_parsed_data.audio_channel_allocation = AUDIO_LOCATION_SIR;
}
}
app_lea_handle_bis_data_path_setup(&data);
}
}
break;
...
}
}
void app_lea_handle_bis_data_path_setup(T_LEA_SETUP_DATA_PATH *p_data)
{
T_APP_LEA_ISO_CHANN *p_iso_chann = app_lea_find_iso_chann(p_data->iso_conn_handle,
p_data->path_direction);
uint8_t chnl_cnt;
uint8_t blocks_num = 1;
if (p_iso_chann != NULL)
{
APP_PRINT_WARN0("app_lea_handle_bis_data_path_setup: iso channel already exist");
return;
}
else
{
p_iso_chann = app_lea_add_iso_chann(p_data->iso_conn_handle,
p_data->path_direction);
if (p_iso_chann == NULL)
{
return;
}
p_iso_chann->iso_mode = p_data->iso_mode;
}
p_iso_chann->codec_data = p_data->codec_parsed_data;
p_iso_chann->presentation_delay = p_data->presentation_delay;
p_iso_chann->time_stamp = 0;
p_iso_chann->pkt_seq_num = 0;
if (p_iso_chann->codec_data.audio_channel_allocation == AUDIO_LOCATION_MONO)
{
chnl_cnt = 1;
}
else
{
chnl_cnt = __builtin_popcount(p_iso_chann->codec_data.audio_channel_allocation);
}
if (p_iso_chann->codec_data.type_exist & CODEC_CFG_TYPE_BLOCKS_PER_SDU_EXIST)
{
blocks_num = p_iso_chann->codec_data.codec_frame_blocks_per_sdu;
}
p_iso_chann->frame_num = blocks_num * chnl_cnt;
APP_PRINT_INFO7("app_lea_handle_bis_data_path_setup: iso handle 0x%04x, frame_num %d, "
"dir %u, sample_frequency 0x%x, audio_channel_allocation 0x%08x, presentation_delay 0x%x, chnl_cnt %d",
p_iso_chann->iso_conn_handle, p_iso_chann->frame_num,
p_iso_chann->path_direction,
p_iso_chann->codec_data.sample_frequency,
p_iso_chann->codec_data.audio_channel_allocation,
p_iso_chann->presentation_delay,
chnl_cnt);
if (p_data->path_direction == DATA_PATH_INPUT_FLAG)
{
if (app_db.bsrc_db.cfg_bis_num == app_db.iso_input_queue.count)
{
app_lea_save_data_format(p_iso_chann);
#if F_APP_A2DP_XMIT_SRC_LEA_SUPPORT
app_a2dp_xmit_lea_pipe_rcfg();
#endif
}
}
}
Broadcast Data
Using the app_lea_iso_data_send()
API, data can be transmitted via BIS or CIS. The ext_flag
is used to specify whether to include ts
and seq
parameters. Typically, when the data source is a MIC, Line-in, or USB, the ts
and seq
values from the registered callback in the Audio Track will be used.
void app_lea_iso_data_send(uint8_t *p_data, uint16_t len, bool ext_flag, uint32_t ts, uint16_t seq)
{
T_GAP_CAUSE cause = GAP_CAUSE_SUCCESS;
T_APP_LEA_ISO_CHANN *p_iso_chann = NULL;
uint8_t chnl_cnt = 0;
uint32_t time_stamp = 0;
uint16_t seq_num = 0;
uint32_t s = os_lock();
for (uint8_t i = 0; i < app_db.iso_input_queue.count; i++)
{
p_iso_chann = (T_APP_LEA_ISO_CHANN *)os_queue_peek(&app_db.iso_input_queue, i);
if (p_iso_chann->codec_data.audio_channel_allocation == AUDIO_LOCATION_MONO)
{
chnl_cnt = 1;
}
else
{
chnl_cnt = __builtin_popcount(p_iso_chann->codec_data.audio_channel_allocation);
}
if (ext_flag)
{
time_stamp = ts;
seq_num = seq;
}
else
{
time_stamp = (p_iso_chann->time_stamp + p_iso_chann->pkt_seq_num) *
(p_iso_chann->sdu_interval);
seq_num = p_iso_chann->pkt_seq_num;
}
if (chnl_cnt == 2)
{
cause = gap_iso_send_data(p_data,
p_iso_chann->iso_conn_handle,
len,
false,
time_stamp,
seq_num);
}
else if (chnl_cnt == 1)
{
if (p_iso_chann->codec_data.audio_channel_allocation & (AUDIO_LOCATION_FL |
AUDIO_LOCATION_SIL))
{
cause = gap_iso_send_data(p_data,
p_iso_chann->iso_conn_handle,
len / 2,
0,
time_stamp,
seq_num);
}
else
{
cause = gap_iso_send_data(p_data + (len / 2),
p_iso_chann->iso_conn_handle,
len / 2,
0,
time_stamp,
seq_num);
}
}
p_iso_chann->pkt_seq_num++;
if (cause != GAP_CAUSE_SUCCESS)
{
APP_PRINT_ERROR1("app_lea_iso_data_send: failed, cause 0x%x", cause);
}
}
os_unlock(s);
return;
}
LE Audio Initiator - CIS
Bluetooth Low Energy Audio CAP roles can be divided into Acceptor, Initiator, and Commander. In transmitter applications, the Initiator role is supported, which can transmit BIS or transmit and receive CIS.
In this chapter, the design specification for CIS will be presented.
Initialization
Initialization is performed through the function of app_lea_profile_init()
, including configuration of LE Audio parameters, initialization of BAP and CAP, as well as the necessary data processing setup.
//app_lea_ini_profile.c
void app_lea_profile_init(void)
{
T_BLE_AUDIO_PARAMS ble_audio_param = {0};
ble_audio_param.evt_queue_handle = audio_evt_queue_handle;
ble_audio_param.io_queue_handle = audio_io_queue_handle;
ble_audio_param.bt_gatt_client_init = (GATT_CLIENT_DISCOV_MODE_REG_SVC_BIT |
GATT_CLIENT_DISCOV_MODE_CCCD_STORAGE_BIT |
GATT_CLIENT_DISCOV_MODE_USE_EXT_CLIENT);
ble_audio_param.acl_link_num = MAX_BLE_LINK_NUM;
ble_audio_param.io_event_type = IO_MSG_TYPE_LE_AUDIO;
ble_audio_init(&ble_audio_param);
app_lea_ini_bap_init();
app_lea_ini_cap_init();
app_lea_audio_data_init();
}
Connect to LE Audio Device
The Initiator initiates scan by using app_lea_ini_scan_start()
to scan for nearby LE Audio devices and reports them to the ACI Host CLI Tool.
// start scanning
void app_lea_ini_scan_start(void)
{
APP_PRINT_INFO0("app_ble_scan_start");
BLE_SCAN_PARAM param;
param.own_addr_type = GAP_LOCAL_ADDR_LE_PUBLIC;
param.phys = GAP_EXT_SCAN_PHYS_1M_BIT;
param.ext_filter_policy = GAP_SCAN_FILTER_ANY;
param.ext_filter_duplicate = GAP_SCAN_FILTER_DUPLICATE_ENABLE;
param.scan_param_1m.scan_type = GAP_SCAN_MODE_PASSIVE;
param.scan_param_1m.scan_interval = 0x140;
param.scan_param_1m.scan_window = 0xD0;
param.scan_param_coded.scan_type = GAP_SCAN_MODE_PASSIVE;
param.scan_param_coded.scan_interval = 0x0050;
param.scan_param_coded.scan_window = 0x0025;
if (ble_scan_start(&app_lea_ini_scan_handle, app_lea_ini_scan_cb, ¶m, NULL))
{
app_lea_clear_scan_dev();
}
}
// report scanned devices to ACI Host
void app_lea_ini_scan_report(T_LE_EXT_ADV_REPORT_INFO *p_report)
{
T_APP_LEA_ADV_DATA lea_data = {0};
T_APP_LEA_SCAN_DEV *p_dev;
APP_PRINT_INFO3("app_lea_ini_scan_report: event_type 0x%x, bd_addr %s, addr_type %d",
p_report->event_type,
TRACE_BDADDR(p_report->bd_addr),
p_report->addr_type);
p_dev = app_lea_find_scan_dev(p_report->bd_addr, p_report->addr_type);
if (p_dev)
{
return;
}
app_lea_ini_scan_data_parse(p_report->data_len, p_report->p_data, &lea_data);
if (lea_data.adv_data_flags)
{
APP_PRINT_INFO5("app_lea_ini_scan_report: adv_data_flags 0x%x, ascs(type %d, sink 0x%x, source 0x%x), cap(type %d)",
lea_data.adv_data_flags,
lea_data.ascs_announcement_type,
lea_data.ascs_sink_available_contexts,
lea_data.ascs_source_available_contexts,
lea_data.cap_announcement_type);
app_lea_add_scan_dev(p_report->bd_addr, p_report->addr_type, &lea_data);
app_lea_ini_scan_report_result(p_report->event_type,
p_report->bd_addr,
p_report->addr_type,
&lea_data);
#if CSIP_SET_COORDINATOR
if (lea_data.adv_data_flags & APP_LEA_ADV_DATA_RSI_BIT)
{
set_coordinator_check_adv_rsi(p_report->data_len, p_report->p_data,
p_report->bd_addr, p_report->addr_type);
}
#endif
}
}
On the ACI Host side, initiate the establishment of an LE connection by invoking the app_lea_create_conn()
API using the CMD_LE_CREATE_CONN
command.
void app_cmd_ble_handle(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t app_idx,
uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
case CMD_LE_CREATE_CONN:
{
...
#if F_APP_LE_AUDIO_INITIATOR_SUPPORT
T_GAP_CAUSE gap_cause = app_lea_create_conn(CMD->remote_bd,
(T_GAP_REMOTE_ADDR_TYPE)CMD->remote_bd_type);
#else
T_GAP_CAUSE gap_cause = le_connect(CMD->init_phys, CMD->remote_bd,
(T_GAP_REMOTE_ADDR_TYPE)CMD->remote_bd_type,
(T_GAP_LOCAL_ADDR_TYPE)CMD->local_bd_type, CMD->scan_timeout);
APP_PRINT_TRACE5("app_cmd_ble_handle: init_phys %d, remote_bd_type %d, remote_bd %s, local_bd_type %d, scan_timeout %d",
CMD->init_phys, CMD->remote_bd_type, TRACE_BDADDR(CMD->remote_bd), CMD->local_bd_type,
CMD->scan_timeout);
#endif
((ACK_PKT)ack_pkt)->status = (uint16_t)gap_cause;
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
...
}
If the remote device supports coordinate set and belongs to a group of devices, the Initiator will
continue to scan for other devices in that group and automatically establish connections. The coordinated
set information is reported in LE_AUDIO_MSG_CSIS_CLIENT_READ_RESULT
message.
uint16_t app_lea_ini_csis_handle_msg(T_LE_AUDIO_MSG msg, void *buf)
{
uint16_t cb_result = BLE_AUDIO_CB_RESULT_SUCCESS;
switch (msg)
{
case LE_AUDIO_MSG_CSIS_CLIENT_READ_RESULT:
{
...
// check if there are more members in the group and start scanning again
if (p_read_result->mem_info.set_mem_size > 1 &&
app_link_get_le_link_num() < p_read_result->mem_info.set_mem_size)
{
app_lea_group_scan_start(group_handle);
}
}
break;
...
}
...
}
CIS Media
For a unicast stream, there are eight different streaming states defined as below.
typedef enum
{
AUDIO_STREAM_STATE_IDLE = 0x00, /**< Available API: bap_unicast_audio_cfg */
AUDIO_STREAM_STATE_IDLE_CONFIGURED = 0x01, /**< Available API: bap_unicast_audio_start, bap_unicast_audio_remove_cfg */
AUDIO_STREAM_STATE_CONFIGURED = 0x02, /**< Available API: bap_unicast_audio_start, bap_unicast_audio_release */
AUDIO_STREAM_STATE_STARTING = 0x03, /**< Available API: bap_unicast_audio_stop, bap_unicast_audio_release */
AUDIO_STREAM_STATE_STREAMING = 0x04, /**< Available API: bap_unicast_audio_stop, bap_unicast_audio_release,
bap_unicast_audio_update */
AUDIO_STREAM_STATE_PARTIAL_STREAMING = 0x05, /**< Available API: bap_unicast_audio_stop, bap_unicast_audio_release,
bap_unicast_audio_update */
AUDIO_STREAM_STATE_STOPPING = 0x06, /**< Available API: bap_unicast_audio_release */
AUDIO_STREAM_STATE_RELEASING = 0x07,
} T_AUDIO_STREAM_STATE;
The changes in the stream state will be notified through AUDIO_GROUP_MSG_BAP_STATE
message in app_lea_ini_group_cb()
and relayed to ACI Host CLI Tool.
void app_lea_ini_group_cb(T_AUDIO_GROUP_MSG msg, T_BLE_AUDIO_GROUP_HANDLE handle,
void *buf)
{
T_APP_RESULT result = APP_RESULT_SUCCESS;
T_APP_LEA_GROUP_INFO *p_group = app_lea_find_group(handle);
switch (msg)
{
...
case AUDIO_GROUP_MSG_BAP_STATE:
{
T_AUDIO_GROUP_BAP_STATE *p_data = (T_AUDIO_GROUP_BAP_STATE *)buf;
APP_PRINT_INFO6("AUDIO_GROUP_MSG_BAP_STATE: group handle 0x%x, session handle 0x%x, curr_action %d, state %d, result %d, cause 0x%x",
handle, p_data->handle, p_data->curr_action,
p_data->state, p_data->result, p_data->cause);
struct
{
void *group_handle;
uint8_t state;
} __attribute__((packed)) rpt = {};
rpt.group_handle = handle;
rpt.state = p_data->state;
app_report_event(CMD_PATH_UART, EVENT_LE_AUDIO_BAP_STATE, 0, (uint8_t *)&rpt,
sizeof(rpt));
if (p_group != NULL && p_group->lea_unicast.session_handle == p_data->handle)
{
p_group->lea_unicast.bap_state = p_data->state;
if ((p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE ||
p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE_CONFIGURED) &&
p_group->lea_unicast.release_req == true)
{
bap_unicast_audio_remove_session(p_group->lea_unicast.session_handle);
}
}
}
break;
...
}
}
To transmit media via CIS, users can invoke app_lea_ini_cis_media_start()
to transition the stream
state to AUDIO_STREAM_STATE_STREAMING
.
bool app_lea_ini_cis_media_start(uint8_t group_idx)
{
bool ret = false;
if (group_idx < app_db.group_handle_queue.count)
{
T_APP_LEA_GROUP_INFO *p_group = (T_APP_LEA_GROUP_INFO *)os_queue_peek(&app_db.group_handle_queue,
group_idx);
if (p_group)
{
ret = app_lea_ini_start_media(p_group->group_handle);
}
}
return ret;
}
Specifically, the functions will be invoked:
app_lea_ini_select_media_prefer_codec()
API to choose the codec type.bap_unicast_audio_cfg()
API to set unicast audio configuration.app_lea_ini_config_codec()
API to configure ASE codec.-
bap_unicast_audio_start()
API to create CIS and set up data path.static bool app_lea_ini_start_media(T_BLE_AUDIO_GROUP_HANDLE group_handle) { T_APP_LEA_GROUP_INFO *p_group; uint8_t err_idx = 0; p_group = app_lea_find_group(group_handle); if (p_group) { if (app_lea_ini_ready_to_start(p_group, AUDIO_CONTEXT_MEDIA, 0)) { if (p_group->lea_unicast.session_handle == NULL) { p_group->lea_unicast.session_handle = audio_stream_session_allocate(p_group->group_handle); if (p_group->lea_unicast.session_handle == NULL) { err_idx = 1; goto failed; } p_group->lea_unicast.bap_state = AUDIO_STREAM_STATE_IDLE; p_group->lea_unicast.contexts_type = AUDIO_CONTEXT_MEDIA; p_group->lea_unicast.target_latency = ASE_TARGET_HIGHER_RELIABILITY; } if (p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE) { if (app_lea_ini_select_media_prefer_codec(p_group) == false) { err_idx = 2; goto failed; } if (bap_unicast_audio_cfg(p_group->lea_unicast.session_handle, p_group->lea_unicast.cfg_type, p_group->lea_unicast.dev_num, p_group->lea_unicast.dev_tbl) == false) { err_idx = 3; goto failed; } if (app_lea_ini_config_codec(p_group) == false) { err_idx = 4; goto failed; } if (bap_unicast_audio_start(p_group->lea_unicast.session_handle) == false) { err_idx = 5; goto failed; } } else if (p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE_CONFIGURED || p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_CONFIGURED) { if (p_group->lea_unicast.contexts_type != AUDIO_CONTEXT_MEDIA) { err_idx = 6; goto failed; } if (bap_unicast_audio_start(p_group->lea_unicast.session_handle) == false) { err_idx = 7; goto failed; } } else { err_idx = 8; goto failed; } } else { err_idx = 9; goto failed; } } else { err_idx = 10; goto failed; } return true; failed: APP_PRINT_ERROR2("app_lea_ini_start_media: failed, group_handle 0x%x, err_idx %d", group_handle, err_idx); return false; }
Once the data path is successfully established, notifications will also be received in
app_lea_ini_group_cb()
with the message of AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH
. In the
app_lea_handle_cis_data_path_setup()
function, data path information is processed and stored in
the form of T_APP_LEA_ISO_CHANN
. Other modules, such as the source play module, can utilize
this information to specify the format for creating an Audio Track. This enables data from sources
like MIC, Line-in, or USB to be transmitted via CIS.
void app_lea_ini_group_cb(T_AUDIO_GROUP_MSG msg, T_BLE_AUDIO_GROUP_HANDLE handle,
void *buf)
{
T_APP_RESULT result = APP_RESULT_SUCCESS;
T_APP_LEA_GROUP_INFO *p_group = app_lea_find_group(handle);
switch (msg)
{
...
case AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH:
{
T_AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH *p_data = (T_AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH *)buf;
APP_PRINT_INFO6("AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH: group handle 0x%x, session handle 0x%x, dev_handle 0x%x, ase_id 0x%x, direction %d, cis_conn_handle 0x%x",
handle, p_data->handle,
p_data->dev_handle,
p_data->ase_id,
p_data->path_direction,
p_data->cis_conn_handle);
T_LEA_SETUP_DATA_PATH data = {0};
data.iso_mode = CIG_ISO_MODE;
data.iso_conn_handle = p_data->cis_conn_handle;
data.codec_parsed_data = p_data->codec_parsed_data;
data.path_direction = p_data->path_direction;
if (p_group != NULL && p_group->lea_unicast.session_handle == p_data->handle)
{
T_AUDIO_SESSION_QOS_CFG qos_cfg;
if (bap_unicast_audio_get_session_qos(p_group->lea_unicast.session_handle, &qos_cfg))
{
if (p_data->path_direction == DATA_PATH_INPUT_FLAG)
{
data.presentation_delay = qos_cfg.sink_presentation_delay;
}
else
{
data.presentation_delay = qos_cfg.source_presentation_delay;
}
}
}
app_lea_handle_cis_data_path_setup(&data);
}
break;
...
}
}
void app_lea_handle_cis_data_path_setup(T_LEA_SETUP_DATA_PATH *p_data)
{
T_APP_LEA_ISO_CHANN *p_iso_chann = app_lea_find_iso_chann(p_data->iso_conn_handle,
p_data->path_direction);
if (p_iso_chann != NULL)
{
APP_PRINT_WARN0("app_lea_handle_cis_data_path_setup: iso channel already exist");
return;
}
else
{
if (p_data->path_direction == DATA_PATH_INPUT_FLAG)
{
p_iso_chann = app_lea_add_iso_pending_chann(p_data->iso_conn_handle,
p_data->path_direction);
}
else
{
p_iso_chann = app_lea_add_iso_chann(p_data->iso_conn_handle,
p_data->path_direction);
}
if (p_iso_chann == NULL)
{
return;
}
p_iso_chann->iso_mode = p_data->iso_mode;
}
p_iso_chann->codec_data = p_data->codec_parsed_data;
p_iso_chann->presentation_delay = p_data->presentation_delay;
p_iso_chann->time_stamp = 0;
p_iso_chann->pkt_seq_num = 0;
p_iso_chann->frame_num = app_lea_get_frame_num(p_iso_chann);
//lea src
if (p_data->path_direction == DATA_PATH_INPUT_FLAG)
{
uint8_t current_iso_cnt = app_db.input_path_pending_q.count + app_db.iso_input_queue.count;
uint8_t link_num = app_link_get_le_link_num();
APP_PRINT_INFO3("app_lea_handle_cis_data_path_setup: current_iso_cnt %d conn_dev %d, link_num %d",
current_iso_cnt,
app_db.conn_dev_queue.count,
link_num);
if (current_iso_cnt >= link_num)
{
uint8_t i;
uint8_t n = app_db.input_path_pending_q.count;
APP_PRINT_INFO1("handle_cis_data_path_setup_cmplt_msg: pending num %u",
app_db.input_path_pending_q.count);
for (i = 0; i < n; i++)
{
p_iso_chann = (T_APP_LEA_ISO_CHANN *)os_queue_out(&app_db.input_path_pending_q);
APP_PRINT_INFO2("app_lea_handle_cis_data_path_setup: adding path handle %u curr_num %x",
p_iso_chann->iso_conn_handle,
app_db.iso_input_queue.count);
os_queue_in(&app_db.iso_input_queue, (void *)p_iso_chann);
}
app_lea_save_data_format(p_iso_chann);
}
}
APP_PRINT_INFO6("app_lea_handle_cis_data_path_setup: iso handle 0x%04x, frame_num %d, "
"dir %u, sample_frequency 0x%x, audio_channel_allocation 0x%08x, presentation_delay 0x%x",
p_iso_chann->iso_conn_handle, p_iso_chann->frame_num,
p_iso_chann->path_direction,
p_iso_chann->codec_data.sample_frequency,
p_iso_chann->codec_data.audio_channel_allocation,
p_iso_chann->presentation_delay);
}
CIS Conversation
Differing from CIS media, CIS Conversation both transmits and receives data streams. However, what is similar is that they share the stream state defined by unicast stream, which can be referenced in chapter CIS Media for an introduction to stream states.
To transmit and receive audio streams via CIS, users can invoke app_lea_ini_start_conversation()
to transition the stream state to AUDIO_STREAM_STATE_STREAMING
. Specifically, the functions will be invoked:
app_lea_ini_select_conversation_prefer_codec()
API to choose the codec type.bap_unicast_audio_cfg()
API to set unicast audio configuration.app_lea_ini_config_codec()
API to configure ASE codec.-
bap_unicast_audio_start()
API to create CIS and set up data path(s).bool app_lea_ini_start_conversation(T_BLE_AUDIO_GROUP_HANDLE group_handle) { T_APP_LEA_GROUP_INFO *p_group; uint8_t err_idx = 0; p_group = app_lea_find_group(group_handle); if (p_group) { if (app_lea_ini_ready_to_start(p_group, AUDIO_CONTEXT_CONVERSATIONAL, AUDIO_CONTEXT_CONVERSATIONAL)) { if (p_group->lea_unicast.session_handle == NULL) { p_group->lea_unicast.session_handle = audio_stream_session_allocate(p_group->group_handle); if (p_group->lea_unicast.session_handle == NULL) { err_idx = 1; goto failed; } p_group->lea_unicast.bap_state = AUDIO_STREAM_STATE_IDLE; p_group->lea_unicast.contexts_type = AUDIO_CONTEXT_CONVERSATIONAL; p_group->lea_unicast.target_latency = ASE_TARGET_LOWER_LATENCY; } if (p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE) { if (app_lea_ini_select_conversation_prefer_codec(p_group) == false) { err_idx = 2; goto failed; } if (bap_unicast_audio_cfg(p_group->lea_unicast.session_handle, p_group->lea_unicast.cfg_type, p_group->lea_unicast.dev_num, p_group->lea_unicast.dev_tbl) == false) { err_idx = 3; goto failed; } if (app_lea_ini_config_codec(p_group) == false) { err_idx = 4; goto failed; } if (bap_unicast_audio_start(p_group->lea_unicast.session_handle) == false) { err_idx = 5; goto failed; } } else if (p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_IDLE_CONFIGURED || p_group->lea_unicast.bap_state == AUDIO_STREAM_STATE_CONFIGURED) { if (p_group->lea_unicast.contexts_type != AUDIO_CONTEXT_CONVERSATIONAL) { err_idx = 6; goto failed; } if (bap_unicast_audio_start(p_group->lea_unicast.session_handle) == false) { err_idx = 7; goto failed; } } else { err_idx = 8; goto failed; } } else { err_idx = 9; goto failed; } } else { err_idx = 10; goto failed; } return true; failed: APP_PRINT_ERROR2("app_lea_ini_start_conversation: failed, group_handle 0x%x, err_idx %d", group_handle, err_idx); return false; }
Similar to CIS Media, once the data path is successfully established, notifications will also be received in app_lea_ini_group_cb()
with the message of AUDIO_GROUP_MSG_BAP_SETUP_DATA_PATH
. In the
app_lea_handle_cis_data_path_setup()
function, data path information is processed and stored in
the form of T_APP_LEA_ISO_CHANN
.
Unlike CIS Media, CIS Conversation establishes both an input-direction data path for sending data and an output-direction data path for receiving data (The direction is defined from the perspective of the Bluetooth Controller).
Unicast Data
CIS, similar to BIS, utilize the API app_lea_iso_data_send()
to send data. Please refer to Broadcast Data for more details.
For CIS Conversation, data reception is also a part of its functionality. Data is reported through the registered app_lea_data_direct_cb()
, allowing further data processing within this function.
void app_lea_data_direct_cb(uint8_t cb_type, void *p_cb_data)
{
T_BT_DIRECT_CB_DATA *p_data = (T_BT_DIRECT_CB_DATA *)p_cb_data;
switch (cb_type)
{
case BT_DIRECT_MSG_ISO_DATA_IND:
{
#if 0
APP_PRINT_TRACE5("app_lea_data_direct_cb: conn_handle 0x%x, iso_sdu_len %d, pkt_seq_num 0x%x, time_stamp 0x%x, pkt_status_flag 0x%x",
p_data->p_bt_direct_iso->conn_handle, p_data->p_bt_direct_iso->iso_sdu_len,
p_data->p_bt_direct_iso->pkt_seq_num, p_data->p_bt_direct_iso->time_stamp,
p_data->p_bt_direct_iso->pkt_status_flag);
#endif
#if 0
uint8_t *p_iso_data = p_data->p_bt_direct_iso->p_buf + p_data->p_bt_direct_iso->offset;
APP_PRINT_INFO5("app_lea_data_direct_cb: BT_DIRECT_MSG_ISO_DATA_IND, iso_sdu_len 0x%x, p_buf %p, offset %d, p_data %p, data %b",
p_data->p_bt_direct_iso->iso_sdu_len, p_data->p_bt_direct_iso->p_buf,
p_data->p_bt_direct_iso->offset, p_iso_data, TRACE_BINARY(p_data->p_bt_direct_iso->iso_sdu_len,
p_iso_data));
#endif
APP_PRINT_TRACE5("app_lea_data_direct_cb: conn_handle 0x%x, iso_sdu_len %d, pkt_seq_num 0x%x, time_stamp 0x%x, pkt_status_flag 0x%x",
p_data->p_bt_direct_iso->conn_handle, p_data->p_bt_direct_iso->iso_sdu_len,
p_data->p_bt_direct_iso->pkt_seq_num, p_data->p_bt_direct_iso->time_stamp,
p_data->p_bt_direct_iso->pkt_status_flag);
if (p_data->p_bt_direct_iso->pkt_status_flag != ISOCH_DATA_PKT_STATUS_VALID_DATA)
{
gap_iso_data_cfm(p_data->p_bt_direct_iso->p_buf);
break;
}
T_APP_LEA_ISO_CHANN *p_iso_chann = app_lea_find_iso_chann(p_data->p_bt_direct_iso->conn_handle,
DATA_PATH_OUTPUT_FLAG);
if (p_iso_chann != NULL)
{
#if 0
//Just for sample. Application can send the data to DSP.
uint16_t written_len;
T_AUDIO_STREAM_STATUS status;
if (p_data->p_bt_direct_iso->iso_sdu_len != 0)
{
status = AUDIO_STREAM_STATUS_CORRECT;
}
else
{
status = AUDIO_STREAM_STATUS_LOST;
}
audio_track_write(handle, p_data->p_bt_direct_iso->time_stamp,
p_data->p_bt_direct_iso->pkt_seq_num,
status,
p_iso_chann->frame_num,
p_data->p_bt_direct_iso->p_buf + p_data->p_bt_direct_iso->offset,
p_data->p_bt_direct_iso->iso_sdu_len,
&written_len);
#endif
}
gap_iso_data_cfm(p_data->p_bt_direct_iso->p_buf);
}
break;
default:
APP_PRINT_ERROR1("app_lea_data_direct_cb: unhandled cb_type 0x%x", cb_type);
break;
}
}
Bluetooth Audio Receiver
A2DP Sink
A2DP Profile Initialization
bt_a2dp_init()
is utilized in app_a2dp_init()
to initialize the A2DP profile and configure the maximum number of remote devices and A2DP latency. A2DP latency is used to enable synchronization of audio and video playback by reporting SNK delay values caused by buffering, decoding, and rendering.
bt_a2dp_role_set()
is utilized to set the Sink or Source of the A2DP role.
bt_a2dp_stream_endpoint_add()
is utilized to add an A2DP stream endpoint.
void app_a2dp_init(void)
{
if (app_cfg_const.supported_profile_mask & A2DP_PROFILE_MASK)
{
bt_a2dp_init(app_cfg_const.a2dp_link_number, A2DP_LATENCY_MS);
#if (F_APP_A2DP_SOURCE_SUPPORT || F_APP_A2DP_XMIT_SRC_SUPPORT || F_SOURCE_PLAY_SUPPORT)
bt_a2dp_role_set(BT_A2DP_ROLE_SRC);
#else
bt_a2dp_role_set(BT_A2DP_ROLE_SNK);
#endif
if (app_cfg_const.a2dp_codec_type_sbc)
{
T_BT_A2DP_STREAM_ENDPOINT sep;
sep.codec_type = BT_A2DP_CODEC_TYPE_SBC;
sep.u.codec_sbc.sampling_frequency_mask = app_cfg_const.sbc_sampling_frequency;
sep.u.codec_sbc.channel_mode_mask = app_cfg_const.sbc_channel_mode;
sep.u.codec_sbc.block_length_mask = app_cfg_const.sbc_block_length;
sep.u.codec_sbc.subbands_mask = app_cfg_const.sbc_subbands;
sep.u.codec_sbc.allocation_method_mask = app_cfg_const.sbc_allocation_method;
sep.u.codec_sbc.min_bitpool = app_cfg_const.sbc_min_bitpool;
sep.u.codec_sbc.max_bitpool = app_cfg_const.sbc_max_bitpool;
bt_a2dp_stream_endpoint_add(sep);
}
if (app_cfg_const.a2dp_codec_type_aac)
{
T_BT_A2DP_STREAM_ENDPOINT sep;
sep.codec_type = BT_A2DP_CODEC_TYPE_AAC;
sep.u.codec_aac.object_type_mask = app_cfg_const.aac_object_type;
sep.u.codec_aac.sampling_frequency_mask = app_cfg_const.aac_sampling_frequency;
sep.u.codec_aac.channel_number_mask = app_cfg_const.aac_channel_number;
sep.u.codec_aac.vbr_supported = app_cfg_const.aac_vbr_supported;
sep.u.codec_aac.bit_rate = app_cfg_const.aac_bit_rate;
bt_a2dp_stream_endpoint_add(sep);
}
#if F_APP_A2DP_CODEC_LDAC_SUPPORT
if (app_cfg_const.a2dp_codec_type_ldac)
{
T_BT_A2DP_STREAM_ENDPOINT sep;
sep.codec_type = BT_A2DP_CODEC_TYPE_LDAC;
sep.u.codec_ldac.sampling_frequency_mask = app_cfg_const.ldac_sampling_frequency;
sep.u.codec_ldac.channel_mode_mask = app_cfg_const.ldac_channel_mode;
bt_a2dp_stream_endpoint_add(sep);
}
#endif
bt_mgr_cback_register(app_a2dp_bt_cback);
}
A2DP Connect
In linkback_profile_search_start()
, the function gap_br_start_sdp_discov()
will first be called to discover A2DP SDP according to A2DP Sink’s UUID.
bool linkback_profile_search_start(uint8_t *bd_addr, uint32_t prof, bool is_special)
{
bool ret = true;
T_GAP_UUID_DATA uuid;
T_GAP_UUID_TYPE uuid_type = GAP_UUID16;
...
switch (prof)
{
...
case A2DP_SINK_PROFILE_MASK:
{
uuid.uuid_16 = UUID_AUDIO_SINK;
}
break;
...
if (ret)
{
if (gap_br_start_sdp_discov(bd_addr, uuid_type, uuid) != GAP_CAUSE_SUCCESS)
{
ret = false;
}
}
return ret;
}
Then, A2DP Sink will initiate profile connection by bt_a2dp_connect_req()
.
bool linkback_profile_connect_start(uint8_t *bd_addr, uint32_t prof, T_LINKBACK_CONN_PARAM *param)
{
bool ret = true;
...
switch (prof)
{
case A2DP_PROFILE_MASK:
ret = bt_a2dp_connect_req(bd_addr, param->protocol_version);
break;
...
return ret;
}
For A2DP Sink, app_a2dp_bt_cback()
function is used to handle Bluetooth Manager A2DP Sink-related events.
bt_a2dp_connect_cfm()
accepts or rejects the incoming connection from the remote device.
static void app_a2dp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
T_APP_BR_LINK *p_active_a2dp_link = &app_db.br_link[active_a2dp_idx];
bool handle = true;
T_APP_BR_LINK *p_link = NULL;
switch (event_type)
{
case BT_EVENT_A2DP_CONN_IND:
{
T_APP_BR_LINK *p_link = app_link_find_br_link(param->a2dp_conn_ind.bd_addr);
if (p_link != NULL)
{
bt_a2dp_connect_cfm(p_link->bd_addr, true);
}
}
break;
...
}
A2DP Streaming Control
For A2DP Sink, the app_a2dp_bt_cback()
function is used to handle Bluetooth Manager A2DP Streaming events. app_judge_active_a2dp_idx_and_qos()
will be called to determine an A2DP link on one device as the active link, allowing AVRCP to control it.
static void app_a2dp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
T_APP_BR_LINK *p_active_a2dp_link = &app_db.br_link[active_a2dp_idx];
bool handle = true;
T_APP_BR_LINK *p_link = NULL;
switch (event_type)
{
...
case BT_EVENT_A2DP_STREAM_START_IND:
{
if ((p_active_a2dp_link->a2dp.is_streaming == false ||
p_active_a2dp_link->avrcp.play_status == BT_AVRCP_PLAY_STATUS_PAUSED) ||
(memcmp(p_active_a2dp_link->bd_addr,
param->a2dp_stream_start_ind.bd_addr, 6) == 0))
{
APP_PRINT_INFO3("app_a2dp_bt_cback: BT_EVENT_A2DP_STREAM_START_IND active_a2dp_idx %d, streaming_fg %d, avrcp_play_status %d",
active_a2dp_idx, p_active_a2dp_link->a2dp.is_streaming, p_active_a2dp_link->avrcp.play_status);
app_sniff_mode_b2s_disable_all(SNIFF_DISABLE_MASK_A2DP);
app_audio_set_bud_stream_state(BUD_STREAM_STATE_AUDIO);
bt_a2dp_stream_start_cfm(p_active_a2dp_link->bd_addr, true);
p_active_a2dp_link->a2dp.is_streaming = true;
app_judge_active_a2dp_idx_and_qos(p_active_a2dp_link->id, JUDGE_EVENT_A2DP_START);
}
}
break;
case BT_EVENT_A2DP_STREAM_START_RSP:
{
if (p_active_a2dp_link->a2dp.is_streaming == false ||
(memcmp(p_active_a2dp_link->bd_addr,
param->a2dp_stream_start_rsp.bd_addr, 6) == 0))
{
APP_PRINT_INFO2("app_a2dp_bt_cback: BT_EVENT_A2DP_STREAM_START_RSP active_a2dp_idx %d, streaming_fg %d",
active_a2dp_idx, p_active_a2dp_link->a2dp.is_streaming);
app_sniff_mode_b2s_disable_all(SNIFF_DISABLE_MASK_A2DP);
app_audio_set_bud_stream_state(BUD_STREAM_STATE_AUDIO);
p_link = app_link_find_br_link(param->a2dp_stream_start_rsp.bd_addr);
if (p_link != NULL)
{
p_link->a2dp.is_streaming = true;
app_judge_active_a2dp_idx_and_qos(p_link->id, JUDGE_EVENT_A2DP_START);
}
}
}
break;
case BT_EVENT_A2DP_STREAM_STOP:
{
if (memcmp(p_active_a2dp_link->bd_addr,
param->a2dp_stream_stop.bd_addr, 6) == 0)
{
if (app_link_get_a2dp_start_num() <= 1)
{
app_sniff_mode_b2s_enable_all(SNIFF_DISABLE_MASK_A2DP);
}
if (app_hfp_get_call_status() == APP_HFP_CALL_IDLE)
{
app_audio_set_bud_stream_state(BUD_STREAM_STATE_IDLE);
}
}
p_link = app_link_find_br_link(param->a2dp_stream_stop.bd_addr);
if (p_link != NULL)
{
p_link->a2dp.is_streaming = false;
app_judge_active_a2dp_idx_and_qos(p_link->id, JUDGE_EVENT_A2DP_STOP);
}
}
break;
case BT_EVENT_A2DP_STREAM_CLOSE:
{
if (memcmp(p_active_a2dp_link->bd_addr,
param->a2dp_stream_close.bd_addr, 6) == 0)
{
if (app_hfp_get_call_status() == APP_HFP_CALL_IDLE)
{
app_audio_set_bud_stream_state(BUD_STREAM_STATE_IDLE);
}
}
p_link = app_link_find_br_link(param->a2dp_stream_close.bd_addr);
if (p_link != NULL)
{
p_link->a2dp.is_streaming = false;
app_judge_active_a2dp_idx_and_qos(p_link->id, JUDGE_EVENT_A2DP_STOP);
}
}
break;
...
}
void app_judge_active_a2dp_idx_and_qos(uint8_t app_idx, T_APP_JUDGE_A2DP_EVENT event)
{
uint8_t active_a2dp_idx = app_a2dp_get_active_idx();
uint8_t active_hf_idx = app_hfp_get_active_idx();
APP_PRINT_TRACE6("app_judge_active_a2dp_idx_and_qos: 1 event %d, active_a2dp_idx %d, app_idx %d, "
"a2dp.wait_resume_link_id %d, streaming_fg %d, active_media_paused %d",
event, active_a2dp_idx, app_idx, a2dp.wait_resume_link_id,
app_db.br_link[active_a2dp_idx].a2dp.is_streaming, app_db.active_media_paused);
switch (event)
{
case JUDGE_EVENT_A2DP_CONNECTED:
{
uint8_t link_number = app_connected_profile_link_num(A2DP_PROFILE_MASK);
if (link_number <= 1)
{
set_active_a2dp_avrcp(app_db.br_link[app_idx].bd_addr);
app_bond_set_priority(app_db.br_link[app_idx].bd_addr);
if (link_number <= 0)
{
//exception
}
}
else
{
if ((app_db.br_link[active_a2dp_idx].a2dp.is_streaming == false) &&
(app_hfp_get_call_status() == APP_HFP_CALL_IDLE))
{
if (app_cfg_const.enable_multi_link)
{
app_bond_set_priority(app_db.br_link[app_idx].bd_addr);
app_bond_set_priority(app_db.br_link[find_other_link_by_bond_prio(app_idx)].bd_addr);
}
else
{
set_active_a2dp_avrcp(app_db.br_link[app_idx].bd_addr);
app_bond_set_priority(app_db.br_link[app_idx].bd_addr);
}
}
}
}
break;
case JUDGE_EVENT_A2DP_START:
{
APP_PRINT_TRACE3("JUDGE_EVENT_A2DP_START: active_a2dp_idx %d, avrcp %d, stream %d",
active_a2dp_idx,
app_db.br_link[active_a2dp_idx].avrcp.play_status,
app_db.br_link[active_a2dp_idx].a2dp.is_streaming);
if (app_cfg_const.enable_multi_sco_disc_resume)
{
app_pause_other_a2dp_avrcp(active_hf_idx, true);
}
else
{
app_pause_other_a2dp_avrcp(active_hf_idx, false);
}
app_bt_policy_qos_param_update(app_db.br_link[app_idx].bd_addr, BP_TPOLL_A2DP_PLAY_EVENT);
}
break;
case JUDGE_EVENT_A2DP_DISC:
{
if (active_a2dp_idx == app_idx)
{
//app_bt_sniffing_stop(app_db.br_link[app_idx].bd_addr, BT_SNIFFING_TYPE_A2DP);
if (app_connected_profile_link_num(A2DP_PROFILE_MASK))
{
set_active_a2dp_avrcp(app_db.br_link[find_other_link_by_bond_prio(
app_idx)].bd_addr);
app_bond_set_priority(app_db.br_link[find_other_link_by_bond_prio(app_idx)].bd_addr);
}
}
app_bt_policy_qos_param_update(app_db.br_link[app_idx].bd_addr, BP_TPOLL_A2DP_STOP_EVENT);
}
break;
case JUDGE_EVENT_A2DP_STOP:
{
app_db.br_link[app_idx].avrcp.play_status = BT_AVRCP_PLAY_STATUS_PAUSED;
#if F_APP_MUTILINK_VA_PREEMPTIVE
app_db.br_link[app_idx].a2dp.stream_only = false;
#endif
if ((active_a2dp_idx == app_idx) && (a2dp.wait_resume_link_id == MAX_BR_LINK_NUM))
{
uint8_t i;
for (i = 0; i < MAX_BR_LINK_NUM; i++)
{
uint8_t idx = find_other_link_by_bond_prio(active_a2dp_idx);
if (app_db.br_link[idx].connected_profile &
(A2DP_PROFILE_MASK | AVRCP_PROFILE_MASK))
{
if ((idx != active_a2dp_idx) &&
(app_db.br_link[idx].a2dp.is_streaming == true))
{
APP_PRINT_TRACE2("JUDGE_EVENT_A2DP_STOP: active_a2dp_idx %d, idx %d", active_a2dp_idx, idx);
set_active_a2dp_avrcp_with_prio(idx);
break;
}
}
}
}
app_bt_policy_qos_param_update(app_db.br_link[app_idx].bd_addr, BP_TPOLL_A2DP_STOP_EVENT);
}
break;
case JUDGE_EVENT_MEDIAPLAYER_PLAYING:
{
if (app_cfg_const.disable_multilink_preemptive)
{
}
else
{
APP_PRINT_TRACE3("JUDGE_EVENT_MEDIAPLAYER_PLAYING: preemptive active_a2dp_idx %d, app_idx %d, streaming_fg %d",
active_a2dp_idx, app_idx, app_db.br_link[app_idx].a2dp.is_streaming);
if (app_db.br_link[app_idx].a2dp.is_streaming == true)
{
if (app_hfp_get_call_status() != APP_HFP_CALL_IDLE)
{
if (app_cfg_const.enable_multi_sco_disc_resume)
{
app_pause_other_a2dp_avrcp(active_hf_idx, true);
}
else
{
app_pause_other_a2dp_avrcp(active_hf_idx, false);
}
}
else
{
set_active_a2dp_avrcp(app_db.br_link[app_idx].bd_addr);
app_pause_other_a2dp_avrcp(app_idx, false);
app_bond_set_priority(app_db.br_link[app_idx].bd_addr);
}
}
}
app_bt_policy_qos_param_update(app_db.br_link[app_idx].bd_addr, BP_TPOLL_A2DP_PLAY_EVENT);
}
break;
case JUDGE_EVENT_MEDIAPLAYER_PAUSED:
{
}
break;
case JUDGE_EVENT_SNIFFING_STOP:
{
}
break;
default:
break;
}
if (active_a2dp_idx == app_idx)
{
if (event == JUDGE_EVENT_A2DP_START)
{
app_audio_a2dp_play_status_update(APP_A2DP_STREAM_A2DP_START);
if (app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off)
{
app_auto_power_off_disable(AUTO_POWER_OFF_MASK_AUDIO);
}
}
else if (event == JUDGE_EVENT_A2DP_STOP)
{
app_audio_a2dp_play_status_update(APP_A2DP_STREAM_A2DP_STOP);
if (app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off)
{
app_auto_power_off_enable(AUTO_POWER_OFF_MASK_AUDIO,
app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off);
}
}
}
APP_PRINT_TRACE4("app_judge_active_a2dp_idx_and_qos: 2 event %d, active_a2dp_idx %d, app_idx %d, "
"a2dp.wait_resume_link_id %d",
event, active_a2dp_idx, app_idx, a2dp.wait_resume_link_id);
}
HFP HF
HFP roles can be divided into AG and HF:
Audio Gateway (AG) is the device that is the gateway of the audio, both for input and output. Typical devices acting as Audio Gateways are cellular phones.
Hands-Free unit (HF) is the device acting as the Audio Gateway’s remote audio input and output mechanism. It also provides some remote control means.
The code flow of HFP HF in the receiver role is introduced here.
HFP HF UUID
HFP HF’s UUID registration is referred to in hfp_sdp_record()
.
static const uint8_t hfp_sdp_record[] =
{
//total length
SDP_DATA_ELEM_SEQ_HDR,
0x4B,//0x37,//0x59,
//attribute SDP_ATTR_SRV_CLASS_ID_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_SRV_CLASS_ID_LIST >> 8),
(uint8_t)SDP_ATTR_SRV_CLASS_ID_LIST,
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x06, //6bytes
SDP_UUID16_HDR, //0x19
(uint8_t)(UUID_HANDSFREE >> 8), //0x111E
(uint8_t)(UUID_HANDSFREE),
SDP_UUID16_HDR, //0x19
(uint8_t)(UUID_GENERIC_AUDIO >> 8), //0x1203
(uint8_t)(UUID_GENERIC_AUDIO),
//attribute SDP_ATTR_PROTO_DESC_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_PROTO_DESC_LIST >> 8),
(uint8_t)SDP_ATTR_PROTO_DESC_LIST,
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x0C, //12bytes
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x03, //3bytes
SDP_UUID16_HDR, //0x19
(uint8_t)(UUID_L2CAP >> 8), //0x0100
(uint8_t)(UUID_L2CAP),
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x05, //5bytes
SDP_UUID16_HDR, //0x19
(uint8_t)(UUID_RFCOMM >> 8), //0x0003
(uint8_t)(UUID_RFCOMM),
SDP_UNSIGNED_ONE_BYTE, //0x08
RFC_HFP_CHANN_NUM, //0x02
//attribute SDP_ATTR_BROWSE_GROUP_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_BROWSE_GROUP_LIST >> 8),
(uint8_t)SDP_ATTR_BROWSE_GROUP_LIST,
SDP_DATA_ELEM_SEQ_HDR,
0x03,
SDP_UUID16_HDR,
(uint8_t)(UUID_PUBLIC_BROWSE_GROUP >> 8),
(uint8_t)UUID_PUBLIC_BROWSE_GROUP,
/*
//attribute SDP_ATTR_LANG_BASE_ATTR_ID_LIST...it is used for SDP_ATTR_SRV_NAME
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_LANG_BASE_ATTR_ID_LIST >> 8),
(uint8_t)SDP_ATTR_LANG_BASE_ATTR_ID_LIST,
SDP_DATA_ELEM_SEQ_HDR,
0x09,
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_LANG_ENGLISH >> 8),
(uint8_t)SDP_LANG_ENGLISH,
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_CHARACTER_UTF8 >> 8),
(uint8_t)SDP_CHARACTER_UTF8,
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_BASE_LANG_OFFSET >> 8),
(uint8_t)SDP_BASE_LANG_OFFSET,
//attribute SDP_ATTR_SRV_NAME
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET) >> 8),
(uint8_t)(SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET),
SDP_STRING_HDR, //0x25 text string
0x0F, //15 bytes
0x48, 0x61, 0x6e, 0x64, 0x73, 0x2d, 0x66, 0x72, 0x65, 0x65,
0x20, 0x75, 0x6e, 0x69, 0x74, //"Hands-free unit"
*/
//attribute SDP_ATTR_PROFILE_DESC_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_PROFILE_DESC_LIST >> 8),
(uint8_t)SDP_ATTR_PROFILE_DESC_LIST,
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x08, //8 bytes
SDP_DATA_ELEM_SEQ_HDR, //0x35
0x06, //6 bytes
SDP_UUID16_HDR, //0x19
(uint8_t)(UUID_HANDSFREE >> 8), //0x111E
(uint8_t)(UUID_HANDSFREE),
SDP_UNSIGNED_TWO_BYTE, //0x09
(uint8_t)(0x0107 >> 8), //version number default hf1.7
(uint8_t)(0x0107),
//Attribute SDP_ATTR_SRV_NAME
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET) >> 8),
(uint8_t)(SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET),
SDP_STRING_HDR,
0x0F,
'H', 'a', 'n', 'd', 's', '-', 'F', 'r', 'e', 'e', ' ', 'u', 'n', 'i', 't',
//attribute SDP_ATTR_SUPPORTED_FEATURES
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_SUPPORTED_FEATURES) >> 8),
(uint8_t)(SDP_ATTR_SUPPORTED_FEATURES),
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(0x003F >> 8),
(uint8_t)(0x003F)
};
HFP Profile Initialization
bt_hfp_init()
is utilized in app_hfp_init()
to initialize the HFP profile, and to configure the maximum link number of HFP links. It sets the HFP RFCOMM channel number and configures the supported HFP features. HFP HF features are initialized in app_cfg_const.hfp_brsf_capability
.
void app_hfp_hf_init(void)
{
if (app_cfg_const.supported_profile_mask & (HFP_PROFILE_MASK | HSP_PROFILE_MASK))
{
bt_hfp_init(app_cfg_const.hfp_link_number, RFC_HFP_CHANN_NUM,
RFC_HSP_CHANN_NUM, app_cfg_const.hfp_hf_brsf_capability);
audio_mgr_cback_register(app_hfp_audio_cback);
bt_mgr_cback_register(app_hfp_bt_cback);
app_timer_reg_cb(app_hfp_timeout_cb, &hfp.tmr.id);
}
}
void app_hfp_init(void)
{
#if (F_APP_SCO_XMIT_AG_SUPPORT || F_SOURCE_PLAY_SUPPORT)
app_hfp_ag_init();
#else
app_hfp_hf_init();
#endif
}
HFP Profile Linkback
HFP HF will call gap_br_start_sdp_discov()
in linkback_profile_search_start()
to discover the HFP SDP record in AG.
bool linkback_profile_search_start(uint8_t *bd_addr, uint32_t prof, bool is_special)
{
bool ret = true;
T_GAP_UUID_DATA uuid;
T_GAP_UUID_TYPE uuid_type = GAP_UUID16;
...
switch (prof)
{
...
case HFP_HF_PROFILE_MASK:
{
uuid.uuid_16 = UUID_HANDSFREE;
}
break;
...
if (ret)
{
if (gap_br_start_sdp_discov(bd_addr, uuid_type, uuid) != GAP_CAUSE_SUCCESS)
{
ret = false;
}
}
return ret;
}
Then, HFP HF will initiate a profile connection to AG by bt_hfp_connect_req()
.
Note
If both devices initiate a connection at the same time, both channels shall be closed and each device shall wait a random time (Not more than 1s and not less than 100ms) and then try to initiate the connection again. If it is known which device is the master, that device can retry at once.
bool linkback_profile_connect_start(uint8_t *bd_addr, uint32_t prof, T_LINKBACK_CONN_PARAM *param)
{
bool ret = true;
...
switch (prof)
{
case HFP_PROFILE_MASK:
#if (F_APP_SCO_XMIT_AG_SUPPORT || F_SOURCE_PLAY_SUPPORT)
ret = bt_hfp_ag_connect_req(bd_addr, param->server_channel, true);
#else
ret = bt_hfp_connect_req(bd_addr, param->server_channel, true);
#endif
break;
return ret;
}
HFP Profile Callback Handler
For HFP HF, the app_hfp_bt_cback()
function is used to handle Bluetooth Manager HFP HF-related events:
HFP profile connection request should be confirmed when receiving
BT_EVENT_HFP_CONN_IND
.HFP profile connection is completed when receiving
BT_EVENT_HFP_CONN_CMPL
.HFP profile disconnection is indicated when receiving
BT_EVENT_HFP_DISCONN_CMPL
.
static void app_hfp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
switch (event_type)
{
case BT_EVENT_HFP_CONN_IND:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_conn_ind.bd_addr);
if (p_link == NULL)
{
APP_PRINT_ERROR0("app_hfp_bt_cback: no acl link found");
return;
}
bt_hfp_connect_cfm(p_link->bd_addr, true);
}
break;
case BT_EVENT_HFP_CONN_CMPL:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_conn_cmpl.bd_addr);
if (p_link != NULL)
{
uint8_t link_number;
uint8_t pair_idx_mapping;
p_link->hfp.call_id_type_chk = true;
p_link->hfp.call_id_type_num = false;
app_bond_get_pair_idx_mapping(p_link->bd_addr, &pair_idx_mapping);
bt_hfp_speaker_gain_level_report(p_link->bd_addr, app_cfg_nv.voice_gain_level[pair_idx_mapping]);
bt_hfp_microphone_gain_level_report(p_link->bd_addr, 0x0a);
app_hfp_batt_level_report(p_link->bd_addr);
link_number = app_connected_profile_link_num(HFP_PROFILE_MASK | HSP_PROFILE_MASK);
if (link_number == 1)
{
app_hfp_set_active_idx(p_link->bd_addr);
app_bond_set_priority(p_link->bd_addr);
}
if (app_db.br_link[app_db.first_hf_index].hfp.state == APP_HF_STATE_STANDBY)
{
app_db.first_hf_index = p_link->id;
}
else
{
app_db.last_hf_index = p_link->id;
}
p_link->hfp.state = APP_HF_STATE_CONNECTED;
}
}
break;
case BT_EVENT_HFP_VOICE_RECOGNITION_ACTIVATION:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_voice_recognition_activation.bd_addr);
if (p_link != NULL)
{
if (p_link->hfp.call_status == APP_HFP_CALL_IDLE)
{
p_link->hfp.call_status = APP_HFP_VOICE_ACTIVATION_ONGOING;
}
app_hfp_update_call_status();
if (p_link->remote_device_vendor_id == APP_REMOTE_DEVICE_IOS)
{
app_start_timer(&hfp.tmr.indices[TIMER_CANCEL_VOICE_DAIL], "cancel_iphone_voice_dail",
hfp.tmr.id, TIMER_CANCEL_VOICE_DAIL, false,
p_link->id, 1000);
}
}
}
break;
case BT_EVENT_HFP_VOICE_RECOGNITION_DEACTIVATION:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_voice_recognition_deactivation.bd_addr);
if (p_link != NULL)
{
if (p_link->hfp.call_status == APP_HFP_VOICE_ACTIVATION_ONGOING)
{
p_link->hfp.call_status = APP_HFP_CALL_IDLE;
}
app_hfp_update_call_status();
}
}
break;
case BT_EVENT_HFP_CALL_STATUS:
{
T_APP_BR_LINK *p_link;
#if C_APP_END_OUTGOING_CALL_PLAY_CALL_END_TONE == 0
uint8_t temp_idx = hfp.active_link_id;
#endif
p_link = app_link_find_br_link(param->hfp_call_status.bd_addr);
if (p_link != NULL)
{
switch (param->hfp_call_status.curr_status)
{
case BT_HFP_CALL_IDLE:
{
p_link->hfp.call_status = APP_HFP_CALL_IDLE;
}
break;
case BT_HFP_CALL_INCOMING:
{
p_link->hfp.call_status = APP_HFP_CALL_INCOMING;
}
break;
case BT_HFP_CALL_OUTGOING:
{
p_link->hfp.call_status = APP_HFP_CALL_OUTGOING;
}
break;
case BT_HFP_CALL_ACTIVE:
{
p_link->hfp.call_status = APP_HFP_CALL_ACTIVE;
}
break;
case BT_HFP_CALL_HELD:
{
//p_link->call_status = APP_HFP_CALL_HELD;
}
break;
case BT_HFP_CALL_ACTIVE_WITH_CALL_WAITING:
{
p_link->hfp.call_status = APP_HFP_CALL_ACTIVE_WITH_CALL_WAITING;
}
break;
case BT_HFP_CALL_ACTIVE_WITH_CALL_HELD:
{
p_link->hfp.call_status = APP_HFP_CALL_ACTIVE_WITH_CALL_HELD;
}
break;
default:
break;
}
if ((app_cfg_const.enable_auto_answer_incoming_call == 1) &&
(p_link->hfp.call_status == APP_HFP_CALL_INCOMING))
{
hfp.auto_answer_call_interval = app_cfg_const.timer_hfp_auto_answer_call * 1000;
app_start_timer(&hfp.tmr.indices[TIMER_AUTO_ANSWER_CALL], "auto_answer_call",
hfp.tmr.id, TIMER_AUTO_ANSWER_CALL, p_link->id, false,
hfp.auto_answer_call_interval);
}
if (p_link->hfp.call_status == APP_HFP_CALL_IDLE)
{
p_link->hfp.call_id_type_chk = true;
p_link->hfp.call_id_type_num = false;
}
app_hfp_update_call_status();
if ((p_link->hfp.call_status == APP_HFP_VOICE_ACTIVATION_ONGOING) &&
(p_link->remote_device_vendor_id == APP_REMOTE_DEVICE_IOS))
{
app_start_timer(&hfp.tmr.indices[TIMER_CANCEL_VOICE_DAIL], "cancel_iphone_voice_dail",
hfp.tmr.id, TIMER_CANCEL_VOICE_DAIL, p_link->id, false,
1000);
}
if (app_cfg_nv.bud_role != REMOTE_SESSION_ROLE_SECONDARY)
{
if (param->hfp_call_status.prev_status == BT_HFP_CALL_INCOMING &&
param->hfp_call_status.curr_status == BT_HFP_CALL_IDLE)
{
if (app_db.reject_call_by_key)
{
app_db.reject_call_by_key = false;
app_audio_tone_type_play(TONE_HF_CALL_REJECT, false, false);
}
}
if (p_link->id == temp_idx &&
param->hfp_call_status.prev_status == BT_HFP_CALL_ACTIVE &&
param->hfp_call_status.curr_status == BT_HFP_CALL_IDLE)
{
app_audio_tone_type_play(TONE_HF_CALL_END, false, false);
}
if (p_link->id == hfp.active_link_id &&
param->hfp_call_status.prev_status != BT_HFP_CALL_ACTIVE &&
param->hfp_call_status.curr_status == BT_HFP_CALL_ACTIVE)
{
app_audio_tone_type_play(TONE_HF_CALL_ACTIVE, false, false);
}
if (param->hfp_call_status.prev_status != BT_HFP_CALL_OUTGOING &&
param->hfp_call_status.curr_status == BT_HFP_CALL_OUTGOING)
{
app_audio_tone_type_play(TONE_HF_OUTGOING_CALL, false, false);
}
}
}
if (param->hfp_call_status.curr_status == BT_HFP_CALL_ACTIVE ||
param->hfp_call_status.curr_status == BT_HFP_CALL_INCOMING ||
param->hfp_call_status.curr_status == BT_HFP_CALL_OUTGOING)
{
if (app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off)
{
app_auto_power_off_disable(AUTO_POWER_OFF_MASK_VOICE);
}
app_audio_set_bud_stream_state(BUD_STREAM_STATE_VOICE);
}
else if (param->hfp_call_status.curr_status == BT_HFP_CALL_IDLE)
{
if (app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off)
{
app_auto_power_off_enable(AUTO_POWER_OFF_MASK_VOICE,
app_cfg_const.timer_auto_power_off_while_phone_connected_and_anc_apt_off);
}
app_audio_set_bud_stream_state(BUD_STREAM_STATE_IDLE);
}
}
break;
case BT_EVENT_HFP_SERVICE_STATUS:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_service_status.bd_addr);
if (p_link != NULL)
{
p_link->hfp.service_status = param->hfp_service_status.status;
app_hfp_check_service_status();
}
}
break;
case BT_EVENT_HFP_CALL_WAITING_IND:
case BT_EVENT_HFP_CALLER_ID_IND:
{
if (app_cfg_nv.bud_role != REMOTE_SESSION_ROLE_SECONDARY)
{
T_APP_BR_LINK *br_link;
T_APP_LE_LINK *le_link;
char *number;
uint16_t num_len;
if (event_type == BT_EVENT_HFP_CALLER_ID_IND)
{
br_link = app_link_find_br_link(param->hfp_caller_id_ind.bd_addr);
le_link = app_link_find_le_link_by_addr(param->hfp_caller_id_ind.bd_addr);
number = (char *)param->hfp_caller_id_ind.number;
num_len = strlen(param->hfp_caller_id_ind.number);
}
else
{
br_link = app_link_find_br_link(param->hfp_call_waiting_ind.bd_addr);
le_link = app_link_find_le_link_by_addr(param->hfp_call_waiting_ind.bd_addr);
number = (char *)param->hfp_call_waiting_ind.number;
num_len = strlen(param->hfp_call_waiting_ind.number);
}
if (br_link != NULL)
{
if (br_link->hfp.call_id_type_chk == true)
{
if (br_link->connected_profile & PBAP_PROFILE_MASK)
{
if (bt_pbap_vcard_listing_by_number_pull(br_link->bd_addr, number) == false)
{
br_link->hfp.call_id_type_chk = false;
br_link->hfp.call_id_type_num = true;
}
}
else
{
br_link->hfp.call_id_type_chk = false;
br_link->hfp.call_id_type_num = true;
}
}
if (br_link->hfp.call_id_type_chk == false)
{
if (br_link->hfp.call_id_type_num == true)
{
T_CMD_PATH cmd_path = CMD_PATH_UART;
uint8_t app_link_id = 0xff;
T_CALLER_ID_TYPE call_id_type = CALLER_ID_NUMBER;
if (br_link->connected_profile & SPP_PROFILE_MASK)
{
app_link_id = br_link->id;
cmd_path = CMD_PATH_SPP;
}
else if (br_link->connected_profile & IAP_PROFILE_MASK)
{
app_link_id = br_link->id;
cmd_path = CMD_PATH_IAP;
}
else if (le_link != NULL)
{
app_link_id = le_link->id;
cmd_path = CMD_PATH_LE;
}
app_hfp_call_id_rpt(cmd_path, app_link_id, call_id_type, num_len, (uint8_t *)number);
}
}
}
}
}
break;
case BT_EVENT_HFP_RING_ALERT:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_ring_alert.bd_addr);
if (p_link != NULL)
{
p_link->hfp.is_inband_ring = param->hfp_ring_alert.is_inband;
if ((app_cfg_const.always_play_hf_incoming_tone_when_incoming_call == false) &&
((p_link->hfp.is_inband_ring == false) ||
(p_link->id != hfp.active_link_id))) /* TODO check active sco link */
{
if (hfp.ring_active == false && p_link->hfp.call_status == APP_HFP_CALL_INCOMING)
{
hfp.ring_active = true;
app_hfp_ring_alert(p_link);
}
}
}
}
break;
case BT_EVENT_HFP_SPK_VOLUME_CHANGED:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_spk_volume_changed.bd_addr);
uint8_t temp_buff[7];
if (p_link != NULL)
{
if (app_db.remote_session_state == REMOTE_SESSION_STATE_DISCONNECTED)
{
uint8_t pair_idx_mapping;
app_bond_get_pair_idx_mapping(p_link->bd_addr, &pair_idx_mapping);
app_cfg_nv.voice_gain_level[pair_idx_mapping] = (param->hfp_spk_volume_changed.volume *
app_dsp_cfg_vol.voice_out_volume_max +
0x0f / 2) / 0x0f;
memcpy(&temp_buff[0], ¶m->hfp_spk_volume_changed.bd_addr, 6);
temp_buff[6] = app_cfg_nv.voice_gain_level[pair_idx_mapping];
app_report_event(CMD_PATH_UART, APP_EVENT_VOLUME_SYNC, 0, temp_buff, sizeof(temp_buff));
app_audio_vol_set(p_link->sco.track_handle, app_cfg_nv.voice_gain_level[pair_idx_mapping]);
app_audio_track_spk_unmute(AUDIO_STREAM_TYPE_VOICE);
}
else
{
}
}
}
break;
case BT_EVENT_HFP_MIC_VOLUME_CHANGED:
{
}
break;
case BT_EVENT_REMOTE_CONN_CMPL:
{
if (app_cfg_nv.bud_role == REMOTE_SESSION_ROLE_PRIMARY)
{
app_hfp_voice_nr_enable();
}
}
break;
case BT_EVENT_HFP_DISCONN_CMPL:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->hfp_disconn_cmpl.bd_addr);
if (p_link != NULL)
{
if (param->hfp_disconn_cmpl.cause == (HCI_ERR | HCI_ERR_CONN_ROLESWAP))
{
//do nothing
}
else
{
p_link->hfp.call_status = APP_HFP_CALL_IDLE;
p_link->hfp.state = APP_HF_STATE_STANDBY;
if (app_db.first_hf_index == p_link->id)
{
app_db.first_hf_index = app_db.last_hf_index;
}
for (uint8_t i = 0; i < MAX_BR_LINK_NUM; i++)
{
if (app_db.br_link[i].connected_profile & (HFP_PROFILE_MASK | HSP_PROFILE_MASK))
{
app_hfp_set_active_idx(app_db.br_link[i].bd_addr);
app_bond_set_priority(app_db.br_link[i].bd_addr);
break;
}
}
if (app_hfp_get_call_status() != APP_HFP_CALL_IDLE)
{
app_hfp_update_call_status();
}
}
if ((param->hfp_disconn_cmpl.cause & ~HCI_ERR) != HCI_ERR_CONN_ROLESWAP)
{
app_hfp_check_service_status();
}
}
}
break;
default:
handle = false;
break;
}
if (handle == true)
{
APP_PRINT_INFO1("app_hfp_bt_cback: event_type 0x%04x", event_type);
}
}
Send HFP Vendor AT Command
bt_hfp_send_vnd_at_cmd_req()
is utilized in br_cmd_handle()
to send HFP vendor AT command.
void br_cmd_handle(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t app_idx,
uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
APP_PRINT_TRACE1("app_cmd br_cmd_handle: cmd_id 0x%04x", cmd_id);
typedef struct
{
uint16_t cmd_id;
uint8_t status;
} __attribute__((packed)) *ACK_PKT;
switch (cmd_id)
{
case CMD_BT_SEND_AT_CMD:
{
uint8_t app_index = cmd_ptr[2];
if (bt_hfp_send_vnd_at_cmd_req(app_db.br_link[app_index].bd_addr, (char *)&cmd_ptr[3]) == false)
{
ack_pkt[2] = CMD_SET_STATUS_DISALLOW;
}
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
}
}
HFP Call Status
HFP HF call status T_BT_HFP_CALL_STATUS
is updated with event BT_EVENT_HFP_CALL_STATUS
when HF receives related AG indicators or HF takes action to change them.
typedef enum t_bt_hfp_call_status
{
BT_HFP_CALL_IDLE = 0x00,
BT_HFP_CALL_INCOMING = 0x01,
BT_HFP_CALL_OUTGOING = 0x02,
BT_HFP_CALL_ACTIVE = 0x03,
BT_HFP_CALL_HELD = 0x04,
BT_HFP_CALL_ACTIVE_WITH_CALL_WAITING = 0x05,
BT_HFP_CALL_ACTIVE_WITH_CALL_HELD = 0x06,
} T_BT_HFP_CALL_STATUS;
HFP HF application could control the audio module and handle answer/reject action according to the call status, which are processed in app_mmi_handle_action()
.
void app_mmi_hf_reject_call(void)
{
T_APP_BR_LINK *p_link;
T_APP_AUDIO_TONE_TYPE tone_type = TONE_TYPE_INVALID;
uint8_t active_hf_idx = app_hfp_get_active_idx();
p_link = &(app_db.br_link[active_hf_idx]);
if (app_cfg_nv.bud_role == REMOTE_SESSION_ROLE_SECONDARY)
{
return;
}
if (p_link != NULL)
{
app_hfp_stop_ring_alert_timer();
tone_type = (T_APP_AUDIO_TONE_TYPE)app_hfp_get_call_in_tone_type(p_link);
app_audio_tone_type_cancel(tone_type, false);
if (bt_hfp_call_terminate_req(p_link->bd_addr))
{
app_db.reject_call_by_key = true;
}
}
}
void app_mmi_hf_end_active_call(void)
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
if (app_cfg_nv.bud_role == REMOTE_SESSION_ROLE_SECONDARY)
{
return;
}
if (app_db.br_link[app_hfp_get_active_idx()].hfp.call_status > APP_HFP_CALL_ACTIVE)
{
bt_hfp_release_active_call_accept_held_or_waiting_call_req(app_db.br_link[active_hf_idx].bd_addr);
}
else
{
bt_hfp_call_terminate_req(app_db.br_link[active_hf_idx].bd_addr);
}
}
void app_mmi_hf_end_outgoing_call(void)
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
if (app_cfg_nv.bud_role == REMOTE_SESSION_ROLE_SECONDARY)
{
return;
}
bt_hfp_call_terminate_req(app_db.br_link[active_hf_idx].bd_addr);
}
void app_mmi_handle_action(uint8_t action)
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
switch (action)
{
...
case MMI_HF_ANSWER_CALL:
{
T_APP_BR_LINK *p_link;
T_APP_AUDIO_TONE_TYPE tone_type = TONE_TYPE_INVALID;
p_link = &(app_db.br_link[active_hf_idx]);
if (app_cfg_nv.bud_role == REMOTE_SESSION_ROLE_SECONDARY)
{
return;
}
if (p_link != NULL)
{
app_hfp_stop_ring_alert_timer();
tone_type = (T_APP_AUDIO_TONE_TYPE)app_hfp_get_call_in_tone_type(p_link);
app_audio_tone_type_cancel(tone_type, false);
bt_hfp_call_answer_req(p_link->bd_addr);
}
}
break;
case MMI_HF_REJECT_CALL:
{
app_mmi_hf_reject_call();
}
break;
case MMI_HF_END_ACTIVE_CALL:
{
app_mmi_hf_end_active_call();
}
break;
case MMI_HF_END_OUTGOING_CALL:
{
app_mmi_hf_end_outgoing_call();
}
break;
...
}
}
MAP MCE
MAP roles can be divided into MSE and MCE:
Message Server Equipment (MSE) is the device that provides the message repository engine (i.e., has the ability to provide a client unit with messages that are stored in this device and notifications of changes in its message repository).
Message Client Equipment (MCE) is the device that uses the message repository engine of the MSE for browsing and displaying existing messages and to upload messages created on the MCE to the MSE.
The code flow of MAP MCE is introduced here.
MAP Initialization
The main function is invoked when the application is powered on or the chip is reset, MAP initialization function is included in app_test_init()
.
void app_test_init(void)
{
bt_map_init(1, RFC_MAP_MNS_CHANN_NUM, L2CAP_MAP_MNS_PSM, 0x0000024F);
...
os_task_create(&app_task_handle, "app_task", app_task, NULL, stack_size, 2);
...
os_sched_start();
return 0;
}
int main(void)
{
...
#if F_APP_TEST_SUPPORT
app_test_init();
#endif
...
}
SDP Record for MNS on MCE Device
There shall be one service record for the MCE device.
#define RFC_MAP_MNS_CHANN_NUM 20 //user defined
#define L2CAP_MAP_MNS_PSM 0x1001 //user defined
#if F_APP_BT_PROFILE_MAP_MCE_SUPPORT
const uint8_t map_mce_sdp_record[] =
{
SDP_DATA_ELEM_SEQ_HDR,
0x49,
//attribute SDP_ATTR_SRV_CLASS_ID_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_SRV_CLASS_ID_LIST >> 8),
(uint8_t)SDP_ATTR_SRV_CLASS_ID_LIST,
SDP_DATA_ELEM_SEQ_HDR,
0x03,
SDP_UUID16_HDR,
(uint8_t)(UUID_MSG_NOTIFICATION_SERVER >> 8),
(uint8_t)(UUID_MSG_NOTIFICATION_SERVER),
//attribute SDP_ATTR_PROTO_DESC_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_PROTO_DESC_LIST >> 8),
(uint8_t)SDP_ATTR_PROTO_DESC_LIST,
SDP_DATA_ELEM_SEQ_HDR,
0x11,
SDP_DATA_ELEM_SEQ_HDR,
0x03,
SDP_UUID16_HDR,
(uint8_t)(UUID_L2CAP >> 8),
(uint8_t)(UUID_L2CAP),
SDP_DATA_ELEM_SEQ_HDR,
0x05,
SDP_UUID16_HDR,
(uint8_t)(UUID_RFCOMM >> 8),
(uint8_t)(UUID_RFCOMM),
SDP_UNSIGNED_ONE_BYTE,
RFC_MAP_MNS_CHANN_NUM, //channel number
SDP_DATA_ELEM_SEQ_HDR,
0x03,
SDP_UUID16_HDR,
(uint8_t)(UUID_OBEX >> 8),
(uint8_t)(UUID_OBEX),
//Attribute SDP_ATTR_SRV_NAME
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET) >> 8),
(uint8_t)(SDP_ATTR_SRV_NAME + SDP_BASE_LANG_OFFSET),
SDP_STRING_HDR,
0x0B,
'R', 'e', 'a', 'l', 't', 'e', 'k', '-', 'M', 'N', 'S',
//attribute SDP_ATTR_PROFILE_DESC_LIST
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(SDP_ATTR_PROFILE_DESC_LIST >> 8),
(uint8_t)SDP_ATTR_PROFILE_DESC_LIST,
SDP_DATA_ELEM_SEQ_HDR,
0x08,
SDP_DATA_ELEM_SEQ_HDR,
0x06,
SDP_UUID16_HDR,
(uint8_t)(UUID_MSG_ACCESS_PROFILE >> 8),
(uint8_t)UUID_MSG_ACCESS_PROFILE,
SDP_UNSIGNED_TWO_BYTE,
0x01,
0x04, //version 1.4
//attribute SDP_ATTR_L2C_PSM
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_L2C_PSM) >> 8),
(uint8_t)(SDP_ATTR_L2C_PSM),
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)(L2CAP_MAP_MNS_PSM >> 8),
(uint8_t)(L2CAP_MAP_MNS_PSM),
//attribute SDP_ATTR_MAP_SUPPERTED_FEATS
SDP_UNSIGNED_TWO_BYTE,
(uint8_t)((SDP_ATTR_MAP_SUPPERTED_FEATS) >> 8),
(uint8_t)(SDP_ATTR_MAP_SUPPERTED_FEATS),
SDP_UNSIGNED_FOUR_BYTE,
(uint8_t)(0x0000024F >> 24),
(uint8_t)(0x0000024F >> 16),
(uint8_t)(0x0000024F >> 8),
(uint8_t)(0x0000024F)
};
#endif
MAP Connect
OBEX connections in MAP shall always be initiated by the MCE device. The establishment of a Message Notification Service connection is done with the MCE as OBEX Server and the MSE as OBEX Client. The establishment of a Message Notification connection requires the previous establishment of a Message Access Service connection.
The MAP connection is realized by CMD, gap_br_start_sdp_discov()
is invoked to establish MAS through SDP using UUID in app_map_handle_cmd()
, UUID needs to be set as UUID_MSG_ACCESS_SERVER
.
void app_map_handle_cmd(uint8_t app_idx, T_CMD_PATH cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
case CMD_MAP_CONNECT:
{
T_GAP_UUID_DATA uuid_data;
uuid_data.uuid_16 = UUID_MSG_ACCESS_SERVER;
gap_br_start_sdp_discov(app_db.br_link[app_idx].bd_addr, GAP_UUID16, uuid_data);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
}
}
void app_handle_cmd_set(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t rx_seqn,
uint8_t app_idx)
{
uint16_t cmd_id;
uint8_t ack_pkt[3];
cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
ack_pkt[0] = cmd_ptr[0];
ack_pkt[1] = cmd_ptr[1];
ack_pkt[2] = CMD_SET_STATUS_COMPLETE;
...
switch (cmd_id)
{
case CMD_MAP_SDP_REQUEST...CMD_MAP_PUSH_MESSAGE:
app_map_handle_cmd(app_idx, (T_CMD_PATH)cmd_path, cmd_ptr, cmd_len, ack_pkt);
break;
}
}
bt_map_mas_msg_notification_set()
is used to register for being notified of the arrival of new messages in app_handle_cmd_set()
.
After this API is called, the MSE device will establish an OBEX connection for the Message Notification Service.
void app_handle_cmd_set(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t rx_seqn,
uint8_t app_idx)
{
...
case CMD_MAP_REG_MSG_NOTIFICATION:
{
struct
{
uint16_t cmd_id;
uint8_t enable;
} __attribute__((packed)) *params = (typeof(params)) cmd_ptr;
bt_map_mas_msg_notification_set(app_db.br_link[app_idx].bd_addr, params->enable);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
bt_map_mns_connect_cfm()
accepts or rejects the incoming MNS connection when receiving BT_EVENT_MAP_MNS_CONN_IND
.
static void app_test_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
...
switch (event_type)
{
...
case BT_EVENT_MAP_MNS_CONN_IND:
{
bt_map_mns_connect_cfm(param->map_mns_conn_ind.bd_addr, true);
}
break;
}
}
MAP Get Message
When getting a message, bt_map_mas_folder_set()
is used to navigate the folders of the MSE.
bt_map_mas_folder_listing_get()
is used to retrieve the Folder-Listing object from the current folder of the MSE.
The Folder-Listing object is an XML object and shall be encoded in UTF-8.
In the context of the MAP profile, the Folder-Listing object shall not contain message entries.
It shall only contain folder entries located in the current folder level.
bt_map_mas_msg_listing_get()
is used to retrieve Messages-Listing objects from the MSE.
The Messages-Listing object is an XML object and shall be encoded in UTF-8.
bt_map_mas_msg_get()
is used to retrieve a specific message from the MSE, and the corresponding event is BT_EVENT_MAP_GET_MSG_CMPL
.
void app_handle_cmd_set(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t rx_seqn,
uint8_t app_idx)
{
...
case CMD_MAP_SET_FOLDER:
{
struct
{
uint16_t cmd_id;
uint8_t folder;
} __attribute__((packed)) *params;
params = (typeof(params)) cmd_ptr;
bt_map_mas_folder_set(app_db.br_link[app_idx].bd_addr, (T_BT_MAP_FOLDER)params->folder);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_MAP_GET_FOLDER_LISTING:
{
struct
{
uint16_t cmd_id;
uint8_t max_list_count;
uint8_t start_offset;
} __attribute__((packed)) *params = (typeof(params)) cmd_ptr;
bt_map_mas_folder_listing_get(app_db.br_link[app_idx].bd_addr, params->max_list_count,
params->start_offset);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
//inbox name is fixed here
case CMD_MAP_GET_MESSAGE_LISTING:
{
struct
{
uint16_t cmd_id;
uint8_t max_list_count;
uint8_t start_offset;
} __attribute__((packed)) *params = (typeof(params)) cmd_ptr;
uint8_t map_path_inbox[12] =
{
0x00, 0x69, 0x00, 0x6e, 0x00, 0x62, 0x00, 0x6f, 0x00, 0x78, 0x00, 0x00
};
bt_map_mas_msg_listing_get(app_db.br_link[app_idx].bd_addr, map_path_inbox, sizeof(map_path_inbox),
params->max_list_count, params->start_offset);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
case CMD_MAP_GET_MESSAGE:
{
struct
{
uint16_t cmd_id;
uint8_t handle_len;
uint8_t msg_handle[];
} __attribute__((packed)) *params = (typeof(params)) cmd_ptr;
bt_map_mas_msg_get(app_db.br_link[app_idx].bd_addr, params->msg_handle, params->handle_len, false);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
MAP Push Message
bt_map_mas_msg_push()
is used to push a message to a folder of the MSE. In the case of sending messages through the MSE, the messages shall be uploaded into the Outbox folder of the MSE. In the case of uploading messages to the MSE, the messages shall not be uploaded into the Outbox folder of the MSE, but any other folder is permitted. In the case of sending messages through the MSE, the MCE shall get a notification when the messages have been sent to the network and thus have been shifted by the MSE from the Outbox to the Sent folder.
case CMD_MAP_PUSH_MESSAGE:
{
struct
{
uint16_t cmd_id;
uint16_t msg_len;
uint8_t msg[];
} __attribute__((packed)) *params = (typeof(params)) cmd_ptr;
uint8_t map_path_outbox[14] =
{
0x00, 0x6f, 0x00, 0x75, 0x00, 0x74, 0x00, 0x62,
0x00, 0x6f, 0x00, 0x78, 0x00, 0x00
};
bt_map_mas_msg_push(app_db.br_link[app_idx].bd_addr, map_path_outbox, sizeof(map_path_outbox),
false, false, params->msg, params->msg_len);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
MAP Disconnect
bt_map_mas_disconnect_req()
is used to disconnect the Message Access Service.
case CMD_MAP_DISCONNECT:
{
bt_map_mas_disconnect_req(app_db.br_link[app_idx].bd_addr);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
PBAP PCE
PBAP roles can be divided into PSE and PCE:
Phone Book Server Equipment (PSE) is the device that contains the source phone book objects.
Phone Book Client Equipment (PCE) is the device that retrieves phone book objects from the Server Equipment.
The code flow of PBAP PCE is introduced here.
PBAP Initialization
bt_pbap_init()
is invoked in app_pbap_init()
when the application is powered on or the chip is reset.
void app_pbap_init(void)
{
if (app_cfg_const.supported_profile_mask & PBAP_PROFILE_MASK)
{
bt_pbap_init(app_cfg_const.pbap_link_number);
bt_mgr_cback_register(app_pbap_bt_cback);
app_timer_reg_cb(app_pbap_timeout_cb, &app_pbap_timer_id);
}
}
int main(void)
{
...
#if F_APP_BT_PROFILE_PBAP_PCE_SUPPORT
app_pbap_init();
#endif
...
}
PBAP Connect
We always use UUID_PBAP_PSE
to do service discovery. OBEX connections in PBAP shall always be initiated by the PCE device using bt_pbap_connect_req()
.
CMD_PBAP_CONNECT
is utilized to initiate SDP in app_pbap_cmd_handle()
, and bt_pbap_connect_req
will be called after receiving BT_EVENT_SDP_ATTR_INFO
.
void app_pbap_cmd_handle(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t app_idx,
uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
uint8_t active_hf_idx = app_hfp_get_active_idx();
switch (cmd_id)
{
case CMD_PBAP_CONNECT:
{
T_GAP_UUID_DATA uuid_data;
uuid_data.uuid_16 = UUID_PBAP_PSE;
gap_br_start_sdp_discov(app_db.br_link[app_idx].bd_addr, GAP_UUID16, uuid_data);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
...
}
}
void app_handle_cmd_set(uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t cmd_path, uint8_t rx_seqn,
uint8_t app_idx)
{
uint16_t cmd_id;
uint8_t ack_pkt[3];
cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
#if F_APP_PBAP_CMD_SUPPORT
case CMD_PBAP_DOWNLOAD...CMD_PBAP_DISCONNECT:
{
app_pbap_cmd_handle(cmd_ptr, cmd_len, cmd_path, app_idx, ack_pkt);
}
break;
#endif
...
}
}
static void app_test_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
...
switch (event_type)
{
...
case BT_EVENT_SDP_ATTR_INFO:
{
T_BT_SDP_ATTR_INFO *sdp_info = ¶m->sdp_attr_info.info;
if (sdp_info->srv_class_uuid_data.uuid_16 == UUID_MSG_ACCESS_SERVER)
{
bt_map_mas_connect_over_rfc_req(param->sdp_attr_info.bd_addr, sdp_info->server_channel);
}
else if (sdp_info->srv_class_uuid_data.uuid_16 == UUID_PBAP_PSE)
{
bt_pbap_connect_req(param->sdp_attr_info.bd_addr, sdp_info->server_channel,
sdp_info->supported_feat);
}
}
break;
...
}
}
Note
It takes time to complete the authorization which involves user participation. So DO NOT initiate a PBAP connection immediately after pairing with the phone. It is recommended to establish a PBAP connection when actually needed.
PBAP Pull
CMD_PBAP_DOWNLOAD
is utilized to download the phone book of interest in app_pbap_cmd_handle()
. Users can choose the content to be pulled according to different cases such as current call history, phone book, all records, etc.
case CMD_PBAP_DOWNLOAD:
{
app_pbap_download_info_set_default();
switch (cmd_ptr[2])
{
...
case PBAP_DOWNLOAD_METHOD_ALL:
{
pbap_download_info.download_flag |= (PBAP_DOWNLOAD_ME_PB_MASK | PBAP_DOWNLOAD_SM_PB_MASK |
PBAP_DOWNLOAD_CCH_MASK);
}
break;
case PBAP_DOWNLOAD_METHOD_ALL_PB:
{
pbap_download_info.download_flag |= (PBAP_DOWNLOAD_ME_PB_MASK | PBAP_DOWNLOAD_SM_PB_MASK);
}
break;
case PBAP_DOWNLOAD_METHOD_CCH:
{
pbap_download_info.storage = BT_PBAP_PHONE_BOOK_CCH;
pbap_download_info.phone_book = BT_PBAP_PHONE_BOOK_CCH;
}
break;
default:
ack_pkt[2] = CMD_SET_STATUS_PARAMETER_ERROR;
break;
}
if (ack_pkt[2] == CMD_SET_STATUS_COMPLETE)
{
pbap_download_info.method = cmd_ptr[2];
pbap_download_info.filter = (uint64_t)(cmd_ptr[3] | (cmd_ptr[4] << 8) | (cmd_ptr[5] << 16) |
(cmd_ptr[6] << 24));
if (bt_pbap_phone_book_size_get(app_db.br_link[active_hf_idx].bd_addr,
(T_BT_PBAP_REPOSITORY)pbap_download_info.repos,
(T_BT_PBAP_PHONE_BOOK)pbap_download_info.phone_book) == false)
{
ack_pkt[2] = CMD_SET_STATUS_PROCESS_FAIL;
}
}
app_report_event(cmd_path, APP_EVENT_ACK, app_idx, ack_pkt, 3);
}
break;
...
Users can also control PBAP pause/continue/abort to pull information through CMD_PBAP_DOWNLOAD_CONTROL
,
where bt_pbap_pull_abort()
, bt_pbap_pull_continue()
are called.
case CMD_PBAP_DOWNLOAD_CONTROL:
{
switch (cmd_ptr[2])
{
case PBAP_DOWNLOAD_CONTROL_ABORT:
{
if (bt_pbap_pull_abort(app_db.br_link[active_hf_idx].bd_addr) == false)
{
ack_pkt[2] = CMD_SET_STATUS_PROCESS_FAIL;
}
else
{
app_stop_timer(&timer_idx_pbap_pull_continue);
}
}
break;
case PBAP_DOWNLOAD_CONTROL_SUSPEND:
{
enable_auto_pbap_download_continue_flag = false;
}
break;
case PBAP_DOWNLOAD_CONTROL_CONTINUE:
{
if (bt_pbap_pull_continue(app_db.br_link[active_hf_idx].bd_addr))
{
enable_auto_pbap_download_continue_flag = true;
}
else
{
ack_pkt[2] = CMD_SET_STATUS_PROCESS_FAIL;
}
}
break;
default:
ack_pkt[2] = CMD_SET_STATUS_PARAMETER_ERROR;
break;
}
app_report_event(cmd_path, APP_EVENT_ACK, app_idx, ack_pkt, 3);
if (ack_pkt[2] == CMD_SET_STATUS_COMPLETE)
{
uint8_t temp_buff[1];
temp_buff[0] = cmd_ptr[2];
app_report_event(cmd_path, APP_EVENT_PBAP_REPORT_SESSION_STATUS, app_idx, temp_buff,
sizeof(temp_buff));
}
}
break;
PBAP Disconnect
When the PBAP connection is no longer needed, call bt_pbap_disconnect_req()
in CMD_PBAP_DISCONNECT
to terminate the PBAP connection.
case CMD_PBAP_DISCONNECT:
{
bt_pbap_disconnect_req(app_db.br_link[app_idx].bd_addr);
app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt);
}
break;
CIS Acceptor
Bluetooth Low Energy Audio CAP roles can be divided into Acceptor, Initiator, and Commander. The Acceptor role can enable Bluetooth Low Energy Audio advertising and be connected by a remote device, control the music player and telephone call by MCP and CCP profile.
CIS Initialization
app_lea_acc_profile_init()
includes the initialization flow for ASCS, VCS, CSIS, MCP, CCP, and other modules.
void app_lea_acc_profile_init(void)
{
T_CAP_INIT_PARAMS cap_init_param = {0};
#if (F_APP_TMAP_CT_SUPPORT || F_APP_TMAP_UMR_SUPPORT)
app_lea_ascs_init();
#endif
#if F_APP_MCP_SUPPORT
app_lea_mcp_init();
#endif
#if F_APP_CCP_SUPPORT
app_lea_ccp_init();
#endif
app_lea_pacs_init();
#if F_APP_VCS_SUPPORT
app_lea_vcs_init();
#endif
#if F_APP_TAMP_BMR_SUPPORT
app_lea_bca_init();
#endif
app_lea_profile_bap_init();
#if F_APP_CSIS_SUPPORT
app_lea_csis_init(&cap_init_param);
#endif
app_lea_profile_cap_init(&cap_init_param);
#if F_APP_TMAS_SUPPORT
app_lea_profile_tmas_init();
#endif
#if (F_APP_TMAP_CT_SUPPORT || F_APP_TMAP_UMR_SUPPORT)
app_lea_uca_init();
#endif
}
CIS Connect
Firstly, the Acceptor needs to enable Bluetooth Low Energy Audio advertising, and then the remote device can scan and create a Bluetooth Low Energy Audio link to it.
The adv_interval_min
and adv_interval_max
parameters can be modified by the user to change the Bluetooth Low Energy Audio advertising interval.
Actually, the user can enable or disable the F_APP_LE_AUDIO_CIS_RND_ADDR
macro to use public or static random address for advertisement.
static void app_lea_adv_set(uint16_t audio_adv_flag, uint8_t service_num)
{
uint16_t audio_adv_len = 0;
T_LE_EXT_ADV_EXTENDED_ADV_PROPERTY adv_event_prop = LE_EXT_ADV_EXTENDED_ADV_CONN_UNDIRECTED;
uint16_t adv_interval_min = 0x40;
uint16_t adv_interval_max = 0x50;
#if F_APP_LE_AUDIO_CIS_RND_ADDR
T_GAP_LOCAL_ADDR_TYPE own_address_type = GAP_LOCAL_ADDR_LE_RANDOM;
#else
T_GAP_LOCAL_ADDR_TYPE own_address_type = GAP_LOCAL_ADDR_LE_PUBLIC;
#endif
T_GAP_REMOTE_ADDR_TYPE peer_address_type = GAP_REMOTE_ADDR_LE_PUBLIC;
uint8_t peer_address[6] = {0, 0, 0, 0, 0, 0};
T_GAP_ADV_FILTER_POLICY filter_policy = GAP_ADV_FILTER_ANY;
#if F_APP_LE_AUDIO_CIS_RND_ADDR
//the type of static random address is 0xC0
if ((app_cfg_nv.lea_static_random_addr[5] & 0xC0) != 0xC0)
{
le_gen_rand_addr(GAP_RAND_ADDR_STATIC, app_cfg_nv.lea_static_random_addr);
app_cfg_store(app_cfg_nv.lea_static_random_addr, 6);
APP_PRINT_TRACE1("app_lea_adv_set: cis_static_random_addr %s",
TRACE_BDADDR(app_cfg_nv.lea_static_random_addr));
}
#endif
audio_adv_len = app_lea_adv_ext_data(audio_adv_flag, service_num);
ble_ext_adv_mgr_init_adv_params(&app_lea_adv_handle, adv_event_prop, adv_interval_min,
adv_interval_max, own_address_type, peer_address_type, peer_address,
filter_policy, audio_adv_len, app_lea_adv_data,
sizeof(app_lea_adv_scan_rsp_data), app_lea_adv_scan_rsp_data, NULL);
ble_ext_adv_mgr_change_adv_phy(app_lea_adv_handle, GAP_PHYS_PRIM_ADV_1M, GAP_PHYS_2M);
ble_ext_adv_mgr_register_callback(app_lea_adv_cback, app_lea_adv_handle);
}
app_lea_adv_start()
enables Bluetooth Low Energy Audio advertising, and app_lea_adv_stop()
disables Bluetooth Low Energy Audio advertising.
void app_lea_adv_start(uint8_t mode)
{
uint8_t public_addr_lsb[6];
uint8_t len = 0;
gap_get_param(GAP_PARAM_BD_ADDR, public_addr_lsb);
APP_PRINT_INFO2("app_lea_adv_start: public addr %s mode %d",
TRACE_BDADDR(public_addr_lsb), mode);
if ((mode == LEA_ADV_MODE_PAIRING) && (app_link_get_le_link_num() > 2))
{
return;
}
if (app_lea_adv_state == BLE_EXT_ADV_MGR_ADV_DISABLED)
{
#if F_APP_LE_AUDIO_CIS_RND_ADDR
ble_ext_adv_mgr_set_random(app_lea_adv_handle, app_cfg_nv.lea_static_random_addr);
#endif
if (ble_ext_adv_mgr_enable(app_lea_adv_handle, 0) == GAP_CAUSE_SUCCESS)
{
if ((T_LEA_ADV_MODE)mode == LEA_ADV_MODE_LINK_LOSS_LINK_BACK)
{
app_start_timer(&timer_idx_lea_adv, "lea_adv_linkloss", app_lea_adv_timer_id,
APP_LEA_TMR_LINKLOSS, 0, false, LEA_ADV_TMR_LINK_LOST * 1000);
}
else if ((T_LEA_ADV_MODE)mode == LEA_ADV_MODE_PAIRING)
{
app_start_timer(&timer_idx_lea_adv, "lea_adv_pairing", app_lea_adv_timer_id,
APP_LEA_TMR_PAIRING, 0, false, LEA_ADV_TMR_PAIRING * 1000);
}
}
}
return;
}
void app_lea_adv_stop()
{
app_stop_timer(&timer_idx_lea_adv);
ble_ext_adv_mgr_disable(app_lea_adv_handle, 0);
}
For the Acceptor, the app_lea_uca_link_sm()
function is used to handle Bluetooth Low Energy Audio and legacy events. It has three states: LEA_LINK_IDLE
, LEA_LINK_CONNECTED
, and LEA_LINK_STREAMING
to handle different events.
Based on some specific events, the state machine can switch its state with the app_lea_uca_link_state_change()
API.
void app_lea_uca_link_sm(uint16_t conn_handle, uint8_t event, void *p_data)
{
T_APP_LE_LINK *p_link;
p_link = app_link_find_le_link_by_conn_handle(conn_handle);
if (p_link == NULL)
{
return;
}
APP_PRINT_INFO3("app_lea_uca_link_sm: conn_handle 0x%x, event %x, %d", conn_handle, event,
p_link->lea_link_state);
switch (p_link->lea_link_state)
{
case LEA_LINK_IDLE:
app_lea_uca_link_idle(p_link, event, p_data);
break;
case LEA_LINK_CONNECTED:
app_lea_uca_link_connected(p_link, event, p_data);
break;
case LEA_LINK_STREAMING:
app_lea_uca_link_streaming(p_link, event, p_data);
break;
default:
break;
}
app_lea_uca_dump_ase_info(p_link);
app_lea_uca_dump_call_info(p_link);
}
static void app_lea_uca_link_state_change(T_APP_LE_LINK *p_link, T_LEA_LINK_STATE state)
{
APP_PRINT_INFO2("app_lea_uca_link_state_change: change from %d to %d", p_link->lea_link_state,
state);
p_link->lea_link_state = state;
}
For example, unicast audio state machine idle state app_lea_uca_link_idle()
handles LEA_CONNECT
event, and the state will change to LEA_LINK_CONNECTED
from LEA_LINK_IDLE
.
static void app_lea_uca_link_idle(T_APP_LE_LINK *p_link, uint8_t event, void *p_data)
{
APP_PRINT_INFO2("app_lea_uca_link_idle: event %x, state %x", event, p_link->lea_link_state);
switch (event)
{
case LEA_CONNECT:
{
// TODO: to avoid service discovery taking long time, change to 7.5ms
//ble_set_prefer_conn_param(p_link->conn_id, 0x06, 0x06, 0, 500);
app_lea_uca_link_state_change(p_link, LEA_LINK_CONNECTED);
app_bond_le_set_bond_flag((void *)p_link, BOND_FLAG_LEA);
app_sniff_mode_b2s_enable_all(SNIFF_DISABLE_MASK_LEA);
}
break;
default:
break;
}
}
CIS Media
For CIS Acceptor down-link audio path, app_lea_uca_handle_iso_data()
sends ISO data to DSP, which can decode LC3 audio data to PCM, and output audio by codec.
void app_lea_uca_handle_iso_data(T_BT_DIRECT_CB_DATA *p_data)
{
T_LEA_ASE_ENTRY *p_ase_entry;
p_ase_entry = app_lea_ascs_find_ase_entry_non_conn(LEA_ASE_DOWN_DIRECT,
(void *)&p_data->p_bt_direct_iso->conn_handle,
NULL);
if (p_ase_entry != NULL)
{
uint16_t written_len;
T_AUDIO_STREAM_STATUS status;
if (p_data->p_bt_direct_iso->iso_sdu_len != 0)
{
status = AUDIO_STREAM_STATUS_CORRECT;
}
else
{
status = AUDIO_STREAM_STATUS_LOST;
}
audio_track_write(p_ase_entry->track_handle, p_data->p_bt_direct_iso->time_stamp,
p_data->p_bt_direct_iso->pkt_seq_num,
status,
p_ase_entry->frame_num,
p_data->p_bt_direct_iso->p_buf + p_data->p_bt_direct_iso->offset,
p_data->p_bt_direct_iso->iso_sdu_len,
&written_len);
}
}
The Acceptor can control the remote connected device’s music player by mcp_client_write_media_cp()
to fill the specific parameter.
For example, the Acceptor can pause the music player.
T_MCP_CLIENT_WRITE_MEDIA_CP_PARAM param;
param.opcode = MCS_MEDIA_CONTROL_POINT_CHAR_OPCODE_PAUSE;
mcp_client_write_media_cp(p_link->conn_handle, 0, p_link->gmcs, ¶m, true);
CIS Conversation
For the conversation scenario, the Acceptor also needs to handle the up-link audio path. app_lea_uca_audio_cback()
receives ISO data from the DSP.
static void app_lea_uca_audio_cback(T_AUDIO_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_AUDIO_EVENT_PARAM *param = event_buf;
switch (event_type)
{
...
case AUDIO_EVENT_TRACK_DATA_IND:
{
uint32_t timestamp;
uint16_t seq_num;
uint8_t frame_num;
uint16_t read_len;
uint8_t *buf;
T_AUDIO_STREAM_STATUS status;
if (param->track_data_ind.len == 0)
{
return;
}
buf = malloc(param->track_data_ind.len);
if (buf == NULL)
{
return;
}
if (audio_track_read(param->track_data_ind.handle,
×tamp,
&seq_num,
&status,
&frame_num,
buf,
param->track_data_ind.len,
&read_len) == true)
{
uint16_t conn_handle = 0;
#if F_APP_CCP_SUPPORT
conn_handle = app_lea_ccp_get_active_conn_handle();
#endif
T_LEA_ASE_ENTRY *p_ase_entry = app_lea_ascs_find_ase_entry(LEA_ASE_TRACK, conn_handle,
param->track_data_ind.handle);
if (p_ase_entry != NULL)
{
if (app_lea_uca_mic_ignore_cnt == 0)
{
gap_iso_send_data((uint8_t *)buf, p_ase_entry->cis_conn_handle, param->track_data_ind.len, true,
timestamp,
seq_num);
}
if (app_lea_uca_mic_ignore_cnt > 0)
{
app_lea_uca_mic_ignore_cnt--;
}
}
}
free(buf);
}
break;
}
}
Actually, the Acceptor can control the remote connected device’s telephone call by ccp_client_write_call_cp()
filling the specific parameter.
For example, the Acceptor can accept the incoming call.
T_CCP_CLIENT_WRITE_CALL_CP_PARAM write_call_cp_param = {0};
write_call_cp_param.opcode = TBS_CALL_CONTROL_POINT_CHAR_OPCODE_ACCEPT;
write_call_cp_param.param.accept_opcode_call_index = p_call_entry->call_index;
ccp_client_write_call_cp(p_link->conn_handle, 0, p_link->gtbs, false, &write_call_cp_param);
CIS VCS
For LE Audio volume control, the Acceptor also needs to save the volume value and SPK mute state to FTL from the volume controller. The function app_lea_vol_update_track_volume()
will configure the current Audio Track volume using the VCS value to accurately reflect the audio output.
static uint16_t app_lea_vcs_handle_vcs_msg(T_LE_AUDIO_MSG msg, void *buf)
{
...
switch (msg)
{
case LE_AUDIO_MSG_VCS_VOLUME_CP_IND:
{
T_APP_LE_LINK *p_link;
T_VCS_VOLUME_CP_IND *p_vcs_vol_state = (T_VCS_VOLUME_CP_IND *)buf;
p_link = app_link_find_le_link_by_conn_handle(p_vcs_vol_state->conn_handle);
if (p_link != NULL)
{
app_cfg_nv.lea_vol_setting = p_vcs_vol_state->volume_setting;
app_cfg_nv.lea_vol_out_mute = p_vcs_vol_state->mute;
APP_PRINT_TRACE3("app_lea_vcs_handle_vcs_msg: LE_AUDIO_MSG_VCS_VOLUME_CP_IND \
conn_handle 0x%02X, volume_setting 0x%02X, mute %d",
p_vcs_vol_state->conn_handle,
p_vcs_vol_state->volume_setting,
p_vcs_vol_state->mute);
}
}
break;
default:
break;
}
return cb_result;
}
static uint16_t app_lea_vol_ble_audio_cback(T_LE_AUDIO_MSG msg, void *buf)
{
...
switch (msg)
{
case LE_AUDIO_MSG_VCS_VOLUME_CP_IND:
{
T_APP_LE_LINK *p_link;
T_VCS_VOLUME_CP_IND *p_vcs_vol_state = (T_VCS_VOLUME_CP_IND *)buf;
p_link = app_link_find_le_link_by_conn_handle(p_vcs_vol_state->conn_handle);
if ((p_link != NULL))
{
app_lea_vol_update_track_volume();
}
}
break;
default:
break;
}
return cb_result;
}
The Acceptor can change the local device’s audio volume, such as volume up/down, and SPK mute/unmute.
It will always notify the new volume state by calling vcs_set_param(&vcs_param)
to the remotely connected device.
T_VCS_PARAM vcs_param;
vcs_get_param(&vcs_param);
...
vcs_param.volume_setting = volume;
vcs_param.mute = mute;
vcs_param.volume_flags = VCS_USER_SET_VOLUME_SETTING;
vcs_param.change_counter++;
vcs_set_param(&vcs_param);
BIS Acceptor
BIS Acceptor Initialization
app_lea_acc_profile_init()
is utilized to initialize BIS Acceptor-related services and profiles.app_lea_pacs_init()
is utilized to initialize PACS.app_lea_vcs_init()
is utilized to initialize VCS.app_lea_bca_init()
is utilized to initialize broadcast audio-related functions.app_lea_profile_bap_init()
is utilized to initialize BAP role and ISO capabilities.app_lea_csis_init()
is utilized to initialize CSIS.app_lea_profile_cap_init()
is utilized to initialize CAS.app_lea_profile_tmas_init()
is utilized to initialize TMAS.
void app_lea_acc_profile_init(void)
{
T_CAP_INIT_PARAMS cap_init_param = {0};
app_lea_pacs_init();
#if F_APP_VCS_SUPPORT
app_lea_vcs_init();
#endif
#if F_APP_TAMP_BMR_SUPPORT
app_lea_bca_init();
#endif
app_lea_profile_bap_init();
#if F_APP_CSIS_SUPPORT
app_lea_csis_init(&cap_init_param);
#endif
app_lea_profile_cap_init(&cap_init_param);
#if F_APP_TMAS_SUPPORT
app_lea_profile_tmas_init();
#endif
}
BIS Acceptor Connect
The Acceptor must establish a connection with the Commander if it wants to be controlled by a remote Commander. To accomplish this, the Acceptor shall enable LE advertising before the Initiator scanning and creating a connection. The Acceptor could stop the advertising by calling app_lea_adv_stop()
if it wants to stop advertising or timeout.
void app_lea_adv_start(uint8_t mode)
{
uint8_t public_addr_lsb[6];
uint8_t len = 0;
gap_get_param(GAP_PARAM_BD_ADDR, public_addr_lsb);
APP_PRINT_INFO2("app_lea_adv_start: public addr %s mode %d",
TRACE_BDADDR(public_addr_lsb), mode);
if ((mode == LEA_ADV_MODE_PAIRING) && (app_link_get_le_link_num() > 2))
{
return;
}
if (app_lea_adv_state == BLE_EXT_ADV_MGR_ADV_DISABLED)
{
#if F_APP_LE_AUDIO_CIS_RND_ADDR
ble_ext_adv_mgr_set_random(app_lea_adv_handle, app_cfg_nv.lea_static_random_addr);
#endif
if (ble_ext_adv_mgr_enable(app_lea_adv_handle, 0) == GAP_CAUSE_SUCCESS)
{
if ((T_LEA_ADV_MODE)mode == LEA_ADV_MODE_LINK_LOSS_LINK_BACK)
{
app_start_timer(&timer_idx_lea_adv, "lea_adv_linkloss", app_lea_adv_timer_id,
APP_LEA_TMR_LINKLOSS, 0, false, LEA_ADV_TMR_LINK_LOST * 1000);
}
else if ((T_LEA_ADV_MODE)mode == LEA_ADV_MODE_PAIRING)
{
app_start_timer(&timer_idx_lea_adv, "lea_adv_pairing", app_lea_adv_timer_id,
APP_LEA_TMR_PAIRING, 0, false, LEA_ADV_TMR_PAIRING * 1000);
}
}
}
return;
}
void app_lea_adv_stop()
{
app_stop_timer(&timer_idx_lea_adv);
ble_ext_adv_mgr_disable(app_lea_adv_handle, 0);
}
app_lea_bca_sm()
function is used to handle Bluetooth Low Energy Audio events based on the corresponding state.
bool app_lea_bca_sm(uint8_t event, void *p_data)
{
APP_PRINT_INFO2("app_lea_bca_sm: event 0x%x, state 0x%x", event,
app_lea_bca_state_machine);
bool accept = true;
switch (app_lea_bca_state_machine)
{
case LEA_BCA_STATE_IDLE:
{
accept = app_lea_bca_state_idle(event, p_data);
}
break;
case LEA_BCA_STATE_PRE_SCAN:
case LEA_BCA_STATE_PRE_ADV:
accept = app_lea_bca_state_starting(event, p_data);
break;
case LEA_BCA_STATE_CONN_SCAN:
case LEA_BCA_STATE_CONN:
accept = app_lea_bca_state_conn(event, p_data);
break;
case LEA_BCA_STATE_SCAN:
accept = app_lea_bca_state_scan(event, p_data);
break;
case LEA_BCA_STATE_STREAMING:
accept = app_lea_bca_state_streaming(event, p_data);
break;
default:
accept = false;
break;
}
return accept;
}
BIS Acceptor Synchronize
If the Acceptor wants to synchronize a source autonomously without an assistant, it must scan the extended advertising and periodic advertising nearby, and create the Periodic Advertising (PA) and Broadcast Isochronous Stream (BIS) synchronization.
In order to receive the broadcast sync state and event, and to initiate state changes and further operations at a higher layer, the following broadcast audio sync callback function needs to be registered.
void app_lea_bca_sync_cb(T_BLE_AUDIO_SYNC_HANDLE handle, uint8_t cb_type, void *p_cb_data)
{
T_BLE_AUDIO_SYNC_CB_DATA *p_sync_cb = (T_BLE_AUDIO_SYNC_CB_DATA *)p_cb_data;
T_APP_LEA_BCA_DB *p_bc_source = app_lea_bca_find_device_by_bs_handle(handle);
if (p_bc_source == NULL)
{
return;
}
APP_PRINT_INFO1("app_lea_bca_sync_cb: cb_type %d", cb_type);
switch (cb_type)
{
...
case MSG_BLE_AUDIO_PA_SYNC_STATE:
{
APP_PRINT_INFO3("MSG_BLE_AUDIO_PA_SYNC_STATE: sync_state %d, action %d, cause 0x%x\r\n",
p_sync_cb->p_pa_sync_state->sync_state,
p_sync_cb->p_pa_sync_state->action,
p_sync_cb->p_pa_sync_state->cause);
T_APP_LEA_BCA_SYNC_CHECK para;
para.type = BS_TYPE_PA;
para.p_sync_cb = p_sync_cb;
para.p_bc_source = p_bc_source;
para.handle = handle;
//PA sync lost
if (((p_sync_cb->p_pa_sync_state->action == BLE_AUDIO_PA_LOST) ||
(p_sync_cb->p_pa_sync_state->action == BLE_AUDIO_PA_SYNC)) &&
(p_sync_cb->p_pa_sync_state->sync_state == GAP_PA_SYNC_STATE_TERMINATED) &&
(p_sync_cb->p_pa_sync_state->cause == 0x13e))
{
if (app_lea_bca_release_all(para))
{
app_lea_bca_tgt_active(false, (APP_BIS_BIS_CTRL_RESET_ACTIVE & APP_BIS_BIS_CTRL_RESET_SD_ACTIVE));
app_lea_mgr_tri_mmi_handle_action(MMI_BIG_START, true);
}
}
else if ((p_sync_cb->p_pa_sync_state->sync_state == GAP_PA_SYNC_STATE_SYNCHRONIZING_WAIT_SCANNING)
&&
!p_bc_source->is_past)
{
p_bc_source->is_past = 1;
app_lea_acc_scan_start();
}
else if (((p_sync_cb->p_pa_sync_state->action == BLE_AUDIO_PA_TERMINATE) &&
(p_sync_cb->p_pa_sync_state->cause == 0x144) &&
(p_sync_cb->p_pa_sync_state->sync_state == GAP_PA_SYNC_STATE_TERMINATED)))
{
if (app_lea_bca_release_all(para))
{
app_lea_bca_tgt_active(false, (APP_BIS_BIS_CTRL_RESET_ACTIVE & APP_BIS_BIS_CTRL_RESET_SD_ACTIVE));
}
}
else if (((p_sync_cb->p_pa_sync_state->action == BLE_AUDIO_PA_TERMINATE) &&
(p_sync_cb->p_pa_sync_state->cause == 0x116) &&
(p_sync_cb->p_pa_sync_state->sync_state == GAP_PA_SYNC_STATE_TERMINATED)))
{
if (app_lea_bca_state() == LEA_BCA_STATE_WAIT_TERM ||
app_lea_bca_state() == LEA_BCA_STATE_WAIT_RETRY)
{
if (app_lea_bca_release_all(para))
{
app_lea_bca_tgt_active(false, (APP_BIS_BIS_CTRL_RESET_ACTIVE & APP_BIS_BIS_CTRL_RESET_SD_ACTIVE));
}
}
}
p_bc_source->sync_state = p_sync_cb->p_pa_sync_state->sync_state;
}
break;
case MSG_BLE_AUDIO_BASE_DATA_MODIFY_INFO:
{
APP_PRINT_TRACE3("MSG_BLE_AUDIO_BASE_DATA_MODIFY_INFO: p_base_mapping %p, used %d, sd_source %d\r\n",
p_sync_cb->p_base_data_modify_info->p_base_mapping, p_bc_source->used, app_lea_bca_get_sd_source());
if (p_sync_cb->p_base_data_modify_info->p_base_mapping)
{
T_BASE_DATA_MAPPING *p_mapping = p_sync_cb->p_base_data_modify_info->p_base_mapping;
app_lea_bca_remap_bis(p_mapping, handle);
p_bc_source->used |= APP_LEA_BCA_BASE_DATA;
if (p_bc_source->used & APP_LEA_BCA_BIG_INFO &&
((app_lea_bca_bs_tgt.ctrl & APP_BIS_BIS_CTRL_SD_ACTIVE) == 0))
{
if (app_lea_bca_get_sd_source() != 0xff && p_bc_source->is_encryp)
{
bass_send_broadcast_code_required(p_bc_source->source_id);
}
else
{
app_lea_bca_big_establish(handle, p_bc_source->is_encryp);
}
}
}
}
break;
case MSG_BLE_AUDIO_PA_REPORT_INFO:
{
if (p_bc_source->big_state == BIG_SYNC_RECEIVER_SYNC_STATE_SYNCHRONIZED)
{
ble_audio_pa_terminate(p_bc_source->sync_handle);
}
}
break;
case MSG_BLE_AUDIO_PA_BIGINFO:
{
T_LE_BIGINFO_ADV_REPORT_INFO *p_adv_report_info = p_sync_cb->p_le_biginfo_adv_report_info;
APP_PRINT_INFO7("MSG_BLE_AUDIO_PA_BIGINFO: num_bis %d, %d, %d, %d, 0x%x ,%d, %d",
p_adv_report_info->num_bis,
p_adv_report_info->encryption,
app_lea_bca_state_machine,
p_bc_source->used,
app_lea_bca_get_sd_source(),
p_bc_source->is_encryp,
p_adv_report_info->encryption);
if (app_lea_bca_state_machine == LEA_BCA_STATE_WAIT_TERM)
{
break;
}
p_bc_source->used |= APP_LEA_BCA_BIG_INFO;
p_bc_source->is_encryp = p_adv_report_info->encryption;
if ((p_bc_source->used & APP_LEA_BCA_BASE_DATA) == 0)
{
break;
}
if (app_lea_bca_bs_tgt.ctrl & APP_BIS_BIS_CTRL_SD_ACTIVE)
{
T_BIG_MGR_SYNC_RECEIVER_BIG_CREATE_SYNC_PARAM sync_param;
app_lea_bca_apply_sync_pera(&sync_param, handle, p_adv_report_info->encryption);
}
else
{
if (app_lea_bca_get_sd_source() != 0xff &&
!p_bc_source->is_encryp && p_adv_report_info->encryption)
{
bass_send_broadcast_code_required(p_bc_source->source_id);
}
else
{
app_lea_bca_big_establish(handle, p_adv_report_info->encryption);
}
}
}
break;
case MSG_BLE_AUDIO_BIG_SYNC_STATE:
{
T_APP_LEA_BCA_SYNC_CHECK para;
para.type = BS_TYPE_BIG;
para.p_sync_cb = p_sync_cb;
para.p_bc_source = p_bc_source;
para.handle = handle;
APP_PRINT_INFO4("MSG_BLE_AUDIO_BIG_SYNC_STATE: sync_state %d, action %d,action role %d, cause 0x%x\r\n",
p_sync_cb->p_big_sync_state->sync_state,
p_sync_cb->p_big_sync_state->action,
p_sync_cb->p_big_sync_state->action_role,
p_sync_cb->p_big_sync_state->cause);
if ((p_sync_cb->p_big_sync_state->sync_state == BIG_SYNC_RECEIVER_SYNC_STATE_TERMINATED) &&
(p_sync_cb->p_big_sync_state->action == BLE_AUDIO_BIG_TERMINATE ||
p_sync_cb->p_big_sync_state->action == BLE_AUDIO_BIG_IDLE) &&
(p_sync_cb->p_big_sync_state->cause == 0))
{
uint8_t dev_info = 0;
APP_PRINT_INFO1("app_lea_bca_sync_cb: BIG_SYNC_RECEIVER_SYNC_STATE_TERMINATED, sync_handle: 0x%x",
p_bc_source->sync_handle);
if (app_lea_bca_release_all(para))
{
app_lea_bca_tgt_active(false, (APP_BIS_BIS_CTRL_RESET_ACTIVE & APP_BIS_BIS_CTRL_RESET_SD_ACTIVE));
}
}
else if ((p_sync_cb->p_big_sync_state->sync_state == BIG_SYNC_RECEIVER_SYNC_STATE_TERMINATED) &&
((p_sync_cb->p_big_sync_state->action == BLE_AUDIO_BIG_SYNC) ||
(p_sync_cb->p_big_sync_state->action == BLE_AUDIO_BIG_IDLE)) &&
((p_sync_cb->p_big_sync_state->cause == 0x113) || (p_sync_cb->p_big_sync_state->cause == 0x108) ||
(p_sync_cb->p_big_sync_state->cause == 0x13e)))
{
uint8_t dev_info = 0;
if (!app_lea_bca_release_all(para))
{
app_lea_bca_state_change(LEA_BCA_STATE_WAIT_RETRY);
}
else
{
app_lea_bca_tgt_active(false, (APP_BIS_BIS_CTRL_RESET_ACTIVE & APP_BIS_BIS_CTRL_RESET_SD_ACTIVE));
}
if (p_bc_source->big_state == BIG_SYNC_RECEIVER_SYNC_STATE_SYNCHRONIZED)
{
app_audio_tone_type_play(TONE_BIS_LOSST, false, false);
}
app_lea_mgr_tri_mmi_handle_action(MMI_BIG_START, true);
}
else if (p_sync_cb->p_big_sync_state->sync_state == BIG_SYNC_RECEIVER_SYNC_STATE_SYNCHRONIZED)
{
if (p_sync_cb->p_big_sync_state->action == BLE_AUDIO_BIG_SYNC)
{
T_BLE_AUDIO_BIS_INFO bis_sync_info;
bool result = ble_audio_get_bis_sync_info(p_bc_source->sync_handle,
&bis_sync_info);
if (result == false)
{
// goto failed;
}
APP_PRINT_ERROR4("BIG_SYNC_RECEIVER_SYNC_STATE_SYNCHRONIZED%d,%d,%d, source_id=%d: ", result,
bis_sync_info.bis_num, bis_sync_info.bis_info[0].bis_idx, p_bc_source->source_id);
uint8_t codec_id[5] = {1, 0, 0, 0, 0};
uint32_t controller_delay = 0x1122;
codec_id[0] = LC3_CODEC_ID;
APP_PRINT_INFO1("app_lea_bca_sync_cb: MSG_BLE_AUDIO_BIG_SYNC_STATE: bis_idx: %d",
p_bc_source->bis_idx);
ble_audio_bis_setup_data_path(handle, p_bc_source->bis_idx, codec_id, controller_delay, 0, NULL);
app_lea_acc_scan_stop();
}
}
if (p_sync_cb->p_big_sync_state->sync_state == BIG_SYNC_RECEIVER_SYNC_STATE_TERMINATED ||
p_sync_cb->p_big_sync_state->sync_state == BIG_SYNC_RECEIVER_SYNC_STATE_TERMINATING)
{
p_bc_source->used &= (APP_LEA_BCA_BASE_DATA_RESET & APP_LEA_BCA_BIG_INFO_RESET);
}
p_bc_source->big_state = p_sync_cb->p_big_sync_state->sync_state;
}
break;
...
default:
break;
}
return;
}
Bluetooth Audio Transceiver
A2DP Transparent Transmission
SPI and A2DP Transmit Manager
In the A2DP Transparent Transmission scenario, Device 1 is connected to the phone to receive A2DP data, which is then transmitted to Device 2 through SPI. Device 2 converts the received data into SBC or LC3 format and forwards it to the headphones through A2DP or BIS. The flow is shown as follows:
-
Initiate SPI.
For Device 1, its SPI role is set as a master, and the relevant functionality is enabled by activating the
F_APP_SPI_ROLE_MASTER
flag. The initialization function to achieve this isapp_spi_master_init()
. For Device 2, its SPI role is set as a slave, and the relevant functionality is enabled by activating theF_APP_SPI_ROLE_SLAVE
flag. The initialization function isapp_spi_slave_init()
. -
A2DP format transmission.
Device 2 utilizes the Audio Pipe to convert data format, thus requiring knowledge of the specific input data format. When Device 1 receives the
BT_EVENT_A2DP_STREAM_START_IND
, it will obtain the negotiated data format and utilize theCMD_A2DP_XMIT_CONFIG
command to forward this information to Device 2 through SPI.//Device 1 sends data format information app_audio_bt_cback() |---app_audio_a2dp_stream_start_handle() |---app_a2dp_xmit_mgr_report_a2dp_param() void app_a2dp_xmit_mgr_report_a2dp_param(uint8_t *a2dp_param, uint16_t len) { app_report_event(CMD_PATH_SPI, CMD_A2DP_XMIT_CONFIG, 0, a2dp_param, len); }
Device 2 processes the received commands within the
app_a2dp_xmit_mgr_handle_cmd_set()
function. When it receives theA2DP FORMAT
command, the corresponding content is stored in thea2dp_xmit_mgr.a2dp_in_format
variable, which will be utilized by the Audio Pipe operations. Additionally, thea2dp_xmit_mgr.a2dp_in_format_ready
is set to true, effectively remembering the current state.//Device 2 receives data format information void app_a2dp_xmit_mgr_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t *ack_pkt) { ... switch (cmd_id) { ... case CMD_A2DP_XMIT_CONFIG: { uint8_t *p_param = &cmd_ptr[2]; uint16_t param_len = cmd_len - 2; app_a2dp_xmit_mgr_save_a2dp_in_format(p_param, param_len); } break; ... } }
void app_a2dp_xmit_mgr_save_a2dp_in_format(uint8_t *format_info, uint16_t param_len) { if (!a2dp_xmit_mgr.a2dp_in_format_ready) { memcpy(&a2dp_xmit_mgr.a2dp_in_format, format_info, param_len); a2dp_xmit_mgr.a2dp_in_format_ready = true; } app_a2dp_xmit_mgr_print_format("app_a2dp_xmit_mgr_save_a2dp_in_format: ", a2dp_xmit_mgr.a2dp_in_format); }
-
A2DP data transmission.
In the A2DP Transparent Transmission scenario, once Device 1 receives audio data from the mobile, it immediately forwards it to Device 2 without local playback. The audio data is transmitted via SPI together with the
CMD_A2DP_XMIT_AUDIO
command:static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len) { switch (event_type) { case BT_EVENT_A2DP_STREAM_DATA_IND: { ... #if (F_APP_A2DP_XMIT_SNK_SUPPORT || F_APP_A2DP_XMIT_SNK_LEA_SUPPORT) app_a2dp_xmit_mgr_report_a2dp_data(param->a2dp_stream_data_ind.payload, param->a2dp_stream_data_ind.len); #endif ... } } void app_a2dp_xmit_mgr_report_a2dp_data(uint8_t *p_data, uint16_t len) { app_report_event(CMD_PATH_SPI, CMD_A2DP_XMIT_AUDIO, 0, p_data, len); }
For Device 2, it cannot directly send the received audio data to the headphones because the format negotiated between Device 2 and the headphones is typically different from the format used for transmission between Device 1 and the mobile phone. Therefore, format conversion is required on the Device 2 side using the Audio Pipe. However, before proceeding with the conversion, it is important to note that upon receiving audio data, Device 2 will first store it in a pre-allocated ring buffer. This buffer serves as a temporary storage, making the data available for the subsequent Audio Pipe processing.
Currently, Device 2 supports two different output methods for audio transmission to the headphones: A2DP and BIS. To modularly build these two different output options, we have implemented the relevant functionalities for A2DP output in the
app_a2dp_xmit_src.c
file and for BIS output in theapp_a2dp_xmit_lea.c
file. The data processing functions for these outputs areapp_a2dp_xmit_src_handle_a2dp_data_ind()
andapp_a2dp_xmit_lea_handle_a2dp_data_ind()
respectively.void app_a2dp_xmit_mgr_handle_a2dp_data_in(uint8_t *p_audio, uint16_t audio_len) { switch (a2dp_xmit_mgr.xmit_play_route) { #if F_APP_A2DP_XMIT_SRC_SUPPORT case XMIT_PLAY_ROUTE_A2DP_SRC: { app_a2dp_xmit_src_handle_a2dp_data_ind(p_audio, audio_len); } break; #endif #if F_APP_A2DP_XMIT_SRC_LEA_SUPPORT case XMIT_PLAY_ROUTE_BIS: { app_a2dp_xmit_lea_handle_a2dp_data_ind(p_audio, audio_len); } break; #endif default: APP_PRINT_ERROR1("app_a2dp_xmit_mgr_handle_a2dp_data_in: invalid route path %d", a2dp_xmit_mgr.xmit_play_route); break; } }
It may have been noticed that when receiving data, the processing is determined based on the variable
a2dp_xmit_mgr.xmit_play_route
which distinguishes whether it should be handled by theapp_a2dp_xmit_src_handle_a2dp_data_ind()
function or theapp_a2dp_xmit_lea_handle_a2dp_data_ind()
function. The value of this variable determines the appropriate function to select for processing the received audio data. From the user’s perspective, a prior selection between A2DP or BIS output must be made by issuing the commandCMD_A2DP_XMIT_ROUTE_OUT_CTRL
through ACI Host CLI Tool. The Bluetooth Audio Transceiver APP will then store this choice in the variablea2dp_xmit_mgr.xmit_play_route
to perform audio data demultiplexing based on the selected output.void app_a2dp_xmit_mgr_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t *ack_pkt) { ... switch (cmd_id) { ... case CMD_A2DP_XMIT_SET_ROUTE_OUT: { a2dp_xmit_mgr.xmit_play_route = (T_A2DP_XMIT_PLAY_ROUTE)cmd_ptr[2]; app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt); } break; ... } }
A2DP Output
-
Request starting streaming.
For Device 2’s output, the initiation or termination is controlled by the
CMD_A2DP_XMIT_ROUTE_OUT_CTRL
command. For A2DP output, upon receiving theXMIT_PLAY_STATE_START
parameter, theapp_a2dp_xmit_src_stream_start_req()
function initiates anAVDTP_START
request to the mobile phone.void app_a2dp_xmit_mgr_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr, uint16_t cmd_len, uint8_t *ack_pkt) { ... switch (cmd_id) { ... case CMD_A2DP_XMIT_ROUTE_OUT_CTRL: { T_A2DP_XMIT_PLAY_STATE type = (T_A2DP_XMIT_PLAY_STATE)cmd_ptr[2]; app_a2dp_xmit_mgr_route_out_start_stop(type); app_cmd_set_event_ack(cmd_path, app_idx, ack_pkt); } break; ... } } app_a2dp_xmit_mgr_route_out_start_stop() |---app_a2dp_xmit_src_start_stop() |---app_a2dp_xmit_src_stream_start_req() |---bt_a2dp_stream_start_req() static void app_a2dp_xmit_src_stream_start_req(void) { bt_a2dp_stream_start_req(a2d_src_ctrl.sink_addr); }
Subsequently, the phone establishes an A2DP connection and negotiates the audio format. The APP retrieves the negotiation result through
BT_EVENT_A2DP_CONFIG_CMPL
and stores the format in thea2d_src_ctrl.format_info_out
variable.app_audio_bt_cback() //BT_EVENT_A2DP_CONFIG_CMPL |---app_a2dp_xmit_src_save_a2dp_out_param() void app_a2dp_xmit_src_save_a2dp_out_param(uint8_t *format_info) { if (!a2d_src_ctrl.data_route_out_ready) { memcpy(&a2d_src_ctrl.format_info_out, format_info, sizeof(T_AUDIO_FORMAT_INFO)); a2d_src_ctrl.data_route_out_ready = true; } app_a2dp_xmit_mgr_print_format("app_a2dp_xmit_src_save_a2dp_out_param: ", a2d_src_ctrl.format_info_out); }
-
Create Audio Pipe.
Once the mobile phone accepts Device 2’s stream start request, the APP receives the
BT_EVENT_A2DP_STREAM_START_RSP
event. At this point, both the audio input format transmitted from Device 1 and the output format negotiated with the phone are available. Consequently, the APP can create the Audio Pipe for further processing.app_a2dp_xmit_src_bt_cback() //BT_EVENT_A2DP_STREAM_START_RSP |---app_a2dp_xmit_src_start_rsp_handler() |---app_a2dp_xmit_src_param_rcfg() static uint8_t app_a2dp_xmit_src_param_rcfg(void) { uint8_t res = A2DP_XMIT_MGR_SUCCESS; if (audio_pipe_handle != NULL) { res = A2DP_XMIT_MGR_PIPE_CREATE_ERROR; return res; } if (!app_a2dp_xmit_mgr_get_a2dp_in_format((uint8_t *)&a2d_src_ctrl.format_info_in)) { APP_PRINT_ERROR0("app_a2dp_xmit_lea_pipe_rcfg: a2dp format info does not exist"); res = A2DP_XMIT_MGR_SYS_ERROR; return res; } if (a2d_src_ctrl.data_route_out_ready) { T_AUDIO_FORMAT_INFO format_info_in = a2d_src_ctrl.format_info_in; T_AUDIO_FORMAT_INFO format_info_out = a2d_src_ctrl.format_info_out; if (audio_pipe_handle == NULL) { audio_pipe_handle = audio_pipe_create(format_info_in, format_info_out, a2dp_gain_table[app_cfg_nv.audio_gain_level[cur_pair_idx]], audio_pipe_callback); } } else { res = A2DP_XMIT_MGR_SYS_ERROR; } return res; }
-
Fill data to Audio Pipe.
When creating the Audio Pipe, the
audio_pipe_callback()
function is registered. Upon receiving theAUDIO_PIPE_EVENT_STARTED
, it indicates that the Audio Pipe is ready to operate. In this event, theapp_a2dp_xmit_src_fill_pipe()
function is used to fill the first packet of data to the Audio Pipe. After completing the filling, anAUDIO_PIPE_EVENT_DATA_FILLED
event is received, allowing for the continued filling of data. When there is no data available in the ring buffer for filling, theflag_pipe_get_data_empty
is set.audio_pipe_callback() //AUDIO_PIPE_EVENT_STARTED |---app_a2dp_xmit_src_fill_pipe() static uint16_t app_a2dp_xmit_src_fill_pipe(void) { uint16_t res = A2DP_XMIT_MGR_SUCCESS; uint8_t frame_info[2]; res = app_a2dp_xmit_mgr_a2dp_raw_data_read(frame_info, 2); if (res == A2DP_XMIT_MGR_SUCCESS) { uint16_t audio_len = (uint16_t)(frame_info[0] | (frame_info[1] << 8)); uint8_t *p_read_data_buf = malloc(audio_len); if (p_read_data_buf) { res = app_a2dp_xmit_mgr_a2dp_raw_data_read(p_read_data_buf, audio_len); if (res == A2DP_XMIT_MGR_SUCCESS) { flag_pipe_get_data_empty = false; APP_PRINT_INFO2("app_a2dp_xmit_src_fill_pipe: seq_num %d, audio_len %d", seq_num, audio_len); if (!audio_pipe_fill(audio_pipe_handle, 0, seq_num, AUDIO_STREAM_STATUS_CORRECT, 0, (void *)p_read_data_buf, audio_len)) { res = A2DP_XMIT_MGR_PIPE_FILL_ERROR; } else { seq_num++; } } else { flag_pipe_get_data_empty = true; } free(p_read_data_buf); } else { res = A2DP_XMIT_MGR_MEM_ERROR; } } else { flag_pipe_get_data_empty = true; } return res; }
If data is received from Device 1 and
flag_pipe_get_data_empty
is set, the data is directly taken and filled into the pipe for processing.app_a2dp_xmit_mgr_handle_a2dp_data_in() |---app_a2dp_xmit_src_handle_a2dp_data_ind() void app_a2dp_xmit_src_handle_a2dp_data_ind(uint8_t *data, uint16_t len) { if (a2d_src_ctrl.play_state != XMIT_PLAY_STATE_START) { APP_PRINT_ERROR1("app_a2dp_xmit_src_handle_a2dp_data_ind: play_state %d err", a2d_src_ctrl.play_state); return; } if (app_a2dp_xmit_mgr_a2dp_raw_data_write(data, len) == A2DP_XMIT_MGR_SUCCESS) { if (flag_pipe_get_data_empty) { app_a2dp_xmit_src_fill_pipe(); } } }
-
Send generated data to earphones.
After converting the data format using the Audio Pipe, the APP will receive an
AUDIO_PIPE_EVENT_DATA_IND
event inaudio_pipe_cllback()
. Upon receiving this event, the transformed audio data can be obtained using theaudio pipe drain()
function and subsequently sent to the headphones with thebt_a2dp_stream_data_send()
function.audio_pipe_callback() //AUDIO_PIPE_EVENT_DATA_IND |---app_a2dp_xmit_src_pipe_data_ind() static uint16_t app_a2dp_xmit_src_pipe_data_ind(void) { uint16_t res = A2DP_XMIT_MGR_SUCCESS; uint16_t data_len = 0; uint16_t frame_number = 0; audio_pipe_drain(audio_pipe_handle, p_drain_data_buf, &data_len, &frame_number); if (data_len == 0) { res = A2DP_XMIT_MGR_PIPE_DRAIN_ERROR; } else { if (src_a2dp_credits) { if (bt_a2dp_stream_data_send(a2d_src_ctrl.sink_addr, a2dp_seq_num, (uint8_t)frame_number, p_drain_data_buf, data_len)) { a2dp_seq_num++; src_a2dp_credits--; } else { res = A2DP_XMIT_MGR_DATA_SEND_ERROR; } } else { APP_PRINT_WARN0("app_a2dp_xmit_src_pipe_data_ind: no reason need to send"); } } APP_PRINT_INFO1("app_a2dp_xmit_src_pipe_data_ind: res 0x%x", res); return res; }
-
Request stopping streaming.
To stop the audio conversion and data transmission on Device 2, similar to the initialization process, use the
CMD_A2DP_XMIT_ROUTE_OUT_CTRL
command with theXMIT_PLAY_STATE_IDLE
parameter. This action will initiate anAVDTP_SUSPEND
request, release the Audio Pipe, and clear any residual audio data from Device 1.app_a2dp_xmit_mgr_route_out_start_stop() |---app_a2dp_xmit_src_start_stop() |---app_a2dp_xmit_src_stream_stop() static void app_a2dp_xmit_src_stream_stop(void) { if (a2d_src_ctrl.bt_strm_state != A2DP_XMIT_SRC_STREAM_STOP) { bt_a2dp_stream_suspend_req(a2d_src_ctrl.sink_addr); } if (a2d_src_ctrl.play_state == XMIT_PLAY_STATE_START) { a2d_src_ctrl.play_state = XMIT_PLAY_STATE_IDLE; } if (audio_pipe_handle != NULL) { audio_pipe_release(audio_pipe_handle); audio_pipe_handle = NULL; } app_a2dp_xmit_mgr_a2dp_raw_data_clear(); app_dlps_enable(APP_DLPS_ENTER_CHECK_PLAYBACK); }
BIS Output
-
Initiate BIS and start streaming.
To utilize the BIS for audio output, first initialize it. By using the
CMD_LEA_BSRC_START
command, options for a single BIS output (Stereo) or dual BIS outputs (Left and Right). Subsequently, use theCMD_A2DP_XMIT_ROUTE_OUT_CTRL
command with theXMIT_PLAY_STATE_START
parameter to establish one or two datapaths for BIS, making it into a streaming state.app_a2dp_xmit_mgr_handle_cmd_set() //CMD_A2DP_XMIT_ROUTE_OUT_CTRL |---app_a2dp_xmit_mgr_route_out_start_stop() |---app_a2dp_xmit_lea_src_start_stop() |---app_lea_bsrc_start() void app_a2dp_xmit_lea_src_start_stop(T_A2DP_XMIT_PLAY_STATE type) { APP_PRINT_INFO1("app_a2dp_xmit_lea_src_start_stop: %d", type); if (type == XMIT_PLAY_STATE_START) { app_lea_bsrc_start(); } else if (type == XMIT_PLAY_STATE_IDLE) { app_lea_bsrc_stop(true); } }
-
Create Audio Pipe.
Upon successful creation of the datapath, the output format will be saved, and an Audio Pipe will be generated in the
app_a2dp_xmit_lea_pipe_rcfg()
function.void app_lea_handle_bis_data_path_setup(T_LEA_SETUP_DATA_PATH *p_data) { ... if (p_data->path_direction == DATA_PATH_INPUT_FLAG) { if (app_db.bsrc_db.cfg_bis_num == app_db.iso_input_queue.count) { app_lea_save_data_format(p_iso_chann); #if F_APP_A2DP_XMIT_SRC_LEA_SUPPORT app_a2dp_xmit_lea_pipe_rcfg(); // TODO: put to BROADCAST_SOURCE_STATE_CONFIGURED? #endif } } }
void app_a2dp_xmit_lea_pipe_rcfg(void) { if (!app_a2dp_xmit_mgr_get_a2dp_in_format((uint8_t *)&a2dp_xmit_lea_ctrl.format_in)) { APP_PRINT_ERROR0("app_a2dp_xmit_lea_pipe_rcfg: a2dp format info does not exist"); return; } if (!app_lea_get_data_format((uint8_t *)&a2dp_xmit_lea_ctrl.format_out)) { APP_PRINT_ERROR0("app_a2dp_xmit_lea_pipe_rcfg: lc3 format info does not exist"); return; } app_a2dp_xmit_mgr_print_format("app_a2dp_xmit_lea_pipe_rcfg: ", a2dp_xmit_lea_ctrl.format_in); app_a2dp_xmit_mgr_print_format("app_a2dp_xmit_lea_pipe_rcfg: ", a2dp_xmit_lea_ctrl.format_out); if (a2dp_xmit_lea_ctrl.format_out.attr.lc3.chann_location == AUDIO_LOCATION_MONO) { a2dp_xmit_lea_ctrl.chnl_cnt = 1; } else { a2dp_xmit_lea_ctrl.chnl_cnt = __builtin_popcount( a2dp_xmit_lea_ctrl.format_out.attr.lc3.chann_location); } uint16_t len = a2dp_xmit_lea_ctrl.format_out.attr.lc3.frame_length * a2dp_xmit_lea_ctrl.chnl_cnt; a2dp_xmit_lea_ctrl.p_lea_send_buf = calloc(1, len); APP_PRINT_INFO1("app_a2dp_xmit_lea_pipe_rcfg: p_lea_send_buf len %d", len); if (a2dp_xmit_lea_ctrl.p_lea_send_buf == NULL) { APP_PRINT_ERROR0("app_a2dp_xmit_lea_pipe_rcfg: p_lea_send_buf malloc fail"); return; } if (audio_pipe_handle == NULL) { audio_pipe_handle = audio_pipe_create(a2dp_xmit_lea_ctrl.format_in, a2dp_xmit_lea_ctrl.format_out, app_dsp_cfg_vol.playback_volume_default, app_a2dp_xmit_lea_pipe_callback); } uint32_t sync_timer_period = 0; if (a2dp_xmit_lea_ctrl.format_out.attr.lc3.frame_duration == AUDIO_LC3_FRAME_DURATION_10_MS) { app_a2dp_xmit_lea_sync_timer_init(10000); } else { app_a2dp_xmit_lea_sync_timer_init(7500); } }
-
Fill data to Audio Pipe.
When creating the Audio Pipe, the
app_a2dp_xmit_lea_pipe_callback()
function is registered. Upon receiving theAUDIO_PIPE_EVENT_STARTED
, it indicates that the Audio Pipe is ready to operate. In this event, theapp_a2dp_xmit_lea_fill_pipe()
function is used to fill the first packet of data to Audio Pipe. After completing the filling, anAUDIO_PIPE_EVENT_DATA_FILLED
event is received, allowing the continued filling of data.When there is no data available in the ring buffer for filling, the
flag_pipe_get_data_empty
is set.app_a2dp_xmit_lea_pipe_callback() //AUDIO_PIPE_EVENT_STARTED |---app_a2dp_xmit_lea_fill_pipe() static uint16_t app_a2dp_xmit_lea_fill_pipe(void) { uint16_t res = A2DP_XMIT_MGR_SUCCESS; uint8_t frame_info[2]; res = app_a2dp_xmit_mgr_a2dp_raw_data_read(frame_info, 2); if (res == A2DP_XMIT_MGR_SUCCESS) { uint16_t audio_len = (uint16_t)(frame_info[0] | (frame_info[1] << 8)); uint8_t *p_read_data_buf = malloc(audio_len); if (p_read_data_buf) { res = app_a2dp_xmit_mgr_a2dp_raw_data_read(p_read_data_buf, audio_len); if (res == A2DP_XMIT_MGR_SUCCESS) { flag_pipe_get_data_empty = false; APP_PRINT_INFO2("app_a2dp_xmit_lea_fill_pipe: pipe_fill_seq %d, audio_len %d", a2dp_xmit_lea_ctrl.pipe_fill_seq, audio_len); if (!audio_pipe_fill(audio_pipe_handle, 0, a2dp_xmit_lea_ctrl.pipe_fill_seq, AUDIO_STREAM_STATUS_CORRECT, 0, (void *)p_read_data_buf, audio_len)) { res = A2DP_XMIT_MGR_PIPE_FILL_ERROR; } else { a2dp_xmit_lea_ctrl.pipe_fill_seq++; } } else { flag_pipe_get_data_empty = true; } free(p_read_data_buf); } else { res = A2DP_XMIT_MGR_MEM_ERROR; } } else { flag_pipe_get_data_empty = true; } return res; }
If data is received from Device 1 and
flag_pipe_get_data_empty
is set, the data is directly taken and filled into the pipe for processing.app_a2dp_xmit_mgr_handle_a2dp_data_in() |---app_a2dp_xmit_lea_handle_a2dp_data_ind() void app_a2dp_xmit_lea_handle_a2dp_data_ind(uint8_t *p_audio, uint16_t audio_len) { if (a2dp_xmit_lea_ctrl.play_state == XMIT_PLAY_STATE_IDLE) { APP_PRINT_ERROR0("app_a2dp_xmit_lea_handle_a2dp_data_ind: brsc not started"); return; } if (app_a2dp_xmit_mgr_a2dp_raw_data_write(p_audio, audio_len) == A2DP_XMIT_MGR_SUCCESS) { if (flag_pipe_get_data_empty) { app_a2dp_xmit_lea_fill_pipe(); } } }
-
Send generated data to earphones.
After converting the data format using the Audio Pipe, the APP will receive a
AUDIO_PIPE_EVENT_DATA_IND
event inapp_a2dp_xmit_lea_pipe_callback()
. Due to the fixed interval required for BIS packet transmission, the converted data cannot be directly sent out, instead, it needs to be stored in the ring buffer ofa2dp_xmit_lea_ctrl.ring_buf
using the function ofapp_a2dp_xmit_lea_iso_data_storage()
.app_a2dp_xmit_lea_pipe_callback //AUDIO_PIPE_EVENT_DATA_IND |---app_a2dp_xmit_lea_pipe_data_ind_handler static uint16_t app_a2dp_xmit_lea_pipe_data_ind_handler(void) { uint16_t res = A2DP_XMIT_MGR_SUCCESS; uint16_t data_len = 0; uint16_t frame_number = 0; audio_pipe_drain(audio_pipe_handle, p_drain_data_buf, &data_len, &frame_number); if (data_len == 0) { res = A2DP_XMIT_MGR_PIPE_DRAIN_ERROR; } else { res = app_a2dp_xmit_lea_iso_data_storage(p_drain_data_buf, data_len); } APP_PRINT_INFO3("app_a2dp_xmit_lea_pipe_data_ind_handler: data_len %d, frame_number %d, res %d", data_len, frame_number, res); return res; }
The packet transmission is controlled by a hardware timer. To ensure a continuous data flow for transmission, the hardware timer initiates its operation only after receiving three sets of converted data.
static bool app_a2dp_xmit_lea_pipe_callback(T_AUDIO_PIPE_HANDLE handle, T_AUDIO_PIPE_EVENT event, uint32_t param) { ... switch (event) { ... case AUDIO_PIPE_EVENT_DATA_IND: { if (app_a2dp_xmit_lea_pipe_data_ind_handler() == A2DP_XMIT_MGR_SUCCESS) { a2dp_xmit_lea_ctrl.pipe_ind_seq++; } if (!a2dp_xmit_lea_ctrl.timer_started && a2dp_xmit_lea_ctrl.pipe_ind_seq > 2) { app_a2dp_xmit_lea_sync_timer_start(); } } break; ... } ... }
When the set interval time elapses, the
app_a2dp_xmit_lea_iso_data_read()
function is utilized to retrieve the stored converted data from the ring buffer. Subsequently, the data in the transformed format is sent out using theapp_lea_iso_data_send()
function.app_a2dp_xmit_lea_sync_timer_handler() |---app_a2dp_xmit_lea_msg_send() |---app_a2dp_xmit_lea_msg_handle() |---app_a2dp_xmit_lea_send_iso_data() static void app_a2dp_xmit_lea_send_iso_data(void) { uint16_t res = A2DP_XMIT_MGR_SUCCESS; uint16_t len = a2dp_xmit_lea_ctrl.format_out.attr.lc3.frame_length * a2dp_xmit_lea_ctrl.chnl_cnt; uint8_t read_ret = app_a2dp_xmit_lea_iso_data_read(a2dp_xmit_lea_ctrl.p_lea_send_buf, len); if (read_ret != A2DP_XMIT_MGR_SUCCESS) { memset(a2dp_xmit_lea_ctrl.p_lea_send_buf, 0, len); } app_lea_iso_data_send(a2dp_xmit_lea_ctrl.p_lea_send_buf, len, false, 0, 0); }
-
Request stopping streaming.
To stop the audio conversion and data transmission on Device 2, similar to the initialization process, you need to use the
CMD_A2DP_XMIT_ROUTE_OUT_CTRL
command with theXMIT_PLAY_STATE_IDLE
parameter. This action will remove the established datapaths.app_a2dp_xmit_mgr_route_out_start_stop() |---app_a2dp_xmit_lea_src_start_stop() |---app_lea_bsrc_stop() void app_a2dp_xmit_lea_src_start_stop(T_A2DP_XMIT_PLAY_STATE type) { APP_PRINT_INFO1("app_a2dp_xmit_lea_src_start_stop: %d", type); if (type == XMIT_PLAY_STATE_START) { app_lea_bsrc_start(); } else if (type == XMIT_PLAY_STATE_IDLE) { app_lea_bsrc_stop(true); } }
Once the datapaths are removed, the Audio Pipe will be released, any residual audio data from Device 1 and generated by the Audio Pipe will be cleared in
app_a2dp_xmit_lea_handle_chann_remove()
.app_lea_handle_bis_data_path_remove() |---app_lea_remove_iso_chann() |---app_a2dp_xmit_lea_handle_chann_remove void app_a2dp_xmit_lea_handle_chann_remove(void) { APP_PRINT_INFO1("app_a2dp_xmit_lea_handle_chann_remove: iso_input_queue %d", app_db.iso_input_queue.count); if (app_db.iso_input_queue.count == 0) { if (a2dp_xmit_lea_ctrl.timer_started) { app_a2dp_xmit_lea_sync_timer_stop(); } a2dp_xmit_lea_ctrl.play_state = XMIT_PLAY_STATE_IDLE; if (audio_pipe_handle != NULL) { audio_pipe_release(audio_pipe_handle); audio_pipe_handle = NULL; } } if (a2dp_xmit_lea_ctrl.p_lea_send_buf != NULL) { free(a2dp_xmit_lea_ctrl.p_lea_send_buf); a2dp_xmit_lea_ctrl.p_lea_send_buf = NULL; } ring_buffer_clear(&a2dp_xmit_lea_ctrl.ring_buf); app_a2dp_xmit_mgr_a2dp_raw_data_clear(); }
HFP Transparent Transmission
SPI and HFP Transmit Manager
In the HFP Transparent Transmission scenario, Device 1 is connected to the phone to receive HFP data, which is then transmitted to Device 2 through SPI. Device 2 forwards it to the headphones after SCO is connected.
For Device 1, its SPI role is set as master, and the relevant functionality is enabled by activating the F_APP_SPI_ROLE_MASTER
flag. For Device 2, its SPI role is configured as slave, and the relevant functionality is enabled by activating the F_APP_SPI_ROLE_SLAVE
flag.
When Device 1 receives the BT_EVENT_SCO_CONN_CMPL
, it will obtain the negotiated data format and utilize the CMD_SCO_XMIT_CONFIG
command in app_audio_sco_conn_cmpl_handle()
to forward this information to Device 2 through SPI.
static void app_audio_sco_conn_cmpl_handle(uint8_t *bd_addr, uint8_t air_mode, uint8_t rx_pkt_len)
{
uint8_t pair_idx_mapping;
T_AUDIO_FORMAT_INFO format_info = {};
p_link = app_link_find_br_link(bd_addr);
...
#if (F_APP_SCO_XMIT_AG_SUPPORT || F_APP_SCO_XMIT_HF_SUPPORT)
app_report_event(CMD_PATH_SPI, CMD_SCO_XMIT_CONFIG, 0, (uint8_t *)&format_info,
sizeof(T_AUDIO_FORMAT_INFO));
...
#endif
...
}
static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
uint8_t active_hf_idx = app_hfp_get_active_idx();
switch (event_type)
{
case BT_EVENT_SCO_CONN_CMPL:
{
...
if (param->sco_conn_cmpl.cause != 0)
{
break;
}
app_audio_sco_conn_cmpl_handle(param->sco_conn_cmpl.bd_addr, param->sco_conn_cmpl.air_mode,
param->sco_conn_cmpl.rx_pkt_len);
...
}
break;
...
}
Device 1 and Device 2 will respectively utilize app_sco_xmit_handle_sco_param()
and
app_sco_xmit_save_output_param()
to save the negotiated data format. After this,
both input and output channels can be marked as Ready by setting sco_ctrl.in_route_ready
.
static void app_sco_xmit_handle_sco_param(uint8_t *param, uint16_t param_len)
{
if (!sco_ctrl.in_route_ready)
{
memcpy(&sco_ctrl.format_info_in, param, param_len);
if (sco_ctrl.format_info_in.type == AUDIO_FORMAT_TYPE_MSBC)
{
APP_PRINT_INFO7("app_sco_xmit_handle_sco_param: type %d, "
"sample_rate %d, allocation_method %d, bitpool %d, block_length %d, chann_mode %d, subband_num %d",
sco_ctrl.format_info_in.type,
sco_ctrl.format_info_in.attr.msbc.sample_rate,
sco_ctrl.format_info_in.attr.msbc.allocation_method,
sco_ctrl.format_info_in.attr.msbc.bitpool,
sco_ctrl.format_info_in.attr.msbc.block_length,
sco_ctrl.format_info_in.attr.msbc.chann_mode,
sco_ctrl.format_info_in.attr.msbc.subband_num);
}
else if (sco_ctrl.format_info_in.type == AUDIO_FORMAT_TYPE_CVSD)
{
APP_PRINT_INFO4("app_sco_xmit_handle_sco_param: type %d, "
"sample_rate %d, chann_num %d, frame_duration %d",
sco_ctrl.format_info_in.type,
sco_ctrl.format_info_in.attr.cvsd.sample_rate,
sco_ctrl.format_info_in.attr.cvsd.chann_num,
sco_ctrl.format_info_in.attr.cvsd.frame_duration);
}
sco_ctrl.in_route_ready = true;
}
#if F_APP_SCO_XMIT_HF_SUPPORT
app_sco_xmit_param_recfg();
#endif
}
void app_sco_xmit_save_output_param(T_AUDIO_FORMAT_INFO *format_info)
{
if (!sco_ctrl.out_route_ready)
{
memcpy(&sco_ctrl.format_info_out, format_info, sizeof(T_AUDIO_FORMAT_INFO));
if (sco_ctrl.format_info_out.type == AUDIO_FORMAT_TYPE_MSBC)
{
APP_PRINT_INFO7("app_sco_xmit_save_output_param: type %d, "
"sample_rate %d, allocation_method %d, bitpool %d, block_length %d, chann_mode %d, subband_num %d",
sco_ctrl.format_info_out.type,
sco_ctrl.format_info_out.attr.msbc.sample_rate,
sco_ctrl.format_info_out.attr.msbc.allocation_method,
sco_ctrl.format_info_out.attr.msbc.bitpool,
sco_ctrl.format_info_out.attr.msbc.block_length,
sco_ctrl.format_info_out.attr.msbc.chann_mode,
sco_ctrl.format_info_out.attr.msbc.subband_num);
}
else if (sco_ctrl.format_info_out.type == AUDIO_FORMAT_TYPE_CVSD)
{
APP_PRINT_INFO4("app_sco_xmit_save_output_param: type %d, "
"sample_rate %d, chann_num %d, frame_duration %d",
sco_ctrl.format_info_out.type,
sco_ctrl.format_info_out.attr.cvsd.sample_rate,
sco_ctrl.format_info_out.attr.cvsd.chann_num,
sco_ctrl.format_info_out.attr.cvsd.frame_duration);
}
}
sco_ctrl.out_route_ready = true;
}
HFP Transmit Data
In the HFP Transparent Transmission scenario, once Device 1 receives SCO data from the mobile while the reference event is BT_EVENT_SCO_DATA_IND
in app_audio_bt_cback()
, it immediately forwards it to Device 2 without creating a local track handle. The SCO data is transmitted via SPI together with the CMD_SCO_XMIT_AUDIO
command. CMD_SCO_XMIT_AUDIO
will be called in app_sco_xmit_handle_cmd_set()
, and SCO data will be sent to headphones via app_sco_xmit_send_sco()
.
static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
uint8_t active_hf_idx = app_hfp_get_active_idx();
switch (event_type)
{
case BT_EVENT_SCO_DATA_IND:
{
...
p_link = app_link_find_br_link(param->sco_data_ind.bd_addr);
if (p_link == NULL)
{
break;
}
p_link->sco.seq_num++;
#if (F_APP_SCO_XMIT_HF_SUPPORT || F_APP_SCO_XMIT_AG_SUPPORT)
app_report_event(CMD_PATH_SPI, CMD_SCO_XMIT_AUDIO, 0, param->sco_data_ind.p_data,
param->sco_data_ind.length);
...
}
static void app_sco_xmit_send_sco(uint8_t *data, uint16_t len)
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(app_db.br_link[active_hf_idx].bd_addr);
if (p_link == NULL)
{
APP_PRINT_ERROR0("app_sco_xmit_send_sco: no br link found");
}
sco_seq_num++;
if (p_link->sco.duplicate_fst_data)
{
p_link->sco.duplicate_fst_data = false;
bt_sco_data_send(p_link->bd_addr, sco_seq_num - 1, data, len);
}
bt_sco_data_send(p_link->bd_addr, sco_seq_num, data, len);
}
void app_sco_xmit_handle_cmd_set(uint8_t app_idx, uint8_t cmd_path, uint8_t *cmd_ptr,
uint16_t cmd_len, uint8_t *ack_pkt)
{
uint16_t cmd_id = (uint16_t)(cmd_ptr[0] | (cmd_ptr[1] << 8));
...
switch (cmd_id)
{
...
case CMD_SCO_XMIT_AUDIO:
{
uint8_t *p_audio = &cmd_ptr[2];
uint16_t audio_len = cmd_len - 2;
if (flag_direct_send)
{
app_sco_xmit_send_sco(p_audio, audio_len);
}
...
}
break;
...
}
}
The specific operation process can refer to ACI Host CLI Test (HFP Transfer).
Bluetooth Audio Transmitter MP3
The path of the playback code is src\sample\bt_audio_trx\music_playback
. The specific operation process can refer to ACI Host CLI Test (Music).
Local Playback
The code of the local playback part is in playback_stream_ctrl.c
. The music start process is as follows.
Receive Data
Device will enter MUSIC_FLOW_JUMP_HEAD
state after receiving the music data transmitted by host in the idle state.
//music flow state
typedef enum
{
MUSIC_FLOW_IDLE = 0x00,
MUSIC_FLOW_JUMP_HEAD = 0x01,
MUSIC_FLOW_CREAT_TRACK = 0x02,
MUSIC_FLOW_START = 0x03,
MUSIC_FLOW_STARTED = 0x04
} T_MUSIC_FLOW_STATE;
static struct
{
T_MUSIC_MODE play_mode;
T_MUSIC_STATE play_state;
uint8_t play_flow;
uint8_t preq_pkts;
uint16_t frame_size;
uint32_t header_len;
bool seamless_start;
bool en_report_play_time;
bool is_short_audio;
} music_info;
uint8_t app_music_start(void)
{
uint8_t res = MUSIC_STATE_ERROR;
APP_PRINT_INFO1("app_music_start: play_flow:%d", music_info.play_flow);
if (music_info.play_flow == MUSIC_FLOW_IDLE)
{
music_info.play_flow = MUSIC_FLOW_JUMP_HEAD;
}
if ((music_info.play_flow == MUSIC_FLOW_JUMP_HEAD) &&
(stream_get_data_size() > STREAM_BUF_CHECK_LEVEL ||
PLAYBACK_AUDIO_FILE_STOPPING == playback_audio_file_get_state()))
{
if (app_music_enter_state(MUSIC_FLOW_JUMP_HEAD))
{
music_info.play_flow = MUSIC_FLOW_CREAT_TRACK;
res = MUSIC_NEXT_STATE_ERROR;
}
}
...
}
Estimate Ring Buffer Water Level
When the file header transmission is completed in app_music_jump_file_header()
, APP will estimate the water level of the play ring buffer according to the current data in app_music_play_get_threshold()
.
After estimating the water level, play_flow
will enter the MUSIC_FLOW_CREAT_TRACK
state.
static bool app_music_jump_file_header(void)
{
bool head_jump_ok = false;
uint8_t head_type = 0xFF;
uint32_t read_len = 0;
uint32_t ring_buf_size = stream_get_data_size();
APP_PRINT_WARN2("app_watch_music_judge_file_header, header len: 0x%x, ring_buf_size: 0x%x",
music_info.header_len, ring_buf_size);
if (ring_buf_size <= 0)
{
return false;
}
if (music_info.header_len == 0)
{
head_type = playback_audio_file_get_header(&music_info.header_len);
switch (head_type)
{
case PLAYBACK_AF_ERR_HEADER:
case PLAYBACK_AF_NOT_HEADER:
ring_buf_size = stream_get_data_size();
stream_remove_data(ring_buf_size, &read_len);
music_info.header_len -= read_len;
head_jump_ok = false;
break;
case PLAYBACK_AF_IS_HEADER:
if (stream_get_data_size() > music_info.header_len)
{
stream_remove_data(music_info.header_len, &read_len);
music_info.header_len -= read_len;
head_jump_ok = true;
}
else if (music_info.header_len > STREAM_BUF_HIGH_LEVEL)
{
ring_buf_size = stream_get_data_size();
stream_remove_data(ring_buf_size, &read_len);
music_info.header_len -= read_len;
head_jump_ok = false;
}
break;
case PLAYBACK_AF_DATA_FRAME:
head_jump_ok = true;
break;
default:
break;
}
}
else
{
ring_buf_size = stream_get_data_size();
if (ring_buf_size >= music_info.header_len)
{
stream_remove_data(music_info.header_len, &read_len);
music_info.header_len -= read_len;
}
else
{
stream_remove_data(ring_buf_size, &read_len);
music_info.header_len -= read_len;
}
if (music_info.header_len == 0)
{
head_jump_ok = true;
}
}
return head_jump_ok;
}
static bool app_music_enter_state(uint8_t state)
{
bool ret_state = false;
switch (state)
{
case MUSIC_FLOW_IDLE:
{
ret_state = true;
}
break;
case MUSIC_FLOW_JUMP_HEAD:
{
ret_state = app_music_jump_file_header();
}
break;
...
}
static uint32_t app_music_play_get_threshold(void)
{
uint32_t start_level = 0;
if (music_info.is_short_audio)
{
start_level = (music_info.preq_pkts) * music_info.frame_size;
}
else
{
uint8_t frame_num_max = STREAM_BUF_HIGH_LEVEL / music_info.frame_size;
if (music_info.frame_size > 2048)
{
start_level = STREAM_BUF_HIGH_LEVEL;
}
else if (music_info.frame_size > 1024)
{
if (frame_num_max > 10)
{
start_level = 10 * music_info.frame_size;
}
else
{
start_level = STREAM_BUF_HIGH_LEVEL;
}
}
else
{
start_level = (music_info.preq_pkts + 8) * music_info.frame_size;
}
}
APP_PRINT_INFO2("app_music_play_get_threshold: 0x%x, is_short_audio: %d", start_level,
music_info.is_short_audio);
return start_level;
}
Start Play Audio Track
After the data is filled in (Referring to playback_audio_file_is_end()
) and reaches the preset water level, create and start the Audio Track playback.
uint8_t app_music_start(void)
{
...
if ((music_info.play_flow == MUSIC_FLOW_CREAT_TRACK) &&
(stream_get_data_size() > STREAM_BUF_CHECK_LEVEL ||
PLAYBACK_AUDIO_FILE_STOPPING == playback_audio_file_get_state()))
{
if (app_music_enter_state(MUSIC_FLOW_CREAT_TRACK))
{
music_info.play_flow = MUSIC_FLOW_START;
res = MUSIC_NEXT_STATE_ERROR;
}
}
if (music_info.play_flow == MUSIC_FLOW_START)
{
if (stream_get_data_size() > app_music_play_get_threshold())
{
if (app_music_enter_state(MUSIC_FLOW_START))
{
res = MUSIC_SUCCESS;
music_info.play_flow = MUSIC_FLOW_STARTED;
app_music_send_player_status(MUSIC_PLAYER_PLAYING);
}
}
}
...
}
static bool app_music_enter_state(uint8_t state)
{
bool ret_state = false;
switch (state)
{
case MUSIC_FLOW_CREAT_TRACK:
{
uint16_t u16_res = 0;
T_PLAYBACK_AF_FORMAT_INFO get_fmt_info;
T_PLAY_SET_INFO set_play_info;
music_info.header_len = 0;
u16_res = audio_fs_decode_before_get_frame(NULL);
if (u16_res != 0)
{
ret_state = false;
break;
}
u16_res = playback_audio_file_get_audio_info(&get_fmt_info);
if (u16_res != 0)
{
ret_state = false;
break;
}
else
{
music_info.frame_size = get_fmt_info.frame_size;
playback_stream_get_music_info(get_fmt_info, &set_play_info);
music_info.preq_pkts = set_play_info.preq_pkts;
ret_state = true;
}
}
break;
case MUSIC_FLOW_START:
{
if (music_info.play_mode == MUSIC_LOCAL_PLAY)
{
playback_stream_ctrl_start();
}
ret_state = true;
}
break;
...
}
music_info.play_flow
will enter MUSIC_FLOW_START
, the API playback_stream_ctrl_start()
in playback_stream_ctrl.c
will be called, which controls the play flow of playback. As a protection, the track that may exist already will be released before the Audio Track creation. When the data is lower than the preset water level, continue to request data playback from the host in playback_stream_put_data()
.
uint8_t playback_stream_ctrl_start(void)
{
uint8_t res = PLAYBACK_SUCCESS;
uint32_t sampling_frequency = 0;
APP_PRINT_TRACE0("playback_stream_ctrl_start ++");
app_dlps_disable(APP_DLPS_ENTER_CHECK_PLAYBACK);
if (playback_track_handle)
{
audio_track_release(playback_track_handle);
playback_track_handle = NULL;
}
if (playback.eq_instance != NULL)
{
eq_release(playback.eq_instance);
playback.eq_instance = NULL;
}
if ((res = playback_stream_parameter_recfg()) != 0)
{
return res;
}
playback.buffer_state = PLAYBACK_BUF_NORMAL;
playback.play_state = PLAYBACK_STATE_PLAY;
playback_stream_volume_set(playback.volume);
if (playback_track_handle != NULL)
{
playback_stream_get_sample_rate(&sampling_frequency);
if ((sampling_frequency == SAMPLE_RATE_44K) || (sampling_frequency == SAMPLE_RATE_48K))
{
app_eq_idx_check_accord_mode();
playback.eq_instance = app_eq_create(EQ_CONTENT_TYPE_AUDIO, EQ_STREAM_TYPE_AUDIO, SPK_SW_EQ,
app_db.spk_eq_mode, app_cfg_nv.eq_idx);
if (playback.eq_instance != NULL)
{
eq_enable(playback.eq_instance);
audio_track_effect_attach(playback_track_handle, playback.eq_instance);
}
}
else
{
APP_PRINT_WARN1("EQ don't support this sample rate: %d", sampling_frequency);
}
audio_track_start(playback_track_handle);
}
return res;
}
//need put data
void playback_stream_put_data(uint8_t pkt_num)
{
uint16_t res = 0;
uint8_t frame_cnt = 0;
uint16_t time_ms = playback.put_data_time_ms;
T_PLAYBACK_FRAME_PKT playback_frame;
static uint16_t s_seq_num = 0;
APP_PRINT_INFO1("playback_stream_put_data pkt_num: %d", pkt_num);
while (frame_cnt < pkt_num)
{
// This maybe AUDIO_EVENT_TRACK_BUFFER_HIGH event
if (playback.buffer_state == PLAYBACK_BUF_HIGH)
{
time_ms = playback.put_data_time_ms * 2;
break;
}
res = playback_audio_file_get_frame(&playback_frame);
if (res != 0)
{
APP_PRINT_ERROR1("playback_stream_put_data ERROR,RES:0x%x", res);
break;
}
uint16_t written_len;
s_seq_num++;
if (audio_track_write(playback_track_handle,
0,// timestamp,
s_seq_num,
AUDIO_STREAM_STATUS_CORRECT,
playback_frame.frame_num,// frame_num,
playback_frame.buf,
playback_frame.length,
&written_len) == false)
{
res = PLAYBACK_AF_WRITE_ERROR;
break;
}
frame_cnt++;
}
stream_check_and_request_data(); // request data from host
playback.buffer_state = PLAYBACK_BUF_NORMAL;
if (res == PLAYBACK_AF_END_ERROR)
{
app_stop_timer(&timer_idx_playback_put_data);
if (frame_cnt == 0)
{
APP_PRINT_WARN0("playback_stream_put_data,file end, and paly next song!!!");
playback_stream_ctrl_stop();
app_music_send_player_status(MUSIC_PLAYER_STOPPED);
}
}
else //if (playback_db.sd_play_state == APP_AUDIO_FS_STATE_PLAY)
{
playback_stream_put_data_start_timer(time_ms);
}
}
A2DP Source Playback
The process of A2DP Source playback is similar to that of local playback. APP will call the a2dp_src_stream_ctrl_start()
when music_info.play_flow
enters the MUSIC_FLOW_START
state.
static struct
{
T_A2DP_SRC_PLAY_STATE play_state;
T_APP_A2DP_SRC_STATE bt_strm_state;
T_A2DP_SRC_BUF_STATE buffer_state;
uint8_t sink_addr[6];
uint8_t frm_num; // check level
} a2d_src_ctrl;
void a2dp_src_stream_ctrl_start(void)
{
APP_PRINT_INFO1("a2dp_src_stream_ctrl_start: bt stream state: %d", a2d_src_ctrl.bt_strm_state);
a2d_src_ctrl.buffer_state = A2DP_SRC_BUF_LOW;
#if A2DP_SRC_STREAM_DBG == 0
if (a2d_src_ctrl.bt_strm_state != APP_A2DP_SRC_STREAM_START)
{
bt_a2dp_stream_start_req(a2d_src_ctrl.sink_addr);
}
else
#endif
{
a2dp_src_stream_param_recfg();
a2d_src_ctrl.play_state = A2DP_SRC_PLAY_STATE_PLAY;
app_dlps_disable(APP_DLPS_ENTER_CHECK_PLAYBACK);
}
}
When sending data to the headset, format conversion is required on the device side. The Audio Pipe will be created in the a2dp_src_stream_param_recfg()
function.
uint8_t a2dp_src_stream_param_recfg(void)
{
uint8_t res = A2DP_SRC_SUCCESS;
uint16_t u16_res = 0;
uint32_t sample_rate = 0;
// uint16_t sample_counts = 1024; /* default */
// uint16_t frame_duration = 20; /* default */
uint16_t frame_size = 512;
uint8_t channel_mode = 0;
// uint32_t bit_rate = 0;
T_PLAYBACK_AF_FORMAT_INFO get_fmt_info;
if (audio_pipe_handle != NULL)
{
res = A2DP_SRC_PIPE_CREATE_ERROR;
return res;
}
u16_res = playback_audio_file_get_audio_info(&get_fmt_info);
if (u16_res == 0)
{
T_AUDIO_FORMAT_INFO format_info;
uint32_t device = AUDIO_DEVICE_OUT_SPK;
format_info = get_fmt_info.format_info;
frame_size = get_fmt_info.frame_size;
a2d_src_ctrl.frm_num = 4;
if (frame_size > 2048)
{
frame_size = 1024;
}
uint8_t frm_num = STREAM_BUF_SIZE / 2 / frame_size;
a2d_src_ctrl.frm_num = (frm_num > 8) ? 7 : 4;
if (format_info.type == AUDIO_FORMAT_TYPE_AAC)
{
APP_PRINT_INFO4("a2dp_src_stream_param_recfg: AAC, "
" transport_format:0x%x, sample_rate:%d, channel_mode:%d, bitrate:%d",
format_info.attr.aac.transport_format,
format_info.attr.aac.sample_rate,
format_info.attr.aac.chann_num,
format_info.attr.aac.bitrate);
}
else if (format_info.type == AUDIO_FORMAT_TYPE_MP3)
{
APP_PRINT_INFO3("a2dp_src_stream_param_recfg: MP3, sample_rate:%d, channel_mode:%d, frm_num:%d",
format_info.attr.mp3.sample_rate,
format_info.attr.mp3.chann_mode,
a2d_src_ctrl.frm_num);
}
T_AUDIO_FORMAT_INFO snk_info;
snk_info.type = AUDIO_FORMAT_TYPE_SBC;
snk_info.attr.sbc.subband_num = 8;
snk_info.attr.sbc.bitpool = a2dp_src_bitpool; //change to be same with min bitpool
snk_info.attr.sbc.sample_rate = 48000;
snk_info.attr.sbc.block_length = 16;
snk_info.attr.sbc.chann_mode = AUDIO_SBC_CHANNEL_MODE_JOINT_STEREO;
snk_info.attr.sbc.allocation_method = 0;
float sbc_block = snk_info.attr.sbc.block_length;
float sbc_subband = snk_info.attr.sbc.subband_num;
a2dp_sbc_time = (sbc_block * sbc_subband) / 48;
if (audio_pipe_handle == NULL)
{
audio_pipe_handle = audio_pipe_create(format_info, snk_info,
a2dp_gain_table[app_cfg_nv.audio_gain_level[cur_pair_idx]],
audio_codec_callback);
}
}
else
{
res = A2DP_SRC_SYS_ERROR;
}
return res;
}
The action of Audio Pipe will be processed mainly in a2dp_src_stream_handle_msg()
:
If the function receives
AUDIO_PIPE_EVENT_CREATED
,audio_pipe_start()
will be called.If the function receives
AUDIO_PIPE_EVENT_STARTED
,a2dp_src_stream_get_data_from_fs()
will be called, and data will be sent to DSP.If the function receives
AUDIO_PIPE_EVENT_DATA_IND
,a2dp_src_stream_data_ind()
will be called.If the function receives
AUDIO_PIPE_EVENT_DATA_FILLED
,a2dp_src_stream_fill_data()
andstream_check_and_request_data()
will be called, which means DSP buffer level low, need more data.If the function receives
AUDIO_PIPE_EVENT_RELEASED
, Audio Pipe will be stopped and ring buffer will be released.If the function receives
AUDIO_A2DP_SRC_EVENT_DATA_SEND
,a2dp_src_stream_send_data()
will be called.
void a2dp_src_stream_handle_msg(T_IO_MSG msg)
{
uint16_t subtype = msg.subtype;
APP_PRINT_INFO1("a2dp_src_stream_handle_msg: subtype: (0x%x)", subtype);
switch (subtype)
{
case AUDIO_PIPE_EVENT_CREATED:
{
uint32_t snk_buf_size = msg.u.param;
APP_PRINT_TRACE1("AUDIO_PIPE_EVENT_CREATED snk_buf_size:0x%x", snk_buf_size);
audio_pipe_start(audio_pipe_handle);
p_snk_data_buf = os_mem_alloc(RAM_TYPE_DSPSHARE, snk_buf_size);
a2d_src_ctrl.play_state = A2DP_SRC_PLAY_STATE_PLAY;
}
break;
case AUDIO_PIPE_EVENT_STARTED: // send first pkt data to dsp
{
uint16_t res_tmp = 0;
res_tmp = a2dp_src_stream_get_data_from_fs();
APP_PRINT_TRACE1("AUDIO_PIPE_EVENT_STARTED res_tmp 0x%x", res_tmp) ;
}
break;
case AUDIO_PIPE_EVENT_DATA_IND: // get encode data to buf_pool
{
if (a2d_src_ctrl.play_state == A2DP_SRC_PLAY_STATE_PLAY)
{
a2dp_src_stream_data_ind();
}
}
break;
case AUDIO_PIPE_EVENT_DATA_FILLED: // dsp buf low, need put data to share memory
{
if (a2d_src_ctrl.play_state == A2DP_SRC_PLAY_STATE_PLAY)
{
a2dp_src_stream_fill_data();
static uint8_t s_check_cnt = 0;
s_check_cnt++;
if (s_check_cnt > 4)
{
s_check_cnt = 0;
stream_check_and_request_data();
}
}
}
break;
case AUDIO_PIPE_EVENT_RELEASED:
{
if (p_snk_data_buf != NULL)
{
free(p_snk_data_buf);
p_snk_data_buf = NULL;
}
if (a2dp_src_stream_ctrl_stop_complete_hook)
{
a2dp_src_stream_ctrl_stop_complete_hook();
}
}
break;
case AUDIO_A2DP_SRC_EVENT_DATA_SEND: // timer msg peek data from buf_pool and send data
{
a2dp_src_stream_send_data();
}
break;
default:
break;
}
}
Bluetooth Audio Integrated Transceiver
A2DP Integrated Transceiver Transmission
In the A2DP Integrated Transceiver Transmission scenario, ACI Device is connected to the Phone to receive A2DP data, which is transmitted to Headset simultaneously. The path of the main code is sdk\src\sample\bt_audio_trx\source_play\app_src_play_a2dp.c
.
Acquisition of A2DP Format
In this scenario, you need to get the specific A2DP formats supported by the Phone and Headset respectively. Define the following structure to store the obtained formats.
typedef struct
{
T_AUDIO_FORMAT_INFO src_a2dp_format;
bool src_a2dp_format_ready;
T_AUDIO_FORMAT_INFO sink_a2dp_format[MAX_BR_LINK_NUM];
bool sink_a2dp_format_ready[MAX_BR_LINK_NUM];
T_SRC_PLAY_A2DP_STATE sink_a2dp_state[MAX_BR_LINK_NUM];
T_MULTI_A2DP_PARAM sink_a2dp_param[MAX_BR_LINK_NUM];
uint8_t num_frame_buf;
uint8_t *p_buf;
T_RING_BUFFER ring_buf;
uint8_t sink_addr[MAX_BR_LINK_NUM][6];
uint8_t src_addr[6];
} T_SRC_PLAY_A2DP;
The A2DP data format of the connected device can be retrieved using the following function. This function is invoked within the app_audio_bt_cback()
after each A2DP connection is established and successfully configured. If the obtained A2DP role (Representing the Device’s role) is BT_A2DP_ROLE_SRC
, then store the format in a2dp_play.sink_a2dp_format
. If the role is BT_A2DP_ROLE_SNK
, store the format in a2dp_play.src_a2dp_format
.
void app_src_play_save_a2dp_format(T_AUDIO_FORMAT_INFO *format_info, uint8_t *bd_addr, uint8_t role)
{
uint8_t link_idx;
link_idx = app_src_play_a2dp_get_connected_idx(bd_addr);
if (role == BT_A2DP_ROLE_SRC)
{
if (!a2dp_play.sink_a2dp_format_ready[link_idx])
{
memcpy(&a2dp_play.sink_a2dp_format[link_idx], format_info, sizeof(T_AUDIO_FORMAT_INFO));
if (a2dp_play.sink_a2dp_format[link_idx].attr.sbc.bitpool == 0)
{
a2dp_play.sink_a2dp_format[link_idx].attr.sbc.bitpool = 0x22;
}
a2dp_play.sink_a2dp_format_ready[link_idx] = true;
}
app_src_play_print_a2dp_format("app_src_play_save_a2dp_snk_format: ",
&a2dp_play.sink_a2dp_format[link_idx]);
}
else if (role == BT_A2DP_ROLE_SNK)
{
if (!a2dp_play.src_a2dp_format_ready)
{
memcpy(&a2dp_play.src_a2dp_format, format_info, sizeof(T_AUDIO_FORMAT_INFO));
if (a2dp_play.src_a2dp_format.attr.sbc.bitpool == 0)
{
a2dp_play.src_a2dp_format.attr.sbc.bitpool = 0x22;
}
a2dp_play.src_a2dp_format_ready = true;
}
app_src_play_print_a2dp_format("app_src_play_save_a2dp_src_format: ",
&a2dp_play.src_a2dp_format);
}
}
static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
bool handle = true;
uint8_t active_a2dp_idx = app_a2dp_get_active_idx();
uint8_t active_hf_idx = app_hfp_get_active_idx();
switch (event_type)
{
case BT_EVENT_A2DP_CONFIG_CMPL:
{
T_AUDIO_FORMAT_INFO format_info = {};
app_audio_save_a2dp_config((uint8_t *)¶m->a2dp_config_cmpl, &format_info);
#if F_SOURCE_PLAY_SUPPORT
app_src_play_save_a2dp_format(&format_info, param->a2dp_config_cmpl.bd_addr,
param->a2dp_config_cmpl.role);
#endif
}
break;
}
}
Transmission of A2DP Data
The start/stop of data transmission is also controlled by the source play commands, which can be referred to Source Play Start/Stop. After output route is started and Phone start to play music, A2DP data can be obtained in BT_EVENT_A2DP_STREAM_DATA_IND
, so the A2DP stream data can also be processed in function app_src_play_pipe_handle_stream_data_in()
invoked in app_audio_bt_cback()
.
bool app_src_play_pipe_handle_stream_data_in(uint8_t *p_data, uint16_t data_len,
uint8_t frame_number)
{
if (flag_direct_send)
{
app_src_play_a2dp_handle_data(p_data, data_len, frame_number);
}
if (a2dp_snk_pipe_play.handle)
{
if (frame_number > 1)
{
a2dp_snk_pipe_play.p_fill_buf = malloc(data_len);
a2dp_snk_pipe_play.fill_len = data_len;
memcpy(a2dp_snk_pipe_play.p_fill_buf, p_data, a2dp_snk_pipe_play.fill_len);
APP_PRINT_TRACE3("app_src_play_pipe_handle_stream_data_in: data_len %d, fill len %d, fill_buf %b",
data_len, a2dp_snk_pipe_play.fill_len,
TRACE_BINARY(data_len, a2dp_snk_pipe_play.p_fill_buf));
if (a2dp_snk_pipe_play.p_fill_buf == NULL)
{
APP_PRINT_ERROR0("app_src_play_pipe_handle_stream_data_in: mem error!!");
return false;
}
if (flag_pipe_get_data_empty)
{
app_src_play_pipe_fill_data();
}
}
else
{
//TODO: ring buffer
}
}
return true;
}
static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
switch (event_type)
{
case BT_EVENT_A2DP_STREAM_DATA_IND:
{
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(param->a2dp_stream_data_ind.bd_addr);
if (p_link == NULL)
{
break;
}
#if F_APP_INTEGRATED_TRANSCEIVER
app_src_play_pipe_handle_stream_data_in(param->a2dp_stream_data_ind.payload,
param->a2dp_stream_data_ind.len, param->a2dp_stream_data_ind.frame_num);
#endif
}
break;
}
}
To improve A2DP transmit quality, function gap_br_vendor_data_rate_set()
in app_bt_policy.c
can be invoked before sending A2DP data to Headset.
static void set_bd_addr(void)
{
...
#if F_APP_INTEGRATED_TRANSCEIVER
gap_br_vendor_data_rate_set(0);
#else
gap_br_vendor_data_rate_set(1);
#endif
...
}
If a2dp_play.src_a2dp_format
and a2dp_play.sink_a2dp_format
is the same, then the flag_direct_send
will be set to 1 and the application will go directly to function app_src_play_a2dp_handle_data()
.
uint16_t app_src_play_a2dp_handle_data(uint8_t *p_data, uint16_t data_len, uint8_t frame_number)
{
uint16_t res = SRC_PLAY_A2DP_SUCCESS;
if (frame_number > 1)
{
res = app_src_play_a2dp_send_data(p_data, data_len, frame_number);
return res;
}
if (ring_buffer_write(&a2dp_play.ring_buf, p_data, data_len))
{
a2dp_play.num_frame_buf += frame_number;
}
else
{
res = SRC_PLAY_A2DP_ERR_RINGBUF;
APP_PRINT_ERROR0("app_src_play_a2dp_handle_data: a2dp_play.ring_buf is full, drop pkt");
}
if (a2dp_play.num_frame_buf == A2DP_PACKET_FRAME_NUM)
{
a2dp_play.num_frame_buf -= A2DP_PACKET_FRAME_NUM;
uint16_t data_len_to_send = data_len * A2DP_PACKET_FRAME_NUM;
uint8_t *p_data_to_send = malloc(data_len_to_send);
if (p_data_to_send)
{
uint32_t actual_len = ring_buffer_read(&a2dp_play.ring_buf, data_len_to_send, p_data_to_send);
APP_PRINT_INFO1("app_src_play_a2dp_handle_data: actual_len %d sent", actual_len);
res = app_src_play_a2dp_send_data(p_data_to_send, data_len_to_send, A2DP_PACKET_FRAME_NUM);
#if F_APP_ATTACH_LOCAL_PLAY_SUPPORT
if (app_src_play_is_local_play_attached())
{
app_src_play_attach_local_play_handle_data(p_data_to_send,
data_len_to_send,
a2dp_seq_num,
A2DP_PACKET_FRAME_NUM,
0);
}
#endif
free(p_data_to_send);
}
else
{
res = SRC_PLAY_A2DP_ERR_RAM;
}
}
return res;
}
HFP Integrated Transceiver Transmission
In the HFP Integrated Transceiver Transmission scenario, ACI Device is connected to the Phone to receive SCO data, which is transmitted to Headset simultaneously. The path of the main code is sdk\src\sample\bt_audio_trx\source_play\app_src_play_hfp.c
.
Acquisition of HFP Format
Similar to Acquisition of A2DP Format, the HFP HF and AG formats supported by Phone and Headset can also be obtained after the SCO connection is successfully established, which is also handled in the app_audio_sco_conn_cmpl_handle()
. The format structure is defined as follows.
static struct
{
T_AUDIO_FORMAT_INFO hfp_hf_format;
bool hfp_hf_format_ready;
uint8_t hf_addr[6];
T_AUDIO_FORMAT_INFO hfp_ag_format;
bool hfp_ag_format_ready;
uint8_t ag_addr[6];
} hfp_play;
After the HFP connection is established, the Device will first save the address of the linked device as hfp_play.hf_addr
and hfp_play.ag_addr
according to the role event BT_EVENT_HFP_AG_CONN_CMPL
and BT_EVENT_HFP_CONN_CMPL
of the connection in app_src_play_hfp_bt_cback()
. The processing function is as follows.
static void app_src_play_hfp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
T_BT_EVENT_PARAM *param = event_buf;
T_APP_BR_LINK *p_link;
bool handle = true;
switch (event_type)
{
case BT_EVENT_HFP_AG_CONN_CMPL:
{
hfp_play.hfp_hf_format_ready = false;
memcpy(hfp_play.hf_addr, param->hfp_ag_conn_cmpl.bd_addr, 6);
}
break;
#if F_APP_INTEGRATED_TRANSCEIVER
case BT_EVENT_HFP_CONN_CMPL:
{
hfp_play.hfp_ag_format_ready = false;
memcpy(hfp_play.ag_addr, param->hfp_conn_cmpl.bd_addr, 6);
}
break;
}
}
The function to get the HFP data format is app_src_play_save_hfp_format
, which will be invoked in app_audio_sco_conn_cmpl_handle()
in the callback function case of BT_EVENT_SCO_CONN_CMPL
, once the SCO connection is established.
void app_src_play_save_hfp_format(T_AUDIO_FORMAT_INFO *format_info, uint8_t *bd_addr)
{
if ((!hfp_play.hfp_hf_format_ready) && (!memcmp(hfp_play.hf_addr, bd_addr, 6)))
{
memcpy(&hfp_play.hfp_hf_format, format_info, sizeof(T_AUDIO_FORMAT_INFO));
hfp_play.hfp_hf_format_ready = true;
app_src_play_print_hfp_format("app_src_play_save_hfp_hf_format: ",
&hfp_play.hfp_hf_format);
}
#if F_APP_INTEGRATED_TRANSCEIVER
else if ((!hfp_play.hfp_ag_format_ready) && (!memcmp(hfp_play.ag_addr, bd_addr, 6)))
{
memcpy(&hfp_play.hfp_ag_format, format_info, sizeof(T_AUDIO_FORMAT_INFO));
hfp_play.hfp_ag_format_ready = true;
app_src_play_print_hfp_format("app_src_play_save_hfp_ag_format: ",
&hfp_play.hfp_ag_format);
}
#endif
}
static void app_audio_sco_conn_cmpl_handle(uint8_t *bd_addr, uint8_t air_mode, uint8_t rx_pkt_len)
{
uint8_t pair_idx_mapping;
T_AUDIO_FORMAT_INFO format_info = {};
T_APP_BR_LINK *p_link;
p_link = app_link_find_br_link(bd_addr);
if (p_link == NULL)
{
return;
}
#if F_SOURCE_PLAY_SUPPORT
app_src_play_save_hfp_format(&format_info, bd_addr);
#endif
}
static void app_audio_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
...
switch (event_type)
{
case BT_EVENT_SCO_CONN_CMPL:
{
...
app_audio_sco_conn_cmpl_handle(param->sco_conn_cmpl.bd_addr, param->sco_conn_cmpl.air_mode,
param->sco_conn_cmpl.rx_pkt_len);
}
break;
}
}
Transmission of SCO Data
To improve call quality, function bt_sco_link_retrans_window_set
can be invoked in BT_EVENT_SCO_CONN_CMPL
in the app_src_play_hfp_bt_cback()
to set the transmit window of lowerstack after the SCO connection is established.
static void app_src_play_hfp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
...
case BT_EVENT_SCO_CONN_CMPL:
{
if (!memcmp(hfp_play.ag_addr, param->sco_conn_cmpl.bd_addr, 6))
{
bt_sco_link_retrans_window_set(hfp_play.ag_addr, 1);
}
else if (!memcmp(hfp_play.hf_addr, param->sco_conn_cmpl.bd_addr, 6))
{
bt_sco_link_retrans_window_set(hfp_play.hf_addr, 1);
}
}
break;
...
}
When the Phone is called, SCO data will be generated simultaneously, and the Device can process the data. By invoking app_src_play_hfp_send_sco()
, SCO data will be sent to the Phone and Headset respectively, enabling two-way communication.
void app_src_play_hfp_send_sco(uint8_t *p_data, uint16_t len, uint8_t bd_addr[6])
{
uint8_t active_hf_idx = app_hfp_get_active_idx();
uint16_t hfp_seq_num;
T_APP_BR_LINK *p_link;
bool sco_tx_result = false;
#if F_APP_INTEGRATED_TRANSCEIVER
p_link = app_link_find_br_link(bd_addr);
#else
p_link = app_link_find_br_link(app_db.br_link[active_hf_idx].bd_addr);
#endif
if (p_link == NULL)
{
APP_PRINT_ERROR0("app_src_play_hfp_send_sco: no br link found");
return;
}
#if F_APP_INTEGRATED_TRANSCEIVER
if (!memcmp(hfp_play.ag_addr, bd_addr, 6))
{
hfp_ag_seq_num++;
hfp_seq_num = hfp_ag_seq_num;
memcpy(bd_addr, hfp_play.hf_addr, 6);
}
else
{
hfp_hf_seq_num++;
hfp_seq_num = hfp_hf_seq_num;
memcpy(bd_addr, hfp_play.ag_addr, 6);
}
#else
hfp_hf_seq_num++;
hfp_seq_num = hfp_hf_seq_num;
memcpy(bd_addr, p_link->bd_addr, 6);
#endif
if (p_link->sco.duplicate_fst_data)
{
p_link->sco.duplicate_fst_data = false;
bt_sco_data_send(bd_addr, hfp_seq_num - 1, p_data, len);
#if CONFIG_REALTEK_APP_BT_AUDIO_TRI_DONGLE
APP_PRINT_ERROR0("app_src_play_hfp_send_sco: duplicate_fst_data");
#endif
}
sco_tx_result = bt_sco_data_send(bd_addr, hfp_seq_num, p_data, len);
if (sco_tx_result == false)
{
APP_PRINT_ERROR0("app_src_play_hfp_send_sco: bt_sco_data_send fail");
}
}
static void app_src_play_hfp_bt_cback(T_BT_EVENT event_type, void *event_buf, uint16_t buf_len)
{
...
case BT_EVENT_SCO_DATA_IND:
{
#if F_APP_INTEGRATED_TRANSCEIVER
if (param->sco_data_ind.status == 0)
{
app_src_play_hfp_send_sco(param->sco_data_ind.p_data, param->sco_data_ind.length,
param->sco_data_ind.bd_addr);
}
}
#endif
...
}
UART DFU
In addition to the OTA module, the Bluetooth Audio Transceiver application also provides UART DFU functionality for device upgrades. This feature allows an External MCU to transfer upgrade packages to the device via UART, without needing to be aware of the format of the upgrade package. It only needs to adhere to the specified interaction commands.
Currently, ACI Host can be used to simulate an External MCU device for implementing UART DFU upgrades.

UART DFU Process
Code Flow on Device Side
The UART DFU feature is disabled by default. To enable the feature, set
F_APP_UART_DFU
to 1 in the app_flags.h
file.
// app_flags.h
#define F_APP_UART_DFU 0
The ACI Host commands are implemented through CMD_UART_DFU
. Different functionalities are achieved
by carrying different opcodes in the CMD_UART_DFU
command, including requesting to start DFU
(UART_DFU_START_REQ
), transferring data (UART_DFU_DATA_IND
), rebooting the device
(UART_DFU_REBOOT
), and interrupting DFU (UART_DFU_ABORT
).
void app_uart_dfu_process(uint8_t cmd_path, uint16_t opcode, uint8_t *p_data, uint16_t len)
{
switch (opcode)
{
case UART_DFU_TEST_EN:
uart_dfu_handle_test_en();
break;
case UART_DFU_START_REQ:
uart_dfu_handle_start_req(cmd_path, p_data, len);
break;
case UART_DFU_DATA_IND:
uart_dfu_handle_data_ind(cmd_path, p_data, len);
break;
case UART_DFU_REBOOT:
uart_dfu_handle_reboot_req();
break;
case UART_DFU_ABORT:
uart_dfu_handle_abort_req();
break;
case UART_DFU_GET_VER:
uart_dfu_handle_get_ver(cmd_path);
break;
default:
break;
}
}
The device communicates its status to ACI Host by sending different events, including whether DFU is
permitted (EVENT_DFU_START_RSP
), requesting data (EVENT_DFU_DATA_REQ
), reporting local version
(EVENT_DFU_LOCAL_VERSION
), and notifying DFU results (EVENT_DFU_RESULT
).
typedef enum
{
...
EVENT_DFU_START_RSP = 0x3160,
EVENT_DFU_DATA_REQ = 0x3161,
EVENT_DFU_RESULT = 0x3162,
EVENT_DFU_LOCAL_VERSION = 0x3163,
...
} T_EVENT_ID;
When ACI Host sends UART_DFU_START_REQ
opcode, it carries the version number of the upgrade
package. The device checks whether its local version is lower than the provided one to determine
whether to accept the upgrade. It then informs ACI Host through the EVENT_DFU_START_RSP
event and
requests the first set of data through the EVENT_DFU_DATA_REQ
event.
static void uart_dfu_handle_start_req(uint8_t cmd_path, uint8_t *data, uint16_t len)
{
uint32_t dfu_version, local_version;
uint32_t next_block_offset;
uint16_t next_block_length;
uint8_t ret = 0;
T_UART_DFU_START_RSP rsp_state = UART_DFU_REJECT;
LE_STREAM_TO_UINT32(dfu_version, data);
local_version = uart_dfu_get_ota_header_ver();
APP_PRINT_INFO3("uart_dfu_handle_start_req: force_update %d, version local 0x%08x, dfu 0x%08x",
force_update, local_version, dfu_version);
if (!force_update && local_version > dfu_version)
{
ret = 1;
goto ERR;
}
if (dfu_mgr.state != UART_DFU_STATE_IDLE)
{
ret = 2;
goto ERR;
}
ota_write_buf = malloc(OTA_WRITE_BUFFER_SIZE);
if (ota_write_buf == NULL)
{
ret = 3;
goto ERR;
}
app_dlps_disable(APP_DLPS_ENTER_CHECK_OTA_TOOLING_PARK);
dfu_mgr.state = UART_DFU_STATE_MERGED_HEADER;
rsp_state = UART_DFU_ACCEPT;
app_report_event(cmd_path, EVENT_DFU_START_RSP, 0, (uint8_t *)&rsp_state, sizeof(rsp_state));
next_block_offset = 0;
next_block_length = OTA_MERGED_FILE_HEAD_SIZE;
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
return;
ERR:
APP_PRINT_INFO1("uart_dfu_handle_start_req: failed %d", -ret);
app_report_event(cmd_path, EVENT_DFU_START_RSP, 0, (uint8_t *)&rsp_state, sizeof(rsp_state));
}
ACI Host retrieves the corresponding data from the file based on the offset and length received in
the EVENT_DFU_DATA_REQ
event, and sends it to the device through the UART_DFU_DATA_IND
opcode.
Subsequent data transfers are facilitated through EVENT_DFU_DATA_REQ
event and UART_DFU_DATA_IND
opcode until the device receives all the data. This process is handled in the
uart_dfu_handle_data_ind()
function. The device then informs ACI Host of the DFU result through
the EVENT_DFU_RESULT
event. ACI Host sends the UART_DFU_REBOOT
opcode to the device to complete
the entire UART DFU process.
static void uart_dfu_handle_data_ind(uint8_t cmd_path, uint8_t *data, uint16_t len)
{
uint32_t next_block_offset = 0;
uint16_t next_block_length = 0;
if (dfu_mgr.cur_expect_block_len != len)
{
...
}
if (dfu_mgr.state == UART_DFU_STATE_MERGED_HEADER) //handle merged header
{
...
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
dfu_mgr.state = UART_DFU_STATE_SUB_FILE_HEADER;
}
else if (dfu_mgr.state == UART_DFU_STATE_SUB_FILE_HEADER) //handle sub_file headers
{
...
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
dfu_mgr.state = UART_DFU_STATE_CONTROL_HEADER;
}
else if (dfu_mgr.state == UART_DFU_STATE_CONTROL_HEADER) // READ handle control header
{
...
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
dfu_mgr.state = UART_DFU_STATE_IMAGE_PAYLOAD;
}
else if (dfu_mgr.state == UART_DFU_STATE_IMAGE_PAYLOAD)
{
...
if ((dfu_mgr.buf_index == OTA_WRITE_BUFFER_SIZE) || // Every 4k write once
(dfu_mgr.cur_sub_image_relative_offset ==
dfu_mgr.sub_bin.sub_image_header[dfu_mgr.cur_sub_image_index].size)) // Or last packet
{
...
//last pkt of one sub image, check image
if (dfu_mgr.cur_sub_image_relative_offset
==
dfu_mgr.sub_bin.sub_image_header[dfu_mgr.cur_sub_image_index].size)
{
...
if (check_image)
{
//update cur_sub_image_relative_offset
dfu_mgr.cur_sub_image_relative_offset = 0;
dfu_mgr.cur_sub_image_index++;
//if it is last sub image, complete check
if (dfu_mgr.cur_sub_image_index == dfu_mgr.end_sub_image_index)
{
//OTA complete
...
uart_dfu_update_complete_check();
T_UART_DFU_RESULT duf_result = UART_DFU_SUCCESS;
app_report_event(CMD_PATH_UART, EVENT_DFU_RESULT, 0, (uint8_t *)&duf_result, sizeof(duf_result));
return;
}
else
{
//request Nth sub image
...
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
return;
}
}
else
{
...
}
}
}
//prepare next block
...
dfu_mgr.state = UART_DFU_STATE_IMAGE_PAYLOAD;
uart_dfu_req_data(cmd_path, next_block_offset, next_block_length);
APP_PRINT_INFO1("uart_dfu_handle: cur relative offset 0x%x", dfu_mgr.cur_sub_image_relative_offset);
}
}
During the UART DFU process, ACI Host can send the UART_DFU_ABORT
opcode to abort the DFU process.
If certain anomalies occur at the device, it will inform ACI Host through the EVENT_DFU_RESULT
event, carrying the UART_DFU_FAIL
status, and subsequently exit the DFU process.
static void uart_dfu_restore(void)
{
memset(&dfu_mgr, 0, sizeof(T_UART_DFU_MGR));
if (ota_write_buf != NULL)
{
free(ota_write_buf);
ota_write_buf = NULL;
}
app_dlps_enable(APP_DLPS_ENTER_CHECK_OTA_TOOLING_PARK);
}
Integration for External MCU
Command Format
UART DFU is based on the command/event interface ACI Data Formats. The External MCU sends commands to the device, and the device sends events to the MCU, reporting its status or requesting data.
The UART packet format is illustrated in the figure below. This chapter focuses on the specific format of UART DFU Command
and UART DFU Event
. Please refer to ACI Data Formats for an explanation of the packet header and packet tail.

UART DFU Packet Format
External MCU should send the command opcode CMD_UART_DFU
with Sub-opcode to achieve specific functions, including requesting to start DFU, sending DFU data, aborting DFU, and rebooting the device.
Opcode: CMD_UART_DFU (0x3270)
Sub-opcode:
UART_DFU_START_REQ = 0x0000,
UART_DFU_DATA_IND = 0x0001,
UART_DFU_ABORT = 0x0002,
UART_DFU_REBOOT = 0x0003,
UART_DFU_TEST_EN = 0x0004,
UART_DFU_GET_VER = 0x0005,
The device supports the following events to report its own status or request data.
EVENT_DFU_START_RSP = 0x3160,
EVENT_DFU_DATA_REQ = 0x3161,
EVENT_DFU_RESULT = 0x3162,
EVENT_DFU_LOCAL_VERSION = 0x3163,
-
Sub-opcode:
UART_DFU_START_REQ
.Direction: External MCU -> Device
Function: Request to start UART DFU.
Format table.
Request to Start UART DFU Byte 0~1
Byte 2~3
Byte 4~7
0x3270
0x0000
dfu_version
Byte 4~7: Version of the upgrade package, for example, 1.2.3.4 would be represented as
0x01020304
. -
Sub-opcode:
UART_DFU_DATA_IND
.Direction: External MCU -> Device
Function: Send DFU data to device.
Format table.
Send DFU Data to Device Byte 0~1
Byte 2~3
Byte 4~N
0x3270
0x0001
Data
Byte 4~N: When a
EVENT_DFU_DATA_REQ
event is received from the device, carryingoffset
andlength
parameters, the External MCU extracts data of the specified length from the upgrade file, starting at the specified offset, and fills the data to this field. -
Sub-opcode:
UART_DFU_ABORT
.Direction: External MCU -> Device
Function: The External MCU terminates the DFU process.
Format table.
Terminate DFU Process Byte 0~1
Byte 2~3
0x3270
0x0002
-
Sub-opcode:
UART_DFU_REBOOT
.Direction: External MCU -> Device
Function: The External MCU needs to instruct the device to reboot after a successful upgrade.
Format table.
DFU Reboot Byte 0~1
Byte 2~3
0x3270
0x0003
-
Sub-opcode:
UART_DFU_TEST_EN
.Direction: External MCU -> Device
Function: If the External MCU wants to downgrade the device, it should send this command before
UART_DFU_START_REQ
to enable test mode.Format table.
Enable Test Mode Byte 0~1
Byte 2~3
0x3270
0x0004
-
Sub-opcode:
UART_DFU_GET_VER
.Direction: External MCU -> Device
Function: Obtain the version number of the OTA header image of the device. This is commonly utilized to allow the External MCU to request the device’s version, enabling the MCU to decide whether to initiate an upgrade request.
Format table.
Request Version Byte 0~1
Byte 2~3
0x3270
0x0005
-
Event:
EVENT_DFU_START_RSP
.Direction: Device -> External MCU
Function: When the device receives the
UART_DFU_START_REQ
command, it compares the current device version with the upgrade package version. Only if the upgrade package version is higher than the device version will it allow the upgrade, responding withUART_DFU_ACCEPT (0x01)
, otherwise, it replies withUART_DFU_REJECT (0x00)
.Format table.
Start Response Byte 0~1
Byte 2
0x3160
Response State
Byte 2:
UART_DFU_ACCEPT (0x01)
orUART_DFU_REJECT (0x00)
-
Event:
EVENT_DFU_DATA_REQ
.Direction: Device -> External MCU
Function: Used when the device accepts the upgrade and actively requests subsequent data packets.
Format table.
Data Request Byte 0~1
Byte 2~5
Byte 6~7
0x3161
Offset
Length
Byte 2~5: Data offset in the upgrade file.
Byte 6~7: Data length.
-
Event:
EVENT_DFU_RESULT
.Direction: Device -> External MCU
Function: The device reports the upgrade result. In case of errors during the upgrade process, the device reports the
UART_DFU_FAIL
status. If the upgrade is completed successfully, the device reports theUART_DFU_SUCCESS
status.Format table.
DFU Result Byte 0~1
Byte 2
0x3162
Status
Byte 2:
UART_DFU_SUCCESS (0x01)
orUART_DFU_FAIL (0x00)
-
Event:
EVENT_DFU_LOCAL_VERSION
.Direction: Device -> External MCU
Function: Report device’s local version if
UART_DFU_GET_VER
received.Format table.
Report Device Version Byte 0~1
Byte 2~5
0x3163
Version
Byte 2~5: Device’s local version, represented as 0x01020304.
Interaction Flow

UART DFU with External MCU
DFU normal mode is employed for device upgrades, while DFU test mode is utilized for device downgrades, typically during the development and debugging phases. In test mode, the External MCU is required to send the UART_DFU_TEST_EN
command before sending the UART_DFU_START_REQ
. This ensures that upon receiving the UART_DFU_START_REQ
command, the device skips version comparison and starts up from a lower version bank after the DFU process is complete.
Exception Handling
DFU Fail
On the device side, if any exception occurs, it will report EVENT_DFU_RESULT
event with parameter
UART_DFU_FAIL (0x00)
, and autonomously recover its state. In this case, the External MCU needs to
restore its own state to ensure it can restart the DFU process successfully next time.
Unexpected Offset and Length
Upon receiving the EVENT_DFU_DATA_REQ
event, the External MCU needs to check whether it can
retrieve the corresponding data from the file based on the offset and length. If the offset and
length exceed the file size, the External MCU should send the UART_DFU_ABORT
command to terminate
the DFU process.
Acoustics MP
Acoustics MP of Bluetooth Audio Transceiver application mainly includes HW EQ Fitting. This feature is used for audio component calibration, including SPK and MIC devices. It also supports calibration via various scenarios, such as playback for SPK response, voice, and record for MIC gain.
Flow on Device Side
The Acoustics MP feature is enabled by default. Two flags are used in the app_flags.h
file:
F_APP_SAIYAN_EQ_FITTING
enables HW EQ Fitting via playback and voice.F_APP_SUPPORT_CAPTURE_ACOUSTICS_MP
enables HW EQ Fitting via record (SPP Capture).
#define F_APP_SAIYAN_EQ_FITTING 1
#define F_APP_SUPPORT_CAPTURE_ACOUSTICS_MP 1
Before calibration, SPP connection should be established for interaction between the device and MP Tool.
The specific commands sent by MP Tool will be processed in app_eq_fitting_cmd_handle()
or app_data_capture_cmd_handle()
on the device’s side.
// HW EQ command
case CMD_HW_EQ_TEST_MODE: // 0x1200
case CMD_HW_EQ_START_SET: // 0x1201
case CMD_HW_EQ_CONTINUE_SET: // 0x1202
case CMD_HW_EQ_CLEAR_CALIBRATE_FLAG: // 0x1204
case CMD_HW_EQ_SET_TEST_MODE_TMP_EQ: // 0x1205
case CMD_HW_EQ_SET_TEST_MODE_TMP_EQ_ADVANCED: // 0x1206
{
app_eq_fitting_cmd_handle(cmd_ptr, cmd_len, cmd_path, app_idx, ack_pkt);
}
break;
// SPP Capture command
case CMD_DSP_CAPTURE_V2_START_STOP: // 0x0220
{
app_data_capture_cmd_handle(cmd_ptr, cmd_len, cmd_path, app_idx, ack_pkt);
}
break;
And after processing the command, the device will reply with the EVENT_ACK
and the other corresponding event to the tool by app_report_event()
.
// EVENT_ACK
EVENT_ACK = 0x0000,
// HW EQ event
EVENT_HW_EQ_TEST_MODE = 0x1200,
EVENT_HW_EQ_START_SET = 0x1201,
EVENT_HW_EQ_CONTINUE_SET = 0x1202,
EVENT_HW_EQ_CLEAR_CALIBRATE_FLAG = 0x1204,
EVENT_HW_EQ_SET_TEST_MODE_TMP_EQ = 0x1205,
EVENT_HW_EQ_SET_TEST_MODE_TMP_EQ_ADVANCED = 0x1206,
// SPP Capture event
EVENT_DSP_CAPTURE_V2_START_STOP_RESULT = 0x0220,
EVENT_DSP_CAPTURE_V2_DATA = 0x0221,
Note
It should be noted that EVENT_DSP_CAPTURE_V2_DATA
does not actually have a corresponding command. It is actually capture data.
A complete Acoustics MP flow should include the following steps:
Enter HW EQ test mode.
Measure and calculate.
Apply temp EQ.
Download to flash.
Exit HW EQ test mode.
Check after download.
It can also be referred to the figure below, and among these steps, the more important are: Apply temp EQ, Download to flash, Check after download.

Acoustics MP Flow
Enter or Exit HW EQ Test Mode
The device uses CMD_HW_EQ_TEST_MODE (0x1200)
to enter or exit HW EQ test mode, which can be distinguished by different parameters in the command. When entering HW EQ test mode, the is_test_mode
flag will be set to true. Otherwise, this flag will be set to false.
case CMD_HW_EQ_TEST_MODE:
{
uint8_t test_mode_status = cmd_ptr[2];
if (test_mode_status == HW_EQ_ENTER_TEST_MODE) // Enter HW EQ Test Mode
{
is_test_mode = true;
...
}
else if (test_mode_status == HW_EQ_EXIT_TEST_MODE) // Exit HW EQ Test Mode
{
is_test_mode = false;
...
}
else
{
ack_pkt[2] = CMD_SET_STATUS_PARAMETER_ERROR;
}
app_report_event(cmd_path, EVENT_ACK, app_idx, ack_pkt, 3);
if (ack_pkt[2] == CMD_SET_STATUS_COMPLETE)
{
app_report_event(cmd_path, EVENT_HW_EQ_TEST_MODE, app_idx, &ack_pkt[2], sizeof(uint8_t));
}
}
break;
Measure and Calculate
Through measurement, the compensation value can be calculated by fitting algorithm for the subsequent apply process. Different sample rates have different parameters. For example, when using a 48k sampling rate to measure the SPK response, three sets of EQ filter parameters with different sample rates will be generated.
Apply Temp EQ
Then the device can use CMD_HW_EQ_SET_TEST_MODE_TMP_EQ_ADVANCED (0x1206)
to apply temp EQ parameters. This command is used to assist Acoustics MP and verify the calibration effect.
When the device is in HW EQ test mode, the tool can send temporary HW EQ parameters to the device. The device opens memory to temporarily store this parameter. Once an audio_track
that meets the conditions (Device, sample rate) is established, the device applies the HW EQ parameters; otherwise, it will take a long time to test if each sample rate per scenario follows.
When the device exits HW EQ test mode, the device will release the temp EQ parameters.
The device uses app_eq_fitting_set_test_mode_tmp_eq_advanced
to handle the details. There will be two situations. One is to take effect in real time. That is, when receiving the temp EQ parameters, audio_track
is already in the STARTED
state by app_eq_fitting_is_streaming
.
app_eq_fitting_cmd_handle()
|---app_eq_fitting_set_test_mode_tmp_eq_advanced()
static void app_eq_fitting_set_test_mode_tmp_eq_advanced(void)
{
if (test_mode_tmp_eq_advanced)
{
...
if (eq_para->device_type == HW_EQ_DEVICE_PRIMARY_SPEAKER) //pri spk
{
eq_type = CODEC_EQ_CONFIG_PATH_DAC;
eq_channel = 0; // DAC0
}
else if (eq_para->device_type == HW_EQ_DEVICE_PRIMARY_MIC) // pri mic
{
eq_type = CODEC_EQ_CONFIG_PATH_ADC;
eq_channel = 0; // ADC0
}
else if (eq_para->device_type == HW_EQ_DEVICE_SECONDARY_MIC) // sec mic
{
eq_type = CODEC_EQ_CONFIG_PATH_ADC;
eq_channel = 1; // ADC1
}
if (app_eq_fitting_is_streaming(AUDIO_STREAM_TYPE_PLAYBACK)) // playback scenario
{
sample_rate = app_eq_fitting_get_sample_rate(AUDIO_CATEGORY_AUDIO); // 44.1k or 48k
if (sample_rate == eq_para->sample_rate) // sample rate match
{
audio_probe_codec_hw_eq_set(eq_type, eq_channel, eq_para->para, para_len); // HW EQ set
}
}
else if (app_eq_fitting_is_streaming(AUDIO_STREAM_TYPE_VOICE)) // voice scenario
{
sample_rate = app_eq_fitting_get_sample_rate(AUDIO_CATEGORY_VOICE); // 16k
if (sample_rate == eq_para->sample_rate) // sample rate match
{
audio_probe_codec_hw_eq_set(eq_type, eq_channel, eq_para->para, para_len); // HW EQ set
}
}
else if (app_eq_fitting_is_streaming(AUDIO_STREAM_TYPE_RECORD)) // record scenario
{
sample_rate = app_eq_fitting_get_sample_rate(AUDIO_CATEGORY_RECORD); // 48k
if (sample_rate == eq_para->sample_rate) // sample rate match
{
audio_probe_codec_hw_eq_set(eq_type, eq_channel, eq_para->para, para_len); // HW EQ set
}
}
}
}
The other situation is that the EQ parameters will be temporarily stored and then applied when audio_track
is established.
static void app_eq_fitting_cback(T_AUDIO_EVENT event_type, void *event_buf, uint16_t buf_len)
{
...
switch (event_type)
{
case AUDIO_EVENT_TRACK_STATE_CHANGED:
{
...
if (is_test_mode) // in HW EQ Test Mode
{
...
app_eq_fitting_set_test_mode_tmp_eq_advanced();
}
...
}
}
...
}
Playback or Voice Scenario
If we want to apply EQ parameters during playback or voice scenario, we can trigger A2DP or HFP play to create the related audio_track
.
Record Scenario
If we want to apply EQ parameters in a record scenario, we can trigger capture start to create the record track. The device uses CMD_DSP_CAPTURE_V2_START_STOP (0x0220)
to start and stop capture.
// start capture
app_data_capture_cmd_handle()
|---app_data_capture_recorder_create()
|---audio_track_create()
// stop capture
app_data_capture_cmd_handle()
|---app_data_capture_stop_process
|---audio_track_release()
// capture data
app_data_capture_cmd_handle()
|---app_data_capture_register()
|---audio_probe_dsp_evt_cback_register(app_data_capture_dsp_event_cback);
void app_data_capture_dsp_event_cback(uint32_t event, void *msg)
{
switch (event)
{
case AUDIO_PROBE_DSP_EVT_MAILBOX_DSP_DATA:
{
...
app_report_event(dsp_capture_data_path, EVENT_DSP_CAPTURE_V2_DATA, dsp_capture_data_app_idx,
p_info->p_data, p_info->data_len); // EVENT_DSP_CAPTURE_V2_DATA
}
break;
...
}
}
Download to Flash
After the apply process is verified, the device can download MP EQ Information
to flash by using CMD_HW_EQ_START_SET (0x1201)
and CMD_HW_EQ_CONTINUE_SET (0x1202)
. The first 1KB of flash is used to store HW EQ Fitting data currently.
app_eq_fitting_cmd_handle()
|---app_eq_fitting_write_hw_eq()
static uint8_t app_eq_fitting_write_hw_eq(uint8_t *data, uint32_t len)
{
uint8_t failed_cause = HW_EQ_WRITE_SUCCESS;
bool resume_bp_lv = false;
uint8_t old_bp_lv;
uint8_t *flash_tem_buf = NULL;
is_disallow_playback = true;
if (!app_eq_fitting_is_media_buffer_idle())
{
failed_cause = HW_EQ_WRITE_WRONG_TRACK_STATE;
goto exit;
}
/* need 4K for temporary storage, take it from heap */
flash_tem_buf = (uint8_t *)malloc(FLASH_NOR_SECTOR_SIZE); // 0x1000
if (!flash_tem_buf)
{
failed_cause = HW_EQ_WRITE_MALLOC_FAIL;
goto exit;
}
if (!fmc_flash_nor_get_bp_lv(EQ_FITTING_ADDR, &old_bp_lv))
{
failed_cause = HW_EQ_WRITE_GET_BP_LV_FAIL;
goto exit;
}
if (fmc_flash_nor_set_bp_lv(EQ_FITTING_ADDR, 0))
{
resume_bp_lv = true;
}
else
{
failed_cause = HW_EQ_WRITE_SET_BP_LV_FAIL;
goto exit;
}
if (fmc_flash_nor_read(EQ_FITTING_ADDR, flash_tem_buf, FLASH_NOR_SECTOR_SIZE))
{
if (len <= EQ_FITTING_SIZE)
{
memcpy(flash_tem_buf, data, len);
}
else
{
failed_cause = HW_EQ_WRITE_WRONG_LEN;
goto exit;
}
}
else
{
failed_cause = HW_EQ_WRITE_READ_FAIL;
goto exit;
}
if (!fmc_flash_nor_erase(EQ_FITTING_ADDR, FMC_FLASH_NOR_ERASE_SECTOR))
{
failed_cause = HW_EQ_WRITE_ERASE_FAIL;
goto exit;
}
if (!fmc_flash_nor_write(EQ_FITTING_ADDR, flash_tem_buf, FLASH_NOR_SECTOR_SIZE))
{
failed_cause = HW_EQ_WRITE_FAIL;
goto exit;
}
exit:
if (resume_bp_lv)
{
fmc_flash_nor_set_bp_lv(EQ_FITTING_ADDR, old_bp_lv);
}
if (flash_tem_buf)
{
free(flash_tem_buf);
}
if (failed_cause != HW_EQ_WRITE_WRONG_TRACK_STATE)
{
is_disallow_playback = false;
}
APP_PRINT_ERROR1("app_eq_fitting_write_hw_eq: cause %d", failed_cause);
return failed_cause;
}
// flash_map.h
#define EQ_FITTING_ADDR 0x02000000
#define EQ_FITTING_SIZE 0x00000400 //1K Bytes
Check after Downloading
Finally, the device exits the MP test mode and returns to user mode. It is possible to trigger A2DP play, HFP play, or capture start again to check the parameters downloaded to flash before. It is expected to use the one downloaded to flash before.
static void app_eq_fitting_cback(T_AUDIO_EVENT event_type, void *event_buf, uint16_t buf_len)
{
...
switch (event_type)
{
case AUDIO_EVENT_TRACK_STATE_CHANGED:
{
...
if (stream_type == AUDIO_STREAM_TYPE_PLAYBACK)
{
app_eq_fitting_send_hw_eq(AUDIO_CATEGORY_AUDIO);
}
else if (stream_type == AUDIO_STREAM_TYPE_VOICE)
{
app_eq_fitting_send_hw_eq(AUDIO_CATEGORY_VOICE);
}
else if (stream_type == AUDIO_STREAM_TYPE_RECORD)
{
app_eq_fitting_send_hw_eq(AUDIO_CATEGORY_RECORD);
}
...
}
}
...
}
static bool app_eq_fitting_send_hw_eq(T_AUDIO_CATEGORY category)
{
...
for (i = 0; i < pysical_path_group.physical_path_num; i++)
{
...
if (eq_config_path != CODEC_EQ_CONFIG_PATH_MAX &&
(io_idx < sizeof(hw_eq_info) / sizeof(T_HW_EQ_INFO *)))
{
T_HW_EQ_INFO *tmp = hw_eq_info[io_idx];
while (tmp)
{
if (tmp->sample_rate == sample_rate) // sample rate match
{
T_HW_EQ_PARA *buf = malloc(tmp->len);
if (buf)
{
if (app_eq_fitting_read_hw_eq(sizeof(T_HW_EQ_DATA) + tmp->offset, (uint8_t *)buf, tmp->len)) // read HW EQ from flash
{
audio_probe_codec_hw_eq_set(eq_config_path, channel, (uint8_t *)buf->para,
buf->stage_number * EQ_STAGE_LEN); // HW EQ set
}
free(buf);
}
break;
}
tmp = tmp->next;
}
}
}
...
return true;
}
Command and Event
Enter or Exit MP EQ Test Mode
-
CMD_HW_EQ_TEST_MODE
Direction: Tool -> Device
Function: Request to enter or exit HW EQ test mode.
Format table.
HW EQ Test Mode Command Format Byte 0~1
Byte 2
0x1200
Test Mode Status
Byte 2:
HW_EQ_EXIT_TEST_MODE (0x00)
orHW_EQ_ENTER_TEST_MODE (0x01)
. -
EVENT_HW_EQ_TEST_MODE
Direction: Device -> Tool
Function: The device reports the CMD process result to the tool.
Format table.
HW EQ Test Mode Event Format Byte 0~1
Byte 2
0x1200
Status
Byte 2: Status.
0x00
means success, and0x03
means parameter error.
Start Set MP EQ Info
-
CMD_HW_EQ_START_SET
Direction: Tool -> Device
Function: Request to start set MP EQ info.
Format table.
HW EQ Start Set Command Format Byte 0~1
Byte 2~3
Byte 4~5
0x1201
EQ_totallen
CRC
Byte 2~3: Total length of all EQ parameters. Byte 4~5: CRC16 of all EQ parameters. The device does check after receiving the complete EQ parameters.
-
EVENT_HW_EQ_START_SET
Direction: Device -> Tool
Function: The device reports the CMD process result and
Max_packet_size
to the tool.Format table.
HW EQ Start Set Event Format Byte 0~1
Byte 2
Byte 3~4
0x1201
Status
Max_packet_size
Byte 2: Status.
0x00
means success, and0x05
means process fail. Byte 3~4:Max_packet_size
. The current setting is 500 bytes, which isMAX_EQ_TOOL_CMD_LENGTH
in macro definition.
Continue Set MP EQ Info
-
CMD_HW_EQ_CONTINUE_SET
Direction: Tool -> Device
Function: Request to continue set MP EQ info.
Format table.
MP EQ Command Format Byte 0~1
Byte 2
Byte 3
Byte 4~N
0x1202
num_of_total_packet
index_packet
Data
Byte 2: Total packet number. Byte 3: Current package index. Valid index should be 0, 1, …,
number_of_total_packet
1. Byte 4~N:HW_EQ_DATA
. That is, the followingT_HW_EQ_DATA
.typedef struct __attribute__((__packed__)) t_hw_eq_para { uint8_t device_type; uint8_t rsv; uint32_t sample_rate; uint8_t stage_number; uint8_t para[0]; /* para len is stage_number * 20 bytes */ } T_HW_EQ_PARA; typedef struct __attribute__((__packed__)) t_hw_eq_data { uint32_t magic_word; uint8_t calibrated; uint16_t total_len; /* total len of all groups of */ uint16_t crc16; /* CRC calc range: all groups of eq para */ T_HW_EQ_PARA eq_para[0]; // sereral Groups of T_HW_EQ_PARA } T_HW_EQ_DATA;
-
EVENT_HW_EQ_CONTINUE_SET
Direction: Device -> Tool
Function: The device reports the CMD process result to the tool.
Format table.
MP EQ Event Format Byte 0~1
Byte 2
0x1202
Status
Byte 2: Status.
0x00
means success,0x03
means parameter error, and0x05
means process fail.
Set Test Mode Temp EQ Advanced
-
CMD_HW_EQ_SET_TEST_MODE_TMP_EQ_ADVANCED
Direction: Tool -> Device
Function: Request to set temp MP EQ info.
Format table.
EQ Advanced Command Format Byte 0~1
Byte 2
Byte 3~N
0x1206
eq_num
Data
Byte 2: Number of EQ.
0x00
indicates clear temp EQ parameter,0x01
~0x03
are all valid number of EQ parameters and others are RFU. Byte 3~N:T_HW_EQ_DATA
. -
EVENT_HW_EQ_SET_TEST_MODE_TMP_EQ_ADVANCED
Direction: Device -> Tool
Function: The device reports the CMD process result to the tool.
Format table.
EQ Advanced Event Format Byte 0~1
Byte 2
0x1206
Status
Byte 2: Status.
0x00
means success and0x05
means process fail.
Find My
Find My is an application technology released by Apple. The magic of this technology is that peripheral products that support it (Such as AirTag) can use nearby Apple devices (iPhone, iPad, AirPods, AirTag, etc.) to help locate themselves even if they do not have a GPS module. For more detailed information, please refer to the official Apple website at Find My.
This document outlines how to enable the Find My feature, provides a software overview of all Find My features, and guides users on getting started with running the Find My application.
Realtek Find My
Realtek Find My SDK is based on Find My Network ADK v1.0 and is compatible with the latest version of the Find My Network Accessory Specification.
Find My APP
To use Find My features, please utilize the Find My APP on iOS devices. The Find My APP is where Apple devices are located, locations are shared with friends and family, and Find My network-enabled accessories are located. The APP displays the location of findable items and includes additional features to protect devices, such as playing sound and using Lost Mode.
Transport
The Find My network accessory protocol uses Bluetooth LE as the primary transport to interact with Apple devices.
Roles
It includes the following four roles: Owner device, Accessory, Find My network, and Apple server. The relationship between these four roles is shown in the following figure:

Four Roles
Owner device
When an accessory is paired with an Apple device through the Find My APP, the accessory is associated with the Apple ID on that device. This device and all other Apple devices signed in with the same Apple ID are treated as owner devices.
Accessory
An accessory is a device that implements the Find My network accessory protocol and can be located using the Apple Find My network and servers. In the following, the accessory refers to the Realtek Bluetooth LE SOC that supports the Find My feature.
Find My Network
The Find My network provides a mechanism to locate accessories by using the vast network of Apple devices that have Find My enabled.
Apple server
The Apple server receives encrypted location data from Finder devices and temporarily stores it.
Feature Enable
Our SDK supports Find My. To enable Find My, simply follow the steps below.
Add Find My File
Copy the Find My related files provided in 3rd_service_findmy
to SDK.

Find My File Path
Copy the mbedtls_config_findmy.h
file from the src\sample\bt_audio_trx\findmy
directory to the src\sample\mbedtls\include\mbedtls
directory, and replace the mbedtls_config.h
file in that directory with this file. Finally, compile the mbedtls project.

Mbedtls Configuration Path
Find My Macro Configuration
Enable the F_APP_FINDMY_FEATURE_SUPPORT
macro for Find My in the app_flags.h
file.
#define F_APP_FINDMY_FEATURE_SUPPORT 1
Global Size Configuration
Because Find My occupies more Global RAM size, please modify the value of APP_GLOBAL_SIZE
in the mem_config.h
file to match the actual RAM_GLOBAL
size.
#define APP_GLOBAL_SIZE (24*1024)
Configuration of MCUConfig Tool
Since Elliptic Curve Cryptography takes more than 4 seconds, the idle task cannot be executed, thus triggering a watchdog reset. Therefore, the watchdog timeout is recommended to be configured to 8 seconds by clicking
.
MCUConfigTool WDG
Software Authentication Token
Licensees must download a provisioned software authentication token and UUID into Realtek nonvolatile memory (Flash) so that the Find My network server can validate the pairing. Per the FMNA specification, the software authentication token is only valid per pairing session. It is necessary to write the initial software authentication token into the accessory flash. Subsequent tokens will be updated in flash whenever new tokens are received from iOS during pairing sessions. These updated tokens will remain in the flash memory even after the system reboots.
For more detailed information, please refer to Software Token Authentication Server Specification.
Note
When reprogramming the Find My images after pairing, ensure that the Erase All for Download and User Data
option is not selected. Choosing this option will result in the latest token being erased or overwritten, preventing successful pairing in the future.

No Erase Token
Download the token:
To generate the
token.bin
for downloading, openTOKEN_DECODE.exe
located in thesdk\tool\token_decode
path. Then, input the base64 encoded token string and the corresponding UUID released by Apple.Copy the
token.bin
andmp_bkupdata2.ini
files to the path ofsdk\tool\Gadgets
.-
Open a command window, input following command.
prepend_header.exe /backup_data2 token.bin /mp_ini mp_bkupdata2.ini /ic_type IC_TYPE
md5.exe token_MP.bin
For RTL87x3E, use
/ic_type 87x3E
. For RTL87x3EP, use/ic_type 8773E
. For RTL87x3D, use/ic_type 87x3D
. -
Using the MPPG Tool, download the
token_MP-XX.bin
file. The written address is defined in the flash map. Please refer to the following figure.Configure Flash Token
Bluetooth Requirements
Bluetooth Low Energy is used as the wireless transport for all communication between Apple products and accessories.
Bluetooth Connection
The accessory must support at least two simultaneous connections in a Peripheral role.
#define APP_FINDMY_MAX_LINKS 2 /** FIND MY LE link number */
Accessory Information Service
The Accessory information service UUID is shown as follows.
#define AIS_SERVICE_BASE_UUID {0x8B, 0x47, 0x38, 0xDC, 0xB9, 0x11, 0xA9, 0xA1, 0xB1, 0x43, 0x51, 0x3C, 0x02, 0x01, 0x29, 0x87}
The UUID for Accessory information service characteristics is 6AA5XXXX-6352-4D57-A7B4-003A416FBB0B
, where XXXX
is unique for each characteristic.
#define GATT_UUID128_PROD_DATA 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x01, 0x00, 0xA5, 0x6A
#define GATT_UUID128_MANU_NAME 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x02, 0x00, 0xA5, 0x6A
#define GATT_UUID128_MODEL_NAME 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x03, 0x00, 0xA5, 0x6A
#define GATT_UUID128_RESERVED 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x04, 0x00, 0xA5, 0x6A
#define GATT_UUID128_ACC_CATEGORY 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x05, 0x00, 0xA5, 0x6A
#define GATT_UUID128_ACC_CAP 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x06, 0x00, 0xA5, 0x6A
#define GATT_UUID128_FW_VERS 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x07, 0x00, 0xA5, 0x6A
#define GATT_UUID128_FINDMY_VERS 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x08, 0x00, 0xA5, 0x6A
#define GATT_UUID128_BATT_TYPE 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x09, 0x00, 0xA5, 0x6A
#define GATT_UUID128_BATT_LVL 0x0B, 0xBB, 0x6F, 0x41, 0x3A, 0x00, 0xB4, 0xA7, 0x57, 0x4D, 0x52, 0x63, 0x0A, 0x00, 0xA5, 0x6A
The characteristic data can be received in ais_attr_read_cb()
.
T_APP_RESULT ais_attr_read_cb(uint8_t conn_id, T_SERVER_ID service_id,
uint16_t attrib_index, uint16_t offset, uint16_t *p_length, uint8_t **pp_value)
Find My Network Service
The Find My network service UUID is shown as follows.
#define FINDMY_UUID_SERVICE 0xFD44
The UUID for Find My network service characteristics is 4F86XXXX-943B-49EF-BED4-2F730304427A
,
where XXXX
is unique for each characteristic.
#define GATT_UUID128_PAIR_CTRL_POINT 0x7A, 0x42, 0x04, 0x03, 0x73, 0x2F, 0xD4, 0xBE, 0xEF, 0x49, 0x3B, 0x94, 0x01, 0x00, 0x86, 0x4F
#define GATT_UUID128_CONF_CTRL_POINT 0x7A, 0x42, 0x04, 0x03, 0x73, 0x2F, 0xD4, 0xBE, 0xEF, 0x49, 0x3B, 0x94, 0x02, 0x00, 0x86, 0x4F
#define GATT_UUID128_NON_OWNER_CTRL_POINT 0x7A, 0x42, 0x04, 0x03, 0x73, 0x2F, 0xD4, 0xBE, 0xEF, 0x49, 0x3B, 0x94, 0x03, 0x00, 0x86, 0x4F
#define GATT_UUID128_PAIRED_OWNER_INFO_CTRL_POINT 0x7A, 0x42, 0x04, 0x03, 0x73, 0x2F, 0xD4, 0xBE, 0xEF, 0x49, 0x3B, 0x94, 0x04, 0x00, 0x86, 0x4F
#define GATT_UUID128_DEBUG_CTRL_POINT 0x7A, 0x42, 0x04, 0x03, 0x73, 0x2F, 0xD4, 0xBE, 0xEF, 0x49, 0x3B, 0x94, 0x05, 0x00, 0x86, 0x4F
Set and update characteristic data through the following interface.
T_APP_RESULT fns_attr_write_cb(uint8_t conn_id, T_SERVER_ID service_id,
uint16_t attrib_index, T_WRITE_TYPE write_type, uint16_t length, uint8_t *p_value,
P_FUN_WRITE_IND_POST_PROC *p_write_ind_post_proc)
void fns_cccd_update_cb(uint8_t conn_id, T_SERVER_ID service_id, uint16_t index,
uint16_t cccbits)
Bluetooth LE Advertising
The chapter will cover the various states and operations related to Bluetooth Low Energy advertising for accessories within the Find My network.
Find My State
The accessory operations can be described using a state machine with the states listed in this chapter and transition between them based on interactions with an owner device.

Find My State
Unpaired
The accessory must be in an unpaired state on first startup or before the accessory setup is completed.
Connected
The accessory must enter the connected state after the Find My network pairing successfully completes with the owner device.
Nearby
The accessory must enter the nearby state immediately after it disconnects from an owner device. The accessory shall remain in the nearby state for nearby time.
Separated
The accessory must enter the separated state under these conditions:
The accessory is paired and starts up from a reset, power cycle, or other reinitialization procedure.
The accessory is in the nearby state and the nearby time has expired.
Payload for Pairing
The accessory that is not Find My network paired shall advertise the Find My network service as a primary service when the user puts the accessory in pairing mode. The Bluetooth LE payload for pairing is shown as follows.
static uint8_t pairing_adv_data[29] =
{
/* Flags */
0x18, /* length */
GAP_ADTYPE_SERVICE_DATA, /* type="Flags" */
LO_WORD(FINDMY_UUID_SERVICE),
HI_WORD(FINDMY_UUID_SERVICE),
};
The fmna_adv_init_pairing()
is used to initialize pairing data.
void fmna_adv_init_pairing(void)
{
fmna_pairing_adv_service_data_init();
fmna_adv_platform_init_pairing((uint8_t *)&m_fmna_pairing_adv_payload,
sizeof(m_fmna_pairing_adv_payload));
}
Payload for Nearby State
The accessory must enter the connected state after the Find My network pairing successfully completes with the owner device. Then, the accessory shall advertise the Find My network Bluetooth LE payload for nearby state.
static uint8_t nearby_adv_data[31] =
{
0x03,
0xFF,
0x4C, 0x00,
};
The fmna_adv_init_nearby()
is used to initialize nearby data.
void fmna_adv_init_nearby(uint8_t pubkey[FMNA_PUBKEY_BLEN])
{
// Initialize Nearby manufacturing data with the public key
fmna_nearby_adv_manuf_data_init(pubkey);
fmna_adv_platform_init_nearby((uint8_t *)&m_fmna_nearby_adv_packet,
sizeof(m_fmna_nearby_adv_packet));
}
Payload for Separated State
When the accessory is in the separated state, the accessory shall advertise the Find My network LE payload.
static uint8_t separate_adv_data[31] =
{
0x03,
0xFF,
0x4C, 0x00,
};
The fmna_adv_platform_init_separated()
is used to initialize separated data.
void fmna_adv_platform_init_separated(uint8_t *separated_adv_manuf_data,
size_t separated_adv_manuf_data_size)
{
fmna_fast_adv_interval = fmna_separated_adv_fast_intv;
fmna_slow_adv_interval = fmna_separated_adv_slow_intv;
memcpy(separate_adv_data + MANU_DATA_OFFSET, separated_adv_manuf_data,
separated_adv_manuf_data_size);
separate_adv_data[0] = 3 + separated_adv_manuf_data_size;
ble_ext_adv_mgr_set_adv_data(app_findmy_adv_get_adv_handle(), 3 + separated_adv_manuf_data_size + 1,
(uint8_t *)separate_adv_data);
FMNA_LOG_INFO("ADV Separate len %d", 3 + separated_adv_manuf_data_size + 1);
FMNA_LOG_HEXDUMP_INFO(separate_adv_data, 31);
}
Find My Pairing
The accessory must be paired with an owner device before it can be located. The owner device initiates the standard Bluetooth LE encryption before accessing the Find My network services.
Find My Pairing Mode
The accessory automatically enters pairing mode after power on. The app_findmy_enter_pair_mode()
is used to enter pairing mode.
void app_findmy_enter_pair_mode(void)
{
APP_PRINT_INFO0("app_findmy_enter_pair_mode");
if (app_cfg_nv.bud_role != REMOTE_SESSION_ROLE_SECONDARY)
{
start_pair_adv();
}
}
Generate Pairing Data
The accessory must respond to a pairing session request using the Send_pairing_data opcode. The response from the accessory must be sent within 60 seconds.
typedef struct
{
uint8_t c1[C1_BLEN];
uint8_t e2[E2_BLEN];
} __attribute__((packed)) fmna_send_pairing_data_t;
The fm_crypto_ckg_gen_c1()
is used to generate C1.
int fm_crypto_ckg_gen_c1(fm_crypto_ckg_context_t ctx, byte out[32])
The populate_e2_generation_encryption_msg()
is used to generate E2.
static void populate_e2_generation_encryption_msg(void)
Send Pairing Data
The accessory must send the encrypted payload generated using the Apple server encryption key Q_E.
fmna_ret_code_t fmna_crypto_generate_send_pairing_data_params(void)
Finalize Pairing
The owner device initiates the finalize pairing process to complete pairing.
fmna_ret_code_t fmna_crypto_finalize_pairing(void)
Sound
Play sound requirements apply exclusively to accessories that are equipped with a sound maker.
typedef enum
{
FMNA_SERVICE_OPCODE_SOUND_START = 0x0200,
FMNA_SERVICE_OPCODE_SOUND_STOP = 0x0201,
FMNA_SERVICE_OPCODE_SOUND_COMPLETED = 0x020D,
} FMNA_Service_Opcode_t;
Start Sound
Click the Play Sound button after the accessory is connected.

Play Sound
The Sound_Start opcode is used to play the TONE_FINDMY_SOUND
tone on the sound maker of the accessory.
case FMNA_SERVICE_OPCODE_SOUND_START:
{
...
{
fmna_connection_update_connection_info(conn_handle, FMNA_MULTI_STATUS_PLAYING_SOUND, true);
response_status = RESPONSE_STATUS_SUCCESS;
fmna_state_machine_dispatch_event(FMNA_SM_EVENT_SOUND_START);
}
} break;
If no operation is performed, the sound will stop after ten seconds.
void fmna_sound_platform_start(void)
{
APP_PRINT_INFO0("fmna_sound_platform_start: Sound starting...");
app_audio_tone_type_play(TONE_FINDMY_SOUND, false, false);//play sound
findmy_sound_state = FMNA_SOUND_PLAY;
app_start_timer(&timer_idx_findmy_sound_stop, "findmy_stop",
findmy_timer_id, APP_TIMER_FINDMY_SOUND_STOP, 0, false,
10 * 1000);
}
Due to the lack of Find My sound sources, modifying the tone type is necessary.
typedef enum
{
TONE_POWER_ON, //0x00
TONE_POWER_OFF, //0x01
...
#if F_APP_FINDMY_FEATURE_SUPPORT
TONE_FINDMY_SOUND = 0x61, //resue of TONE_APT_EQ_0
#endif
} T_APP_AUDIO_TONE_TYPE;
Stop Sound
Click the Stop Sound button when playing the sound.

Stopsound
The Sound_Stop opcode is used to stop an ongoing sound request.
case FMNA_SERVICE_OPCODE_SOUND_STOP:
{
if (!fmna_connection_is_status_bit_enabled(CONN_HANDLE_ALL, FMNA_MULTI_STATUS_PLAYING_SOUND))
{
response_status = RESPONSE_STATUS_INVALID_STATE;
}
else
{
fmna_state_machine_dispatch_event(FMNA_SM_EVENT_SOUND_STOP);
}
} break;
Then the accessory will be in the FMNA_SM_EVENT_SOUND_COMPLETE
state.
void fmna_sound_platform_stop(void)
{
ret_code_t ret_code = app_timer_stop(m_fmna_sound_timeout_timer_id);
APP_ERROR_CHECK(ret_code);
fmna_state_machine_dispatch_event(FMNA_SM_EVENT_SOUND_COMPLETE);
}
Sound Completed
The accessory will confirm the completion of the stop sound procedure by sending the Sound_Completed
message.
uint32_t fmna_generic_evt_sound_complete_handler(FMNA_SM_Event_t fmna_evt, void *p_context)
{
uint16_t sound_conn_handle = fmna_connection_get_conn_handle_with_multi_status_enabled(
FMNA_MULTI_STATUS_PLAYING_SOUND);
if (sound_conn_handle == CONN_HANDLE_INVALID)
{
return FMNA_SM_STATUS_SUCCESS;
}
if (fmna_connection_is_status_bit_enabled(sound_conn_handle, FMNA_MULTI_STATUS_ENCRYPTED))
{
fmna_gatt_send_indication(sound_conn_handle, FMNA_SERVICE_OPCODE_SOUND_COMPLETED, NULL, 0);
}
else
{
fmna_gatt_send_indication(sound_conn_handle, FMNA_SERVICE_NON_OWNER_OPCODE_SOUND_COMPLETED, NULL, 0);
}
fmna_connection_update_connection_info_all(FMNA_MULTI_STATUS_PLAYING_SOUND, false);
return FMNA_SM_STATUS_SUCCESS;
}
SDK Usage with iOS Find My APP
This chapter will guide users through the process of testing and using the Find My function on iOS devices.
Command Operation
-
To enter the Find My network pairing mode and start pairing advertising when the accessory is in the unpaired state, send the command findmy enter_pairing. The default timeout for this process is 10 minutes.
2023-07-28 16:19:59,073 INFO: input command: findmy enter_pairing 2023-07-28 16:19:59,073 INFO: Parser cmdParser: cmd opcode: 2300 param: [00] name: CMD_FINDMY_FEATURE 2023-07-28 16:19:59,073 INFO: Packet sendPacket: <<<send packet: AA 05 03 00 00 23 00 D5 2023-07-28 16:19:59,088 INFO: AciHostCLI continuously_read: >>>receive packet: AA 02 05 00 00 00 00 23 00 D6 2023-07-28 16:19:59,088 INFO: Parser eventParser: syncWord: AA,seqn: 02,len: 5,opcode: 0000,params: [00 23 00], check_sum: D6 2023-07-28 16:19:59,088 INFO: Parser eventParser: event name: APP_EVENT_ACK 2023-07-28 16:19:59,088 INFO: Parser eventParser: event_id: [00 23] 2023-07-28 16:19:59,088 INFO: Parser eventParser: status: [00]
-
After the accessory is paired, send the command mmi freset to reset it to the default factory settings and clear all bonding information.
2023-07-28 16:19:33,066 INFO: input command: mmi freset 2023-07-28 16:19:33,066 INFO: Parser cmdParser: cmd opcode: 0004 param: [00 58] name: APP_CMD_MMI 2023-07-28 16:19:33,066 INFO: Packet sendPacket: <<<send packet: AA 02 04 00 04 00 00 58 9E 2023-07-28 16:19:33,082 INFO: AciHostCLI continuously_read: >>>receive packet: AA 02 05 00 00 00 04 00 00 F5 2023-07-28 16:19:33,082 INFO: Parser eventParser: syncWord: AA,seqn: 02,len: 5,opcode: 0000,params: [04 00 00], check_sum: F5 2023-07-28 16:19:33,082 INFO: Parser eventParser: event name: APP_EVENT_ACK 2023-07-28 16:19:33,082 INFO: Parser eventParser: event_id: [04 00] 2023-07-28 16:19:33,082 INFO: Parser eventParser: status: [00] 2023-07-28 16:19:33,598 INFO: AciHostCLI continuously_read: >>>receive packet: AA 03 03 00 07 00 00 F3 2023-07-28 16:19:33,598 INFO: Parser eventParser: syncWord: AA,seqn: 03,len: 3,opcode: 0007,params: [00], check_sum: F3 2023-07-28 16:19:33,598 INFO: Parser eventParser: event name: EVENT_DEVICE_STATE 2023-07-28 16:19:33,598 INFO: Parser eventParser: device_state: [00] 2023-07-28 16:19:33,598 INFO: Packet sendPacket: <<<send packet: AA 03 05 00 00 00 07 00 00 F1 2023-07-28 16:19:35,186 ERROR: gap in packets, between 3 and 1 packet before: [170, 3, 3, 0, 7, 0, 0, 243] packet after: [170, 1, 8, 0, 35, 0, 19, 34, 119, 86, 34, 3, 173] 2023-07-28 16:19:35,186 INFO: AciHostCLI continuously_read: >>>receive packet: AA 01 08 00 23 00 13 22 77 56 22 03 AD 2023-07-28 16:19:35,186 INFO: Parser eventParser: syncWord: AA,seqn: 01,len: 8,opcode: 0023,params: [13 22 77 56 22 03], check_sum: AD 2023-07-28 16:19:35,186 INFO: Parser eventParser: event name: EVENT_BT_READY 2023-07-28 16:19:35,186 INFO: Parser eventParser: factory_addr: [13 22 77 56 22 03] 2023-07-28 16:19:35,186 INFO: Packet sendPacket: <<<send packet: AA 04 05 00 00 00 23 00 00 D4
-
After the accessory is separated, send the findmy put_serial_number command to initiate the serial number read state.
2023-07-28 16:24:35,541 INFO: input command: findmy put_serial_number 2023-07-28 16:24:35,541 INFO: Parser cmdParser: cmd opcode: 2300 param: [01] name: CMD_FINDMY_FEATURE 2023-07-28 16:24:35,541 INFO: Packet sendPacket: <<<send packet: AA 22 03 00 00 23 01 B7 2023-07-28 16:24:35,558 INFO: AciHostCLI continuously_read: >>>receive packet: AA 1F 05 00 00 00 00 23 00 B9 2023-07-28 16:24:35,558 INFO: Parser eventParser: syncWord: AA,seqn: 1F,len: 5,opcode: 0000,params: [00 23 00], check_sum: B9 2023-07-28 16:24:35,568 INFO: Parser eventParser: event name: APP_EVENT_ACK 2023-07-28 16:24:35,568 INFO: Parser eventParser: event_id: [00 23] 2023-07-28 16:24:35,568 INFO: Parser eventParser: status: [00]
Run the First Test
To test the Find My function, please launch the Find My APP on an iOS device and follow the steps below:
Press the Add New Item button to enroll a new Tag device.
Press the Other Supported Items button to start scanning for the pairing advertising sent by the accessory.
Send the findmy enter pairing command to trigger pairing advertising. When the mobile application scans the pairing advertising, the following window will appear. Press the Connect button within the application.
Press the Continue button, enter the name, and set the icon to complete the pairing session.

Find My Process
Users should be able to pair their Find My network-enabled accessory and test the following features using the Find My APP:
GFPS Finder
The GFPS Finder, a novel protocol launched by Google, delineates an end-to-end encryption method specifically for tracking beaconing Bluetooth LE devices. It represents an expansion of the existing GFPS specification.
This extension should be implemented by vendors who are keen to activate location tracking for their devices, ensuring compatibility with Eddystone-E2EE-EID.
This document is partitioned into the following sections for a comprehensive introduction:
Enable GFPS Finder Feature: Presents the method for activating the GFPS Finder function.
GFPS Finder Features: Provides an overview of the capabilities supported by GFPS Finder.
Test with Find My Device App: Introduces the procedure for testing using the Find My Device APP.
Enable GFPS Finder Feature
To enable GFPS, users need to make the appropriate configurations in app_gfps_cfg.c
and the project.
Parameters Configuration
GFPS Finder parameters can be configured in app_gfps_cfg.c
.
void app_gfps_cfg_init(void)
{
app_gfps_cfg.gfps_model_id[0] = 0;
app_gfps_cfg.gfps_model_id[1] = 0;
app_gfps_cfg.gfps_model_id[2] = 0;
app_gfps_cfg.gfps_support = 1;
app_gfps_cfg.gfps_finder_support = 1;
app_gfps_cfg.gfps_le_device_support = 1;
app_gfps_cfg.gfps_enable_tx_power = 1;
app_gfps_cfg.gfps_tx_power = -6;
app_gfps_cfg.tone_gfps_findme = 0x9C;//power_on.wav
app_gfps_cfg.gfps_account_key_num = 5;
app_gfps_cfg.gfps_discov_adv_interval = 32;
app_gfps_cfg.gfps_not_discov_adv_interval = 100;
app_gfps_cfg.gfps_battery_info_enable = 0;
app_gfps_cfg.gfps_le_disconn_force_enter_pairing_mode = 0;
app_gfps_cfg.gfps_le_device_mode = GFPS_LE_DEVICE_MODE_LE_MODE_WITHOUT_CSIP;
uint8_t gfps_public_key[64] = {0};
uint8_t gfps_private_key[32] = {0};
memcpy(app_gfps_cfg.gfps_public_key, gfps_public_key, 64);
memcpy(app_gfps_cfg.gfps_private_key, gfps_private_key, 32);
app_gfps_cfg.tone_gfps_dult = 0x9C;//power_on.wav
uint8_t device_name[64] = {0};
uint8_t company_name[64] = {0};
memcpy(app_gfps_cfg.gfps_company_name, company_name, sizeof(company_name));
memcpy(app_gfps_cfg.gfps_device_name, device_name, sizeof(device_name));
app_gfps_cfg.gfps_device_type = GFPS_LOCATOR_TRACKER;
}
-
Transport:
app_gfps_cfg.gfps_support
must be set to 1. Otherwise, the GFPS advertising will not be advertised, and there will be no GFPS function, which is equivalent to enabling/disabling the GFPS function.
-
Module ID/Anti-spoofing public key/Anti-spoofing private key (Hex):
app_gfps_cfg.gfps_model_id
: Model ID information.app_gfps_cfg.gfps_public_key
: Public key information.app_gfps_cfg.gfps_private_key
: Private key information.
The registered Model ID, public key, and private key must be filled in. These parameters shall not all be zero. Users need to register these parameters by themselves. The registration address is Nearby.
-
Enable TX power data in ADV:
app_gfps_cfg.gfps_tx_power
: The mobile phone utilizes this TX power data to estimate the distance between itself and the device. If the distance surpasses a certain limit, the GFPS notification window will not appear. This value must invariably be provided. However, if the TX power data has already been specified while registering the Model ID on the Google Nearby Device console page, this value will be disregarded by the mobile phone. Consequently, abstaining from setting the TX power during the registration of the Model ID is recommended.
-
Discoverable/Not Discoverable advertising interval (0.625 ms/per unit), GFPS advertising interval setting must be filled in:
app_gfps_cfg.gfps_discov_adv_interval
: Discoverable advertising interval. The default value is 32. If this value is set too large, it may affect the efficiency of the initial pairing.app_gfps_cfg.gfps_not_discov_adv_interval
: Not Discoverable advertising interval. The default value is 100. If wanting to minimize the power consumption in device idle, the maximum value can be set to 400. However, it may affect the efficiency of the subsequent pairing.
-
Account key number:
app_gfps_cfg.gfps_account_key_num
: The maximum number of GFPS account keys the device could store in the FTL, which must be filled in.
-
Enable Finder:
app_gfps_cfg.gfps_finder_support
: Enable or disable the GFPS Finder feature.
-
Configure DULT informations:
app_gfps_cfg.gfps_company_name
: The company name of the device series registered in the Nearby Console.app_gfps_cfg.gfps_device_name
: The device name of the device series registered in the Nearby Console.-
app_gfps_cfg.gfps_device_type
: Device type, which can be referred toT_GFPS_DEVICE_TYPE
:typedef enum { GFPS_LOCATOR_TRACKER = 1, GFPS_WATCH = 146, GFPS_HEADPHONES = 149, GFPS_EARPHONES = 150, } T_GFPS_DEVICE_TYPE;
Add GFPS Finder Files
Copy the GFPS related files provided in 3rd_service_gfps
to sdk
.

Adding GFPS Finder Include Files

Adding GFPS Finder APP Files

GFPS Finder Library Files
Set up GFPS Finder Environment
Enable GFPS-related flags in app_flags.h
.
#undef CONFIG_REALTEK_GFPS_FEATURE_SUPPORT
#define CONFIG_REALTEK_GFPS_FEATURE_SUPPORT 1
#undef CONFIG_REALTEK_GFPS_FINDER_SUPPORT
#define CONFIG_REALTEK_GFPS_FINDER_SUPPORT (1 && CONFIG_REALTEK_GFPS_FEATURE_SUPPORT)
#undef CONFIG_REALTEK_GFPS_LE_DEVICE_SUPPORT
#define CONFIG_REALTEK_GFPS_LE_DEVICE_SUPPORT (1 && CONFIG_REALTEK_GFPS_FEATURE_SUPPORT)
Enable build gfps.lib
in Keil project: Find gfps.lib
in Keil project, then right click the .

Building GFPS Library
Enable build gfps
in Keil project: Find the folder gfps
in Keil project then right click the .

Building GFPS Project
GFPS Finder Features
This chapter introduces GFPS Finder support features:
GFPS Finder Provisioning will be introduced in chapter GFPS Finder Provisioning.
GFPS Finder Ring will be introduced in chapter GFPS Finder Ring.
GFPS Finder Set UTP Mode will be introduced in chapter GFPS Finder Set UTP Mode.
GFPS Finder Unprovision will be introduced in chapter GFPS Finder Unprovision.
GFPS Finder Provisioning
Provisioning refers to the process from not supporting finder to supporting finder. For users intent on utilizing the GFPS Finder feature, commencement of the provisioning process is necessary.

GFPS Finder Provisioning
T_GFPS_FINDER_CAUSE app_gfps_finder_cb(T_GFPS_FINDER_CB_DATA *p_data)
{
T_GFPS_FINDER_CAUSE cause = GFPS_FINDER_CAUSE_SUCCESS;
T_GFPS_FINDER_CB_DATA data;
memcpy(&data, p_data, sizeof(T_GFPS_FINDER_CB_DATA));
uint8_t evt = data.evt;
uint8_t ret_err = 0;
APP_PRINT_INFO1("app_gfps_finder_cb: evt %d", evt);
switch (evt)
{
case GFPS_FINDER_EVT_SET_EIK:
{
memcpy(&p_app_gfps_finder->p_finder->eik, &data.msg_data.eik, sizeof(T_GFPS_EIK));
int8_t save_ret = ftl_save_to_storage(&p_app_gfps_finder->p_finder->eik,
GFPS_FINDER_EIK_FLASH_OFFSET,
sizeof(T_GFPS_EIK));
if (save_ret)
{
ret_err = 1;
goto err;
}
T_BLE_EXT_ADV_MGR_STATE adv_state = gfps_finder_adv_get_adv_state();
if (adv_state == BLE_EXT_ADV_MGR_ADV_DISABLED)
{
uint8_t hash;
uint32_t counter = (p_app_gfps_finder->p_finder->clock_value / 1024) * 1024;
app_gfps_finder_generate_adv_ei_and_hash(p_app_gfps_finder->p_finder->adv_ei,
p_app_gfps_finder->p_finder->eik.key, counter, &hash);
gfps_finder_adv_update_adv_ei_hash(p_app_gfps_finder->p_finder->adv_ei, hash);
gfps_finder_adv_start(0);
app_gfps_finder_start_update_adv_ei_timer();
}
T_GFPS_FINDER test;
memset(&test, 0, sizeof(T_GFPS_FINDER));
int8_t load_ret = ftl_load_from_storage(&test.eik, GFPS_FINDER_EIK_FLASH_OFFSET,
sizeof(T_GFPS_EIK));
if (load_ret)
{
APP_PRINT_ERROR1("app_gfps_finder_cb: load eik from ftl fail %d", load_ret);
}
APP_PRINT_INFO2("app_gfps_finder_cb: load eik %b, valid %d",
TRACE_BINARY(32, test.eik.key), test.eik.valid);
}
break;
......
default:
{
}
break;
}
return cause;
err:
cause = GFPS_FINDER_CAUSE_INVALID_VALUE;
APP_PRINT_ERROR1("app_gfps_finder_cb: err %d", ret_err);
return cause;
}
GFPS Finder Ring
GFPS Finder supports the function of making the device ring. If users want to use this function, please refer to the following flow chart.

GFPS Finder Ring
T_GFPS_FINDER_CAUSE app_gfps_finder_cb(T_GFPS_FINDER_CB_DATA *p_data)
{
T_GFPS_FINDER_CAUSE cause = GFPS_FINDER_CAUSE_SUCCESS;
T_GFPS_FINDER_CB_DATA data;
memcpy(&data, p_data, sizeof(T_GFPS_FINDER_CB_DATA));
uint8_t evt = data.evt;
uint8_t ret_err = 0;
APP_PRINT_INFO1("app_gfps_finder_cb: evt %d", evt);
switch (evt)
{
......
case GFPS_FINDER_EVT_RING:
{
p_app_gfps_finder->gfps_finder_conn_id = data.msg_data.ring.conn_id;
p_app_gfps_finder->gfps_finder_service_id = data.msg_data.ring.service_id;
uint8_t ring_type = data.msg_data.ring.ring_type;
uint16_t ring_time = data.msg_data.ring.ring_time;
uint8_t ring_volume_level = data.msg_data.ring.ring_volume_level;
/*the first byte may hold a special value of 0xFF to ring all components that can ring.*/
if (ring_type == 0xFF)
{
ring_type = GFPS_ALL_RING;
}
app_gfps_msg_set_ring_timeout(ring_time);
app_gfps_msg_handle_ring_event(ring_type);
if ((ring_volume_level != GFPS_FINDER_RING_VOLUME_DEFAULT) &&
(p_app_gfps_finder->p_finder->ring_volume_modify == GFPS_FINDER_RING_VOLUME_ENABLE))
{
p_app_gfps_finder->gfps_finder_ring_volume_level = ring_volume_level;
}
/*A byte indicating the new ringing state*/
uint8_t ring_state;
if (ring_type == GFPS_ALL_STOP)
{
ring_state = GFPS_FINDER_RING_GATT_STOP;
}
else
{
ring_state = GFPS_FINDER_RING_STARTED;
}
gfps_finder_rsp_ring_request(p_app_gfps_finder->gfps_finder_conn_id,
p_app_gfps_finder->gfps_finder_service_id,
ring_state, ring_type, ring_time);
}
break;
......
default:
{
}
break;
}
return cause;
err:
cause = GFPS_FINDER_CAUSE_INVALID_VALUE;
APP_PRINT_ERROR1("app_gfps_finder_cb: err %d", ret_err);
return cause;
}
GFPS Finder Set UTP Mode
Unwanted Tracking Protection mode is intended to allow any client to identify abusive devices with no server communication. By default, the provider should rotate all identifiers as described in the ID rotation chapter below. The server can decide to relay an Unwanted Tracking Protection mode activation request through a sighter of the provider. By doing so, it causes the provider to temporarily use a fixed MAC address, allowing clients to detect it and warn the user of possible unwanted tracking.
To activate or deactivate the Unwanted Tracking Protection mode of the beacon, the seeker should perform a write operation to the characteristic.

GFPS Finder Set UTP Mode
T_GFPS_FINDER_CAUSE app_gfps_finder_cb(T_GFPS_FINDER_CB_DATA *p_data)
{
T_GFPS_FINDER_CAUSE cause = GFPS_FINDER_CAUSE_SUCCESS;
T_GFPS_FINDER_CB_DATA data;
memcpy(&data, p_data, sizeof(T_GFPS_FINDER_CB_DATA));
uint8_t evt = data.evt;
uint8_t ret_err = 0;
APP_PRINT_INFO1("app_gfps_finder_cb: evt %d", evt);
switch (evt)
{
......
case GFPS_FINDER_EVT_UTP_ACTIVE:
{
gfps_finder_adv_update_frame_type(0x41);
app_gfps_finder_upt_enable_start_update_rpa_timer();
}
break;
case GFPS_FINDER_EVT_UTP_DEACTIVE:
{
gfps_finder_adv_update_frame_type(0x40);
app_gfps_finder_upt_enable_stop_rpa_timer();
}
break;
......
default:
{
}
break;
}
return cause;
err:
cause = GFPS_FINDER_CAUSE_INVALID_VALUE;
APP_PRINT_ERROR1("app_gfps_finder_cb: err %d", ret_err);
return cause;
}
GFPS Finder Unprovision
GFPS Finder Unprovision refers to the process of transitioning from supporting the Finder to not supporting it.

GFPS Finder Unprovision
T_GFPS_FINDER_CAUSE app_gfps_finder_cb(T_GFPS_FINDER_CB_DATA *p_data)
{
T_GFPS_FINDER_CAUSE cause = GFPS_FINDER_CAUSE_SUCCESS;
T_GFPS_FINDER_CB_DATA data;
memcpy(&data, p_data, sizeof(T_GFPS_FINDER_CB_DATA));
uint8_t evt = data.evt;
uint8_t ret_err = 0;
APP_PRINT_INFO1("app_gfps_finder_cb: evt %d", evt);
switch (evt)
{
......
case GFPS_FINDER_EVT_CLEAR_EIK:
{
memset(&p_app_gfps_finder->p_finder->eik, 0, sizeof(T_GFPS_EIK));
uint8_t ret = ftl_save_to_storage(&p_app_gfps_finder->p_finder->eik, GFPS_FINDER_EIK_FLASH_OFFSET,
sizeof(T_GFPS_EIK));
if (ret)
{
ret_err = 2;
goto err;
}
T_BLE_EXT_ADV_MGR_STATE adv_state = gfps_finder_adv_get_adv_state();
if (adv_state == BLE_EXT_ADV_MGR_ADV_ENABLED)
{
gfps_finder_adv_stop(APP_STOP_ADV_CAUSE_GFPS_FINDER);
app_gfps_finder_stop_update_adv_ei_timer();
app_gfps_finder_stop_update_random_window_timer();
}
}
break;
......
default:
{
}
break;
}
return cause;
err:
cause = GFPS_FINDER_CAUSE_INVALID_VALUE;
APP_PRINT_ERROR1("app_gfps_finder_cb: err %d", ret_err);
return cause;
}
Test With Find My Device APP
-
Factory reset and power on the device:
Factory reset: Input mmi freset in ACI Host CLI Tool to perform a factory reset.
Power on: Input mmi pwron in ACI Host CLI Tool to power on.
-
Click the Connect button on the popup window to connect.
Connect Button Popup
-
Agree to user responsibility by clicking Agree and continue and Enter screen lock.
Agree to User Responsibility and Screen Lock
-
Add the device to the Find My Device APP.
Add Device to Find My Device APP
One Wire UART
The purpose of this document is to give an overview of the one wire UART function. This document will be divided into the following parts:
How to enable one wire UART will be introduced in chapter One Wire UART Configuration.
Source code of one wire UART will be introduced in chapter Code Overview.
One Wire UART Configuration
This chapter introduces how to enable one wire UART:
How to configure peripheral will be introduced in chapter Peripheral Configuration.
How to configure a one wire UART macro will be introduced in chapter Macro Configuration.
Peripheral Configuration
Firstly, the parameter app_cfg_const.one_wire_uart_support
should be set to enable one wire UART.
app_cfg_const.one_wire_uart_support = 1;
Secondly, PINMUX should be configured as UART. If users want to enable two or more UARTs, the corresponding number of pins should be configured.
Currently, only UART0
and UART2
are available. The macro ONE_WIRE_UART0_PINMUX
is configured as UART0
and ONE_WIRE_UART2_PINMUX
is configured as UART2
.
#define ONE_WIRE_UART0_PINMUX P3_0
#define ONE_WIRE_UART2_PINMUX P3_1
Thirdly, baud rate should be configured the same as the other side. Currently, baud rate is configured as 115200 through parameter
app_transfer_cfg.data_uart_baud_rate
.
app_transfer_cfg.data_uart_baud_rate = BAUD_RATE_115200;
Macro Configuration
Set macro F_APP_ENABLE_TWO_ONE_WIRE_UART
in app_flags.h
.
#define F_APP_ENABLE_TWO_ONE_WIRE_UART 1
Code Overview
This chapter introduces the one wire UART source code:
One wire UART initialization will be introduced in chapter One Wire UART Initialization.
One wire UART data send will be introduced in chapter One Wire UART Data Send.
One wire UART data receive will be introduced in chapter One Wire UART Data Receive.
One Wire UART Initialization
Initialization of one wire UART includes initialization of app_uart
and setting of UART. The initialization flow is as shown in the diagram below.

One Wire UART Initialization Flow
The function app_console_uart_init()
is used for the APP to set parameters to UART.
static void app_console_uart_init(void)
{
...
T_CONSOLE_UART_CONFIG console_uart_config;
...
console_uart_config.one_wire_uart_support = app_cfg_const.one_wire_uart_support;
console_uart_config.data_uart_baud_rate = app_transfer_cfg.data_uart_baud_rate;
console_uart_config.callback = app_console_handle_wake_up;
console_uart_config.uart_rx_dma_enable = false;
console_uart_config.uart_tx_dma_enable = false;
...
console_uart_config_init(&console_uart_config);
}
The function app_uart_buffer_alloc()
is used to initialize the APP database and configure one wire UART by calling function UART_OneWireConfig
and register callback to UART to receive data.
void app_uart_init(void)
{
app_uart_buffer_alloc(0);
app_uart_buffer_alloc(2);
console_uart_init(app_uart_callback);
}
When the app_task
is created, the function app_one_wire_uart_open()
is called to configure PINMUX mode and pad and initialize the UART driver by calling function app_console_uart_driver_init()
.
void app_one_wire_uart_open(uint8_t index)
{
switch (index)
{
case T_IDX_UART0:
{
...
Pinmux_Deinit(ONE_WIRE_UART0_PINMUX);
Pinmux_Config(ONE_WIRE_UART0_PINMUX, UART0_TX);
Pad_Config(ONE_WIRE_UART0_PINMUX, PAD_PINMUX_MODE, PAD_IS_PWRON, PAD_PULL_UP, PAD_OUT_DISABLE,
PAD_OUT_LOW);
Pad_PullConfigValue(ONE_WIRE_UART0_PINMUX, PAD_STRONG_PULL);
app_console_uart_driver_init(0);
UART_ClearRxFifo(UART0);
UART_INTConfig(UART0, UART_INT_RD_AVA | UART_INT_IDLE,
ENABLE);//for normal pin, line status intr unnecessary
...
}
break;
case T_IDX_UART2:
{
...
}
break;
default:
break;
}
}
One Wire UART Data Send
UART will automatically transition to TX mode before sending any data. Following data transmission, UART will enter RX mode.
If users want to send data by 2 one wire UARTs at the same time, these three functions shall be called in different conditions:
-
If the command format is the same as current in the code, users can call
app_report_uart_event()
directly.static void app_report_uart_event(uint16_t event_id, uint8_t *data, uint16_t len, uint8_t index) { if (app_cfg_const.enable_data_uart || app_cfg_const.one_wire_uart_support) { uint16_t total_len; uint8_t check_sum; typedef struct { uint8_t sync_byte; uint8_t seq; uint16_t pkt_len; uint16_t event_id; uint8_t data[0]; } __attribute__((packed)) T_SEND_BUF; ... send_buf->sync_byte = CMD_SYNC_BYTE; send_buf->seq = uart_tx_seqn[index]; send_buf->pkt_len = len + sizeof(send_buf->event_id); send_buf->event_id = event_id; if (len) { memcpy(send_buf->data, data, len); } check_sum = app_util_calc_checksum((uint8_t *)&send_buf->seq, total_len - 2); send_buf->data[len] = check_sum; if (app_push_data_transfer_queue(CMD_PATH_UART, (uint8_t *)send_buf, total_len, index) == false) { free(send_buf); } } }
If the command format is different from current in the code, users can fix the command format in
app_report_uart_event()
or callapp_push_data_transfer_queue()
. The parameter index represents UART index. The function includes flow control in the APP.If users don’t need any flow control in the APP, the function
console_uart_write()
can be called. The parameter index represents UART index.
One Wire UART Data Receive
Data received from UART is sent to the callback app_uart_callback()
and received data will be sent to the app_task
.
RAM_TEXT_SECTION void app_uart_callback(T_CONSOLE_UART_EVENT evt, void *buf, uint16_t len, uint8_t index)
{
APP_PRINT_INFO4("app_uart_callback len %x, index %x, evt %x, data %b", len, index, evt, TRACE_BINARY(len, buf));
uint32_t s;
switch (evt)
{
case CONSOLE_UART_EVENT_DATA_IND:
{
T_IO_MSG msg;
if (index == cmd_block[index].id)
{
...
msg.type = IO_MSG_TYPE_UART;
msg.subtype = 0;
msg.u.param = index;
app_io_msg_send(&msg);
}
}
break;
default:
break;
}
}
Data can be parsed in the function app_uart_parser()
and specific commands shall be handled in app_handle_cmd_set()
after parsing.
USB Mass Storage
This application note describes the functional implementation method and the employed USB Mass Storage descriptors. This USB Mass Storage addresses Bulk-Only transport, or in other words, transport of command, data, and status occurring solely via bulk endpoints (Not via interrupt or control endpoints). This specification only uses the default pipe to clear a STALL condition on the bulk endpoints and to issue class-specific requests as defined below. This specification does not require the use of an interrupt endpoint.
Requirements
The device descriptors and related descriptors for USB Mass Storage are mostly introduced in this paragraph.
USB Device Configure Descriptor
Device descriptor describes general information about USB device.
USB Mass Storage Configure Descriptor
This application exposes the following features:
USB Mass Storage interface descriptor, which needs to configure the mass storage class specification.
Endpoint Descriptor, which configures endpoint packet size.
Source Code Overview
The following sections describe important parts of this application.
Initialization
The USB main function is invoked when the application is in the USB initialization flow, and it performs the following initialization functions.
void app_usb_init(void)
{
memset(&app_usb_db, 0, sizeof(T_APP_USB_DB));
usb_dm_cb_register(app_usb_dm_cb);
adp_register_state_change_cb(ADP_DETECT_5V, (P_ADP_PLUG_CBACK)app_usb_adp_state_change_cb, NULL);
usb_dev_init();
T_USB_CORE_CONFIG config = {.speed = USB_SPEED_FULL, .class_set = {.hid_enable = 0, .uac_enable = 0}};
config.speed = USB_SPEED_HIGH;
usb_dm_core_init(config);
#if F_APP_USB_MSC_SUPPORT
sd_config_init((T_SD_CONFIG *)&sd_card_cfg);
sd_board_init();
sd_card_init();
extern int usb_ms_scsi_init(void);
usb_ms_scsi_init();
extern int usb_ms_disk_init(void);
usb_ms_disk_init();
usb_msc_init();
#endif
}
USB Device Configure
The USB device application can configure the descriptor.
USB Mass Storage Configure Descriptor
In analogy to the device descriptor, a mass storage configuration descriptor is applicable only in the case of mass storage only devices.
static T_USB_INTERFACE_DESC usb_msc_if_desc =
{
.bLength = sizeof(T_USB_INTERFACE_DESC),
.bDescriptorType = USB_DESC_TYPE_INTERFACE,
.bInterfaceNumber = 0,
.bAlternateSetting = 0,
.bNumEndpoints = 2,
.bInterfaceClass = USB_CLASS_MSC,
.bInterfaceSubClass = 0x06, //SCSI
.bInterfaceProtocol = 0x50, //BBB
.iInterface = 4
};
static const T_USB_ENDPOINT_DESC usb_ms_bi_ep_desc_hs =
{
.bLength = sizeof(T_USB_ENDPOINT_DESC),
.bDescriptorType = USB_DESC_TYPE_ENDPOINT,
.bEndpointAddress = MSC_BULK_IN_EP,
.bmAttributes = USB_EP_TYPE_BULK,
.wMaxPacketSize = 512,
.bInterval = 4,
};
static const T_USB_ENDPOINT_DESC usb_ms_bo_ep_desc_hs =
{
.bLength = sizeof(T_USB_ENDPOINT_DESC),
.bDescriptorType = USB_DESC_TYPE_ENDPOINT,
.bEndpointAddress = MSC_BULK_OUT_EP,
.bmAttributes = USB_EP_TYPE_BULK,
.wMaxPacketSize = 512,
.bInterval = 4,
};
static const T_USB_ENDPOINT_DESC usb_ms_bi_ep_desc_fs =
{
.bLength = sizeof(T_USB_ENDPOINT_DESC),
.bDescriptorType = USB_DESC_TYPE_ENDPOINT,
.bEndpointAddress = 0x82,
.bmAttributes = USB_EP_TYPE_BULK,
.wMaxPacketSize = 64,
.bInterval = 1,
};
static const T_USB_ENDPOINT_DESC usb_ms_bo_ep_desc_fs =
{
.bLength = sizeof(T_USB_ENDPOINT_DESC),
.bDescriptorType = USB_DESC_TYPE_ENDPOINT,
.bEndpointAddress = 0x02,
.bmAttributes = USB_EP_TYPE_BULK,
.wMaxPacketSize = 64,
.bInterval = 1,
};
void usb_msc_init(void)
{
usb_ms_driver_if_desc_register((T_USB_DESC_HDR **)usb_ms_if_descs_fs,
(T_USB_DESC_HDR **)usb_ms_if_descs_hs);
usb_ms_driver_init();
}
USB Control
USB will boot up and successfully list all of its features when this API is run.
void app_usb_start(void)
{
if (app_cfg_const.bud_role != REMOTE_SESSION_ROLE_SECONDARY)
{
app_usb_set_hp_mode();
app_auto_power_off_disable(AUTO_POWER_OFF_MASK_USB_AUDIO_MODE);
app_dlps_disable(APP_DLPS_ENTER_CHECK_USB);
#if (TARGET_RTL8773DO == 1)
pmu_vcore3_pon_domain_enable(PMU_USB);
usb_dm_start(false);
#else
usb_dm_start(false);
#endif
app_ipc_publish(USB_IPC_TOPIC, USB_IPC_EVT_PLUG, NULL);
}
}
APP Configurable Functions
APP configurable functions are defined in app_flags.h
. F_APP_USB_MSC_SUPPORT
can enable the USB Mass Storage function in app_flags.h
.
Test Procedure
Make sure that the USB Mass Storage functionality is implemented on the EVB and the system is properly configured. This may require software development and configuration on the EVB. Connect the EVB to the host using a USB cable. Ensure that the USB cable can transfer data correctly and that the EVB is recognized by the host.
Set up Test Environment
To set up a USB Mass Storage testing environment, the following components are needed: USB Mass Storage EVB, and the SD card connects with EVB via SDIO. This is a hardware device that supports USB Mass Storage and is typically used to simulate a storage device.

Setup for Testing Environment
USB Cable: This is used to connect the USB Mass Storage EVB to a USB port on a computer.
Computer: This is used to run the test code and communicate with the USB Mass Storage EVB.
Test USB Mass Storage
Open Device Manager (Windows) on the host to check if the connected USB device is enumerated correctly.

Test USB Mass Storage
Files can be copied, erased, and other operations performed in the mass storage.
LE HID Host
The purpose of this document is to provide an overview of the Bluetooth LE HID host application based on HOGP. HOGP defines the procedures and features that are utilized by Bluetooth LE HID devices through GATT, as well as Bluetooth HID hosts through GATT. This profile shall operate over an LE transport only.
HID Roles
HOGP defines three roles:
The HID Device shall be a GATT server.
The Boot Host shall be a GATT client.
The Report Host shall be a GATT client.
Use of the term HID Host refers to both host roles: Boot Host, and Report Host. A Report Host is required to support an HID Parser and be able to handle arbitrary formats for data transfers (Known as reports) whereas a Boot Host is not required to support an HID Parser as all data transfers for Boot Protocol mode are of predefined length and format.
Roles/Service Relationships
The relationship between services and the profile roles is shown below.

Boot Host and HID Device Roles/Service Relationship

Report Host and HID Device Roles/Service Relationship
Note
Profile roles are represented by yellow boxes and services are represented by orange boxes.
The Report Host supports the Scan Client role of the Scan Parameters Profile. The Boot Host shall not support the Scan Client role of the Scan Parameters Profile.
LE HID Host Configuration
This chapter provides an introduction on how to enable the LE HID host feature.
Set the macro BLE_HID_CLIENT_SUPPORT
and F_APP_BLE_HID_HOST_SUPPORT
in app_flags.h
.
#define BLE_HID_CLIENT_SUPPORT 1
#define F_APP_BLE_HID_HOST_SUPPORT 1
API Introduction
This chapter defines the APIs for the LE HID host.
HID Interaction
Below is a flow chart depicting the interaction between an HID host and an HID device based on Low Energy connections.

Interaction between HID Host and HID Device
HID Client Initialization
To initialize the HID client and configure the maximum number of HID links, the following interface can be used.
bool hids_add_client(P_FUN_HIDS_CLIENT_APP_CB app_cb, uint8_t link_num)
{
T_ATTR_UUID srv_uuid = {0};
srv_uuid.is_uuid16 = true;
srv_uuid.p.uuid16 = GATT_UUID_HIDS;
if (gatt_client_spec_register(&srv_uuid, hids_client_cbs) == GAP_CAUSE_SUCCESS)
{
/* register callback for profile to inform application that some events happened. */
hids_client_cb = app_cb;
hids_client_link_num = link_num;
hids_table = calloc(1, link_num * sizeof(T_HIDS_LINK));
return true;
}
return false;
}
HID Read Report Map
After the HID discovery is completed, the report map can be read using the characteristic UUID.
T_APP_RESULT app_ble_hid_service_cback(uint16_t conn_handle, uint8_t type,
void *p_data)
{
T_APP_RESULT app_result = APP_RESULT_SUCCESS;
if (type == GATT_MSG_HIDS_CLIENT_DIS_DONE)
{
T_HIDS_CLIENT_DIS_DONE *p_dis_done = (T_HIDS_CLIENT_DIS_DONE *)p_data;
APP_PRINT_TRACE1("app_ble_hid_service_cback: discover state %d",
p_dis_done->is_found);
if (p_dis_done->is_found)
{
hid_conn_handle = conn_handle;
for (uint8_t i = 0; i < p_dis_done->srv_instance_num; i++)
{
hids_client_read_report_map_value(conn_handle, i);
}
}
}
else if (type == GATT_MSG_HIDS_CLIENT_READ_RESULT)
{
T_HIDS_CLIENT_READ_RESULT *p_read_result = (T_HIDS_CLIENT_READ_RESULT *)p_data;
APP_PRINT_TRACE1("app_ble_hid_service_cback: char_uuid 0x%x",
p_read_result->char_uuid);
if (p_read_result->char_uuid == GATT_UUID_CHAR_REPORT_MAP)
{
app_ble_hid_host_read_report_map_handle(conn_handle,
p_read_result->data.hids_report_map.p_hids_report_map,
p_read_result->data.hids_report_map.hids_report_map_len);
}
}
return app_result;
}
Send the report map of the HID device to the ACI host via UART path.
void app_ble_hid_host_read_report_map_handle(uint16_t conn_handle, uint8_t *report_buf,
uint16_t report_len)
{
T_APP_LE_LINK *p_link;
p_link = app_link_find_le_link_by_conn_handle(conn_handle);
if (p_link != NULL)
{
APP_PRINT_INFO3("app_ble_hid_host_read_report_map_handle: le_addr %s, conn_handle 0x%x, len %d",
TRACE_BDADDR(p_link->bd_addr), conn_handle, report_len);
if ((report_buf == NULL) || (report_len == 0))
{
return;
}
struct
{
uint16_t conn_handle;
uint16_t pkt_len;
uint8_t payload[];
} __attribute__((packed)) *rpt = NULL;
rpt = calloc(1, report_len + sizeof(*rpt));
rpt->conn_handle = conn_handle;
rpt->pkt_len = report_len;
memcpy(&rpt->payload, report_buf, report_len);
app_report_event(CMD_PATH_UART, EVENT_BLE_HID_READ_REPORT_MAP, 0, (uint8_t *)rpt,
report_len + sizeof(*rpt));
free(rpt);
}
}
HID Read Report
After the HID discovery is complete, users can read the HID report by using the characteristic UUID.
T_APP_RESULT app_ble_hid_service_cback(uint16_t conn_handle, uint8_t type,
void *p_data)
{
if (type == GATT_MSG_HIDS_CLIENT_READ_RESULT)
{
T_HIDS_CLIENT_READ_RESULT *p_read_result = (T_HIDS_CLIENT_READ_RESULT *)p_data;
APP_PRINT_TRACE1("app_ble_hid_service_cback: char_uuid 0x%x",
p_read_result->char_uuid);
if (p_read_result->char_uuid == GATT_UUID_CHAR_REPORT)
{
app_ble_hid_host_read_feature_report_handle(conn_handle,
p_read_result->data.hids_report.p_hids_report_value,
p_read_result->data.hids_report.hids_report_value_len,
p_read_result->data.hids_report.report_id);
}
}
}
To send the HID device report to the ACI Host via UART path.
void app_ble_hid_host_read_feature_report_handle(uint16_t conn_handle, uint8_t *p_value,
uint16_t length, uint8_t report_id)
{
T_APP_LE_LINK *p_link;
p_link = app_link_find_le_link_by_conn_handle(conn_handle);
if (p_link != NULL)
{
APP_PRINT_INFO3("app_ble_hid_host_read_feature_report_handle: le_addr %s, conn_handle 0x%x, len %d",
TRACE_BDADDR(p_link->bd_addr), conn_handle, length);
struct
{
uint16_t conn_handle;
uint8_t report_id;
uint16_t pkt_len;
uint8_t payload[];
} __attribute__((packed)) *rpt = NULL;
rpt = calloc(1, length + sizeof(*rpt));
rpt->conn_handle = conn_handle;
rpt->report_id = report_id;
rpt->pkt_len = length;
memcpy(&rpt->payload, p_value, length);
app_report_event(CMD_PATH_UART, EVENT_BLE_HID_READ_REPORT, 0, (uint8_t *)rpt,
length + sizeof(*rpt));
free(rpt);
}
}
HID Input Report
After the HID device sends the report data, the HID host will receive the input reports.
T_APP_RESULT app_ble_hid_service_cback(uint16_t conn_handle, uint8_t type,
void *p_data)
{
if (type == GATT_MSG_HIDS_CLIENT_NOTIFY_IND)
{
T_HIDS_CLIENT_NOTIFY_DATA *p_notify_data = (T_HIDS_CLIENT_NOTIFY_DATA *)p_data;
APP_PRINT_TRACE3("app_ble_hid_service_cback: report id 0x%x, data length %d, data %b",
p_notify_data->report_id, p_notify_data->data_len,
TRACE_BINARY(p_notify_data->data_len, p_notify_data->p_data));
app_ble_hid_host_inreport_handle(conn_handle, p_notify_data->p_data,
p_notify_data->data_len, p_notify_data->report_id);
}
}
To send the HID input report data to the ACI Host via UART path.
void app_ble_hid_host_inreport_handle(uint16_t conn_handle, uint8_t *p_value,
uint16_t length, uint8_t report_id)
{
T_APP_LE_LINK *p_link;
p_link = app_link_find_le_link_by_conn_handle(conn_handle);
if (p_link != NULL)
{
APP_PRINT_INFO3("app_ble_hid_host_inreport_handle: le_addr %s, conn_handle 0x%x, len %d",
TRACE_BDADDR(p_link->bd_addr), conn_handle, length);
struct
{
uint16_t conn_handle;
uint8_t report_id;
uint16_t pkt_len;
uint8_t payload[];
} __attribute__((packed)) *rpt = NULL;
rpt = calloc(1, length + sizeof(*rpt));
rpt->conn_handle = conn_handle;
rpt->report_id = report_id;
rpt->pkt_len = length;
memcpy(&rpt->payload, p_value, length);
app_report_event(CMD_PATH_UART, EVENT_BLE_HID_IN_REPORT, 0, (uint8_t *)rpt,
length + sizeof(*rpt));
free(rpt);
}
}