本文已参与「新人创作礼」活动,一起开启掘金创作之路。
BLE蓝牙连接,处理蓝牙数据帧 读取数据和写入数据
蓝牙的概念:
蓝牙分为经典蓝牙(很少用了)和BLE蓝牙,而不是4.0之后的蓝牙才算BLE,因为4.0之后的蓝牙是双模,是兼容经典蓝牙,市面上的设备都可以直接使用BLE的Api来做,因为它的连接速度快,低功耗的特点在智能穿戴,物联网,车载系统上的使用越来越广泛。
项目介绍:
几年前做的一个项目了,是给电工使用的,用于读取智能电表的信息,并修改数据,内置了蓝牙和定位地图导航相关的功能
一. 蓝牙的连接
1.1 声明权限
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
检查权限:
private void checkPermissions() {
RxPermissions rxPermissions = new RxPermissions(MainActivity.this);
rxPermissions.request(android.Manifest.permission.ACCESS_FINE_LOCATION)
.subscribe(new io.reactivex.functions.Consumer<Boolean>() {
@Override
public void accept(Boolean aBoolean) throws Exception {
if (aBoolean) {
// 用户已经同意该权限
scanDevice();
} else {
// 用户拒绝了该权限,并且选中『不再询问』
ToastUtils.showLong("用户开启权限后才能使用");
}
}
});
}
1.2 初始化蓝牙服务
mBluetoothManager= (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
mBluetoothAdapter=mBluetoothManager.getAdapter();
if (mBluetoothAdapter==null||!mBluetoothAdapter.isEnabled()){
Intent intent=new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(intent,0);
}
1.3 扫描设备
/**
* 开始扫描 10秒后自动停止
*/
private void scanDevice(){
tvSerBindStatus.setText("正在搜索");
isScaning=true;
pbSearchBle.setVisibility(View.VISIBLE);
mBluetoothAdapter.startLeScan(scanCallback);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//结束扫描
mBluetoothAdapter.stopLeScan(scanCallback);
runOnUiThread(new Runnable() {
@Override
public void run() {
isScaning=false;
pbSearchBle.setVisibility(View.GONE);
}
});
}
},10000);
}
蓝牙扫描如果不停止,会持续扫描,很消耗资源,一般都是开启10秒左右停止
BluetoothAdapter.LeScanCallback scanCallback=new BluetoothAdapter.LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
Log.e(TAG, "run: scanning...");
if (!mDatas.contains(device)){
mDatas.add(device);
mRssis.add(rssi);
mAdapter.notifyDataSetChanged();
}
}
};
1.4 连接设备与回调
BluetoothDevice bluetoothDevice= mDatas.get(position);
//连接设备
tvSerBindStatus.setText("连接中");
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
true, gattCallback, TRANSPORT_LE);
} else {
mBluetoothGatt = bluetoothDevice.connectGatt(MainActivity.this,
true, gattCallback);
}
连接回调,如果重复连接报133错误,需要close重新连接,注意回调中不要做耗时的操作,并且不要更新UI
private BluetoothGattCallback gattCallback=new BluetoothGattCallback() {
/**
* 断开或连接 状态发生变化时调用
* */
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
super.onConnectionStateChange(gatt, status, newState);
Log.e(TAG,"onConnectionStateChange()");
if (status==BluetoothGatt.GATT_SUCCESS){
//连接成功
if (newState== BluetoothGatt.STATE_CONNECTED){
Log.e(TAG,"连接成功");
//发现服务
gatt.discoverServices();
}
}else{
//连接失败
Log.e(TAG,"失败=="+status);
mBluetoothGatt.close();
isConnecting=false;
}
}
/**
* 发现设备(真正建立连接)
* */
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//直到这里才是真正建立了可通信的连接
isConnecting=false;
Log.e(TAG,"onServicesDiscovered()---建立连接");
//获取初始化服务和特征值
initServiceAndChara();
//订阅通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);
runOnUiThread(new Runnable() {
@Override
public void run() {
bleListView.setVisibility(View.GONE);
operaView.setVisibility(View.VISIBLE);
tvSerBindStatus.setText("已连接");
}
});
}
/**
* 读操作的回调
* */
@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicRead(gatt, characteristic, status);
Log.e(TAG,"onCharacteristicRead()");
}
/**
* 写操作的回调
* */
@Override
public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
super.onCharacteristicWrite(gatt, characteristic, status);
Log.e(TAG,"onCharacteristicWrite() status="+status+",value="+HexUtil.encodeHexStr(characteristic.getValue()));
}
/**
* 接收到硬件返回的数据
* */
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
super.onCharacteristicChanged(gatt, characteristic);
Log.e(TAG,"onCharacteristicChanged()"+characteristic.getValue());
final byte[] data=characteristic.getValue();
runOnUiThread(new Runnable() {
@Override
public void run() {
addText(tvResponse,bytes2hex(data));
}
});
}
};
1.5 发现服务建立真的连接
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
super.onServicesDiscovered(gatt, status);
//直到这里才是真正建立了可通信的连接
isConnecting=false;
Log.e(TAG,"onServicesDiscovered()---建立连接");
//获取初始化服务和特征值
initServiceAndChara();
//订阅通知
mBluetoothGatt.setCharacteristicNotification(mBluetoothGatt
.getService(notify_UUID_service).getCharacteristic(notify_UUID_chara),true);
runOnUiThread(new Runnable() {
@Override
public void run() {
bleListView.setVisibility(View.GONE);
operaView.setVisibility(View.VISIBLE);
tvSerBindStatus.setText("已连接");
}
});
}
到此蓝牙的连接 回调操作完成了,觉得麻烦的同学可以使用BLE框架来实现也是可以的,也更方便哦。
二. 蓝牙的读写操作
首先我们明白一个概念 我们看到的字符串都是16进制 10进制这样的,那蓝牙设备传输过来的都是byte这种单位例如:
A55A4001027B0A2022686C7A223A2022317831222C0A2022627971223A20223762
那我们想要看到我们想要的指定的格式就需要转码,也就是网上很多的工具类类似于hexToBytes toHexString这种的工具类方法。
这里直接上一个自用的工具类:
/**
* Byte的处理,截取,转换
*/
public class ByteUtil {
/**
* 截取字节数组
*
* @param src
* @param begin
* @param count
* @return
*/
public static byte[] subBytes(byte[] src, int begin, int count) {
byte[] bs = new byte[count];
for (int i = begin; i < begin + count; i++) bs[i - begin] = src[i];
return bs;
}
public static byte[] toByteArray(int iSource, int iArrayLen) {
byte[] bLocalArr = new byte[iArrayLen];
for (int i = 0; (i < 4) && (i < iArrayLen); i++) {
bLocalArr[i] = (byte) (iSource >> 8 * i & 0xFF);
}
return bLocalArr;
}
public static int byteToInt(byte b) {
return b & 0xFF;
}
public static int byteArrayToInt(byte[] b) {
// return b[3] & 0xFF |
// (b[2] & 0xFF) << 8 |
// (b[1] & 0xFF) << 16 |
// (b[0] & 0xFF) << 24;
// 一个byte数据左移24位变成0x??000000,再右移8位变成0x00??0000
int targets = (b[0] & 0xff) | ((b[1] << 8) & 0xff00) // | 表示安位或
| ((b[2] << 24) >>> 8) | (b[3] << 24);
return targets;
}
public static String getLongHex(byte[] buf) {
long r = 0;
for (int i = buf.length - 1; i >= 0; i--) {
r <<= 8;
r |= (buf[i] & 0x00000000000000ff);
}
String result = "0000000" + Long.toHexString(r).toUpperCase();
return result.substring(result.length() - 8);
}
final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();
public static String bytesToHex(byte[] bytes) {
char[] hexChars = new char[bytes.length * 2];
for (int j = 0; j < bytes.length; j++) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
public static byte[] hexToBytes(String hexRepresentation) {
int len = hexRepresentation.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hexRepresentation.charAt(i), 16) << 4)
+ Character.digit(hexRepresentation.charAt(i + 1), 16));
}
return data;
}
/**
* 将byte[]数组转化为String类型
*
* @param arg 需要转换的byte[]数组
* @param length 需要转换的数组长度
* @return 转换后的String队形
*/
public static String toHexString(byte[] arg, int length) {
String result = new String();
if (arg != null) {
for (int i = 0; i < length; i++) {
result = result
+ (Integer.toHexString(
arg[i] < 0 ? arg[i] + 256 : arg[i]).length() == 1 ? "0"
+ Integer.toHexString(arg[i] < 0 ? arg[i] + 256
: arg[i])
: Integer.toHexString(arg[i] < 0 ? arg[i] + 256
: arg[i])) + " ";
}
return result;
}
return "";
}
}
在此基础上我们就可以做一些高级操作,读取到蓝牙设备传输的数据帧,校验帧头,计数,帧长度,帧尾,做一些校验 并获取到真正的内容之后转换我们想要的进制数据。
三. 蓝牙的帧数据处理
硬件设备工程师那一块会给你提供通信协议和帧数据结构
部分协议如下:
乌漆嘛黑一大段,人都整傻了。😂
关键就几点,记住帧格式,和上行,下行的封装格式就行了。
我们拿到对应的byte数组之后需要获取到指定位置的byte,先比对帧头,帧尾是否匹配。然后获取内容的长度,这里注意要转成数字的进制为10进制,取到内容对应的byte数组之后再转为String字符串,就是真正的内容。
话不多说,直接上代码
/**
* 读取Notify中的数据
*/
@Override
public void BLEReadNotifyData(BleDevice bleDevice, BluetoothGatt gatt) {
if (BleManager.getInstance().isConnected(bleDevice)) {
//循环遍历全部的服务和通道,找到指定的通道
List<BluetoothGattService> serviceList = gatt.getServices();
for (BluetoothGattService service : serviceList) {
UUID serviceUuid = service.getUuid();
serviceUuidStr = serviceUuid.toString();
List<BluetoothGattCharacteristic> characteristicList = service.getCharacteristics();
for (BluetoothGattCharacteristic characteristic : characteristicList) {
UUID uuid_chara = characteristic.getUuid();
characteristicUUidStr = uuid_chara.toString();
if (characteristicUUidStr.equals("49535343-aca3-481c-91ec-d85e28a60318")) {
break;
}
}
}
LogUtil.w("serviceUuid:" + serviceUuidStr);
LogUtil.w("chara_uuid:" + characteristicUUidStr);
//连接成功开始接受通知数据
BleManager.getInstance().notify(bleDevice, serviceUuidStr, characteristicUUidStr,
new BleNotifyCallback() {
@Override
public void onNotifySuccess() {
// 打开通知操作成功
LogUtil.w("打开通知成功,等待接收数据");
}
@Override
public void onNotifyFailure(BleException exception) {
// 打开通知操作失败
LogUtil.w("打开通知操作失败" + exception.getDescription());
ToastUtils.makeText(CommUtils.getContext(), "打开通知操作失败." + exception.getDescription());
}
@Override
public void onCharacteristicChanged(byte[] data) {
// 打开通知后,设备发过来的数据将在这里出现
handleReadNotifyData(data, bleDevice);
}
});
//发送开始接受的指令
sendReadNotifyDirect(bleDevice);
} else {
ToastUtils.makeText(CommUtils.getContext(), "连接失效了,需要重新连接");
}
}
//延时发送读取通知的指令
private void sendReadNotifyDirect(BleDevice bleDevice) {
contentLenght = 1;
allContent = new StringBuilder();
//连接成功直接读取数据
if (BleManager.getInstance().isConnected(bleDevice)) {
CommUtils.getHandler().postDelayed(() -> {
//写入数据命令-接受Notify的数据
byte[] bytes = HexUtil.hexStringToBytes("A55A000001BB0196");
LogUtil.w("准备写入数据命令:" + bytes.toString());
BleManager.getInstance().write(bleDevice, serviceUuidStr, characteristicUUidStr, bytes
, new BleWriteCallback() {
@Override
public void onWriteSuccess(int current, int total, byte[] justWrite) {
}
@Override
public void onWriteFailure(BleException exception) {
LogUtil.w("写入数据命令失败:" + exception.getDescription());
}
});
}, 300);
} else {
ToastUtils.makeText(CommUtils.getContext(), "连接失效了,需要重新连接");
}
}
StringBuilder allContent = new StringBuilder();
int contentLenght = 1;
/**
* 处理通知的接受数据(转码解析数据)
*/
private void handleReadNotifyData(byte[] data, BleDevice bleDevice) {
allContent.append(ByteUtil.bytesToHex(data));
// buffer.writeBytes(data);
if (data.length > 5 && ByteUtil.bytesToHex(ByteUtil.subBytes(data, 0, 2)).toUpperCase().equals("A55A")) {
LogUtil.w("确定了帧头");
//拿到长度信息
String length1 = ByteUtil.bytesToHex(ByteUtil.subBytes(data, 2, 1));
String length2 = ByteUtil.bytesToHex(ByteUtil.subBytes(data, 3, 1));
//长度16进制转10进制
contentLenght = Integer.parseInt(length2 + length1, 16);
LogUtil.e("contentLenght:" + contentLenght);
}
if (ByteUtil.bytesToHex(ByteUtil.subBytes(data, data.length - 1, 1)).equals("96")) {
LogUtil.w("确定了帧尾");
//校验长度是否一致
byte[] allBytes = ByteUtil.hexToBytes(allContent.toString());
int allLength = allBytes.length;
int subLength = allLength - 8;
LogUtil.e("allLength:" + allLength);
if (contentLenght == subLength) {
//验证成功
byte[] jsonBytes = ByteUtil.subBytes(allBytes, 5, subLength);
String jsonStr = new String(jsonBytes);
LogUtil.w("jsonStr:" + jsonStr);
//关闭通知
BleManager.getInstance().stopNotify(bleDevice, serviceUuidStr, characteristicUUidStr);
//回调
mMyBleCallback.BLENotifyDataSuccessCallback(jsonStr, bleDevice);
} else {
//验证失败,从新读取
ToastUtils.makeText(CommUtils.getContext(), "验证失败,重新读取");
contentLenght = 1;
allContent = new StringBuilder();
//发送开始接受的指令
sendReadNotifyDirect(bleDevice);
}
}
}
写入数据到设备就是把我想要传输的数据转成byte数组,然后再加上硬件工程师指定的格式,加上帧头,帧尾,长度等信息。 代码如下:
//写入数据到设备
@Override
public void BLEWriteData(BleDevice bleDevice, String jsonStr) {
if (BleManager.getInstance().isConnected(bleDevice)) {
//延时打开通知开关
CommUtils.getHandler().postDelayed(() -> {
//连接成功开始接受通知数据
BleManager.getInstance().notify(bleDevice, serviceUuidStr, characteristicUUidStr,
new BleNotifyCallback() {
@Override
public void onNotifySuccess() {
// 打开通知操作成功
LogUtil.w("打开写入通知成功,等待接收数据");
}
@Override
public void onNotifyFailure(BleException exception) {
// 打开通知操作失败
LogUtil.w("打开写入通知成功" + exception.getDescription());
ToastUtils.makeText(CommUtils.getContext(), "打开通知操作失败." + exception.getDescription());
}
@Override
public void onCharacteristicChanged(byte[] data) {
// 打开通知后,设备发过来的数据将在这里出现
handleWriteNotifyData(data, bleDevice);
}
});
//开始写数据
byte[] jsonStrByte = jsonStr.getBytes();
String hexJsonContentStr = ByteUtil.bytesToHex(jsonStrByte);
String hexLengthStr = Integer.toHexString(jsonStrByte.length);
//赋值长度
if (hexLengthStr.length() == 2) {
hexLengthStr = hexLengthStr + "00";
} else if (hexLengthStr.length() == 3) {
hexLengthStr = "0" + hexLengthStr;
String substring1 = hexLengthStr.substring(0, 2);
String substring2 = hexLengthStr.substring(2);
hexLengthStr = substring2 + substring1;
} else if (hexLengthStr.length() == 4) {
String substring1 = hexLengthStr.substring(0, 2);
String substring2 = hexLengthStr.substring(2);
hexLengthStr = substring2 + substring1;
}
String crc3 = CRC16Util.getCRC3(ByteUtil.hexToBytes("A55A" + hexLengthStr + "03" + hexJsonContentStr));
String hexWriteStr = "A55A" + hexLengthStr + "03" + hexJsonContentStr + crc3 + "96";
LogUtil.w("hexLengthStr:" + hexLengthStr);
LogUtil.w("hexJsonContentStr:" + hexJsonContentStr);
LogUtil.w("crc3:" + crc3);
LogUtil.w("hexWriteStr:" + hexWriteStr);
//延时发送写入数据指令
sendWriteNotifyDirect(bleDevice, hexWriteStr);
}, 250);
} else {
ToastUtils.makeText(CommUtils.getContext(), "连接失效了,需要重新连接");
}
}
//发送开始接受的指令
private void sendWriteNotifyDirect(BleDevice bleDevice, String hexStr) {
contentLenght = 1;
allContent = new StringBuilder();
//连接成功直接读取数据
if (BleManager.getInstance().isConnected(bleDevice)) {
CommUtils.getHandler().postDelayed(() -> {
//写入数据命令-接受Notify的数据
BleManager.getInstance().write(
bleDevice,
serviceUuidStr,
characteristicUUidStr,
HexUtil.hexStringToBytes(hexStr),
new BleWriteCallback() {
@Override
public void onWriteSuccess(final int current, final int total, final byte[] justWrite) {
LogUtil.w("写入成功");
}
@Override
public void onWriteFailure(final BleException exception) {
LogUtil.w("写入数据命令失败:" + exception.getDescription());
}
});
}, 300);
} else {
ToastUtils.makeText(CommUtils.getContext(), "连接失效了,需要重新连接");
}
}
因为项目不是基于UI驱动的项目,很难展示一些项目截图,这里放一些原始数据和解析之后的字符串:
7B0A2022686C7A223A2022317831222C0A2022627971223A2022376264353335316362383861373037336136353236623633316530 313532376264353063633832623239222C0A202262647A223A202230363231343236323830222C0A 20227968223A202231303234383339323034222C0A2022786C223A202230303031323232373136222C0A2022776422
解析到正文内容如下:
{ "hlz": "1x2", "byq": "7bd5351cb88a7073a6526b631e01527bd50cc82b29", "bdz": "0621426280", "yh": "1024839204", "xl": "0001222716", "wd": "30.870162644668973", "tq": "0001355947", "jd": "114.37234841525762", "zclx": "单体表箱", "zcbh": "1712020968", "sbmc": "计量箱", "sbcz": "热镀锌金属" }
我们想要修改指定智能电表的字段,直接发送我们的json文本:
{"hlz": "1x2","byq": "7bd5351cb88a7073a6526b631e01527bd50cc82b29","bdz": "0621426280","yh": "1024839204","xl": "0001222716","wd": "30.870162644668973","tq": "0001355947","jd": "114.37234841525762","zclx": "单体表箱","zcbh": "1712020968","sbmc": "计量箱","sbcz": "热镀锌金属" }
对应生成的byte如下
A55A4001027B0A2022686C7A223A2022317831222C0A2022627971223A20223762 643533353163623838613730373361363532366236333165303135323762643530636338 32623239222C0A202262647A223A202230363231343236323830222C0A20227968223A2022313 03234383339323034222C0A2022786C223A202230303031323232373136222C0A
总结Tips
蓝牙的操作有事出现无效的情况,因为蓝牙是线程安全,操作频繁会出现busy的情况,我们只需要延时200ms执行问题多半就能解决。
而蓝牙的数据操作就是一些进制的转换,10进制 16进制等,然后再转为Byte[]。
最后说下这个硬件工程师给的帧数据不要生搬硬套,每个硬件定义的格式肯定不同的啦,根据你们的格式来,大致上都差不多的拉。
好啦,完成啦,最后的最后说明一下智能电表项目已经是我3年前做的,历史悠久,没有维护了,如果大家有兴趣可以去项目看看源码。