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

