架构说明:串口必须在 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 主线程阻塞

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐