最近有个需求是用普通android
手机可以与串口设备通信,读取数据,考虑到前端实现,整体需要用ReactNative
混合开发实现,因此,就需要对串口通信部分进行原生封装,封装为原生模块供RN
前端调用。Android
与串口设备通信,外接一个OTG
转串口模块即可,这里串口协议为RS485
,因此淘宝买了OTG
(typec
口)转RS485/232
模块,如下:
整体串口通信实现示意如下:
若不是485
协议,如TTL
或232
等,选择相应的转换模块即可,如OTG
转TTL
或OTG
转232
,有的转换模块也同时可以支持多种协议,底层原理都是相同的。
原生层封装
串口通信原生模块封装
配置串口库
首先在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
,将封装好的基础模块加入进去。 - 在
MainApplication
的ReactNativeHost
实现中将模块包注册添加进去。
新建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>
}