之前从未与串口通讯打过交道的我,暑假实习第一个任务居然就是串口通讯,其实我对于这种底层玩意儿一点都不熟,于是乎只能在网上找第三方的开源库,最后使用的是jssc。
需求
通过串口按照某种私有协议与设备通信进行通信,对串口设备进行增删查改(大雾)
数据帧格式
基本就是类似这个样子,图示的日期,时间值是属于数据域,是可选部分。
结构设计
整个数据帧是一个对象,含有起始符,数据帧长度,通讯流水,功能码,地址,数据域,校验码,结束符八个属性,再将数据域给抽出来,作为一个接口,不同的数据域有不同的解析(生成)实现。 接口设计:
public interface DataCode {
//获取data的byte数组
byte[] getBytes();
//获取data长度
int getSize();
//获取data的完整含义
String getInfo();
}
典型结构
就是一个一层一层装箱的过程,额外有一个命令工厂,用来生成数据帧。
数据接收
编码部分比较简单,记一下接收过程的解码。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);
}
大致就是,从缓冲区读取到数据,然后第一步是根据控制码对应的返回的响应码以及流水号,来作为一个关键标志,然后根据紧挨着的数据长度来判断起始位是否正确,因为串口返回数据并不是即时的,所以有时需要等待。依旧是使用递归,其中增加了一个超时设置。 数据解析是通过响应码对应的数据类型做的反射。