SpringBoot中集成串口通信
串口通信介绍
串口通信是一种按位发送和接收字节的简单概念,尽管比并行通信慢,但串口可以同时使用一根线发送数据和接收数据。
串口通信简单且能够实现远距离通信,例如,串口的长度可达1200米,而并行通信的长度限制为20米.
串口通常用于ASCII码字符的传输,通信使用地线、发送线和接收线三根线完成。
重要的参数有波特率、数据位、停止位和奇偶校验。
波特率
这是一个衡量符号传输速率的参数。指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。一般调制速率大于波特率,比如通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信
数据位
这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
停止位
用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
奇偶校验位
在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
开始集成
组件介绍
对于Java集成串口通信,常见的选择有 原生Java串口通信API、RXTX库、jSerialComm库,
Maven依赖导入
com.fazecast
jSerialComm
2.6.2
cn.hutool
hutool-all
5.6.5
配置类
创建一个 SerialConfig 用于定义串口通用信息配置
import com.fazecast.jSerialComm.SerialPort;
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* 用于定义串口通用信息配置
* */
@Configuration
public class SerialConfig {
/**
* 波特率
* */
public static int baudRate = 19200;
/**
* 数据位
*/
public static int dataBits = 8;
/**
* 停止位 ( 1停止位 = 1 、 1.5停止位 = 2 、2停止位 = 3)
* */
public static int stopBits = 1;
/**
* 校验模式 ( 无校验 = 0 、奇校验 = 1 、偶校验 = 2、 标记校验 = 3、 空格校验 = 4 )
* */
public static int parity = 1;
/**
* 是否为 Rs485通信
* */
public static boolean rs485Mode = true;
/**
* 串口读写超时时间(毫秒)
* */
public static int timeOut = 300;
/**
* 消息模式
* 非阻塞模式: #TIMEOUT_NONBLOCKING 【在该模式下,readBytes(byte[], long)和writeBytes(byte[], long)调用将立即返回任何可用数据。】
* 写阻塞模式: #TIMEOUT_WRITE_BLOCKING 【在该模式下,writeBytes(byte[], long)调用将阻塞,直到所有数据字节都成功写入输出串口设备。】
* 半阻塞读取模式: #TIMEOUT_READ_SEMI_BLOCKING 【在该模式下,readBytes(byte[], long)调用将阻塞,直到达到指定的超时时间或者至少可读取1个字节的数据。】
* 全阻塞读取模式:#TIMEOUT_READ_BLOCKING 【在该模式下,readBytes(byte[], long)调用将阻塞,直到达到指定的超时时间或者可以返回请求的字节数。】
* 扫描器模式:#TIMEOUT_SCANNER 【该模式适用于使用Java的java.util.Scanner类从串口进行读取,会忽略手动指定的超时值以确保与Java规范的兼容性】
* */
public static int messageModel = SerialPort.TIMEOUT_READ_BLOCKING;
/**
* 已打开的COM串口 (重复打开串口会导致后面打开的无法使用,所以打开一次就要记录到公共变量存储)
* */
public final static Map portMap = new HashMap();
}
串口工具类
准备一个SerialService 用于创建串口,关闭串口,收发消息
import cn.hutool.core.codec.BCD;
import com.fazecast.jSerialComm.SerialPort;
import com.tce.station.common.config.SerialConfig;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 串口服务类
* */
@AllArgsConstructor
@Slf4j
@Service
public class SerialService {
/**
* 获取串口及状态
* */
public Map getPortStatus(){
Map comStatusMap = new HashMap();
List commPorts = Arrays.asList(SerialPort.getCommPorts());
commPorts.forEach(port->{
comStatusMap.put(port.getSystemPortName(), port.isOpen());
});
return comStatusMap;
}
/**
* 添加串口连接
* */
public void connectSerialPort(String portName){
SerialPort commPort = SerialPort.getCommPort(portName);
if (commPort.isOpen()){
throw new RuntimeException("该串口已被占用");
}
if (SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("该串口已被占用");
}
// 打开端口
commPort.openPort();
if (!commPort.isOpen()){
throw new RuntimeException("打开串口失败");
}
// 设置串口参数 (波特率、数据位、停止位、校验模式、是否为Rs485)
commPort.setComPortParameters(SerialConfig.baudRate, SerialConfig.dataBits,SerialConfig.stopBits, SerialConfig.stopBits, SerialConfig.rs485Mode);
// 设置串口超时和模式
commPort.setComPortTimeouts(SerialConfig.messageModel ,SerialConfig.timeOut, SerialConfig.timeOut);
// 添加至串口记录Map
SerialConfig.portMap.put(portName, commPort);
}
/**
* 关闭串口连接
* */
public boolean closeSerialPort(String portName){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("该串口未启用");
}
// 获取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 关闭串口
port.closePort();
// 需要等待一些时间,否则串口关闭不完全,会导致无法打开
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (port.isOpen()){
return false;
}else {
// 关闭成功返回
return true;
}
}
/**
* 串口发送数据
* */
public void sendComData(String portName, byte[]sendBytes){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("该串口未启用");
}
// 获取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 发送串口数据
int i = port.writeBytes(sendBytes, sendBytes.length);
if (i == -1){
log.error("发送串口数据失败{}, 数据内容{}",portName, BCD.bcdToStr(sendBytes));
throw new RuntimeException("发送串口数据失败");
}
}
/**
* 串口读取数据
* */
public byte[] readComData(String portName){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("该串口未启用");
}
// 获取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 读取串口流
InputStream inputStream = port.getInputStream();
// 获取串口返回的流大小
int availableBytes = 0;
try {
availableBytes = inputStream.available();
} catch (Exception e) {
e.printStackTrace();
}
// 读取指定的范围的数据流
byte[] readByte = new byte[availableBytes];
int bytesRead = 0;
try {
bytesRead = inputStream.read(readByte);
} catch (Exception e) {
e.printStackTrace();
}
return readByte;
}
}
串口业务类使用
基于以上的工具类就已经可以对串口通信进行开发了,以下是使用案例
1.创建串口连接
可以使用监听器方式接收数据,但是需要进行绑定,后续会介绍
// 从数据库或者配置表中读取设定要打开的串口
List comList = comService.list();
// 关闭之前的监听连接(提取所有串口避免重复关闭)
SerialConfig.portMap.forEach((com,serialPort) ->{
serialService.closeSerialPort(com);
});
// 等待之前的串口发送和 2倍监听超时,避免还有串口通信线程未关闭
try {
Thread.sleep((SerialConfig.timeOut + 1) * 2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// 清空COM口记录
SerialConfig.portMap.clear();
// 重新连接串口
gunList.forEach(gun->{
// 如果COM口没有就打开
if (!SerialConfig.portMap.containsKey(gun.getCom())){
// 创建连接
SerialPort serialPort = serialService.connectSerialPort(gun.getCom());
// 绑定监听器
// serialPort.addDataListener(new MessageListener());
}
});
2.关闭串口连接
String com = "COM1";
serialService.closeSerialPort(com);
3.定时发送串口数据
/**
* 周期性向串口发送数据
* */
@Scheduled(fixedRate = 1500L)
public void send{
// 因为是阻塞是监听线程,所以使用线程处理
Thread thread = new Thread(() -> {
try {
SerialConfig.portMap.forEach((com,serialPort)->{
// 等待0.1秒
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
// 调用业务逻辑获取需要推送的数据
byte[] sendBytes = getPushData(com);
// 发送串口数据
serialService.sendComData(com, sendBytes);
log.info("向串口发送 {}",gun.getGunNum(), com, BCD.bcdToStr(sendBytes));
});
}catch (ConcurrentModificationException e){
log.info("COM口配置发生变化,等待配置生效");
}
});
// 开启发送线程
thread.start();
}
4.周期性读取串口数据
/**
* 周期性读取串口数据
* */
@Scheduled(fixedRate = 1000L)
public void readComData() {
// 遍历监听
SerialConfig.portMap.forEach((com,serialPort)->{
// 因为是阻塞是监听线程,所以使用线程处理,否则某个读取失败,会阻塞整个程序
Thread thread = new Thread(() -> {
byte[] readByte = serialService.readComData(com);
// 有数据才执行
if (readByte.length > 1) {
try {
log.info("收到串口数据: {}", BCD.bcdToStr(readByte));
// 调用串口响应业务操作
comOperationByData(comResult,BCD.strToBcd(res), com);
}catch (Exception e){
e.printStackTrace();
}
});
// 开启线程
thread.start();
}
5.监听式读取串口数据
监听式读取数据使用的是非阻塞行读取数据,有数据就会触发
创建一个监听器
@Slf4j
public class MessageListener implements SerialPortDataListener {
@Autowired
ICommandService commandService;
/**
* 监听事件设置
* */
@Override
public int getListeningEvents() {
// 持续返回数据流模式
return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;
// 收到数据立即返回
// return SerialPort.LISTENING_EVENT_DATA_RECEIVED;
}
/**
* 收到数据监听回调
* */
@Override
public void serialEvent(SerialPortEvent event) {
// 因为是阻塞是监听线程,所以使用线程处理
Thread thread = new Thread(() -> {
// 读取串口流
InputStream inputStream = event.getSerialPort().getInputStream();
// 获取串口返回的流大小
int availableBytes = 0;
try {
availableBytes = inputStream.available();
} catch (Exception e) {
e.printStackTrace();
}
// 读取指定的范围的数据流
byte[] readByte = new byte[availableBytes];
int bytesRead = 0;
try {
bytesRead = inputStream.read(readByte);
} catch (Exception e) {
e.printStackTrace();
}
try {
inputStream.close();
} catch (IOException e) {
throw new RuntimeException("关闭串口流失败"+e.getMessage());
}
// 有数据才执行
if (readByte.length > 1) {
try {
log.info("收到串口数据: {}", BCD.bcdToStr(readByte));
// 调用串口响应业务操作
comOperationByData(comResult,BCD.strToBcd(res), com);
}catch (Exception e){
e.printStackTrace();
}
}
});
// 开启线程
thread.start();
}
}
给串口连接进行绑定监听器
// 创建连接
SerialPort serialPort = serialService.connectSerialPort(gun.getCom());
// 绑定监听器
serialPort.addDataListener(new MessageListener());
需要注意的是监听器接收数据和定时接收数据选取其中一个就好了