App 端 Modbus 通信链路 React Native + Java 技术栈下的完整 Modbus-RTU 方案
·
架构说明:串口必须在 Java 原生层做(Android 无法在 JS 层直接开串口),所以协议核心放 Java,RN 通过 NativeModule 暴露 Promise 接口;若你的设备走 TCP 转串口模块(如 USR-TCP232),可以 Java 用 Socket 替代 SerialPort,桥接层完全不变。

① Java 端:CRC16-Modbus
package com.yourpackage.modbus;
/**
* CRC16-Modbus:多项式 0x8005(反射 0xA001)、初值 0xFFFF、结果低字节在前
*/
public final class ModbusCRC16 {
private static final int[] TABLE = new int[256];
static {
for (int i = 0; i < 256; i++) {
int c = i;
for (int j = 0; j < 8; j++) {
c = (c & 1) != 0 ? (c >>> 1) ^ 0xA001 : (c >>> 1);
}
TABLE[i] = c & 0xFFFF;
}
}
private ModbusCRC16() {}
/** 计算整段数据 CRC16,返回 16 位整数 */
public static int calc(byte[] data) {
return calc(data, 0, data.length);
}
public static int calc(byte[] data, int offset, int length) {
int crc = 0xFFFF;
for (int i = offset; i < offset + length; i++) {
crc = (crc >>> 8) ^ TABLE[(crc ^ (data[i] & 0xFF)) & 0xFF];
}
return crc & 0xFFFF;
}
}
② Java 端:组帧 / 拆包(ModbusFrame.java)
package com.yourpackage.modbus;
import java.util.Arrays;
/**
* Modbus-RTU 帧 组包/拆包(含松下 FP 系列地址换算)
* 参照号:40001=DT0 / 30001=WL0 / 302001=LD2000 / 10001=X0 / 00001=Y0
*/
public final class ModbusFrame {
// 功能码
public static final int FC_READ_COILS = 0x01; // Y/R 读
public static final int FC_READ_DISCRETE_INPUTS = 0x02; // X 读
public static final int FC_READ_HOLDING_REGISTERS = 0x03; // DT 读
public static final int FC_READ_INPUT_REGISTERS = 0x04; // WL/LD 读
public static final int FC_WRITE_SINGLE_COIL = 0x05;
public static final int FC_WRITE_SINGLE_REGISTER = 0x06;
public static final int FC_WRITE_MULTIPLE_COILS = 0x0F;
public static final int FC_WRITE_MULTIPLE_REGISTERS = 0x10;
/** Modbus 参照号(40001/30001/10001/00001)→ PDU 偏移地址 */
public static int addrToOffset(int addr) {
if (addr >= 40001) return addr - 40001; // 保持寄存器
if (addr >= 30001) return addr - 30001; // 输入寄存器
if (addr >= 10001) return addr - 10001; // 离散输入
return addr - 1; // 线圈
}
/** 在帧尾追加 CRC(低字节在前,符合 RTU 线序) */
private static byte[] appendCrc(byte[] body) {
int crc = ModbusCRC16.calc(body);
byte[] frame = Arrays.copyOf(body, body.length + 2);
frame[body.length] = (byte) (crc & 0xFF);
frame[body.length + 1] = (byte) ((crc >>> 8) & 0xFF);
return frame;
}
/** 读指令:01/02/03/04。offset 为 PDU 偏移,quantity ≤ 2000(位)/ 125(字) */
public static byte[] buildReadRequest(int slave, int fc, int offset, int quantity) {
if (slave < 0 || slave > 247) throw new IllegalArgumentException("从站号 0~247");
if (offset < 0 || offset > 0xFFFF) throw new IllegalArgumentException("地址越界");
byte[] body = new byte[6];
body[0] = (byte) slave;
body[1] = (byte) fc;
body[2] = (byte) (offset >> 8);
body[3] = (byte) offset;
body[4] = (byte) (quantity >> 8);
body[5] = (byte) quantity;
return appendCrc(body);
}
/** 写单线圈 05:on=true 发 FF00,false 发 0000 */
public static byte[] buildWriteSingleCoil(int slave, int offset, boolean on) {
byte[] body = new byte[6];
body[0] = (byte) slave;
body[1] = (byte) FC_WRITE_SINGLE_COIL;
body[2] = (byte) (offset >> 8);
body[3] = (byte) offset;
body[4] = on ? (byte) 0xFF : 0x00;
body[5] = 0x00;
return appendCrc(body);
}
/** 写单寄存器 06:value 为 16 位无符号值 */
public static byte[] buildWriteSingleRegister(int slave, int offset, int value) {
byte[] body = new byte[6];
body[0] = (byte) slave;
body[1] = (byte) FC_WRITE_SINGLE_REGISTER;
body[2] = (byte) (offset >> 8);
body[3] = (byte) offset;
body[4] = (byte) (value >> 8);
body[5] = (byte) value;
return appendCrc(body);
}
/** 写多线圈 0F:values 布尔数组 */
public static byte[] buildWriteMultipleCoils(int slave, int offset, boolean[] values) {
int qty = values.length;
int byteCount = (qty + 7) / 8;
byte[] body = new byte[7 + byteCount];
body[0] = (byte) slave;
body[1] = (byte) FC_WRITE_MULTIPLE_COILS;
body[2] = (byte) (offset >> 8);
body[3] = (byte) offset;
body[4] = (byte) (qty >> 8);
body[5] = (byte) qty;
body[6] = (byte) byteCount;
for (int i = 0; i < qty; i++) {
if (values[i]) body[7 + (i >> 3)] |= (byte) (1 << (i & 7));
}
return appendCrc(body);
}
/** 写多寄存器 10:values 16 位数组 */
public static byte[] buildWriteMultipleRegisters(int slave, int offset, int[] values) {
int qty = values.length;
byte[] body = new byte[7 + qty * 2];
body[0] = (byte) slave;
body[1] = (byte) FC_WRITE_MULTIPLE_REGISTERS;
body[2] = (byte) (offset >> 8);
body[3] = (byte) offset;
body[4] = (byte) (qty >> 8);
body[5] = (byte) qty;
body[6] = (byte) (qty * 2);
for (int i = 0; i < qty; i++) {
body[7 + i * 2] = (byte) (values[i] >> 8); // 大端序
body[8 + i * 2] = (byte) values[i];
}
return appendCrc(body);
}
/**
* 根据接收缓冲判断完整帧长度(粘包/半包处理核心)
* @return >=0 帧长;-2 半包需等待;-1 帧头非法应丢弃
*/
public static int guessFrameLength(byte[] buf, int len) {
if (len < 2) return -2;
int fc = buf[1] & 0xFF;
if ((fc & 0x80) != 0) return len >= 5 ? 5 : -2; // 异常响应固定 5 字节
switch (fc) {
case FC_READ_COILS:
case FC_READ_DISCRETE_INPUTS:
case FC_READ_HOLDING_REGISTERS:
case FC_READ_INPUT_REGISTERS:
if (len < 3) return -2;
return 3 + (buf[2] & 0xFF) + 2; // 站号+功能码+字节数+数据+CRC
case FC_WRITE_SINGLE_COIL:
case FC_WRITE_SINGLE_REGISTER:
case FC_WRITE_MULTIPLE_COILS:
case FC_WRITE_MULTIPLE_REGISTERS:
return len >= 8 ? 8 : -2; // 写指令正常应答回显 8 字节
default:
return -1;
}
}
/** 解析一帧完整响应 */
public static Response parseResponse(byte[] frame) {
int len = frame.length;
int crcRecv = (frame[len - 2] & 0xFF) | ((frame[len - 1] & 0xFF) << 8);
int crcCalc = ModbusCRC16.calc(frame, 0, len - 2);
if (crcRecv != crcCalc) return Response.error("CRC 校验失败");
int slave = frame[0] & 0xFF;
int fc = frame[1] & 0xFF;
if ((fc & 0x80) != 0) return Response.exception(slave, fc & 0x7F, frame[2] & 0xFF);
if (fc == FC_READ_COILS || fc == FC_READ_DISCRETE_INPUTS) {
int byteCount = frame[2] & 0xFF;
boolean[] bits = new boolean[byteCount * 8];
for (int i = 0; i < byteCount; i++)
for (int b = 0; b < 8; b++)
bits[i * 8 + b] = ((frame[3 + i] >> b) & 1) != 0;
return Response.okBits(slave, fc, bits);
}
if (fc == FC_READ_HOLDING_REGISTERS || fc == FC_READ_INPUT_REGISTERS) {
int byteCount = frame[2] & 0xFF;
int[] regs = new int[byteCount / 2];
for (int i = 0; i < regs.length; i++) {
regs[i] = ((frame[3 + i * 2] & 0xFF) << 8) | (frame[4 + i * 2] & 0xFF); // 大端
}
return Response.okRegisters(slave, fc, regs);
}
return Response.okEcho(slave, fc); // 05/06/0F/10 正常应答
}
/** 响应结果封装 */
public static final class Response {
public static final Response PLACEHOLDER = new Response(false, 0, 0, null, null, -1, "pending");
public final boolean ok;
public final int slave;
public final int fc;
public final boolean[] bits; // 01/02 读位结果
public final int[] registers; // 03/04 读寄存器结果
public final int exceptionCode; // 异常码,-1 表示无
public final String error;
private Response(boolean ok, int slave, int fc, boolean[] bits, int[] registers, int exceptionCode, String error) {
this.ok = ok; this.slave = slave; this.fc = fc;
this.bits = bits; this.registers = registers;
this.exceptionCode = exceptionCode; this.error = error;
}
static Response okBits(int slave, int fc, boolean[] bits) { return new Response(true, slave, fc, bits, null, -1, null); }
static Response okRegisters(int slave, int fc, int[] regs) { return new Response(true, slave, fc, null, regs, -1, null); }
static Response okEcho(int slave, int fc) { return new Response(true, slave, fc, null, null, -1, null); }
static Response exception(int slave, int fc, int code) {
String[] msg = {"", "非法功能码", "非法数据地址", "非法数据值", "从站设备故障", "确认", "从站忙", "", "存储奇偶性错误"};
String desc = code >= 0 && code < msg.length && !msg[code].isEmpty() ? msg[code] : "异常码 " + code;
return new Response(false, slave, fc, null, null, code, desc);
}
static Response error(String msg) { return new Response(false, 0, 0, null, null, -1, msg); }
}
}
③ Java 端:串口客户端(ModbusSerialClient.java)
package com.yourpackage.modbus;
import android_serialport_api.SerialPort; // 或 com.licheedev 等串口库,构造参数按库调整
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
/**
* Modbus-RTU 串口客户端:接收线程 + 粘包缓冲 + 一问一答 + 超时
* 串口已在外部打开(Android 13 Root 环境,设备节点如 /dev/ttyS1)
*/
public class ModbusSerialClient {
private final InputStream in;
private final OutputStream out;
private final long timeoutMs;
private final Object lock = new Object();
private final Response[] slot = new Response[1]; // 当前挂起事务
private volatile boolean running = true;
private Thread readerThread;
private byte[] buffer = new byte[0];
/** @param serialPort 已打开的串口(波特率 9600/19200,8N1) */
public ModbusSerialClient(SerialPort serialPort, long timeoutMs) throws IOException {
this.in = serialPort.getInputStream();
this.out = serialPort.getOutputStream();
this.timeoutMs = timeoutMs;
startReader();
}
private void startReader() {
readerThread = new Thread(() -> {
byte[] tmp = new byte[1024];
try {
while (running) {
int n = in.read(tmp);
if (n <= 0) continue;
synchronized (this) {
buffer = concat(buffer, Arrays.copyOf(tmp, n));
}
drain();
}
} catch (IOException ignored) {
// 串口关闭,线程退出
}
}, "modbus-reader");
readerThread.setDaemon(true);
readerThread.start();
}
private static byte[] concat(byte[] a, byte[] b) {
byte[] out = new byte[a.length + b.length];
System.arraycopy(a, 0, out, 0, a.length);
System.arraycopy(b, 0, out, a.length, b.length);
return out;
}
/** 拆包:从缓冲里取完整帧并结算挂起事务 */
private void drain() {
while (true) {
final int len;
final byte[] frame;
synchronized (this) {
if (buffer.length == 0) return;
int fl = ModbusFrame.guessFrameLength(buffer, buffer.length);
if (fl == -2) return; // 半包,等下一段
if (fl == -1) { buffer = new byte[0]; continue; } // 非法帧,丢弃重来
if (buffer.length < fl) return;
frame = Arrays.copyOf(buffer, fl);
buffer = Arrays.copyOfRange(buffer, fl, buffer.length);
}
synchronized (lock) {
if (slot[0] == null) continue; // 无等待请求(已超时),忽略
slot[0] = ModbusFrame.parseResponse(frame);
lock.notifyAll();
}
}
}
/** 发送请求帧并等待响应(串行事务,超时抛 IOException) */
public ModbusFrame.Response request(byte[] requestFrame) throws IOException, InterruptedException {
synchronized (lock) {
if (slot[0] != null) throw new IllegalStateException("上一请求未完成");
slot[0] = ModbusFrame.Response.PLACEHOLDER;
out.write(requestFrame);
out.flush();
long deadline = System.currentTimeMillis() + timeoutMs;
while (slot[0] == ModbusFrame.Response.PLACEHOLDER) {
long remain = deadline - System.currentTimeMillis();
if (remain <= 0) {
slot[0] = null;
throw new IOException("响应超时");
}
lock.wait(remain);
}
ModbusFrame.Response r = slot[0];
slot[0] = null;
return r;
}
}
/** 便捷:读保持寄存器(松下 DT),返回寄存器数组或抛异常 */
public int[] readHoldingRegisters(int slave, int offset, int quantity)
throws IOException, InterruptedException {
byte[] frame = ModbusFrame.buildReadRequest(
slave, ModbusFrame.FC_READ_HOLDING_REGISTERS, offset, quantity);
ModbusFrame.Response r = request(frame);
if (!r.ok) throw new IOException(r.error);
return r.registers;
}
/** 便捷:写单寄存器 */
public void writeSingleRegister(int slave, int offset, int value)
throws IOException, InterruptedException {
ModbusFrame.Response r = request(ModbusFrame.buildWriteSingleRegister(slave, offset, value));
if (!r.ok) throw new IOException(r.error);
}
/** 关闭串口(由外部 SerialPort 负责 close) */
public void close() {
running = false;
if (readerThread != null) readerThread.interrupt();
synchronized (lock) { lock.notifyAll(); }
}
}
④ Java 端:React Native 桥接(ModbusModule.java)
package com.yourpackage.modbus;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.WritableArray;
import android_serialport_api.SerialPort;
import java.io.File;
public class ModbusModule extends ReactContextBaseJavaModule {
private ModbusSerialClient client;
private SerialPort serialPort;
public ModbusModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return "Modbus"; // JS 端 NativeModules.Modbus
}
/** 打开串口:device 如 /dev/ttyS1;baudRate 9600/19200(8N1) */
@ReactMethod
public void connect(String device, int baudRate, int timeoutMs, Promise promise) {
try {
// Android 13 Root 环境:先确保节点权限(chmod 666 /dev/ttyS1)
File dev = new File(device);
if (!dev.exists()) {
promise.reject("DEVICE_NOT_FOUND", "串口节点不存在: " + device);
return;
}
serialPort = new SerialPort(dev, baudRate, 0); // 8 数据位、无校验、1 停止位
client = new ModbusSerialClient(serialPort, timeoutMs);
promise.resolve(true);
} catch (Exception e) {
promise.reject("OPEN_FAIL", "串口打开失败: " + e.getMessage());
}
}
@ReactMethod
public void disconnect() {
if (client != null) client.close();
if (serialPort != null) serialPort.close();
client = null;
serialPort = null;
}
/** 读保持寄存器:offset 传 PDU 偏移(或直接传 40001 系参照号,自动换算) */
@ReactMethod
public void readHoldingRegisters(int slave, int offset, int quantity, Promise promise) {
if (client == null) { promise.reject("NOT_CONNECTED", "未连接串口"); return; }
try {
if (offset >= 40001) offset = ModbusFrame.addrToOffset(offset); // 参照号自动换算
int[] regs = client.readHoldingRegisters(slave, offset, quantity);
WritableArray arr = Arguments.createArray();
for (int v : regs) arr.pushInt(v);
promise.resolve(arr);
} catch (Exception e) {
promise.reject("MODBUS_ERR", e.getMessage());
}
}
/** 读线圈(Y/R):返回布尔数组 */
@ReactMethod
public void readCoils(int slave, int offset, int quantity, Promise promise) {
if (client == null) { promise.reject("NOT_CONNECTED", "未连接串口"); return; }
try {
if (offset >= 40001 || offset >= 10001) offset = ModbusFrame.addrToOffset(offset);
byte[] frame = ModbusFrame.buildReadRequest(
slave, ModbusFrame.FC_READ_COILS, offset, quantity);
ModbusFrame.Response r = client.request(frame);
if (!r.ok) { promise.reject("MODBUS_ERR", r.error); return; }
WritableArray arr = Arguments.createArray();
for (boolean b : r.bits) arr.pushBoolean(b);
promise.resolve(arr);
} catch (Exception e) {
promise.reject("MODBUS_ERR", e.getMessage());
}
}
/** 写单寄存器 */
@ReactMethod
public void writeSingleRegister(int slave, int offset, int value, Promise promise) {
if (client == null) { promise.reject("NOT_CONNECTED", "未连接串口"); return; }
try {
if (offset >= 40001) offset = ModbusFrame.addrToOffset(offset);
client.writeSingleRegister(slave, offset, value);
promise.resolve(true);
} catch (Exception e) {
promise.reject("MODBUS_ERR", e.getMessage());
}
}
/** 写单线圈 */
@ReactMethod
public void writeSingleCoil(int slave, int offset, boolean on, Promise promise) {
if (client == null) { promise.reject("NOT_CONNECTED", "未连接串口"); return; }
try {
byte[] frame = ModbusFrame.buildWriteSingleCoil(slave, offset, on);
ModbusFrame.Response r = client.request(frame);
if (!r.ok) promise.reject("MODBUS_ERR", r.error);
else promise.resolve(true);
} catch (Exception e) {
promise.reject("MODBUS_ERR", e.getMessage());
}
}
}
注册模块(MainApplication.java/ 包):
// packages 包下新建 ModbusPackage.java
public class ModbusPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new ModbusModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
}
// MainApplication.getPackages() 中加入 new ModbusPackage()
⑤ React Native JS 端调用
// src/utils/modbus.js
import { NativeModules } from 'react-native';
const { Modbus } = NativeModules;
export default {
/** 打开串口,如 /dev/ttyS1 */
connect: (device = '/dev/ttyS1', baudRate = 9600, timeoutMs = 800) =>
Modbus.connect(device, baudRate, timeoutMs),
disconnect: () => Modbus.disconnect(),
/** 读松下 FP DT 寄存器:offset 可传 PDU 偏移(0起)或参照号(41001) */
readHoldingRegisters: (slave, offset, quantity) =>
Modbus.readHoldingRegisters(slave, offset, quantity),
readCoils: (slave, offset, quantity) =>
Modbus.readCoils(slave, offset, quantity),
writeSingleRegister: (slave, offset, value) =>
Modbus.writeSingleRegister(slave, offset, value),
writeSingleCoil: (slave, offset, on) =>
Modbus.writeSingleCoil(slave, offset, on),
};
// 页面中使用(React 组件 / 业务层)
import modbus from '../utils/modbus';
async function readPlcData() {
try {
await modbus.connect('/dev/ttyS1', 9600, 800); // 打开串口 8N1
const regs = await modbus.readHoldingRegisters(17, 1000, 3); // 从站17, DT1000起3字
console.log('DT1000~1002 =', regs); // [0x1100, 0x3322, 0x5544]
const coils = await modbus.readCoils(17, 0, 20); // Y0~Y13 共20位
console.log('Y0~Y13 =', coils);
await modbus.writeSingleRegister(17, 1000, 0x55AA); // 写 DT1000 = 0x55AA
await modbus.writeSingleCoil(17, 2048 + 10, true); // R10 ON(R 从 2048 偏移起)
} catch (e) {
console.error('PLC 通信失败:', e.message);
}
}
使用注意
| 项 | 说明 |
|---|---|
| 串口权限 | Android 13 Root 环境:chmod 666 /dev/ttyS1(或 su -c 提权后打开);非 root 需 SerialPort 库内部提权逻辑 |
| 8-N-1 | 波特率 9600/19200、8 数据位、无校验、1 停止位,SerialPort(dev, baud, 0) 默认即 8N1 |
| 地址换算 | 松下 R 继电器从偏移 2048 起(R0=2048),DT 参照号 41001 = PDU 偏移 1000;桥接层已内置自动换算 |
| 事务串行 | Modbus 一问一答,request() 未完成前再次调用会抛 "上一请求未完成",轮询场景请 await 串行 |
| 数据上限 | 一次读位 ≤2040、读字 ≤127(FP-X 协议限制),超限会收到从站异常码 03 |
| 线程 | 读线程为 daemon,串口 close 后自动退出;请求线程建议放工作线程,勿在 JS 主线程阻塞 |
更多推荐


所有评论(0)