蓝牙HID 您所在的位置:网站首页 HID蓝牙为什么可以自动连接 蓝牙HID

蓝牙HID

2024-07-11 21:33| 来源: 网络整理| 查看: 265

前言

本篇为蓝牙HID系列篇章之一,本篇以红米K30(MIUI13即Android 12)手机作为蓝牙HID设备,可以与电脑、手机、平板等其他蓝牙主机进行配对从而实现鼠标触控板的功能。 蓝牙HID系列篇章: 蓝牙HID——将android设备变成蓝牙键盘(BluetoothHidDevice) 蓝牙HID——android利用手机来解锁电脑/平板/iPhone 蓝牙HID——Android手机注册HID时出现 Could not bind to Bluetooth (HID Device) Service with Intent * 的问题分析

HID开发

Android 9开放了 BluetoothHidDevice 等HID相关的API,通过与系统蓝牙HID服务通信注册成蓝牙HID设备。首先通过 BluetoothProfile.HID_DEVICE 的描述类型得到 BluetoothHidDevice 抽象实例:

private BluetoothAdapter mBtAdapter; private BluetoothHidDevice mHidDevice; private void callBluetooth() { Log.d(TAG, "callBluetooth"); mBtAdapter = BluetoothAdapter.getDefaultAdapter(); mBtAdapter.getProfileProxy(mContext, new BluetoothProfile.ServiceListener() { @Override public void onServiceConnected(int i, BluetoothProfile bluetoothProfile) { Log.d(TAG, "onServiceConnected:" + i); if (i == BluetoothProfile.HID_DEVICE) { if (!(bluetoothProfile instanceof BluetoothHidDevice)) { Log.e(TAG, "Proxy received but it's not BluetoothHidDevice"); return; } mHidDevice = (BluetoothHidDevice) bluetoothProfile; registerBluetoothHid(); } } @Override public void onServiceDisconnected(int i) { Log.d(TAG, "onServiceDisconnected:" + i); } }, BluetoothProfile.HID_DEVICE); }

再调用 BluetoothHidDevice.registerApp() 将 Android 设备注册成蓝牙HID设备:

private BluetoothDevice mHostDevice; private final BluetoothHidDeviceAppQosSettings qosSettings = new BluetoothHidDeviceAppQosSettings(BluetoothHidDeviceAppQosSettings.SERVICE_BEST_EFFORT, 800, 9, 0, 11250, BluetoothHidDeviceAppQosSettings.MAX ); private final BluetoothHidDeviceAppSdpSettings mouseSdpSettings = new BluetoothHidDeviceAppSdpSettings( HidConfig.MOUSE_NAME, HidConfig.DESCRIPTION, HidConfig.PROVIDER, BluetoothHidDevice.SUBCLASS1_MOUSE, HidConfig.MOUSE_COMBO); private void registerBluetoothHid() { if (mHidDevice == null) { Log.e(TAG, "hid device is null"); return; } mHidDevice.registerApp(mouseSdpSettings, null, qosSettings, Executors.newCachedThreadPool(), new BluetoothHidDevice.Callback() { @Override public void onAppStatusChanged(BluetoothDevice pluggedDevice, boolean registered) { Log.d(TAG, "onAppStatusChanged:" + (pluggedDevice != null ? pluggedDevice.getName() : "null") + " registered:" + registered); if (registered) { Log.d(TAG, "paired devices: " + mHidDevice.getConnectionState(pluggedDevice)); if (pluggedDevice != null && mHidDevice.getConnectionState(pluggedDevice) != BluetoothProfile.STATE_CONNECTED) { boolean result = mHidDevice.connect(pluggedDevice); Log.d(TAG, "hidDevice connect:" + result); } } if (mBluetoothHidStateListener != null) { mBluetoothHidStateListener.onRegisterStateChanged(registered, pluggedDevice != null); } } @Override public void onConnectionStateChanged(BluetoothDevice device, int state) { Log.d(TAG, "onConnectionStateChanged:" + device + " state:" + state); if (state == BluetoothProfile.STATE_CONNECTED) { mHostDevice = device; } if (state == BluetoothProfile.STATE_DISCONNECTED) { mHostDevice = null; } if (mBluetoothHidStateListener != null) { mBluetoothHidStateListener.onConnectionStateChanged(state); } } }); }

蓝牙鼠标Mouse的描述信息如下,主要 为 MOUSE_COMBO 的描述协议,正确的描述协议才能成功与其他设备通信。

public class HidConfig { public final static String MOUSE_NAME = "VV Mouse"; public final static String DESCRIPTION = "VV for you"; public final static String PROVIDER = "VV"; public static final byte[] MOUSE_COMBO = { (byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop) (byte) 0x09, (byte) 0x02, // USAGE (Mouse) (byte) 0xa1, (byte) 0x01, // COLLECTION (Application) (byte) 0x85, (byte) 0x04, // REPORT_ID (4) (byte) 0x09, (byte) 0x01, // USAGE (Pointer) (byte) 0xa1, (byte) 0x00, // COLLECTION (Physical) (byte) 0x05, (byte) 0x09, // USAGE_PAGE (Button) (byte) 0x19, (byte) 0x01, // USAGE_MINIMUM (Button 1) (byte) 0x29, (byte) 0x02, // USAGE_MAXIMUM (Button 2) (byte) 0x15, (byte) 0x00, // LOGICAL_MINIMUM (0) (byte) 0x25, (byte) 0x01, // LOGICAL_MAXIMUM (1) (byte) 0x95, (byte) 0x03, // REPORT_COUNT (3) (byte) 0x75, (byte) 0x01, // REPORT_SIZE (1) (byte) 0x81, (byte) 0x02, // INPUT (Data,Var,Abs) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x75, (byte) 0x05, // REPORT_SIZE (5) (byte) 0x81, (byte) 0x03, // INPUT (Cnst,Var,Abs) (byte) 0x05, (byte) 0x01, // USAGE_PAGE (Generic Desktop) (byte) 0x09, (byte) 0x30, // USAGE (X) (byte) 0x09, (byte) 0x31, // USAGE (Y) (byte) 0x09, (byte) 0x38, // USAGE (Wheel) (byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127) (byte) 0x25, (byte) 0x7F, // LOGICAL_MAXIMUM (127) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8) (byte) 0x95, (byte) 0x03, // REPORT_COUNT (3) (byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel) //水平滚轮 (byte) 0x05, (byte) 0x0c, // USAGE_PAGE (Consumer Devices) (byte) 0x0a, (byte) 0x38, (byte) 0x02, // USAGE (AC Pan) (byte) 0x15, (byte) 0x81, // LOGICAL_MINIMUM (-127) (byte) 0x25, (byte) 0x7f, // LOGICAL_MAXIMUM (127) (byte) 0x75, (byte) 0x08, // REPORT_SIZE (8) (byte) 0x95, (byte) 0x01, // REPORT_COUNT (1) (byte) 0x81, (byte) 0x06, // INPUT (Data,Var,Rel) (byte) 0xc0, // END_COLLECTION (byte) 0xc0, // END_COLLECTION };

在注册完成后启动设备发现,让HID能被其他设备发现,下面ActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE) 相当于调用 BluetoothAdapter.setScanMode() 的隐藏API

private ActivityResultLauncher mActivityResultLauncher; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_mouse); mActivityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { Log.d(TAG, "onActivityResult:" + result.toString()); }); } @Override public void onRegisterStateChanged(boolean registered, boolean hasDevice) { if (registered) { if (!hasDevice) { // startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), 1); mActivityResultLauncher.launch(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE)); } } }

ActivityResultLauncher 的相关方法也可用 startActivityForResult(new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE), REQUEST_CODE) 来替代,但 startActivityForResult() 是废弃的方法,不建议使用。 接下来与蓝牙主机(电脑、手机等)进行蓝牙配对,已配对过需要取消配对。配对完成即可实现对蓝牙主机的鼠标触摸控制。

手势识别

手势识别通过对触摸事件以及手势监听进行各种手势的判断(移动鼠标、左键单击、左键双击、右键双指单击、双指垂直/水平滚动)。

CustomMotionListener customMotionListener = new CustomMotionListener(this, mBluetoothHidManager); findViewById(R.id.layout_touch).setOnTouchListener(customMotionListener);

手势逻辑处理代码如下:

package com.example.bluetoothproject; import android.content.Context; import android.view.GestureDetector; import android.view.MotionEvent; import android.view.View; import android.view.ViewConfiguration; import org.apache.commons.lang3.concurrent.BasicThreadFactory; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; public class CustomMotionListener implements View.OnTouchListener, GestureDetector.OnGestureListener, GestureDetector.OnDoubleTapListener { private final GestureDetector mGestureDetector; private BluetoothHidManager mBluetoothHidManager; private int mPointCount; private long mDoubleFingerTime; private final ScheduledExecutorService mExecutorService; private float mPreX; private float mPreY; private boolean mLongPress; public CustomMotionListener(Context context, BluetoothHidManager bluetoothHidManager) { mBluetoothHidManager = bluetoothHidManager; mGestureDetector = new GestureDetector(context, this); mGestureDetector.setOnDoubleTapListener(this); mExecutorService = new ScheduledThreadPoolExecutor(1, new BasicThreadFactory.Builder().namingPattern("mouse-schedule-pool-%d").daemon(true).build()); } @Override public boolean onSingleTapConfirmed(MotionEvent e) { return false; } @Override public boolean onDoubleTap(MotionEvent e) { return false; } @Override public boolean onDoubleTapEvent(MotionEvent e) { //左键单指双击(选中文本的效果) if (e.getAction() == MotionEvent.ACTION_DOWN) { mBluetoothHidManager.sendLeftClick(true); } else if (e.getAction() == MotionEvent.ACTION_UP) { mBluetoothHidManager.sendLeftClick(false); } return true; } @Override public boolean onDown(MotionEvent e) { return false; } @Override public void onShowPress(MotionEvent e) { } @Override public boolean onSingleTapUp(MotionEvent e) { //左键单击 mBluetoothHidManager.sendLeftClick(true); mBluetoothHidManager.sendLeftClick(false); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { //双指滚动,x为水平滚动,y为垂直滚动,消抖处理 if (mPointCount == 2) { if (Math.abs(distanceX) > Math.abs(distanceY)) { distanceX = distanceX > 0 ? 1 : distanceX //单键长按效果 mBluetoothHidManager.sendLeftClick(true); mLongPress = true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { return false; } @Override public boolean onTouch(View v, MotionEvent event) { float x = event.getX(); float y = event.getY(); if (mGestureDetector.onTouchEvent(event)) { return true; } mPointCount = event.getPointerCount(); switch (event.getActionMasked()) { case MotionEvent.ACTION_POINTER_DOWN: //双指单击代表右键记录时间 if (event.getPointerCount() == 2) { mDoubleFingerTime = System.currentTimeMillis(); } break; case MotionEvent.ACTION_MOVE: //单指代表移动鼠标 if (event.getPointerCount() == 1) { float dx = x - mPreX; if (dx > 127) dx = 127; if (dx 127) dy = 127; if (dy mBluetoothHidManager.sendLeftClick(false); mLongPress = false; } break; case MotionEvent.ACTION_POINTER_UP: //双指按下代表右键 if (event.getPointerCount() == 2 && System.currentTimeMillis() - mDoubleFingerTime @Override public void run() { mBluetoothHidManager.sendRightClick(false); } }, 0, 50, TimeUnit.MILLISECONDS); } break; default: break; } mPreX = x; mPreY = y; return true; } }

向蓝牙主机发送的鼠标触摸按键的报告如下:

private boolean mLeftClick; private boolean mRightClick; public void sendLeftClick(boolean click) { mLeftClick = click; senMouse((byte) 0x00, (byte) 0x00); } public void sendRightClick(boolean click) { mRightClick = click; senMouse((byte) 0x00, (byte) 0x00); } public void senMouse(byte dx, byte dy) { if (mHidDevice == null) { Log.e(TAG, "senMouse failed, hid device is null!"); return; } if (mHostDevice == null) { Log.e(TAG, "senMouse failed, hid device is not connected!"); return; } byte[] bytes = new byte[5]; //bytes[0]字节:bit0: 1表示左键按下 0表示左键抬起 | bit1: 1表示右键按下 0表示右键抬起 | bit2: 1表示中键按下 | bit7~3:补充的常数,无意义,这里为0即可 bytes[0] = (byte) (bytes[0] | (mLeftClick ? 1 : 0)); bytes[0] = (byte) (bytes[0] | (mRightClick ? 1 : 0) Log.e(TAG, "sendWheel failed, hid device is null!"); return; } if (mHostDevice == null) { Log.e(TAG, "sendWheel failed, hid device is not connected!"); return; } byte[] bytes = new byte[5]; bytes[3] = vWheel; //垂直滚轮 bytes[4] = hWheel; //水平滚轮 Log.d(TAG, "sendWheel vWheel:" + vWheel + ",hWheel:" + hWheel); mHidDevice.sendReport(mHostDevice, 4, bytes); } 效果

实现以上步骤即可将手机变成蓝牙鼠标/触控板,下面是实现的效果: 在这里插入图片描述

鼠标移动: 请添加图片描述

左键单击: 请添加图片描述

左键单指快速双击: 请添加图片描述

右键双指单击: 请添加图片描述

双指水平左右滚动: 请添加图片描述

双指垂直上下滚动: 请添加图片描述

完整视频效果展示:

蓝牙HID——将android设备变成蓝牙鼠标/触控板



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有