SpringBoot中集成串口通信的项目实践

 更新时间:2023年08月02日 11:35:04   作者:空山返景  
本文主要介绍了SpringBoot中集成串口通信,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

串口通信介绍

  • 串口通信是一种按位发送和接收字节的简单概念,尽管比并行通信慢,但串口可以同时使用一根线发送数据和接收数据。
  • 串口通信简单且能够实现远距离通信,例如,串口的长度可达1200米,而并行通信的长度限制为20米.
  • 串口通常用于ASCII码字符的传输,通信使用地线、发送线和接收线三根线完成。
  • 重要的参数有波特率、数据位、停止位和奇偶校验。

波特率

这是一个衡量符号传输速率的参数。指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。一般调制速率大于波特率,比如通常电话线的波特率为14400,28800和36600。波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是GPIB设备的通信

数据位

这是衡量通信中实际数据位的参数。当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。如何设置取决于你想传送的信息。比如,标准的ASCII码是0~127(7位)。扩展的ASCII码是0~255(8位)。如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。

停止位

用于表示单个包的最后一位。典型的值为1,1.5和2位。由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。

奇偶校验位

在串口通信中一种简单的检错方式。有四种检错方式:偶、奇、高和低。当然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。

开始集成

组件介绍

对于Java集成串口通信,常见的选择有 原生Java串口通信API、RXTX库、jSerialComm库,

  • 原生Java串口通信API只支持到Java6版本,后续便不再维护,所以不推荐使用
  • RXTX库是过去主流开发串口通信使用的依赖组件,但是由于需要在jvm包中添加指定的依赖组件,其次,RXTX的稳定性和兼容性可能存在一些问题,且仅维护至Jdk8版本,后续不再持续维护了,所以本次也不考虑使用它
  • 所以本次采用的是jSerialComm库,以下是jSerialComm库的一些主要特点和功能:
    • 跨平台支持:jSerialComm可以在多个操作系统上使用,包括Windows、Linux和MacOS等。
    • 多串口支持:它可以同时管理多个串口,通过获取和管理已连接的串口列表,方便选择和使用特定的串口。
    • 简单的API:jSerialComm提供了简洁易用的API,使串口的打开、读取、写入和关闭等操作变得简单和直观。
    • 支持异步读取:可以使用回调函数或监听器来异步读取串口数据,实现非阻塞的读取操作。
    • 高性能:jSerialComm使用了底层的串口通信库,具有高效的读写性能,适用于处理大量的串口数据。
    • 可靠性和稳定性:它经过了充分测试和优化,具有良好的稳定性和可靠性,能够处理各种串口通信场景。
    • 开源免费:jSerialComm是一个开源库,使用MIT许可证,可以免费使用和修改。

Maven依赖导入

<!--    COM串口通信    -->
        <dependency>
            <groupId>com.fazecast</groupId>
            <artifactId>jSerialComm</artifactId>
            <version>2.6.2</version>
        </dependency>
        <!--    hutool工具    -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.6.5</version>
        </dependency>

配置类

创建一个 SerialConfig 用于定义串口通用信息配置

import com.fazecast.jSerialComm.SerialPort;
import lombok.Data;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
 *  用于定义串口通用信息配置
 * */
@Configuration
public class SerialConfig {
    /**
     *  波特率
     * */
    public static int baudRate = 19200;
    /**
     * 数据位
     */
    public static int dataBits = 8;
    /**
     * 停止位 ( 1停止位 = 1  、 1.5停止位 = 2 、2停止位 = 3)
     * */
    public static int stopBits = 1;
    /**
     * 校验模式 ( 无校验 = 0  、奇校验 = 1 、偶校验 = 2、 标记校验 = 3、 空格校验 = 4  )
     * */
    public static int parity = 1;
    /**
     *  是否为 Rs485通信
     * */
    public static boolean rs485Mode = true;
    /**
     *  串口读写超时时间(毫秒)
     * */
    public static int timeOut = 300;
    /**
     * 消息模式
     * 非阻塞模式: #TIMEOUT_NONBLOCKING           【在该模式下,readBytes(byte[], long)和writeBytes(byte[], long)调用将立即返回任何可用数据。】
     * 写阻塞模式: #TIMEOUT_WRITE_BLOCKING        【在该模式下,writeBytes(byte[], long)调用将阻塞,直到所有数据字节都成功写入输出串口设备。】
     * 半阻塞读取模式: #TIMEOUT_READ_SEMI_BLOCKING 【在该模式下,readBytes(byte[], long)调用将阻塞,直到达到指定的超时时间或者至少可读取1个字节的数据。】
     * 全阻塞读取模式:#TIMEOUT_READ_BLOCKING       【在该模式下,readBytes(byte[], long)调用将阻塞,直到达到指定的超时时间或者可以返回请求的字节数。】
     * 扫描器模式:#TIMEOUT_SCANNER                【该模式适用于使用Java的java.util.Scanner类从串口进行读取,会忽略手动指定的超时值以确保与Java规范的兼容性】
     * */
    public static int messageModel = SerialPort.TIMEOUT_READ_BLOCKING;
    /**
     *  已打开的COM串口 (重复打开串口会导致后面打开的无法使用,所以打开一次就要记录到公共变量存储)
     * */
    public final static Map<String, SerialPort> portMap = new HashMap<>();
}

串口工具类

准备一个SerialService 用于创建串口,关闭串口,收发消息

import cn.hutool.core.codec.BCD;  
import com.fazecast.jSerialComm.SerialPort;  
import com.tce.station.common.config.SerialConfig;  
import lombok.AllArgsConstructor;  
import lombok.extern.slf4j.Slf4j;  
import org.springframework.stereotype.Service;  
import java.io.InputStream;  
import java.util.Arrays;  
import java.util.HashMap;  
import java.util.List;  
import java.util.Map;  
/**  
* 串口服务类  
* */  
@AllArgsConstructor  
@Slf4j  
@Service  
public class SerialService {  
/**  
* 获取串口及状态  
* */  
public Map<String, Boolean> getPortStatus(){  
        Map<String, Boolean> comStatusMap = new HashMap<>();  
        List<SerialPort> commPorts = Arrays.asList(SerialPort.getCommPorts());  
        commPorts.forEach(port->{  
        comStatusMap.put(port.getSystemPortName(), port.isOpen());  
    });  
    return comStatusMap;  
}  
/**  
* 添加串口连接  
* */  
public void connectSerialPort(String portName){  
    SerialPort commPort = SerialPort.getCommPort(portName);  
    if (commPort.isOpen()){  
        throw new RuntimeException("该串口已被占用");  
    }  
    if (SerialConfig.portMap.containsKey(portName)){  
        throw new RuntimeException("该串口已被占用");  
    }  
    // 打开端口  
    commPort.openPort();  
    if (!commPort.isOpen()){  
        throw new RuntimeException("打开串口失败");  
    }  
    // 设置串口参数 (波特率、数据位、停止位、校验模式、是否为Rs485)  
    commPort.setComPortParameters(SerialConfig.baudRate, SerialConfig.dataBits,SerialConfig.stopBits, SerialConfig.stopBits, SerialConfig.rs485Mode);  
    // 设置串口超时和模式  
    commPort.setComPortTimeouts(SerialConfig.messageModel ,SerialConfig.timeOut, SerialConfig.timeOut);  
    // 添加至串口记录Map  
    SerialConfig.portMap.put(portName, commPort);  
}  
/**  
* 关闭串口连接  
* */  
public boolean closeSerialPort(String portName){  
    if (!SerialConfig.portMap.containsKey(portName)){  
        throw new RuntimeException("该串口未启用");  
    }  
    // 获取串口  
    SerialPort port = SerialConfig.portMap.get(portName);  
    // 关闭串口  
    port.closePort();  
    // 需要等待一些时间,否则串口关闭不完全,会导致无法打开  
    try {  
        Thread.sleep(3000);  
    } catch (InterruptedException e) {  
        throw new RuntimeException(e);  
    }  
    if (port.isOpen()){  
        return false;  
    }else {  
        // 关闭成功返回  
        return true;  
    }  
}  
/**  
* 串口发送数据  
* */  
public void sendComData(String portName, byte[]sendBytes){  
    if (!SerialConfig.portMap.containsKey(portName)){  
        throw new RuntimeException("该串口未启用");  
    }  
    // 获取串口  
    SerialPort port = SerialConfig.portMap.get(portName);  
    // 发送串口数据  
    int i = port.writeBytes(sendBytes, sendBytes.length);  
    if (i == -1){  
        log.error("发送串口数据失败{}, 数据内容{}",portName, BCD.bcdToStr(sendBytes));  
        throw new RuntimeException("发送串口数据失败");  
    }  
}  
/**  
* 串口读取数据  
* */  
public byte[] readComData(String portName){  
    if (!SerialConfig.portMap.containsKey(portName)){  
        throw new RuntimeException("该串口未启用");  
    }  
    // 获取串口  
    SerialPort port = SerialConfig.portMap.get(portName);   
    // 读取串口流  
    InputStream inputStream = port.getInputStream();  
    // 获取串口返回的流大小  
    int availableBytes = 0;  
    try {  
        availableBytes = inputStream.available();  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    // 读取指定的范围的数据流  
    byte[] readByte = new byte[availableBytes];  
    int bytesRead = 0;  
    try {  
        bytesRead = inputStream.read(readByte);  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
    return readByte;  
}  
}

串口业务类使用

基于以上的工具类就已经可以对串口通信进行开发了,以下是使用案例

1.创建串口连接

可以使用监听器方式接收数据,但是需要进行绑定,后续会介绍

// 从数据库或者配置表中读取设定要打开的串口
List<String> comList = comService.list();
// 关闭之前的监听连接(提取所有串口避免重复关闭)  
SerialConfig.portMap.forEach((com,serialPort) ->{  
    serialService.closeSerialPort(com);  
});
// 等待之前的串口发送和 2倍监听超时,避免还有串口通信线程未关闭  
try {  
    Thread.sleep((SerialConfig.timeOut + 1) * 2);  
} catch (InterruptedException e) {  
    throw new RuntimeException(e);  
}
// 清空COM口记录  
SerialConfig.portMap.clear();
// 重新连接串口  
gunList.forEach(gun->{  
    // 如果COM口没有就打开  
    if (!SerialConfig.portMap.containsKey(gun.getCom())){  
        // 创建连接  
        SerialPort serialPort = serialService.connectSerialPort(gun.getCom());  
        // 绑定监听器  
        // serialPort.addDataListener(new MessageListener());  
    }   
});

2.关闭串口连接

String com = "COM1";
serialService.closeSerialPort(com); 

3.定时发送串口数据

/**  
* 周期性向串口发送数据  
* */  
@Scheduled(fixedRate = 1500L)  
public void send{  
    // 因为是阻塞是监听线程,所以使用线程处理  
    Thread thread = new Thread(() -> {  
        try {  
            SerialConfig.portMap.forEach((com,serialPort)->{  
                // 等待0.1秒  
                try {  
                    Thread.sleep(100);  
                }catch (Exception e){  
                    e.printStackTrace();  
                }  
                // 调用业务逻辑获取需要推送的数据
                byte[] sendBytes = getPushData(com);
                // 发送串口数据  
                serialService.sendComData(com, sendBytes);  
                log.info("向串口发送 {}",gun.getGunNum(), com, BCD.bcdToStr(sendBytes));  
            });  
        }catch (ConcurrentModificationException e){  
            log.info("COM口配置发生变化,等待配置生效");  
        }  
    });
    // 开启发送线程  
    thread.start();  
}

4.周期性读取串口数据

/**  
* 周期性读取串口数据  
* */  
@Scheduled(fixedRate = 1000L)  
public void readComData() {  
    // 遍历监听  
    SerialConfig.portMap.forEach((com,serialPort)->{  
        // 因为是阻塞是监听线程,所以使用线程处理,否则某个读取失败,会阻塞整个程序  
        Thread thread = new Thread(() -> {  
            byte[] readByte = serialService.readComData(com);  
            // 有数据才执行
            if (readByte.length > 1) {  
            try {  
                log.info("收到串口数据: {}", BCD.bcdToStr(readByte));  
                // 调用串口响应业务操作  
                comOperationByData(comResult,BCD.strToBcd(res), com);  
            }catch (Exception e){  
                e.printStackTrace();  
            }  
        });  
        // 开启线程
        thread.start();  
    }

5.监听式读取串口数据

监听式读取数据使用的是非阻塞行读取数据,有数据就会触发
创建一个监听器

@Slf4j  
public class MessageListener implements SerialPortDataListener {  
@Autowired  
ICommandService commandService;  
/**  
* 监听事件设置  
* */  
@Override  
public int getListeningEvents() {  
    // 持续返回数据流模式  
    return SerialPort.LISTENING_EVENT_DATA_AVAILABLE;  
    // 收到数据立即返回  
    // return SerialPort.LISTENING_EVENT_DATA_RECEIVED;  
}  
/**  
* 收到数据监听回调  
* */  
@Override  
public void serialEvent(SerialPortEvent event) {  
    // 因为是阻塞是监听线程,所以使用线程处理  
    Thread thread = new Thread(() -> {  
        // 读取串口流  
        InputStream inputStream = event.getSerialPort().getInputStream();  
        // 获取串口返回的流大小  
        int availableBytes = 0;  
        try {  
            availableBytes = inputStream.available();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        // 读取指定的范围的数据流  
        byte[] readByte = new byte[availableBytes];  
        int bytesRead = 0;  
        try {  
            bytesRead = inputStream.read(readByte);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        try {  
            inputStream.close();  
        } catch (IOException e) {  
            throw new RuntimeException("关闭串口流失败"+e.getMessage());  
        }  
        // 有数据才执行
        if (readByte.length > 1) {  
            try {  
                log.info("收到串口数据: {}", BCD.bcdToStr(readByte));  
                // 调用串口响应业务操作  
                comOperationByData(comResult,BCD.strToBcd(res), com);  
            }catch (Exception e){  
                e.printStackTrace();  
            } 
        } 
    });  
// 开启线程
thread.start();  
}  
}

给串口连接进行绑定监听器

// 创建连接  
SerialPort serialPort = serialService.connectSerialPort(gun.getCom());  
// 绑定监听器  
serialPort.addDataListener(new MessageListener());

需要注意的是监听器接收数据和定时接收数据选取其中一个就好了

到此这篇关于SpringBoot中集成串口通信的项目实践的文章就介绍到这了,更多相关SpringBoot 串口通信内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • nacos如何修改默认的用户名密码

    nacos如何修改默认的用户名密码

    这篇文章主要介绍了nacos如何修改默认的用户名密码问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Spring中ApplicationListener的使用解析

    Spring中ApplicationListener的使用解析

    这篇文章主要介绍了Spring中ApplicationListener的使用解析,ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,需要的朋友可以参考下
    2023-12-12
  • Java日期处理工具类DateUtils详解

    Java日期处理工具类DateUtils详解

    这篇文章主要为大家详细介绍了Java日期处理工具类DateUtils的相关代码,包含日期和时间常用操作,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-12-12
  • Springboot中的Validation参数校验详解

    Springboot中的Validation参数校验详解

    这篇文章主要介绍了Springboot中的Validation参数校验详解,Springboot参数校验是一种常用的验证机制,在传递参数时进行校验,以确保参数的有效性和正确性,该机制可以帮助开发者在代码实现前就避免一些常见的错误,需要的朋友可以参考下
    2023-10-10
  • Java版画板的实现方法

    Java版画板的实现方法

    这篇文章主要为大家详细介绍了Java版画板的实现方法,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-07-07
  • Java序列化常见实现方法代码实例

    Java序列化常见实现方法代码实例

    这篇文章主要介绍了Java序列化常见实现方法代码实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-11-11
  • Log4j 配置日志打印时区的实现方法

    Log4j 配置日志打印时区的实现方法

    下面小编就为大家分享一篇Log4j 配置日志打印时区的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2017-12-12
  • spring注解 @Valid 的作用说明

    spring注解 @Valid 的作用说明

    这篇文章主要介绍了spring注解 @Valid 的作用说明,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-01-01
  • java中的方法重载知识点总结

    java中的方法重载知识点总结

    在本篇文章里小编给大家整理了关于java中的方法重载知识点总结,有兴趣的朋友们可以跟着学习参考下。
    2020-02-02
  • java设计模式之观察者模式简单解读

    java设计模式之观察者模式简单解读

    这篇文章主要介绍了java设计模式之观察者模式简单解读,观察者模式是在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新,需要的朋友可以参考下
    2023-10-10

最新评论