javax.comm包提供了java原生的串口通信API,实际中用到的场景很多,例如很多设备的控制信号都是通过串口进行控制的,只要向指定串口发送指定消息,就可以控制设备或读取设备信息,例如读取温度传感器信息、控制自动贩卖机出货等等。
使用javax.comm进行串口通信大概分为以下几个步骤:
1、选择一个可利用串口如COM1,得到一个CommPortIdentifier类。
2、设置初始化参数(波特率、数据位、停止位、校验位),利用CommPortIdentifier.open()方法得到一个SerialPort。
3、利用SerialPort.getOutputStream得到串口输出流,向串口写入数据。
4、利用SerialPort.addEventListener(SerialPortEventListener listener)为串口添加监听事件,当串口返回数据时,在SerialPortEventListener监听器的public void serialEvent(SerialPortEvent arg0)方法中,通过SerialPort.getInputStream得到串口输入流来读取响应数据。
这里提供一个串口通信Utils类。由于串口的通信机制,SerialPortEventListener监听器每接收到8个字节时会调用一次serialEvent,因此在数据未读取完毕时需要追加读入字节数组,这样才能取到完整的返回字节数组。
源码如下,代码中的Log记录类和自定义Exception类请自行替换后通过编译。
Util类 SerialPortCommUtil:
/**
*
*/
package com.ktvm.common.serial;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Enumeration;
import java.util.Timer;
import java.util.TimerTask;
import javax.comm.CommPortIdentifier;
import javax.comm.SerialPort;
import javax.comm.SerialPortEvent;
import javax.comm.SerialPortEventListener;
import com.ktvm.common.exception.KtVendExceptoin;
import com.ktvm.common.util.LogUtil;
import com.ktvm.common.util.SysConstant;
/**
* @author weih535
* 温度监控器
*/
public class SerialPortCommUtil implements SerialPortEventListener{
private SerialPort serialPort;
private InputStream inputStream;
private OutputStream outputStream;
private CommPortIdentifier commPort;
private int timeout = 2000; // open 端口时的等待时间
/**串口响应时间*/
private int responseTime;
private String portName; //端口名称
private int baudRate; //波特率
private int dataBits=8; //数据位
private int stopBit=1; //停止位
private int verifyBit=0; //检验位
private int totalDataLen = 0; //串口返回字节数据长度
private int readlength = 0;
private boolean initTest = false;
private int callbackTimes; //串口通信当前回调次数
byte[] readBuffer = null;
private boolean enableLog = false;
private boolean isWatting = false; //是否正在等待串口数据返回
private boolean isTimeout = false;
private static final String VERIFYSTRING = "FE 04 06"; //校验字串
/**定时器*/
private Timer timer;
private SerialPortWatcher watcher;
public SerialPortCommUtil(String portName, int baudRate, int dataBits,
int stopBit, int verifyBit) {
this.portName = portName.toUpperCase();
this.baudRate = baudRate;
this.dataBits = dataBits;
this.stopBit = stopBit;
this.verifyBit = verifyBit;
this.enableLog = Boolean.valueOf(PropUtil.getInstance().getProperty("enableLog"));
}
/**
* 初始化串口通信
*/
public void initSerial() {
LogUtil.info(LogUtil.INFO_SERIAL + "SerialCommunication.initSerial 串口通信初始化!");
responseTime = SysConstant.SERIAL_RESPONSE_TIME * 1000;
try {
if(!initTest)
listPort();
selectPort(portName);
resetCallbackTimes();
readBuffer = null;
initTest = true;
} catch (KtVendExceptoin e) {
LogUtil.error(e, e.getMessage());
e.getMessage();
}
}
/**
* 列出所有可用的串口
*
* @return :void
*/
@SuppressWarnings("rawtypes")
public void listPort() {
if(enableLog){
LogUtil.info(LogUtil.INFO_SERIAL + "SerialCommunication.listPort 列出所有可用的串口 ");
CommPortIdentifier cpid;
Enumeration en = CommPortIdentifier.getPortIdentifiers();
while (en.hasMoreElements()) {
cpid = (CommPortIdentifier) en.nextElement();
if (cpid.getPortType() == CommPortIdentifier.PORT_SERIAL) {
LogUtil.debug(cpid.getName() + ", " + cpid.getCurrentOwner());
}
}
}
}
/**
* 选择一个端口(比如:COM1)
*
* @param portName
* @return :void
*/
@SuppressWarnings("rawtypes")
public void selectPort(String portName) {
LogUtil.info(LogUtil.INFO_SERIAL + "选择端口: " + portName);
this.commPort = null;
CommPortIdentifier cpid;
Enumeration en = CommPortIdentifier.getPortIdentifiers();
while (en.hasMoreElements()) {
cpid = (CommPortIdentifier) en.nextElement();
if (cpid.getPortType() == CommPortIdentifier.PORT_SERIAL && cpid.getName().equals(portName)) {
this.commPort = cpid;
break;
}
}
openPort(portName);
}
/**
* 打开SerialPort
*
* @return :void
*/
private void openPort(String portName) {
if (commPort == null)
throw new KtVendExceptoin(String.format("无法找到名字为'%1$s'的串口!", portName));
else {
if(enableLog)
LogUtil.debug("打开端口:" + commPort.getName() + ",现在实例化 SerialPort:");
try {
serialPort = (SerialPort) commPort.open("TemperatureMonitor", timeout);
serialPort.setSerialPortParams(baudRate,
SysConstant.SERIAL_DATA_BITS,
SysConstant.SERIAL_STOP_BITS,
SysConstant.SERIAL_PARITY);
serialPort.addEventListener(this);
if(enableLog)
LogUtil.debug("实例 SerialPort 成功!");
} catch (Exception e) {
LogUtil.error(e);
throw new KtVendExceptoin(String.format("端口'%1$s'正在使用中!", commPort.getName()), e);
}
}
}
/**
* 数据接收的监听处理函数
*
* @return : void
*/
@Override
public void serialEvent(SerialPortEvent arg0) {
if(totalDataLen == 0)
throw new KtVendExceptoin("返回数据长度未初始化...");
switch (arg0.getEventType()) {
case SerialPortEvent.BI:/* Break interrupt,通讯中断 */
break;
case SerialPortEvent.OE:/* Overrun error,溢位错误 */
break;
case SerialPortEvent.FE:/* Framing error,传帧错误 */
errLog(portName + " Framing error,传帧错误");
break;
case SerialPortEvent.PE:/* Parity error,校验错误 */
break;
case SerialPortEvent.CD:/* Carrier detect,载波检测 */
break;
case SerialPortEvent.CTS:/* Clear to send,清除发送 */
log(portName + " ====CTS====");
break;
case SerialPortEvent.DSR:/* Data set ready,数据设备就绪 */
break;
case SerialPortEvent.RI:/* Ring indicator,响铃指示 */
break;
case SerialPortEvent.OUTPUT_BUFFER_EMPTY:/* Output buffer is empty,输出缓冲区清空*/
log(portName + " ====OUTPUT_BUFFER_EMPTY====");
break;
case SerialPortEvent.DATA_AVAILABLE:/* Data available at the serial port 端口有可用数据*/
//温度监控读取响应数据分两次返回,因此在第二次返回时接着上一次的开始读
LogUtil.info("callback=" + callbackTimes + " readlength=" + readlength);
if(readlength % totalDataLen == 0){
if(readBuffer == null)
readBuffer = new byte[totalDataLen];
readlength = 0;
}
callbackTimes++;
try {
// 读取串口返回信息
if (inputStream!=null && inputStream.available() > 0) {
readlength += inputStream.read(readBuffer, readlength, totalDataLen-readlength);
if(enableLog){
LogUtil.info("callback:" + callbackTimes + "--readed:" + readlength);
LogUtil.info("串口返回数据[len:" +readlength +"]" + AppUtil.showByteData(readBuffer, readlength));
}
if(initTest) {
if(readlength == totalDataLen){
boolean verifyRst = verifyRtnData(readBuffer);
// LogUtil.info(LogUtil.INFO_SERIAL + "返回数据校验成功!");
//数据读取完成时调用监视事件
if(watcher != null){
WatchEvent event = new WatchEvent();
if(verifyRst)
event.setData(readBuffer);
else
event.setData(correctData(readBuffer));
watcher.doWatch(event);
}
//准备下一次数据读取
resetCallbackTimes();
// 关闭定时器
timer.cancel();
log("set watting = false");
setWatting(false);
serialPort.notifyOnDataAvailable(false);
//close input stream
if(inputStream!=null){
log("close inputStream...");
try {
inputStream.close();
inputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
//TODO 视情况可放开以重新初始化端口
if(!verifyRst){
reInitSerial();
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
LogUtil.error(new KtVendExceptoin("串口通信失败!", e));
}
}
}
/**
* 向端口发送数据
*
* @param message
* @return :void
*/
public void write(byte[] message) {
setWatting(true);
if(enableLog)
LogUtil.info(LogUtil.INFO_SERIAL + portName + "发送数据Preapre:" + AppUtil.showByteData(message, 8));
checkPort();
serialTimeOut();
try {
outputStream = new BufferedOutputStream(serialPort.getOutputStream());
} catch (IOException e) {
throw new KtVendExceptoin("获取端口的OutputStream出错:" + e.getMessage(), e);
}
try {
outputStream.write(message);
if(enableLog)
LogUtil.info(LogUtil.INFO_SERIAL + portName + "发送数据成功");
if(inputStream == null){
inputStream = new BufferedInputStream(serialPort.getInputStream());
}
serialPort.notifyOnDataAvailable(true);
// startRead();
} catch (IOException e) {
throw new KtVendExceptoin(portName + "向端口发送信息时出错:" + e.getMessage(), e);
} finally {
try {
outputStream.close();
} catch (Exception e) {
}
}
}
/**
* 关闭 SerialPort
*
* @return :void
*/
public void close() {
final Timer closeTimer = new Timer();
TimerTask task=new TimerTask() {
@Override
public void run() {
closeNow();
SysConstant.tempWatchTimer.cancel();
closeTimer.cancel();
}
};
closeTimer.schedule(task, 1000);
}
public void closeNow(){
if(serialPort != null)
serialPort.close();
serialPort = null;
commPort = null;
if(inputStream!=null){
try {
inputStream.close();
inputStream = null;
} catch (IOException e) {
e.printStackTrace();
}
}
LogUtil.info(LogUtil.INFO_SERIAL + portName + "已关闭!");
}
/**
* 串口通信状态监控定时器,状态不正常时重启端口
*/
private void serialTimeOut(){
if(timer!=null){
timer.cancel();
timer = null;
}
timer = new Timer();
TimerTask timerTask=new TimerTask() {
@Override
public void run() {
LogUtil.error(LogUtil.ERR + portName + "---串口通信" + responseTime + "秒内未返回完整数据!");
LogUtil.info(LogUtil.INFO_SERIAL + "---重新启动端口---" + SysConstant.spc.getPortName() + " Start...");
reInitSerial();
LogUtil.info(LogUtil.INFO_SERIAL + "---重新启动端口---" + SysConstant.spc.getPortName() + " Success...");
setTimeout(true);
}
};
timer.schedule(timerTask, responseTime);
}
private void reInitSerial(){
log("重新初始化串口" + portName + "...");
closeNow();
initSerial();
setWatting(false); //让端口可写
}
/** 校验读取到的数据是否合法 **/
private boolean verifyRtnData(byte[] readBuffer) {
if(AppUtil.getHexByteString(readBuffer, 0, 3).equals(VERIFYSTRING))
return true;
LogUtil.error(LogUtil.INFO_SERIAL + portName+"数据校验失败...");
return false;
}
/** 校验不合法时重构缓冲区数组
串口会返回8+N个字节。N为前回未读取完的字节数。如下面的第一个FE是多节的:
FE FE 04 06 08 00 08 06 08
**/
private byte[] correctData(byte[] buffer) {
LogUtil.error(LogUtil.INFO_SERIAL + portName+"重构缓冲区数组...");
int indexOf = AppUtil.indexOfHexByteArray(buffer, "FE 04 06");
byte[] newData = new byte[totalDataLen];
for(int i=indexOf,j=0; i < totalDataLen; i++,j++){
newData[j] = buffer[i];
}
readlength = readlength - indexOf;//修正已读取长度,把inputstream把剩下的数据读完,否则下次还是错误的。
//for(int i=0, j=totalDataLen-indexOf; i < indexOf; i++,j++){
// System.out.println(j);
// newData[j] = buffer[i];
//}
return newData;
}
/**
* 检查端口是否正确连接
*
* @return :void
*/
private void checkPort() {
if (commPort == null) {
throw new KtVendExceptoin("没有找到端口!");
}
if (serialPort == null) {
throw new KtVendExceptoin("SerialPort 对象无效!");
}
}
private void resetCallbackTimes(){
log("[IN]resetCallbackTimes.");
this.callbackTimes = 0;
this.readlength = 0;
readBuffer = null;
}
public String getPortName() {
return portName;
}
public void setPortName(String portName) {
this.portName = portName.toUpperCase();
}
public int getBaudRate() {
return baudRate;
}
public void setBaudRate(int baudRate) {
this.baudRate = baudRate;
}
public int getDataBits() {
return dataBits;
}
public void setDataBits(int dataBits) {
this.dataBits = dataBits;
}
public int getStopBit() {
return stopBit;
}
public void setStopBit(int stopBit) {
this.stopBit = stopBit;
}
public int getVerifyBit() {
return verifyBit;
}
public void setVerifyBit(int verifyBit) {
this.verifyBit = verifyBit;
}
public int getRtnDataLen() {
return totalDataLen;
}
public void setRtnDataLen(int rtnDataLen) {
this.totalDataLen = rtnDataLen;
this.readlength = 0;
}
public void setWatcher(SerialPortWatcher watcher) {
this.watcher = watcher;
}
public boolean isWatting() {
return isWatting;
}
public void setWatting(boolean isWatting) {
this.isWatting = isWatting;
}
public boolean isTimeout() {
return isTimeout;
}
public void setTimeout(boolean isTimeout) {
this.isTimeout = isTimeout;
}
public int getResponseTime() {
return responseTime;
}
private void log(String str){
if(enableLog){
LogUtil.info(LogUtil.INFO_SERIAL + portName + ":" + str);
}
}
private void errLog(String str){
if(enableLog){
LogUtil.error(LogUtil.INFO_SERIAL + portName + ":" + str);
}
}
}
private String showByteData(byte[] b, int length) {
String str = "";
for (int i=0; i<b.length && i < length; i++) {
str += String.format("%02X", b[i]) + " ";
}
return str;
}
添加一个串口监视器接口,当串口有数据返回时,利用doWatch(WatchEvent we)方法做实际业务想做的事情:
/**
* @author weih535
* 串口监视器接口
*/
public interface SerialPortWatcher {
public void doWatch(WatchEvent event);
}
串口监视器具体类:
@Component
public class TemperatureWatcher implements SerialPortWatcher {
@Resource
private WebServiceManager webServiceManager; //注入自己的业务service类
/* 温度监控器
* @see com.kotei.ktvm.common.serial.SerialPortWatcher#doWatch(com.kotei.ktvm.common.serial.WatchEvent)
*/
@Override
public void doWatch(WatchEvent event) {
// TODO Auto-generated method stub
byte[] data = event.getData();
System.out.println("DoWatch:::::" + showByteData(data, data.length));
//调用service上传温度监控值。这里请根据自己业务需要调用业务方法。
ServiceResponse serviceResponse = webServiceManager.upTemperatureInfo(
SysConstant.UP_TEMPINFO, JsonUtil.listToJson(data));
}
private static String showByteData(byte[] b, int length) {
String str = "";
if(b!=null){
for (int i=0; i<b.length && i < length; i++) {
str += String.format("%02X", b[i]) + " ";
}
}
return str;
}
}
测试类:
package com.ktvm.common.serial;
public class TestComm {
public static void main(String[] args) {
testSerial();
}
private static void testSerial() {
final SerialPortCommUtil sp = new SerialPortCommUtil("com1", 9600, 8 , 1, 0);
sp.setRtnDataLen(11); //这里根据串口设备的API文档说明,将串口返回的字节数量设为11.(这意味着串口监听需要2跳才能接收到一组完整数据)
sp.initSerial();
sp.write(new byte[]{(byte)0xFE, 0x04, 0x00 ,0x00 ,0x00 ,0x03, (byte)0xA4, 0x04});
sp.close();
//定时写入、读取测试
/*final Timer closeTimer = new Timer();
SysConstant.spc.initSerial();
//定时写入温度串口信息
TimerTask task=new TimerTask() {
@Override
public void run() {
if(!sp.isWatting())
sp.write(new byte[]{(byte)0xFE, 0x04, 0x00 ,0x00 ,0x00 ,0x03, (byte)0xA4, 0x04});
else
LogUtil.info(LogUtil.INFO_SERIAL + SysConstant.spc.getPortName() + " is watting...");
}
};
closeTimer.schedule(task, 1000, SysConstant.SERIAL_TEMP_INTERVAL * 1000);
}
}
运行结果如下:
2015-10-24 17:34:45:789 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] SerialCommunication.initSerial 串口通信初始化!
2015-10-24 17:34:45:791 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] SerialCommunication.listPort 列出所有可用的串口
2015-10-24 17:34:45:798 DEBUG [LogUtil.java:40] - COM1, Port currently not owned
2015-10-24 17:34:45:799 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] 选择端口: COM1
2015-10-24 17:34:45:800 DEBUG [LogUtil.java:40] - 端口选择成功,当前端口:COM1,现在实例化 SerialPort:
2015-10-24 17:34:45:803 DEBUG [LogUtil.java:40] - 实例 SerialPort 成功!
2015-10-24 17:34:45:806 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] COM1发送数据Preapre:FE 04 00 00 00 03 A4 04
2015-10-24 17:34:45:807 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] COM1已发送数据:FE 04 00 00 00 03 A4 04
callback:0--readed:8
2015-10-24 17:34:45:833 DEBUG [LogUtil.java:40] - 串口返回数据[len:8]FE 04 06 0A DF 0A E2 0A
2015-10-24 17:34:45:833 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] 返回数据校验成功!
callback:1--readed:11
2015-10-24 17:34:45:841 DEBUG [LogUtil.java:40] - 串口返回数据[len:11]FE 04 06 0A DF 0A E2 0A EC 14 DD
2015-10-24 17:34:45:841 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] 返回数据校验成功!
2015-10-24 17:34:47:808 INFO [LogUtil.java:36] - [DEBUGINFO][SERIAL] COM1已关闭!