Skip to content

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 IDInterfacePurposeReport LengthUsage Page / Usage
1KeyboardSends key presses (the demo sends the letter h by default)8 bytesGeneric Desktop / Keyboard
2MouseRelative movement + 3 buttons + wheel4 bytesGeneric Desktop / Mouse
3ConsumerMultimedia: volume up/down, play/pause, previous/next track, mute2 bytesConsumer 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
html
[主机 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 0x00 to suspend and 0x01 to 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
html
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).

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
c
#
if
 defined(BL602)

    HBN_32K_Sel(HBN_32K_RC);
/* 片上 RC,所有模组都有,最稳 */

/* HBN_32K_XTAL 仅当板载外部 32.768k 晶振时才用 */

#
endif

2.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
html
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

ItemDescription
ChipBouffalo Lab BL602 (32-bit RISC-V, up to 192 MHz)
ModuleAi-Thinker Ai-WB2 (onboard BL602 + RF + PCB antenna)
SDKbouffalo_sdk (Bouffalo Lab unified SDK)
BLE stackblestack (Zephyr/NimBLE-style Host API: bt_* , gatt.h)
RTOSFreeRTOS (vTaskStartScheduler starts the scheduler)
GPIO driverbflb_gpio (Bouffalo peripheral library, not HOSAL)
Build systemCMake (invoked via the tools/make/make wrapper)
Flashing toolBouffalo Flash Cube (BLFlashCommand.exe, UART download)
Target boardBOARD=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 COMx via a USB-to-serial adapter (e.g., CH340); use COM6 for flashing (change as needed).

4. Project Structure

Click to expand full code
html
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_BASE using a Windows-style path.

5.1 Compile Only

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

Or use the make wrapper (when the SDK is already on PATH or discoverable):

Click to expand: make BL_SDK_BASE=D:/bouffalo_s
bash
make BL_SDK_BASE=D:/bouffalo_sdk BOARD=bl602dk CHIP=bl602

After success, the following artifacts are generated:

Click to expand full code
html
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 Common Compilation Pitfalls

Symptom/ErrorCauseFix
find_package(bouffalo_sdk) failsBL_SDK_BASE not set or /d/... usedExplicitly pass BL_SDK_BASE=D:/bouffalo_sdk
Link error: ble_controller_init not foundWrongly used BL616/618's btble_controller_initUse ble_lib_api.h + ble_controller_init on BL602
GPIO_FUNC_GPIO undefinedOnly BL616/618 have this macroUse bflb_gpio_init(..., GPIO_INPUT
cli.h: No such fileCONFIG_BT_STACK_CLI is enabledSet CONFIG_BT_STACK_CLI=n in defconfig

6. Flashing

6.1 Command

Click to expand: cd
bash
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: the download serial port (change to the actual port in Device Manager).
  • BAUDRATE=921600: the serial baud rate (the variable is BAUDRATE, not b=).
  • make flash compiles first, then flashes, relying on flash_prog_cfg.ini.

6.2 Before Flashing

  1. Close the serial monitor occupying the port (e.g., PuTTY, a serial assistant), otherwise you get PermissionError.
  2. Confirm that COM6 is indeed the device port (check CH340/CP210x in Device Manager).
  3. 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
html
Flash writing succeeded
All programming completed successfully

If 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
html
BLE ready, registering HID composite service
Advertising started - connect to 'Ai-WB2 Combo'

After connecting:

Click to expand full code
html
BLE connected
  1. Turn on Bluetooth on the phone, search for and pair with "Ai-WB2 Combo".
  2. After pairing, the system recognizes it as a "keyboard" / "mouse" / "multimedia" composite device.
  3. 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 (h appears in the input field)
    • 3rd press → the mouse moves relatively (+20,+10)
    • And so on, cycling
  4. The serial log prints Demo: Consumer Volume+ / Demo: Keyboard 'h' / Demo: Mouse move and so on.

For a selfie stick only: change DEMO_MODE at the top of main.c to 0, and the button will only send Consumer 0xE9 (shutter).

7.3 Verifying on Linux / nRF Connect (Optional)

  • Scan with nRF Connect: Ai-WB2 Combo is visible, and the service list includes 0x1812 (HIDS).
  • Expanding it, you can read the Report Map (0x2A4B), HID Information (0x2A4A), and Protocol Mode (0x2A4E).
  • Write 0x0001 to 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

PlatformStatusDescription
Android✅ Fully workingNative HOGP composite device support; all three interfaces work
Linux✅ WorkingGeneric HID stack; verifiable with nRF Connect / bluez
macOS⚠️ Mostly workingSupports HOGP; behaves similarly to Linux; not specifically verified
Windows❌ Not recognized in this versionThe system requires Boot Protocol characteristics (0x2A22/0x2A32/0x2A33) + Report ID prefix

9. Key Implementation Quick Reference (main.c)

LocationContent
main.c:39SELFIE_BTN_PIN 12 button GPIO
main.c:45DEMO_MODE 1 cycles the three interfaces / 0 shutter only
main.c:72hid_info : bcdHID=0x0111, flags=0x00
main.c:101report_map[] : merged HID descriptors for the three interfaces
main.c:313hids_attrs[] : HIDS GATT service definition
main.c:407disconnected() : re-advertises via start_adv() on disconnection
main.c:454send_keyboard() : sends all-zeros release 20ms after the key press
main.c:473send_mouse() : 4-byte relative movement
main.c:488send_consumer() : 2-byte 16-bit usage, 40ms release
main.c:604HBN_32K_Sel(HBN_32K_RC) prevents disconnections
main.c:582ble_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 (includes examples/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
c
#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;
}

Released under the MIT License. Build Time 2026-09-11 14:52:23