學(xué)校寒假有個(gè)程序設(shè)計(jì)比賽,我也一直想要去寫一個(gè)安卓模擬的藍(lán)牙鍵盤,這樣無(wú)論到哪里,比如班班通和沒(méi)有鍵盤的電腦設(shè)備,有手機(jī)就可以操作它,也比USB方便一些。忙活了一個(gè)寒假,也走了不少歪路,終于整成了,下面分享一些經(jīng)驗(yàn)。
(學(xué)校的軟件設(shè)計(jì)比賽已經(jīng)交了終稿了,我的倉(cāng)庫(kù)開(kāi)源在Gitee和GitHub,求求star:
Gitee:https://gitee.com/FengyunTHU/keyboard
GitHub:https://github.com/FengyunTHU/keyboardOFbluetooth)
自己在寫代碼的過(guò)程中也參考了很多CSDN博客,列舉如下:
藍(lán)牙HID——將android設(shè)備變成藍(lán)牙鍵盤(BluetoothHidDevice)
僅通過(guò)藍(lán)牙HID將安卓手機(jī)模擬成鼠標(biāo)和鍵盤
使用舊手檢做成藍(lán)牙鍵盤
CSDN上大佬真的很多!
代碼思路
①第一步是藍(lán)牙HID的初始化
在安卓API28后開(kāi)放了BluetoothHidDevice類,主要就是用它來(lái)完成。首先是注冊(cè)HID服務(wù):
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();
// 創(chuàng)建一個(gè)BluetoothHidDeviceAppSdpSettings對(duì)象
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();// 注冊(cè)
} else {
Toast.makeText(context, "Disable for HID profile", Toast.LENGTH_SHORT).show();
}
// 啟用設(shè)備發(fā)現(xiàn)
// requestLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE));
Log.d(TAG, "Discover");
}
}
@SuppressLint("MissingPermission")
@Override
public void onServiceDisconnected(int profile) {// 斷開(kāi)連接
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) {
// 應(yīng)用已注冊(cè)
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的設(shè)備
// Log.d(TAG, "hidDevice connect:" + result);
// Toast.makeText(context, "hidDevice connect:" + result, Toast.LENGTH_SHORT).show();
// } else if (matchingDevices != null && matchingDevices.size() > 0) {
// // 選擇連接的設(shè)備
// mHostDevice = matchingDevices.get(0);// 獲得第一個(gè)已經(jīng)配對(duì)過(guò)的設(shè)備
// Toast.makeText(context, "device_is_ok: " + mHostDevice.getName() + mHostDevice.getAddress(), Toast.LENGTH_SHORT).show();
// } else {
// // 注冊(cè)成功未配對(duì)
// }
}
// } else {
// // 應(yīng)用未注冊(cè)
// }
}
@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) {// 已經(jīng)連接了
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()
中注冊(cè),其中onServiceConnected()
會(huì)在開(kāi)始注冊(cè)時(shí)調(diào)用,其中的mHidDevice.registerApp()
就是注冊(cè)采用的方法,提供的SdpSettings
是最主要的的HID描述,其中定義一系列常量用于描述模擬的HID設(shè)備。
進(jìn)行注冊(cè)時(shí)會(huì)有一個(gè)回調(diào)mCallback
,onAppStatusChanged()
調(diào)用在注冊(cè)成功,onConnectStateChanged()
則是在藍(lán)牙連接狀態(tài)改變時(shí)調(diào)用,如連接上、斷開(kāi)、正在連接,其中的日志可以反應(yīng)藍(lán)牙連接的狀態(tài)。
注意注冊(cè)HID時(shí),藍(lán)牙必須處于打開(kāi)狀態(tài)。打開(kāi)藍(lán)牙的代碼我暫未編寫。
②發(fā)起藍(lán)牙連接
在發(fā)起連接上,我試了從電腦端發(fā)起連接、從手機(jī)端發(fā)起連接,而且使用的都是點(diǎn)進(jìn)系統(tǒng)藍(lán)牙列表的方式,均無(wú)法建立穩(wěn)定連接。
后來(lái)看到大佬文章,解決了這個(gè)問(wèn)題,即使用代理連接:
@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地址改成所想要連接的藍(lán)牙設(shè)備的mac即可。電腦可以采用cmd指令ipconfig /all
,拉到最底即可;手機(jī)使用adb連接后,輸入指令adb shell settings get secure bluetooth_address
即可。當(dāng)然也可以直接掃描,但我目前還未完成相關(guān)代碼。
③發(fā)送報(bào)告
@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
});// 這是松開(kāi)按鍵的報(bào)告
Log.d(TAG,"after_send: "+key);
}
發(fā)送報(bào)告使用sendReport()
,發(fā)送對(duì)應(yīng)ID和字節(jié)的報(bào)告即可。
整體寫完其實(shí)代碼量并不多,但是前期對(duì)API的研究還是挺費(fèi)時(shí)間的。
完成代碼后,耗時(shí)間的還有一些配置:
①HidConfig.java
——HID配置文件
這玩意在安卓上適配都挺好,但Windows上會(huì)有一些問(wèn)題。我自己找了一版描述符,目前是正常的(也是在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)
};
}
也確實(shí)嘗試了很多版,這版可以。但需要注意其中的(byte) 0x85, (byte) 0x08, // REPORT_ID (Keyboard)
反映了報(bào)告的ID = 8,需要和report中的相對(duì)應(yīng),如:
mHidDevice.sendReport(mHostDevice,/*ID = 8*/8,new byte[]{
0,0,0,0,0,0,0,0
});// 這是松開(kāi)按鍵的報(bào)告
②Windows上的適配
實(shí)際測(cè)試發(fā)現(xiàn),Android適配很好,但Windows總是沒(méi)反應(yīng),也困擾了我很長(zhǎng)時(shí)間。
后來(lái)使用Wireshark對(duì)藍(lán)牙抓包,發(fā)現(xiàn)安卓是這樣的:
而Windows總是這樣:
顯示正在Pending,無(wú)法直接success;而且相同SCID的request最后會(huì)以PSM not support請(qǐng)求失敗。HID-Control對(duì)應(yīng)的PSM是0x0011。
在Windows開(kāi)發(fā)文檔上看到了以下:
接收傳入 L2CAP 連接請(qǐng)求
若要接收來(lái)自特定 PSM 的任何遠(yuǎn)程設(shè)備的傳入 L2CAP 連接請(qǐng)求,配置文件驅(qū)動(dòng)程序應(yīng)首先生成并發(fā)送 BRB_L2CA_REGISTER_標(biāo)準(zhǔn)版RVER 請(qǐng)求,并在請(qǐng)求的 _BRB_L2CA_REGISTER_標(biāo)準(zhǔn)版RVER 結(jié)構(gòu)的 Psm 成員中指定 NULL,并在請(qǐng)求的 _BRB_L2CA_REGISTER_標(biāo)準(zhǔn)版RVER 結(jié)構(gòu)的 Psm 成員中指定 NULL。 發(fā)送 BRB_L2CA_REGISTER_標(biāo)準(zhǔn)版RVER 請(qǐng)求時(shí),配置文件驅(qū)動(dòng)程序還必須向藍(lán)牙驅(qū)動(dòng)程序堆棧注冊(cè) L2CAP 回調(diào)函數(shù)。 這使藍(lán)牙驅(qū)動(dòng)程序堆棧能夠通知配置文件驅(qū)動(dòng)程序傳入 L2CAP 連接請(qǐng)求。
然后,配置文件驅(qū)動(dòng)程序應(yīng)生成并發(fā)送BRB_REGISTER_PSM請(qǐng)求,以便藍(lán)牙驅(qū)動(dòng)程序堆棧將接受請(qǐng)求注冊(cè)的 PSM 的連接。 否則,藍(lán)牙驅(qū)動(dòng)程序堆棧將拒絕具有未知(未注冊(cè))連接請(qǐng)求的所有連接請(qǐng)求。 有關(guān) PSM 的詳細(xì)信息,請(qǐng)參閱 _BRB_PSM 結(jié)構(gòu)。
所以就感覺(jué)是不是驅(qū)動(dòng)的問(wèn)題。最后下載更新最新版的藍(lán)牙驅(qū)動(dòng)即可。注意更新完后要重啟。
于是問(wèn)題就解決了。抓包結(jié)果是,雖然也不是立刻success,但是最后依然請(qǐng)求成功。這估計(jì)是因?yàn)閃indows多了以上的請(qǐng)求過(guò)程機(jī)制。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-860930.html
④一些連接問(wèn)題
-
不要在手機(jī)或電腦的系統(tǒng)列表中點(diǎn)擊設(shè)備進(jìn)行連接。 直接在模擬鍵盤端使用
connect()
的代理連接即可,直接連到mac地址; - 需要兩個(gè)設(shè)備提前配對(duì)。當(dāng)然在發(fā)起連接的過(guò)程中配對(duì)也可以。如果無(wú)法連接,嘗試刪除設(shè)備后在重新配對(duì)連接;
- iOS和Mac系統(tǒng),因?yàn)槲覜](méi)有對(duì)應(yīng)的設(shè)備,沒(méi)有進(jìn)行測(cè)試。不過(guò)也可以參考我參考文章中的第二篇;
- 注意到很容易斷開(kāi)連接。所以可能需要在斷開(kāi)時(shí)控制繼續(xù)連上。測(cè)試下來(lái)繼續(xù)連接的用時(shí)是很短的。
所有代碼開(kāi)源在Gitee倉(cāng)庫(kù)keyboard。筆者不是計(jì)算機(jī)專業(yè)的學(xué)生,甚至所學(xué)專業(yè)相差甚遠(yuǎn);作為在校大學(xué)生,時(shí)間也十分有限,Java也是邊寫邊學(xué)的,代碼格式不規(guī)范有勞大家諒解。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-860930.html
到了這里,關(guān)于Android模擬藍(lán)牙藍(lán)牙鍵盤——適配Android和Windows的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!