上个月有个需求的背景是某硬件制造商想做智能化平台接入,想用某个实验室做试点,需要搭建一个所谓的“智能化平台”,实际上就是个能接入很多很多硬件设备的后台管理平台。
和硬件厂商第一次对接的时候就充满了不在一个频道的感觉
某笨:采集你们的数据要走啥接口啊,http还是websocket?
厂家:串口的,我们是用rs485串口连接的。
然后就是一通鸡对鸭讲。总而言之,最后定下的方案是多个仪器通过rs485接口转成usb接口接入服务器,通过modbus rtu协议进行通讯。
使用的框架数据库以及运行环境:spring boot+mybatis+Modbus4j+mysql+centos7.3。
在这个过程中那真的是相当多的坑,算是记录一下吧。
驱动
首先遇到的第一个问题就是如何进行通讯,因为现在绝大部分笔记本,开发用电脑,以及没有专门配置板子的服务器上面是没有串口的。我是用的usb转串口,直接淘宝/京东上面弄个这种东西就行了。
绝大部分这种转换器都需要装驱动,而驱动是由里面的芯片决定的,普通市面上的绝大多数都是pl2303芯片,这个芯片在windows下和osx下都有支持的驱动,下载安装就完事了。至于服务器上,比如centos7上,实际上内核里面是自带了驱动的,插上去赋权就可以了,不需要专门配置。如果内核里面不自带的,需要自己编译一下,具体里流程因为我没试过就不说了。
驱动这一块有两个坑,第一个是linux下,也就是服务器上部署的时候是需要赋权的,否则串口会禁止访问,赋权:
666 /dev/ttyUSB0
/dev/ttyUSB0就是要访问的串口名称,自己去dev下 ls grep一下看看就行了。
注意的是linux下每次插拔都需要重新赋权。
第二个则是在osx下如果程序运行过程中不是自然关闭,或者在强行关闭的时候没有destroy打开的串口连接的话,会导致串口占用并锁死,严重情况会导致驱动崩溃,需要重装。我当时就是因为这个坑至少重装了10次驱动,也就是重启了10次+。
惹不起的rxtx
在网上java如何连接串口以及modbus4j协议连接的时候,都会告诉你用rxtx库进行连接。我最开始也是用rxtx进行的连接,但是rxtx库有这么个问题:需要加载指定的dll/jar文件到系统环境中,还需要进行赋权等操作。本身使用java就是为了方便,结果还需要在不同的平台进行的不同的配置,实在是不方便(个人感觉)。
可能通过docker部署会好很多,但是因为没有尝试过,所以暂时未知。最后参考了一些别的资料,使用的是jssc的方式进行串口通信。
需要引入的依赖
<dependency>
版本可以去maven仓库自己查。
因为实际上Modbus4j里面只规定了如何调用,而调用的底层协议不管是tcp还是rtu都是可以自己来实现的,所以利用jssc实际上是实现了SerialPortWrapper这个接口。在这个类里面需要实现的方法以及定义的变量为:
public
里面引用到的SerialInputStream和SerialOutputStream我是直接引用了Freedomotic这个项目里面的实现,参考
freedomotic/freedomoticgithub.com
注意里面的一个坑,SerialInputStream里面会数组越界报错,自行修改成readBuf.length就可以了。
Modbus rtu协议传输
传输使用的是Modbus4j库
<dependency>
需要做的是实现一个rtu工具类进行调用
public
进行通信的时候只需要调用:
ModbusMaster master = Modbus4jUtilsRtu.getMaster();
就创建了一个modbus协议下的主站,也就是Master(具体可以去参考modbus协议的主从站资料)。
那么如何进行通讯呢,实际上Modbus协议下有很多种读写的方式,常用的是使用03方式读取数据,也就是从HoldingRegister里面读取需要的数据。那么就需要实现一个从里面读取数据的方式,标准的deemo里面都是告诉你这么读取的:
/**
* 读取[03 Holding Register类型 2x]模拟量数据
*
* @param slaveId slave Id
* @param offset 位置
* @param dataType 数据类型,来自com.serotonin.modbus4j.code.DataType
* @return
* @throws ModbusTransportException 异常
* @throws ErrorResponseException 异常
* @throws ModbusInitException 异常
*/
public static Number readHoldingRegister(ModbusMaster master, int slaveId, int offset, int dataType)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
// 03 Holding Register类型数据读取
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
Number value = master.getValue(loc);
return value;
}
如果只需要读取两个字节码的数据,那么没有问题,如果需要读取的数据类型里面是标准类型里面有的,也就是在com.serotonin.modbus4j.code.DataType这个类里面定义了的数据类型,那么也没有问题。但是实际上很多工业设备都是自己定义了如何在寄存器里面存储数据的,最后实际上是需要拿出从协议原始嘛里面拿出来的数据,那么就需要自己来实现如何拿回需要的数据了。
/**
* 读取[03 Holding Register类型 2x]模拟量数据
*
* @param slaveId slave Id
* @param offset 位置
* @param dataType 数据类型,来自com.serotonin.modbus4j.code.DataType
* @param registerNumber 需要返回的数据数量
* @return
* @throws ModbusTransportException 异常
* @throws ErrorResponseException 异常
* @throws ModbusInitException 异常
*/
public static <T> T readHoldingRegisterByte(ModbusMaster master, int slaveId, int offset, int dataType, int registerNumber)
throws ModbusTransportException, ErrorResponseException, ModbusInitException {
BaseLocator<Number> loc = BaseLocator.holdingRegister(slaveId, offset, dataType);
byte[] value = (byte[]) getValue(loc,master, registerNumber);
return (T) value;
}
/**
*
* @param locator
* @param modbus
* @param <T>
* @return
* @throws ModbusTransportException
* @throws ErrorResponseException
*/
public static <T> Object getValue(BaseLocator<T> locator, ModbusMaster modbus, int registerNumber) throws ModbusTransportException, ErrorResponseException {
BatchRead<String> batch = new BatchRead();
batch.addLocator("", locator);
return send(batch,modbus, registerNumber);
}
public static <K> byte[] send(BatchRead<K> batch, ModbusMaster modbus, int registerNumber) throws ModbusTransportException, ErrorResponseException {
if (!modbus.isInitialized()) {
throw new ModbusTransportException("not initialized");
} else {
BatchResults<K> results = new BatchResults();
List<ReadFunctionGroup<K>> functionGroups = batch.getReadFunctionGroups(modbus);
Iterator var4 = functionGroups.iterator();
byte[] resultByte = null;
while(var4.hasNext()) {
ReadFunctionGroup<K> functionGroup = (ReadFunctionGroup)var4.next();
resultByte = sendFunctionGroup(functionGroup, results, batch.isErrorsInResults(), batch.isExceptionsInResults(), modbus, registerNumber);
if (batch.isCancel()) {
break;
}
}
return resultByte;
}
}
private static <K> byte[] sendFunctionGroup(ReadFunctionGroup<K> functionGroup, BatchResults<K> results, boolean errorsInResults, boolean exceptionsInResults, ModbusMaster modbus, int registerNumber) throws ModbusTransportException, ErrorResponseException {
int slaveId = functionGroup.getSlaveAndRange().getSlaveId();
int startOffset = functionGroup.getStartOffset();
//int length = functionGroup.getLength();
int length = registerNumber;
Object request;
if (functionGroup.getFunctionCode() == 1) {
request = new ReadCoilsRequest(slaveId, startOffset, length);
} else if (functionGroup.getFunctionCode() == 2) {
request = new ReadDiscreteInputsRequest(slaveId, startOffset, length);
} else if (functionGroup.getFunctionCode() == 3) {
request = new ReadHoldingRegistersRequest(slaveId, startOffset, length);
} else {
if (functionGroup.getFunctionCode() != 4) {
throw new RuntimeException("Unsupported function");
}
request = new ReadInputRegistersRequest(slaveId, startOffset, length);
}
ReadResponse response;
Iterator var11;
KeyedModbusLocator locator;
try {
response = (ReadResponse)modbus.send((ModbusRequest)request);
} catch (ModbusTransportException var15) {
ModbusTransportException e = var15;
if (!exceptionsInResults) {
throw var15;
}
var11 = functionGroup.getLocators().iterator();
while(var11.hasNext()) {
locator = (KeyedModbusLocator)var11.next();
results.addResult((K) locator.getKey(), e);
}
return null;
}
byte[] data = null;
if (!errorsInResults && response.isException()) {
throw new ErrorResponseException((ModbusRequest)request, response);
} else {
if (!response.isException()) {
data = response.getData();
return data;
}
var11 = functionGroup.getLocators().iterator();
while(true) {
while(var11.hasNext()) {
locator = (KeyedModbusLocator)var11.next();
if (errorsInResults && response.isException()) {
results.addResult((K) locator.getKey(), new ExceptionResult(response.getExceptionCode()));
} else {
try {
results.addResult((K) locator.getKey(), locator.bytesToValue(data, startOffset));
} catch (RuntimeException var14) {
throw new RuntimeException("Result conversion exception. data=" + ArrayUtils.toHexString(data) + ", startOffset=" + startOffset + ", locator=" + locator + ", functionGroup.functionCode=" + functionGroup.getFunctionCode() + ", functionGroup.startOffset=" + startOffset + ", functionGroup.length=" + length, var14);
}
}
}
return null;
}
}
}
其实就是参考Modbus4j的源码,重写了一遍如何取数据,在对返回数据进行转换之前就把原始数据字节码返回,至于在之后如何进行转换就取决于每个硬件厂商如何进行数据存储了。
调用的话只需要:
byte[] result = Modbus4jUtilsRtu.readHoldingRegisterByte(master,100,8,DataType.FOUR_BYTE_INT_UNSIGNED,6);
其实3是从站地址,0是起始的offset,然后是指定的返回类型(实际上不起作用),最后一个是需要读取多少个寄存器里面的数据。比如上面就是从地址为100的从站里面第八个地址为开始读取12个寄存器(6个16位数据,因为默认两个寄存器里面保存一个所需要的数据)里面的数据。
实际上需要读取的数据数量是由
int length = functionGroup.getLength();
决定的,但是因为时间太紧(十一之后才立项开始接触厂商并开始开发),并没有看到functionGroup这个类是如何设置的,所以直接使用的重写,希望以后有时间再慢慢整理这一块把。
destroy的重要性
因为是通过定时任务Schedule进行定时通讯,所以在每一次定时任务结束了以后一定要
((RtuMaster)master).destroy();
我的理解是,每一个定时任务开了线程进行通讯以后,并没有关闭串口,会一直保持会话,即使定时任务结束了,这个会话因为没有destroy所以也一直保持着,大概相当于内存泄漏了?总之会导致串口锁死,记得destroy就可以了。