Java 串口通信(RS232/485)

  • 一.串口通信页面
  • 二.串口服务实现
    • 1.Java 串口通信配置
      • 1.扩展包和依赖库
      • 2.Pom配置
    • 2.启动类
    • 3.工具包类
      • 1.Common
      • 2.Crc16Modbus
      • 3.SerialUtil
    • 4.WebSocket 配置
      • 1.启动配置
      • 2.监听配置
    • 5.UI交互类
      • 1.串口配置对象
      • 2.串口信息获取接口
      • 3.RS232接口
      • 4.RS485接口
    • 6.串口配置类
      • 1.串口配置
      • 2.RS232串口配置
      • 3.RS232串口监听
      • 4.RS485串口配置
      • 5.RS485串口监听
  • 三.UI代码
  • 四.测试效果
    • 1.串口通信
    • 2.CRC16通信

一.串口通信页面


Java 实现串口通信,同时通过 WebSocket 与 UI 实时交互传递通信数据

准备工作:

虚拟串口工具:Launch Virtual Serial Port Driver
串口调试助手:SSCOM

RS485

根据 Modbus 协议,常规485通讯的信息发送形式如下:地址功能码 数据信息 校验码1byte 1bytenbyte2byte

在线 CRC检验码计算:CRC 测试链接

二.串口服务实现

1.Java 串口通信配置

1.扩展包和依赖库

RXTXcomm.jar 放入 {JAVA_HOME}/jre/lib/extrxtxserial.dll 放入 {JAVA_HOME}/jre/bin

以上两个包可以直接网上下载,注意和JDK版本搭配即可

2.Pom配置

串口通信包:rxtx

<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 */@SpringBootApplicationpublic 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){//先转为数字在转为 bytedata[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 */@Configurationpublic class WebSocketConfig {/** * 开启 websocket 配置 * @return */@Beanpublic 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 */@OnOpenpublic void onOpen(@PathParam("sid") String sid,Session session){this.session = session;this.sid = sid;webSocketMap.put(sid,this);//sendMessage(sid,"Hello:");}/** * 关闭连接 * @param sid */@OnClosepublic 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 */@OnMessagepublic 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 */@Datapublic 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 {@AutowiredSerialPortConfig 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 {@AutowiredRs232Config 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 {@AutowiredRs485Config 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@Componentpublic class SerialPortConfig {/** * 缓存端口信息 */private static Map<String, CommPortIdentifier> serialMap;@PostConstructprivate 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@Componentpublic class Rs232Config {private static final int DELAY_TIME = 1000;@AutowiredSerialPortConfig 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;}@Overridepublic 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@Componentpublic class Rs485Config {private static final int DELAY_TIME = 1000;@AutowiredSerialPortConfig 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;}@Overridepublic 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><!-- DTRRTS --></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("连接成功,现在你可以发送信息啦!!!");} function onClose(evt) {// writeToScreen("websocket连接已断开!!!");websocket.close();} function onMessage(evt) {var stopShow = $("#stopShow").prop('checked');if (!stopShow) {writeToScreen('' + formatDate(new Date()) + ' : ' + evt.data + '');}} function onError(evt) {writeToScreen('error: ' + 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('你发送的信息' + formatDate(new Date()) + '
'
+ message);websocket.send(message);} function writeToScreen(message) {var div = "" + message + "";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] + "");}}});}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] + "");}var list = [5,6,7,8]for (var i in list) {$("#dataBit").append("<option value='" + list[i] + "''>" + list[i] + "");}var list = [1,2]for (var i in list) {$("#stopBit").append("<option value='" + list[i] + "''>" + list[i] + "");}var list = ['无','奇','偶']for (var i in list) {$("#checkBit").append("<option value='" + list[i] + "''>" + list[i] + "");}var list = ['ASCII','HEX']for (var i in list) {$("#dataType").append("<option value='" + list[i] + "''>" + list[i] + "");$("#receiveDataType").append("<option value='" + list[i] + "''>" + list[i] + "");}var list = ['发送新行','自动发送']for (var i in list) {$("#sendType").append("<option value='" + list[i] + "''>" + list[i] + "");}}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 = urla.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 发数