| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090 |
- /**
- * Bluetooth A2DP Sink implementation for ESP32.
- *
- * Uses the ESP-IDF Bluedroid stack to receive A2DP audio (SBC codec,
- * decoded internally to PCM) and play it through the shared I2S output.
- *
- * Architecture:
- * BT stack → data callback → ringbuffer → I2S writer task → I2S DMA
- *
- * Integration with AirPlay:
- * - On BT connect: notify main → stop AirPlay services
- * - On BT disconnect: notify main → restart AirPlay services
- * - LED/display updates via rtsp_events (reuses the same event types)
- * - AVRCP metadata → RTSP_EVENT_METADATA for display
- */
- #include "a2dp_sink.h"
- #include "spiram_task.h"
- #include "audio_output.h"
- #include "dac.h"
- #include "dac_njw1195.h"
- #include "led.h"
- #include "rtsp_events.h"
- #include "settings.h"
- #include "panel_uart.h"
- #include "esp_a2dp_api.h"
- #include "esp_avrc_api.h"
- #include "esp_hf_client_api.h"
- #include "esp_bt.h"
- #include "esp_bt_device.h"
- #include "esp_bt_main.h"
- #include "esp_gap_bt_api.h"
- #include "esp_log.h"
- #include "esp_timer.h"
- #include "freertos/FreeRTOS.h"
- #include "freertos/queue.h"
- #include "freertos/ringbuf.h"
- #include "freertos/semphr.h"
- #include "freertos/task.h"
- #include <inttypes.h>
- #include <string.h>
- #include "playback_control.h"
- #include "audio_prompt.h"
- static const char *TAG = "bt_a2dp";
- /* ========================================================================== */
- /* Configuration */
- /* ========================================================================== */
- #define RINGBUF_SIZE ((size_t)64 * 1024)
- #define RINGBUF_PREFETCH (RINGBUF_SIZE * 6 / 10)
- #define BT_TASK_STACK 4096
- #define BT_TASK_PRIO (configMAX_PRIORITIES - 3)
- #define BT_TASK_QLEN 10
- #define I2S_TASK_STACK 2560
- #define I2S_TASK_PRIO 7
- #if CONFIG_FREERTOS_UNICORE
- #define I2S_TASK_CORE 0
- #else
- #define I2S_TASK_CORE 1
- #endif
- /* ========================================================================== */
- /* BT App Task Types */
- /* ========================================================================== */
- typedef void (*bt_app_cb_t)(uint16_t event, void *param);
- typedef struct {
- uint16_t sig;
- uint16_t event;
- bt_app_cb_t cb;
- void *param;
- } bt_app_msg_t;
- enum { BT_APP_SIG_WORK_DISPATCH = 0 };
- enum { BT_APP_EVT_STACK_UP = 0 };
- /* ========================================================================== */
- /* Ringbuffer mode */
- /* ========================================================================== */
- typedef enum {
- RINGBUF_MODE_PREFETCHING,
- RINGBUF_MODE_PROCESSING,
- RINGBUF_MODE_DROPPING,
- } ringbuf_mode_t;
- /* ========================================================================== */
- /* State */
- /* ========================================================================== */
- static bt_a2dp_state_cb_t s_state_cb = NULL;
- static volatile bool s_connected = false;
- static volatile bool s_hfp_connected = false;
- static volatile bool s_audio_started = false;
- static volatile bool s_avrc_playing = false; /* AVRCP play state (instant) */
- static volatile bool s_i2s_task_running = false;
- static bool s_bt_discoverable = true;
- static uint8_t s_avrc_volume = 64; /* 0-127, AVRCP absolute volume */
- static volatile bool s_vol_ntf_pending =
- false; /* phone registered for volume change */
- static int64_t s_last_local_vol_time = 0; /* 防止回音震荡的时间戳 */
- static esp_bd_addr_t s_reconnect_bda;
- static esp_bd_addr_t s_current_bda;
- static RingbufHandle_t s_ringbuf = NULL;
- static SemaphoreHandle_t s_i2s_sem = NULL;
- static ringbuf_mode_t s_ringbuf_mode = RINGBUF_MODE_PREFETCHING;
- static TaskHandle_t s_bt_task_handle = NULL;
- static TaskHandle_t s_i2s_task_handle = NULL;
- static QueueHandle_t s_bt_task_queue = NULL;
- static uint32_t s_sample_rate = 44100;
- /* ========================================================================== */
- /* BT App Task — context-switch from BT stack to app task */
- /* ========================================================================== */
- static bool bt_app_send_msg(bt_app_msg_t *msg) {
- if (s_bt_task_queue == NULL) {
- return false;
- }
- return xQueueSend(s_bt_task_queue, msg, pdMS_TO_TICKS(10)) == pdTRUE;
- }
- static bool bt_app_work_dispatch(bt_app_cb_t cb, uint16_t event, void *param,
- int param_len) {
- bt_app_msg_t msg = {
- .sig = BT_APP_SIG_WORK_DISPATCH,
- .event = event,
- .cb = cb,
- .param = NULL,
- };
- if (param_len > 0 && param != NULL) {
- msg.param = malloc((size_t)param_len);
- if (!msg.param) {
- return false;
- }
- memcpy(msg.param, param, (size_t)param_len);
- }
- if (!bt_app_send_msg(&msg)) {
- free(msg.param);
- return false;
- }
- return true;
- }
- static void bt_app_task(void *arg) {
- bt_app_msg_t msg;
- while (true) {
- if (xQueueReceive(s_bt_task_queue, &msg, portMAX_DELAY) == pdTRUE) {
- if (msg.sig == BT_APP_SIG_WORK_DISPATCH && msg.cb) {
- msg.cb(msg.event, msg.param);
- }
- free(msg.param);
- }
- }
- }
- /* ========================================================================== */
- /* I2S Writer Task — reads ringbuffer, writes to I2S */
- /* ========================================================================== */
- static void bt_i2s_writer_task(void *arg) {
- // Wait for ringbuffer to reach prefetch level
- xSemaphoreTake(s_i2s_sem, portMAX_DELAY);
- int16_t silence[256] = {0};
- while (s_i2s_task_running) {
- size_t item_size = 0;
- void *data =
- xRingbufferReceiveUpTo(s_ringbuf, &item_size, pdMS_TO_TICKS(20), 512);
- if (data != NULL && item_size > 0) {
- audio_output_write(data, item_size, portMAX_DELAY);
- vRingbufferReturnItem(s_ringbuf, data);
- } else {
- // Buffer underrun — write silence to keep I2S fed
- audio_output_write(silence, sizeof(silence), pdMS_TO_TICKS(10));
- }
- }
- s_i2s_task_handle = NULL;
- vTaskDelete(NULL);
- }
- static void i2s_task_start(void) {
- if (s_i2s_task_handle != NULL) {
- return;
- }
- s_ringbuf_mode = RINGBUF_MODE_PREFETCHING;
- if (s_ringbuf == NULL) {
- s_ringbuf = xRingbufferCreate(RINGBUF_SIZE, RINGBUF_TYPE_BYTEBUF);
- }
- if (s_i2s_sem == NULL) {
- s_i2s_sem = xSemaphoreCreateBinary();
- }
- s_i2s_task_running = true;
- xTaskCreatePinnedToCore(bt_i2s_writer_task, "bt_i2s", I2S_TASK_STACK, NULL,
- I2S_TASK_PRIO, &s_i2s_task_handle, I2S_TASK_CORE);
- }
- static void i2s_task_stop(void) {
- s_i2s_task_running = false;
- // Unblock the writer if it's waiting on the semaphore
- if (s_i2s_sem) {
- xSemaphoreGive(s_i2s_sem);
- }
- // Wait for the task to exit
- int timeout = 20;
- while (s_i2s_task_handle != NULL && timeout-- > 0) {
- vTaskDelay(pdMS_TO_TICKS(50));
- }
- // Flush and free the ringbuffer so heap is returned before AirPlay restarts
- if (s_ringbuf) {
- void *data;
- size_t size;
- while ((data = xRingbufferReceive(s_ringbuf, &size, 0)) != NULL) {
- vRingbufferReturnItem(s_ringbuf, data);
- }
- vRingbufferDelete(s_ringbuf);
- s_ringbuf = NULL;
- }
- if (s_i2s_sem) {
- vSemaphoreDelete(s_i2s_sem);
- s_i2s_sem = NULL;
- }
- // Reset sample rate to AirPlay default
- audio_output_set_sample_rate(44100);
- audio_output_flush();
- }
- /* ========================================================================== */
- /* A2DP data callback — BT stack context → ringbuffer */
- /* ========================================================================== */
- static void bt_a2dp_data_cb(const uint8_t *data, uint32_t len) {
- // Snapshot the ringbuf handle so it can't be freed between the NULL
- // check and the API calls (i2s_task_stop sets s_ringbuf = NULL).
- RingbufHandle_t rb = s_ringbuf;
- if (rb == NULL || len == 0) {
- return;
- }
- // Feed LED VU meter from the decoded PCM
- led_audio_feed((const int16_t *)data, len / 4);
- size_t free_size = xRingbufferGetCurFreeSize(rb);
- switch (s_ringbuf_mode) {
- case RINGBUF_MODE_PREFETCHING:
- xRingbufferSend(rb, data, len, 0);
- if ((RINGBUF_SIZE - free_size + len) >= RINGBUF_PREFETCH) {
- s_ringbuf_mode = RINGBUF_MODE_PROCESSING;
- xSemaphoreGive(s_i2s_sem);
- }
- break;
- case RINGBUF_MODE_PROCESSING:
- if (free_size < (size_t)len) {
- s_ringbuf_mode = RINGBUF_MODE_DROPPING;
- ESP_LOGW(TAG, "Ringbuf full, dropping");
- } else {
- xRingbufferSend(rb, data, len, 0);
- }
- break;
- case RINGBUF_MODE_DROPPING:
- if (free_size > RINGBUF_PREFETCH) {
- s_ringbuf_mode = RINGBUF_MODE_PROCESSING;
- xRingbufferSend(rb, data, len, 0);
- }
- break;
- }
- }
- /* ========================================================================== */
- /* A2DP event handler (runs in app task context) */
- /* ========================================================================== */
- static volatile bool s_reconnect_abort = false;
- static void bt_reconnect_task(void *arg) {
- ESP_LOGI(TAG, "蓝牙回连启动... 等待1秒");
- vTaskDelay(pdMS_TO_TICKS(1000));
- s_reconnect_abort = false;
- for (int i = 0; i < 60; i++) { // 60次 * 5秒 = 5分钟
- if (s_connected || s_reconnect_abort) {
- ESP_LOGI(TAG, "蓝牙已连接或回连被取消,回连结束。");
- break;
- }
-
- if (playback_control_get_source() == PLAYBACK_SOURCE_AIRPLAY) {
- ESP_LOGD(TAG, "AirPlay 正在使用中,暂缓蓝牙回连...");
- vTaskDelay(pdMS_TO_TICKS(5000));
- continue;
- }
- ESP_LOGI(TAG, "自动回连中... ( %d / 60 )", i + 1);
- esp_err_t err = esp_a2d_sink_connect(s_reconnect_bda);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "自动回连呼叫失败: %s", esp_err_to_name(err));
- }
- vTaskDelay(pdMS_TO_TICKS(5000));
- }
- ESP_LOGI(TAG, "回连重试任务结束。");
- vTaskDelete(NULL);
- }
- static void bt_a2dp_evt_handler(uint16_t event, void *param) {
- esp_a2d_cb_param_t *a2d = (esp_a2d_cb_param_t *)param;
- switch (event) {
- case ESP_A2D_CONNECTION_STATE_EVT: {
- esp_a2d_connection_state_t state = a2d->conn_stat.state;
- if (state == ESP_A2D_CONNECTION_STATE_CONNECTING) {
- ESP_LOGI(TAG, "A2DP connecting...");
- } else if (state == ESP_A2D_CONNECTION_STATE_CONNECTED) {
- ESP_LOGI(TAG, "A2DP connected");
- s_connected = true;
- audio_prompt_play(PROMPT_BT_CONNECTED);
- // Stop AirPlay and prepare I2S for Bluetooth
- playback_control_set_source(PLAYBACK_SOURCE_BLUETOOTH);
- memcpy(s_current_bda, a2d->conn_stat.remote_bda, sizeof(esp_bd_addr_t));
- settings_set_last_bt_bda(a2d->conn_stat.remote_bda);
- // Notify main app to disable AirPlay (stops playback task + RTSP)
- if (s_state_cb) {
- s_state_cb(true);
- }
- // Start BT playback
- i2s_task_start();
- // LED/display: show connected state
- rtsp_events_emit(RTSP_EVENT_CLIENT_CONNECTED, NULL);
- // Don't accept more connections while connected
- esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
- } else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTING) {
- ESP_LOGI(TAG, "A2DP disconnecting...");
- } else if (state == ESP_A2D_CONNECTION_STATE_DISCONNECTED) {
- ESP_LOGI(TAG, "A2DP disconnected");
- s_connected = false;
- s_audio_started = false;
- s_avrc_playing = false;
- audio_prompt_play(PROMPT_BT_DISCONNECTED);
- // Tell playback_control BT stopped streaming, so it can switch back to
- // AirPlay or Idle
- playback_control_set_source(PLAYBACK_SOURCE_NONE);
- // Stop BT playback
- i2s_task_stop();
- // LED/display: show disconnected state
- rtsp_events_emit(RTSP_EVENT_DISCONNECTED, NULL);
- // Notify main app to re-enable AirPlay (restarts playback + RTSP)
- if (s_state_cb) {
- s_state_cb(false);
- }
- // Discoverable state is managed by main via set_discoverable()
- // Persist the last-used volume to NVS once at disconnect
- settings_persist_bt_volume();
- }
- break;
- }
- case ESP_A2D_AUDIO_STATE_EVT: {
- esp_a2d_audio_state_t state = a2d->audio_stat.state;
- if (state == ESP_A2D_AUDIO_STATE_STARTED) {
- ESP_LOGI(TAG, "Audio stream started");
- s_audio_started = true;
- rtsp_events_emit(RTSP_EVENT_PLAYING, NULL);
- } else {
- ESP_LOGI(TAG, "Audio stream stopped/suspended");
- s_audio_started = false;
- rtsp_events_emit(RTSP_EVENT_PAUSED, NULL);
- }
- break;
- }
- case ESP_A2D_AUDIO_CFG_EVT: {
- ESP_LOGI(TAG, "Audio codec configured, type=%d", a2d->audio_cfg.mcc.type);
- if (a2d->audio_cfg.mcc.type == ESP_A2D_MCT_SBC) {
- // Use the new struct-based CIE accessors
- uint8_t sf = a2d->audio_cfg.mcc.cie.sbc_info.samp_freq;
- if (sf & ESP_A2D_SBC_CIE_SF_48K) {
- s_sample_rate = 48000;
- } else if (sf & ESP_A2D_SBC_CIE_SF_44K) {
- s_sample_rate = 44100;
- } else if (sf & ESP_A2D_SBC_CIE_SF_32K) {
- s_sample_rate = 32000;
- } else if (sf & ESP_A2D_SBC_CIE_SF_16K) {
- s_sample_rate = 16000;
- }
- ESP_LOGI(TAG, "SBC sample rate: %" PRIu32 " Hz", s_sample_rate);
- audio_output_set_sample_rate(s_sample_rate);
- }
- break;
- }
- case ESP_A2D_PROF_STATE_EVT:
- ESP_LOGI(TAG, "A2DP profile %s",
- a2d->a2d_prof_stat.init_state == ESP_A2D_INIT_SUCCESS
- ? "inited"
- : "deinited");
- if (a2d->a2d_prof_stat.init_state == ESP_A2D_INIT_SUCCESS) {
- if (settings_get_last_bt_bda(s_reconnect_bda) == ESP_OK) {
- ESP_LOGI(TAG, "准备蓝牙回连: %02x:%02x:%02x:%02x:%02x:%02x",
- s_reconnect_bda[0], s_reconnect_bda[1], s_reconnect_bda[2],
- s_reconnect_bda[3], s_reconnect_bda[4], s_reconnect_bda[5]);
- xTaskCreate(bt_reconnect_task, "bt_reconn", 3072, NULL, 5, NULL);
- }
- }
- break;
- default:
- break;
- }
- }
- /* A2DP callback (BT stack context) → dispatch to app task */
- static void bt_a2dp_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t *param) {
- bt_app_work_dispatch(bt_a2dp_evt_handler, (uint16_t)event, param,
- sizeof(esp_a2d_cb_param_t));
- }
- /* ========================================================================== */
- /* AVRCP Controller — get metadata from source device */
- /* ========================================================================== */
- static void bt_avrc_ct_evt_handler(uint16_t event, void *param) {
- esp_avrc_ct_cb_param_t *rc = (esp_avrc_ct_cb_param_t *)param;
- static rtsp_event_data_t meta_data;
- switch (event) {
- case ESP_AVRC_CT_CONNECTION_STATE_EVT:
- ESP_LOGI(TAG, "AVRC CT %s",
- rc->conn_stat.connected ? "connected" : "disconnected");
- if (rc->conn_stat.connected) {
- // Request track metadata
- esp_avrc_ct_send_metadata_cmd(
- 0, ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST |
- ESP_AVRC_MD_ATTR_ALBUM | ESP_AVRC_MD_ATTR_GENRE);
- // Request current play status (duration + position + play state)
- esp_avrc_ct_send_get_play_status_cmd(0);
- // Register for notifications: track change, play status, position
- esp_avrc_ct_send_register_notification_cmd(1, ESP_AVRC_RN_TRACK_CHANGE,
- 0);
- esp_avrc_ct_send_register_notification_cmd(
- 2, ESP_AVRC_RN_PLAY_STATUS_CHANGE, 0);
- esp_avrc_ct_send_register_notification_cmd(3,
- ESP_AVRC_RN_PLAY_POS_CHANGED,
- 10); // report every 10 seconds
- }
- break;
- case ESP_AVRC_CT_METADATA_RSP_EVT: {
- // Metadata response — one attribute per event.
- // attr_text was deep-copied in bt_avrc_ct_cb and must be freed here.
- uint8_t attr_id = rc->meta_rsp.attr_id;
- uint8_t *text = rc->meta_rsp.attr_text;
- int len = rc->meta_rsp.attr_length;
- if (text == NULL || len == 0) {
- free(text);
- break;
- }
- // Build metadata event for display
- char *dst = NULL;
- size_t dst_size = 0;
- switch (attr_id) {
- case ESP_AVRC_MD_ATTR_TITLE:
- dst = meta_data.metadata.title;
- dst_size = sizeof(meta_data.metadata.title);
- break;
- case ESP_AVRC_MD_ATTR_ARTIST:
- dst = meta_data.metadata.artist;
- dst_size = sizeof(meta_data.metadata.artist);
- break;
- case ESP_AVRC_MD_ATTR_ALBUM:
- dst = meta_data.metadata.album;
- dst_size = sizeof(meta_data.metadata.album);
- break;
- case ESP_AVRC_MD_ATTR_GENRE:
- dst = meta_data.metadata.genre;
- dst_size = sizeof(meta_data.metadata.genre);
- break;
- default:
- break;
- }
- if (dst) {
- size_t copy_len = (size_t)len < dst_size - 1 ? (size_t)len : dst_size - 1;
- memcpy(dst, text, copy_len);
- dst[copy_len] = '\0';
- ESP_LOGI(TAG, "Metadata attr %d: %s", attr_id, dst);
- // Emit metadata event after each attribute update
- rtsp_events_emit(RTSP_EVENT_METADATA, &meta_data);
- }
- free(text);
- break;
- }
- case ESP_AVRC_CT_PLAY_STATUS_RSP_EVT: {
- uint32_t duration_ms = rc->play_status_rsp.song_length;
- uint32_t position_ms = rc->play_status_rsp.song_position;
- esp_avrc_playback_stat_t status = rc->play_status_rsp.play_status;
- ESP_LOGI(TAG, "Play status: state=%d pos=%lums dur=%lums", status,
- (unsigned long)position_ms, (unsigned long)duration_ms);
- // Update duration and position in metadata for display
- // 0xFFFFFFFF means "not available" per AVRCP spec
- if (duration_ms != 0xFFFFFFFF) {
- meta_data.metadata.duration_secs = duration_ms / 1000;
- }
- if (position_ms != 0xFFFFFFFF) {
- meta_data.metadata.position_secs = position_ms / 1000;
- }
- rtsp_events_emit(RTSP_EVENT_METADATA, &meta_data);
- // Emit play state from AVRCP (faster than A2D audio state)
- if (status == ESP_AVRC_PLAYBACK_PLAYING) {
- s_avrc_playing = true;
- rtsp_events_emit(RTSP_EVENT_PLAYING, NULL);
- } else if (status == ESP_AVRC_PLAYBACK_PAUSED ||
- status == ESP_AVRC_PLAYBACK_STOPPED) {
- s_avrc_playing = false;
- rtsp_events_emit(RTSP_EVENT_PAUSED, NULL);
- }
- break;
- }
- case ESP_AVRC_CT_CHANGE_NOTIFY_EVT:
- if (rc->change_ntf.event_id == ESP_AVRC_RN_TRACK_CHANGE) {
- ESP_LOGI(TAG, "Track changed");
- // Clear old metadata so stale fields don't persist
- memset(&meta_data, 0, sizeof(meta_data));
- // Request new metadata and play status
- esp_avrc_ct_send_metadata_cmd(
- 0, ESP_AVRC_MD_ATTR_TITLE | ESP_AVRC_MD_ATTR_ARTIST |
- ESP_AVRC_MD_ATTR_ALBUM | ESP_AVRC_MD_ATTR_GENRE);
- esp_avrc_ct_send_get_play_status_cmd(0);
- // Re-register for track change
- esp_avrc_ct_send_register_notification_cmd(1, ESP_AVRC_RN_TRACK_CHANGE,
- 0);
- } else if (rc->change_ntf.event_id == ESP_AVRC_RN_PLAY_STATUS_CHANGE) {
- esp_avrc_playback_stat_t status = rc->change_ntf.event_parameter.playback;
- ESP_LOGI(TAG, "Play status changed: %d", status);
- // Refresh position/duration from source
- esp_avrc_ct_send_get_play_status_cmd(0);
- if (status == ESP_AVRC_PLAYBACK_PLAYING) {
- s_avrc_playing = true;
- rtsp_events_emit(RTSP_EVENT_PLAYING, NULL);
- } else if (status == ESP_AVRC_PLAYBACK_PAUSED ||
- status == ESP_AVRC_PLAYBACK_STOPPED) {
- s_avrc_playing = false;
- rtsp_events_emit(RTSP_EVENT_PAUSED, NULL);
- }
- // Re-register
- esp_avrc_ct_send_register_notification_cmd(
- 2, ESP_AVRC_RN_PLAY_STATUS_CHANGE, 0);
- } else if (rc->change_ntf.event_id == ESP_AVRC_RN_PLAY_POS_CHANGED) {
- uint32_t pos_ms = rc->change_ntf.event_parameter.play_pos;
- ESP_LOGD(TAG, "Play position: %lums", (unsigned long)pos_ms);
- if (pos_ms != 0xFFFFFFFF) {
- meta_data.metadata.position_secs = pos_ms / 1000;
- rtsp_events_emit(RTSP_EVENT_METADATA, &meta_data);
- }
- // Re-register
- esp_avrc_ct_send_register_notification_cmd(
- 3, ESP_AVRC_RN_PLAY_POS_CHANGED, 10);
- }
- break;
- default:
- break;
- }
- }
- static void bt_avrc_ct_cb(esp_avrc_ct_cb_event_t event,
- esp_avrc_ct_cb_param_t *param) {
- esp_avrc_ct_cb_param_t local = *param;
- // Deep-copy attr_text for metadata responses — the BT stack frees the
- // original buffer when this callback returns, so the shallow copy made
- // by bt_app_work_dispatch would leave a dangling pointer that the
- // handler later free()s, corrupting the heap.
- //
- // Always overwrite local.meta_rsp.attr_text so the stack-owned pointer
- // never escapes this function: on success it points at our malloc'd
- // copy, on failure (zero-length, malloc returns NULL, or any non-
- // metadata event that happens to alias the union) it is NULL. The
- // handler does free(text) unconditionally, and free(NULL) is a no-op.
- if (event == ESP_AVRC_CT_METADATA_RSP_EVT) {
- uint8_t *copy = NULL;
- if (param->meta_rsp.attr_text && param->meta_rsp.attr_length > 0) {
- copy = malloc((size_t)param->meta_rsp.attr_length + 1);
- if (copy) {
- memcpy(copy, param->meta_rsp.attr_text,
- (size_t)param->meta_rsp.attr_length);
- copy[param->meta_rsp.attr_length] = '\0';
- }
- }
- local.meta_rsp.attr_text = copy;
- }
- bt_app_work_dispatch(bt_avrc_ct_evt_handler, (uint16_t)event, &local,
- sizeof(esp_avrc_ct_cb_param_t));
- }
- /* ========================================================================== */
- /* AVRCP Target — handle volume commands from source device */
- /* ========================================================================== */
- static void bt_avrc_tg_evt_handler(uint16_t event, void *param) {
- esp_avrc_tg_cb_param_t *tg = (esp_avrc_tg_cb_param_t *)param;
- switch (event) {
- case ESP_AVRC_TG_CONNECTION_STATE_EVT:
- ESP_LOGI(TAG, "AVRC TG %s",
- tg->conn_stat.connected ? "connected" : "disconnected");
- break;
- case ESP_AVRC_TG_SET_ABSOLUTE_VOLUME_CMD_EVT: {
- uint8_t volume = tg->set_abs_vol.volume; // 0-127
-
- // 始终更新本地保存的手机端音量,防止状态脱节导致后续面板调节时不发通知
- s_avrc_volume = volume;
-
- // Ignore incoming command if it arrives within 500ms of a manual
- // volume change to prevent echo-rollback.
- if (esp_timer_get_time() - s_last_local_vol_time < 500000) {
- ESP_LOGI(TAG, "Ignore echo absolute volume: %d/127", volume);
- break;
- }
-
- ESP_LOGI(TAG, "Set absolute volume: %d/127", volume);
- uint8_t pct = (volume * 100) / 127;
- dac_njw1195_set_vol_main(pct);
- break;
- }
- case ESP_AVRC_TG_REGISTER_NOTIFICATION_EVT:
- if (tg->reg_ntf.event_id == ESP_AVRC_RN_VOLUME_CHANGE) {
- // Respond with current volume (interim response)
- esp_avrc_rn_param_t rn_param;
- rn_param.volume = s_avrc_volume;
- esp_avrc_tg_send_rn_rsp(ESP_AVRC_RN_VOLUME_CHANGE,
- ESP_AVRC_RN_RSP_INTERIM, &rn_param);
- s_vol_ntf_pending = true;
- }
- break;
- default:
- break;
- }
- }
- static void bt_avrc_tg_cb(esp_avrc_tg_cb_event_t event,
- esp_avrc_tg_cb_param_t *param) {
- bt_app_work_dispatch(bt_avrc_tg_evt_handler, (uint16_t)event, param,
- sizeof(esp_avrc_tg_cb_param_t));
- }
- /* ========================================================================== */
- /* GAP callback */
- /* ========================================================================== */
- static void bt_gap_cb(esp_bt_gap_cb_event_t event,
- esp_bt_gap_cb_param_t *param) {
- switch (event) {
- case ESP_BT_GAP_ACL_DISCONN_CMPL_STAT_EVT: {
- uint8_t reason = param->acl_disconn_cmpl_stat.reason;
- ESP_LOGI(TAG, "ACL Disconnected. Reason: 0x%02x", reason);
-
- // HCI Reason 0x13: Remote User Terminated Connection
- // HCI Reason 0x14: Remote Device Terminated Connection due to Low Resources
- // HCI Reason 0x15: Remote Device Terminated Connection due to Power Off
- // HCI Reason 0x16: Connection Terminated By Local Host (本地主动断开)
- if (reason == 0x13 || reason == 0x14 || reason == 0x15 || reason == 0x16) {
- ESP_LOGI(TAG, "主动断开,设备可被发现 。");
- s_reconnect_abort = true;
- esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
- } else {
- ESP_LOGI(TAG, "意外断开,尝试回连5min...");
- if (settings_get_last_bt_bda(s_reconnect_bda) == ESP_OK) {
- xTaskCreate(bt_reconnect_task, "bt_reconn", 3072, NULL, 5, NULL);
- }
- }
- break;
- }
- case ESP_BT_GAP_AUTH_CMPL_EVT:
- if (param->auth_cmpl.stat == ESP_BT_STATUS_SUCCESS) {
- ESP_LOGI(TAG, "Auth success: %s", param->auth_cmpl.device_name);
- } else {
- ESP_LOGW(TAG, "Auth failed: status=%d", param->auth_cmpl.stat);
- }
- break;
- case ESP_BT_GAP_CFM_REQ_EVT:
- ESP_LOGI(TAG, "SSP confirm request (num=%" PRIu32 "), auto-accepting",
- param->cfm_req.num_val);
- esp_bt_gap_ssp_confirm_reply(param->cfm_req.bda, true);
- break;
- case ESP_BT_GAP_PIN_REQ_EVT:
- ESP_LOGI(TAG, "Legacy PIN request");
- break;
- case ESP_BT_GAP_KEY_NOTIF_EVT:
- ESP_LOGI(TAG, "SSP passkey notify: %" PRIu32, param->key_notif.passkey);
- break;
- case ESP_BT_GAP_KEY_REQ_EVT:
- ESP_LOGI(TAG, "SSP passkey request");
- break;
- case ESP_BT_GAP_MODE_CHG_EVT:
- ESP_LOGD(TAG, "GAP mode changed to %d", param->mode_chg.mode);
- break;
- default:
- break;
- }
- }
- /* ========================================================================== */
- /* HFP Client callback */
- /* ========================================================================== */
- static void bt_hf_client_cb(esp_hf_client_cb_event_t event, esp_hf_client_cb_param_t *param) {
- switch (event) {
- case ESP_HF_CLIENT_CONNECTION_STATE_EVT:
- if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_SLC_CONNECTED) {
- ESP_LOGI(TAG, "HFP Client SLC connected");
- s_hfp_connected = true;
- // 注册苹果专属的电量上报和底座状态特性
- esp_hf_client_send_xapl("0505-1995-0610", ESP_HF_CLIENT_XAPL_FEAT_BATTERY_REPORT | ESP_HF_CLIENT_XAPL_FEAT_DOCKED);
- // 立刻上报一次当前电量
- bt_a2dp_send_battery_level(panel_uart_get_battery_percent());
- } else if (param->conn_stat.state == ESP_HF_CLIENT_CONNECTION_STATE_DISCONNECTED) {
- ESP_LOGI(TAG, "HFP Client disconnected");
- s_hfp_connected = false;
- }
- break;
- default:
- break;
- }
- }
- /* ========================================================================== */
- /* Stack-up handler — called when Bluedroid is (re-)enabled */
- /* ========================================================================== */
- static void bt_stack_evt_handler(uint16_t event, void *param) {
- (void)param;
- if (event != BT_APP_EVT_STACK_UP) {
- return;
- }
- ESP_LOGI(TAG, "BT stack up, initializing profiles");
- // GAP
- esp_bt_gap_register_callback(bt_gap_cb);
- #ifdef CONFIG_BT_SSP_ENABLED
- // Secure Simple Pairing — Just Works (No Input No Output)
- esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE;
- esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_NONE;
- esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t));
- #endif
- // Legacy Pairing — fixed PIN from Kconfig
- esp_bt_pin_type_t pin_type = ESP_BT_PIN_TYPE_FIXED;
- esp_bt_pin_code_t pin_code;
- const char *pin_str = CONFIG_BT_PIN_CODE;
- uint8_t pin_len = strlen(pin_str);
- if (pin_len > ESP_BT_PIN_CODE_LEN) {
- pin_len = ESP_BT_PIN_CODE_LEN;
- }
- memcpy(pin_code, pin_str, pin_len);
- esp_bt_gap_set_pin(pin_type, pin_len, pin_code);
- ESP_LOGI(TAG, "BT PIN set (%d digits)", pin_len);
- // AVRC Controller (to get metadata from source)
- esp_avrc_ct_register_callback(bt_avrc_ct_cb);
- esp_avrc_ct_init();
- // AVRC Target (to receive volume commands)
- esp_avrc_tg_register_callback(bt_avrc_tg_cb);
- esp_avrc_tg_init();
-
- esp_avrc_rn_evt_cap_mask_t evt_set = {0};
- esp_avrc_rn_evt_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &evt_set, ESP_AVRC_RN_VOLUME_CHANGE);
- esp_avrc_tg_set_rn_evt_cap(&evt_set);
- // Set supported passthrough commands (iOS requires VOL_UP/VOL_DOWN to enable Absolute Volume)
- esp_avrc_psth_bit_mask_t cmd_set = {0};
- esp_avrc_psth_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &cmd_set, ESP_AVRC_PT_CMD_VOL_UP);
- esp_avrc_psth_bit_mask_operation(ESP_AVRC_BIT_MASK_OP_SET, &cmd_set, ESP_AVRC_PT_CMD_VOL_DOWN);
- esp_avrc_tg_set_psth_cmd_filter(ESP_AVRC_PSTH_FILTER_SUPPORTED_CMD, &cmd_set);
- // A2DP Sink
- esp_a2d_register_callback(bt_a2dp_cb);
- esp_a2d_sink_register_data_callback(bt_a2dp_data_cb);
- esp_a2d_sink_init();
- // HFP Client
- esp_hf_client_register_callback(bt_hf_client_cb);
- esp_hf_client_init();
- // Apply saved discoverable state
- if (s_bt_discoverable) {
- esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
- } else {
- esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
- }
- // Force Class of Device AFTER A2DP and HFP are initialized
- // A2DP and HFP initializations internally reset the CoD, so we must set it here.
- esp_bt_cod_t cod;
- memset(&cod, 0, sizeof(cod));
- cod.major = 4; // Audio/Video
- cod.minor = 5; // Loudspeaker
- cod.service = (1 << 8) | (1 << 5); // Audio (bit 21) + Rendering (bit 18)
- esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_MAJOR_MINOR);
- esp_bt_gap_set_cod(cod, ESP_BT_SET_COD_SERVICE_CLASS);
- {
- uint8_t saved_vol;
- if (settings_get_bt_volume(&saved_vol) == ESP_OK) {
- s_avrc_volume = saved_vol;
- }
- ESP_LOGI(TAG, "BT volume restored from settings: %d/127", s_avrc_volume);
- }
- ESP_LOGI(TAG, "BT A2DP sink ready, waiting for connections");
- }
- /* ========================================================================== */
- /* Public API */
- /* ========================================================================== */
- esp_err_t bt_a2dp_sink_init(const char *device_name,
- bt_a2dp_state_cb_t state_cb) {
- s_state_cb = state_cb;
- ESP_LOGI(TAG, "Initializing Bluetooth A2DP Sink: %s", device_name);
- // Release BLE memory — we only need Classic BT
- ESP_ERROR_CHECK(esp_bt_controller_mem_release(ESP_BT_MODE_BLE));
- // Initialize BT controller
- esp_bt_controller_config_t bt_cfg = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
- esp_err_t err = esp_bt_controller_init(&bt_cfg);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "BT controller init failed: %s", esp_err_to_name(err));
- return err;
- }
- err = esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "BT controller enable failed: %s", esp_err_to_name(err));
- return err;
- }
- // Initialize Bluedroid
- esp_bluedroid_config_t bluedroid_cfg = BT_BLUEDROID_INIT_CONFIG_DEFAULT();
- #ifndef CONFIG_BT_SSP_ENABLED
- bluedroid_cfg.ssp_en = false; // Disable SSP to force legacy PIN pairing
- #endif
- err = esp_bluedroid_init_with_cfg(&bluedroid_cfg);
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "Bluedroid init failed: %s", esp_err_to_name(err));
- return err;
- }
- err = esp_bluedroid_enable();
- if (err != ESP_OK) {
- ESP_LOGE(TAG, "Bluedroid enable failed: %s", esp_err_to_name(err));
- return err;
- }
- // Set device name
- esp_bt_gap_set_device_name(device_name);
- // Create the BT app task and event queue
- s_bt_task_queue = xQueueCreate(BT_TASK_QLEN, sizeof(bt_app_msg_t));
- if (s_bt_task_queue == NULL) {
- ESP_LOGE(TAG, "Failed to create BT task queue");
- return ESP_ERR_NO_MEM;
- }
- BaseType_t ret =
- task_create_spiram(bt_app_task, "bt_app", BT_TASK_STACK, NULL,
- BT_TASK_PRIO, &s_bt_task_handle, NULL);
- if (ret != pdPASS) {
- ESP_LOGE(TAG, "Failed to create BT app task");
- return ESP_ERR_NO_MEM;
- }
- // Dispatch stack-up event to app task
- bt_app_work_dispatch(bt_stack_evt_handler, BT_APP_EVT_STACK_UP, NULL, 0);
- return ESP_OK;
- }
- bool bt_a2dp_sink_is_connected(void) {
- return s_connected;
- }
- void bt_a2dp_sink_disconnect_current(void) {
- esp_bd_addr_t bda;
- bool has_bda = false;
-
- if (s_connected) {
- memcpy(bda, s_current_bda, sizeof(esp_bd_addr_t));
- has_bda = true;
- } else {
- if (settings_get_last_bt_bda(bda) == ESP_OK) {
- has_bda = true;
- }
- }
- if (has_bda) {
- ESP_LOGI(TAG, "Forcing disconnect of BT device: %02X:%02X:%02X:%02X:%02X:%02X",
- bda[0], bda[1], bda[2], bda[3], bda[4], bda[5]);
- // 强制断开 A2DP (音频流)
- esp_a2d_sink_disconnect(bda);
- // 强制断开 HFP (免提/电量报告通道,手机常常因为这个不断开而显示“已连接”)
- esp_hf_client_disconnect(bda);
- } else {
- ESP_LOGW(TAG, "No BT device known to disconnect");
- }
- }
- void bt_a2dp_sink_set_discoverable(bool discoverable) {
- s_bt_discoverable = discoverable;
- if (s_connected) {
- return;
- }
- if (discoverable) {
- ESP_LOGI(TAG, "BT discoverable enabled");
- esp_bt_gap_set_scan_mode(ESP_BT_CONNECTABLE, ESP_BT_GENERAL_DISCOVERABLE);
- } else {
- ESP_LOGI(TAG, "BT discoverable disabled");
- esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
- }
- }
- // ============================================================================
- // AVRCP passthrough commands for hardware button control
- // ============================================================================
- static void send_passthrough(uint8_t key_code) {
- if (!s_connected) {
- return;
- }
- esp_avrc_ct_send_passthrough_cmd(0, key_code, ESP_AVRC_PT_CMD_STATE_PRESSED);
- vTaskDelay(pdMS_TO_TICKS(50));
- esp_avrc_ct_send_passthrough_cmd(0, key_code, ESP_AVRC_PT_CMD_STATE_RELEASED);
- }
- void bt_a2dp_send_playpause(void) {
- if (s_avrc_playing) {
- send_passthrough(ESP_AVRC_PT_CMD_PAUSE);
- ESP_LOGI(TAG, "AVRCP: pause");
- } else {
- send_passthrough(ESP_AVRC_PT_CMD_PLAY);
- ESP_LOGI(TAG, "AVRCP: play");
- }
- }
- void bt_a2dp_send_next(void) {
- send_passthrough(ESP_AVRC_PT_CMD_FORWARD);
- ESP_LOGI(TAG, "AVRCP: next");
- }
- void bt_a2dp_send_prev(void) {
- send_passthrough(ESP_AVRC_PT_CMD_BACKWARD);
- ESP_LOGI(TAG, "AVRCP: prev");
- }
- /**
- * Notify the source (phone) that local volume changed via AVRCP TG.
- * Only sends if the phone previously registered for the notification.
- */
- static void notify_volume_changed(void) {
- if (s_vol_ntf_pending) {
- esp_avrc_rn_param_t rn_param;
- rn_param.volume = s_avrc_volume;
- esp_avrc_tg_send_rn_rsp(ESP_AVRC_RN_VOLUME_CHANGE, ESP_AVRC_RN_RSP_CHANGED,
- &rn_param);
- s_vol_ntf_pending = false;
- }
- }
- void bt_a2dp_send_volume_up(void) {
- // Adjust local AVRCP volume by ~10 units (≈3 dB equivalent)
- uint8_t new_vol = s_avrc_volume;
- if (new_vol <= 117) {
- new_vol += 10;
- } else {
- new_vol = 127;
- }
- s_avrc_volume = new_vol;
- s_last_local_vol_time = esp_timer_get_time();
- float volume_db = ((float)new_vol / 127.0f) * 30.0f - 30.0f;
- dac_set_volume(volume_db);
- notify_volume_changed();
- ESP_LOGI(TAG, "AVRCP: volume up -> %d/127", new_vol);
- }
- void bt_a2dp_send_volume_down(void) {
- uint8_t new_vol = s_avrc_volume;
- if (new_vol >= 10) {
- new_vol -= 10;
- } else {
- new_vol = 0;
- }
- s_avrc_volume = new_vol;
- s_last_local_vol_time = esp_timer_get_time();
- float volume_db = ((float)new_vol / 127.0f) * 30.0f - 30.0f;
- dac_set_volume(volume_db);
- notify_volume_changed();
- ESP_LOGI(TAG, "AVRCP: volume down -> %d/127", new_vol);
- }
- void bt_a2dp_set_volume(uint8_t volume) {
- if (volume > 127) {
- volume = 127;
- }
- if (s_avrc_volume != volume) {
- s_avrc_volume = volume;
- s_last_local_vol_time = esp_timer_get_time();
- float volume_db = ((float)volume / 127.0f) * 30.0f - 30.0f;
- dac_set_volume(volume_db);
- notify_volume_changed();
- }
- }
- void bt_a2dp_send_battery_level(uint8_t percent) {
- if (!s_hfp_connected) {
- return;
- }
- if (percent > 100) percent = 100;
- // Convert 0-100 to 0-9
- uint8_t bat_level = (percent + 5) / 10;
- if (bat_level > 9) bat_level = 9;
-
- ESP_LOGI(TAG, "Report battery to iOS: %d%% -> level %d", percent, bat_level);
- esp_hf_client_send_iphoneaccev(bat_level, false);
- }
|