Android模拟蓝牙蓝牙键盘——适配Android和Windows

这篇具有很好参考价值的文章主要介绍了Android模拟蓝牙蓝牙键盘——适配Android和Windows。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

学校寒假有个程序设计比赛,我也一直想要去写一个安卓模拟的蓝牙键盘,这样无论到哪里,比如班班通和没有键盘的电脑设备,有手机就可以操作它,也比USB方便一些。忙活了一个寒假,也走了不少歪路,终于整成了,下面分享一些经验。

(学校的软件设计比赛已经交了终稿了,我的仓库开源在Gitee和GitHub,求求star:
Gitee:https://gitee.com/FengyunTHU/keyboard
GitHub:https://github.com/FengyunTHU/keyboardOFbluetooth)

自己在写代码的过程中也参考了很多CSDN博客,列举如下:
蓝牙HID——将android设备变成蓝牙键盘(BluetoothHidDevice)
仅通过蓝牙HID将安卓手机模拟成鼠标和键盘
使用旧手检做成蓝牙键盘
CSDN上大佬真的很多!

代码思路

①第一步是蓝牙HID的初始化

在安卓API28后开放了BluetoothHidDevice类,主要就是用它来完成。首先是注册HID服务:

mBtAdapter.getProfileProxy(context, new BluetoothProfile.ServiceListener() {
            @Override
            public void onServiceConnected(int profile, BluetoothProfile proxy) {
                Log.d(TAG, "onServiceConnected: " + profile);
                Toast.makeText(context, "Okk_connected_service", Toast.LENGTH_SHORT).show();
                if (profile == BluetoothProfile.HID_DEVICE) {
                    Log.d(TAG, "Proxy received but it isn't hid_OUT");
                    if (!(proxy instanceof BluetoothHidDevice)) {
                        Log.e(TAG, "Proxy received but it isn't hid");
                        return;
                    }
                    Log.d(TAG,"Connecting HID…");
                    mHidDevice = (BluetoothHidDevice) proxy;
                    Log.d(TAG, "proxyOK");
                    BluetoothHidDeviceAppSdpSettings Sdpsettings = new BluetoothHidDeviceAppSdpSettings(
                            HidConfig.KEYBOARD_NAME,
                            HidConfig.DESCRIPTION,
                            HidConfig.PROVIDER,
                            BluetoothHidDevice.SUBCLASS1_KEYBOARD,
                            HidConfig.KEYBOARD_COMBO
                    );
                    if (mHidDevice != null) {
                        Toast.makeText(context, "OK for HID profile", Toast.LENGTH_SHORT).show();
                        Log.d(TAG, "HID_OK");
                        Log.d(TAG, "Get in register");
                        //getPermission();
                        // 创建一个BluetoothHidDeviceAppSdpSettings对象

                        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                            // TODO: Consider calling
                            Log.d(TAG,"return before register");
                            String[] list = new String[] {
                                    Manifest.permission.BLUETOOTH_SCAN,
                                    Manifest.permission.BLUETOOTH_CONNECT
                            };
                            requestPermissions(activity,list,1);
                            return;
                        }
                        BluetoothHidDeviceAppQosSettings inQos = new BluetoothHidDeviceAppQosSettings(
                                BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 200, 2, 200,
                                10000 /* 10 ms */, 10000 /* 10 ms */);
                        BluetoothHidDeviceAppQosSettings outQos = new BluetoothHidDeviceAppQosSettings(
                                BluetoothHidDeviceAppQosSettings.SERVICE_GUARANTEED, 900, 9, 900,
                                10000 /* 10 ms */, 10000 /* 10 ms */);
                        mHidDevice.registerApp(Sdpsettings, null, null, Executors.newCachedThreadPool(), mCallback);
                        // registerApp();// 注册
                    } else {
                        Toast.makeText(context, "Disable for HID profile", Toast.LENGTH_SHORT).show();
                    }
                    // 启用设备发现
                    // requestLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE));
                    Log.d(TAG, "Discover");
                }
            }

            @SuppressLint("MissingPermission")
            @Override
            public void onServiceDisconnected(int profile) {// 断开连接
                if (profile == BluetoothProfile.HID_DEVICE) {
                    Log.d(TAG, "Unexpected Disconnected: " + profile);
                    mHidDevice = null;
                    mHidDevice.unregisterApp();
                }
            }
        }, BluetoothProfile.HID_DEVICE);
    }



    public final BluetoothHidDevice.Callback mCallback = new BluetoothHidDevice.Callback() {
        private final int[] mMatchingStates = new int[]{
                BluetoothProfile.STATE_DISCONNECTED,
                BluetoothProfile.STATE_CONNECTING,
                BluetoothProfile.STATE_CONNECTED
        };
        @Override
        public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) {
            Log.d(TAG, "ccccc_str");
            if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                // TODO: Consider calling
                return;
            }
            Log.d(TAG, "onAppStatusChanged: " + (pluggedDevice != null ? pluggedDevice.getName() : "null") + "registered:" + registered);
            // Toast.makeText(context, "onAppStatusChanged", Toast.LENGTH_SHORT).show();
            IsRegisted = registered;
            if (registered) {
                // 应用已注册
                Log.d(TAG, "register OK!.......");
//                List<BluetoothDevice> matchingDevices = mHidDevice.getDevicesMatchingConnectionStates(mMatchingStates);
//                Log.d(TAG, "paired devices: " + matchingDevices + "  " + mHidDevice.getConnectionState(pluggedDevice));
//                Toast.makeText(context, "paired devices: " + matchingDevices + "  " + mHidDevice.getConnectionState(pluggedDevice), Toast.LENGTH_SHORT).show();
//                if (pluggedDevice != null && mHidDevice.getConnectionState(pluggedDevice) != BluetoothProfile.STATE_CONNECTED) {
//                    boolean result = mHidDevice.connect(pluggedDevice);// pluggedDevice即为连接到模拟HID的设备
//                    Log.d(TAG, "hidDevice connect:" + result);
//                    Toast.makeText(context, "hidDevice connect:" + result, Toast.LENGTH_SHORT).show();
//                } else if (matchingDevices != null && matchingDevices.size() > 0) {
//                    // 选择连接的设备
//                    mHostDevice = matchingDevices.get(0);// 获得第一个已经配对过的设备
//                    Toast.makeText(context, "device_is_ok: " + mHostDevice.getName() + mHostDevice.getAddress(), Toast.LENGTH_SHORT).show();
//                } else {
//                    // 注册成功未配对
//                }
            }
//            } else {
//                // 应用未注册
//            }
        }

        @Override
        public void onConnectionStateChanged(BluetoothDevice device, int state) {
            Log.d(TAG, "onConnectStateChanged:" + device + "  state:" + state);
            // Toast.makeText(context, state, Toast.LENGTH_SHORT).show();
            if (state == BluetoothProfile.STATE_CONNECTED) {// 已经连接了
                connected = true;
                mHostDevice = device;
                if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) != PackageManager.PERMISSION_GRANTED) {
                    // TODO: Consider calling
                    return;
                }
                Log.d(TAG,"hid state is connected");
                Log.d(TAG,"-----------------------------------connected HID");
                Log.d(TAG,device.getName().toString());
                // Toast.makeText(context, "device_is_ok: " + mHostDevice.getName() + mHostDevice.getAddress(), Toast.LENGTH_SHORT).show();
            } else if (state == BluetoothProfile.STATE_DISCONNECTED) {
                connected = false;
                Log.d(TAG,"hid state is disconnected");
                // mHostDevice = null;
                // Toast.makeText(context, "device_is_null", Toast.LENGTH_SHORT).show();
            } else if (state == BluetoothProfile.STATE_CONNECTING) {
                Log.d(TAG,"hid state is connecting");
            }
        }
    };

mBtAdapter.getProfileProxy()中注册,其中onServiceConnected()会在开始注册时调用,其中的mHidDevice.registerApp()就是注册采用的方法,提供的SdpSettings是最主要的的HID描述,其中定义一系列常量用于描述模拟的HID设备
进行注册时会有一个回调mCallbackonAppStatusChanged()调用在注册成功,onConnectStateChanged()则是在蓝牙连接状态改变时调用,如连接上、断开、正在连接,其中的日志可以反应蓝牙连接的状态。
注意注册HID时,蓝牙必须处于打开状态。打开蓝牙的代码我暂未编写。

②发起蓝牙连接

在发起连接上,我试了从电脑端发起连接、从手机端发起连接,而且使用的都是点进系统蓝牙列表的方式,均无法建立稳定连接。
后来看到大佬文章,解决了这个问题,即使用代理连接:

@SuppressLint("MissingPermission")
    public void ConnectotherBluetooth() {
        mHostDevice = mBtAdapter.getRemoteDevice("B4:8C:9D:AD:9B:9A");
        if (mHostDevice!=null) {
            Log.d(TAG,"Connected is OK");
            Log.d(TAG,mHostDevice.getName());
        }
        mHidDevice.connect(mHostDevice);// 代理连接
    }

只要把mac地址改成所想要连接的蓝牙设备的mac即可。电脑可以采用cmd指令ipconfig /all,拉到最底即可;手机使用adb连接后,输入指令adb shell settings get secure bluetooth_address即可。当然也可以直接扫描,但我目前还未完成相关代码。

③发送报告
    @JavascriptInterface
    @SuppressLint("MissingPermission")
    public void sendKey(String key) {
        byte b1 = 0;
        if (key.length()<=1) {
            char keychar = key.charAt(0);
            if ((keychar>=65)&&(keychar<=90)){
                b1 = 2;
            }
        }
        if (keyMap.SHITBYTE.containsKey(key)) {
            b1 = 2;
        }
        Log.d(TAG,"pre_send: "+key);

        mHidDevice.sendReport(mHostDevice,8,new byte[]{
                b1,0,keyMap.KEY2BYTE.get(key.toUpperCase()),0,0,0,0,0
        });
        mHidDevice.sendReport(mHostDevice,8,new byte[]{
                0,0,0,0,0,0,0,0
        });// 这是松开按键的报告
        Log.d(TAG,"after_send: "+key);
    }

发送报告使用sendReport(),发送对应ID字节的报告即可。

整体写完其实代码量并不多,但是前期对API的研究还是挺费时间的。

完成代码后,耗时间的还有一些配置:

HidConfig.java——HID配置文件

这玩意在安卓上适配都挺好,但Windows上会有一些问题。我自己找了一版描述符,目前是正常的(也是在GitHub上找的):

public class HidConfig {
    public final static String KEYBOARD_NAME = "My Keyboard";
    public final static String DESCRIPTION = "KKKey";
    public final static String PROVIDER = "Alphabet";
    public final static byte ID_KEYBOARD = 1;

    // HID码表【不知道干啥的】
    public static final byte[] KEYBOARD_COMBO =
        {
				(byte) 0x05, (byte) 0x01,                         // Usage Page (Generic Desktop)
                (byte) 0x09, (byte) 0x06,                         // Usage (Keyboard)
                (byte) 0xA1, (byte) 0x01,                         // Collection (Application)
                (byte) 0x85, (byte) 0x08,                         //   REPORT_ID (Keyboard)
                (byte) 0x05, (byte) 0x07,                         //   Usage Page (Key Codes)
                (byte) 0x19, (byte) 0xE0,                         //   Usage Minimum (224)
                (byte) 0x29, (byte) 0xE7,                         //   Usage Maximum (231)
                (byte) 0x15, (byte) 0x00,                         //   Logical Minimum (0)
                (byte) 0x25, (byte) 0x01,                         //   Logical Maximum (1)
                (byte) 0x75, (byte) 0x01,                         //   Report Size (1)
                (byte) 0x95, (byte) 0x08,                         //   Report Count (8)
                (byte) 0x81, (byte) 0x02,                         //   Input (Data, Variable, Absolute)
                (byte) 0x95, (byte) 0x01,                         //   Report Count (1)
                (byte) 0x75, (byte) 0x08,                         //   Report Size (8)
                (byte) 0x81, (byte) 0x01,                         //   Input (Constant) reserved byte(1)
                (byte) 0x95, (byte) 0x05,                         //   Report Count (5)
                (byte) 0x75, (byte) 0x01,                         //   Report Size (1)
                (byte) 0x05, (byte) 0x08,                         //   Usage Page (Page# for LEDs)
                (byte) 0x19, (byte) 0x01,                         //   Usage Minimum (1)
                (byte) 0x29, (byte) 0x05,                         //   Usage Maximum (5)
                (byte) 0x91, (byte) 0x02,                         //   Output (Data, Variable, Absolute), Led report
                (byte) 0x95, (byte) 0x01,                         //   Report Count (1)
                (byte) 0x75, (byte) 0x03,                         //   Report Size (3)
                (byte) 0x91, (byte) 0x01,                         //   Output (Data, Variable, Absolute), Led report padding
                (byte) 0x95, (byte) 0x06,                         //   Report Count (6)
                (byte) 0x75, (byte) 0x08,                         //   Report Size (8)
                (byte) 0x15, (byte) 0x00,                         //   Logical Minimum (0)
                (byte) 0x25, (byte) 0x65,                         //   Logical Maximum (101)
                (byte) 0x05, (byte) 0x07,                         //   Usage Page (Key codes)
                (byte) 0x19, (byte) 0x00,                         //   Usage Minimum (0)
                (byte) 0x29, (byte) 0x65,                         //   Usage Maximum (101)
                (byte) 0x81, (byte) 0x00,                         //   Input (Data, Array) Key array(6 bytes)
                (byte) 0xC0                                       // End Collection (Application)
        };
}

也确实尝试了很多版,这版可以。但需要注意其中的(byte) 0x85, (byte) 0x08, // REPORT_ID (Keyboard)反映了报告的ID = 8,需要和report中的相对应,如:

mHidDevice.sendReport(mHostDevice,/*ID = 8*/8,new byte[]{
                0,0,0,0,0,0,0,0
        });// 这是松开按键的报告

②Windows上的适配

实际测试发现,Android适配很好,但Windows总是没反应,也困扰了我很长时间。
后来使用Wireshark对蓝牙抓包,发现安卓是这样的:
android 设备做从模拟鼠标键盘,android,windows,计算机外设
而Windows总是这样:
android 设备做从模拟鼠标键盘,android,windows,计算机外设
显示正在Pending,无法直接success;而且相同SCID的request最后会以PSM not support请求失败。HID-Control对应的PSM是0x0011
在Windows开发文档上看到了以下:

接收传入 L2CAP 连接请求

若要接收来自特定 PSM 的任何远程设备的传入 L2CAP 连接请求,配置文件驱动程序应首先生成并发送 BRB_L2CA_REGISTER_标准版RVER 请求,并在请求的 _BRB_L2CA_REGISTER_标准版RVER 结构的 Psm 成员中指定 NULL,并在请求的 _BRB_L2CA_REGISTER_标准版RVER 结构的 Psm 成员中指定 NULL。 发送 BRB_L2CA_REGISTER_标准版RVER 请求时,配置文件驱动程序还必须向蓝牙驱动程序堆栈注册 L2CAP 回调函数。 这使蓝牙驱动程序堆栈能够通知配置文件驱动程序传入 L2CAP 连接请求。
然后,配置文件驱动程序应生成并发送BRB_REGISTER_PSM请求,以便蓝牙驱动程序堆栈将接受请求注册的 PSM 的连接。 否则,蓝牙驱动程序堆栈将拒绝具有未知(未注册)连接请求的所有连接请求。 有关 PSM 的详细信息,请参阅 _BRB_PSM 结构。

所以就感觉是不是驱动的问题。最后下载更新最新版的蓝牙驱动即可。注意更新完后要重启。
于是问题就解决了。抓包结果是,虽然也不是立刻success,但是最后依然请求成功。这估计是因为Windows多了以上的请求过程机制。

④一些连接问题

  • 不要在手机或电脑的系统列表中点击设备进行连接。 直接在模拟键盘端使用connect()的代理连接即可,直接连到mac地址;
  • 需要两个设备提前配对。当然在发起连接的过程中配对也可以。如果无法连接,尝试删除设备后在重新配对连接;
  • iOSMac系统,因为我没有对应的设备,没有进行测试。不过也可以参考我参考文章中的第二篇;
  • 注意到很容易断开连接。所以可能需要在断开时控制继续连上。测试下来继续连接的用时是很短的

所有代码开源在Gitee仓库keyboard。笔者不是计算机专业的学生,甚至所学专业相差甚远;作为在校大学生,时间也十分有限,Java也是边写边学的,代码格式不规范有劳大家谅解。文章来源地址https://www.toymoban.com/news/detail-860930.html

到了这里,关于Android模拟蓝牙蓝牙键盘——适配Android和Windows的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Android连接蓝牙设备问题(android.permission.BLUETOOTH)

            近期遇到一个问题,之前发布的APP连接蓝牙都是正常的,现在有人反映连不上了。经过测试发现:android 12 和 harmonyOS 3.0.0 都会有这个问题,而之前的版本就不会有这个。         经过网上一番查找,原来是因为最近Google发布的Android 12,新引入了 BLUETOOTH_SCAN、

    2024年01月16日
    浏览(47)
  • RK3288 Android11 RTL8723DS WiFi 和 蓝牙Bluetooth 适配

      瑞芯微RK系列对“REALTEK瑞昱公司”的wifi、蓝牙双模的模组都有内置适配的,因此HAL层、Framework层、协议栈及库文件都不需要移植适配,只需修改设备树和一些配置文件即可。 补充: RTL8723DS时钟输入源讲解   RTL8723DS模组的第24号引脚是模组的时钟输入源,需要给此提供

    2024年02月07日
    浏览(78)
  • 《Android学习笔记》Android12蓝牙扫描不到设备的权限问题

    Android12 关于蓝牙这部分新增了 BLUETOOTH_SCAN 、 BLUETOOTH_ADVERTISE 和 BLUETOOTH_CONNECT 权限,这些权限都属于敏感权限,都需要在代码中动态申请。移除了Android11 及以下版本中必须申请的位置权限[ FINE_LOCATION ] 和 [ COARES_LOCATION ]。 1、在Manifest.xml清单文件中添加对应的权限。 其中 An

    2024年02月15日
    浏览(51)
  • 基于XG24-EK2703A的BLE HID蓝牙键盘+鼠标复合设备功能开发(BLE+HID+FreeRTOS+Gecko SDK)

    👉 【Funpack3-1】基于XG24-EK2703A的BLE HID蓝牙键盘+鼠标复合设备 👉 Github: EmbeddedCamerata/XG24_ble_hid_keymouse 本项目基于Silicon Labs XG24-EK2703A开发板,通过HID协议实现了一个蓝牙键盘+鼠标复合设备,可通过按键实现上下翻页、发送字符功能。使用板载两个按键,当BTN0按下,向上翻页;

    2024年01月25日
    浏览(47)
  • 【Unity 实用插件篇】 | UI适配神器 Device Simulator 移动设备模拟器 的详细使用方法

    前言 今天带来的是Unity提供的一个设备模拟器 Device Simulator 。 它可以帮助开发者在编辑器中模拟出移动端的环境,直接进行测试。 所有操作都可以在编辑器上进行#

    2024年02月11日
    浏览(45)
  • Android 全局监听软键盘弹起隐藏 动态修改布局并适配无限循环的问题

    要在 Android 应用中全局检测软键盘的弹起,您可以使用 ViewTreeObserver.OnGlobalLayoutListener 监听器来监听布局树的变化。当软键盘弹起或隐藏时,布局树会发生变化,因此您可以在监听器中捕获这些变化。 在上面的代码中, rootView 是您布局的根视图,您需要将其替换为您实际布局

    2024年02月11日
    浏览(40)
  • 【Unity 实用工具篇】✨ | UI适配神器 Device Simulator 移动设备模拟器 的详细使用方法

    前言 今天带来的是Unity提供的一个设备模拟器 Device Simulator 。 它可以帮助开发者在编辑器中模拟出移动端的环境,直接进行测试。 所有操作都可以在编辑器上进行#

    2024年02月14日
    浏览(46)
  • Android Ble蓝牙App(六)请求MTU与显示设备信息

      在上一篇文章中已经了解了数据操作的方式,而数据交互的字节长度取决于我们手机与蓝牙设备的最大支持长度。 Ble蓝牙App(一)扫描 Ble蓝牙App(二)连接与发现服务 Ble蓝牙App(三)特性和属性 Ble蓝牙App(四)UI优化和描述符 Ble蓝牙App(五)数据操作 Ble蓝牙App(六)

    2024年02月04日
    浏览(46)
  • Android HID设备(键盘、遥控等)功能实现流程及键值映射关系

    HID (Human Interface Device,人机接口设备)是USB设备中常用的设备类型,是直接与人交互的USB设备,例如键盘、遥控器、鼠标与游戏杆等。在USB设备中,HID设备的成本较低。     之前文章 android 键盘(遥控)键值定义大全 中整理了android中各种功能键值定义,那么从键盘按键到

    2024年02月04日
    浏览(39)
  • 如何启动Android studio 中的设备模拟器

    一、在 Android Studio 中单独启动设备模拟器 打开 Android Studio,确保您已经创建了一个虚拟设备或者正在运行一个设备模拟器。 在工具栏中找到并点击 \\\"AVD Manager\\\" 按钮。 在 AVD Manager 窗口中,找到您要启动的设备模拟器,并点击其右侧的 \\\"Play\\\" 按钮。 此时,Android Studio 将会启动

    2024年02月12日
    浏览(58)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包