这些小活动你都参加了吗?快来围观一下吧!>>
电子产品世界 » 论坛首页 » DIY与开源设计 » 电子DIY » [ESP32-S31Functioncoreboard-1测评]LLM控制蓝牙

共1条 1/1 1 跳转至页

[ESP32-S31Functioncoreboard-1测评]LLM控制蓝牙

助工
2026-10-19 09:43:12     打赏

        上一篇[ESP32-S31Functioncoreboard-1测评]重构环境并实现ESP-CLAW与蓝牙共存-电子产品世界论坛实现了esp-claw中加入蓝牙音频功能,现在采用Skill + Capability 组合模式,通过agent来控制蓝牙来实现对手机蓝牙的连接、断开、播放和暂停等操作。

capability分工.png

        框架为:

Skill 层(LLM 可见的“用户功能”)
    └── SKILL.md(bt_audio)
            │ metadata.cap_groups: ["cap_bt_audio"]
            ▼
Capability 层(LLM 可调用的“内部工具”)
    └── cap_bt_audio
            ├── bt_audio_play
            ├── bt_audio_pause
            ├── bt_audio_stop
            ├── bt_audio_next
            ├── bt_audio_prev    
            ├── bt_audio_volume_set
            ├── bt_audio_volume_up
            ├── bt_audio_volume_down
            ├── bt_audio_connect
            └── bt_audio_disconnect
            │
            ▼
执行层(实际控制蓝牙)
    └── cmd_reg.c 的 bt_audio_* 函数
            └── esp_bt_audio_* API

        在edge_agent/components下新建组件bt_audio来实现蓝牙的功能,结构为:

bt_audio
├── include
│   ├── cmd_reg.h
│   ├── codec_defs.h
│   ├── example_bt_audio.h
│   ├── pool_reg.h
│   └── stream_proc.h
├── src
│   ├── cmd_reg.c
│   ├── example_bt_audio.c
│   ├── pool_reg.c
│   └── stream_proc.c
├── CMakeLists.txt
├── idf_component.yml
└── Kconfig

        codec_defs.h、pool_reg.h、stream_proc.h、pool_reg.c与stream_proc.c保持原来一样,其他文件修改如下:

        1,components/bt_audio/include/cmd_reg.h,去除cli的相关内容,不需要通过串口发送指令来控制蓝牙

/*
 * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#pragma once

#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"


/**
 * @brief  设备发现回调
 *
 * 当蓝牙扫描到新设备时,由主事件回调调用。
 * 在 A2DP Source 模式下,若设置了目标设备名,会尝试自动连接。
 *
 * @param[in]  name  设备名称
 * @param[in]  bda   设备蓝牙地址(6 字节)
 */
void bt_audio_on_device_found(const char *name, const uint8_t *bda);

/**
 * @brief  连接状态变化回调
 *
 * 用于维护当前连接设备的 BDA,便于 disconnect 等命令使用。
 *
 * @param[in]  bda        设备蓝牙地址
 * @param[in]  connected  true=已连接,false=已断开
 */
void bt_audio_on_conn_state_changed(const uint8_t *bda, bool connected);

esp_err_t bt_audio_play(void);
esp_err_t bt_audio_pause(void);
esp_err_t bt_audio_stop(void);
esp_err_t bt_audio_next(void);
esp_err_t bt_audio_prev(void);
esp_err_t bt_audio_volume_set(int volume);
esp_err_t bt_audio_volume_up(void);
esp_err_t bt_audio_volume_down(void);
esp_err_t bt_audio_connect(const uint8_t bda[6]);
esp_err_t bt_audio_disconnect(void);

        2,components/bt_audio/include/example_bt_audio.h

#pragma once
#ifdef __cplusplus
extern "C" {
#endif
void bt_audio_task(void);
#ifdef __cplusplus
}
#endif

        3,components/bt_audio/src/cmd_reg.c

/*
 * SPDX-FileCopyrightText: 2026 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "esp_heap_caps.h"
#include "esp_system.h"
#include "esp_log.h"

#include "esp_bt_audio_defs.h"
#include "esp_bt_audio_classic.h"
#include "esp_bt_audio_vol.h"
#include "esp_bt_audio_media.h"
#include "esp_bt_audio_playback.h"
#include "esp_bt_audio_tel.h"
#include "esp_bt_audio_pb.h"

#include "esp_board_manager.h"
#include "esp_board_manager_defs.h"
#include "dev_audio_codec.h"
#include "cmd_reg.h"
/* 记录当前连接设备的 BDA,供 disconnect 等命令使用 */
static uint8_t target_device_bda[6] = {0};
#ifdef CONFIG_GMF_EXAMPLE_A2DP_SOURCE
/* A2DP Source 模式:记录目标设备名,用于发现后自动连接 */
static char target_device_name[32] = {0};
static const char *TAG = "CMD_REG";
#endif  /* CONFIG_GMF_EXAMPLE_A2DP_SOURCE */

/**
 * @brief  设备发现回调(Source 模式自动连接逻辑)
 */
void bt_audio_on_device_found(const char *name, const uint8_t *bda)
{
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    if (strlen(target_device_name) > 0) {
        if (strstr(name, target_device_name) != NULL) {
            ESP_LOGI(TAG, "Found target device '%s', initiating auto-connect...", target_device_name);
            esp_bt_audio_classic_discovery_stop();
            esp_err_t ret = esp_bt_audio_classic_connect(ESP_BT_AUDIO_CLASSIC_ROLE_A2DP_SRC, (uint8_t *)bda);
            if (ret == ESP_OK) {
                ESP_LOGI(TAG, "Connecting to %s...n", name);
                memset(target_device_name, 0, sizeof(target_device_name));
            } else {
                ESP_LOGE(TAG, "Failed to connect to %s: %sn", name, esp_err_to_name(ret));
            }
        }
    }
#endif  /* CONFIG_GMF_EXAMPLE_A2DP_SOURCE */
}
/**
 * @brief  连接状态变化回调,维护 target_device_bda
 */
void bt_audio_on_conn_state_changed(const uint8_t *bda, bool connected)
{
    if (connected) {
        memcpy(target_device_bda, bda, sizeof(target_device_bda));
    } else {
        memset(target_device_bda, 0, sizeof(target_device_bda));
    }
}

esp_err_t bt_audio_play(void)
{
    return esp_bt_audio_playback_play();
}

esp_err_t bt_audio_pause(void)
{
    return esp_bt_audio_playback_pause();
}

esp_err_t bt_audio_stop(void)
{
    return esp_bt_audio_playback_stop();
}

esp_err_t bt_audio_next(void)
{
    return esp_bt_audio_playback_next();
}

esp_err_t bt_audio_prev(void)
{
    return esp_bt_audio_playback_prev();
}

esp_err_t bt_audio_volume_set(int volume)
{
    if (volume < 0 || volume > 100) {
        return ESP_ERR_INVALID_ARG;
    }
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    return esp_bt_audio_vol_set_absolute((uint32_t)volume);
#else
    dev_audio_codec_handles_t *codec_handle = NULL;
    esp_board_manager_get_device_handle(ESP_BOARD_DEVICE_NAME_AUDIO_DAC, (void **)&codec_handle);
    esp_codec_dev_set_out_vol(codec_handle->codec_dev, volume);
    return esp_bt_audio_vol_notify((uint32_t)volume);
#endif
}

esp_err_t bt_audio_volume_up(void)
{
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    return esp_bt_audio_vol_set_relative(true);
#else
    int current_volume = 0;
    dev_audio_codec_handles_t *codec_handle = NULL;
    esp_board_manager_get_device_handle(ESP_BOARD_DEVICE_NAME_AUDIO_DAC, (void **)&codec_handle);
    esp_codec_dev_get_out_vol(codec_handle->codec_dev, &current_volume);
    current_volume = (current_volume >= 90) ? 100 : current_volume + 10;
    esp_codec_dev_set_out_vol(codec_handle->codec_dev, current_volume);
    return esp_bt_audio_vol_notify(current_volume);
#endif
}

esp_err_t bt_audio_volume_down(void)
{
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    return esp_bt_audio_vol_set_relative(false);
#else
    int current_volume = 0;
    dev_audio_codec_handles_t *codec_handle = NULL;
    esp_board_manager_get_device_handle(ESP_BOARD_DEVICE_NAME_AUDIO_DAC, (void **)&codec_handle);
    esp_codec_dev_get_out_vol(codec_handle->codec_dev, &current_volume);
    current_volume = (current_volume <= 10) ? 0 : current_volume - 10;
    esp_codec_dev_set_out_vol(codec_handle->codec_dev, current_volume);
    return esp_bt_audio_vol_notify(current_volume);
#endif
}
esp_err_t bt_audio_connect(const uint8_t bda[6])
{
    if (!bda) {
        return ESP_ERR_INVALID_ARG;
    }
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    esp_err_t ret = esp_bt_audio_classic_connect(ESP_BT_AUDIO_CLASSIC_ROLE_A2DP_SRC, (uint8_t *)bda);
#else
    esp_err_t ret = esp_bt_audio_classic_connect(ESP_BT_AUDIO_CLASSIC_ROLE_A2DP_SNK, (uint8_t *)bda);
#endif
    if (ret == ESP_OK) {
        memcpy(target_device_bda, bda, sizeof(target_device_bda));
    }
    return ret;
}

esp_err_t bt_audio_disconnect(void)
{
    bool is_bda_empty = true;
    for (int i = 0; i < sizeof(target_device_bda); ++i) {
        if (target_device_bda[i] != 0) {
            is_bda_empty = false;
            break;
        }
    }
    if (is_bda_empty) {
        return ESP_ERR_INVALID_STATE;
    }
#if CONFIG_GMF_EXAMPLE_A2DP_SOURCE
    esp_err_t ret = esp_bt_audio_classic_disconnect(ESP_BT_AUDIO_CLASSIC_ROLE_A2DP_SRC, target_device_bda);
#else
    esp_err_t ret = esp_bt_audio_classic_disconnect(ESP_BT_AUDIO_CLASSIC_ROLE_A2DP_SNK, target_device_bda);
#endif
    if (ret == ESP_OK) {
        memset(target_device_bda, 0, sizeof(target_device_bda));
    }
    return ret;
}

        4,components/bt_audio/src/example_bt_audio.c,主要去掉cli_init()。

        5,components/bt_audio/CMakeLists.txt

idf_component_register(
    SRC_DIRS "src"
    INCLUDE_DIRS "include"
    REQUIRES
        "esp_bt_audio"
        "esp_board_manager"
        "gmf_core"
        "gmf_audio"
        "gmf_io"
        "gmf_misc"
    PRIV_REQUIRES
        "nvs_flash"
        "console"
)

        6,components/bt_audio/idf_component.yml

## IDF Component Manager Manifest File
dependencies:
  espressif/esp_bt_audio:
    version: ^0.8
  espressif/gmf_ai_audio:
    version: ^0.8
  espressif/gmf_audio:
    version: ^0.8
  espressif/gmf_core:
    version: ^0.8
  espressif/gmf_io:
    version: ^0.8
  espressif/gmf_misc:
    version: ^0.8

        7,components/bt_audio/Kconfig

menu "BT Audio Basic Example (GMF)"

    menu "Classic Audio Roles Configuration"
        depends on BT_CLASSIC_ENABLED
        default GMF_EXAMPLE_A2DP_SINK
        choice GMF_EXAMPLE_A2DP_ROLE
            prompt "A2DP role"
            depends on BT_A2DP_ENABLE

        config GMF_EXAMPLE_A2DP_SINK
            bool "A2DP Sink"
            help
                Enable A2DP Sink role

        config GMF_EXAMPLE_A2DP_SOURCE
            bool "A2DP Source"
            help
                Enable A2DP Source role

        endchoice

        config GMF_EXAMPLE_HFP_HF
            depends on BT_HFP_CLIENT_ENABLE && GMF_EXAMPLE_A2DP_SINK
            bool "HFP Handsfree"
            default y
            help
                Enable HFP Handsfree role

        config GMF_EXAMPLE_AVRC_CT
            depends on BT_AVRCP_ENABLED
            bool "AVRC Controller"
            default y
            help
                Enable AVRC Controller role

        config GMF_EXAMPLE_AVRC_TG
            depends on BT_AVRCP_ENABLED
            bool "AVRC Target"
            default y
            help
                Enable AVRC Target role

        config GMF_EXAMPLE_PBAP_PCE
            depends on BT_PBAC_ENABLED && GMF_EXAMPLE_HFP_HF
            bool "Phone book client equipment"
            default n
            help
                Enable phone book client equipment role
    endmenu

endmenu

        8,main/CMakeLists.txt,添加组件bt_audio,才能使用蓝牙任务接口。如果报错:edge_agent_version.c:9:10: fatal error: esp_app_desc.h: No such file or directory,就是缺失组件esp_app_format。如果报错:edge_agent/main/main.c:25:10: fatal error: cmd_wifi.h: No such file or directory,就是缺失组件 cmd_wifi。

idf_component_register(
    SRC_DIRS "."
    INCLUDE_DIRS "."
    REQUIRES "bt_audio" "cmd_wifi" "esp_app_format"
)

        9,main/Kconfig.projbuild,去除components/bt_audio/Kconfig的内容,和原始文件一样即可

        接下来在esp-claw/components/claw_capabilities下新增一个capability目录cap_bt_audio,结构为:

esp‑claw
└── components
    └── claw_capabilities
        └── cap_bt_audio
            ├── include
            │   └── cap_bt_audio.h
            ├── skills
            │   └── SKILL.md
            ├── src
            │   └── cap_bt_audio.c
            └── CMakeLists.txt

        具体代码实现:

        10,components/claw_capabilities/cap_bt_audio/include/cap_bt_audio.h

#pragma once

#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

/**
 * @brief 注册 BT Audio Capability Group
 *
 * 注册后在应用初始化时调用,Group 内的工具才能被框架识别。
 * 此 Group 默认不应加入 LLM 可见白名单,需通过 Skill 激活后才可见。
 */
esp_err_t cap_bt_audio_register_group(void);

#ifdef __cplusplus
}
#endif

        11,components/claw_capabilities/cap_bt_audio/src/cap_bt_audio.c

#include "cap_bt_audio.h"

#include <stdio.h>
#include <string.h>

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_check.h"

#include "claw_cap.h"

#include "cmd_reg.h"
#include "cJSON.h"

static const char *TAG = "cap_bt_audio";


static esp_err_t bt_audio_play_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_play();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}

static esp_err_t bt_audio_pause_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_pause();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}

static esp_err_t bt_audio_stop_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_stop();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}
static esp_err_t bt_audio_next_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_next();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}
static esp_err_t bt_audio_prev_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_prev();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}


static esp_err_t bt_audio_vol_set_execute(const char *input_json,
                                          const claw_cap_call_context_t *ctx,
                                          char *output, size_t output_size)
{
    (void)ctx;
    cJSON *json = cJSON_Parse(input_json);
    if (!json) {
        snprintf(output, output_size, "Error: invalid json");
        return 0x102;
    }
    cJSON *vol = cJSON_GetObjectItem(json, "volume");
    if (!cJSON_IsNumber(vol)) {
        cJSON_Delete(json);
        snprintf(output, output_size, "Error: missing 'volume'");
        return 0x102;
    }
    int v = vol->valueint;
    cJSON_Delete(json);
    esp_err_t ret = bt_audio_volume_set(v);
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok","volume":%d}", v);
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}

static esp_err_t bt_audio_volume_up_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_volume_up();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}
static esp_err_t bt_audio_vol_down_execute(const char *input_json,
                                       const claw_cap_call_context_t *ctx,
                                       char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_volume_down();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"ok"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}
static esp_err_t bt_audio_connect_execute(const char *input_json,
                                          const claw_cap_call_context_t *ctx,
                                          char *output, size_t output_size)
{
    (void)ctx;
    cJSON *json = cJSON_Parse(input_json);
    if (!json) {
        snprintf(output, output_size, "Error: invalid json");
        return 0x102;
    }
    cJSON *mac = cJSON_GetObjectItem(json, "mac");
    if (!cJSON_IsString(mac)) {
        cJSON_Delete(json);
        snprintf(output, output_size, "Error: missing 'mac'");
        return 0x102;
    }

    uint8_t bda[6] = {0};
    if (sscanf(mac->valuestring, "%02hhx:%02hhx:%02hhx:%02hhx:%02hhx:%02hhx",
               &bda[0], &bda[1], &bda[2], &bda[3], &bda[4], &bda[5]) != 6) {
        cJSON_Delete(json);
        snprintf(output, output_size, "Error: invalid mac format");
        return 0x102;
    }
    cJSON_Delete(json);

    esp_err_t ret = bt_audio_connect(bda);
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"connecting"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}

static esp_err_t bt_audio_disconnect_execute(const char *input_json,
                                             const claw_cap_call_context_t *ctx,
                                             char *output, size_t output_size)
{
    (void)input_json;
    (void)ctx;
    esp_err_t ret = bt_audio_disconnect();
    if (ret == ESP_OK) {
        snprintf(output, output_size, "{"status":"disconnected"}");
    } else {
        snprintf(output, output_size, "Error: %s", esp_err_to_name(ret));
    }
    return ret;
}
/* ========== 描述符数组 ========== */
static const claw_cap_descriptor_t s_bt_audio_descriptors[] = {
    { 
        .id = "bt_audio_play",
        .name = "bt_audio_play",
        .family = "bt_audio",
        .description = "播放当前媒体",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_play_execute 
    },
    { 
        .id = "bt_audio_pause",
        .name = "bt_audio_pause",
        .family = "bt_audio",
        .description = "暂停播放",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_pause_execute 
    },
    { 
        .id = "bt_audio_stop",
        .name = "bt_audio_stop",
        .family = "bt_audio",
        .description = "停止播放",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_stop_execute 
    },
    { 
        .id = "bt_audio_next",
        .name = "bt_audio_next",
        .family = "bt_audio",
        .description = "切换下一首",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_next_execute 
    },
    { 
        .id = "bt_audio_prev",
        .name = "bt_audio_prev",
        .family = "bt_audio",
        .description = "切换上一首",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_prev_execute 
    },
    { 
        .id = "bt_audio_vol_set",
        .name = "bt_audio_vol_set",
        .family = "bt_audio",
        .description = "音量设置",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_vol_set_execute 
    },
    { 
        .id = "bt_audio_vol_up",
        .name = "bt_audio_vol_up",
        .family = "bt_audio",
        .description = "音量增加",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_volume_up_execute 
    },
    { 
        .id = "bt_audio_vol_down",
        .name = "bt_audio_vol_down",
        .family = "bt_audio",
        .description = "音量降低",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_vol_down_execute 
    },
    {
        .id = "bt_audio_connect",
        .name = "bt_audio_connect",
        .family = "bt_audio",
        .description = "Connect to a Bluetooth device by MAC address. "
                       "The MAC must be in the form XX:XX:XX:XX:XX:XX.",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_connect_execute
    },
    { 
        .id = "bt_audio_disconnect",
        .name = "bt_audio_disconnect",
        .family = "bt_audio",
        .description = "断开连接",
        .kind = CLAW_CAP_KIND_CALLABLE,
        .cap_flags = CLAW_CAP_FLAG_CALLABLE_BY_LLM,
        .execute = bt_audio_disconnect_execute 
    },
};

/* ========== Group 定义与注册 ========== */
static const claw_cap_group_t s_bt_audio_group = {
    .group_id = "cap_bt_audio",
    .descriptors = s_bt_audio_descriptors,
    .descriptor_count = sizeof(s_bt_audio_descriptors) / sizeof(s_bt_audio_descriptors[0]),
};

esp_err_t cap_bt_audio_register_group(void)
{
    esp_err_t err = claw_cap_register_group(&s_bt_audio_group);
    if (err != ESP_OK) {
        ESP_LOGE(TAG, "Failed to register group: %s", esp_err_to_name(err));
    } else {
        ESP_LOGI(TAG, "Group 'cap_bt_audio' registered (%d tools)",
                 (int)s_bt_audio_group.descriptor_count);
    }
    return err;
}

       12,components/claw_capabilities/cap_bt_audio/CMakeLists.txt

idf_component_register(
    SRC_DIRS "src"
    INCLUDE_DIRS "include"
    REQUIRES "claw_cap" "bt_audio"
    PRIV_REQUIRES "freertos"
)

         13,components/claw_capabilities/cap_bt_audio/skills/SKILL.md

---
name: bt_audio
description: "蓝牙音频控制:播放、暂停、切歌、音量调节"
metadata:
  manage_mode: runtime
  cap_groups:
    - cap_bt_audio
---

## 何时使用
当用户希望控制蓝牙音频播放时使用。

## 工具
- `bt_audio_play`:播放
- `bt_audio_pause`:暂停
- `bt_audio_stop`:停止
- `bt_audio_next`:下一首
- `bt_audio_prev`:上一首
- `bt_audio_vol_set`:设置音量,参数 `{"volume": 0-100}`
- `bt_audio_vol_up`:音量加 10
- `bt_audio_vol_down`:音量减 10
- `bt_audio_connect`:连接指定 MAC 的蓝牙设备,参数 `{"mac": "XX:XX:XX:XX:XX:XX"}`
- `bt_audio_disconnect`:断开当前连接的设备,无参数

## 注意
蓝牙任务在系统启动时已由 app_main 创建,无需额外开启。

        此外还需要把这个capability加入到app_claw中,才能正常使用cap_bt_audio

        14,修改esp-claw/components/common/app_claw/Kconfig,在menu "App Claw Capabilities"中新增

        config APP_CLAW_CAP_BT_AUDIO
            bool "Enable BT Audio capability"
            depends on BT_CLASSIC_ENABLED
            default y
            help
                Expose BT audio start/stop tools to the agent.
                Activate via the 'bt_audio' skill.

        15,修改esp-claw/components/common/app_claw/app_capabilities.c

// 1,加入头文件
#if CONFIG_APP_CLAW_CAP_BT_AUDIO
#include "cap_bt_audio.h"
#endif

// 2,新增注册接口
#if CONFIG_APP_CLAW_CAP_BT_AUDIO
static esp_err_t app_cap_register_bt_audio(const app_claw_config_t *config,
                                           const app_claw_storage_paths_t *paths)
{
    (void)config;
    (void)paths;
    return cap_bt_audio_register_group();
}
#endif

// 3,在s_capability_group_entries[]中新增
#if CONFIG_APP_CLAW_CAP_BT_AUDIO
    { "cap_bt_audio", "BT Audio", "Register BT audio cap", true, NULL, app_cap_register_bt_audio },
#endif

// 4,在s_capability_group_infos[]中新增
#if CONFIG_APP_CLAW_CAP_BT_AUDIO
    { "cap_bt_audio", "BT Audio", true},
#endif

        16,修改esp-claw/components/common/app_claw/idf_component.yml,新增内容

  cap_bt_audio:
    rules:
      - if: $CONFIG{APP_CLAW_CAP_BT_AUDIO} == True
    path: ../../claw_capabilities/cap_bt_audio

        之后在esp-claw Settings配置页面中可见功能已打开

配置页面.png

        注意:bt_audio_connect 只能连接已配对的设备。若手机从未与本设备配对过,需要先在手机的蓝牙设置中完成配对,之后才能通过 MAC 地址发起连接。

实现源码:esp-claw.zip





关键词: ESP32-S31     LLM     蓝牙    

共1条 1/1 1 跳转至页

回复

匿名不能发帖!请先 [ 登陆 注册 ]