【DIY电子作品】Ai-M61-32SU 手机蓝牙自拍杆

[复制链接]
查看84 | 回复11 | 5 天前 | 显示全部楼层 |阅读模式

本帖最后由 lazy 于 2024-10-11 14:18 编辑

闲话

开始其实想做蓝牙键盘的,后来顺便把自拍杆功能也实现了。

虽然市面上有很多这样的产品,但是作为DIY爱好者的快乐不就是折腾吗。折腾使我快乐。

比如,刚到手的AiPi-KVM被我用12V点亮的故事

【我和小安派】故(shi)事(gu)AiPi-KVM短暂的一生后续 https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=44995&fromuid=16612

后来买了一堆零件,还想着用烙铁焊上结果焊盘都干掉了。

HID[简]介(就是简单介绍)

The Human Interface Device (HID) ,即人机交互设备。定义了蓝牙在人机接口设备中的协议、特征和使用规程。典型的应用包括蓝牙鼠标、蓝牙键盘、蓝牙游戏手柄等。该协议改编自USB HID Protocol。

手机蓝牙的HID是指人机接口设备。

HID是蓝牙技术中的一种协议,用于描述设备与人之间的交互接口。下面是详细的解释:

  1. HID基本含义:HID是英文“Human Interface Devices”的缩写,中文可以翻译为“人机接口设备”。在蓝牙技术中,HID被广泛应用在各种设备之间,尤其是手机与外设之间。比如,我们常常用手机的蓝牙连接鼠标、键盘等外部设备,这时就会用到HID协议。
  2. 工作原理:当手机通过蓝牙与另一个设备建立连接时,如果另一设备支持HID协议,那么手机就可以识别并与之通信。这种通信允许用户通过这些外设设备进行更直观、便捷的操作。比如,使用蓝牙连接的键盘输入文字,或者使用鼠标移动屏幕上的光标。
  3. 手机中的应用场景:在日常生活中,手机蓝牙的HID功能经常被用于连接各种外部设备,如耳机、音箱、游戏手柄等。这使得手机的功能得到了扩展,提高了用户的使用体验。通过HID协议,这些设备可以与手机快速建立连接,并进行数据传输和控制。

总的来说,手机蓝牙的HID是指人机接口设备协议,它使得手机能够识别并与各种外部设备进行通信,提高了用户的使用体验和便捷性。

详细学习参考可以下资料

【USB系列】自定义USB HID设备(bzhou830)玛丽哥 https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=45000&fromuid=16612

【小安派试玩】基于HID协议的USB键盘测试(iiv)七哥 https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=395&fromuid=16612

(二十)零基础开发小安派-Eyes-S1【番外篇】——BLE基础通讯(起个名字好难啊)莫工 https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=44815&fromuid=16612

用btstack开发一个简单的蓝牙自拍杆

【低功耗蓝牙】⑤ HID协议

USB HID报告描述符教程 - 知乎

这里就不对HID协议进行详细介绍了(其实我也是一知半解),我们这里主题是如何实现自拍功能。

HID自拍原理

其实想要实现蓝牙自拍功能其实比较简单,目前市面上的手机大多都可以通过按“音量-”按键进行拍照。知道了这个实现起来就比较简单了。只要我们能够模拟点击“音量-”按键就可以实现遥控拍照功能。

既然知道了拍照原理下一步我们就要开始想办法通过HID实现这个功能。

前置条件


自拍杆HID报告描述【使用的话把# 替换换成 //】

 # Report ID 1: Advanced buttons
 0x05, 0x0C,       # Usage Page (Consumer)
 0x09, 0x01,       # Usage (Consumer Control)
 0xA1, 0x01,       # Collection (Application)
 0x85, 0x01,       #     Report Id (1)
 0x15, 0x00,       #     Logical minimum (0)
 0x25, 0x01,       #     Logical maximum (1)
 0x75, 0x01,       #     Report Size (1)
 0x95, 0x01,       #     Report Count (1)

 0x09, 0xCD,       #     Usage (Play/Pause)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x0A, 0x83, 0x01, #     Usage (AL Consumer Control Configuration)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x09, 0xB5,       #     Usage (Scan Next Track)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x09, 0xB6,       #     Usage (Scan Previous Track)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)

 0x09, 0xEA,       #     Usage (Volume Down)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x09, 0xE9,       #     Usage (Volume Up)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x0A, 0x25, 0x02, #     Usage (AC Forward)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0x0A, 0x24, 0x02, #     Usage (AC Back)
 0x81, 0x06,       #     Input (Data,Value,Relative,Bit Field)
 0xC0              # End Collection 
 作者:我是鹏老师 https://www.bilibili.com/read/cv15067064/ 出处:bilibili

还有另外一套

// 通用按键
0x05, 0x0C,        // Usage Page (Consumer)
0x09, 0x01,        // Usage (Consumer Control)
0xA1, 0x01,        // Collection (Application)
0x85, 0x03,        //   Report ID (3)
0x15, 0x00,        //   Logical Minimum (0)
0x25, 0x01,        //   Logical Maximum (1)
0x75, 0x01,        //   Report Size (1)
0x95, 0x0B,        //   Report Count (11)
0x0A, 0x23, 0x02,  //   Usage (AC Home)
0x0A, 0x21, 0x02,  //   Usage (AC Search)
0x0A, 0xB1, 0x01,  //   Usage (AL Screen Saver)
0x09, 0xB8,        //   Usage (Eject)
0x09, 0xB6,        //   Usage (Scan Previous Track)
0x09, 0xCD,        //   Usage (Play/Pause)
0x09, 0xB5,        //   Usage (Scan Next Track)
0x09, 0xE2,        //   Usage (Mute)
0x09, 0xEA,        //   Usage (Volume Decrement)
0x09, 0xE9,        //   Usage (Volume Increment)
0x09, 0x30,        //   Usage (Power)
0x0A, 0xAE, 0x01,  //   Usage (AL Keyboard Layout)
0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0x95, 0x01,        //   Report Count (1)
0x75, 0x0D,        //   Report Size (13)
0x81, 0x03,        //   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
0xC0,              // End Collection

有了它我们就可以模拟手机按键了。

具体实现

这里蓝牙部分主要参考官方的教程里面的蓝牙功能

【完全开源】智能桌面助手——AiPi-DSL_Dashboard https://bbs.ai-thinker.com/forum.php?mod=viewthread&tid=42026&fromuid=16612

资料获取

AiPi-DSL_Dashboard资料包地址:https://docs.ai-thinker.com/dsl AiPi-DSL_Dashboard资料包地址(Github): https://github.com/Ai-Thinker-Open/AiPi-Open-Kits/tree/master/AiPi-DSL_Dashboard

目录结构.PNG

项目目录结构


-BLE_HID 负责蓝牙

-main 程序主入口

-wifi MQTT接入准备

程序


main

int main(void)
{
  ……
   // 保留蓝牙相关任务
   xTaskCreate(ble_hid_task, (char*)"ble_hid_task", 1024, NULL, 10, NULL);
   vTaskStartScheduler();
  ……
}

ble_hid_dev.c 蓝牙任务管理

/**
 * @brief HID 任务
 *
 * @param arg
*/
void ble_hid_task(void* arg)
{
    // 主要是通知【lvgl】UI更新蓝牙状态的由于没有屏幕暂时注释以下两行代码
    // ble_queue = xQueueCreate(1, 512);
    // xTaskCreate(queue_receive_ble_task, "queue_ble_task", 1024, arg, 7, NULL);
    vTaskDelay(200/portTICK_RATE_MS);
    hid_key_num_t kb_num;
    btblecontroller_em_config();
    ble_init();
    bas_init();
    dis_init(0x01, 0x07AF, 0x707, 0x2A50);
    hog_kb_init();
    ble_kb_start();
    ble_hid_queue = xQueueCreate(1, 4);
    while (1) {
        xQueueReceive(ble_hid_queue, &kb_num, portMAX_DELAY);
        ble_hid_dev_send(kb_num);
    }
}

ble_hid_dev.h文件中添加

typedef  enum {
    HID_KEY_NUMBLE_NONE = 0,
    HID_KEY_NUMBLE_SELFIE_STICK,// 自拍杆
    ……
}

kb.h 文件中添加

typedef enum {
    KEY_NUMBLE_SELFIE_STICK = 0X10, // 拍照
    ……
}

int send_selfie_stick_value(struct bt_conn* conn, uint8_t* keyboard_cmd); // 拍照指令

修改kb.c

增加

static uint8_t report_selfie_stick_map[] =
{
    // Report ID 1: Advanced buttons
    0x05, 0x0C,       // Usage Page (Consumer)
    0x09, 0x01,       // Usage (Consumer Control)
    0xA1, 0x01,       // Collection (Application)
    0x85, 0x01,       //     Report Id (1)
    0x15, 0x00,       //     Logical minimum (0)
    0x25, 0x01,       //     Logical maximum (1)
    0x75, 0x01,       //     Report Size (1)
    0x95, 0x01,       //     Report Count (1)
    0x09, 0xCD,       //     Usage (Play/Pause)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x0A, 0x83, 0x01, //     Usage (AL Consumer Control Configuration)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x09, 0xB5,       //     Usage (Scan Next Track)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x09, 0xB6,       //     Usage (Scan Previous Track)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x09, 0xEA,       //     Usage (Volume Down)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x09, 0xE9,       //     Usage (Volume Up)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x0A, 0x25, 0x02, //     Usage (AC Forward)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0x0A, 0x24, 0x02, //     Usage (AC Back)
    0x81, 0x06,       //     Input (Data,Value,Relative,Bit Field)
    0xC0              // End Collection
    //通用按键
    // 0x05, 0x0C,        // Usage Page (Consumer)
    // 0x09, 0x01,        // Usage (Consumer Control)
    // 0xA1, 0x01,        // Collection (Application)
    // 0x85, 0x03,        //   Report ID (3)
    // 0x15, 0x00,        //   Logical Minimum (0)
    // 0x25, 0x01,        //   Logical Maximum (1)
    // 0x75, 0x01,        //   Report Size (1)
    // 0x95, 0x0B,        //   Report Count (11)
    // 0x0A, 0x23, 0x02,  //   Usage (AC Home)
    // 0x0A, 0x21, 0x02,  //   Usage (AC Search)
    // 0x0A, 0xB1, 0x01,  //   Usage (AL Screen Saver)
    // 0x09, 0xB8,        //   Usage (Eject)
    // 0x09, 0xB6,        //   Usage (Scan Previous Track)
    // 0x09, 0xCD,        //   Usage (Play/Pause)
    // 0x09, 0xB5,        //   Usage (Scan Next Track)
    // 0x09, 0xE2,        //   Usage (Mute)
    // 0x09, 0xEA,        //   Usage (Volume Decrement)
    // 0x09, 0xE9,        //   Usage (Volume Increment)
    // 0x09, 0x30,        //   Usage (Power)
    // 0x0A, 0xAE, 0x01,  //   Usage (AL Keyboard Layout)
    // 0x81, 0x02,        //   Input (Data,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    // 0x95, 0x01,        //   Report Count (1)
    // 0x75, 0x0D,        //   Report Size (13)
    // 0x81, 0x03,        //   Input (Const,Var,Abs,No Wrap,Linear,Preferred State,No Null Position)
    // 0xC0,              // End Collection
};

经过测试以上两套报告描述都可以拍照,内容稍有不同。

修改

static ssize_t read_report_map(struct bt_conn* conn,
                   const struct bt_gatt_attr* attr, void* buf,
                   uint16_t len, uint16_t offset)
{
    printf("read_report_map:%d \r\n", len);
    // report_selfie_stick_map 这个是自拍杆报告描述
    return bt_gatt_attr_read(conn, attr, buf, len, offset, report_selfie_stick_map,
                 sizeof(report_selfie_stick_map));
}

增加

int send_selfie_stick_value(struct bt_conn* conn, uint8_t* keyboard_cmd)
{
    struct bt_gatt_attr* attr;
    attr = &attrs[BT_CHAR_BLE_HID_REPORT_ATTR_VAL_INDEX];
    return bt_gatt_notify(conn, attr, keyboard_cmd, 1);
}

ble_hid_dev_send方法中添加

  switch (key_num)
    {
      case HID_KEY_NUMBLE_SELFIE_STICK:
      {
            key_vaule[0] = KEY_NUMBLE_SELFIE_STICK; // 0x10
            // 按下音量键-
            send_selfie_stick_value(ble_conn_handle, key_vaule);
            vTaskDelay(100/portTICK_RATE_MS);
            key_vaule[0] = 0x00;
            // 释放音量键-
            send_selfie_stick_value(ble_conn_handle, key_vaule);
            LOG_I("HID SEND: 0x10");
       }
      break;
  ……
}

拍照发送的指令为什么是0x10呢,看下面消息体信息就会理解。

企业微信截图_20241011133209.png

0x10表示音量-

现在消息发送搞定了,那要怎么将消息发出去,如何触发呢。

按钮


目前最简单的就是增加按钮了那么如何增加按钮呢

可以参考,以下两张图摘自

32单片机基础:GPIO输入

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://blog.csdn.net/weixin_64484421/article/details/136200996 ————————————————

原文链接:https://blog.csdn.net/weixin_64484421/article/details/136200996

2dc31d7e0283e8ccb4ff8472a7fef493.png

73030e93f47369e7125543fcc6639de1.png

两种方式,我们一般用下接的方式。

第一个图:注意点。当按键按下,PA0接地,被置为低电平, 但是一旦按键松手,PA0悬空,引脚电压不确定。所以无论怎么读引脚也不知道知否被按下,所以为了解决这个问题,所以必须要求PA0是上拉输入的模式,这样引脚悬空的话,就会被置为高电平,这样我们我们就可以读取PA0的电压就知道按键是否被按下。

但是第二个图就不会出现问题,按下时,被置为低电平,松手,由于上拉电阻的作用,被置为高电平。这样引脚就不会出现浮空状态。所以此时PA0可以配置浮空输入和上拉输入。上拉输入,两个电阻共同作用,这样高电平就会更加稳定一些,

第三个图同样注意要使用下拉输入模式。

这里没有上下拉,直接使用的Ai-M61-32SU内部的上拉

struct bflb_device_s* btn_gpio; // 初始化gpio

int btn_clicked = 0;
// 按钮检测任务
static void btn_event(void* args){
    while (1)
    {
        int status = bflb_gpio_read(btn_gpio, GPIO_PIN_14);
        // 检测gpio14是否为低电平,默认上拉高电平
        if(status == 0){
            // 消除抖动
            vTaskDelay(15/portTICK_RATE_MS);
            再判断一次
            if(status == 0){
                // 防止多次触发
                if(btn_clicked){
                    continue;;
                }
                LOG_I("点击");
                btn_clicked = 1;
                hid_key_num_t hid_key_num = HID_KEY_NUMBLE_SELFIE_STICK;
                // 发送音量-按键进行拍照
                xQueueSend(ble_hid_queue, &hid_key_num, portMAX_DELAY);
            }
        }else{
            btn_clicked = 0;
        }
    }

}

int main(void)
{
    board_init();

    // gpio初始化
    btn_gpio = bflb_device_get_by_name("gpio");
    // 默认上拉
    bflb_gpio_init(btn_gpio, GPIO_PIN_14, GPIO_INPUT| GPIO_PULLUP | GPIO_SMT_EN | GPIO_DRV_1);
    ……
    // 创建按钮检测任务
    xTaskCreate(btn_event, "btn_event", 1024, NULL, 1, NULL);
    ……
    vTaskStartScheduler();
}

以上就完成了自拍杆的全部功能了。

企业微信截图_20241011140846.png

这个就是发现的自拍杆设备蓝牙名称与外观。

其他


这里有个有意思的地方就是可以改变蓝牙的外观图标。HID服务的UUID是0x1812,鼠键的外观是0x03C0,键盘的外观是0x03C1,鼠标的外观是0x03C2,游戏手柄的外观是0x03C3。

想要改变蓝牙设备外观

修改 kb.h 第10行

#define BLE_APPEARANCE_HID_KEYBOARD 0x03C3

编译并烧录完成后,搜索蓝牙就可以看到效果了。

源码在评论区自取。

目前只实现了功能,外观上还没设计比较丑陋暂时就不上图了。

回复

使用道具 举报

lazy | 5 天前 | 显示全部楼层
源码

ai-m61-hid-devices-master.zip

19.65 KB, 下载次数: 4

源码

回复

使用道具 举报

lazy | 5 天前 | 显示全部楼层
本帖最后由 lazy 于 2024-10-11 14:45 编辑

测试手机为荣耀magic
随便摆了摆,想着自拍杆功能太单一了,WIFI部分完全浪费了,还有就是按键,如果单按键操作有限。
企业微信截图_20241011144302.png 企业微信截图_20241011144322.png
是不是可以做成游戏机的形状。
那几个彩色按钮是从小朋友玩具车上拆下来的。
还有就是电池部分,简单亮了一下排针内部空间差不多能容纳一块800mah-900mah的电池这个配置能用多久。

231803mvzwvdgpzvp0qhgp.jpg
回复 支持 反对

使用道具 举报

爱笑 | 5 天前 | 显示全部楼层
最好是附上外壳,要不然莫工那关过不了。
用心做好保姆工作
回复 支持 反对

使用道具 举报

lazy | 5 天前 | 显示全部楼层
爱笑 发表于 2024-10-11 14:20
最好是附上外壳,要不然莫工那关过不了。

好的,外观正在设计。3D建图不是很熟练正在设计,单独自拍杆功能太单一了。准备多加几个模块增加可玩性
回复 支持 反对

使用道具 举报

很不错哟,正想做这个呢
选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

wifi的初始化不是必须的,没有启动起来是下面的这个没有掉用。哈哈,我把键盘搞起来了。
    if (0 != rfparam_init(0, NULL, 0)) {
        LOG_I("PHY RF init failed!");
        return 0;
    }
选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

爱笑 发表于 2024-10-11 14:20
最好是附上外壳,要不然莫工那关过不了。

DIY都需要外壳吗?
选择去发光,而不是被照亮
回复 支持 反对

使用道具 举报

爱笑 | 5 天前 | 显示全部楼层
bzhou830 发表于 2024-10-11 15:28
DIY都需要外壳吗?

是的,莫工要求外壳。
用心做好保姆工作
回复 支持 反对

使用道具 举报

lazy | 5 天前 | 显示全部楼层
bzhou830 发表于 2024-10-11 15:27
wifi的初始化不是必须的,没有启动起来是下面的这个没有掉用。哈哈,我把键盘搞起来了。
    if (0 != rfpa ...

非常感谢玛丽哥,我试下这个。
WiFi的话其实也比较常用,所以加上也可以。就是一直不明白为啥有了wifi就可以了。
原来是这个的原因
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则