一、串口概述
波特率
1、衡量信号传输速率的参数
2、指的是信号被调制以后在单位时间内的变化,即单位时间内载波参数变化的次数,如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps。一般调制速率大于波特率,比如曼彻斯特编码)
3、通常电话线的波特率为 14400,28800 和 36600,波特率可以远远大于这些值,但是波特率和距离成反比。高波特率常常用于放置的很近的仪器间的通信,典型的例子就是 GPIB 设备的通信。
1、衡量通信中实际数据位的参数
2、当计算机发送一个信息包,实际的数据往往不会是 8 位的,标准的值是 6、7 和 8 位。如何设置取决于你想传送的信息。比如,标准的 ASCII 码是 0~127(7位)。扩展的 ASCII 码是0~255(8位)
3、如果数据使用简单的文本(标准 ASCII码),那么每个数据包使用7位数据。每个包是指一个字节,包括开始/停止位,数据位和奇偶校验位。由于实际数据位取决于通信协议的选取,术语“包”指任何通信的情况。
停止位
1、用于表示单个包的最后一位,典型的值为 1,1.5 和 2 位
2、由于数据是在传输线上定时的,并且每一个设备有其自己的时钟,很可能在通信中两台设备间出现了小小的不同步。因此停止位不仅仅是表示传输的结束,并且提供计算机校正时钟同步的机会。适用于停止位的位数越多,不同时钟同步的容忍程度越大,但是数据传输率同时也越慢。
奇偶校验位
1、串口通信中一种简单的检错方式
2、有四种检错方式:偶、奇、高和低,当然没有校验位也是可以的
3、对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。例如,如果数据是011,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有3个逻辑高位。高位和低位不真正的检查数据,简单置位逻辑高或者逻辑低校验。这样使得接收设备能够知道一个位的状态,有机会判断是否有噪声干扰了通信或者是否传输和接收数据是否不同步。
二、串口调试助手
可以使用如下的串口调试工具:Serial Port Utility(友善串口调试助手)
三、引入依赖
1.maven
<!-- https://mvnrepository.com/artifact/org.bidib.jbidib.org.qbang.rxtx/rxtxcomm -->
<dependency>
<groupId>org.bidib.jbidib.org.qbang.rxtx</groupId>
<artifactId>rxtxcomm</artifactId>
<version>2.2</version>
</dependency>
2.rxtxSerial.dll、rxtxParallel.dll 文件必须放到 <JAVA_HOME>\jre\bin 下,如果是 Windows 系统,则也可以放到 C:\Windows\System32 目录下,否则运行会报错如下:
java.lang.UnsatisfiedLinkError: no rxtxSerial in java.library.path] with root cause
java.lang.UnsatisfiedLinkError: no rxtxSerial in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1867)
at java.lang.Runtime.loadLibrary0(Runtime.java:870)
at java.lang.System.loadLibrary(System.java:1122)
下载地址:http://fizzed.com/oss/rxtx-for-java
根据自己电脑的系统选择下载:
Version | File | Information |
---|---|---|
RXTX-2-2-20081207 | Windows-x64 | Based on CVS snapshot of RXTX taken on 2008-12-07 |
以 Windows-x64 为例,下载解压后如下:
四、代码
package com.centerm.ctidclient.util;
import com.centerm.ctidclient.exception.BusinessException;
import com.centerm.ctidclient.exception.ResourceNotFoundException;
import gnu.io.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import sun.misc.BASE64Encoder;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.*;
/**
* Created by Administrator on 2019/3/18 0018.
* 串口工具类
*/
public class SerialPortUtils {
private static final Logger logger = LoggerFactory.getLogger(SerialPortUtils.class);//slf4j 日志记录器
private static final char[] HEX_CHAR = {'0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
public static SerialPort realSerialPort = null;
public static Long QRCODE_TIME = null;
public static String qrCode = null;
/**
* 查找电脑上所有可用 com 端口
*
* @return 可用端口名称列表,没有时 列表为空
*/
public static final ArrayList<String> findSystemAllComPort() {
/**
* getPortIdentifiers:获得电脑主板当前所有可用串口
*/
Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers();
ArrayList<String> portNameList = new ArrayList<>();
/**
* 将可用串口名添加到 List 列表
*/
while (portList.hasMoreElements()) {
String portName = portList.nextElement().getName();//名称如 COM1、COM2....
portNameList.add(portName);
}
return portNameList;
}
/**
* 打开电脑上指定的串口
*
* @param portName 端口名称,如 COM1,为 null 时,默认使用电脑中能用的端口中的第一个
* @param b 波特率(baudrate),如 9600
* @param d 数据位(datebits),如 SerialPort.DATABITS_8 = 8
* @param s 停止位(stopbits),如 SerialPort.STOPBITS_1 = 1
* @param p 校验位 (parity),如 SerialPort.PARITY_NONE = 0
* @return 打开的串口对象,打开失败时,返回 null
*/
public static final SerialPort openComPort(String portName, int b, int d, int s, int p) {
CommPort commPort = null;
try {
//当没有传入可用的 com 口时,默认使用电脑中可用的 com 口中的第一个
if (StringUtils.isEmpty(portName)) {
List<String> comPortList = findSystemAllComPort();
if (comPortList != null && comPortList.size() > 0) {
portName = comPortList.get(0);
}
}
logger.info("开始打开串口:portName=" + portName + ",baudrate=" + b + ",datebits=" + d + ",stopbits=" + s + ",parity=" + p);
//通过端口名称识别指定 COM 端口
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
/**
* open(String TheOwner, int i):打开端口
* TheOwner 自定义一个端口名称,随便自定义即可
* i:打开的端口的超时时间,单位毫秒,超时则抛出异常:PortInUseException if in use.
* 如果此时串口已经被占用,则抛出异常:gnu.io.PortInUseException: Unknown Application
*/
commPort = portIdentifier.open(portName, 5000);
/**
* 判断端口是不是串口
* public abstract class SerialPort extends CommPort
*/
if (commPort instanceof SerialPort) {
SerialPort serialPort = (SerialPort) commPort;
/**
* 设置串口参数:setSerialPortParams( int b, int d, int s, int p )
* b:波特率(baudrate)
* d:数据位(datebits),SerialPort 支持 5,6,7,8
* s:停止位(stopbits),SerialPort 支持 1,2,3
* p:校验位 (parity),SerialPort 支持 0,1,2,3,4
* 如果参数设置错误,则抛出异常:gnu.io.UnsupportedCommOperationException: Invalid Parameter
* 此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用
*/
serialPort.setSerialPortParams(b, d, s, p);
logger.info("打开串口 " + portName + " 成功...");
return serialPort;
} else {
logger.error("当前端口 " + commPort.getName() + " 不是串口...");
}
} catch (NoSuchPortException e) {
e.printStackTrace();
} catch (PortInUseException e) {
logger.warn("串口 " + portName + " 已经被占用,请先解除占用...");
e.printStackTrace();
} catch (UnsupportedCommOperationException e) {
logger.warn("串口参数设置错误,关闭串口,数据位[5-8]、停止位[1-3]、验证位[0-4]...");
e.printStackTrace();
if (commPort != null) {//此时必须关闭串口,否则下次 portIdentifier.open 时会打不开串口,因为已经被占用
commPort.close();
}
}
logger.error("打开串口 " + portName + " 失败...");
return null;
}
/**
* 往串口发送数据
*
* @param serialPort 串口对象
* @param orders 待发送数据
*/
public static void sendDataToComPort(SerialPort serialPort, byte[] orders) {
OutputStream outputStream = null;
try {
if (serialPort != null) {
outputStream = serialPort.getOutputStream();
outputStream.write(orders);
outputStream.flush();
logger.info("往串口 " + serialPort.getName() + " 发送数据:" + Arrays.toString(orders) + " 完成...");
} else {
logger.error("gnu.io.SerialPort 为null,取消数据发送...");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 从串口读取数据
*
* @param serialPort 要读取的串口
* @return 读取的数据
*/
public static byte[] readData(SerialPort serialPort) {
InputStream is = null;
byte[] bytes = null;
try {
is = serialPort.getInputStream();//获得串口的输入流
int bufflenth = is.available();//获得数据长度
while (bufflenth != 0) {
bytes = new byte[bufflenth];//初始化byte数组
is.read(bytes);
bufflenth = is.available();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (is != null) {
is.close();
is = null;
}
} catch (IOException e) {
e.printStackTrace();
}
}
return bytes;
}
/**
* 给串口设置监听
*
* @param serialPort
* @param listener
*/
public static void setListenerToSerialPort(SerialPort serialPort, SerialPortEventListener listener) {
try {
//给串口添加事件监听
serialPort.addEventListener(listener);
} catch (TooManyListenersException e) {
e.printStackTrace();
}
serialPort.notifyOnDataAvailable(true);//串口有数据监听
serialPort.notifyOnBreakInterrupt(true);//中断事件监听
}
/**
* 关闭串口
*
* @param serialPort 待关闭的串口对象
*/
public static void closeComPort(SerialPort serialPort) {
if (serialPort != null) {
serialPort.close();
logger.info("关闭串口 " + serialPort.getName());
}
}
/**
* 16进制字符串转十进制字节数组
* 这是常用的方法,如某些硬件的通信指令就是提供的16进制字符串,发送时需要转为字节数组再进行发送
*
* @param strSource 16进制字符串,如 "455A432F5600",每两位对应字节数组中的一个10进制元素
* 默认会去除参数字符串中的空格,所以参数 "45 5A 43 2F 56 00" 也是可以的
* @return 十进制字节数组, 如 [69, 90, 67, 47, 86, 0]
*/
public static byte[] hexString2Bytes(String strSource) {
if (strSource == null || "".equals(strSource.trim())) {
System.out.println("hexString2Bytes 参数为空,放弃转换.");
return null;
}
strSource = strSource.replace(" ", "");
int l = strSource.length() / 2;
byte[] ret = new byte[l];
for (int i = 0; i < l; i++) {
ret[i] = Integer.valueOf(strSource.substring(i * 2, i * 2 + 2), 16).byteValue();
}
return ret;
}
/**
* 方法二:
* byte[] to hex string
*
* @param bytes
* @return
*/
public static String bytesToHexFun2(byte[] bytes) {
char[] buf = new char[bytes.length * 2];
int index = 0;
for (byte b : bytes) { // 利用位运算进行转换,可以看作方法一的变种
buf[index++] = HEX_CHAR[b >>> 4 & 0xf];
buf[index++] = HEX_CHAR[b & 0xf];
}
return new String(buf);
}
/**
* 初始化找到二维码模块的串口并开启在线模式
*
* @return
* @throws InterruptedException
*/
public static void init() throws InterruptedException {
//发起在线模式指令
String order = "50 31 31 31 30 3B";
byte[] bytes = hexString2Bytes(order);
ArrayList<String> allComPort = findSystemAllComPort();
if (CollectionUtils.isEmpty(allComPort)) {
return;
}
//遍历找到所需要的串口
for (String portName : allComPort) {
SerialPort serialPort = SerialPortUtils.openComPort(portName, 115200, 8, 1, 0);
SerialPortUtils.sendDataToComPort(serialPort, bytes);
byte[] readDataBytes = null;
for (int i = 0; i < 20; i++) {
readDataBytes = SerialPortUtils.readData(serialPort);
if (readDataBytes != null) break;
Thread.sleep(500);
}
if (readDataBytes != null) {
//找到结束
String readData = new String(readDataBytes);
if (readData.contains("P1110ok")) {
realSerialPort = serialPort;
break;
}
}
SerialPortUtils.closeComPort(serialPort);
}
//设置串口的listener
SerialPortUtils.setListenerToSerialPort(realSerialPort, new SerialPortEventListener() {
@Override
public void serialEvent(SerialPortEvent arg0) {
if (arg0.getEventType() == SerialPortEvent.DATA_AVAILABLE) {//数据通知
byte[] readDataBytes = SerialPortUtils.readData(realSerialPort);
QRCODE_TIME = System.currentTimeMillis() / 1000;
//解析二维码模组返回的数据
String hexFun = bytesToHexFun2(readDataBytes);
if (hexFun.length() > 400) {
String left = hexFun.substring(0, 486);
String right = hexFun.substring(488, hexFun.length() - 2);
String code = left + right;
BASE64Encoder base64Encoder = new BASE64Encoder();
qrCode = base64Encoder.encode(hexString2Bytes(code)).replaceAll("\r\n", "");
}
}
}
});
}
/**
* 获取二维码
*
* @return
* @throws InterruptedException
* @throws BusinessException
* @throws IOException
*/
public static String getQrCode() throws InterruptedException, BusinessException, IOException {
GWQUtils.showQrCodeTip();
Long now = System.currentTimeMillis() / 1000;
for (int i = 0; i < 60; i++) {
if (QRCODE_TIME != null && QRCODE_TIME > now) {
break;
}
Thread.sleep(500);
}
if (QRCODE_TIME == null || QRCODE_TIME < now) {
throw new BusinessException("没有扫描到二维码");
}
GWQUtils.closeQrCodeTip();
return qrCode;
}
public static void main(String[] args) throws IOException, InterruptedException, BusinessException, ResourceNotFoundException {
init();
getQrCode();
}
}