小工具知识网

当前位置: 首页 > 行业知识 > Android模拟蓝牙蓝牙键盘——适配Android和Windows

行业知识

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

2026-11-06 07:08:09 来源:互联网转载

学校寒假有个程序设计比赛,我也一直想要去写一个安卓模拟的蓝牙键盘,这样无论到哪里,比如班班通和没有键盘的电脑设备,有手机就可以操作它,也比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 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设备。

进行注册时会有一个回调mCallback,onAppStatusChanged()调用在注册成功,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()            char keychar = key.charAt(0);            if ((keychar=65)&&(keychar                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);    }    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)        };}                0,0,0,0,0,0,0,0        });// 这是松开按键的报告

上一篇:电视墙服务器怎么接线?

下一篇:怎么使用云服务器ip?