解决方案

Android 蓝牙开发 —— BLE

seo靠我 2023-09-26 01:44:49

蓝牙——BLE

介绍

1.BLE 是 Bluetooth Low Energy 的缩写,意思为低功耗蓝牙。由蓝牙技术联盟(Bluetooth SIG)设计的无线通讯技术,主要用于医疗,健身,安全和家庭娱乐SEO靠我行业。 与传统蓝牙相比,蓝牙低功耗旨在大幅降低功耗和成本,同时也能够达到相同的通讯效果。

支持多个平台,包括 IOS,Android,Windows Phone 和 BlackBerry 以及 macOSEO靠我S,Linux,Windows 8 和 Windows 10 在内的移动操作系统本身支持蓝牙低功耗。 蓝牙 SIG 预测,到 2018 年,超过 90% 的蓝牙智能手机将支持蓝牙低功耗。

在安卓平台,

SEO靠我 Android 4.3 (API level 18) 以后引进来的,通过这些 API 可以扫描蓝牙设备、连接设备,查询 services、读写设备的 characteristics(属性特征),然后SEO靠我通过属性进行数据传输。

特点:

低功耗,使用 BLE 与周围设备进行通讯时,其峰值功耗为传统蓝牙的一半

传输距离提升到 100 米

低延时,最短可在3 ms内完成连接并开始进行数据传输

缺点:

传输数据量较小,最SEO靠我大 512 个字节,超过 20 个字节需要分包处理

应用领域:

主要用于智能硬件,像健康护理、运动和健身、设备电源管理等

连接模式

对于BLE单设备来讲常见的蓝牙模块的工作模有四种:

主设备模式从设备模式广播模SEO靠我式Mesh组网模式

主设备模式

可以与一个从设备进行连接。在此模式下可以对周围设备进行搜索并选择需要连接的从设备进行连接。同时可以设置默认连接从设备的MAC地址,这样模块上电之后就可以查找此模块并进行连接SEO靠我

从设备模式

BLE支持从设备模式,在此模式下完全符合BLE4.1协议,用户可以根据协议自己开发APP。此模式下包含一个串口收发的Service,用户可以通过UUID找到它,里面有两个通道,分别是读和写SEO靠我。用户可以操作这两个通道进行数据的传输。

广播模式

在这种模式下模块可以一对多进行广播。用户可以通过AT指令设置模块广播的数据,模块可以在低功耗的模式下持续的进行广播,应用于极低功耗,小数据量,单向传输的SEO靠我应用场合,比如无线抄表,室内定位等功能。

Mesh组网模式

在这种模式下模块可以实现简单的自组网络,每个模块只需要设置相同的通讯密码就可以加入到同一网络当中,每一个模块都可以发起数据,每个模块可以收到数据SEO靠我并且进行回复。并且不需要网关,即使某一个设备出现故障也会跳过并选择最近的设备进行传输。

GATT协议

GATT generic Attributes的缩写,中文是通用属性,是低功耗蓝牙设备之间进行通信的协SEO靠我议。

GATT定义了一种多层的数据结构,已连接的低功耗蓝牙设备用它来进行通信,GATT层是传输真正数据所在的层。一个GATT服务器通过一个称为属性表的表格组织数据,这些数据就是用于真正发送的数据。

GATSEO靠我T定义的多层数据结构简要概括起来就是服务(service)可以包含多个特征(characteristic),每个特征包含属性(properties)和值(value),还可以包含多个描述(descriSEO靠我ptor)。它形象的结构如下图:

profile(数据配置文件)

一个profile文件可以包含一个或者多个服务,一个profile文件包含需要的服务的信息或者为对等设备如何交互的配置文件的选项信息。设备SEO靠我的GAP和GATT的角色都可能在数据的交换过程中改变,因此,这个文件应该包含广播的种类、所使用的连接间隔、所需的安全等级等信息。

需要注意的是: 一个profile中的属性表不能包含另一个属性表。

属性

SEO靠我个属性包含句柄、UUID(类型)、值,句柄是属性在GATT表中的索引,在一个设备中每一个属性的句柄都是唯一的。UUID包含属性表中数据类型的信息,它是理解属性表中的值的每一个字节的意义的关键信息。在一SEO靠我个GATT表中可能有许多属性,这些属性能可能有相同的UUID。

个人理解,属性指的是 Service、Characteristic 这样的对象

Service

一个低功耗蓝牙设备可以定义许多 ServiceSEO靠我, Service 可以理解为一个功能的集合。设备中每一个不同的 Service 都有一个 128 bit 的 UUID 作为这个 Service 的独立标志。蓝牙核心规范制定了两种不同的UUID,一SEO靠我种是基本的UUID,一种是代替基本UUID的16位UUID。所有的蓝牙技术联盟定义UUID共用了一个基本的UUID:

0x0000xxxx-0000-1000-8000-00805F9B34FB

为了进一SEO靠我步简化基本UUID,每一个蓝牙技术联盟定义的属性有一个唯一的16位UUID,以代替上面的基本UUID的‘x’部分。例如,心率测量特性使用0X2A37作为它的16位UUID,因此它完整的128位UUIDSEO靠我为:

0x00002A37-0000-1000-8000-00805F9B34FB

Characteristic

在 Service 下面,又包括了许多的独立数据项,我们把这些独立的数据项称作 CharacSEO靠我teristic。同样的,每一个 Characteristic 也有一个唯一的 UUID 作为标识符。在 Android 开发中,建立蓝牙连接后,我们说的通过蓝牙发送数据给外围设备就是往这些 CharSEO靠我acteristic 中的 Value 字段写入数据;外围设备发送数据给手机就是监听这些 Charateristic 中的 Value 字段有没有变化,如果发生了变化,手机的 BLE API 就会收到SEO靠我一个监听的回调。

DesCriptor

任何在特性中的属性不是定义为属性值就是为描述符。描述符是一个额外的属性以提供更多特性的信息,它提供一个人类可识别的特性描述的实例。然而,有一个特别的描述符值得特别地SEO靠我提起:客户端特性配置描述符(Client Characteristic Configuration Descriptor,CCCD),这个描述符是给任何支持通知或指示功能的特性额外增加的。在CCCD中SEO靠我写入“1”使能通知功能,写入“2”使能指示功能,写入“0”同时禁止通知和指示功能。

使用过程

常采用的模式是主机模式,然后扫描客户端硬件,然后连接,获取相关服务和特性,然后进行数据传输。

1. 扫描 SEO靠我 权限获取 <uses-permission android:name="android.permission.BLUETOOTH"/> 使用蓝牙所需要的权限 SEO靠我 <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/> 使用扫描和设置蓝牙的权限(申明这一个权限必须申明上面一个权限SEO靠我

在Android5.0之前,是默认申请GPS硬件功能的。而在Android 5.0 之后,需要在manifest 中申明GPS硬件模块功能的使用。

<!-- Needed only if your aSEO靠我pp targets Android 5.0 (API level 21) or higher. --><uses-feature android:name="android.hardware.locSEO靠我ation.gps" />

在 Android 6.0 及以上,还需要打开位置权限。如果应用没有位置权限,蓝牙扫描功能不能使用(其它蓝牙操作例如连接蓝牙设备和写入数据不受影响)。

<uses-permisSEO靠我sion android:name="android.permission.ACCESS_COARSE_LOCATION"/>

除了上面的设置之外,如果想设置设备只支持 BLE,可以加上下面这句话

<usSEO靠我es-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

同样,如果不想添加 BLE 的支持,那SEO靠我么可以设置 required="false"

然后可以在运行时判断设备是否支持 BLE,

// Use this check to determine whether BLE is supported oSEO靠我n the device. Then// you can selectively disable BLE-related features.if (!getPackageManager().hasSySEO靠我stemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {Toast.makeText(this, R.string.ble_not_supported, SEO靠我Toast.LENGTH_SHORT).show();finish();} 初始化

判断 BLE 在设备上是否支持,如果不支持的话,那么可以不用继续后面的操作了;如果支持,但是有可能蓝牙SEO靠我被禁掉了,因为开着蓝牙比较好点,用户一般都会关闭蓝牙,这时候可以发送请求,来打开蓝牙,可以通过两个步骤来完成。

获取 BluetoothAdapter

BluetoothAdapter 对于一个设备来说唯SEO靠我一的,整个系统或者应用,对蓝牙进行操作时都是需要这个的适配器。它的获取需要通过系统服务来获取。

private BluetoothAdapter mBluetoothAdapter;...// InitSEO靠我ializes Bluetooth adapter.final BluetoothManager bluetoothManager =(BluetoothManager) getSystemServiSEO靠我ce(Context.BLUETOOTH_SERVICE);mBluetoothAdapter = bluetoothManager.getAdapter();

2.打开蓝牙

一般对于用户来说,在手机上蓝SEO靠我牙是关闭,当开启你的应用时就需要开启蓝牙,有两种方式,一种是跳转到设置界面,由用户自己开启蓝牙;

另外一种时,直接在应用开启蓝牙,不需要用户打开,而是直接帮用户开启手机上的蓝牙。

跳转到设置界面

// EnSEO靠我sures Bluetooth is available on the device and it is enabled. If not, // displays a dialog rSEO靠我equesting user permission to enable Bluetooth. if (mBluetoothAdapter == null || !mBluetoothASEO靠我dapter.isEnabled()) {Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);starSEO靠我tActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }

直接开启蓝牙

// 打开蓝牙if (!mBluetoothAdapter.SEO靠我isEnabled()) {mBluetoothAdapter.enable();} 扫描

扫描蓝牙设备可以通过startLeScan(),其中有一个参数是 ScanCallback,通SEO靠我过它返回扫描结果,因为扫描过程是很耗电的,所以在扫描过程需要保证

1.一旦找到目标设备,需要停止扫描

2.扫描不要设置循环,而且需要设置一个时间

回调如下

// 设备扫描回调private ScanCallbSEO靠我ack mScanCallback = new ScanCallback() {@Overridepublic void onScanResult(int callbackType, final ScSEO靠我anResult result) {runOnUiThread(new Runnable() {@Overridepublic void run() {// 广播的信息,可以在result中获取MDeSEO靠我vice mDev = new MDevice(result.getDevice(), result.getRssi());if (!mList.contains(mDev)) {mList.add(SEO靠我mDev);}if (mList.size() > 0) {mScanner.stopScan(mScanCallback);Toast.makeText(MainActivity.this, "扫描SEO靠我结束,设备数 " + mList.size(), Toast.LENGTH_SHORT).show();}}});}};

开始扫描

private BluetoothAdapter mBluetoothASEO靠我dapter;private boolean mScanning;private Handler mHandler;// Stops scanning after 10 seconds.privateSEO靠我 static final long SCAN_PERIOD = 10000;...private void scanLeDevice(final boolean enable) {if (enablSEO靠我e) {// Stops scanning after a pre-defined scan period.mHandler.postDelayed(new Runnable() {@OverrideSEO靠我public void run() {mScanning = false;mBluetoothAdapter.stopLeScan(mLeScanCallback);}}, SCAN_PERIOD);SEO靠我mScanning = true;mBluetoothAdapter.startLeScan(mLeScanCallback);} else {mScanning = false;mBluetoothSEO靠我Adapter.stopLeScan(mLeScanCallback);}...}

这里遇到一个坑,就是实际中手机与一些智能硬件连接时,也就是需要连接指定的硬件,设备有一个UUID,所以可以通过如下方法SEO靠我连接

startLeScan(UUID[], BluetoothAdapter.LeScanCallback)

但是实际中使用时,连接时会出错,仍需要再次验证。

我当时的做法是采用了另外一种方法,当时这种方SEO靠我法,要求 API 高于 21。private void scanLeDevice() {//50秒后停止扫描mHander.postDelayed(stopScanRunnable, 50000);LSEO靠我ist<ScanFilter> filters = new ArrayList<>();ScanFilter filter = new ScanFilter.Builder()//"D8:B0:4C:SEO靠我E8:66:DC" 测试MAC 1//"D8:B0:4C:E2:45:2A" 测试MAC 2.setDeviceAddress("D8:B0:4C:E2:45:2A").build();filtersSEO靠我.add(filter);// 扫描mScanner.startScan(filters, new ScanSettings.Builder().setScanMode(ScanSettings.SCSEO靠我AN_MODE_LOW_LATENCY).build(), mScanCallback);}

扫描结束后需要停止扫描

boolean startLeScan(BluetoothAdapter.LeScanSEO靠我Callback callback)

连接

设备连接

通过扫描能够获得设备 BluetoothDevice,包含地址和名字

通过设备连接并获取 BluetoothGatt,后面通过 BluetoothGattSEO靠我 的实例来进行client的操作,如使用该实例去发现服务,获取读、写、通知等属性public static BluetoothGatt mBluetoothGatt;mBluetoothGatt = SEO靠我device.connectGatt(context, false, mGattCallback);

通过连接回调来监听连接的状态,包含三种状态,连接、断开、正在连接,根据状态可以发送广播,

在接收广播的SEO靠我位置进行做相应的处理private final static BluetoothGattCallback mGattCallback = new BluetoothGattCallback() {@OSEO靠我verridepublic void onConnectionStateChange(BluetoothGatt gatt, int status,int newState) {String inteSEO靠我ntAction;// GATT Server connectedif (newState == BluetoothProfile.STATE_CONNECTED) {System.out.printSEO靠我ln("---------------------------->已经连接");intentAction = ACTION_GATT_CONNECTED;mConnectionState = STATSEO靠我E_CONNECTED;broadcastConnectionUpdate(intentAction);}// GATT Server disconnectedelse if (newState ==SEO靠我 BluetoothProfile.STATE_DISCONNECTED) {System.out.println("---------------------------->连接断开");intenSEO靠我tAction = ACTION_GATT_DISCONNECTED;mConnectionState = STATE_DISCONNECTED;broadcastConnectionUpdate(iSEO靠我ntentAction);}// GATT Server disconnectedelse if (newState == BluetoothProfile.STATE_DISCONNECTING) SEO靠我{System.out.println("---------------------------->正在连接"); // intentAction = ACTION_GATT_DISCSEO靠我ONNECTING; // mConnectionState = STATE_DISCONNECTING; // broadcastConnectionUpdate(iSEO靠我ntentAction);}}}

当操作完成后,需要关闭连接,必须调用 BluetoothGatt#close 方法释放连接资源

发现服务

由于有了 mBluetoothGatt,就可以去发现服务,再通过服SEO靠我务去获取可以操作的属性

mBluetoothGatt.discoverServices();

发现服务以及获取其他属性,如write和read,notify,Descriptor相关的属性,均是在 BluSEO靠我etoothGattCallback 中有回调,在回调中就可以通过发送广播,然后在其他位置做处理,

如接收数据就会有回调,然后将数据传递出去,对数据解析等@Overridepublic void onSSEO靠我ervicesDiscovered(BluetoothGatt gatt, int status) {// GATT Services discovered//发现新的服务if (status == SEO靠我BluetoothGatt.GATT_SUCCESS) {System.out.println("---------------------------->发现服务");broadcastConnecSEO靠我tionUpdate(ACTION_GATT_SERVICES_DISCOVERED);} }//通过 Descriptor 写监听@Override public void onDescriptorSEO靠我Write(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,int status) {}// 通过 Descriptor 读监听@OverSEO靠我ridepublic void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor,int status) SEO靠我{}@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristiccharacteSEO靠我ristic, int status) {//write操作会调用此方法if (status == BluetoothGatt.GATT_SUCCESS) {System.out.println("oSEO靠我nCharacteristicWrite ------------------->write success");Intent intent = new Intent(ACTION_GATT_CHARSEO靠我ACTERISTIC_WRITE_SUCCESS);mContext.sendBroadcast(intent);} else {Intent intent = new Intent(ACTION_GSEO靠我ATT_CHARACTERISTIC_ERROR);intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status)SEO靠我;mContext.sendBroadcast(intent);}}@Overridepublic void onCharacteristicRead(BluetoothGatt gatt,BluetSEO靠我oothGattCharacteristic characteristic, int status) {// 接收数据}@Overridepublic void onCharacteristicChaSEO靠我nged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {//notify 会回调用此方法broadcastNotifSEO靠我yUpdate(characteristic);}@Overridepublic void onMtuChanged(BluetoothGatt gatt, int mtu, int status) SEO靠我{super.onMtuChanged(gatt, mtu, status);} 数据传输 数据读取

这里有两个方法:

方法一:一般数据读取的话,想到的是用 read 属性SEO靠我,所以需要获取特定通道的 BluetoothGattCharactristic。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattService#getChaSEO靠我ractristic 获取 BluetoothGattCharactristic,这里获取的 BluetoothGattCharactristic 是有指定 UUID 的,也就是说不同的 CharacSEO靠我tristic的 UUID 是不同的,读和写的通道不同,根据不同的操作,然后通过UUID获取相应的通道

3)BluetoothGattCharactristic#readCharacteristic 方SEO靠我法可以通知系统去读取特定的数据

4)BluetoothGattCallback#onCharacteristicRead 方法。通过 BluetoothGattCharacteristic#getValSEO靠我ue 可以读取到蓝牙设备的数据

方法二:采用 notify 属性,客户端发送数据,服务端监听属性变化,然后根据 属性的 UUID 判断是否是 notify 的属性,如果是的话,说确实是由远程设备发过来的SEO靠我数据。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattService#getCharactristic 获取 BluetoothGattCharactrisSEO靠我tic,这里的这个属性是 notify 属性

3)获得属性后需要进行判断设备是否支持notify操作,然后再设备打开notify通知

void prepareBroadcastDataNotify(BluSEO靠我etoothGattCharacteristic characteristic) {final int charaProp = characteristic.getProperties();ToastSEO靠我.makeText(this, " " + charaProp, Toast.LENGTH_SHORT).show();if ((charaProp | BluetoothGattCharacteriSEO靠我stic.PROPERTY_NOTIFY) > 0) {BluetoothLeService.setCharacteristicNotification(characteristic, true);}SEO靠我}

4) 设置属性时,也要通知远程设备端也要开启 notify 属性

public static void setCharacteristicNotification(BluetoothGattCharaSEO靠我cteristic characteristic, boolean enabled) {if (mBluetoothAdapter == null || mBluetoothGatt == null)SEO靠我 {return;}//通知远程端开启 notify if (characteristic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHSEO靠我ARACTERISTIC_CONFIG)) != null) {if (enabled == true) {BluetoothGattDescriptor descriptor = characterSEO靠我istic.getDescriptor(UUID.fromString(GattAttributes.CLIENT_CHARACTERISTIC_CONFIG));descriptor.setValuSEO靠我e(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);mBluetoothGatt.writeDescriptor(descriptor);} elSEO靠我se {BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(GattAttributesSEO靠我.CLIENT_CHARACTERISTIC_CONFIG));descriptor.setValue(BluetoothGattDescriptor.DISABLE_NOTIFICATION_VALSEO靠我UE);mBluetoothGatt.writeDescriptor(descriptor);}}mBluetoothGatt.setCharacteristicNotification(characSEO靠我teristic, enabled);} 数据写入

对于 BLE 方式的数据传输来说,数据的大小是有限制的,一次性最多可以传输512个字节,这也是BLE小数据量传输的特点,另外,对于每次SEO靠我传输,也有限制,每个数据包大小不超过20个字节,超过20个字节的话,需要分包处理。写的步骤和读取类似。

1)BluetoothGatt#getService 得到服务

2)BluetoothGattSerSEO靠我vice#getCharactristic 获取 BluetoothGattCharactristic,这里的这个属性是 write 属性

3)写入字节数据

public static void writSEO靠我eCharacteristicGattDb(BluetoothGattCharacteristic characteristic, byte[] byteArray) {if (mBluetoothASEO靠我dapter == null || mBluetoothGatt == null) {return;} else {byte[] valueByte = byteArray;characteristiSEO靠我c.setValue(valueByte);mBluetoothGatt.writeCharacteristic(characteristic);}}

4)对于手机端,写入数据后,远程端会接受,同时回调SEO靠我中也会能够接收,也可以在回调中做一下数据判断,看是否是自己发出的数据

@Overridepublic void onCharacteristicWrite(BluetoothGatt gatt, BluSEO靠我etoothGattCharacteristiccharacteristic, int status) {//write操作会调用此方法if (status == BluetoothGatt.GATTSEO靠我_SUCCESS) {System.out.println("onCharacteristicWrite ------------------->write success");Intent inteSEO靠我nt = new Intent(ACTION_GATT_CHARACTERISTIC_WRITE_SUCCESS);// 这里通过属性能够读取你发送的数据,可以对此数据进行判断characteristSEO靠我ic.getValue();mContext.sendBroadcast(intent);} else {Intent intent = new Intent(ACTION_GATT_CHARACTESEO靠我RISTIC_ERROR);intent.putExtra(Constants.EXTRA_CHARACTERISTIC_ERROR_MESSAGE, "" + status);mContext.seSEO靠我ndBroadcast(intent);}}

其他

一般在通讯过程中,需要有连接的心跳包,来检测是否仍处于连接状态,可以通过设置定时器,主机端定时 write 数据,客户端定时 notify 数据 2.

SEO靠我开连接

操作完成,需要断开蓝牙并释放资源,通过 BluetoothGatt#disconnect 断开连接,然后回调中会收到断开的监听,可以根据状态释放资源。BluetoothGattCallback#SEO靠我onConnectionStateChange回调中通过这个方法的 newState 参数可以判断是连接成功还是断开成功的回调,断开成功的话,然后调用 BluetoothGatt#close 方法释放SEO靠我资源

参考

官方文档

Android 开发入门介绍

通用属性配置文件(GATT)及其服务,特性与属性介绍

“SEO靠我”的新闻页面文章、图片、音频、视频等稿件均为自媒体人、第三方机构发布或转载。如稿件涉及版权等问题,请与 我们联系删除或处理,客服邮箱:html5sh@163.com,稿件内容仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同 其观点或证实其内容的真实性。

网站备案号:浙ICP备17034767号-2