Android BLE蓝牙入门
一、什么是BLE蓝牙
google官方对BLE蓝牙的解释
简述:API级别:Android 4.3(API 级别 18)引入。低功耗蓝牙区别于“经典蓝牙”。
局限:最多只支持20个字节(后面会展示)。
低功耗蓝牙优势:1.低功耗,使用纽扣电池就可运行数月至数年;2.小体积、低成本;3.与现有的大部分手机、平板电脑和计算机兼容。(百度百科)
二、硬件准备工作
1.蓝牙开发模块(如果有现成的模块可以直接进行调试)
2.串口调试工具(文章末尾会给出软件的下载方式)
3.支持BLE蓝牙的Android手机。
三、准备开发
1.权限添加
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<!-- 仅支持低耗蓝牙 -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-feature
android:name="android.hardware.bluetooth_le"
android:required="true" />
2.检查开关和权限
搜索设备准备:
1.当前设备是否支持BLE蓝牙功能
2.设备的蓝牙功能是否处于开启状态
3.判断设备的api是否需要开启定位权限
(PS:至于为什么要开启定位权限,这你得问Google了)
3.1GPS是否打开了
3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备
这里的蓝牙,定位开关状态,权限获取比较麻烦但是不复杂
代码:
package com.my.mwble;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.view.View;
import android.widget.Toast;
import com.my.mwble.util.BleUtil;
import com.my.mwble.util.GpsUtil;
import com.my.mwble.util.LogUtil;
import com.my.mwble.util.ToastUtil;
/**
* Created by Android Studio.
* User: mwb
* Date: 2020/10/24 0024
* Time: 上午 11:32
* Describe:BLE蓝牙基础
*/
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private BluetoothAdapter bluetoothAdapter;
private static int REQUEST_ENABLE_BT = 1; // 打开蓝牙页面请求代码
private static final int REQUEST_CODE_ACCESS_COARSE_LOCATION = 1; // 位置权限
private static final int SET_GPS_OPEN_STATE = 2; // 设置GPS是否打开了
private static final int REQUEST_STORY_CODE = 3; // 文件读取权限
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
initDevice();
}
private void initView() {
findViewById(R.id.btn_seach).setOnClickListener(this);
}
private void initDevice() {
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
bluetoothAdapter = bluetoothManager.getAdapter();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_seach: // 搜索蓝牙设备
seach();
break;
}
}
/**
* 搜索设备
* 1.当前设备是否支持BLE蓝牙功能
* 2.设备的蓝牙功能是否处于开启状态
* 2.1 没有开启则去开启
* 3.判断设备的api是否需要开启定位权限
* (PS:至于为什么要开启定位权限,这你得问Google了)
* 3.1GPS是否打开了
* 3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备
*/
private void seach() {
// 当前的系统版本 < Android 4.3 API=18,目前市面大部分系统都在6.0了...这个判断几乎可以不用写了。可省略
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
ToastUtil.show(this, "当前设备系统版本不支持BLE蓝牙功能!请升级系统版本到4.3以上");
return;
}
//1. 当前设备是否支持BLE蓝牙设备
if (BleUtil.checkDeviceSupportBleBlueTooth(this)) {
// 2.判断蓝牙设备是否打开了
if (checkBlueIsOpen()) {
// 3.断设备的api是否需要开启定位权限
checkGPS();
} else { // 没有打开,跳转到系统蓝牙页面
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
}
} else {
ToastUtil.show(this, "当前设备不支持BLE蓝牙功能!");
}
}
/**
* GPS是否开启了
*/
private void checkGPS() {
// 3.1GPS是否打开了
if (GpsUtil.isOPen(this)) { // GPS已经开启了
checkGpsPermission();
} else {
// 3.2是否拥有GPS权限,需要使用GPS才能使用蓝牙设备
tipGPSSetting();
}
}
/**
* 蓝牙是否打开了
*
* @return true 打开了,false 没有打开
*/
private boolean checkBlueIsOpen() {
if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
return false;
} else {
return true;
}
}
/**
* 搜索蓝牙设备
*/
private void seachBlueTooth() {
ToastUtil.show(this, "开始搜索蓝牙设备");
}
/**
* 提示需要开启蓝牙
*/
private void tipGPSSetting() {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("安卓6.0以后使用蓝牙需要开启定位功能,但本应用不会使用到您的位置信息,开始定位只是为了扫描到蓝牙设备。是否确定打开");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
GpsUtil.openGPS(MainActivity.this, SET_GPS_OPEN_STATE);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您无法使用此功能");
}
});
builder.show();
}
/**
* 蓝牙需要的定位权限
*/
private void checkGpsPermission() {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { // 如果当前版本是9.0(包含)以下的版本
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
String[] strings =
{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION};
ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
} else {
seachBlueTooth();
}
} else {
// 10.0系统
if (ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED
|| ActivityCompat.checkSelfPermission(this,
"android.permission.ACCESS_BACKGROUND_LOCATION") != PackageManager.PERMISSION_GRANTED) {
String[] strings = {android.Manifest.permission.ACCESS_FINE_LOCATION,
android.Manifest.permission.ACCESS_COARSE_LOCATION,
"android.permission.ACCESS_BACKGROUND_LOCATION"};
ActivityCompat.requestPermissions(this, strings, REQUEST_CODE_ACCESS_COARSE_LOCATION);
} else {
seachBlueTooth();
}
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_ENABLE_BT) { // 从蓝牙页面返回了,在检查一次是否打开了
if (checkBlueIsOpen()) {
// 蓝牙打开了
seach();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("蓝牙没有打开将无法使用此功能,是否确定打开");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
seach(); // 再次执行搜索
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您无法使用此功能");
}
});
builder.setCancelable(false);
builder.show();
}
}else if (requestCode == SET_GPS_OPEN_STATE) { // GPS是否打开了
if (GpsUtil.isOPen(this)) { // GPS打开了
checkGpsPermission();
} else {
tipGPSSetting();
}
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CODE_ACCESS_COARSE_LOCATION:
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) { // 得到了权限
seachBlueTooth();
} else {
AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this);
builder.setTitle("提示");
builder.setMessage("安卓6.0以后使用蓝牙需要开启定位功能,但本应用不会使用到您的位置信息,开启定位只是为了扫描到蓝牙设备。是否确定打开");
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
launchAppDetailsSettings(MainActivity.this);
}
});
builder.setNegativeButton("取消", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
ToastUtil.show(MainActivity.this, "您无法使用此功能");
}
});
builder.setCancelable(false);
builder.show();
}
break;
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
break;
}
}
/**
* 跳转权限Activity
*/
public void launchAppDetailsSettings(Activity activity) {
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + activity.getPackageName()));
if (!isIntentAvailable(this, intent)) {
ToastUtil.show(this, "请手动跳转到权限页面,给予权限!");
return;
}
activity.startActivity(intent);
}
/**
* 意图是否可用
*
* @param intent The intent.
* @return {@code true}: yes<br>{@code false}: no
*/
public boolean isIntentAvailable(Activity activity, Intent intent) {
return activity
.getPackageManager()
.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.size() > 0;
}
}
看一下效果:GIF太大了,就截几个图吧…
3.搜索设备
注意:搜索到的设备可能会多次出现需要我们自己进行筛选
关键代码:
/**
* 搜索蓝牙设备
* 创建搜索callback 返回扫描到的信息
* 创建定时任务,在指定时间内结束蓝牙扫描,蓝牙扫描是一个很耗电的操作!
*/
private void seachBlueTooth() {
ToastUtil.show(this, "开始搜索蓝牙设备");
mBluetoothLeScanner = bluetoothAdapter.getBluetoothLeScanner();
mBluetoothLeScanner.startScan(null, createScanSetting(), scanCallback);
bluetoothAdapter.startDiscovery();
handler.postDelayed(new Runnable() { // 指定时间内停止蓝牙搜索
@Override
public void run() {
closeSeach();
}
}, SCAN_PERIOD);
deviceData.clear();
}
/**
* 回调
*/
private ScanCallback scanCallback = new ScanCallback() {
@Override
public void onScanResult(int callbackType, ScanResult result) {
BluetoothDevice device = result.getDevice();
LogUtil.i("name:" + result.getDevice().getName() + ";强度:" + result.getRssi());
if (device != null) {
if (deviceData.size() > 0) {
if (!deviceData.contains(device)) { // 扫描到会有很多重复的数据,剔除,只添加第一次扫描到的设备
deviceData.add(device);
}
} else {
deviceData.add(device);
}
adapter.setData(deviceData);
}
}
@Override
public void onBatchScanResults(List<ScanResult> results) {
super.onBatchScanResults(results);
}
@Override
public void onScanFailed(int errorCode) {
super.onScanFailed(errorCode);
}
};
效果图:
至此我们就得到了扫描到了设备信息了。
4.连接BLE蓝牙设备
概述:每个BLE蓝牙设备都会包含几个服务Service
而每个Service中还包含了多个Characteristics(特征)
他们的关系如下图:
开启通信我们还需要绑定指定Service中的Characteristics(特征)。
至于使用哪个Service或者哪个Characteristics(特征)需要跟你们的硬件开发人员进行沟通。
获取当前设备的Service UUID,和特征的UUID
关键代码:
/**
* 绑定蓝牙
*
* @param device
*/
private void bindBlueTooth(BluetoothDevice device) {
//连接设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = device.connectGatt(this,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = device.connectGatt(this, false, mGattCallback);
}
}
//定义蓝牙Gatt回调类
public class mWBluetoothGattCallback extends BluetoothGattCallback {
//连接状态回调
@Override
public void onConnectionStateChange(BluetoothGatt gatt, final int status, final int newState) {
super.onConnectionStateChange(gatt, status, newState);
// status 用于返回操作是否成功,会返回异常码。
// newState 返回连接状态,如BluetoothProfile#STATE_DISCONNECTED、BluetoothProfile#STATE_CONNECTED
runOnUiThread(new Runnable() {
@Override
public void run() {
//操作成功的情况下
if (status == BluetoothGatt.GATT_SUCCESS) {
//判断是否连接码
if (newState == BluetoothProfile.STATE_CONNECTED) {
runOnUiThread(new Runnable() {
@Override
public void run() {
ToastUtil.show(MainActivity.this, "蓝牙已连接");
LogUtil.i("设备已连接上,开始扫描服务");
// 发现服务
mBluetoothGatt.discoverServices();
}
});
} else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
//判断是否断开连接码
ToastUtil.show(MainActivity.this, "连接已断开");
}
} else {
//异常码
// 重连次数不大于最大重连次数
if (reConnectionNum < maxConnectionNum) {
// 重连次数自增
reConnectionNum++;
LogUtil.i("重新连接:" + reConnectionNum);
// 连接设备
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this,
false, mGattCallback, BluetoothDevice.TRANSPORT_LE);
} else {
mBluetoothGatt = mBluetoothDevice.connectGatt(MainActivity.this, false, mGattCallback);
}
} else {
// 断开连接,失败回调
ToastUtil.show(MainActivity.this, "蓝牙连接失败,建议重启APP,或者重启蓝牙,或重启设备");
closeBLE();
}
}
}
});
}
//服务发现回调
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
runOnUiThread(new Runnable() {
@Override
public void run() {
LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());
for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
}
}
}
});
}
}
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
}
//特征写入回调
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
}
//外设特征值改变回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
}
//描述写入回调
@Override
public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) {
super.onDescriptorWrite(gatt, descriptor, status);
LogUtil.i("开启监听成功");
}
}
Service的Uuid,和Characteristics(特征)的Uuid
配置Uuid连接设备:
修改 onServicesDiscovered中的代码:
//服务发现回调
@Override
public void onServicesDiscovered(final BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
if (status == BluetoothGatt.GATT_SUCCESS) {
runOnUiThread(new Runnable() {
@Override
public void run() {
// LogUtil.i("mmmm:" + mBluetoothGatt.getServices().size());
// for (int i = 0; i < mBluetoothGatt.getServices().size(); i++) {
// LogUtil.i("mmmm service:" + mBluetoothGatt.getServices().get(i).getUuid());
//
//
// for (int k = 0; k < mBluetoothGatt.getServices().get(i).getCharacteristics().size(); k++) {
// LogUtil.i("mmmm Characteristic:" + mBluetoothGatt.getServices().get(i).getCharacteristics().get(k).getUuid());
// }
//
// }
//获取指定uuid的service
BluetoothGattService gattService = mBluetoothGatt.getService(UUID.fromString(UUDI_1));
// bluetoothGattServiceList.add(gattService);
//获取到特定的服务不为空
if (gattService != null) {
LogUtil.i("获取服务成功!");
BluetoothGattCharacteristic gattCharacteristic =
gattService.getCharacteristic(UUID.fromString(CHARACTERISTIC_UUID_1));
mGattCharacteristic = gattCharacteristic;
if (gattCharacteristic != null) {
LogUtil.i("获取特征成功!");
boolean isEnableNotification = mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true);
if (isEnableNotification) {
LogUtil.i("开启通知成功!");
//通过GATt实体类将,特征值写入到外设中。
mBluetoothGatt.writeCharacteristic(gattCharacteristic);
//如果只是需要读取外设的特征值:
//通过Gatt对象读取特定特征(Characteristic)的特征值
mBluetoothGatt.readCharacteristic(gattCharacteristic);
} else {
LogUtil.i("开启通知失败!");
}
} else {
LogUtil.i("获取特征失败!");
}
} else {
//获取特定服务失败
LogUtil.i("获取服务失败!");
}
}
});
}
}
修改onCharacteristicChanged中的代码
//外设特征值改变回调
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
final byte[] value = characteristic.getValue();
runOnUiThread(new Runnable() {
@Override
public void run() {
// value为设备发送的数据,根据数据协议进行解析
LogUtil.i("原始数据:" + new String(characteristic.getValue()));
LogUtil.i("设备发送数据:" + DigitalTrans.byte2hex(value)); // 这是一个byte转16进制的工具类,后面会给完整的代码,所以现在不用纠结
}
});
}
再次连接设备,连接成功后我们来进行测试
数据接收成功。需要注意的地方是,接收到的数据是byte类型的,使用的时候需要自己进行转化。
至此接收数据完成。
下面我们来看看发送数据怎么完成
修改代码:
/**
* 发送数据
* 将输入的16进制转化为byte发送
*/
private void sendMsg(String msg) {
if (null == mGattCharacteristic || null == mBluetoothGatt) {
ToastUtil.show(MainActivity.this, "请先连接蓝牙设备");
return;
}
mGattCharacteristic.setValue(NumUtil.hexString2Bytes(msg));
mBluetoothGatt.writeCharacteristic(mGattCharacteristic);
}
关闭蓝牙:
/**
* 关闭BLE蓝牙连接
*/
public void closeBLE() {
if (mBluetoothGatt == null) {
return;
}
mBluetoothGatt.disconnect();
mBluetoothGatt.close();
mBluetoothGatt = null;
ToastUtil.show(this, "蓝牙已断开");
}
至此蓝牙的接收和发送已全部完成。
问题
蓝牙发送数据大于20个的问题:
这明明是40个数据才拆分了啊,你这是不是欺负老实人吗?
请听我狡辩:
从XCOM串口工具中我勾选了16进制发送,Byte是从0-255的无符号类型。16进制的最大表示FF = 255, 所以两个16进制代表一个Byte,在实际的开发中我们用到的也会是16进制根据规定的协议进行沟通。
可以看到数据被拆分了,如果数据大于20个字节需要进行拼包操作。
如果有时间的话我以后会发拼包的功能实现。
=2020/11/25=
新增内容:
手机端向BLE蓝牙发送数据最大也是20个字节。
测试如下:
手机端写入数据如下图:
我们再来看看接收到的参数:
如果手机发送的信息超过了20个字节你就该使用拼包的操作了。
,如果有什么问题,请留言进行沟通。
代码和工具类奉上:
XCOM串口调试工具:链接:https://pan.baidu.com/s/1i-9W31CjXd-551mqphi_hg
提取码:95vv
代码:https://github.com/No98K/MWBle