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 服务 UUID:
0x1812(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=bl602dk,CHIP=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 |
defconfig 中 CONFIG_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 烧录前准备
- 关闭占用串口的串口监视器(如 PuTTY、串口助手),否则
PermissionError。
- 确认
COM6 确实是设备端口(设备管理器查看 CH340/CP210x)。
- 板子正常上电(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 上验证(推荐)
- 手机打开蓝牙,搜索并配对 「Ai-WB2 Combo」。
- 配对成功后,系统将其识别为「键盘」「鼠标」「多媒体」复合设备。
- 打开一个文本输入框(如备忘录),按板子 GPIO12 按键:
- 第 1 次 → Consumer
Volume+(相当于快门;拍照 App 会触发快门)
- 第 2 次 → 键盘发送字母
h(输入框出现 h)
- 第 3 次 → 鼠标相对移动
(+20,+10)
- 循环往复
- 串口日志会打印
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;
}