之前从未与串口通讯打过交道的我,暑假实习第一个任务居然就是串口通讯,其实我对于这种底层玩意儿一点都不熟,于是乎只能在网上找第三方的开源库,最后使用的是jssc。

需求

通过串口按照某种私有协议与设备通信进行通信,对串口设备进行增删查改(大雾)

数据帧格式

数据帧格式

基本就是类似这个样子,图示的日期,时间值是属于数据域,是可选部分。

结构设计

整个数据帧是一个对象,含有起始符,数据帧长度,通讯流水,功能码,地址,数据域,校验码,结束符八个属性,再将数据域给抽出来,作为一个接口,不同的数据域有不同的解析(生成)实现。 接口设计:

public interface DataCode {
    
    //获取data的byte数组
    byte[] getBytes();

    //获取data长度
    int getSize();

    //获取data的完整含义
    String getInfo();
    
}

典型结构 串口通信.png

就是一个一层一层装箱的过程,额外有一个命令工厂,用来生成数据帧。

数据接收

编码部分比较简单,记一下接收过程的解码。jssc接收数据是用的监听器,我这边是直接将所有从串口发来的信息转换为byte,放入缓冲区,然后额外开个线程对缓冲期进行筛选。


    /**
     * 根据控制码信息获取有效的返回数据帧
     *
     * @param sn          流水号
     * @param controlCode 控制码信息
     * @param buffer_temp 用来存储的缓冲对象
     * @param timeout     超时(毫秒)
     * @param offset      从缓冲区读取的偏移量
     * @return 读取到的数据帧
     */
    static DataFrame getValidData(int sn, ControlCodes.ControlCode controlCode, LinkedList<Byte> buffer_temp, long timeout, int offset) {
        Long startTime = System.currentTimeMillis();
        //取出缓冲区所有数据
        List<Byte> temp = new LinkedList<Byte>();
        buffer.drainTo(temp);
        //加入缓冲区复制块
        buffer_temp.addAll(temp);

        DataFrame response = null;
        //获取到对应控制码的信息

        //获取流水号位
        byte[] ids = BytesUtil.fillBlank(BytesUtil.intToByte(sn), 2);
        BytesUtil.swapHL(ids);

        //响应特征码(流水号+控制码)
        List<Byte> flag = new LinkedList<Byte>(Arrays.asList(new Byte[]{ids[0], ids[1], controlCode.getRespByte()}));
        int s, e;
        //如果不包含起始位
        if (!buffer_temp.contains((byte) 0x68)) {
            logger.debug(BytesUtil.byteToHexStr(buffer_temp.toArray(new Byte[]{})));
            logger.debug(timeout - (System.currentTimeMillis() - startTime) + "-不包含起始位:" + buffer_temp.size());
            offset = buffer_temp.size();
        }
        //当缓冲区包含起始码时
        else {
            for (int i = offset; i <= buffer_temp.size() - 14; i++) {
                //如果匹配到起始码
                if (buffer_temp.get(i).equals((byte) 0x68)) {
                    List<Byte> flagTemp = buffer_temp.subList(i + 3, i + 6);
                    if (flagTemp.equals(flag)) {
                        //获取到长度
                        byte[] lengthByte = new byte[2];
                        lengthByte[0] = buffer_temp.get(i + 1);
                        lengthByte[1] = buffer_temp.get(i + 2);
                        BytesUtil.swapHL(lengthByte);
                        int length = 0;
                        try {
                            length = BytesUtil.byteToInt(lengthByte);
                        } catch (Exception ex) {
                            //do nothing
                            length = 0;
                        }
                        //获取不到长度
                        if (length == 0) {
                            offset = i + 1;
                        }
                        //能获取到长度
                        else {
                            //长度不够
                            if (i + length - 1 > buffer_temp.size()) {
                                break;
                            }
                            //长度足够
                            else {
                                //如果终止码匹配
                                if (buffer_temp.get(i + length - 1).equals((byte) 0x16)) {
                                    List<Byte> frameTemp = buffer_temp.subList(i, i + length);
                                    //尝试开始解析
                                    try {
                                        String pkg = SerialPortManager.class.getPackage().getName();
                                        logger.debug("开始解析");
                                        if (controlCode.getDataType() == null) {
                                            response = DataFrame.newInstance(frameTemp.toArray(new Byte[frameTemp.size()]));
                                        }
                                        else {
                                            response = DataFrame.newInstance(frameTemp.toArray(new Byte[frameTemp.size()]), Class.forName(pkg + ".data." + controlCode.getDataType()));
                                        }
                                    } catch (ClassNotFoundException e1) {
                                        logger.debug("解析失败");
                                    }
                                    //如果成功解析
                                    if (response != null) {
                                        logger.debug(BytesUtil.byteToHexStr(buffer_temp.toArray(new Byte[]{})));
                                        logger.debug("解析成功");
                                        return response;
                                    }
                                }
                                //如果不匹配
                                else {
                                    offset = i + 1;
                                }
                            }
                        }
                    }
                    //特征码不匹配
                    else {
                        offset = i + 1;
                    }
                }
            }
        }
        try {
            logger.debug("暂停");
            Thread.sleep(500);
        } catch (InterruptedException e1) {
            logger.debug("休眠出错");
        }
        timeout -= (System.currentTimeMillis() - startTime);
        if (timeout <= 0) {
            logger.debug(BytesUtil.byteToHexStr(buffer_temp.toArray(new Byte[]{})));
            logger.debug("未收到有效数据");
            return null;
        }
        return getValidData(sn, controlCode, buffer_temp, timeout, offset);
    }

大致就是,从缓冲区读取到数据,然后第一步是根据控制码对应的返回的响应码以及流水号,来作为一个关键标志,然后根据紧挨着的数据长度来判断起始位是否正确,因为串口返回数据并不是即时的,所以有时需要等待。依旧是使用递归,其中增加了一个超时设置。 数据解析是通过响应码对应的数据类型做的反射。