前端串口serialport.js串口通信库快速入门(附经验总结)
公司项目需要开发一个windows客户端,提供串口modbusRTU数据读取、处理、显示和控制功能。
项目使用了electron 31.0.2
版本,串口通信库使用了serialport 12.0.0
版本。
- serialport.js官方文档地址:https://serialport.io/docs/
- npm包地址:https://www.npmjs.com/package/serialport
- github项目地址:https://github.com/serialport/node-serialport
一、serialport简介
serialport.js
是一个用于 Node.js
的串口通信库,允许开发者通过 JavaScript 与串口设备(如 Arduino、传感器、GPS 模块等)进行通信。它提供了一个简单且强大的 API,用于打开、配置、读取和写入串口数据。serialport.js
是开源的,基于 MIT
许可证,广泛应用于物联网(IoT)、嵌入式系统和硬件开发项目中。
重点,serialport.js
必须运行与Node.js
环境,在浏览器环境是无法使用的,所以一般结合electron.js
使用,serialport.js
在electron的主进程中调用并使用。
1.1 安装
可以通过 npm 安装 serialport.js(cnpm、pnpm、yarn都是一样的)
现在默认安装的应该是12.0.0版本
npm i serialport
1.2 基本用法
import { SerialPort } from 'serialport';
// 打开串口
const port = new SerialPort('/dev/ttyUSB0', {
baudRate: 9600, // 设置波特率
dataBits: 8, // 数据位
parity: 'none', // 校验位
stopBits: 1, // 停止位
autoOpen: false // 不自动打开
});
// 打开串口时触发
port.open((err) => {
if (err) {
return console.error('Error opening port:', err.message);
}
console.log('Port opened successfully');
});
// 监听数据事件
port.on('data', (data) => {
console.log('Received data:', data.toString());
});
// 监听错误事件
port.on('error', (err) => {
console.error('Error:', err.message);
});
// 关闭串口
setTimeout(() => {
port.close((err) => {
if (err) {
return console.error('Error closing port:', err.message);
}
console.log('Port closed successfully');
});
}, 10000); // 10秒后关闭串口
1.3 完整示例代码
/*
* @Description: 串口通信相关自定义接口
* @Date: 2024-02-02 15:05:44
* @FilePath: \demod:\03code\04electron\information-panel\src\main\serialport.ts
*/
import { BrowserWindow, ipcMain, Notification } from 'electron';
import { SerialPort } from 'serialport';
import { InterByteTimeoutParser } from '@serialport/parser-inter-byte-timeout';
let serialPortConnection: any = null; // 串口连接
// 获取串口列表
const getSerialPortList: any = async () => {
const list = await SerialPort.list();
console.log('获取串口列表', list);
return list;
};
// 创建串口连接
const creatSerialPortConnection: any = async (portName: string) => {
console.log('创建串口连接-串口名:' + portName);
serialPortConnection = new SerialPort({
path: portName,
baudRate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
autoOpen: false,
});
// 重要:添加解析器。串口返回的数据可能会被分包成多个包,需要解析器来处理。这里是在指定时间内未收到任何字节或达到最大缓冲区大小后发出数据。
const parser = serialPortConnection.pipe(new InterByteTimeoutParser({ interval: 300 }));
return new Promise((resolve, reject) => {
serialPortConnection.open((err) => {
if (err) {
console.log('---创建串口连接失败: ' + err.message + '\n');
parser.close(); // 关闭连接
reject(err);
}
parser.on('error', (err) => {
console.log('1发生错误: ' + err.message + '\n');
});
parser.on('data', (data) => {
const hexString = Array.from(data)
.map((byte: any) => byte.toString(16).padStart(2, '0'))
.join(' ');
console.log('Hex string:', hexString);
console.log('2主进程串口收到的原始数据: ' + data + '\n');
// 将接收到的数据发送到渲染进程
const win = BrowserWindow.getAllWindows()[0];
const webContents = win.webContents;
webContents.send('serialport:sendData:reply', data);
});
console.log('---串口连接成功' + '\n');
resolve(portName);
});
});
};
// 关闭串口连接
const closeSerialPortConnection = () => {
if (serialPortConnection) {
serialPortConnection.close();
}
};
// 向指定端口发送数据
const sendSerialPortData = async (portName: string, data: string) => {
console.log('串口名:' + portName);
console.log('发送数据:' + data);
try {
serialPortConnection.write(data);
} catch (err: any) {
console.log('3发送数据失败: ' + err.message + '\n');
}
};
// 获取串口列表
ipcMain.on('serialport:getSerialPortList', async (event, _data) => {
console.log('前端发送来的数据_data: ', _data);
const result = await getSerialPortList();
// 将数据发送回渲染进程
event.reply('serialport:getSerialPortList:reply', result);
});
// 创建串口连接
ipcMain.on('serialport:creatSerialPortConnection', async (event, data) => {
creatSerialPortConnection(data)
.then((res) => {
console.log('res: ', res);
event.reply('serialport:creatSerialPortConnection:reply', res);
sysNotice('创建串口连接成功', '创建串口:' + res + '连接成功');
})
.catch((err: any) => {
console.log('err: ', err);
event.reply('serialport:creatSerialPortConnection:reply', err);
sysNotice('创建串口连接失败', err);
});
});
// 关闭串口连接
ipcMain.on('serialport:closeSerialPortConnection', () => {
closeSerialPortConnection();
});
// 发送串口数据
ipcMain.on('serialport:sendData', (_event, data) => {
sendSerialPortData(data.path, data.data);
});
function sysNotice(title, body) {
new Promise((ok, fail) => {
if (!Notification.isSupported()) fail('当前系统不支持通知');
const ps = typeof title == 'object' ? title : { title, body };
const n = new Notification(ps);
n.on('click', ok);
n.show();
});
}
export { creatSerialPortConnection, closeSerialPortConnection, getSerialPortList, sendSerialPortData };
二、问题
2.1 数据包被拆分(已解决)
前端使用serialport.js监听串口的数据为啥会被拆分成多个?
例如串口发送01 03 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 E4 59
,
前端serialPortConnection.on(‘data’, (data) => {console.log(data)})会打印两次,
第一次返回01 03 10 00 00 00 00
第二次返回00 00 00 00 00 00 00 00 00 00 00 00 e4 59
。
实际应该on的串口监听值返回一次完整的数据,但是这里被拆分成了两个,并且被监听了两次。
原因:串口数据传输是基于字节流的。串口通信的速度(波特率)和设备缓冲区大小等因素会影响数据的发送和接收。当发送的数据量较大或者接收端处理速度跟不上发送速度时,数据可能会被拆分成多个部分进行传输。
解决:serialport.js
提供了专用的解析器,用于获取原始二进制数据(或Uint8Array)并将其转换为可用的消息。
一般常用的有
DelimiterParser
分隔符解析器,收到字节序列时则发出数据的转换流,完成一次监听。InterByteTimeoutParser
时间限制解析器,转换流会缓冲数据,并在指定时间内未收到任何字节或达到最大缓冲区大小后发出数据。
简单示例:
const { SerialPort } = require('serialport')
const { InterByteTimeoutParser } = require('@serialport/parser-inter-byte-timeout')
const port = new SerialPort({ path: '/dev/ROBOT', baudRate: 14400 })
const parser = port.pipe(new InterByteTimeoutParser({ interval: 30 }))
parser.on('data', console.log) // will emit data if there is a pause between packets of at least 30ms
其他的可以看官网文档:
2.2 串口返回的多种数据,如何区分类别(待解决)
例如下方发送的两类数据,串口返回了两种数据:
-
发送1:
01 03 00 20 00 08 45 C6
-
接收1:
01 03 10 00 01 00 01 00 00 00 00 00 00 00 00 00 00 00 00 27 D8
-
发送2:
01 03 00 70 00 10 45 DD
-
接收2:
01 03 20 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 12 00 33 01 40 01 F4 00 00 00 00 00 00 00 00 00 00 C9 FB
返回的数据要在不同的组件中使用,现在改如何解决?
- 未完待续…
- 后续有啥经验我会继续更新的!