Contributed by bzhou830, organized by Ai-Thinker
Ai-WB2 BLE HID Three-in-One Composite Device
Ai-WB2 BLE HID Three-in-One Composite Device
From principle to implementation, covering the SDK, development platform, compilation, flashing, testing/verification, and platform compatibility. This version is fully usable on Android; however, Windows cannot recognize it as a system device because the Boot Protocol characteristic is missing.
1. Project Introduction
This project implements a HID-over-GATT (HOGP) selfie stick / composite input device using BLE (Bluetooth Low Energy) on the BL602 (Ai-Thinker Ai-WB2) module. It exposes three HID interfaces simultaneously within a single GATT service:
| Report ID | Interface | Purpose | Report Length | Usage Page / Usage |
|---|---|---|---|---|
| 1 | Keyboard | Sends key presses (the demo sends the letter h by default) | 8 bytes | Generic Desktop / Keyboard |
| 2 | Mouse | Relative movement + 3 buttons + wheel | 4 bytes | Generic Desktop / Mouse |
| 3 | Consumer | Multimedia: volume up/down, play/pause, previous/next track, mute | 2 bytes | Consumer Control |
Key point: most phones treat the Consumer Control Volume Increment (Usage 0xE9) as the "shutter button", so the original selfie stick behavior is implemented via 0xE9 with Report ID 3 — this project keeps it as one of the three interfaces in the combo.
Button: GPIO12 (active-low, internal pull-up); each press cycles through the three demos — Keyboard → Mouse → Consumer, making it easy to verify each interface one by one.
2. How It Works
2.1 HID over GATT (HOGP) Protocol Stack
BLE HID devices do not use classic Bluetooth (BR/EDR); instead, HID reports are packed into GATT characteristic values and pushed to the host via Notification:
Click to expand full code
[主机 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 service UUID:
0x1812(Primary Service). - Report Map (0x2A4B): a byte stream of HID descriptors that tells the host the field layout of each Report ID.
- HID Information (0x2A4A): bcdHID version, country code, flags.
- Protocol Mode (0x2A4E): 0x00 = Boot Protocol, 0x01 = Report Protocol (this project defaults to Report).
- HID Control Point (0x2A4C): the host writes
0x00to suspend and0x01to resume the device. - Input Report (0x2A4D): the actual data channel, configured with
BT_GATT_CHRC_NOTIFY+ a CCC descriptor.
2.2 Report Map Split Across the Three Interfaces
This project uses a single merged Report Map, distinguishing the three top-level collections by Report ID (the 0x85 tag):
Click to expand full code
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 音量+)Each Input Report characteristic carries a Report Reference descriptor (referencing 0x2A4D, UUID 0x2908), whose value is {id, type}, allowing the host to map "which characteristic" to "which Report ID" (see kbd_report_ref / mouse_report_ref / consumer_report_ref in main.c).
2.3 BLE Link Clock
The BL602 BLE Link Layer uses the 32.768 kHz low-power clock as the connection-anchor reference. A clock source that does not match the hardware is the #1 cause of "the connection drops right after connecting (HCI error 0x22 / 0x13)". This project explicitly selects the on-chip RC in main():
Click to expand full code
#
if
defined(BL602)
HBN_32K_Sel(HBN_32K_RC);
/* 片上 RC,所有模组都有,最稳 */
/* HBN_32K_XTAL 仅当板载外部 32.768k 晶振时才用 */
#
endif2.4 Automatic Re-advertising After Disconnection
The disconnection callback disconnected() directly calls start_adv() again, ensuring the host can reconnect deterministically, without needing a manual reset because the device "stopped advertising".
2.5 Software Architecture
Click to expand full code
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. Development Platform and SDK
| Item | Description |
|---|---|
| Chip | Bouffalo Lab BL602 (32-bit RISC-V, up to 192 MHz) |
| Module | Ai-Thinker Ai-WB2 (onboard BL602 + RF + PCB antenna) |
| SDK | bouffalo_sdk (Bouffalo Lab unified SDK) |
| BLE stack | blestack (Zephyr/NimBLE-style Host API: bt_* , gatt.h) |
| RTOS | FreeRTOS (vTaskStartScheduler starts the scheduler) |
| GPIO driver | bflb_gpio (Bouffalo peripheral library, not HOSAL) |
| Build system | CMake (invoked via the tools/make/make wrapper) |
| Flashing tool | Bouffalo Flash Cube (BLFlashCommand.exe, UART download) |
| Target board | BOARD=bl602dk , CHIP=bl602 |
3.1 Dependencies (Windows)
- bouffalo_sdk: local path
D:/bouffalo_sdk(use Windows drive-letter style, not MSYS/d/...). - Toolchain: the SDK ships with RISC-V GCC (no separate installation needed).
- Python: the SDK build scripts depend on Python (3.x recommended).
- Serial port: the device appears as
COMxvia a USB-to-serial adapter (e.g., CH340); useCOM6for flashing (change as needed).
4. Project Structure
Click to expand full code
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)Key items in flash_prog_cfg.ini: boot2 at 0x000000, partition at 0xE000, the FW address references the partition table via @partition (0x10000), and the filename template ble_hid_selfie*_$(CHIPNAME)*.bin must match the actual build artifact name.
5. Compilation
Must be run in Git Bash / native bash (not MSYS paths), with
BL_SDK_BASEusing a Windows-style path.
5.1 Compile Only
Click to expand: cd
cd
ble_hid_selfie
BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602 \
/d/bouffalo_sdk/tools/make/makeOr use the make wrapper (when the SDK is already on PATH or discoverable):
Click to expand: make BL_SDK_BASE=D:/bouffalo_s
make BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602After success, the following artifacts are generated:
Click to expand full code
build/build_out/ble_hid_selfie_bl602.bin # 主固件(约 248 KB)
build/build_out/boot2_bl602_release_v8.1.3.bin
build/build_out/partition.bin5.2 Common Compilation Pitfalls
| Symptom/Error | Cause | Fix |
|---|---|---|
| find_package(bouffalo_sdk) fails | BL_SDK_BASE not set or /d/... used | Explicitly pass BL_SDK_BASE=D:/bouffalo_sdk |
| Link error: ble_controller_init not found | Wrongly used BL616/618's btble_controller_init | Use ble_lib_api.h + ble_controller_init on BL602 |
| GPIO_FUNC_GPIO undefined | Only BL616/618 have this macro | Use bflb_gpio_init(..., GPIO_INPUT |
| cli.h: No such file | CONFIG_BT_STACK_CLI is enabled | Set CONFIG_BT_STACK_CLI=n in defconfig |
6. Flashing
6.1 Command
Click to expand: cd
cd
ble_hid_selfie
BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602 \
COMX=COM6 BAUDRATE=921600 \
/d/bouffalo_sdk/tools/make/make flashCOMX=COM6: the download serial port (change to the actual port in Device Manager).BAUDRATE=921600: the serial baud rate (the variable isBAUDRATE, notb=).make flashcompiles first, then flashes, relying onflash_prog_cfg.ini.
6.2 Before Flashing
- Close the serial monitor occupying the port (e.g., PuTTY, a serial assistant), otherwise you get
PermissionError. - Confirm that
COM6is indeed the device port (check CH340/CP210x in Device Manager). - The board is powered normally (USB connected).
6.3 Entering Download Mode (Important)
The tool auto-resets the chip into download mode via DTR/RTS. If the handshake stalls at Start handshake / reports LOAD HELP BIN FAIL:
- Enter download mode manually: press and hold the BOOT button on the board (GPIO8 pulled low) → press RESET once → release RESET → release BOOT.
- Success is indicated when the log shows
Handshake succeeded/The ack data is b'4f4b'.
6.4 Success Indicators
The following appears at the end of the log:
Click to expand full code
Flash writing succeeded
All programming completed successfullyIf all three sections (boot2 / partition / FW) show Verification succeeded, the flashing is correct.
7. Testing and Verification
7.1 Serial Logs
After flashing, reset the board and open a serial monitor (115200 or the SDK default baud rate); you should see:
Click to expand full code
BLE ready, registering HID composite service
Advertising started - connect to 'Ai-WB2 Combo'After connecting:
Click to expand full code
BLE connected7.2 Verifying on Android (Recommended)
- Turn on Bluetooth on the phone, search for and pair with "Ai-WB2 Combo".
- After pairing, the system recognizes it as a "keyboard" / "mouse" / "multimedia" composite device.
- Open a text input field (e.g., Notes), press the GPIO12 button on the board:
- 1st press → Consumer
Volume+(acts as the shutter; camera apps will trigger the shutter) - 2nd press → the keyboard sends the letter
h(happears in the input field) - 3rd press → the mouse moves relatively
(+20,+10) - And so on, cycling
- 1st press → Consumer
- The serial log prints
Demo: Consumer Volume+/Demo: Keyboard 'h'/Demo: Mouse moveand so on.
For a selfie stick only: change
DEMO_MODEat the top ofmain.cto0, and the button will only send Consumer0xE9(shutter).
7.3 Verifying on Linux / nRF Connect (Optional)
- Scan with nRF Connect:
Ai-WB2 Combois visible, and the service list includes0x1812 (HIDS). - Expanding it, you can read the Report Map (0x2A4B), HID Information (0x2A4A), and Protocol Mode (0x2A4E).
- Write
0x0001to the CCC of the three Input Reports to enable Notification; pressing the button then delivers notification packets.
7.4 Verifying Reconnection After Disconnection
After connecting, manually turn off the phone's Bluetooth and turn it back on, then re-pair; the device log prints BLE disconnected (reason 0x..) and immediately Advertising started, and you can connect again.
8. Platform Compatibility
| Platform | Status | Description |
|---|---|---|
| Android | ✅ Fully working | Native HOGP composite device support; all three interfaces work |
| Linux | ✅ Working | Generic HID stack; verifiable with nRF Connect / bluez |
| macOS | ⚠️ Mostly working | Supports HOGP; behaves similarly to Linux; not specifically verified |
| Windows | ❌ Not recognized in this version | The system requires Boot Protocol characteristics (0x2A22/0x2A32/0x2A33) + Report ID prefix |
9. Key Implementation Quick Reference (main.c)
| Location | Content |
|---|---|
| main.c:39 | SELFIE_BTN_PIN 12 button GPIO |
| main.c:45 | DEMO_MODE 1 cycles the three interfaces / 0 shutter only |
| main.c:72 | hid_info : bcdHID=0x0111, flags=0x00 |
| main.c:101 | report_map[] : merged HID descriptors for the three interfaces |
| main.c:313 | hids_attrs[] : HIDS GATT service definition |
| main.c:407 | disconnected() : re-advertises via start_adv() on disconnection |
| main.c:454 | send_keyboard() : sends all-zeros release 20ms after the key press |
| main.c:473 | send_mouse() : 4-byte relative movement |
| main.c:488 | send_consumer() : 2-byte 16-bit usage, 40ms release |
| main.c:604 | HBN_32K_Sel(HBN_32K_RC) prevents disconnections |
| main.c:582 | ble_controller_init() (BL602-specific) |
10. Frequently Asked Questions (FAQ)
Q1: It connects but drops after a few seconds (log 0x22 / 0x13)? A: Almost always a 32.768K clock source issue. Make sure main() has HBN_32K_Sel(HBN_32K_RC); if the board has an external crystal, use HBN_32K_XTAL instead.
Q2: Flashing reports LOAD HELP BIN FAIL? A: The chip did not enter download mode. First close the serial monitor, then manually press BOOT+RESET to enter download mode and re-run make flash.
Q3: The button does not respond? A: First confirm that it is BLE connected and the corresponding CCC is enabled (Notification). The send_* functions check the *_notify flag at the start and will not send if it is not enabled. The serial prints the demo branch, which helps you determine this.
Q4: Just want a pure selfie stick? A: Set DEMO_MODE to 0; the button always sends Consumer 0xE9 (shutter). The other interfaces remain but are never triggered.
11. References
- bouffalo_sdk:
D:/bouffalo_sdk(includesexamples/peripheral/ble/,tools/bflb_tools) - HID Usage Tables: USB-IF HID Usage Tables (Keyboard/Mouse/Consumer pages)
- Bluetooth SIG: HID Service Specification (HIDS 0x1812)
- Ai-Thinker Ai-WB2 module documentation
Click to expand full code
#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;
}
