最近有个需求是用普通android手机可以与串口设备通信,读取数据,考虑到前端实现,整体需要用ReactNative混合开发实现,因此,就需要对串口通信部分进行原生封装,封装为原生模块供RN前端调用。Android与串口设备通信,外接一个OTG转串口模块即可,这里串口协议为RS485,因此淘宝买了OTG(typec口)转RS485/232模块,如下:

Snipaste20220820111819png

整体串口通信实现示意如下:

Snipaste20220820111322png

若不是485协议,如TTL232等,选择相应的转换模块即可,如OTGTTLOTG232,有的转换模块也同时可以支持多种协议,底层原理都是相同的。

原生层封装

串口通信原生模块封装

配置串口库

首先在android中引入串口库,这里选择usb-serial-for-android

加入gradle依赖

android/app/build.gradle中加入jitpack仓库:

allprojects {
    repositories {
        maven { url 'https://jitpack.io' }
    }
}

然后加入依赖:

dependencies {
     ...
    implementation 'com.github.mik3y:usb-serial-for-android:3.4.6' // 添加依赖
}

设置串口模块插入/拔出提示权限

OTG转串口模块插入或拔出时,可以进行监听提示,首先在res/xml中加入设备过滤设置文件device_filter.xml,内容如下:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!-- 0x0403 / 0x60??: FTDI -->
    <usb-device vendor-id="1027" product-id="24577" /> <!-- 0x6001: FT232R -->
    <usb-device vendor-id="1027" product-id="24592" /> <!-- 0x6010: FT2232H -->
    <usb-device vendor-id="1027" product-id="24593" /> <!-- 0x6011: FT4232H -->
    <usb-device vendor-id="1027" product-id="24596" /> <!-- 0x6014: FT232H -->
    <usb-device vendor-id="1027" product-id="24597" /> <!-- 0x6015: FT230X, FT231X, FT234XD -->

    <!-- 0x10C4 / 0xEA??: Silabs CP210x -->
    <usb-device vendor-id="4292" product-id="60000" /> <!-- 0xea60: CP2102 and other CP210x single port devices -->
    <usb-device vendor-id="4292" product-id="60016" /> <!-- 0xea70: CP2105 -->
    <usb-device vendor-id="4292" product-id="60017" /> <!-- 0xea71: CP2108 -->

    <!-- 0x067B / 0x23?3: Prolific PL2303x -->
    <usb-device vendor-id="1659" product-id="8963" /> <!-- 0x2303: PL2303HX, HXD, TA, ... -->
    <usb-device vendor-id="1659" product-id="9123" /> <!-- 0x23a3: PL2303GC -->
    <usb-device vendor-id="1659" product-id="9139" /> <!-- 0x23b3: PL2303GB -->
    <usb-device vendor-id="1659" product-id="9155" /> <!-- 0x23c3: PL2303GT -->
    <usb-device vendor-id="1659" product-id="9171" /> <!-- 0x23d3: PL2303GL -->
    <usb-device vendor-id="1659" product-id="9187" /> <!-- 0x23e3: PL2303GE -->
    <usb-device vendor-id="1659" product-id="9203" /> <!-- 0x23f3: PL2303GS -->

    <!-- 0x1a86 / 0x?523: Qinheng CH34x -->
    <usb-device vendor-id="6790" product-id="21795" /> <!-- 0x5523: CH341A -->
    <usb-device vendor-id="6790" product-id="29987" /> <!-- 0x7523: CH340 -->

    <!-- CDC driver -->
    <usb-device vendor-id="9025" />                   <!-- 0x2341 / ......: Arduino -->
    <usb-device vendor-id="5824" product-id="1155" /> <!-- 0x16C0 / 0x0483: Teensyduino  -->
    <usb-device vendor-id="1003" product-id="8260" /> <!-- 0x03EB / 0x2044: Atmel Lufa -->
    <usb-device vendor-id="7855" product-id="4"    /> <!-- 0x1eaf / 0x0004: Leaflabs Maple -->
    <usb-device vendor-id="3368" product-id="516"  /> <!-- 0x0d28 / 0x0204: ARM mbed -->
    <usb-device vendor-id="1155" product-id="22336" /><!-- 0x0483 / 0x5740: ST CDC -->
    <usb-device vendor-id="11914" product-id="5"   /> <!-- 0x2E8A / 0x0005: Raspberry Pi Pico Micropython -->
    <usb-device vendor-id="11914" product-id="10"  /> <!-- 0x2E8A / 0x000A: Raspberry Pi Pico SDK -->
    <usb-device vendor-id="6790" product-id="21972" /><!-- 0x1A86 / 0x55D4: Qinheng CH9102F -->
</resources>

AndroidManifest.xml中加入USB权限:

<activity
    android:name=".MainActivity"
    android:label="@string/app_name"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize|uiMode"
    android:launchMode="singleTask"
    android:windowSoftInputMode="adjustResize">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
         <!-- 监听USB插拔动作 -->
        <action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <!-- 元数据配置,配置设备过滤列表 -->
    <meta-data
         ndroid:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
         android:resource="@xml/device_filter" />
</activity>

封装串口通信模块

usb-serial-for-android本身的使用非常简单,核心操作即为打开串口设备与读写数据,打开串口设备:

UsbManager usbManager = getUsbManager(); // usb管理对象
UsbSerialDriver firstDriver = getFirstSerialDevice(); // 获取到第一个串口设备
UsbDeviceConnection connection = usbManager.openDevice(firstDriver.getDevice()); // 打开连接通道
UsbSerialPort usbSerialPort= firstDriver.getPorts().get(0); // 获取到第一个端口,一般串口设备也只有一个端口
usbSerialPort.open(connection); // 在端口上打开链接
usbSerialPort.setParameters(9600, 8, UsbSerialPort.STOPBITS_1, UsbSerialPort.PARITY_NONE); // 设置 波特率、数据位、停止位、校验位参数

读写数据:

// 发送
byte[] data = new byte[]{0x31, 0x32, 0x33};
usbSerialPort.write(data , 2 * 1000);
// 读取
byte[] bytes = new byte[1024];
len = usbSerialPort.read(bytes, 500);

RN的原生模块封装分为三步:

  • 新建基础模块继承ReactContextBaseJavaModule,封装所有需要的功能,RN层拿到的原生模块即为此模块的实例。
  • 新建模块包,实现接口ReactPackage,将封装好的基础模块加入进去。
  • MainApplicationReactNativeHost实现中将模块包注册添加进去。

新建SerialPortModule.java,继承ReactContextBaseJavaModule原生java模块,将串口的相关操作封装为原生模块,如下:

package ink.labrador.rnserial.modules;

import android.content.Context;
import android.hardware.usb.UsbDeviceConnection;
import android.hardware.usb.UsbManager;
import android.os.CountDownTimer;
import android.widget.Toast;

import androidx.annotation.NonNull;

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.hoho.android.usbserial.driver.UsbSerialDriver;
import com.hoho.android.usbserial.driver.UsbSerialPort;
import com.hoho.android.usbserial.driver.UsbSerialProber;
import org.apache.commons.codec.binary.Hex;

import ink.labrador.rnserial.constant.SerialResponseCodeConstant;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;

public class SerialPortModule extends ReactContextBaseJavaModule {

    private final int READ_TIME_OUT_MILLS = 100; // 读超时
    private final int READ_DELAY_MILLS = 3 * 1000; // 读最多持续时间,超过改时间,即使没读完也不再读了
    private final int READ_BYTE_LEN = 1024; // 每次读取的最大字节数
    private UsbSerialPort usbSerialPort;
    private boolean connected = false;

    public SerialPortModule(ReactApplicationContext reactContext) {
        super(reactContext);

    }

    @ReactMethod
    public void connect(Integer baudRate, Integer dataBits, Integer stopBits, Integer parity, Promise promise) {
        // 获取到usb管理对象
        UsbManager usbManager = getUsbManager();
        // 第一个串口设备
        UsbSerialDriver firstDriver = getFirstSerialDevice();
        if (firstDriver == null) {
            promise.resolve(SerialResponseCodeConstant.NO_DEVICE);
            return;
        }
        // 打开设备连接
        UsbDeviceConnection connection = usbManager.openDevice(firstDriver.getDevice());
        if (connection == null) {
            promise.resolve(SerialResponseCodeConstant.CONNECT_FAILED);
            return;
        }
        // 设备的第一个端口
        this.usbSerialPort = firstDriver.getPorts().get(0);
        try {
            // 打开设备端口
            usbSerialPort.open(connection);
            // 设置波特率、数据位、停止位、校验位
            usbSerialPort.setParameters(baudRate, dataBits, stopBits, parity);
            connected =  true;
            promise.resolve(SerialResponseCodeConstant.OK);
        } catch (IOException e) {
            promise.resolve(SerialResponseCodeConstant.OPEN_FAILED);
        }
    }

    @ReactMethod
    public void close(Promise promise) {
        close();
        promise.resolve(SerialResponseCodeConstant.OK);
    }

    public void close() {
        try {
            if (usbSerialPort != null) {
                // 关闭连接
                usbSerialPort.close();
                connected = false;
                usbSerialPort = null;
            }
        } catch (IOException e) {
            connected = false;
            usbSerialPort = null;
        }
    }

    @NonNull
    @Override
    public String getName() {
        // 原生模块名称,RN层需要根据此名称获取到原生模块实例
        return "SerialPortModuleAndroid";
    }

    public UsbManager getUsbManager() {
        Context context = getReactApplicationContext();
        return (UsbManager) context.getSystemService(Context.USB_SERVICE);
    }

    public List<UsbSerialDriver> getAvailableDrivers(UsbManager manger) {
        return UsbSerialProber.getDefaultProber().findAllDrivers(manger);
    }

    @ReactMethod
    public void hasDevice(Promise promise) {
        promise.resolve(hasSerialDevice());
    }

    public boolean hasSerialDevice() {
        return getFirstSerialDevice() != null;
    }

    private UsbSerialDriver getFirstSerialDevice() {
        UsbManager usbManager = getUsbManager();
        List<UsbSerialDriver> availableDrivers = getAvailableDrivers(usbManager);
        if (availableDrivers.isEmpty()) {
            return null;
        }
        return availableDrivers.get(0);
    }

    @ReactMethod
    public void isConnected(Promise promise) {
        promise.resolve(isSerialDeviceConnected());
    }

    public boolean isSerialDeviceConnected() {
        return connected;
    }

    /**
    * 写数据并读取返回
    * 其中data是传输数据经过hex编码表示的字符串,返回的也是hex编码的字符串,
    * RN层与原生层都通过显式hex编码字符串传输比较方便
    */
    @ReactMethod
    public void sendAndReceive(String data, Promise promise) {
        if (this.usbSerialPort == null) {
            promise.resolve(SerialResponseCodeConstant.NO_DEVICE);
        }
        byte[] dataBytes;
        try {
            // 将hex编码数据还原为 字节数据
            dataBytes = Hex.decodeHex(data);
        } catch (Exception e) {
            promise.resolve(SerialResponseCodeConstant.PARSE_HEX_FAILED);
            return;
        }
        // 写数据
        if (!write(dataBytes)) {
            promise.resolve(SerialResponseCodeConstant.WRITE_FAILED);
            return;
        }
        // 启用一个计数器读取数据,每100毫秒读取一次
        new CountDownTimer(READ_DELAY_MILLS, 100) {
            final StringBuilder hex = new StringBuilder();
            boolean hasData = false; // 开始读取到数据了

            @Override
            public void onTick(long l) {
                byte[] bytes = new byte[READ_BYTE_LEN];
                int len = 0;
                try {
                    // len 即为实际读取到的数据长度
                    len = usbSerialPort.read(bytes, READ_TIME_OUT_MILLS);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (len > 0) {
                    hex.append(Hex.encodeHexString(Arrays.copyOf(bytes, len)));
                    hasData = true;
                } else if (hasData) {
                    // 从开始有数据读取到数据没了,说明读取完成,返回数据并取消后续执行
                    promise.resolve(hex.toString());
                    this.cancel();
                }
            }


            @Override
            public void onFinish() {
                // 最大等待时间内都没读取到任何数据或者依然有数据没读完,会走到最后
                byte[] bytes = new byte[READ_BYTE_LEN];
                try {
                    // 最后读一次
                    int len = usbSerialPort.read(bytes, READ_TIME_OUT_MILLS);
                    if (len > 0) {
                        hex.append(Hex.encodeHexString(Arrays.copyOf(bytes, len)));
                    }
                    promise.resolve(hex.toString());
                } catch (Exception e) {
                    promise.resolve(SerialResponseCodeConstant.READ_FAILED);
                }
            }
        }.start();
    }

    boolean write(byte[] bytes) {
        try {
            usbSerialPort.write(bytes, 2 * 1000);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    void clear() {
        if (!connected) {
            return;
        }
        byte[] bytes = new byte[READ_BYTE_LEN];
        try {
            usbSerialPort.read(bytes, 500);
        } catch (IOException ignored) {
        }
    }
}

其中SerialResponseCodeConstant是自定义的一些状态码:

package ink.labrador.rnserial.constant;

public class SerialResponseCodeConstant {
    public static final String OK = "0000"; // 成功
    public static final String NO_DEVICE = "0001"; // 没有插入串口设备
    public static final String CONNECT_FAILED = "0002"; // 连接设备失败
    public static final String OPEN_FAILED = "0003"; // 打开串口失败

    public static final String READ_FAILED = "0004"; // 读取数据失败
    public static final String WRITE_FAILED = "0005"; // 写数据失败

    public static final String CLOSE_FAILED = "0006"; // 关闭设备失败

    public static final String PARSE_HEX_FAILED = "0007"; // 转义hex字符到字节数据失败
}

然后新建SerialPortModulePackage.java,实现ReactPackage接口,将封装好的模块再封装到包列表中:

package ink.labrador.rnserialr.modules;

import androidx.annotation.NonNull;

import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SerialPortModulePackage implements ReactPackage {
    // 将模块对象拿出来作为类属性,是为了在外部可以获取到,因为在监听设备插入/拔出的广播时还需要用到模块的实例
    private SerialPortModule serialPortModule;

    @NonNull
    @Override
    public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
        List<NativeModule> modules = new ArrayList<>();
        SerialPortModule serialPortModule = new SerialPortModule(reactContext);
        this.serialPortModule = serialPortModule;
        modules.add(serialPortModule);
        return modules;
    }

    @NonNull
    @Override
    public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
        return Collections.emptyList();
    }

    public SerialPortModule getSerialPortModule() {
        return serialPortModule;
    }
}

最后在MainAppliance中找到ReactNativeHost的实现,将模块列表添加进去:

public class MainApplication extends Application implements ReactApplication {
    // 同样,将对象作为类属性是为了后面能拿到模块的实例
    private final SerialPortModulePackage serialPortModulePackage = new SerialPortModulePackage();

    private final ReactNativeHost mReactNativeHost =
            new ReactNativeHost(this) {
                @Override
                public boolean getUseDeveloperSupport() {
                    return BuildConfig.DEBUG;
                }

                @Override
                protected List<ReactPackage> getPackages() {
                    @SuppressWarnings("UnnecessaryLocalVariable")
                    List<ReactPackage> packages = new PackageList(this).getPackages();
                    // 加入到列表中
                    packages.add(serialPortModulePackage);
                    return packages;
                }

                @Override
                protected String getJSMainModuleName() {
                    return "index";
                }
           };
    ...
}

至此,原生模块的封装就完成了。

监听串口模块插入/拔出广播

在原生层监听串口模块的插入/拔出广播,在模块插入或拔出时即可获取到通知,然后再向RN层发送通知事件,这样RN层便可以监听该通知事件来实时监听模块的插入/拔出动作,可以进行消息提示。

广播的订阅在MainAppliance.java中的onCreate方法中进行,如下:

@Override
public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    initializeFlipper(this, getReactNativeHost().getReactInstanceManager());

    // 定义广播监听
    BroadcastReceiver receiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            ReactNativeHost reactNativeHost = getReactNativeHost();
            ReactContext reactContext = reactNativeHost.getReactInstanceManager().getCurrentReactContext();
            if (reactContext == null) {
                // 这里一定要判空
                return;
            }
            SerialPortModule serialPortModule = serialPortModulePackage.getSerialPortModule();
            if (serialPortModule == null) {
                return;
            }
            if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction())) {
                // usb设备插入,若此时有了串口设备,说明插入的是串口模块,向RN层发送事件
                if (serialPortModule.hasSerialDevice()) {
                    EventUtil.sendSerialActionEvent(reactContext, SerialActionConstants.ATTACHED);
                }
            } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
                // usb设备拔出,将串口设备关闭一下,向RN层发送事件
                if (serialPortModule.isSerialDeviceConnected()) {
                    serialPortModule.close();
                }
                EventUtil.sendSerialActionEvent(reactContext, SerialActionConstants.DETACHED);
            }
        }
    };

    // 注册广播
    IntentFilter filter = new IntentFilter();
    filter.addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED);
    filter.addAction(UsbManager.ACTION_USB_DEVICE_DETACHED);
    registerReceiver(receiver, filter);
}

其中SerialActionConstants是自定义的动作常量:

package ink.labrador.rnserial.constant;

public enum SerialActionConstants {
    ATTACHED("ATTACHED"),
    DETACHED("DETACHED");
    String action;
    SerialActionConstants(String action) {
        this.action = action;
    }
    public String value() {
        return action;
    }
}

EventUtil是向RN层发送事件的封装:

package ink.labrador.rnserial.util;

import ink.labrador.rnserialer.constant.SerialActionConstants;
import com.facebook.react.bridge.ReactContext;
import com.facebook.react.modules.core.DeviceEventManagerModule;

public class EventUtil {
    // 事件的名字,RN层监听该事件即可
    static final String SERIAL_ACTION_EVENT_NAME = "RNSerial:SERIAL_ACTION";

    public static void sendSerialActionEvent(ReactContext context, SerialActionConstants action) {
        context.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
                .emit(SERIAL_ACTION_EVENT_NAME, action.value());
    }
}

RN层调用

读写数据

因为用的是typescript,所以可以首先定义一个对应原生模块的类型声明(若是javacscript开发则没必要):

interface SerialPortNativeModule {
    isConnected(): Promise<boolean>;
    sendAndReceive(hexData: string): Promise<string>;
    close(): Promise<string>;
    connect(baudRate: number, dataBits: number, stopBits: number, parity: number): Promise<string>;
    hasDevice(): Promise<boolean>;
}

然后封装一个串口操作的工具类:

import { NativeModules } from "react-native";
import _ from "lodash";

import { SerialPortNativeModule } from "./types";

// 原生模块实例,引入依据的便是原生模块的名称,也即原生模块中getName方法返回的值
const serialNativeModule = NativeModules.SerialPortModuleAndroid as SerialPortNativeModule;

export default class SerialPortModule implements SerialPortNativeModule  {
    private constructor() {}
    private static instance: SerialPortModule;

    public static getInstance() {
        if (_.isNil(this.instance)) {
            this.instance = new SerialPortModule();
        }
        return this.instance;
    }

    async sendAndReceive(hexData: string): Promise<string> {
        return serialNativeModule.sendAndReceive(hexData);
    }

    async connect(baudRate: number, dataBits: number, stopBits: number, parity: number): Promise<string> {
        return serialNativeModule.connect(baudRate, dataBits, stopBits, parity);
    }

    async close(): Promise<string> {
        return serialNativeModule.close();
    }

    async hasDevice(): Promise<boolean> {
        return serialNativeModule.hasDevice();
    }

    async isConnected(): Promise<boolean> {
        return serialNativeModule.isConnected();
    }
}

然后在组件中便可以使用了:

import React, { useEffect, useState } from "react";
import { View, Text } from "react-native";
import SerialPortModule from "./native_modules/SerialPortModule";

const serialPortModule = SerialPortModule.getInstance();
export default function SerialDataReader() {
    const [baudRate, dataBits, stopBits, parity] = [9600, 8, 1, 2];
    const [data, setData] = useState("");

    const sendAndReadData = async () => {
        return serialPortModule.sendAndReceive("313233");
    };   

    useEffect(() => {
        serialPortModule.connect(baudRate, dataBits, stopBits, parity).then(async connected => {
            if (connected) {
                const result = sendAndReadData();
                setData(__ => result);
            } 
        });
    }, []);

    return <View>
        <Text>{data}<Text>
    </View>
}

监听设备拔插动作

直接监听原生层的事件即可:

import React, { useEffect } from "react";
import { DeviceEventEmitter } from "react-native";
import SerialPortModule from "./native_modules/SerialPortModule";

const serialPortModule = SerialPortModule.getInstance();
type SerialAction = 'ATTACHED' | 'DETACHED';
export default function SerialDataReader() {
    useEffect(() => {
        // 监听事件
        DeviceEventEmitter.addListener("RNSerial:SERIAL_ACTION", (action: SerialAction) => {
            switch (action) {
                case 'ATTACHED': {
                    console.log("检测到串口模块插入");
                    break;
                };
                case 'DETACHED': {
                    console.log("检测到串口模块拔出");
                    break;
                }
            }
        });
        return () => {
            DeviceEventEmitter.removeAllListeners("RNSerial:SERIAL_ACTION");
        }
    }, []);

    return <View></View>
}