发帖
2 0 0

Ai-WB2 BLE HID 三合一复合设备

bzhou830
论坛元老

85

主题

1852

回帖

1万

积分

论坛元老

积分
18264
Ai-WB2系列 24 2 半小时前

Ai-WB2 BLE HID 三合一复合设备

从原理到实现,覆盖 SDK、开发平台、编译、烧录、测试验证与平台兼容性。
该版本在 Android 上完整可用;但是Windows 因缺少 Boot Protocol 特征无法被系统识别.


1. 项目简介

本项目在 BL602(Ai-Thinker Ai-WB2) 模组上,用 BLE(Bluetooth Low Energy)实现一个
HID-over-GATT(HOGP) 自拍杆 / 复合输入设备。它在单个 GATT 服务里同时暴露三个 HID 接口:

Report ID 接口 用途 报告长度 Usage Page / Usage
1 键盘 发送按键(demo 默认发字母 h 8 字节 Generic Desktop / Keyboard
2 鼠标 相对位移 + 三键 + 滚轮 4 字节 Generic Desktop / Mouse
3 Consumer 多媒体:音量 ±、播放/暂停、上下曲、静音 2 字节 Consumer Control

关键点:大多数手机把 Consumer Control 的 Volume Increment(Usage 0xE9)当作「快门键」,
因此原始自拍杆行为就是以 Report ID 3 的 0xE9 实现——本项目把它作为三合一中的一路保留下来。

按键:GPIO12(低电平有效,片上拉),每按一次在 键盘 → 鼠标 → Consumer 三个 demo 之间轮询,
方便逐一验证三个接口。


2. 工作原理

2.1 HID over GATT(HOGP)协议栈

BLE HID 设备并不走经典蓝牙(BR/EDR),而是把 HID 报文装进 GATT 特征值,通过 Notification 推送给主机:

[主机 Host]  <-- BLE GATT Notification -->  [BL602 设备]
    |                                        |
    |  (1) 扫描广播,发现 HIDS 0x1812        |
    |  (2) 连接,读取 Report Map / HID Info  |
    |  (3) 使能 CCC(Client Characteristic  |
    |       Configuration)开启 Notification |
    |                                        |
    |  (4) 设备 notify Input Report --------->|
    |  (5) 主机按 Report Map 解析报文        |
  • HIDS 服务 UUID0x1812(Primary Service)。
  • Report Map(0x2A4B):一段 HID 描述符字节流,告诉主机每个 Report ID 的字段布局。
  • HID Information(0x2A4A):bcdHID 版本、国家码、flags。
  • Protocol Mode(0x2A4E):0x00 = Boot Protocol,0x01 = Report Protocol(本工程默认 Report)。
  • HID Control Point(0x2A4C):主机写 0x00 挂起、0x01 恢复设备。
  • Input Report(0x2A4D):实际数据通道,配 BT_GATT_CHRC_NOTIFY + CCC 描述符。

2.2 三个接口的 Report Map 拆分

本工程用一份合并的 Report Map,靠 Report ID(0x85 标签)区分三个顶层 Collection:

Report ID 1:  Keyboard   (Usage Page 0x01/0x06, 8 字节: 1 modifier + 1 reserved + 6 keycodes)
Report ID 2:  Mouse      (Usage Page 0x01/0x02, 4 字节: 3 buttons + X + Y + wheel)
Report ID 3:  Consumer   (Usage Page 0x0C,     2 字节: 16-bit usage, 如 0xE9 音量+)

每个 Input Report 特征值都挂了一个 Report Reference 描述符(0x2A4D 引用,UUID 0x2908
值为 {id, type},让主机能把「哪个特征」和「哪个 Report ID」对应起来(见 main.c
kbd_report_ref / mouse_report_ref / consumer_report_ref)。

2.3 BLE 链路时钟

BL602 的 BLE Link Layer 用 32.768 kHz 低功耗时钟 作为连接锚点(anchor)参考。
时钟源与硬件不符是导致「刚连上就掉线(HCI 错误 0x22 / 0x13)」的头号原因。
本工程在 main() 里显式选择片上 RC:

#if defined(BL602)
    HBN_32K_Sel(HBN_32K_RC);   /* 片上 RC,所有模组都有,最稳 */
    /* HBN_32K_XTAL 仅当板载外部 32.768k 晶振时才用 */
#endif

2.4 断线自动重广播

连接断开回调 disconnected() 中直接再调 start_adv(),保证主机可确定性地重连,
不会因为设备「停止广播」而需要手动复位。

2.5 软件架构

main()
  ├─ board_init()
  ├─ HBN_32K_Sel(HBN_32K_RC)          // 32K 时钟
  ├─ rfparam_init()                   // RF 参数(BL602 必须)
  ├─ bflb_gpio_init(GPIO12, 输入上拉) // 按键
  ├─ xTaskCreate(app_start_task)      // 启动 BLE 协议栈
  └─ xTaskCreate(selfie_task)         // 按键轮询 → 发 Report

app_start_task()
  ├─ ble_controller_init()            // BL602 蓝牙控制器(Host+Link Layer 库)
  ├─ hci_driver_init()
  └─ bt_enable(ble_ready)             // 异步回调

ble_ready()
  ├─ bt_gatt_service_register(&hids)  // 注册 HIDS 服务
  ├─ bt_conn_cb_register()
  └─ start_adv()                       // 开始广播 "Ai-WB2 Combo"

3. 开发平台与 SDK

项目 说明
芯片 Bouffalo LabBL602(32-bit RISC-V,最高 192 MHz)
模组 Ai-ThinkerAi-WB2(板载 BL602 + 射频 + PCB 天线)
SDK bouffalo_sdk(Bouffalo Lab 统一 SDK)
BLE 协议栈 blestack(Zephyr/NimBLE 风格 Host API:bt_*gatt.h
RTOS FreeRTOS(vTaskStartScheduler 启动调度器)
GPIO 驱动 bflb_gpio(Bouffalo 外设库,非 HOSAL)
构建系统 CMake(经 tools/make/make 包装调用)
烧录工具 Bouffalo Flash Cube(BLFlashCommand.exe,UART 下载)
目标板 BOARD=bl602dkCHIP=bl602

3.1 依赖环境(Windows)

  • bouffalo_sdk:本机路径 D:/bouffalo_sdk(注意用 Windows 盘符风格,不能用 MSYS 的 /d/...)。
  • 工具链:SDK 自带 RISC-V GCC(无需另行安装)。
  • Python:SDK 构建脚本依赖 Python(建议 3.x)。
  • 串口:设备通过 USB 转串口(如 CH340)出现在 COMx,烧录用 COM6(按实际修改)。

4. 工程结构

ble_hid_selfie/
├── main.c              # 全部实现:HID 服务、广播、按键任务、BLE 启动
├── CMakeLists.txt      # sdk_add_include_directories / sdk_set_main_file
├── Makefile            # 复用 bouffalo_sdk 的 project.build
├── defconfig           # 关闭 SHELL/BREDR/MESH,开启 BLUETOOTH/RF/FREERTOS
├── flash_prog_cfg.ini  # 烧录分区:[boot2]/[partition]/[FW]
├── FreeRTOSConfig.h    # FreeRTOS 配置
├── Kconfig             # 工程 Kconfig
├── .gitignore          # 忽略 build/、*.log 等
└── build/              # 编译产物(build/build_out/*.bin)

flash_prog_cfg.ini 关键项:boot2 在 0x000000,partition 在 0xE000
FW 地址用 @partition 引用 partition 表(0x10000),文件名模板
ble_hid_selfie*_$(CHIPNAME)*.bin 必须与实际产物名匹配。


5. 编译

必须在 Git Bash / 原生 bash(非 MSYS 路径)下执行,且 BL_SDK_BASE 用 Windows 风格路径。

5.1 仅编译

cd ble_hid_selfie
BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602 \
  /d/bouffalo_sdk/tools/make/make

或用 make 包装(SDK 已在 PATH 或可定位时):

make BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602

成功后会生成:

build/build_out/ble_hid_selfie_bl602.bin   # 主固件(约 248 KB)
build/build_out/boot2_bl602_release_v8.1.3.bin
build/build_out/partition.bin

5.2 常见编译坑点

现象/错误 原因 解决
find_package(bouffalo_sdk) 失败 BL_SDK_BASE 未设置或用了 /d/... 显式传 BL_SDK_BASE=D:/bouffalo_sdk
链接报错:找不到 ble_controller_init 误用 BL616/618 的 btble_controller_init BL602 用 ble_lib_api.h + ble_controller_init
GPIO_FUNC_GPIO 未定义 仅 BL616/618 有该宏 BL602 用 bflb_gpio_init(..., GPIO_INPUT|GPIO_PULLUP...)
cli.h: No such file 开了 CONFIG_BT_STACK_CLI defconfigCONFIG_BT_STACK_CLI=n

6. 烧录(Flash)

6.1 命令

cd ble_hid_selfie
BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602 \
  COMX=COM6 BAUDRATE=921600 \
  /d/bouffalo_sdk/tools/make/make flash
  • COMX=COM6:下载串口(按设备管理器实际端口改)。
  • BAUDRATE=921600:串口波特率(变量名是 BAUDRATE,不是 b=)。
  • make flash先编译再烧录,依赖 flash_prog_cfg.ini

6.2 烧录前准备

  1. 关闭占用串口的串口监视器(如 PuTTY、串口助手),否则 PermissionError
  2. 确认 COM6 确实是设备端口(设备管理器查看 CH340/CP210x)。
  3. 板子正常上电(USB 连接)。

6.3 进入下载模式(重要)

工具通过 DTR/RTS 自动复位芯片进下载模式。若握手卡在 Start handshake / 报
LOAD HELP BIN FAIL

  • 手动进入下载模式:按住板子 BOOT 键(GPIO8 拉低)→ 按一下 RESET → 松开 RESET → 松开 BOOT。
  • 看到日志出现 Handshake succeeded / The ack data is b'4f4b' 即表示成功。

6.4 成功标志

日志末尾出现:

Flash writing succeeded
All programming completed successfully

三段(boot2 / partition / FW)均 Verification succeeded 即烧录无误。


7. 测试与验证

7.1 串口日志

烧录后复位板子,打开串口监视器(115200 或 SDK 默认波特率),应看到:

BLE ready, registering HID composite service
Advertising started - connect to 'Ai-WB2 Combo'

连接后:

BLE connected

7.2 在 Android 上验证(推荐)

  1. 手机打开蓝牙,搜索并配对 「Ai-WB2 Combo」
  2. 配对成功后,系统将其识别为「键盘」「鼠标」「多媒体」复合设备。
  3. 打开一个文本输入框(如备忘录),按板子 GPIO12 按键
    • 第 1 次 → Consumer Volume+(相当于快门;拍照 App 会触发快门)
    • 第 2 次 → 键盘发送字母 h(输入框出现 h
    • 第 3 次 → 鼠标相对移动 (+20,+10)
    • 循环往复
  4. 串口日志会打印 Demo: Consumer Volume+ / Demo: Keyboard 'h' / Demo: Mouse move 等。

想只做自拍杆:把 main.c 顶部 DEMO_MODE 改为 0,按键将只发 Consumer 0xE9(快门)。

7.3 在 Linux / nRF Connect 上验证(可选)

  • nRF Connect 扫描,可见 Ai-WB2 Combo,服务列表含 0x1812 (HIDS)
  • 展开可读到 Report Map(0x2A4B)、HID Information(0x2A4A)、Protocol Mode(0x2A4E)。
  • 给三个 Input Report 的 CCC 写 0x0001 使能 Notification,按键即可收到通知报文。

7.4 断线重连验证

连接后手动关闭手机蓝牙再重开、重新配对,设备日志会打印
BLE disconnected (reason 0x..) 并立即 Advertising started,可再次连上。


8. 平台兼容性

平台 状态 说明
Android ✅ 完整可用 原生支持 HOGP 复合设备,三接口均生效
Linux ✅ 可用 通用 HID 栈,nRF Connect / bluez 可验证
macOS ⚠️ 基本可用 支持 HOGP,行为接近 Linux;未专门验证
Windows ❌ 本版本不识别 系统要求 Boot Protocol 特征(0x2A22/0x2A32/0x2A33)+ Report ID 前缀

9. 关键实现速查(main.c)

位置 内容
main.c:39 SELFIE_BTN_PIN 12 按键 GPIO
main.c:45 DEMO_MODE 1 三接口轮询 / 0 仅快门
main.c:72 hid_info:bcdHID=0x0111, flags=0x00
main.c:101 report_map[]:合并三接口 HID 描述符
main.c:313 hids_attrs[]:HIDS GATT 服务定义
main.c:407 disconnected():断线即 start_adv() 重广播
main.c:454 send_keyboard():发按键后 20ms 发全 0 释放
main.c:473 send_mouse():4 字节相对位移
main.c:488 send_consumer():2 字节 16-bit usage,40ms 释放
main.c:604 HBN_32K_Sel(HBN_32K_RC) 防掉线
main.c:582 ble_controller_init()(BL602 专用)

10. 常见问题(FAQ)

Q1:能连上但几秒后掉线(日志 0x22 / 0x13)?
A:几乎都是 32.768K 时钟源问题。确认 main() 里有 HBN_32K_Sel(HBN_32K_RC)
若板载外部晶振则改用 HBN_32K_XTAL

Q2:烧录提示 LOAD HELP BIN FAIL
A:芯片没进下载模式。先关闭串口监视器,再手动按 BOOT+RESET 进下载模式后重跑 make flash

Q3:按键没反应?
A:先确认已 BLE connected 且对应 CCC 已使能(Notification)。send_* 函数开头都检查了
*_notify 标志,未使能不会发。串口会打印 demo 分支,可据此判断。

Q4:只想做纯自拍杆?
A:把 DEMO_MODE 设为 0,按键固定发 Consumer 0xE9(快门),其余接口仍在但不会被触发。


11. 参考

  • bouffalo_sdk:D:/bouffalo_sdk(含 examples/peripheral/ble/tools/bflb_tools
  • HID Usage Tables:USB-IF HID Usage Tables(Keyboard/Mouse/Consumer 各 Page)
  • Bluetooth SIG:HID Service Specification(HIDS 0x1812)
  • Ai-Thinker Ai-WB2 模组资料
#include "FreeRTOS.h"
#include "task.h"
#include "board.h"

#include "bluetooth.h"
#include "conn.h"
#include "gatt.h"
#include "hci_driver.h"
#include "hci_core.h"
#include "rfparam_adapter.h"

#if defined(BL602)
#include "ble_lib_api.h"
#include "bl602_glb.h"
#include "bl602_hbn.h"
#endif

/* ------------------------------------------------------------------ */
/*  Configuration                                                     */
/* ------------------------------------------------------------------ */
#define SELFIE_BTN_PIN  12          /* GPIO12, active-low, internal pull-up */
#define DEBOUNCE_MS     40
#define SELFIE_TASK_PRIO (configMAX_PRIORITIES - 3)

/* 1 = button cycles through keyboard/mouse/consumer demos
 * 0 = button always sends Consumer Volume+ (selfie shutter)          */
#define DEMO_MODE       1

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#endif

/* ------------------------------------------------------------------ */
/*  HID report structures                                             */
/* ------------------------------------------------------------------ */
struct hids_info {
    u16_t bcdHID;
    u8_t  bCountryCode;
    u8_t  flags;
};

struct hids_report_ref {
    u8_t id;
    u8_t type;   /* 0x01 = Input, 0x02 = Output, 0x03 = Feature */
};

/* Per-report buffer used for GATT read/write of the Input Reports */
struct hid_report {
    u8_t buf[8];
    u8_t len;
};

/* HID Information: HID 1.1.1, no country, no wake / no virtual cable */
static const struct hids_info hid_info = {
    .bcdHID = 0x0111,
    .bCountryCode = 0x00,
    .flags = 0x00,
};

/* Report Reference descriptors (distinguish the three Input Reports) */
static const struct hids_report_ref kbd_report_ref = {
    .id = 0x01, .type = 0x01,
};
static const struct hids_report_ref mouse_report_ref = {
    .id = 0x02, .type = 0x01,
};
static const struct hids_report_ref consumer_report_ref = {
    .id = 0x03, .type = 0x01,
};

/* Protocol Mode: 0x00 = Boot, 0x01 = Report (default) */
static u8_t proto_mode = 0x01;

/* Live report buffers (last value sent, for host reads) */
static struct hid_report kbd_report      = { .len = 8 };
static struct hid_report mouse_report    = { .len = 4 };
static struct hid_report consumer_report = { .len = 2 };

/*
 * Combined HID Report Map: three top-level collections, each with a
 * distinct Report ID so the host can tell them apart.
 */
static const u8_t report_map[] = {
    /* ===== Report ID 1: Keyboard ===== */
    0x05, 0x01,        /* Usage Page (Generic Desktop)        */
    0x09, 0x06,        /* Usage (Keyboard)                    */
    0xA1, 0x01,        /* Collection (Application)           */
    0x85, 0x01,        /*   Report ID (1)                    */
    0x05, 0x07,        /*   Usage Page (Key Codes)           */
    0x19, 0xE0,        /*   Usage Min (Left Ctrl)            */
    0x29, 0xE7,        /*   Usage Max (Right GUI)            */
    0x15, 0x00,        /*   Logical Min (0)                 */
    0x25, 0x01,        /*   Logical Max (1)                 */
    0x75, 0x01,        /*   Report Size (1)                 */
    0x95, 0x08,        /*   Report Count (8)                */
    0x81, 0x02,        /*   Input (Data,Var,Abs)  modifier  */
    0x95, 0x01,        /*   Report Count (1)                */
    0x75, 0x08,        /*   Report Size (8)                 */
    0x81, 0x03,        /*   Input (Const,Var,Abs)  reserved */
    0x95, 0x06,        /*   Report Count (6)                */
    0x75, 0x08,        /*   Report Size (8)                 */
    0x15, 0x00,        /*   Logical Min (0)                 */
    0x25, 0x65,        /*   Logical Max (101)               */
    0x05, 0x07,        /*   Usage Page (Key Codes)          */
    0x19, 0x00,        /*   Usage Min (0)                   */
    0x29, 0x65,        /*   Usage Max (101)                 */
    0x81, 0x00,        /*   Input (Data,Array,Abs) keycodes */
    0xC0,              /* End Collection                    */

    /* ===== Report ID 2: Mouse ===== */
    0x05, 0x01,        /* Usage Page (Generic Desktop)        */
    0x09, 0x02,        /* Usage (Mouse)                      */
    0xA1, 0x01,        /* Collection (Application)           */
    0x09, 0x01,        /*   Usage (Pointer)                  */
    0xA1, 0x00,        /*   Collection (Physical)            */
    0x85, 0x02,        /*     Report ID (2)                  */
    0x05, 0x09,        /*     Usage Page (Buttons)           */
    0x19, 0x01,        /*     Usage Min (Button 1)           */
    0x29, 0x03,        /*     Usage Max (Button 3)           */
    0x15, 0x00,        /*     Logical Min (0)                */
    0x25, 0x01,        /*     Logical Max (1)                */
    0x75, 0x01,        /*     Report Size (1)                */
    0x95, 0x03,        /*     Report Count (3)               */
    0x81, 0x02,        /*     Input (Data,Var,Abs) buttons   */
    0x95, 0x01,        /*     Report Count (1)               */
    0x75, 0x05,        /*     Report Size (5)                */
    0x81, 0x03,        /*     Input (Const,Var,Abs) padding  */
    0x05, 0x01,        /*     Usage Page (Generic Desktop)   */
    0x09, 0x30,        /*     Usage (X)                      */
    0x09, 0x31,        /*     Usage (Y)                      */
    0x15, 0x81,        /*     Logical Min (-127)             */
    0x25, 0x7F,        /*     Logical Max (127)              */
    0x75, 0x08,        /*     Report Size (8)                */
    0x95, 0x02,        /*     Report Count (2)               */
    0x81, 0x06,        /*     Input (Data,Var,Rel) X,Y       */
    0x09, 0x38,        /*     Usage (Wheel)                  */
    0x15, 0x81,        /*     Logical Min (-127)             */
    0x25, 0x7F,        /*     Logical Max (127)              */
    0x75, 0x08,        /*     Report Size (8)                */
    0x95, 0x01,        /*     Report Count (1)               */
    0x81, 0x06,        /*     Input (Data,Var,Rel) wheel     */
    0xC0,              /*   End Collection (Physical)        */
    0xC0,              /* End Collection (Application)       */

    /* ===== Report ID 3: Consumer Control (multimedia) ===== */
    0x05, 0x0C,        /* Usage Page (Consumer)              */
    0x09, 0x01,        /* Usage (Consumer Control)           */
    0xA1, 0x01,        /* Collection (Application)           */
    0x85, 0x03,        /*   Report ID (3)                    */
    0x15, 0x00,        /*   Logical Min (0)                  */
    0x26, 0xFF, 0x03,  /*   Logical Max (1023)  (16-bit)     */
    0x75, 0x10,        /*   Report Size (16)                 */
    0x95, 0x01,        /*   Report Count (1)                 */
    0x05, 0x0C,        /*   Usage Page (Consumer)            */
    0x09, 0xE9,        /*   Usage (Volume Increment)         */
    0x09, 0xEA,        /*   Usage (Volume Decrement)         */
    0x09, 0xB0,        /*   Usage (Play/Pause)               */
    0x09, 0xB5,        /*   Usage (Scan Next Track)          */
    0x09, 0xB6,        /*   Usage (Scan Prev Track)          */
    0x09, 0xCD,        /*   Usage (Mute)                     */
    0x81, 0x02,        /*   Input (Data,Var,Abs)             */
    0xC0               /* End Collection                     */
};

/* ------------------------------------------------------------------ */
/*  GATT read/write helpers                                           */
/* ------------------------------------------------------------------ */
static ssize_t read_u8(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                       void *buf, u16_t len, u16_t offset)
{
    const u8_t *val = attr->user_data;

    if (offset > 0) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    if (len < 1) {
        return 0;
    }
    ((u8_t *)buf)[0] = *val;
    return 1;
}

static ssize_t write_u8(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                        const void *buf, u16_t len, u16_t offset, u8_t flags)
{
    u8_t *val = attr->user_data;

    if (offset > 0 || len < 1) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    *val = ((const u8_t *)buf)[0];
    return len;
}

/* Generic read/write for the variable-length Input Reports */
static ssize_t read_report(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                           void *buf, u16_t len, u16_t offset)
{
    const struct hid_report *r = attr->user_data;

    if (offset >= r->len) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    u16_t n = r->len - offset;

    if (n > len) {
        n = len;
    }
    memcpy(buf, r->buf + offset, n);
    return n;
}

static ssize_t write_report(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                            const void *buf, u16_t len, u16_t offset, u8_t flags)
{
    struct hid_report *r = attr->user_data;

    if (offset >= r->len) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    u16_t n = r->len - offset;

    if (n > len) {
        n = len;
    }
    memcpy(r->buf + offset, buf, n);
    return n;
}

static ssize_t read_report_map(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                               void *buf, u16_t len, u16_t offset)
{
    if (offset >= sizeof(report_map)) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    u16_t tocopy = sizeof(report_map) - offset;

    if (tocopy > len) {
        tocopy = len;
    }
    memcpy(buf, report_map + offset, tocopy);
    return tocopy;
}

static ssize_t read_hid_info(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                             void *buf, u16_t len, u16_t offset)
{
    const u8_t *src = (const u8_t *)&hid_info;

    if (offset >= sizeof(hid_info)) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    u16_t tocopy = sizeof(hid_info) - offset;

    if (tocopy > len) {
        tocopy = len;
    }
    memcpy(buf, src + offset, tocopy);
    return tocopy;
}

static ssize_t read_report_ref(struct bt_conn *conn, const struct bt_gatt_attr *attr,
                               void *buf, u16_t len, u16_t offset)
{
    const u8_t *src = (const u8_t *)attr->user_data;

    if (offset >= sizeof(struct hids_report_ref)) {
        return BT_GATT_ERR(BT_ATT_ERR_INVALID_OFFSET);
    }
    u16_t tocopy = sizeof(struct hids_report_ref) - offset;

    if (tocopy > len) {
        tocopy = len;
    }
    memcpy(buf, src + offset, tocopy);
    return tocopy;
}

/* ------------------------------------------------------------------ */
/*  HID GATT service (3 Input Reports)                                */
/* ------------------------------------------------------------------ */
/* Value-attribute indices into hids_attrs[] (0-based):
 *   Keyboard  Input Report value -> 10
 *   Mouse     Input Report value -> 14
 *   Consumer  Input Report value -> 18
 */
#define KBD_INPUT_REPORT_VAL_IDX      10
#define MOUSE_INPUT_REPORT_VAL_IDX    14
#define CONSUMER_INPUT_REPORT_VAL_IDX 18

static void kbd_ccc_changed(const struct bt_gatt_attr *attr, u16_t value);
static void mouse_ccc_changed(const struct bt_gatt_attr *attr, u16_t value);
static void consumer_ccc_changed(const struct bt_gatt_attr *attr, u16_t value);

static struct bt_gatt_attr hids_attrs[] = {
    BT_GATT_PRIMARY_SERVICE(BT_UUID_HIDS),

    /* Protocol Mode (0x2A4E) - read/write, Report Protocol default */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_PROTOCOL_MODE,
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
                           BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
                           read_u8, write_u8, &proto_mode),

    /* Report Map (0x2A4B) */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT_MAP,
                           BT_GATT_CHRC_READ,
                           BT_GATT_PERM_READ,
                           read_report_map, NULL, (void *)report_map),

    /* HID Information (0x2A4A) */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_INFO,
                           BT_GATT_CHRC_READ,
                           BT_GATT_PERM_READ,
                           read_hid_info, NULL, (void *)&hid_info),

    /* HID Control Point (0x2A4C) - write only */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_CTRL_POINT,
                           BT_GATT_CHRC_WRITE_WITHOUT_RESP,
                           BT_GATT_PERM_WRITE,
                           NULL, write_u8, &proto_mode),

    /* ---- Input Report 1: Keyboard (Report ID 1) ---- */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT,
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
                           read_report, write_report, &kbd_report),
    BT_GATT_CCC(kbd_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF,
                       BT_GATT_PERM_READ,
                       read_report_ref, NULL, (void *)&kbd_report_ref),

    /* ---- Input Report 2: Mouse (Report ID 2) ---- */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT,
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
                           read_report, write_report, &mouse_report),
    BT_GATT_CCC(mouse_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF,
                       BT_GATT_PERM_READ,
                       read_report_ref, NULL, (void *)&mouse_report_ref),

    /* ---- Input Report 3: Consumer Control (Report ID 3) ---- */
    BT_GATT_CHARACTERISTIC(BT_UUID_HIDS_REPORT,
                           BT_GATT_CHRC_READ | BT_GATT_CHRC_WRITE | BT_GATT_CHRC_NOTIFY,
                           BT_GATT_PERM_READ | BT_GATT_PERM_WRITE,
                           read_report, write_report, &consumer_report),
    BT_GATT_CCC(consumer_ccc_changed, BT_GATT_PERM_READ | BT_GATT_PERM_WRITE),
    BT_GATT_DESCRIPTOR(BT_UUID_HIDS_REPORT_REF,
                       BT_GATT_PERM_READ,
                       read_report_ref, NULL, (void *)&consumer_report_ref),
};

static struct bt_gatt_service hids = BT_GATT_SERVICE(hids_attrs);

/* ------------------------------------------------------------------ */
/*  Connection state                                                  */
/* ------------------------------------------------------------------ */
static struct bt_conn *default_conn = NULL;
static bool            kbd_notify = false;
static bool            mouse_notify = false;
static bool            consumer_notify = false;

static void kbd_ccc_changed(const struct bt_gatt_attr *attr, u16_t value)
{
    kbd_notify = (value == BT_GATT_CCC_NOTIFY);
}

static void mouse_ccc_changed(const struct bt_gatt_attr *attr, u16_t value)
{
    mouse_notify = (value == BT_GATT_CCC_NOTIFY);
}

static void consumer_ccc_changed(const struct bt_gatt_attr *attr, u16_t value)
{
    consumer_notify = (value == BT_GATT_CCC_NOTIFY);
}

static void connected(struct bt_conn *conn, u8_t err)
{
    if (err) {
        return;
    }
    default_conn = bt_conn_ref(conn);
    printf("BLE connected\n");
}

static void start_adv(void);  /* forward decl; defined after ad[]/sd[] */

static void disconnected(struct bt_conn *conn, u8_t reason)
{
    if (default_conn) {
        bt_conn_unref(default_conn);
        default_conn = NULL;
    }
    kbd_notify = mouse_notify = consumer_notify = false;
    printf("BLE disconnected (reason 0x%02x)\n", reason);
    /* Restart advertising so the host can reconnect deterministically. */
    start_adv();
}

static struct bt_conn_cb conn_callbacks = {
    .connected = connected,
    .disconnected = disconnected,
};

/* ------------------------------------------------------------------ */
/*  Advertising                                                       */
/* ------------------------------------------------------------------ */
static const struct bt_data ad[] = {
    BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
    BT_DATA_BYTES(BT_DATA_UUID16_ALL, 0x12, 0x18), /* HIDS 0x1812 (LE) */
    BT_DATA_BYTES(BT_DATA_NAME_COMPLETE,
                  'A','i','-','W','B','2',' ','C','o','m','b','o'),
};

/* Scan response: HID keyboard appearance (0x03C1) */
static const struct bt_data sd[] = {
    BT_DATA_BYTES(BT_DATA_GAP_APPEARANCE, 0xC1, 0x03),
};

static void start_adv(void)
{
    int adv_err = bt_le_adv_start(BT_LE_ADV_CONN, ad, ARRAY_SIZE(ad),
                                  sd, ARRAY_SIZE(sd));
    if (adv_err) {
        printf("Advertising failed to start (err %d)\n", adv_err);
    } else {
        printf("Advertising started - connect to 'Ai-WB2 Combo'\n");
    }
}

/* ------------------------------------------------------------------ */
/*  Report send helpers                                               */
/* ------------------------------------------------------------------ */
/* Keyboard: modifier byte + up to 6 keycodes. Sends press then release. */
static void send_keyboard(u8_t modifier, const u8_t *keys, u8_t nkeys)
{
    if (!default_conn || !kbd_notify) {
        return;
    }
    memset(kbd_report.buf, 0, 8);
    kbd_report.buf[0] = modifier;
    for (u8_t i = 0; i < nkeys && i < 6; i++) {
        kbd_report.buf[2 + i] = keys[i];
    }
    bt_gatt_notify(default_conn, &hids.attrs[KBD_INPUT_REPORT_VAL_IDX],
                   kbd_report.buf, 8);
    vTaskDelay(pdMS_TO_TICKS(20));
    memset(kbd_report.buf, 0, 8);
    bt_gatt_notify(default_conn, &hids.attrs[KBD_INPUT_REPORT_VAL_IDX],
                   kbd_report.buf, 8);
}

/* Mouse: buttons (bit0=L,bit1=M,bit2=R) + X + Y + wheel (relative). */
static void send_mouse(u8_t buttons, int x, int y, int wheel)
{
    if (!default_conn || !mouse_notify) {
        return;
    }
    u8_t buf[4] = {
        buttons,
        (u8_t)(int8_t)x,
        (u8_t)(int8_t)y,
        (u8_t)(int8_t)wheel,
    };
    bt_gatt_notify(default_conn, &hids.attrs[MOUSE_INPUT_REPORT_VAL_IDX], buf, 4);
}

/* Consumer Control: 16-bit usage (e.g. 0xE9 = Volume Increment). */
static void send_consumer(u16_t usage)
{
    if (!default_conn || !consumer_notify) {
        return;
    }
    u8_t press[2] = { (u8_t)(usage & 0xFF), (u8_t)(usage >> 8) };
    u8_t release[2] = { 0x00, 0x00 };

    bt_gatt_notify(default_conn, &hids.attrs[CONSUMER_INPUT_REPORT_VAL_IDX],
                   press, 2);
    vTaskDelay(pdMS_TO_TICKS(40));
    bt_gatt_notify(default_conn, &hids.attrs[CONSUMER_INPUT_REPORT_VAL_IDX],
                   release, 2);
}

/* ------------------------------------------------------------------ */
/*  Button polling task (bflb_gpio) - demo / selfie trigger          */
/* ------------------------------------------------------------------ */
static struct bflb_device_s *gpio_dev;

static void selfie_task(void *arg)
{
    int last = 1;          /* idle = high (pull-up) */
    int demo_step = 0;     /* 0=consumer, 1=keyboard, 2=mouse */

    for (;;) {
        int cur = bflb_gpio_read(gpio_dev, SELFIE_BTN_PIN) ? 1 : 0;

        if (last == 1 && cur == 0) {
            vTaskDelay(pdMS_TO_TICKS(DEBOUNCE_MS));
            if (bflb_gpio_read(gpio_dev, SELFIE_BTN_PIN) == 0) {
#if DEMO_MODE
                switch (demo_step) {
                case 0:
                    if (consumer_notify) {
                        printf("Demo: Consumer Volume+\n");
                        send_consumer(0xE9);      /* selfie shutter */
                    }
                    break;
                case 1: {
                    u8_t k[] = { 0x0B };          /* HID keycode for 'h' */
                    if (kbd_notify) {
                        printf("Demo: Keyboard 'h'\n");
                        send_keyboard(0, k, 1);
                    }
                    break;
                }
                case 2:
                    if (mouse_notify) {
                        printf("Demo: Mouse move (+20,+10)\n");
                        send_mouse(0, 20, 10, 0);
                        send_mouse(0, 0, 0, 0);   /* release */
                    }
                    break;
                }
                if (default_conn && (kbd_notify || mouse_notify || consumer_notify)) {
                    demo_step = (demo_step + 1) % 3;
                }
#else
                if (consumer_notify) {
                    printf("Selfie: Consumer Volume+\n");
                    send_consumer(0xE9);
                }
#endif
            }
        }
        last = cur;
        vTaskDelay(pdMS_TO_TICKS(20));
    }
}

/* ------------------------------------------------------------------ */
/*  BLE enable callback                                               */
/* ------------------------------------------------------------------ */
static void ble_ready(int err)
{
    if (err) {
        printf("BT enable failed (err %d)\n", err);
        return;
    }
    printf("BLE ready, registering HID composite service\n");

    bt_gatt_service_register(&hids);
    bt_conn_cb_register(&conn_callbacks);

    start_adv();
}

/* ------------------------------------------------------------------ */
/*  BLE host/controller start task                                    */
/* ------------------------------------------------------------------ */
static void app_start_task(void *arg)
{
#if defined(BL602)
    ble_controller_init(configMAX_PRIORITIES - 1);
#else
    btble_controller_init(configMAX_PRIORITIES - 1);
#endif
    hci_driver_init();
    bt_enable(ble_ready);
    vTaskDelete(NULL);
}

/* ------------------------------------------------------------------ */
/*  main                                                              */
/* ------------------------------------------------------------------ */
int main(void)
{
    board_init();

    /* Select the 32.768 kHz low-power clock that the BLE link layer uses as the
     * connection-anchor reference. Mismatching this with the hardware is the #1
     * cause of "connection established then drops with HCI 0x22" on BL602.
     *   HBN_32K_RC   : on-chip RC, present on every module (default, safe)
     *   HBN_32K_XTAL : external 32.768 kHz crystal (use only if populated on board) */
#if defined(BL602)
    HBN_32K_Sel(HBN_32K_RC);
#endif

    /* Init RF (required on BL602) */
    if (0 != rfparam_init(0, NULL, 0)) {
        printf("PHY RF init failed!\n");
        return 0;
    }

    /* Configure the demo/selfie button as input with internal pull-up */
    gpio_dev = bflb_device_get_by_name("gpio");
    bflb_gpio_init(gpio_dev, SELFIE_BTN_PIN,
                   GPIO_INPUT | GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_0);

    /* Start BLE stack in its own task */
    xTaskCreate(app_start_task, "app_start", 1024, NULL,
                configMAX_PRIORITIES - 2, NULL);

    /* Start button polling task */
    xTaskCreate(selfie_task, "selfie", 512, NULL, SELFIE_TASK_PRIO, NULL);

    vTaskStartScheduler();

    for (;;) {
    }
    return 0;
}

──── 0人觉得很赞 ────

使用道具 举报

很棒~
您需要登录后才可以回帖 立即登录
高级模式
返回
统计信息
  • 会员数: 31201 个
  • 话题数: 42282 篇