用 Node.js 打造“硬核”IoT 网关:当 JavaScript 遇上 UART 串口与 Modbus 协议

Hi,我是前端人类学

在大多数人的刻板印象里,前端工程师似乎只会处理网页交互、对接 REST API。但在嵌入式全栈开发中,上位机软件和边缘计算网关同样是全栈能力的重要拼图。

很多嵌入式设备(如工业 PLC、智能电表)通过串口(UART)或 RS485 输出数据。常规做法是写一个 C/C++ 的网关程序,再转给云端。但在快速迭代的物联网项目中,Node.js 凭借其强大的异步 I/O 能力、庞大的 npm 生态和对 JSON 的原生支持,正成为构建轻量级 IoT 网关的利器。

本文将抛弃简单的 "Hello World" 演示,深入到一个实际场景:用 Node.js 编写一个高并发、防阻塞的串口网关,在 Node 环境下直接解析工业级 Modbus RTU 协议,并实时推送到 Web 前端进行监控。


文章目录

    • [一、工业数据采集与 Web 可视化](#一、工业数据采集与 Web 可视化)
    • [二、 关键实战:串口通信与任务队列优化](#二、 关键实战:串口通信与任务队列优化)
      • [2.1 设计可靠的串口数据缓冲器](#2.1 设计可靠的串口数据缓冲器)
      • [2.2 异步任务队列:解决并发冲突](#2.2 异步任务队列:解决并发冲突)
    • [三、 前后端联动:实时监控与闭环控制](#三、 前后端联动:实时监控与闭环控制)
      • [3.1 后端 WebSocket 推送](#3.1 后端 WebSocket 推送)
      • [3.2 前端动态可视化面板](#3.2 前端动态可视化面板)
    • [四、 工程化细节补充](#四、 工程化细节补充)

一、工业数据采集与 Web 可视化

1.1 业务场景定义:

我们需要对接一个工厂传感器网络。下位机通过 RS485 总线连接了 5 个温湿度传感器(采用 Modbus RTU 协议)。我们需要在 Linux 网关(如树莓派/香橙派)上:

  1. 轮询读取这 5 个传感器的数值。
  2. 将数据存入本地数据库并实时通过 WebSocket 推送给浏览器。
  3. Web 前端实现仪表盘可视化,并能下发控制指令(如调节某个参数阈值)。

1.2 核心难点:

  • 串口数据粘包与超时处理: 串口是流式的,一个 Modbus 报文可能会被切割成多个数据块到达,Node.js 的 serialport 库如果只简单监听 data 事件,极易出现解析错误。
  • 轮询调度与异步阻塞: 如果使用同步 await 串行读取 5 个设备,总耗时可能达到 500ms。如何利用 Node.js 单线程的异步非阻塞特性,在同时接收多个传感器回复时不乱序?
  • 硬件交互: 不能只在内网运行,还需要给 Web 前端提供实时数据推送。

二、 关键实战:串口通信与任务队列优化

2.1 设计可靠的串口数据缓冲器

串口数据具有流式特性,一个完整的 Modbus 帧可能会被拆分成多个数据块到达。如果直接在 data 事件中解析,极易产生错误。

处理逻辑:基于"静默期"来识别完整帧。

javascript 复制代码
const { SerialPort } = require('serialport');
const port = new SerialPort({ path: '/dev/ttyUSB0', baudRate: 9600 });

let rxBuffer = Buffer.alloc(0);
let lastDataTime = Date.now();

port.on('data', (chunk) => {
    rxBuffer = Buffer.concat([rxBuffer, chunk]);
    lastDataTime = Date.now();

    // 利用 Modbus RTU 3.5字符间隔特性断帧
    setImmediate(() => {
        if (Date.now() - lastDataTime > 20) { // 20ms内无新数据视为一帧结束
            handleModbusFrame(rxBuffer);
            rxBuffer = Buffer.alloc(0);
        }
    });
});

2.2 异步任务队列:解决并发冲突

Modbus RTU 协议要求主从通信必须串行处理,不能同时发送多条指令。若直接使用 await 串行读取,5 个设备耗时过长;若并发写入,则会引发总线冲突。

处理逻辑:使用任务队列将指令串行化,结合 setImmediate 保证高吞吐量。

javascript 复制代码
class ModbusGateway {
    constructor() {
        this.taskQueue = [];
        this.isProcessing = false;
        this.slaveIds = [1, 2, 3, 4, 5];
    }

    startPolling() {
        setInterval(() => {
            this.slaveIds.forEach(id => this.enqueueTask(id));
        }, 1000);
    }

    enqueueTask(slaveId) {
        const cmd = Buffer.from([slaveId, 0x03, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00]);
        this.taskQueue.push({ cmd, slaveId });
        this.processQueue();
    }

    async processQueue() {
        if (this.isProcessing || this.taskQueue.length === 0) return;
        this.isProcessing = true;

        const task = this.taskQueue.shift();
        try {
            const response = await this.sendSerialAndWait(task.cmd, 200);
            this.updateDataStore(task.slaveId, response);
        } catch (error) {
            console.error(`从机 ${task.slaveId} 通信失败`);
        } finally {
            this.isProcessing = false;
            setImmediate(() => this.processQueue()); // 非阻塞循环
        }
    }
}

三、 前后端联动:实时监控与闭环控制

3.1 后端 WebSocket 推送

数据解析完成后,需要实时触达前端。

javascript 复制代码
const socketIO = require('socket.io')(server);

// 每次完成数据解析后,触发广播
gateway.on('dataUpdated', (data) => {
    socketIO.emit('sensor-data', data);
});

3.2 前端动态可视化面板

前端通过监听 socket 事件更新仪表盘。同时,页面上的"重启设备"按钮触发的不仅是一个 API 调用,而是直接下发给硬件。

javascript 复制代码
// 前端 Vue 示例逻辑
socket.on('sensor-data', (data) => {
    this.temperature = data.temp;
    this.updateChart(data);
});

// 下发指令
handleReboot(slaveId) {
    socket.emit('control-command', { action: 'reboot', target: slaveId });
}

后端收到 control-command 后,将其转化为 Modbus 写寄存器指令(Function Code 0x06),通过串口发送,形成数据采集与反向控制的完整闭环。

四、 工程化细节补充

在正式项目中,还需在以下几个方面做好落地:

  • 校验与容错: 解析层必须加入 CRC16 校验,对校验失败的数据直接丢弃并触发重试,防止脏数据污染后端。
  • 进程守护: 使用 process.on('SIGINT', ...) 捕获退出信号,在 Node 进程结束前主动发送指令切断传感器通信,确保硬件安全。
  • 重连机制: 前端建立与 Socket.io 的状态机管理,处理 WebSocket 意外掉线后的自动重连。

本文展示了一个基于 JavaScript 全栈的工业数据采集方案。通过 Node.js 的异步非阻塞特性,配合合理的任务队列设计和字节缓冲区处理,能够稳定地处理底层串口协议,并通过 WebSocket 将数据实时推送到前端,打通了从硬件底层到 Web 展示层的完整数据链路。