Android BLE蓝牙连接与数据帧操作

1,349 阅读10分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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年前做的,历史悠久,没有维护了,如果大家有兴趣可以去项目看看源码