JAVA串口通信开发
JAVA串口通信开发
前言
最近几个月一直在接触串口,与硬件打交道,还是学到了不少之前没听过的东西,特此记录一下,其中不免有语焉不详或一知半解的地方,欢迎各位指教。
提示:以下是本篇文章正文内容,下面案例可供参考
一、项目背景
首先说串口是什么,百度上说串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。实际上就是传输数据用的物理接口,一般可以按照接线方式分为RS-232和RS-485,对于程序开发来说,这两者并没有什么不同。
之后说一下实际使用的项目背景,首先会有一台计算机,计算机上有一排物理串口,串口上接的是232的控制器,控制器连接实际的机械设备。而我们的目前是使用程序向232控制器发生指令来操控机械设备实现不同动作,程序最终会以HTTP接口的方式对外暴露。
二、实际开发
1.引入库
对于JAVA的串口通信开发,一般能查到的都是使用RXTXcomm.jar,同时需要rxtxParallel.dll和rxtxSerial.dll两个dll文件。最开始我也是使用了这种方式,但是后连在实际测试中发现了一个非常致命的问题,因为我的程序最后是一组HTTP接口,所以避免不了会同时对多个串口操作,而一旦发生同时或短时间内操作多个串口时,程序会崩溃,类似这样。

这个问题我实在没有搞清楚产生的原因,我怀疑可能使用的RXTXcomm并不支持同时操作,另外可能与JDK版本有关,建议1.8.0_144。
因为产生了这个问题目前又无法解决,所以最终我决定换一个驱动,不再采用RXTXcomm,而是选用了purejavacomm。purejavacomm使用的是JNA,并不需要额外引用DLL,使用方式与RXTXcomm相同,还是比较便捷的,我个人觉得绝对是JAVA串口开发最好的驱动了,可以直接用pom引用。
<dependency> <groupId>com.github.purejavacomm</groupId> <artifactId>purejavacomm</artifactId> <version>1.0.1.RELEASE</version> </dependency>
2.串口通信工具类
废话不多说,代码如下:
package com.water.api.util;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.TooManyListenersException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import purejavacomm.CommPort;
import purejavacomm.CommPortIdentifier;
import purejavacomm.NoSuchPortException;
import purejavacomm.PortInUseException;
import purejavacomm.SerialPort;
import purejavacomm.SerialPortEventListener;
import purejavacomm.UnsupportedCommOperationException;
@Component
public class SerialTool {
private static Logger logger = LoggerFactory.getLogger(SerialTool.class);
public static final ArrayList<String> findPorts() {
Enumeration<CommPortIdentifier> portList = CommPortIdentifier.getPortIdentifiers();
ArrayList<String> portNameList = new ArrayList<String>();
while (portList.hasMoreElements()) {
String portName = portList.nextElement().getName();
portNameList.add(portName); }
return portNameList;
}
public static SerialPort openPort(String portName, Integer baudrate, Integer dataBits, Integer stopBits, Integer parity)
throws Exception { try {
CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);
CommPort commPort = portIdentifier.open(portName, 2000);
if (commPort instanceof SerialPort) { SerialPort serialPort = (SerialPort) commPort;
try { serialPort.setSerialPortParams(baudrate, dataBits, stopBits, parity);
logger.info("串口" + portName + "打开成功"); }
catch (UnsupportedCommOperationException e) {
logger.error("设置串口" + portName + "参数失败:" + e.getMessage()); throw e; }
return serialPort; } else { logger.error("不是串口" + portName); throw new Exception(); } }
catch (NoSuchPortException e1) { logger.error("无此串口" + portName); throw e1; }
catch (PortInUseException e2) { logger.error("串口使用中" + portName); throw e2; }
catch (Exception e) { throw e; } }
public static byte[] HexString2Bytes(String src)
{ if (null == src || 0 == src.length()) { return null; } byte[] ret = new byte[src.length() / 2];
byte[] tmp = src.getBytes();
for (int i = 0; i < (tmp.length / 2); i++) {
ret[i] = uniteBytes(tmp[i * 2], tmp[i * 2 + 1]); } return ret; }
public static byte uniteBytes(byte src0, byte src1) { byte _b0 = Byte.decode("0x" + new String(new byte[] {
src0 })).byteValue();
_b0 = (byte) (_b0 << 4);
byte _b1 = Byte.decode("0x" + new String(new byte[]
{ src1 })).byteValue();
byte ret = (byte) (_b0 ^ _b1);
return ret; }
public static synchronized void closePort(SerialPort serialPort)
throws IOException {
if (serialPort != null) {
serialPort.close();
logger.info("串口" + serialPort.getName() + "已关闭");
}
}
public static void sendToPort(byte[] order, SerialPort serialPort) throws IOException {
OutputStream out = null;
try {
out = serialPort.getOutputStream(); out.write(order); out.flush();
logger.info("发送数据成功" + serialPort.getName()); } catch (IOException e) { logger.error("发送数据失败" + serialPort.getName()); throw e; }
finally { try { if (out != null) { out.close(); out = null; } } catch (IOException e) { logger.error("关闭串口对象的输出流出错"); throw e; } } }
public static byte[] readFromPort(SerialPort serialPort) throws Exception {
InputStream in = null; byte[] bytes = null;
try {
if (serialPort != null) { in = serialPort.getInputStream(); } else { return null; } int bufflenth = in.available();
while (bufflenth != 0) { bytes = new byte[bufflenth];
in.read(bytes); bufflenth = in.available(); } }
catch (Exception e) { throw e; }
finally {
try {
if (in != null) { in.close(); in = null; } }
catch (IOException e) { throw e; } }
return bytes; }
public static void addListener(SerialPortEventListener listener, SerialPort serialPort) throws TooManyListenersException {
try {
serialPort.addEventListener(listener);
serialPort.notifyOnDataAvailable(true);
serialPort.notifyOnBreakInterrupt(true); }
catch (TooManyListenersException e) { throw e; } } }
这部分其实没什么好说的,网上一搜一堆,唯一需要注意的是,需要考虑在程序里每一个串口的生命周期。对于一个串口,系统全局应当只有一个实例,频繁的开关串口并不是一个好的选择。
上面的代码只是说明了如何向串口发送数据,从串口中读数据需要添加监听,当有数据返回时会把数据推送到监听里。当然这种方式使用起来非常不爽,因为发送和接收是异步的,换句话说,发送指令是一个线程,而接收数据又是一个线程,实际使用中,很多时候需要得到返回值,然后来判断接下来发送的指令,这样代码写起来非常复杂,作为一个使用者肯定希望在同一位置完成收发,所以最后我封装了一个操作类来完成收发。
public class SerialResquest {
private static Logger logger = LoggerFactory.getLogger(SerialResquest.class);
public static void resquest(String portName, Integer baudrate, Integer dataBits, Integer stopBits, Integer parity,byte[] data) throws Exception {
SerialPort serialPort; if (!GlobalCache.smap.containsKey(portName)) {
GlobalCache.bmap.put(portName, false);
serialPort = SerialTool.openPort(portName, baudrate, dataBits, stopBits, parity);
GlobalCache.smap.put(portName, serialPort);
SerialTool.addListener(new SerialPortEventListener() { @Override
public void serialEvent(SerialPortEvent event) { try { Thread.sleep(50); }
catch (InterruptedException e1) { logger.error("SerialResquest 监听异常!"+e1); }
switch (event.getEventType()) {
case SerialPortEvent.DATA_AVAILABLE: byte[] readBuffer = null; int availableBytes = 0;
try { availableBytes = serialPort.getInputStream().available(); if (availableBytes > 0) {
try { readBuffer = SerialTool.readFromPort(serialPort); GlobalCache.bmap.put(portName, true);
GlobalCache.dmap.put(portName, readBuffer); } catch (Exception e) { logger.error("读取推送信息异常!"+e); } } }
catch (IOException e) { logger.error("读取流信息异常!"+e); } } } }, serialPort); }else
{ serialPort = GlobalCache.smap.get(portName); } SerialTool.sendToPort(data, serialPort); }
public static byte[] response(String portName) throws InterruptedException {
Thread.sleep(100); int i =0; while (!GlobalCache.bmap.get(portName))
{ Thread.sleep(100); if (i++>30) { return new byte[0]; } } GlobalCache.bmap.put(portName, false);
return GlobalCache.dmap.get(portName); } public static void close(String portName) throws IOException
{ SerialTool.closePort(GlobalCache.smap.get(portName)); GlobalCache.smap.remove(portName); } }
对于上面的代码,可以调用resquest方法来完成发送指令,如果系统没有当前串口实例,会生成一个并添加监听,如果有,则直接使用实例发送指令,通过response方法来接收返回值。需要注意的是,在监听中做了一次Thread.sleep(50),这是因为实际使用时发现返回值是断断续续的,例如发送一条指令A,理论上应该立即返回一条结果如AABBCCDD,但是实际会多次返回不同的部分,如先返回AA,然后返回BBC,每次返回的不完整,可能的原因是程序调用的是CPU资源,串口返回值是走串口连接线,速度上有差异,sleep后可以得到完整的返回值。如果实际串口连接线比较长,可以适当增大sleep时间。
3.数据解析
上面说过,与程序通过串口通信的实际上是控制器,当然有些设备也可以直接连接串口通信。与这些硬件通信的时候,避免不了数据交互,有些设备可能返回的数据比较友好,可以直观的看到数据值,但大部分返回的都需要解析。
数据的解析方式需要依据设备厂商提供的文档,但是原理大同小异,一般来说,返回的数据格式为short或float居多。而我们从程序读到的都是字节数组,我们需要做的就是把字节转成short或float。对于short,我们知道它占两字节,也就是16bit,那么我们只需要知道字节组中哪两位代表了一个short就可以解析出这个值,方法如下:
public static short toShort(byte b1, byte b2) { return (short) (b1 << 8 | b2 & 0xFF); }
对于float,占四个字节,也就是32bit,同样知道字节组中哪四位代表了一个float就可以解析出这个值,方法如下:
private float bytes2Float(byte[] bytes) { String BinaryStr = bytes2BinaryStr(bytes);
Long s = Long.parseLong(BinaryStr.substring(0, 1));
Long e = Long.parseLong(BinaryStr.substring(1, 9), 2);
String M = BinaryStr.substring(9);
float m = 0, a, b; for (int i = 0; i < M.length(); i++) { a = Integer.valueOf(M.charAt(i) + "");
b = (float) Math.pow(2, i + 1); m = m + (a / b); }
Float f = (float) ((Math.pow(-1, s)) * (1 + m) * (Math.pow(2, (e - 127))));
return f; }
private static String bytes2BinaryStr(byte[] bytes) {
StringBuffer binaryStr = new StringBuffer();
for (int i = 0; i < bytes.length; i++)
{
String str = Integer.toBinaryString((bytes[i] & 0xFF) + 0x100).substring(1);
binaryStr.append(str); } return binaryStr.toString();
}
总结
说了半天可能有些东西还是没说明白,或者我自己也没有理解。如果您有类似的困惑,欢迎与我联系,我们可以一起探讨。