公司的一个产品设计项目,采用了搭载Android系统的单板机来操控外设和大量的传感器。
单板机上有两个串口,外设也是在串口的PLC管控下的。
不过,Android系统有两个问题:
1) Android不支持串口读写:
我采用了android_serialport_api第三方类库来解决了,参见让android_serialport_api支持奇偶校验、数据位、停止位等参数;
2) Android没有Modbus类库,任何的类库:
严格说起来,Modbus类库还真找到一个:Android Modbus的实现--Modbus4Android。不过Modbus4Android类库是个半成品,且只支持Modbus TCP,不支持Modbus RTU,所以放弃。
幸而,我曾经做过Python的项目,用到过Python的modbus_tk类库,这是一个非常成熟的类库,功能很完备性能很稳定。而且,作为一个开源类库,自然是有源代码的。于是,自然就想到了借鉴、改编、抄袭。。。(无论怎么说吧)
先上源代码(改编结果):
/*
Modbus RTU master
*/
public class ModbusMaster {
private int timeout = 1000;
private SerialPort port;
public ModbusMaster(SerialPort port) {
this.port = port;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
/**
* 执行modbus读写操纵
* @param slave 取值 1-255
* @param function_code 功能码,取值 1-6,参见 {@link ModbusFunction}
* @param starting_address 读写起止地址,取值 0-65535
* @param quantity_of_x 每次读写数量,取值1-255
* @param output_value 写入数值,仅在写操作时有效
* @return 读取到的数值
* @throws IOException
* @throws ModbusError
*/
synchronized public int[] execute(int slave, int function_code, int starting_address, int quantity_of_x, int output_value) throws IOException, ModbusError {
if (slave < 0 || slave > 255) {
throw new ModbusError(ModbusErrorType.ModbusInvalidArgumentError, "Invalid slave " + slave);
}
if (starting_address < 0 || starting_address > 0xffff) {
throw new ModbusError(ModbusErrorType.ModbusInvalidArgumentError, "Invalid starting_address " + starting_address);
}
if (quantity_of_x < 1 || quantity_of_x > 0xff) {
throw new ModbusError(ModbusErrorType.ModbusInvalidArgumentError, "Invalid quantity_of_x " + quantity_of_x);
}
boolean is_read_function = false;
int expected_length = 0;
// 构造request
ByteArrayWriter request = new ByteArrayWriter();
request.writeInt8(slave);
if (function_code == ModbusFunction.READ_COILS || function_code == ModbusFunction.READ_DISCRETE_INPUTS) {
is_read_function = true;
request.writeInt8(function_code);
request.writeInt16(starting_address);
request.writeInt16(quantity_of_x);
expected_length = (int) Math.ceil(0.1d * quantity_of_x / 8.0) + 5;
} else if (function_code == ModbusFunction.READ_INPUT_REGISTERS || function_code == ModbusFunction.READ_HOLDING_REGISTERS) {
is_read_function = true;
request.writeInt8(function_code);
request.writeInt16(starting_address);
request.writeInt16(quantity_of_x);
expected_length = 2 * quantity_of_x + 5;
} else if (function_code == ModbusFunction.WRITE_SINGLE_COIL || function_code == ModbusFunction.WRITE_SINGLE_REGISTER) {
if (function_code == ModbusFunction.WRITE_SINGLE_COIL)
if (output_value != 0) output_value = 0xff00;
request.writeInt8(function_code);
request.writeInt16(starting_address);
request.writeInt16(output_value);
expected_length = 8;
} else {
throw new ModbusError(ModbusErrorType.ModbusFunctionNotSupportedError, "Not support function " + function_code);
}
byte[] bytes = request.toByteArray();
int crc = CRC16.compute(bytes);
request.writeInt16Reversal(crc);
// 发送到设备
bytes = request.toByteArray();
port.getOutputStream().write(bytes);
// 从设备接收反馈
byte[] responseBytes;
try (ByteArrayWriter response = new ByteArrayWriter()) {
ThreadUtil.sleep(150);
final int finalExpected_length = expected_length;
final boolean[] complete = new boolean[1];
boolean done = TimeoutUtil.execute(new Runnable() {
@Override
public void run() {
try {
byte[] bytes = new byte[64];
while (!complete[0]) {
if (port.getInputStream().available() > 0) {
int len = port.getInputStream().read(bytes, 0, bytes.length);
if (len > 0) {
response.write(bytes, 0, len);
if (response.size() >= finalExpected_length) {
break;
}
}
}
ThreadUtil.sleep(1);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, timeout);
complete[0] = true;
response.flush();
if (!done) {
throw new ModbusError(ModbusErrorType.ModbusTimeoutError, String.format("Timeout of %d ms.", timeout));
}
responseBytes = response.toByteArray();
}
if (responseBytes == null || responseBytes.length != expected_length) {
throw new ModbusError(ModbusErrorType.ModbusInvalidResponseError, "Response length is invalid " + responseBytes.length);
}
ByteArrayReader reader = new ByteArrayReader(responseBytes);
int responseSlave = reader.readInt8();
if (responseSlave != slave) {
throw new ModbusError(ModbusErrorType.ModbusInvalidResponseError,
String.format("Response slave %d is different from request slave %d", responseSlave, slave));
}
int return_code = reader.readInt8();
if (return_code > 0x80) {
int error_code = reader.readInt8();
throw new ModbusError(error_code);
}
int data_length = 0;
if (is_read_function) {
// get the values returned by the reading function
data_length = reader.readInt8();
int actualLength = responseBytes.length - 5;
if (data_length != actualLength) {
throw new ModbusError(ModbusErrorType.ModbusInvalidResponseError,
String.format("Byte count is %d while actual number of bytes is %d. ", data_length, actualLength));
}
}
// 读取反馈数据
int[] result = new int[quantity_of_x];
if (function_code == ModbusFunction.READ_COILS || function_code == ModbusFunction.READ_DISCRETE_INPUTS) {
bytes = new byte[data_length];
for (int i = 0; i < data_length; i++) {
bytes[i] = (byte) reader.readInt8();
}
BitSet bits = BitSet.valueOf(bytes);
for (int i = 0; i < quantity_of_x; i++) {
result[i] = bits.get(i) ? 1 : 0;
}
} else if (function_code == ModbusFunction.READ_INPUT_REGISTERS || function_code == ModbusFunction.READ_HOLDING_REGISTERS) {
for (int i = 0; i < quantity_of_x; i++) {
result[i] = reader.readInt16();
}
} else if (function_code == ModbusFunction.WRITE_SINGLE_COIL || function_code == ModbusFunction.WRITE_SINGLE_REGISTER) {
result[0] = reader.readInt16();
//result[1] = reader.readInt16();
}
return result;
}
public int[] readCoils(int slave, int startAddress, int numberOfPoints) throws IOException, ModbusError {
return execute(slave, ModbusFunction.READ_COILS, startAddress, numberOfPoints, 0);
}
public int[] readHoldingRegisters(int slave, int startAddress, int numberOfPoints) throws IOException, ModbusError {
return execute(slave, ModbusFunction.READ_HOLDING_REGISTERS, startAddress, numberOfPoints, 0);
}
public int[] readInputRegisters(int slave, int startAddress, int numberOfPoints) throws IOException, ModbusError {
return execute(slave, ModbusFunction.READ_INPUT_REGISTERS, startAddress, numberOfPoints, 0);
}
public int[] readInputs(int slave, int startAddress, int numberOfPoints) throws IOException, ModbusError {
return execute(slave, ModbusFunction.READ_DISCRETE_INPUTS, startAddress, numberOfPoints, 0);
}
public void writeSingleCoil(int slave, int address, boolean value) throws IOException, ModbusError {
execute(slave, ModbusFunction.WRITE_SINGLE_COIL, address, 1, value ? 1 : 0);
}
public void writeSingleRegister(int slave, int address, int value) throws IOException, ModbusError {
execute(slave, ModbusFunction.WRITE_SINGLE_REGISTER, address, 1, value);
}
public boolean readCoil(int slave, int address) throws IOException, ModbusError {
int[] values = readCoils(slave, address, 1);
return values[0] > 0;
}
public int readHoldingRegister(int slave, int address) throws IOException, ModbusError {
int[] values = readHoldingRegisters(slave, address, 1);
return values[0];
}
public int readInputRegister(int slave, int address) throws IOException, ModbusError {
int[] values = readInputRegisters(slave, address, 1);
return values[0];
}
public boolean readInput(int slave, int address) throws IOException, ModbusError {
int[] values = readInputs(slave, address, 1);
return values[0] > 0;
}
}
简单说明一下:
1) 全部功能代码都集中在 ModbusMaster的execute方法中;
2)代码中没有import部分,是因为里面包含有公司代号,不好写出来;
3)ModbusMaster.execute方法里的代码,比modbus_tk里的代码简单许多,原因在于:水平有限,modbus_tk的代码没有完全看懂,我不是故意的;
4)串口操作用的是android_serialport_api,参见让android_serialport_api支持奇偶校验、数据位、停止位等参数;
5) 源代码中用到了一些工具类, 已经跟主程序一起打包上传,可以在下边找到下载链接。
然后我简单说一下代码结构:
1)首先,在实例化这个ModbusMaster类之前,你需要先得有个SerialPort实例 (android_serialport_api);
2) 然后,你就可以调用execute方法读写modbus设备了;
3) execute方法的第一部分,自然是参数验证;
4) 第二部分,是根据功能码类型,构造传入请求的字节组(这部分比起原版的modbus_tk精简很多);
5) 第三部分,写入请求的字节组,并同步读取返回结果,注意,我这里采用了同步方式读取返回结果,如果采用异步方式可能会显著提高性能,但程序代码(包括调用方的程序),将会复杂很多。为了简化程序(也为了偷懒),使用了同步方式,幸而我们的项目没有要求太高的性能。注意,之所以啰嗦这么多,是因为我测试的Modbus串口操作,每次读写差不多要0.1秒才能完成,所以对性能要求很高的场景,必然需要改成异步读写。
6) 第四部分,根据功能码处理返回的字节组,得到结果数值。
以上是本文章的全部内容。
本文的全部源代码在这儿下载