美文网首页
蓝牙通讯简单总结

蓝牙通讯简单总结

作者: 梧叶已秋声 | 来源:发表于2022-09-04 09:46 被阅读0次

本文为经典蓝牙通讯过程简单梳理。

1.实用工具

image.png

sscom可以调试收发数据。当蓝牙设备连接到PC端,确保相关驱动安装之后,PC能识别蓝牙设备,然后选择适当的波特率,一般是9600,选中端口号后打开串口,就可以发送数据了,也能显示接收的数据。
开发过程中,可以使用这个工具测试一些数据收发问题。

2.蓝牙通讯基本流程

参考这篇即可:
Android蓝牙串口通信
然后还有一些定位权限问题,socket异常处理,需要额外添加。
基本流程如下。

3.数据处理

蓝牙SPP的协议栈使用是rfcomm(串口仿真协议),通过一个虚拟串口通道和对方通信。



蓝牙协议栈

这里蓝牙设备与Android设备采用异步串行接口(UART)进行数据通信。UART是异步串口通信协议的一种,数据传输一般都是以字节传输的,一个字节8个位,将传输数据一位接一位地传输,要传一个字节就需要传8次。

3.1粘包和半包

粘包:
指数据在传输时,在一条消息中读取到了另一条消息的部分数据,这种现象就叫做粘包。
比如发送了两条消息,分别为“ABC”和“DEF”,那么正常情况下接收端也应该收到两条消息“ABC”和“DEF”,但接收端却收到的是“ABCD”,像这种情况就叫做粘包,如下图所示:

image.png
半包:
是指接收端只收到了部分数据,而非完整的数据的情况就叫做半包。比如发送了一条消息是“ABC”,而接收端却收到的是“AB”和“C”两条信息,这种情况就叫做半包,如下图所示:
image.png

产生原因
粘包的主要原因:

  • 发送方每次写入数据 < 套接字(Socket)缓冲区大小
  • 接收方读取套接字(Socket)缓冲区数据不够及时

半包的主要原因:

  • 发送方每次写入数据 > 套接字(Socket)缓冲区大小
  • 发送的数据大于协议的 MTU (Maximum Transmission Unit,最大传输单元),因此必须拆包

其实我们可以换个角度看待问题:

  • 从收发的角度看,便是一个发送可能被多次接收,多个发送可能被一次接收。
  • 从传输的角度看,便是一个发送可能占用多个传输包,多个发送可能共用一个传输包。

根本原因,其实是TCP 是流式协议,消息无边界。
(PS : UDP 虽然也可以一次传输多个包或者多次传输一个包,但每个消息都是有边界的,因此不会有粘包和半包问题。)

粘包和半包问题是数据传输中比较常见的问题。

它的解决方案有很多,比较常见的解决方案有:
设置固定的数据传输大小、自定义请求协议的封装,在请求头中加入传输数据的长度、使用特殊符号作为结束符等。

3.2 发送数据处理

为确保数据的完整性(减少错误),数据按照固定的帧格式进行传输。

3.2.1 CRC校验

验证CRC 计算的网站:http://www.ip33.com/crc.html
输入要传入的数据,然后选择参数模型即可,例如CRC-16/Modbus。

B与bit
数据存储是以“字节”(Byte)为单位,数据传输大多是以“位”(bit,又名“比特”)为单位,一个位就代表一个0或1(即二进制),每8个位(bit,简写为b)组成一个字节(Byte,简写为B),是最小一级的信息单位

image.png
image.png

java不论是负数还是正数在定义、存储、计算的过程中,都是用其补码


public static String getCRC(byte[] bytes) {
//        ModBus 通信协议的 CRC ( 冗余循环校验码含2个字节, 即 16 位二进制数。
//        CRC 码由发送设备计算, 放置于所发送信息帧的尾部。
//        接收信息设备再重新计算所接收信息 (除 CRC 之外的部分)的 CRC,
//        比较计算得到的 CRC 是否与接收到CRC相符, 如果两者不相符, 则认为数据出错。
//
//        1) 预置 1 个 16 位的寄存器为十六进制FFFF(即全为 1) , 称此寄存器为 CRC寄存器。
//        2) 把第一个 8 位二进制数据 (通信信息帧的第一个字节) 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器。
//        3) 把 CRC 寄存器的内容右移一位( 朝低位)用 0 填补最高位, 并检查右移后的移出位。
//        4) 如果移出位为 0, 重复第 3 步 ( 再次右移一位); 如果移出位为 1, CRC 寄存器与多项式A001 ( 1010 0000 0000 0001) 进行异或。
//        5) 重复步骤 3 和步骤 4, 直到右移 8 次,这样整个8位数据全部进行了处理。
//        6) 重复步骤 2 到步骤 5, 进行通信信息帧下一个字节的处理。
//        7) 将该通信信息帧所有字节按上述步骤计算完成后,得到的16位CRC寄存器的高、低字节进行交换。
//        8) 最后得到的 CRC寄存器内容即为 CRC码。

        int CRC = 0x0000ffff;
        int POLYNOMIAL = 0x0000a001;

        int i, j;
        for (i = 0; i < bytes.length; i++) {
            CRC ^= (int) bytes[i];
            for (j = 0; j < 8; j++) {
                if ((CRC & 0x00000001) == 1) {
                    CRC >>= 1;
                    CRC ^= POLYNOMIAL;
                } else {
                    CRC >>= 1;
                }
            }
        }
        //高低位转换,看情况使用(譬如本人这次对led彩屏的通讯开发就规定校验码高位在前低位在后,也就不需要转换高低位)
        //CRC = ( (CRC & 0x0000FF00) >> 8) | ( (CRC & 0x000000FF ) << 8);
        Log.d(TAG,"int CRC = " + CRC + " , string src = " + Integer.toHexString(CRC));

        return Integer.toHexString(CRC);
    }

如果协议要求16位CRC高8位在后,低8位在前,就需要使用高低位转换。

3.2.2 数据转换

串口传输接口底层是按位(bit)发送的,上层是按byte发送和接收的,但协议为了方便描述,每个byte用十六进制数(0x00~0xFF)表示,范相当于十进制的0~255,而byte为八位且是有符号类型,相当于十进制的-128~127,明显0x8F0xFF(128~255)是不能准确转换为byte的,咋办?因为0xFF被当作255整数处理,255超过了byte(-128127)的范围,不能直接赋值,只能强转啊。
byte b = (byte) 0xFF;
但此时你会不会觉得这样写就和协议想发送0xFF的想法不匹配了呢,我明明想发个255来着,那我告诉你,你一个一个字节的发,每个字节就只能是-128~127,不能在其他范围,所以强转的目的是你可以发送这个0xFF的前提,发送的值肯定不是255,具体参考下面的例子:

public class Main {
 
    public static void main(String[] args) {
        byte c1 = (byte) 0xFF;
        int c2 = c1 & 0xFF;
        System.out.println(""+c1);
        System.out.println(""+c2);
    }
}// -1  255

可见,实际上是一个-1,但接受端为了表达这是个发送端发送给我的是0xFF(255)而不是-1,你可以通过取到该字节值后 “ 该值 & 0xFF”转为无符号值即255.

3.2.3 组帧

现存在一个byte数组,如下,发送命令前需按CRC-16/Modbus计算CRC,16位CRC高8位在后,低8位在前。

private byte[] bytes= {0x5A, (byte) 0xA5,0x01 ,0x06 ,0x00 ,0x01, 0x00 ,0x17};
...

getCRC(bytes);

可得到数据int CRC = 23976 , string src = 5da8
返回 5da8 。

image.png

CRC-16/Modbus是16位CRC高8位在后,低8位在前。
所以实际需要发送的数据 是
A8 5D
那么getCRC就需要修改一下。使用CRC = ( (CRC & 0x0000FF00) >> 8) | ( (CRC & 0x000000FF ) << 8);进行高低位转换。

如果需要返回byte[]数据的话,就new byte。

return new byte[]
 {(byte) (CRC & 0xff), (byte) (CRC >> 8 & 0xff);};

然后再使用 System.arraycopy拼接。
System.arraycopy(buf, 0, out, 5, len);

arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

  • src:源数组;
  • srcPos:源数组要复制的起始位置;
  • dest:目的数组;
  • destPos:目的数组放置的起始位置;
  • length:复制的长度.
    5个参数
  • 第一个参数是要被复制的数组
  • 第二个参数是被复制的数字开始复制的下标
  • 第三个参数是目标数组,也就是要把数据放进来的数组
  • 第四个参数是从目标数据第几个下标开始放入数据
  • 第五个参数表示从被复制的数组中拿几个数值放到目标数组中

3.3 处理接收数据

简单举例。

指令格式



格式说明:
字段说明STX起始符(固定为0x80,1 byte)LEN数据长度(CMD、DATA的长度,2 bytes)CMD业务类型(1 byte)DATA业务参数(TLV格式,可变长度)ETX结束符(固定为0x81,1 byte)CRCCRC32校验(取高位两个字节,2 bytes)

处理逻辑:将每次串口收到的数据追加保存到全局的ByteBuffer中,并每次对ByteBuffer中实际数据长度与字段LEN计算的长度进行比较,截取一条完整有效的指令处理。

public class SerialPortUtils {
    /**
     * 串口数据
     */
    private ByteBuffer byteBuffer;

    public void receiveSerialMsg(String msg) {
        try {
            byte[] receiveBytes = hexStr2bytes(msg);
            // 将串口收到的数据追加到ByteBuffer
            byteBuffer.put(receiveBytes);

            if(byteBuffer.position() >= 3) {
                byte[] byteLength = new byte[2];
                byteLength[0] = byteBuffer.get(1);
                byteLength[1] = byteBuffer.get(2);
// 计算一条完整指令的长度
// 6表示STX(1 byte) + ETX(1 byte) + LEN(2 bytes) + CRC(2 bytes)
                int realLen = (byteLength[0] << 8) + byteLength[1] + 6;
                //byteBuffer实际长度与LEN字段长度比较,若相等则表示已经接收到一条完整的指令
                if(realLen == byteBuffer.position()) {

                    byte[] effectiveBytes = new byte[realLen];
                    byteBuffer.flip();
                    byteBuffer.get(effectiveBytes, 0, realLen);
                    byteBuffer.clear();

                    dealEffectiveBytes(effectiveBytes);

                } else if(realLen < byteBuffer.position()) {   
                //粘包,截取有效数据,当一次收到多条完整指令时,根据业务场景决定多条指令的解析
                    int remainLen = byteBuffer.position() - realLen;
                    byte[] effectiveBytes = new byte[realLen];
                    byte[] remainBytes = new byte[remainLen];

                    byteBuffer.flip();
                    byteBuffer.get(effectiveBytes, 0, realLen);
                    byteBuffer.get(remainBytes, 0, remainLen);  //剩余字节
                    byteBuffer.clear();
                    byteBuffer.put(remainBytes);  //回填剩余字节,下一次收到数据继续追加接

// 根据业务场景,一次性收到多条完整指令时,只处理第一条指令,其余的舍弃
                    dealEffectiveBytes(effectiveBytes);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

// 解析一条完整的指令
    private void dealEffectiveBytes(byte[] effectiveBytes) {
        Map<String, String> outputMap = new HashMap<>();
        //将指令解析结果存到Map中
        int result = dataParser(effectiveBytes, outputMap);
    }
    
    public byte[] hexStr2bytes(String hex) {
        int len = (hex.length() / 2);
        byte[] result = new byte[len];
        char[] achar = hex.toUpperCase().toCharArray();
        for (int i = 0; i < len; i++) {
            int pos = i * 2;
            result[i] = (byte) (hexChar2byte(achar[pos]) << 4 | hexChar2byte(achar[pos + 1]));
        }
        return result;
    }
    //指令中业务参数解析
public int dataParser(byte[] data, Map<String, String> outputMap) {
// 校验STX、ETX
    if (data[0] != (byte) 0x80 || data[data.length - 3] != (byte) 0x81) {
      return ERROR_STX_ETX;
    }
// 校验CRC
    if (!checkCRC(data)) {
      return ERROR_CRC;
    }

    int index, curLen = 0;
    int dataLen = (data[1] << 8) + data[2];
    // 校验长度
    if ((dataLen + 6) != data.length) {
      return ERROR_LENGTH;
    }

    byte[] temp = new byte[1];
    //解析CMD
    temp[0] = data[3];
    String strCmd = byte2hex(temp);
    outputMap.put("SERIAL_PORT_CMD", strCmd.toUpperCase());

    if (data[3] == (byte) 0xA0) { //自定义的CMD
      index = 4;
      while (curLen < (dataLen - 1)) {//解析DATA,TLV格式

        byte type = data[index++];
        int length = data[index++];
        byte[] value = new byte[length];
        for (int i = 0; i < length; i++) {
          value[i] = data[index++];
        }

        if (type == (byte) 0x01) { // 参数:时间
          outputMap.put("SERIAL_PORT_TIME", new String(value));
        } else if(type == (byte) 0x02) { // 参数:握手数据
          outputMap.put("SERIAL_PORT_TEST", new String(value));
        }

        curLen += length + 2;
      }
    }

    return 0;
  }
}

receiveSerialMsg方法,在线程中循环接收到串口数据后调用。

基本逻辑就是 根据数据格式(帧头和帧尾)去取有效数据,过滤无效数据。然后有时候,有效数据中夹杂有一定规律的无效数据,就需要根据具体业务逻辑,用正则去匹配,删掉这些数据。

参考链接:
android串口通信接受自定义协议数据并解析问题

Android蓝牙串口通信
比isConnected()更靠谱的的获取socket实时连接状态!
Android经典蓝牙开发全流程
Android蓝牙开发—经典蓝牙详细开发流程
[蓝牙] 1、蓝牙核心技术了解(蓝牙协议、架构、硬件和软件笔记)
经典蓝牙那些事儿——(登录、登录异常、自动连接、中断、断后重连等)
关于蓝牙开发,必须注意的广播

Android串口通信,分包/黏包数据解析
java实现CRC16 MODBUS校验算法
java byte和int_Java里byte和int对比问题
socket的封包、粘包、解包,做一个即时通讯项目
SOCKET/串口通信粘包问题处理,附带详细代码
Android串口通信,分包/黏包数据解析
【粘包和拆包】数据帧粘包和拆包处理方式
串口通讯(含协议)支持粘包分包解析数据
tcp粘包和拆包的处理方案
android串口通信接受自定义协议数据并解析问题
System.arraycopy的使用方法详解
Modbus 通信协议详解

相关文章

  • 蓝牙通讯简单总结

    本文为经典蓝牙通讯过程简单梳理。 1.实用工具 sscom可以调试收发数据。当蓝牙设备连接到PC端,确保相关驱动安...

  • ExternalAccessory框架

    ●前言 前几天公司项目蓝牙外设与程序通讯出了问题,所以今天来简单总结下ExternalAccessory框架。 ●...

  • 蓝牙开发模块

    一、引言蓝牙是设备近距离通信的一种方便手段,在iPhone引入蓝牙4.0后,设备之间的通讯变得更加简单。相关的蓝牙...

  • iOS蓝牙通讯开发

    刚开发完一款APP,其中涉及到应用和硬件进行蓝牙通讯需求,记录分享总结.场景:APP扫描制定的蓝牙设备,连接设备,...

  • 蓝牙通讯

    关于蓝牙协议栈,这里分享两处整理的较为全面和细致的资源:蜗窝科技CY大象 前段时间一直在研究蓝牙, 加了一些做蓝牙...

  • Android 蓝牙4.0入门开发

    本文针对一对一的蓝牙进行通讯,适合没有开发过蓝牙的同学来看,也适合大部分物联网简单开发,没有深入蓝牙的开发。对于没...

  • 【转】iOS开发之蓝牙通信

    iOS开发之蓝牙通讯 一、引言 蓝牙是设备近距离通信的一种方便手段,在iPhone引入蓝牙4.0后,设备之间的通讯...

  • Android 蓝牙开发

    近期的项目涉及到蓝牙通讯,于是就整理了一下蓝牙的通讯机制的知识点。蓝牙通讯主要是配对和连接两个过程。 配对和连接是...

  • iOS蓝牙开发CoreBlueTooth库核心方法使用介绍

    一、引言 蓝牙是设备近距离通信的一种方便手段,在iPhone引入蓝牙4.0后,设备之间的通讯变得更加简单。相关的蓝...

  • iOS蓝牙开发CoreBlueTooth库核心方法使用介绍

    一、引言 蓝牙是设备近距离通信的一种方便手段,在iPhone引入蓝牙4.0后,设备之间的通讯变得更加简单。相关的蓝...

网友评论

      本文标题:蓝牙通讯简单总结

      本文链接:https://www.haomeiwen.com/subject/bvevnrtx.html