SpringBoot中集成串口通信的項目實踐
串口通信介紹
- 串口通信是一種按位發(fā)送和接收字節(jié)的簡單概念,盡管比并行通信慢,但串口可以同時使用一根線發(fā)送數據和接收數據。
- 串口通信簡單且能夠實現遠距離通信,例如,串口的長度可達1200米,而并行通信的長度限制為20米.
- 串口通常用于ASCII碼字符的傳輸,通信使用地線、發(fā)送線和接收線三根線完成。
- 重要的參數有波特率、數據位、停止位和奇偶校驗。
波特率
這是一個衡量符號傳輸速率的參數。指的是信號被調制以后在單位時間內的變化,即單位時間內載波參數變化的次數,如每秒鐘傳送240個字符,而每個字符格式包含10位(1個起始位,1個停止位,8個數據位),這時的波特率為240Bd,比特率為10位*240個/秒=2400bps。一般調制速率大于波特率,比如通常電話線的波特率為14400,28800和36600。波特率可以遠遠大于這些值,但是波特率和距離成反比。高波特率常常用于放置的很近的儀器間的通信,典型的例子就是GPIB設備的通信
數據位
這是衡量通信中實際數據位的參數。當計算機發(fā)送一個信息包,實際的數據往往不會是8位的,標準的值是6、7和8位。如何設置取決于你想傳送的信息。比如,標準的ASCII碼是0~127(7位)。擴展的ASCII碼是0~255(8位)。如果數據使用簡單的文本(標準 ASCII碼),那么每個數據包使用7位數據。每個包是指一個字節(jié),包括開始/停止位,數據位和奇偶校驗位。由于實際數據位取決于通信協(xié)議的選取,術語“包”指任何通信的情況。
停止位
用于表示單個包的最后一位。典型的值為1,1.5和2位。由于數據是在傳輸線上定時的,并且每一個設備有其自己的時鐘,很可能在通信中兩臺設備間出現了小小的不同步。因此停止位不僅僅是表示傳輸的結束,并且提供計算機校正時鐘同步的機會。適用于停止位的位數越多,不同時鐘同步的容忍程度越大,但是數據傳輸率同時也越慢。
奇偶校驗位
在串口通信中一種簡單的檢錯方式。有四種檢錯方式:偶、奇、高和低。當然沒有校驗位也是可以的。對于偶和奇校驗的情況,串口會設置校驗位(數據位后面的一位),用一個值確保傳輸的數據有偶個或者奇?zhèn)€邏輯高位。例如,如果數據是011,那么對于偶校驗,校驗位為0,保證邏輯高的位數是偶數個。如果是奇校驗,校驗位為1,這樣就有3個邏輯高位。高位和低位不真正的檢查數據,簡單置位邏輯高或者邏輯低校驗。這樣使得接收設備能夠知道一個位的狀態(tài),有機會判斷是否有噪聲干擾了通信或者是否傳輸和接收數據是否不同步。
開始集成
組件介紹
對于Java集成串口通信,常見的選擇有 原生Java串口通信API、RXTX庫、jSerialComm庫,
- 原生Java串口通信API只支持到Java6版本,后續(xù)便不再維護,所以不推薦使用
- RXTX庫是過去主流開發(fā)串口通信使用的依賴組件,但是由于需要在jvm包中添加指定的依賴組件,其次,RXTX的穩(wěn)定性和兼容性可能存在一些問題,且僅維護至Jdk8版本,后續(xù)不再持續(xù)維護了,所以本次也不考慮使用它
- 所以本次采用的是jSerialComm庫,以下是jSerialComm庫的一些主要特點和功能:
- 跨平臺支持:jSerialComm可以在多個操作系統(tǒng)上使用,包括Windows、Linux和MacOS等。
- 多串口支持:它可以同時管理多個串口,通過獲取和管理已連接的串口列表,方便選擇和使用特定的串口。
- 簡單的API:jSerialComm提供了簡潔易用的API,使串口的打開、讀取、寫入和關閉等操作變得簡單和直觀。
- 支持異步讀?。嚎梢允褂没卣{函數或監(jiān)聽器來異步讀取串口數據,實現非阻塞的讀取操作。
- 高性能:jSerialComm使用了底層的串口通信庫,具有高效的讀寫性能,適用于處理大量的串口數據。
- 可靠性和穩(wěn)定性:它經過了充分測試和優(yōu)化,具有良好的穩(wěn)定性和可靠性,能夠處理各種串口通信場景。
- 開源免費: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>配置類
創(chuàng)建一個 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)調用將阻塞,直到所有數據字節(jié)都成功寫入輸出串口設備?!?
? ? ?* 半阻塞讀取模式: #TIMEOUT_READ_SEMI_BLOCKING 【在該模式下,readBytes(byte[], long)調用將阻塞,直到達到指定的超時時間或者至少可讀取1個字節(jié)的數據。】
? ? ?* 全阻塞讀取模式:#TIMEOUT_READ_BLOCKING ? ? ? 【在該模式下,readBytes(byte[], long)調用將阻塞,直到達到指定的超時時間或者可以返回請求的字節(jié)數。】
? ? ?* 掃描器模式:#TIMEOUT_SCANNER ? ? ? ? ? ? ? ?【該模式適用于使用Java的java.util.Scanner類從串口進行讀取,會忽略手動指定的超時值以確保與Java規(guī)范的兼容性】
? ? ?* */
? ? public static int messageModel = SerialPort.TIMEOUT_READ_BLOCKING;
? ? /**
? ? ?* ?已打開的COM串口 (重復打開串口會導致后面打開的無法使用,所以打開一次就要記錄到公共變量存儲)
? ? ?* */
? ? public final static Map<String, SerialPort> portMap = new HashMap<>();
}串口工具類
準備一個SerialService 用于創(chuàng)建串口,關閉串口,收發(fā)消息
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 {
/**
* 獲取串口及狀態(tài)
* */
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;
}
}
/**
* 串口發(fā)送數據
* */
public void sendComData(String portName, byte[]sendBytes){
if (!SerialConfig.portMap.containsKey(portName)){
throw new RuntimeException("該串口未啟用");
}
// 獲取串口
SerialPort port = SerialConfig.portMap.get(portName);
// 發(fā)送串口數據
int i = port.writeBytes(sendBytes, sendBytes.length);
if (i == -1){
log.error("發(fā)送串口數據失敗{}, 數據內容{}",portName, BCD.bcdToStr(sendBytes));
throw new RuntimeException("發(fā)送串口數據失敗");
}
}
/**
* 串口讀取數據
* */
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;
}
}串口業(yè)務類使用
基于以上的工具類就已經可以對串口通信進行開發(fā)了,以下是使用案例
1.創(chuàng)建串口連接
可以使用監(jiān)聽器方式接收數據,但是需要進行綁定,后續(xù)會介紹
// 從數據庫或者配置表中讀取設定要打開的串口
List<String> comList = comService.list();
// 關閉之前的監(jiān)聽連接(提取所有串口避免重復關閉) ?
SerialConfig.portMap.forEach((com,serialPort) ->{ ?
?? ?serialService.closeSerialPort(com); ?
});
// 等待之前的串口發(fā)送和 2倍監(jiān)聽超時,避免還有串口通信線程未關閉 ?
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())){ ?
?? ??? ?// 創(chuàng)建連接 ?
?? ??? ?SerialPort serialPort = serialService.connectSerialPort(gun.getCom()); ?
?? ??? ?// 綁定監(jiān)聽器 ?
?? ??? ?// serialPort.addDataListener(new MessageListener()); ?
?? ?} ??
});2.關閉串口連接
String com = "COM1"; serialService.closeSerialPort(com);
3.定時發(fā)送串口數據
/**
* 周期性向串口發(fā)送數據
* */
@Scheduled(fixedRate = 1500L)
public void send{
// 因為是阻塞是監(jiān)聽線程,所以使用線程處理
Thread thread = new Thread(() -> {
try {
SerialConfig.portMap.forEach((com,serialPort)->{
// 等待0.1秒
try {
Thread.sleep(100);
}catch (Exception e){
e.printStackTrace();
}
// 調用業(yè)務邏輯獲取需要推送的數據
byte[] sendBytes = getPushData(com);
// 發(fā)送串口數據
serialService.sendComData(com, sendBytes);
log.info("向串口發(fā)送 {}",gun.getGunNum(), com, BCD.bcdToStr(sendBytes));
});
}catch (ConcurrentModificationException e){
log.info("COM口配置發(fā)生變化,等待配置生效");
}
});
// 開啟發(fā)送線程
thread.start();
}4.周期性讀取串口數據
/**
* 周期性讀取串口數據
* */
@Scheduled(fixedRate = 1000L)
public void readComData() {
// 遍歷監(jiān)聽
SerialConfig.portMap.forEach((com,serialPort)->{
// 因為是阻塞是監(jiān)聽線程,所以使用線程處理,否則某個讀取失敗,會阻塞整個程序
Thread thread = new Thread(() -> {
byte[] readByte = serialService.readComData(com);
// 有數據才執(zhí)行
if (readByte.length > 1) {
try {
log.info("收到串口數據: {}", BCD.bcdToStr(readByte));
// 調用串口響應業(yè)務操作
comOperationByData(comResult,BCD.strToBcd(res), com);
}catch (Exception e){
e.printStackTrace();
}
});
// 開啟線程
thread.start();
}5.監(jiān)聽式讀取串口數據
監(jiān)聽式讀取數據使用的是非阻塞行讀取數據,有數據就會觸發(fā)
創(chuàng)建一個監(jiān)聽器
@Slf4j ?
public class MessageListener implements SerialPortDataListener { ?
@Autowired ?
ICommandService commandService; ?
/** ?
* 監(jiān)聽事件設置 ?
* */ ?
@Override ?
public int getListeningEvents() { ?
?? ?// 持續(xù)返回數據流模式 ?
?? ?return SerialPort.LISTENING_EVENT_DATA_AVAILABLE; ?
?? ?// 收到數據立即返回 ?
?? ?// return SerialPort.LISTENING_EVENT_DATA_RECEIVED; ?
} ?
/** ?
* 收到數據監(jiān)聽回調 ?
* */ ?
@Override ?
public void serialEvent(SerialPortEvent event) { ?
?? ?// 因為是阻塞是監(jiān)聽線程,所以使用線程處理 ?
?? ?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()); ?
?? ??? ?} ?
?? ??? ?// 有數據才執(zhí)行
?? ??? ?if (readByte.length > 1) { ?
?? ??? ??? ?try { ?
?? ??? ??? ??? ?log.info("收到串口數據: {}", BCD.bcdToStr(readByte)); ?
?? ??? ??? ??? ?// 調用串口響應業(yè)務操作 ?
?? ??? ??? ??? ?comOperationByData(comResult,BCD.strToBcd(res), com); ?
?? ??? ??? ?}catch (Exception e){ ?
?? ??? ??? ??? ?e.printStackTrace(); ?
?? ??? ??? ?}?
?? ??? ?}?
?? ?}); ?
// 開啟線程
thread.start(); ?
} ?
}給串口連接進行綁定監(jiān)聽器
// 創(chuàng)建連接 SerialPort serialPort = serialService.connectSerialPort(gun.getCom()); // 綁定監(jiān)聽器 serialPort.addDataListener(new MessageListener());
需要注意的是監(jiān)聽器接收數據和定時接收數據選取其中一個就好了
到此這篇關于SpringBoot中集成串口通信的項目實踐的文章就介紹到這了,更多相關SpringBoot 串口通信內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
@RefreshScope在Quartz 觸發(fā)器類導致異常問題解決分析
這篇文章主要為大家介紹了@RefreshScope在Quartz 觸發(fā)器類導致異常問題解決分析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02
springboot配置請求超時時間(Http會話和接口訪問)
本文主要介紹了springboot配置請求超時時間,包含Http會話和接口訪問兩種,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-07-07
Java 8 Stream.distinct() 列表去重的操作
這篇文章主要介紹了Java 8 Stream.distinct() 列表去重的操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-12-12
解決Java字符串JSON轉換異常:cn.hutool.json.JSONException:?Mismatched?
這篇文章主要給大家介紹了關于如何解決Java字符串JSON轉換異常:cn.hutool.json.JSONException:?Mismatched?hr?and?body的相關資料,文中將解決的辦法通過代碼介紹的非常詳細,需要的朋友可以參考下2024-01-01

