Java 串口通信(RS232/485)

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 串口通信(RS232/485)_第1张图片
Java 实现串口通信,同时通过 WebSocket 与 UI 实时交互传递通信数据

准备工作:

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

Java 串口通信(RS232/485)_第2张图片

RS485

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

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

二.串口服务实现

1.Java 串口通信配置

1.扩展包和依赖库

RXTXcomm.jar   放入 {JAVA_HOME}/jre/lib/ext
rxtxserial.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.0modelVersion>

    <groupId>org.examplegroupId>
    <artifactId>SerialPortartifactId>
    <version>1.0-SNAPSHOTversion>

    <properties>
        <maven.compiler.source>8maven.compiler.source>
        <maven.compiler.target>8maven.compiler.target>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    properties>


    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
            <version>2.7.4version>
        dependency>
        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.24version>
        dependency>

        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-websocketartifactId>
            <version>5.3.27version>
        dependency>
        <dependency>
            <groupId>org.rxtxgroupId>
            <artifactId>rxtxartifactId>
            <version>2.1.7version>
        dependency>
    dependencies>

    <repositories>
        <repository>
            <id>nexus-aliyunid>
            <name>nexus-aliyunname>
            <url>http://maven.aliyun.com/nexus/content/groups/public/url>
            <releases>
                <enabled>trueenabled>
            releases>
            <snapshots>
                <enabled>falseenabled>
            snapshots>
        repository>
    repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>publicid>
            <name>aliyun nexusname>
            <url>http://maven.aliyun.com/nexus/content/groups/public/url>
            <releases>
                <enabled>trueenabled>
            releases>
            <snapshots>
                <enabled>falseenabled>
            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 Communicationtitle>
        <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>RS485legend>
                    <div>
                        <table>
                            <tr><th style="width:100px">RS485th><td style="width:160px"><input type="checkbox" id="isRS485"/>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>WebSocketlegend>
                        <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" >字符串转HEXbutton>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>STRlegend>
                                <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>HEXlegend>
                                <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(" + result[i] + ""); } }}); } function getBitRate() { var list = [2400,4800,9600,19200,38400,57600,115200,128000,230400,256000,460800] for (var i in list) { $("#bitRate").append(" + list[i] + ""); } var list = [5,6,7,8] for (var i in list) { $("#dataBit").append(" + list[i] + ""); } var list = [1,2] for (var i in list) { $("#stopBit").append(" + list[i] + ""); } var list = ['无','奇','偶'] for (var i in list) { $("#checkBit").append(" + list[i] + ""); } var list = ['ASCII','HEX'] for (var i in list) { $("#dataType").append(" + list[i] + ""); $("#receiveDataType").append(" + list[i] + ""); } var list = ['发送新行','自动发送'] for (var i in list) { $("#sendType").append(" + 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 = 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 收数

Java 串口通信(RS232/485)_第3张图片

ASCII发数

Java 串口通信(RS232/485)_第4张图片

切换为自动发送后即自动发送当前数据

Java 串口通信(RS232/485)_第5张图片

Hex 收数

Java 串口通信(RS232/485)_第6张图片

Hex 发数

Java 串口通信(RS232/485)_第7张图片

2.CRC16通信

Hex 收数

Java 串口通信(RS232/485)_第8张图片

Hex 发数

Java 串口通信(RS232/485)_第9张图片

你可能感兴趣的:(JavaWeb,服务框架,java,串口通信,rs232/rs485,rxtxserial.dll,RXTXcomm.jar)