串口通信:工业场景下的串口读写操作(91)
在鸿蒙(HarmonyOS)工业场景下的串口通信开发中,系统提供了完善的 serialManager 模块(USB Serial Communication Management)来支持主机与串口设备(如 RS-232、RS-485 等)之间的数据传输。
一、 基础通信流程:权限、打开与数据读写
在工业场景中,标准的串口通信需要严格遵循“获取设备列表 -> 申请权限 -> 打开串口 -> 数据读写”的流程。系统同时提供了异步(Promise)和同步两种读写方式。
核心代码示例:
import { serialManager } from '@kit.BasicServicesKit';
import { buffer } from '@kit.ArkTS';
async function startSerialCommunication() {
// 1. 获取主机连接的 USB 串口设备列表
let portList: serialManager.SerialPort[] = serialManager.getPortList();
if (portList === undefined || portList.length === 0) {
console.error('未检测到串口设备');
return;
}
let portId: number = portList[0].portId;
// 2. 检查并请求串口设备的访问权限
if (!serialManager.hasSerialRight(portId)) {
let result = await serialManager.requestSerialRight(portId);
if (!result) {
console.error('用户拒绝授权串口访问');
return;
}
}
// 3. 打开串口设备
try {
serialManager.open(portId);
console.info(`串口打开成功,端口ID: ${portId}`);
} catch (error) {
console.error(`串口打开失败: ${error}`);
return;
}
// 4. 异步读取数据
let readBuffer: Uint8Array = new Uint8Array(64);
serialManager.read(portId, readBuffer, 2000).then((size: number) => {
console.info(`成功读取 ${size} 字节数据`);
}).catch((error: Error) => {
console.error(`读取数据异常: ${error}`);
});
// 5. 异步写入数据(向工业设备发送指令)
let writeBuffer: Uint8Array = new Uint8Array(buffer.from('Hello Industrial Device', 'utf-8').buffer);
serialManager.write(portId, writeBuffer, 2000).then((size: number) => {
console.info(`成功写入 ${size} 字节数据`);
}).catch((error: Error) => {
console.error(`写入数据异常: ${error}`);
});
}
二、 协议配置:波特率与数据帧格式
工业现场设备(如传感器、PLC、读卡器)对通信协议参数有严格要求。开发者可通过 getAttribute 和 setAttribute 接口动态配置波特率、数据位、校验位和停止位。
核心代码示例:
// 获取当前串口配置
let attribute: serialManager.SerialAttribute = serialManager.getAttribute(portId);
// 修改配置:例如设置为 115200 波特率,8数据位,无校验,1停止位
attribute.baudRate = 115200;
attribute.dataBits = 8;
attribute.parity = serialManager.ParityType.PARITY_NONE;
attribute.stopBits = 1;
// 应用新配置
try {
serialManager.setAttribute(portId, attribute);
console.info('串口参数配置成功');
} catch (error) {
console.error('串口参数配置失败:', error);
}
三、 进阶架构:非标设备的扩展驱动开发(USB Serial DDK)
在工业场景中,常会遇到一些老旧或定制的非标串口设备(如特殊温湿度计、定制身份读卡器)。如果系统没有适配驱动,开发者可以使用 USB Serial DDK(Driver Develop Kit) 在应用层开发专属的 USB 串口驱动。
架构原理:
非标外设应用通过扩展外设管理服务获取设备 ID,通过 RPC 将操作下发给 USB 串口驱动应用(DriverExtensionAbility)。驱动应用调用 DDK 接口设置串口属性并进行读写,DDK 再通过 HDI 服务将指令下发至内核驱动与设备通信。
Native 层核心接口(C++):
// 1. 初始化 USB Serial DDK
OH_UsbSerial_Init();
// 2. 打开指定的 USB 串口设备
UsbSerial_Device *dev = nullptr;
OH_UsbSerial_Open(deviceId, interfaceIndex, &dev);
// 3. 设置波特率等属性
OH_UsbSerial_SetBaudRate(dev, 9600);
// 4. 读写数据
uint8_t buff[128];
uint32_t bytesRead = 0;
OH_UsbSerial_Read(dev, buff, sizeof(buff), &bytesRead);
// 5. 使用完毕后关闭设备,防止内存泄漏
OH_UsbSerial_Close(&dev);
OH_UsbSerial_Release();
- 生命周期管理:在使用 DDK 接口时,务必在
DriverExtensionAbility的生命周期内调用,并在设备使用完后严格调用close接口,否则会造成底层内存泄漏。 - 权限声明:使用 USB Serial DDK 开放 API 开发扩展驱动时,必须在
module.json5中声明匹配的 ACL 权限,例如ohos.permission.ACCESS_DDK_USB_SERIAL。 - 默认参数注意:在未显式配置串口参数时,系统默认的波特率为 9600bps、8位数据位、无校验位和1位停止位。对接工业设备前务必确认参数匹配,否则会导致数据解析乱码。
- 同步与异步选择:在工业高频数据采集场景中,若需要保证数据帧的绝对顺序且不阻塞主线程,建议结合鸿蒙的 FFRT(Function Flow Runtime)串行队列进行异步读写任务的调度。
四、 跨平台架构:Flutter 鸿蒙串口通信桥接
对于使用 Flutter 构建鸿蒙工业 HMI 的团队,Dart 层无法直接访问底层硬件。必须借助鸿蒙的 Platform Channel 机制,在 ArkTS/Native 层拦截权限并完成串口操作,再通过事件通道(EventChannel)将数据流推送到 Dart 层。
核心代码示例(ArkTS 侧):
// 1. 拦截并授予串口权限(解决鸿蒙 Webview/Flutter 容器的安全限制)
methodChannel.on('requestSerialPort', async () => {
let portId = portList[0].portId;
if (!serialManager.hasSerialRight(portId)) {
await serialManager.requestSerialRight(portId);
}
methodChannel.send('serialGranted', true);
});
// 2. 建立事件通道,将底层硬件数据主动推送到 Flutter UI 层
eventChannel.onListen(() => {
// 监听串口数据接收
serialManager.read(portId, readBuffer, 2000).then((size: number) => {
// 将原生的 ArrayBuffer 转换为 Hex 字符串或 Base64 回传给 Dart 端
const strData = bufferToHex(readBuffer);
eventChannel.send(strData);
});
});
五、 工业协议集成:Modbus RTU/TCP 的鸿蒙化适配
在工业场景中,串口通常仅作为物理传输层,上层往往运行着 Modbus 等标准协议。开发者可以通过引入纯 Dart 逻辑库(如 modbus_client),结合鸿蒙原生串口驱动,快速构建工业网关。
核心代码示例(Dart 侧):
import 'package:modbus_client/modbus_client.dart';
// 定义工业数据模型(将寄存器直接映射为类型安全的对象)
final temperatureSensor = ModbusNumericElement(
name: 'MachineTemp',
address: 0x0001, // Modbus 寄存器地址
type: NumericType.uint16,
scaleFactor: 0.1 // 缩放因子:原始值 / 10 = 实际温度
);
// 在鸿蒙端发起 Modbus RTU 读取请求
Future<void> readIndustrialData() async {
// 结合鸿蒙底层串口构建 Modbus RTU 客户端
final client = ModbusClientRtu(serialPort: ohosSerialPortInstance);
final result = await client.read(temperatureSensor);
print('当前设备温度: ${result?.value} ℃');
}
六、 性能优化:高频数据流的非阻塞与降采样
工业传感器(如高频振动传感器)的回传数据量极大。如果将全量数据直接透传给 UI 层,会导致严重的内存抖动和界面卡顿。必须在原生层进行“边缘计算”或“降采样”。
核心架构建议:
- Dart Stream 异步消费:在 Dart 侧使用
port.readable.stream.listen()进行非阻塞式的高频数据交互,避免同步读取阻塞主线程。 - 原生层数据聚合:在 ArkTS 或 C++ 层设置数据缓冲区,例如每累积 100 个采样点,计算出一个平均值或峰值后,再触发一次跨线程通信传递给 Flutter 绘制波形图。
七、 稳定性保障:物理链路的断连重连机制
鸿蒙手持设备在工业现场移动时,USB 连接器极易因震动发生物理断开。应用必须具备静默恢复通信的能力。
核心代码示例:
// 实现心跳包与重连轮询机制
async function maintainSerialConnection() {
while (isAppRunning) {
try {
// 1. 轮询检查已授权的端口是否依然有效
let currentPorts = serialManager.getPortList();
let isConnected = currentPorts.some(p => p.portId === targetPortId);
if (!isConnected) {
console.warn('检测到串口物理断开,正在尝试重新连接...');
// 2. 触发重连逻辑并重新配置波特率等参数
await reconnectAndConfigure();
}
} catch (error) {
console.error('心跳检测异常:', error);
}
// 3. 设置合理的轮询间隔(如 1000ms),避免过度消耗 CPU
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
- WebView 权限拦截(Critical):在鸿蒙系统中,默认的 Webview 容器出于安全考虑会禁用
serialAPI。开发者必须在鸿蒙 Native 层拦截onPermissionRequest事件,并显式授予ohos.permission.SERIAL_PORT权限,否则串口请求将静默失效。 - 读写超时控制:工业现场电磁环境复杂,极易发生丢包。在调用
serialManager.read时,务必设置合理的超时时间(如 2000ms),并在catch块中做好超时重试机制,防止进程无限挂起。 - 资源释放:在组件销毁(
aboutToDisappear)或应用退至后台时,必须严格调用serialManager.close(portId)关闭串口,释放底层硬件文件描述符,防止下次启动时提示“设备被占用”。 - 跨平台一致性:在 Flutter 鸿蒙混合开发中,遵循“让鸿蒙做硬事(底层串口驱动、权限管理),让 Flutter 做软事(UI 渲染、Modbus 业务逻辑)”的核心心法,能最大化发挥鸿蒙分布式硬件的优势。
1、 WebView 权限拦截(Critical):显式授予串口权限
在鸿蒙的 ArkWeb 容器中,出于安全沙箱限制,Web 页面发起的硬件请求默认会被拦截。必须在 ArkTS 侧拦截 onPermissionRequest 事件,并显式授予 ohos.permission.SERIAL_PORT 权限。
核心代码示例:
import { webview } from '@kit.ArkWeb';
// 在 Web 组件的控制器中拦截权限请求
let webController = new webview.WebviewController();
// 监听 Web 页面发起的权限请求
webController.on('onPermissionRequest', (event: webview.PermissionRequestEvent) => {
// 检查请求的权限是否为串口访问权限
if (event.permissionList.includes('ohos.permission.SERIAL_PORT')) {
// 显式授予串口权限
event.grant(event.permissionList);
console.info('已显式授予 Web 组件串口访问权限');
} else {
// 拒绝其他未授权的权限
event.reject();
}
});
2、 读写超时控制:异步读取与异常重试机制
工业现场电磁干扰严重,极易发生数据丢包或响应延迟。必须为 serialManager.read 设置合理的超时时间,并在 catch 块中捕获超时错误码(如 31400006),触发重试机制。
核心代码示例:
import { serialManager } from '@kit.BasicServicesKit';
async function safeReadWithRetry(portId: number, maxRetries: number = 3): Promise<Uint8Array | null> {
let buffer = new Uint8Array(64);
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// 设置 2000ms 超时时间,防止底层 I/O 无限挂起
let size = await serialManager.read(portId, buffer, 2000);
if (size > 0) {
return buffer.subarray(0, size); // 返回有效数据切片
}
} catch (error) {
// 捕获超时错误码 (31400006) 或 I/O 异常 (31400007)
if (error.code === 31400006 || error.code === 31400007) {
console.warn(`第 ${attempt} 次读取超时/异常,准备重试...`);
if (attempt === maxRetries) {
console.error('达到最大重试次数,放弃本次读取');
return null;
}
// 简单的退避延迟,避免连续重试压垮底层驱动
await new Promise(resolve => setTimeout(resolve, 500));
} else {
throw error; // 抛出非超时类的致命错误
}
}
}
return null;
}
3、 资源释放:生命周期绑定与防占用机制
硬件文件描述符(FD)是极其宝贵的系统资源。必须在 UI 组件销毁(aboutToDisappear)或应用退至后台时,严格调用 close 接口,防止设备被永久锁死。
核心代码示例:
import { serialManager } from '@kit.BasicServicesKit';
@Entry
@Component
struct SerialMonitorPage {
private portId: number = -1;
aboutToAppear() {
// 初始化并打开串口
this.initSerialPort();
}
// 核心:在组件销毁时强制释放硬件资源
aboutToDisappear() {
if (this.portId !== -1) {
try {
serialManager.close(this.portId);
console.info(`串口 ${this.portId} 已安全关闭,释放底层文件描述符`);
} catch (error) {
console.error(`关闭串口异常: ${error}`);
}
}
}
private async initSerialPort() {
let portList = serialManager.getPortList();
if (portList.length > 0) {
this.portId = portList[0].portId;
if (!serialManager.hasSerialRight(this.portId)) {
await serialManager.requestSerialRight(this.portId);
}
serialManager.open(this.portId);
}
}
build() {
// UI 布局...
Column() {}
}
}
4、 跨平台一致性:Flutter 与鸿蒙的软硬分工架构
在 Flutter for OpenHarmony 架构下,遵循“鸿蒙做硬事,Flutter做软事”的原则。ArkTS/Native 层负责底层驱动和权限,Flutter 层专注 Modbus 协议解析和 UI 渲染。
核心代码示例(Dart 侧):
import 'package:flutter/services.dart';
import 'package:modbus_client/modbus_client.dart';
class OhosIndustrialBridge {
static const _channel = MethodChannel('com.example/serial_bridge');
// 1. 通过 Channel 通知鸿蒙 Native 层初始化并打开串口
static Future<void> initHardware() async {
await _channel.invokeMethod('initSerialPort');
}
// 2. 监听底层推上来的原始数据流(EventChannel)
static Stream<Uint8List> get rawSerialStream {
const eventChannel = EventChannel('com.example/serial_stream');
return eventChannel.receiveBroadcastStream().map((data) => Uint8List.fromList(data));
}
// 3. 在 Dart 层处理 Modbus 协议(软事)
static void processModbusData(Uint8List rawData) {
// 将底层透传的字节流交给 Modbus 解析器
// 例如:ModbusClientRtu.parse(rawData);
print('收到工业数据帧,长度: ${rawData.length}');
}
}
核心代码示例(ArkTS 侧 - 对应上述 Dart 的硬件事件):
// 在鸿蒙侧的 MethodChannel 处理中
methodChannel.on('initSerialPort', async () => {
let portList = serialManager.getPortList();
if (portList.length > 0) {
let portId = portList[0].portId;
if (!serialManager.hasSerialRight(portId)) {
await serialManager.requestSerialRight(portId);
}
serialManager.open(portId);
// 启动后台轮询,将数据通过 EventChannel 推送给 Flutter
startBackgroundReadLoop(portId);
}
});更多推荐



所有评论(0)