一.串口通信页面
Java 实现串口通信,同时通过 WebSocket 与 UI 实时交互传递通信数据
准备工作:
虚拟串口工具:Launch Virtual Serial Port Driver
串口调试助手:SSCOM
RS485
根据 Modbus 协议,常规485通讯的信息发送形式如下:
地址 功能码 数据信息 校验码
1byte 1byte nbyte 2byte
在线 CRC检验码计算:点这里
二.串口服务实现
1.Java 串口通信配置
1.扩展包和依赖库
RXTXcomm.jar 放入 {JAVA_HOME}/jre/lib/ext
rxtxserial.dll 放入 {JAVA_HOME}/jre/bin
以上两个包可以直接网上下载,注意和JDK版本搭配即可
2.Pom配置
串口通信包:rxtx
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>SerialPort</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.4</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
<version>5.3.27</version>
</dependency>
<dependency>
<groupId>org.rxtx</groupId>
<artifactId>rxtx</artifactId>
<version>2.1.7</version>
</dependency>
</dependencies>
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
2.启动类
package com.serial.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @author
* @date 2023-07-01 12:41
* @since 1.8
*/
@SpringBootApplication
public class SerialApplication {
public static void main(String[] args) {
SpringApplication.run(SerialApplication.class,args);
}
}
3.工具包类
1.Common
package com.serial.demo.util;
/**
* @author
* @date 2023-07-03 22:17
* @since 1.8
*/
public class Common {
public static String HEX_STRING = "0123456789ABCDEF";
public static final String NONE = "无";
public static final String ODD = "奇";
public static final String EVEN = "偶";
public static final String FORMAT_HEX="HEX";
}
2.Crc16Modbus
CRC16 Modbus Java 实现:计算数据的校验码
package com.serial.demo.util;
/**
* @author
* @date 2023-07-04 20:37
* @since 1.8
*/
public class Crc16Modbus {
/**
* CRC 循环冗余校验 即通过生成多项式对原始数据进行计算,将计算结果拼接到数据上一起发送
* 接收方计算接收到的数据校验接收结果是否准确
* CRC 即对生成多项式的模二运算
*
* 1.预置1个16位的寄存器为十六进制 FFFF(即全为1),称此寄存器为CRC寄存器
* 2.把第1个8位二进制数据(帧头字节)与 CRC 寄存器的低8位相异或并写回寄存器 高8位数据不变
* 3.把 CRC 循环右移 高位补 0 取得移出位
* 4.如果移出位为 0 继续右移 如果移出位为 1 则 CRC 寄存器与多项式 A001(1010 0000 0000 0001)进行异或运算
* 5.重复步骤 3 和 4 直到右移 8 次
* 6.重复步骤 2 到 5 进行数据帧下一个字节的处理 直到将数据帧所有字节按上述步骤计算
* 7.根据需要将寄存器的高、低字节进行交换 得到最终 CRC码
*
*/
/**
* 初始值 CRC-16 寄存器
*/
private static final int INITIAL_VALUE = 0xFFFF;
private static final boolean IS_OUT_PUT_OVER_TURN = true;
/**
* 原始数据 + CRC码
*
* @param hexes 16 进制字符串
* @return
*/
public static byte[] getData(String... hexes) {
byte[] data = new byte[hexes.length];
int i = 0;
for (String hex:hexes){
//先转为数字在转为 byte
data[i++] = (byte) Integer.parseInt(hex, 16);
}
return merge(data);
}
/**
* 原始数据 + CRC码
*
* @param data
* @return
*/
public static byte[] merge(byte[] data) {
byte[] crc = getCrc16(data);
int dLen = data.length;
int cLen = crc.length;
byte[] result = new byte[dLen + cLen];
System.arraycopy(data,0,result,0,dLen);
System.arraycopy(crc,0,result,dLen,cLen);
return result;
}
/**
* 基于 CRC16 Modbus 计算校验码
* CRC 16 Modbus 默认多项式为 x16+x15+x2+1 => 8005 反转即 A001
*
* @param data
* @return
*/
private static byte[] getCrc16(byte[] data) {
int len = data.length;
int crc = INITIAL_VALUE;
int i, j;
for (i = 0; i < len; i++) {
// 把第一个 8 位二进制数据 与 16 位的 CRC寄存器的低 8 位相异或, 把结果放于 CRC寄存器
crc = ((crc & 0xFF00) | (crc & 0x00FF) ^ (data[i] & 0xFF));
for (j = 0; j < 8; j++) {
// 把 CRC 寄存器的内容右移一位(朝低位)用 0 填补最高位, 并检查右移后的移出位
if ((crc & 0x0001) > 0) {
// 如果移出位为 1, CRC寄存器与多项式A001进行异或
crc = crc >> 1;
crc = crc ^ 0xA001;
} else {
// 如果移出位为 0,再次右移一位
crc = crc >> 1;
}
}
}
return intToBytes(crc);
}
/**
* 将 int 转换成 byte 数组 低位在前 高位在后
*/
private static byte[] intToBytes(int value) {
byte[] src = new byte[2];
byte hig = (byte) ((value>>8) & 0xFF);
byte low = (byte) (value & 0xFF);
if (IS_OUT_PUT_OVER_TURN){
src[0] = low;
src[1] = hig;
} else {
src[0] = hig;
src[1] = low;
}
return src;
}
/**
* 将字节数组转换成十六进制字符串
*/
public static String byteTo16String(byte[] data) {
StringBuffer buffer = new StringBuffer();
for (byte b : data) {
byteToHex(buffer,b);
}
return buffer.toString().toUpperCase();
}
/**
* 将字节转换成十六进制字符串
*
* int 转 byte 对照表
* [128,255],0,[1,128)
* [-128,-1],0,[1,128)
*/
public static void byteToHex(StringBuffer buffer ,byte b) {
if (b < 0) {
buffer.append(Integer.toString(b + 256, 16));
} else if (b == 0) {
buffer.append("00 ");
} else if (b > 0 && b <= 15) {
buffer.append("0" + Integer.toString(b, 16));
} else if (b > 15) {
buffer.append(Integer.toString(b, 16));
}
buffer.append(" ");
}
}
3.SerialUtil
package com.serial.demo.util;
import gnu.io.SerialPort;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
/**
* @author
* @date 2023-07-03 21:52
* @since 1.8
*/
public class SerialUtil {
/**
* 转为 HEX
* @param str
* @return
*/
public static String toHex(String str){
StringBuffer sbf = new StringBuffer();
byte[] b = str.getBytes(StandardCharsets.UTF_8);
for (int i = 0; i < b.length; i++) {
String hex = Integer.toHexString(b[i] & 0xFF);
if (hex.length() == 1) {
hex = '0' + hex;
}
sbf.append(hex.toUpperCase() + " ");
}
return sbf.toString().trim();
}
/**
*
* @param hex
* @return
*/
public static String toStr(String hex) {
return new String(hexToByte(hex));
}
/**
* 转 HEX 字节
* @param hex
* @return
*/
public static byte[] hexToByte(String hex){
hex = hex.toUpperCase().replace(" ","");
ByteArrayOutputStream bao = new ByteArrayOutputStream(hex.length() / 2);
// 将每2位16进制整数组装成一个字节
for (int i = 0; i < hex.length(); i += 2) {
bao.write((Common.HEX_STRING.indexOf(hex.charAt(i)) << 4 | Common.HEX_STRING.indexOf(hex.charAt(i + 1))));
}
return bao.toByteArray();
}
/**
* 获取校验位配置
* @param checkBit
* @return
*/
public static int getParity(String checkBit){
if (Common.NONE.equals(checkBit)){
return SerialPort.PARITY_NONE;
} else if (Common.ODD.equals(checkBit)){
return SerialPort.PARITY_ODD;
} else if (Common.EVEN.equals(checkBit)){
return SerialPort.PARITY_EVEN;
} else {
return SerialPort.PARITY_NONE;
}
}
/**
* 读取数据
* @param in
* @return
*/
public static byte[] readFromPort(InputStream in) {
byte[] bytes = {};
try {
// 缓冲区大小为一个字节
byte[] readBuffer = new byte[1];
int bytesNum = in.read(readBuffer);
while (bytesNum > 0) {
bytes = concat(bytes, readBuffer);
bytesNum = in.read(readBuffer);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
in = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bytes;
}
/**
* 字节转换
* @param format
* @param b
* @return
*/
public static String printHexString(String format, byte[] b) {
String result = new String(b);
if (Common.FORMAT_HEX.equals(format)){
return SerialUtil.toHex(result);
}
return result;
}
/**
* 合并数组
*
* @param firstArray 第一个数组
* @param secondArray 第二个数组
* @return 合并后的数组
*/
public static byte[] concat(byte[] firstArray, byte[] secondArray) {
if (firstArray == null || secondArray == null) {
if (firstArray != null) {
return firstArray;
}
if (secondArray != null) {
return secondArray;
}
return null;
}
byte[] bytes = new byte[firstArray.length + secondArray.length];
System.arraycopy(firstArray, 0, bytes, 0, firstArray.length);
System.arraycopy(secondArray, 0, bytes, firstArray.length, secondArray.length);
return bytes;
}
}
4.WebSocket 配置
1.启动配置
package com.serial.demo.socket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author
* @date 2023-07-02 21:05
* @since 1.8
*/
@Configuration
public class WebSocketConfig {
/**
* 开启 websocket 配置
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter(){
return new ServerEndpointExporter();
}
}
2.监听配置
package com.serial.demo.socket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author
* @date 2023-07-02 21:07
* @since 1.8
*/
@Slf4j
@Component
@ServerEndpoint("/websocket/{sid}")
public class SerialWebSocket {
/**
* 缓存通信实例
*/
private static Map<String,SerialWebSocket> webSocketMap = new ConcurrentHashMap<>(16);
/**
* 会话
*/
private Session session;
/**
* 标识
*/
private String sid;
/**
* 建立连接
* @param sid
* @param session
*/
@OnOpen
public void onOpen(@PathParam("sid") String sid,Session session){
this.session = session;
this.sid = sid;
webSocketMap.put(sid,this);
//sendMessage(sid,"Hello:");
}
/**
* 关闭连接
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid){
try {
SerialWebSocket socket = webSocketMap.remove(sid);
if (socket != null){
socket.session.close();
socket = null;
}
} catch (IOException e) {
log.error("Close {} exception:",sid,e);
}
}
/**
* 接收消息
* @param message
*/
@OnMessage
public void onMessage(String message){
log.info("sid {} msg {}",this.sid,message);
}
/**
* 发送消息
* @param message
* @param sid
*/
public static void sendMessage(String sid,String message){
SerialWebSocket socket = webSocketMap.get(sid);
if (socket != null){
try {
socket.session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("Send {} message {} exception:",sid,message,e);
}
}
}
/**
* 广播消息
* @param message
*/
public static void broadcast(String message){
for (String sid:webSocketMap.keySet()){
sendMessage(sid,message);
}
}
}
5.UI交互类
1.串口配置对象
package com.serial.demo.entity;
import lombok.Data;
/**
* @author
* @date 2023-07-02 22:58
* @since 1.8
*/
@Data
public class SerialEntity {
private String portId;
private int bitRate;
private int dataBit;
private int stopBit;
private String checkBit;
private String format;
}
2.串口信息获取接口
package com.serial.demo.controller;
import com.serial.demo.config.SerialPortConfig;
import com.serial.demo.util.SerialUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* @author
* @date 2023-07-01 16:37
* @since 1.8
*/
@CrossOrigin
@RestController
@RequestMapping("/serial")
public class SerialController {
@Autowired
SerialPortConfig serial;
/**
* 获取端口列表
* @return
*/
@GetMapping("/getSerialPortList")
public List<String> getSerialPortList(){
return serial.getSerialPortList();
}
/**
* 字符串 转 HEX
* @return
*/
@GetMapping("/toHex")
public String toHex(String str){
return SerialUtil.toHex(str);
}
/**
* HEX 转 字符串
* @return
*/
@GetMapping("/toStr")
public String toStr(String hex){
return SerialUtil.toStr(hex);
}
}
3.RS232接口
package com.serial.demo.controller;
import com.serial.demo.config.Rs232Config;
import com.serial.demo.entity.SerialEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author
* @date 2023-07-03 1:03
* @since 1.8
*/
@CrossOrigin
@RestController
@RequestMapping("/serial/232")
public class Rs232Controller {
@Autowired
Rs232Config rs232Config;
/**
* 监听端口
* @param serial
*/
@PostMapping("/open")
public boolean open(@RequestBody SerialEntity serial){
return rs232Config.openPort(serial);
}
/**
* 获取端口列表
* @return
*/
@GetMapping("/close/{portId}")
public void close(@PathVariable("portId") String portId){
rs232Config.closePort(portId);
}
/**
* 获取端口列表
* @return
*/
@GetMapping("/send/{portId}/{format}/{msg}")
public void close(@PathVariable("portId") String portId,@PathVariable("format") String format,@PathVariable("msg") String msg){
rs232Config.sendData(portId,format,msg);
}
}
4.RS485接口
package com.serial.demo.controller;
import com.serial.demo.config.Rs485Config;
import com.serial.demo.entity.SerialEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @author
* @date 2023-07-03 23:33
* @since 1.8
*/
@CrossOrigin
@RestController
@RequestMapping("/serial/485")
public class Rs485Controller {
@Autowired
Rs485Config rs485Config;
/**
* 监听端口
* @param serial
*/
@PostMapping("/open")
public boolean open(@RequestBody SerialEntity serial){
return rs485Config.openPort(serial);
}
/**
* 获取端口列表
* @return
*/
@GetMapping("/close/{portId}")
public void close(@PathVariable("portId") String portId){
rs485Config.closePort(portId);
}
/**
* 获取端口列表
* @return
*/
@GetMapping("/send/{portId}/{format}/{msg}")
public void close(@PathVariable("portId") String portId,@PathVariable("format") String format,@PathVariable("msg") String msg){
rs485Config.sendData(portId,format,msg);
}
}
6.串口配置类
1.串口配置
package com.serial.demo.config;
import gnu.io.CommPortIdentifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
/**
* @author
* @date 2023-07-03 1:01
* @since 1.8
*/
@Slf4j
@Component
public class SerialPortConfig {
/**
* 缓存端口信息
*/
private static Map<String, CommPortIdentifier> serialMap;
@PostConstruct
private void init(){
refreshCom();
}
/**
* 刷新端口
*/
public void refreshCom(){
Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers();
CommPortIdentifier serial;
Map<String,CommPortIdentifier> temp = new ConcurrentHashMap<>(16);
while (portList.hasMoreElements()){
serial = portList.nextElement();
if (serial.getPortType() == CommPortIdentifier.PORT_SERIAL){
temp.put(serial.getName(),serial);
}
}
serialMap = Collections.unmodifiableMap(temp);
}
/**
* 获取端口列表
* @return
*/
public List<String> getSerialPortList(){
return serialMap.keySet().stream().sorted().collect(Collectors.toList());
}
/**
* 获取串口
* @return
*/
public Map<String, CommPortIdentifier> getSerialMap(){
return serialMap;
}
}
2.RS232串口配置
package com.serial.demo.config;
import com.serial.demo.entity.SerialEntity;
import com.serial.demo.util.Common;
import com.serial.demo.util.SerialUtil;
import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TooManyListenersException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author
* @date 2023-07-01 16:22
* @since 1.8
*/
@Slf4j
@Component
public class Rs232Config {
private static final int DELAY_TIME = 1000;
@Autowired
SerialPortConfig config;
/**
* 缓存端口实例
*/
private Map<String, SerialPort> serialPortMap = new ConcurrentHashMap<>(16);
/**
* 监听端口
* @param serial
*/
public boolean openPort(SerialEntity serial) {
String portId = serial.getPortId();
CommPortIdentifier commPortIdentifier = config.getSerialMap().get(portId);
if (null != commPortIdentifier){
SerialPort serialPort = null;
int bitRate = 0,dataBit = 0,stopBit = 0,parity = 0;
try {
serialPort = (SerialPort) commPortIdentifier.open(portId,DELAY_TIME);
// 设置监听器生效 当有数据时通知
serialPort.notifyOnDataAvailable(true);
// 比特率、数据位、停止位、奇偶校验位
bitRate = serial.getBitRate();
dataBit = serial.getDataBit();
stopBit = serial.getStopBit();
parity = SerialUtil.getParity(serial.getCheckBit());
serialPort.setSerialPortParams(bitRate, dataBit, stopBit,parity);
} catch (PortInUseException e) {
log.error("Open CommPortIdentifier {} Exception:",serial.getPortId(),e );
return false;
} catch (UnsupportedCommOperationException e) {
log.error("Set SerialPortParams BitRate {} DataBit {} StopBit {} Parity {} Exception:",bitRate,dataBit,stopBit,parity,e);
return false;
}
// 设置当前串口的输入输出流
InputStream input;
OutputStream output;
try {
input = serialPort.getInputStream();
output = serialPort.getOutputStream();
} catch (IOException e) {
log.error("Get serialPort data stream exception:",e);
return false;
}
// 给当前串口添加一个监听器
try {
serialPort.addEventListener(new Serial232Listener(input,output,serial.getFormat()));
} catch (TooManyListenersException e) {
log.error("Get serialPort data stream exception:",e);
return false;
}
serialPortMap.put(portId,serialPort);
return true;
}
return false;
}
/**
* 关闭端口
* @param portId
*/
public void closePort(String portId){
SerialPort serialPort = serialPortMap.remove(portId);
if (null != serialPort){
serialPort.close();
}
}
/**
* 发送数据
* @param portId
* @param format
* @param message
*/
public void sendData(String portId,String format,String message){
SerialPort serialPort = serialPortMap.get(portId);
if (null == serialPort){
return;
}
OutputStream output = null;
try {
byte[] bytes;
if (Common.FORMAT_HEX.equals(format)){
bytes = SerialUtil.hexToByte(message);
} else {
bytes = message.getBytes(StandardCharsets.UTF_8);
}
output = serialPort.getOutputStream();
output.write(bytes);
output.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (null != output){
try {
output.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
3.RS232串口监听
package com.serial.demo.config;
import com.serial.demo.socket.SerialWebSocket;
import com.serial.demo.util.Crc16Modbus;
import com.serial.demo.util.SerialUtil;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author
* @date 2023-07-01 17:06
* @since 1.8
*/
public class Serial232Listener implements SerialPortEventListener {
InputStream inputStream;
OutputStream outputStream;
String format;
public Serial232Listener(InputStream input, OutputStream output, String format){
inputStream = input;
outputStream = output;
this.format = format;
}
@Override
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.BI:
case SerialPortEvent.OE:
case SerialPortEvent.FE:
case SerialPortEvent.PE:
case SerialPortEvent.CD:
case SerialPortEvent.CTS:
case SerialPortEvent.DSR:
case SerialPortEvent.RI:
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
break;
case SerialPortEvent.DATA_AVAILABLE:
// 当有可用数据时读取数据
byte[] readBuffer = null;
int availableBytes = 0;
try {
availableBytes = inputStream.available();
while (availableBytes > 0) {
readBuffer = SerialUtil.readFromPort(inputStream);
String needData = Crc16Modbus.byteTo16String(readBuffer);
SerialWebSocket.broadcast(needData);
availableBytes = inputStream.available();
}
} catch (IOException e) {
}
default:
break;
}
}
}
4.RS485串口配置
package com.serial.demo.config;
import com.serial.demo.entity.SerialEntity;
import com.serial.demo.util.Common;
import com.serial.demo.util.Crc16Modbus;
import com.serial.demo.util.SerialUtil;
import gnu.io.CommPortIdentifier;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.TooManyListenersException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author
* @date 2023-07-03 1:00
* @since 1.8
*/
@Slf4j
@Component
public class Rs485Config {
private static final int DELAY_TIME = 1000;
@Autowired
SerialPortConfig config;
/**
* 缓存端口实例
*/
private Map<String, SerialPort> serialPortMap = new ConcurrentHashMap<>(16);
/**
* 监听端口
* @param serial
*/
public boolean openPort(SerialEntity serial) {
String portId = serial.getPortId();
CommPortIdentifier commPortIdentifier = config.getSerialMap().get(portId);
if (null != commPortIdentifier){
SerialPort serialPort;
int bitRate = 0,dataBit = 0,stopBit = 0,parity = 0;
try {
serialPort = (SerialPort) commPortIdentifier.open(portId,DELAY_TIME);
// 设置监听器生效 当有数据时通知
serialPort.notifyOnDataAvailable(true);
serialPort.setDTR(true);
serialPort.setRTS(true);
// 比特率、数据位、停止位、奇偶校验位
bitRate = serial.getBitRate();
dataBit = serial.getDataBit();
stopBit = serial.getStopBit();
parity = SerialUtil.getParity(serial.getCheckBit());
serialPort.setSerialPortParams(bitRate, dataBit, stopBit,parity);
} catch (PortInUseException e) {
log.error("Open CommPortIdentifier {} Exception:",serial.getPortId(),e );
return false;
} catch (UnsupportedCommOperationException e) {
log.error("Set SerialPortParams BitRate {} DataBit {} StopBit {} Parity {} Exception:",bitRate,dataBit,stopBit,parity,e);
return false;
}
// 设置当前串口的输入输出流
InputStream input;
OutputStream output;
try {
input = serialPort.getInputStream();
output = serialPort.getOutputStream();
} catch (IOException e) {
log.error("Get serialPort data stream exception:",e);
return false;
}
// 给当前串口添加一个监听器
try {
serialPort.addEventListener(new Serial485Listener(input,output,serial.getFormat()));
} catch (TooManyListenersException e) {
log.error("Get serialPort data stream exception:",e);
return false;
}
serialPortMap.put(portId,serialPort);
return true;
}
return false;
}
/**
* 关闭端口
* @param portId
*/
public void closePort(String portId){
SerialPort serialPort = serialPortMap.remove(portId);
if (null != serialPort){
serialPort.close();
}
}
/**
* 发送数据
* @param portId
* @param format
* @param message
*/
public void sendData(String portId,String format,String message){
SerialPort serialPort = serialPortMap.get(portId);
if (null == serialPort){
return;
}
OutputStream output = null;
try {
byte[] bytes = new byte[0];
if (Common.FORMAT_HEX.equals(format)){
bytes = SerialUtil.hexToByte(message);
bytes = Crc16Modbus.merge(bytes);
}
output = serialPort.getOutputStream();
output.write(bytes);
output.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (null != output){
try {
output.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
5.RS485串口监听
package com.serial.demo.config;
import com.serial.demo.socket.SerialWebSocket;
import com.serial.demo.util.Crc16Modbus;
import com.serial.demo.util.SerialUtil;
import gnu.io.SerialPortEvent;
import gnu.io.SerialPortEventListener;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* @author
* @date 2023-07-03 23:21
* @since 1.8
*/
public class Serial485Listener implements SerialPortEventListener {
InputStream inputStream;
OutputStream outputStream;
String format;
public Serial485Listener(InputStream input, OutputStream output, String format){
inputStream = input;
outputStream = output;
this.format = format;
}
@Override
public void serialEvent(SerialPortEvent event) {
switch (event.getEventType()) {
case SerialPortEvent.BI:
case SerialPortEvent.OE:
case SerialPortEvent.FE:
case SerialPortEvent.PE:
case SerialPortEvent.CD:
case SerialPortEvent.CTS:
case SerialPortEvent.DSR:
case SerialPortEvent.RI:
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:
break;
case SerialPortEvent.DATA_AVAILABLE:
// 当有可用数据时读取数据
byte[] readBuffer = null;
int availableBytes = 0;
try {
availableBytes = inputStream.available();
while (availableBytes > 0) {
readBuffer = SerialUtil.readFromPort(inputStream);
String needData = printHexString(readBuffer);
SerialWebSocket.broadcast(needData);
availableBytes = inputStream.available();
}
} catch (IOException e) {
}
default:
break;
}
}
/**
* 转为 16 进制字符串
* @param b
* @return
*/
public static String printHexString(byte[] b) {
return Crc16Modbus.byteTo16String(b);
}
}
三.UI代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Serial Communication</title>
<meta name="robots" content="all" />
<meta name="keywords" content="Serial Communication" />
<meta name="description" content="Serial Communication" />
<style>
.btn-group{
display: inline-block;
}
.left {
width: 300px;
height: 500px;
float: left;
}
.right {
width: calc(100% - 330px);
height: 500px;
margin-left: 300px;
}
.configSelect{
width: 100%;
}
.bk{
border-color: #D5DBDB;
background-color: #1E1E1E;
margin: 1px;
}
.bkw{
border-color: #D5DBDB;
background-color: #1E1E1E;
margin: 1px;
border-left: none;
border-right: none;
border-bottom: none;
border-top: none;
}
select{
background-color: #3C3C3C;
color: white;
}
button{
background-color: #3C3C3C;
color: white;
cursor: pointer;
}
</style>
</head>
<body style="background-color: #1E1E1E;color: white;">
<div>
<div class="left">
<fieldset class="bk">
<legend>串口配置</legend>
<div style="background: #1E1E1E;width: 260px;">
<table>
<tr><th style="width:100px">端口</th><td style="width:160px"><select class="configSelect" id="serialPortList"></select></td></tr>
<tr><th>波特率</th><td><select class="configSelect" id="bitRate"></select></td></tr>
<tr><th>数据位</th><td><select class="configSelect" id="dataBit"></select></td></tr>
<tr><th>停止位</th><td><select class="configSelect" id="stopBit"></select></td></tr>
<tr><th>校验位</th><td><select class="configSelect" id="checkBit"></select></td></tr>
<tr><th>操作</th><td><button type="button" class="btn btn-default" style="width:100%" id="switchSerialPort" >打开串口</button></td></tr>
</table>
</div>
</fieldset>
<fieldset class="bk">
<legend>RS485</legend>
<div>
<table>
<tr><th style="width:100px">RS485</th><td style="width:160px"><input type="checkbox" id="isRS485"/></td></tr>
<!-- <tr><th>DTR</th><td><input type="checkbox" id="isDTR"/></td></tr>
<tr><th>RTS</th><td><input type="checkbox" id="isRTS"/></td></tr> -->
</table>
</div>
</fieldset>
<fieldset class="bk">
<legend>接收区设置</legend>
<div>
<table>
<tr><th style="width:100px">数据格式</th><td style="width:160px"><select class="configSelect" id="receiveDataType"></select></td></tr>
<tr><th>停止显示</th><td><input type="checkbox" id="stopShow"/></td></tr>
<tr><th></th><td><button type="button" class="btn btn-default" style="width:100%" id="clearReceiveData" >清空接收区</button></td></tr>
<tr><th></th><td><button type="button" class="btn btn-default" style="width:100%" id="saveToFile" >保存到文件</button></td></tr>
</table>
</div>
</fieldset>
</div>
<div class="right">
<fieldset class="bkw">
<legend>WebSocket</legend>
<input type='text' value='ws://localhost:8080/websocket/test-0' class="form-control" style='width:390px;display:inline' id='wsaddr' />
<div class="btn-group" >
<button type="button" class="btn btn-default" onclick='addsocket();'>连接</button>
<button type="button" class="btn btn-default" onclick='closesocket();'>断开</button>
<button type="button" class="btn btn-default" onclick='$("#wsaddr").val("")' style="display:none">清空</button>
</div>
<div class="row">
<div id="output" style="border:1px solid #ccc;height:390px;overflow: auto;margin: 20px 0;background: #4B4B4B;"></div>
<div style="display:none">
<input type="text" id='message' class="form-control" style='width:810px' placeholder="待发信息" onkeydown="en(event);">
<span class="input-group-btn">
<button class="btn btn-default" type="button" onclick="doSend();">发送</button>
</span>
</div>
</div>
</fieldset>
</div>
</div>
<div>
<div class="left" style="height: 160px;">
<fieldset class="bk">
<legend>发送区设置</legend>
<table>
<tr><th style="width:100px">自动发送</th><td style="width:160px"><input id="autoSendTimer" value="1000"/></td></tr>
<tr><th>数据格式</th><td><select class="configSelect" id="dataType"></select></td></tr>
<tr><th>类型</th><td><select class="configSelect" id="sendType"></select></td></tr>
<tr><th>发送</th><td><button type="button" class="btn btn-default" style="width:100%" id="sendData" >发送数据</button></td></tr>
</table>
</fieldset>
</div>
<div class="right" style="height: 160px;">
<fieldset class="bkw">
<legend></legend>
<textarea id="sendMessages" style="width:100%;height:133px;margin-top: 5px;background-color: #4B4B4B;color: white;"></textarea>
</fieldset>
</div>
</div>
<div>
<div class="left" style="height: 90px;">
<fieldset class="bk">
<legend>类型转换</legend>
<table>
<tr><th style="width:90px"></th><td style="width:160px"><button type="button" class="btn btn-default" style="width:100%" id="toHex" >字符串转HEX</button></td></tr>
<tr><th></th><td><button type="button" class="btn btn-default" style="width:100%" id="toStr" >HEX转字符串</button></td></tr>
</tr>
</table>
</fieldset>
</div>
<div class="right" style="height: 90px;">
<table style="width:100%">
<tr style="width:100%">
<th style="width:50%">
<fieldset class="bkw">
<legend>STR</legend>
<textarea id="strShow" style="width:100%;height:54px;margin-top: 5px;background-color: #4B4B4B;color: white;"></textarea>
</fieldset>
</th>
<th style="width:50%">
<fieldset class="bkw">
<legend>HEX</legend>
<textarea id="hexShow" style="width:100%;height:54px;margin-top: 5px;background-color: #4B4B4B;color: white;"></textarea>
</fieldset>
</th>
</tr>
</table>
</div>
</div>
</body>
<script src="https://code.jquery.com/jquery-3.1.1.min.js"></script>
<script language="javascript" type="text/javascript">
function formatDate(now) {
var year = now.getFullYear();
var month = now.getMonth() + 1;
var date = now.getDate();
var hour = now.getHours();
var minute = now.getMinutes();
var second = now.getSeconds();
return year + "-" + (month = month < 10 ? ("0" + month) : month) + "-" + (date = date < 10 ? ("0" + date) : date) +
" " + (hour = hour < 10 ? ("0" + hour) : hour) + ":" + (minute = minute < 10 ? ("0" + minute) : minute) + ":" + (
second = second < 10 ? ("0" + second) : second);
}
var output;
var websocket;
function addsocket() {
$("#output").text("");
var wsaddr = $("#wsaddr").val();
if (wsaddr == '') {
alert("set websocket address!");
return false;
}
StartWebSocket(wsaddr);
}
function closesocket() {
websocket.close();
}
function StartWebSocket(wsUri) {
websocket = new WebSocket(wsUri);
websocket.onopen = function(evt) {
onOpen(evt)
};
websocket.onclose = function(evt) {
onClose(evt)
};
websocket.onmessage = function(evt) {
onMessage(evt)
};
websocket.onerror = function(evt) {
onError(evt)
};
}
function onOpen(evt) {
// writeToScreen("<span style='color:red'>连接成功,现在你可以发送信息啦!!!</span>");
}
function onClose(evt) {
// writeToScreen("<span style='color:red'>websocket连接已断开!!!</span>");
websocket.close();
}
function onMessage(evt) {
var stopShow = $("#stopShow").prop('checked');
if (!stopShow) {
writeToScreen('<span style="color:blue">' + formatDate(new Date()) + ' : </span><span class="bubble">' + evt.data + '</span>');
}
}
function onError(evt) {
writeToScreen('<span style="color: red;">error:</span> ' + evt.data);
}
function doSend() {
var message = $("#message").val();
if (message == '') {
alert("Please input message");
$("#message").focus();
return false;
}
if (typeof websocket === "undefined") {
alert("websocket is not connected");
return false;
}
if (websocket.readyState == 3) {
alert("websocket is closed,please reconnected");
return false;
}
console.log(websocket);
$("#message").val('');
writeToScreen('<span style="color:green">你发送的信息 ' + formatDate(new Date()) + '</span><br/>' + message);
websocket.send(message);
}
function writeToScreen(message) {
var div = "<div class='newmessage'>" + message + "</div>";
var d = $("#output");
var d = d[0];
var doScroll = d.scrollTop == d.scrollHeight - d.clientHeight;
$("#output").append(div);
if (doScroll) {
d.scrollTop = d.scrollHeight - d.clientHeight;
}
}
function en(event) {
var evt = evt ? evt : (window.event ? window.event : null);
if (evt.keyCode == 13) {
doSend()
}
}
var http = "http://localhost:8080/serial";
var httpSerial = "http://localhost:8080/serial/232";
function getSerialPortList(){
$("#serialPortList").html('');
$.ajax({url:http + "/getSerialPortList",success:function(result){
for (var i in result) {
$("#serialPortList").append("<option value='" + result[i] + "''>" + result[i] + "</option>");
}
}});
}
function getBitRate() {
var list = [2400,4800,9600,19200,38400,57600,115200,128000,230400,256000,460800]
for (var i in list) {
$("#bitRate").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
var list = [5,6,7,8]
for (var i in list) {
$("#dataBit").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
var list = [1,2]
for (var i in list) {
$("#stopBit").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
var list = ['无','奇','偶']
for (var i in list) {
$("#checkBit").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
var list = ['ASCII','HEX']
for (var i in list) {
$("#dataType").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
$("#receiveDataType").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
var list = ['发送新行','自动发送']
for (var i in list) {
$("#sendType").append("<option value='" + list[i] + "''>" + list[i] + "</option>");
}
}
function updateConfig(type){
if (type == 1) {
$("#serialPortList").attr("disabled","disabled")
$("#bitRate").attr("disabled","disabled")
$("#dataBit").attr("disabled","disabled")
$("#stopBit").attr("disabled","disabled")
$("#checkBit").attr("disabled","disabled")
$("#receiveDataType").attr("disabled","disabled")
$("#isRS485").attr("disabled","disabled")
} else {
$("#serialPortList").removeAttr("disabled")
$("#bitRate").removeAttr("disabled")
$("#dataBit").removeAttr("disabled")
$("#stopBit").removeAttr("disabled")
$("#checkBit").removeAttr("disabled")
var isRS485 = $("#isRS485").prop('checked');
if (!isRS485) {
$("#receiveDataType").removeAttr("disabled")
}
$("#isRS485").removeAttr("disabled")
}
}
function init() {
addsocket();
getSerialPortList();
getBitRate();
$("#bitRate").val(9600)
$("#dataBit").val(8)
}
init();
$("#switchSerialPort").click(function(){
var status = $("#switchSerialPort").html();
if (status == '关闭串口') {
$.ajax({
url:httpSerial + "/close/" + $("#serialPortList").val(),
success:function(result){
$("#switchSerialPort").html('打开串口')
$("#switchSerialPort").css("background-color","");
updateConfig(0)
},
error:function(result){
}
});
} else {
var portId = $("#serialPortList").val();
var bitRate = $("#bitRate").val();
var dataBit = $("#dataBit").val();
var stopBit = $("#stopBit").val();
var checkBit = $("#checkBit").val();
var format = $("#receiveDataType").val();
var serialData = {"portId": portId, "bitRate": bitRate, "dataBit": dataBit, "stopBit": stopBit, "checkBit": checkBit,"format":format};
// console.log(serialData)
$.ajax({
url:httpSerial + "/open" ,
type: "post",
contentType : "application/json",
dataType: "json",
data: JSON.stringify(serialData),
success:function(result){
if (result) {
$("#switchSerialPort").html('关闭串口')
$("#switchSerialPort").css("background-color","#02A1DD");
updateConfig(1)
}
},
error:function(result){
}
});
}
});
$("#sendData").click(function(){
var portId = $("#serialPortList").val();
var data = $("#sendMessages").val();
var dataType = $("#dataType").val();
if (data == '') {
alert('Please input message .')
$("#sendMessages").focus();
return;
}
$.ajax({
url:httpSerial + "/send/" + portId + "/" + dataType + "/" + data,
success:function(result){
var sendType = $("#sendType").val();
if (sendType == '发送新行') {
$("#sendMessages").val('');
}
},
error:function(result){
}
});
});
var timer
$("#sendType").change(()=>{
var data = $("#sendMessages").val();
if (data == '') {
clearInterval(timer)
$("#sendType").val("发送新行");
$("#sendType").attr("disabled","disabled")
}
var sendType = $("#sendType").val();
if (sendType == '自动发送') {
$("#sendData").attr("disabled","disabled")
$("#sendData").css("background-color","#02A1DD");
var interval = $("#autoSendTimer").val()
timer = setInterval(function(){
$("#sendData").trigger("click")
},interval);
} else {
clearInterval(timer)
$("#sendData").removeAttr("disabled")
$("#sendData").css("background-color","");
}
})
$("#sendType").attr("disabled","disabled")
$("#sendMessages").keyup(()=>{
var data = $("#sendMessages").val();
if (data == '') {
clearInterval(timer)
$("#sendType").val("发送新行");
$("#sendType").attr("disabled","disabled")
} else {
$("#sendType").removeAttr("disabled")
}
})
$("#clearReceiveData").click(function(){
$("#output").text("");
});
$("#saveToFile").click(function(){
let a = document.createElement('a')
let url = window.URL.createObjectURL(
new Blob([$("#output").text()], {
type: ''
})
)
a.href = url
a.download = 'ReveiveData' + Date.parse(new Date()) + '.log'
a.click()
window.URL.revokeObjectURL(url)
});
$("#toHex").click(function(){
var str = $("#strShow").val();
$.ajax({
url:http + "/toHex?str=" + str,
success:function(result){
$("#hexShow").val(result)
},
error:function(result){
}
});
})
$("#toStr").click(function(){
var hex = $("#hexShow").val();
$.ajax({
url:http + "/toStr?hex=" + hex,
success:function(result){
$("#strShow").val(result)
},
error:function(result){
}
});
})
$("#isRS485").click(function(){
var isRS485 = $("#isRS485").prop('checked');
if (isRS485) {
$("#dataType").val("HEX");
$("#receiveDataType").val("HEX");
$("#dataType").attr("disabled","disabled")
$("#receiveDataType").attr("disabled","disabled")
httpSerial = "http://localhost:8080/serial/485";
} else {
$("#dataType").removeAttr("disabled")
$("#receiveDataType").removeAttr("disabled")
httpSerial = "http://localhost:8080/serial/232";
}
})
</script>
</html>
四.测试效果
1.串口通信
ASCII 收数
ASCII发数
切换为自动发送后即自动发送当前数据
Hex 收数
Hex 发数
2.CRC16通信
Hex 收数
Hex 发数