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:

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:

  1. Call the ram_config() API to configure DSP to share 80KB RAM with MCU.

  2. Call the log_module_trace_set() API to disable some trace logs during system initialization.

  3. Call the APP_PRINT_INFO2() API to print the wake-up reason, compile time, and restart reason.

  4. Call the os_msg_queue_create() API to create a message and event queue for task communication.

  5. Call the app_init_timer() API to initialize APP timer.

  6. Call the pm_cpu_freq_init() API to initialize CPU frequency module.

  7. Call the app_cfg_init() API to initialize the configuration.

  8. Call the board_init() API to initialize PINMUX and pad settings.

  9. Call the app_bt_gap_init() API to initialize GAP parameters of Bluetooth and Bluetooth Low Energy.

  10. 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.

  11. Call the app_ota_init() API to initialize OTA.

  12. 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.

  13. Call the app_auto_power_off_init() API to initialize the auto power off module.

  14. Call the app_audio_init() API to initialize audio, such as initializing EQ, setting VP language, setting volume, etc.

  15. Call the app_mmi_init() API to initialize MMI.

  16. Call the app_test_init() API to initialize test.

  17. Call the app_gap_init() API to initialize GAP.

  18. Call the app_ble_gap_init() API to initialize Bluetooth Low Energy GAP.

  19. Call the app_bt_policy_init() API to initialize Bluetooth policy.

  20. Call the app_ble_client_init() API to initialize LE client.

  21. Call the gatt_svc_init() API to initialize GATT service.

  22. 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.

  23. 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.

  24. Call the app_lea_acc_profile_init() API to initialize LE Audio service and profiles.

  25. Call the os_task_create() API to create a new task and add it to the list of tasks that are ready to run.

  26. 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)

DSP Share Memory

#define DSP_SHM_TOTAL_SIZE     80 * 1024
#define DSP_SHM_GLOBAL_SIZE    10 * 1024
#define DSP_SHM_HEAP_ADDR      DSP_SHM_GLOBAL_ADDR + DSP_SHM_GLOBAL_SIZE
#define DSP_SHM_HEAP_SIZE      (DSP_SHM_TOTAL_SIZE - DSP_SHM_GLOBAL_SIZE)

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:

  1. 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.

    ../../../_images/bt_audio_trx_use_gpio_pin_step_1.png

    Use GPIO Pin 1

  2. Mute voice prompts and tones.

    Open the macro of F_APP_DISABLE_NOTIFICATION_SUPPORT in app_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
    
  3. Set AVCC driver.

    Set AVCC driver active by clicking System Configuration ‣ AVCCDRV always on ‣ Always active.

    ../../../_images/bt_audio_trx_use_gpio_pin_step_3.png

    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.

DMA Channel

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
  1. 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
        ...
    }
    
  2. 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.

../../../_images/bt_audio_trx_key_table.png

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.

../../../_images/bt_audio_trx_hybrid_key_table.png

Hybrid Key Table

For example, configure the following:

  • Trigger MMI_DEV_MIC_VOL_DOWN by operating HYBRID_TYPE_0 (double-clicking KEY1) in the CALL_IDLE status.

  • Trigger MMI_START_ROLESWAP by operating HYBRID_TYPE_1 (triple-clicking KEY1) in the CALL_IDLE status.

  • Trigger MMI_DEV_MIC_VOL_DOWN by simultaneously short pressing KEY1 and KEY2 (HYBRID_TYPE_2) while in the CALL_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:

  1. Create an advertisement.

    Go through the command CMD_XM_LE_ADV_CREATE in app_cmd_ble.c, it calls static function adv_create. The adv_create function calls ble_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 in p_elem to ble.advs.q. Handle is the most important member of p_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;
    }
    
  2. Start an advertisement.

    Glance over the CMD_XM_LE_START_ADVERTISING command, and you will find a wrapper function called adv_start, which requires a handle and triggers ble_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;
    }
    
  3. Check the state of an advertisement.

    As the description above, adv_callback registered by ble_ext_adv_mgr_register_callback() will check advertisement states from the lower layer. In the case of BLE_EXT_ADV_STATE_CHANGE, it gets the advertising state from cb_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 in p_elem in ble.advs.q.

    if (p_adv_elem)
    {
        p_adv_elem->state = cb_data.p_ble_state_change->state;
        ......
    }
    
  4. 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:

  1. 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 as conn_interval, conn_latency and sup_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));
    }
    
  2. 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, &param, NULL))
        {
            app_lea_clear_scan_dev();
        }
    }
    

    The scan results are reported in the callback registered by ble_scan_start. The scan information is defined in BLE_SCAN_EVT_DATA.

    typedef union
    {
        T_LE_EXT_ADV_REPORT_INFO *report;
    } BLE_SCAN_EVT_DATA;
    
  3. 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 in CMD_LE_CREATE_CONN initiates a connection and the key parameter is bd_addr received from T_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.

../../../_images/bt_audio_trx_ringtone_index.png

Ringtone Index

../../../_images/bt_audio_trx_VP_index.png

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, &param, 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:

  1. app_lea_ini_select_media_prefer_codec() API to choose the codec type.

  2. bap_unicast_audio_cfg() API to set unicast audio configuration.

  3. app_lea_ini_config_codec() API to configure ASE codec.

  4. 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:

  1. app_lea_ini_select_conversation_prefer_codec() API to choose the codec type.

  2. bap_unicast_audio_cfg() API to set unicast audio configuration.

  3. app_lea_ini_config_codec() API to configure ASE codec.

  4. 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], &param->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 = &param->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, &param, 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,
                                &timestamp,
                                &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:

  1. 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 is app_spi_master_init(). For Device 2, its SPI role is set as a slave, and the relevant functionality is enabled by activating the F_APP_SPI_ROLE_SLAVE flag. The initialization function is app_spi_slave_init().

  2. 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 the CMD_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 the A2DP FORMAT command, the corresponding content is stored in the a2dp_xmit_mgr.a2dp_in_format variable, which will be utilized by the Audio Pipe operations. Additionally, the a2dp_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);
    }
    
  3. 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 the app_a2dp_xmit_lea.c file. The data processing functions for these outputs are app_a2dp_xmit_src_handle_a2dp_data_ind() and app_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 the app_a2dp_xmit_src_handle_a2dp_data_ind() function or the app_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 command CMD_A2DP_XMIT_ROUTE_OUT_CTRL through ACI Host CLI Tool. The Bluetooth Audio Transceiver APP will then store this choice in the variable a2dp_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
  1. 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 the XMIT_PLAY_STATE_START parameter, the app_a2dp_xmit_src_stream_start_req() function initiates an AVDTP_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 the a2d_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);
    }
    
  2. 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;
    }
    
  3. Fill data to Audio Pipe.

    When creating the Audio Pipe, the audio_pipe_callback() function is registered. Upon receiving the AUDIO_PIPE_EVENT_STARTED, it indicates that the Audio Pipe is ready to operate. In this event, the app_a2dp_xmit_src_fill_pipe() function is used to fill the first packet of data to the Audio Pipe. After completing the filling, an AUDIO_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, the flag_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();
            }
        }
    }
    
  4. 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 in audio_pipe_cllback(). Upon receiving this event, the transformed audio data can be obtained using the audio pipe drain() function and subsequently sent to the headphones with the bt_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;
    }
    
  5. 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 the XMIT_PLAY_STATE_IDLE parameter. This action will initiate an AVDTP_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
  1. 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 the CMD_A2DP_XMIT_ROUTE_OUT_CTRL command with the XMIT_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);
        }
    }
    
  2. 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);
        }
    }
    
  3. Fill data to Audio Pipe.

    When creating the Audio Pipe, the app_a2dp_xmit_lea_pipe_callback() function is registered. Upon receiving the AUDIO_PIPE_EVENT_STARTED, it indicates that the Audio Pipe is ready to operate. In this event, the app_a2dp_xmit_lea_fill_pipe() function is used to fill the first packet of data to Audio Pipe. After completing the filling, an AUDIO_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();
            }
        }
    }
    
  4. 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 in app_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 of a2dp_xmit_lea_ctrl.ring_buf using the function of app_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 the app_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);
    }
    
  5. 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 the XMIT_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() and stream_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 *)&param->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.

../../../_images/bt_audio_trx_uart_dfu_process.png

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.

../../../_images/bt_audio_trx_uart_dfu_packet.png

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,
  1. 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.

  2. 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, carrying offset and length 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.

  3. 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

  4. 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

  5. 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

  6. 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

  7. 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 with UART_DFU_ACCEPT (0x01), otherwise, it replies with UART_DFU_REJECT (0x00).

    Format table.

    Start Response

    Byte 0~1

    Byte 2

    0x3160

    Response State

    Byte 2: UART_DFU_ACCEPT (0x01) or UART_DFU_REJECT (0x00)

  8. 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.

  9. 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 the UART_DFU_SUCCESS status.

    Format table.

    DFU Result

    Byte 0~1

    Byte 2

    0x3162

    Status

    Byte 2: UART_DFU_SUCCESS (0x01) or UART_DFU_FAIL (0x00)

  10. 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
../../../_images/bt_audio_trx_uart_dfu_with_external_mcu.png

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.

../../../_images/bt_audio_trx_acoustics_mp_flow.png

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
  1. 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) or HW_EQ_ENTER_TEST_MODE (0x01).

  2. 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, and 0x03 means parameter error.

Start Set MP EQ Info
  1. 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.

  2. 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, and 0x05 means process fail. Byte 3~4: Max_packet_size. The current setting is 500 bytes, which is MAX_EQ_TOOL_CMD_LENGTH in macro definition.

Continue Set MP EQ Info
  1. 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 following T_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;
    
  2. 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, and 0x05 means process fail.

Set Test Mode Temp EQ Advanced
  1. 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.

  2. 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 and 0x05 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:

../../../_images/four_roles.png

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.

../../../_images/findmy_file_path.png

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.

../../../_images/mbedtls_config_file_path.png

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 System Configuration ‣ Watchdog Timeout (s) ‣ 8.

../../../_images/MCUConfigTool_wdt.png

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.

../../../_images/no_erase_token.png

No Erase Token

Download the token:

  1. To generate the token.bin for downloading, open TOKEN_DECODE.exe located in the sdk\tool\token_decode path. Then, input the base64 encoded token string and the corresponding UUID released by Apple.

  2. Copy the token.bin and mp_bkupdata2.ini files to the path of sdk\tool\Gadgets.

  3. 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.

  4. 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.

    ../../../_images/configure_flash_token.png

    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.

../../../_images/findmystate.png

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:

  1. The accessory is paired and starts up from a reset, power cycle, or other reinitialization procedure.

  2. 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.

../../../_images/playsound.png

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.

../../../_images/stopsound.png

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
  1. 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]
    
  2. 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
    
  3. 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:

  1. Press the Add New Item button to enroll a new Tag device.

  2. Press the Other Supported Items button to start scanning for the pairing advertising sent by the accessory.

  3. 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.

  4. Press the Continue button, enter the name, and set the icon to complete the pairing session.

../../../_images/findmyprocess.png

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:

  1. View location and remove the accessory by clicking Remove.

    ../../../_images/view_location_removing_accessory.png

    View Location

  2. Try Crowd sourced locations ‣ Move around with the accessory (Ensure other iOS devices running iOS 13 or later are in the vicinity).

    ../../../_images/Apply_the_crowd_finding_with_the_accessory.png

    Try Crowd-sourced Locations

  3. Play sound.

    ../../../_images/Ring_the_accessory.png

    Play Sound

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

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 to T_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.

../../../_images/gfps_finder_add_inc_files.png

Adding GFPS Finder Include Files

../../../_images/gfps_finder_app_add_files.png

Adding GFPS Finder APP Files

../../../_images/gfps_lib_rtl87x3e.png

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 Options for File ‘gfps.lib’ ‣ Include in Target Build.

Building GFPS Library

Building GFPS Library

Enable build gfps in Keil project: Find the folder gfps in Keil project then right click the Options for Group ‘gfps’ ‣ Include in Target Build.

Building GFPS Project

Building GFPS Project

GFPS Finder Features

This chapter introduces GFPS Finder support features:

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.

../../../_images/GFPS_Finder_Provisioning.png

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.

../../../_images/GFPS_Finder_Ring.png

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.

../../../_images/GFPS_Finder_Active_UTP_Mode.png

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.

../../../_images/GFPS_Finder_Unprovision.png

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

  1. 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.

  2. Click the Connect button on the popup window to connect.

    ../../../_images/Test_with_FMD_1.png

    Connect Button Popup

  3. Agree to user responsibility by clicking Agree and continue and Enter screen lock.

    ../../../_images/Test_with_FMD_2.png

    Agree to User Responsibility and Screen Lock

  4. Add the device to the Find My Device APP.

    ../../../_images/Test_with_FMD_3.png

    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:

One Wire UART Configuration

This chapter introduces how to enable one wire UART:

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

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

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:

  1. 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);
            }
        }
    }
    
  2. If the command format is different from current in the code, users can fix the command format in app_report_uart_event() or call app_push_data_transfer_queue(). The parameter index represents UART index. The function includes flow control in the APP.

  3. 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.

../../../_images/USB_Connect_picture.png

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.

../../../_images/USB_msc_picture.png

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.

../../../_images/Boot_Host_HID_Device_Relationship.png

Boot Host and HID Device Roles/Service Relationship

../../../_images/Report_Host_HID_Device_Relationships.png

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.

../../../_images/hid_host_hid_device_Interaction.png

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);
    }
}