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库,
- 原生Java串口通信API只支持到Java6版本,后续便不再维护,所以不推荐使用
- RXTX库是过去主流开发串口通信使用的依赖组件,但是由于需要在jvm包中添加指定的依赖组件,其次,RXTX的稳定性和兼容性可能存在一些问题,且仅维护至Jdk8版本,后续不再持续维护了,所以本次也不考虑使用它
- 所以本次采用的是jSerialComm库,以下是jSerialComm库的一些主要特点和功能:
- 跨平台支持:jSerialComm可以在多个操作系统上使用,包括Windows、Linux和MacOS等。
- 多串口支持:它可以同时管理多个串口,通过获取和管理已连接的串口列表,方便选择和使用特定的串口。
- 简单的API:jSerialComm提供了简洁易用的API,使串口的打开、读取、写入和关闭等操作变得简单和直观。
- 支持异步读取:可以使用回调函数或监听器来异步读取串口数据,实现非阻塞的读取操作。
- 高性能:jSerialComm使用了底层的串口通信库,具有高效的读写性能,适用于处理大量的串口数据。
- 可靠性和稳定性:它经过了充分测试和优化,具有良好的稳定性和可靠性,能够处理各种串口通信场景。
- 开源免费:jSerialComm是一个开源库,使用MIT许可证,可以免费使用和修改。
Maven依赖导入
<!-- COM串口通信 --> <dependency> <groupId>com.fazecast</groupId> <artifactId>jSerialComm</artifactId> <version>2.6.2</version> </dependency> <!-- hutool工具 --> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.6.5</version> </dependency>
配置类
创建一个 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<String, SerialPort> 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<String, Boolean> getPortStatus(){ Map<String, Boolean> comStatusMap = new HashMap<>(); List<SerialPort> 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<String> 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());
需要注意的是监听器接收数据和定时接收数据选取其中一个就好了
到此这篇关于SpringBoot中集成串口通信的项目实践的文章就介绍到这了,更多相关SpringBoot 串口通信内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!