JAVA串口通信开发

  • 前言
  • 一、项目背景
  • 二、实际开发
  • 1.引入库
  • 2.串口通信工具类
  • 3.数据解析
  • 总结



前言

最近几个月一直在接触串口,与硬件打交道,还是学到了不少之前没听过的东西,特此记录一下,其中不免有语焉不详或一知半解的地方,欢迎各位指教。


提示:以下是本篇文章正文内容,下面案例可供参考

一、项目背景

首先说串口是什么,百度上说串行接口简称串口,也称串行通信接口或串行通讯接口(通常指COM接口),是采用串行通信方式的扩展接口。串行接口 (Serial Interface)是指数据一位一位地顺序传送。实际上就是传输数据用的物理接口,一般可以按照接线方式分为RS-232和RS-485,对于程序开发来说,这两者并没有什么不同。
之后说一下实际使用的项目背景,首先会有一台计算机,计算机上有一排物理串口,串口上接的是232的控制器,控制器连接实际的机械设备。而我们的目前是使用程序向232控制器发生指令来操控机械设备实现不同动作,程序最终会以HTTP接口的方式对外暴露。

二、实际开发

1.引入库

对于JAVA的串口通信开发,一般能查到的都是使用RXTXcomm.jar,同时需要rxtxParallel.dll和rxtxSerial.dll两个dll文件。最开始我也是使用了这种方式,但是后连在实际测试中发现了一个非常致命的问题,因为我的程序最后是一组HTTP接口,所以避免不了会同时对多个串口操作,而一旦发生同时或短时间内操作多个串口时,程序会崩溃,类似这样。

安卓java串口通信 java串口通信入门_串口通信


这个问题我实在没有搞清楚产生的原因,我怀疑可能使用的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>();
		// 将可用串口名添加到List并返回该List
		while (portList.hasMoreElements()) {
			String portName = portList.nextElement().getName();
			portNameList.add(portName);
		}
		return portNameList;
	}

	/**
	 * 打开串口
	 * 
	 * @param portName
	 *            端口名称
	 * @param baudrate
	 *            波特率
	 * @return 串口对象
	 * @throws Exception
	 * @throws SerialPortParameterFailure
	 *             设置串口参数失败
	 * @throws NotASerialPort
	 *             端口指向设备不是串口类型
	 * @throws NoSuchPort
	 *             没有该端口对应的串口设备
	 * @throws PortInUse
	 *             端口已被占用
	 */
	public static SerialPort openPort(String portName, Integer baudrate, Integer dataBits, Integer stopBits,
			Integer parity) throws Exception {

		try {

			// 通过端口名识别端口
			CommPortIdentifier portIdentifier = CommPortIdentifier.getPortIdentifier(portName);

			// 打开端口,并给端口名字和一个timeout(打开操作的超时时间)
			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;
	}

	// byte类型数据,转成十六进制形式;
	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;
	}

	/**
	 * 关闭串口
	 * 
	 * @throws IOException
	 */
	public static synchronized void closePort(SerialPort serialPort) throws IOException {
		if (serialPort != null) {
			serialPort.close();
			logger.info("串口" + serialPort.getName() + "已关闭");
		}
	}

	/**
	 * 往串口发送数据
	 * 
	 * @param order
	 *            待发送数据
	 * @throws SendDataToSerialPortFailure
	 *             向串口发送数据失败
	 * @throws SerialPortOutputStreamCloseFailure
	 *             关闭串口对象的输出流出错
	 */
	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;
			}
		}

	}

	/**
	 * 从串口读取数据
	 * 
	 * @param serialPort
	 *            当前已建立连接的SerialPort对象
	 * @return 读取到的数据
	 * @throws ReadDataFromSerialPortFailure
	 *             从串口读取数据时出错
	 * @throws SerialPortInputStreamCloseFailure
	 *             关闭串口对象输入流出错
	 */
	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(); // 获取buffer里的数据长度
			while (bufflenth != 0) {
				bytes = new byte[bufflenth]; // 初始化byte数组为buffer中数据的长度
				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;

	}

	/**
	 * 添加监听器
	 * 
	 * @param port
	 *            串口对象
	 * @param listener
	 *            串口监听器
	 * @throws TooManyListeners
	 *             监听类对象过多
	 */
	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 {
		/*if (!GlobalCache.dmap.containsKey(portName)) {
			return null;
		}*/
		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);
		// 符号位S
		Long s = Long.parseLong(BinaryStr.substring(0, 1));
		// 指数位E
		Long e = Long.parseLong(BinaryStr.substring(1, 9), 2);
		// 位数M
		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();
	}

总结

说了半天可能有些东西还是没说明白,或者我自己也没有理解。如果您有类似的困惑,欢迎与我联系,我们可以一起探讨。