Node.js 对接 Modbus RTU:node-modbusrtu 完整实操指南 简介node-modbusrtu 是一个基于 Node.js 的通信模块专为需要与支持 Modbus RTU 协议的工业设备进行数据交互的开发者设计可运行于 GNU/Linux 环境核心实现全部采用原生 JavaScript无需额外编译。模块目前已覆盖 Modbus RTU 主站的常用功能码包括读取线圈、读取离散输入、读写保持寄存器、读取输入寄存器、写单线圈、写单个寄存器、写多个线圈与写多个寄存器可满足 PLC、传感器、智能电表等设备的数据读写需求适用于工业自动化、物联网数据采集等实际项目。资源包以 zip 格式提供压缩后仅 8KB共包含 9 个文件其中有 4 个 JavaScript 源码文件、1 份 JSON 配置文件、1 份 Markdown 说明文档以及许可证等。模块内部将 CRC16 校验、串口通信、Modbus 协议处理与测试拆分为独立部分结构清晰、依赖极少既方便直接嵌入到 Node.js 项目中也适合作为 Modbus 协议实现的参考学习素材。目前该资源已有 1698 人学习使用适合正在寻找 Node.js 串口通信方案的工程师也适合希望深入理解工业协议的开发者。通过该资源读者可以拿到可直接运行的 Modbus RTU 通信代码包括 CRC16 校验、串口收发层、功能码封装与响应解析等配合测试文件和说明文档能够快速掌握安装调用方法并可对照真实设备调试排错为后续二次开发和工程落地提供便利。 工业现场跑过的人都知道Modbus RTU 是绕不开的一个协议。仪表、PLC、变频器、温控器基本上你能想到的 RS485 设备都会给你留一个 Modbus RTU 接口。我之前一直用传统上位机方案去对接这些设备直到在一次能耗采集项目里试了 node-modbusrtu在 NodeJS 环境里直接和现场设备通信才真正意识到这条技术路线的效率优势。这篇文章就围绕这个模块展开把从环境准备、核心 API、完整实操到问题排查的一整套流程讲透适合要快速接入 Modbus RTU 设备的 Node.js 开发者也适合原本用 LabVIEW、C# 写上位机、想试试轻量方案的朋友。1. 先说清楚Modbus RTU 通信到底在做什么1.1 一主多从、一问一答的帧结构Modbus RTU 是典型的主从架构一个主机带上多个从站每个从站有唯一地址1 到 247。主机发出请求帧从站应答主机不发从站不说话。整个过程可以用一个场景类比主机像老师点名从站像学生只有听到自己学号才回答而且回答必须当场完成不能拖到下节课。RTU 的报文格式非常紧凑从站地址1 字节、功能码1 字节、数据区N 字节、CRC16 校验2 字节。比如主机要读从站 1 的保持寄存器起始地址 0读 10 个寄存器原始请求帧就是01 03 00 00 00 0A C5 CD最后两个字节是 CRC 校验。我在代码里调 node-modbusrtu 的时候根本不用手拼这些字节但理解这个结构有实际意义排查问题时你迟早要跟原始报文打交道。CRC16 校验是 RTU 和 ASCII 模式的重要区别。RTU 用二进制帧加 CRC 保证数据完整性如果通信线上有干扰导致某个字节变了CRC 对不上接收方就知道这帧是坏的直接丢弃。这也是为什么 Modbus RTU 在工业现场这么能活——抗干扰能力是靠协议层扎扎实实撑起来的。1.2 功能码决定了你能干什么功能码是每一帧请求的核心它告诉从站你要干什么。平时项目里最常用的就这几个01读线圈状态读开关量输出02读离散输入读开关量输入03读保持寄存器读可读可写的寄存器04读输入寄存器读只读的寄存器05写单个线圈06写单个寄存器160x10写多个寄存器绝大多数仪表、传感器的数据都通过 03 和 04 暴露出来。比如一个温湿度变送器温度值放在保持寄存器地址 0湿度值放在地址 1你只要用readHoldingRegisters(1, 0, 2)就能把两个值一起读回来。03 和 04 的区别要留意保持寄存器一般是可写的有可能误操作输入寄存器是只读的拿来存传感器测量值更常见。选错功能码设备会回一个异常码最常见的异常码是 02非法数据地址翻译过来就是你问的寄存器我没这个地址。1.3 为什么我用 Node.js 而不是传统上位机在 Node.js 生态还不成熟的时候大家都在用 C#、LabVIEW、Python 写 Modbus 上位机。LabVIEW 做界面和波形显示确实快但一旦涉及业务逻辑、数据库同步、对接 Web 平台就会变得很别扭。C# 功能强但开发环境重、部署链路过长一个小采集工具也要装 .NET 运行时。Node.js 的优势在于异步 I/O 和事件驱动。串口这种慢速设备天然适合用回调、Promise、async/await 的方式来处理——发一个请求挂一个回调数据回来就触发不用像同步语言那样死等或者开线程。另外node-modbusrtu 这类模块把 Modbus 协议栈整个封装好了你不需要关心 CRC 计算、超时重试这些底层细节。再加上 npm 生态里 serialport 这种底层串口库已经相当稳定Node.js 采集完数据可以直接用 Express 或 WebSocket 把数据送到前端前后端同一种语言开发效率确实高。我实测下来从零搭一个能稳定运行的数据采集服务半天时间足够。这个速度在传统上位机方案里很难想象。2. 环境准备Node.js 安装与 npm 常见坑2.1 安装 Node.js 与环境变量配置技术选型定了接下来的问题就是环境。Node.js 安装本身不难但有几个细节会影响后面的开发体验。我的建议是去官网下载 LTS长期支持版本Windows 用 msi 安装包一路 Next 就行。msi 安装包会自动把 Node.js 的路径写进系统环境变量装完打开新终端输入node -v能看到版本号就说明装好了。有些朋友喜欢下载 zip 压缩包手动解压这样做的好处是不用装系统服务、可以随意换版本但需要自己配置环境变量。操作也不复杂把解压目录比如D:\nodejs设成NODE_HOME然后在系统PATH里加上%NODE_HOME%和%NODE_HOME%\node_modules。配置完一定要重开终端让环境变量生效。我见过太多人改完环境变量不重开终端直接执行命令报错还以为自己改错了。顺便提一句如果电脑里装了 nvm-windowsNode 版本管理器可以直接用 nvm 切换 Node 版本。做 Modbus 采集这种长期跑的服务我一般都锁定在 LTS 版本上省得 Node 版本升级后某些原生模块需要重新编译。2.2 搞定 npm.ps1 禁止运行脚本的问题搜索NPM 无法加载文件的人是真的多各种路径的npm.ps1报错刷屏npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。这个问题的本质不是 Node.js 坏了而是 PowerShell 的执行策略默认禁止运行.ps1脚本。Windows 下的 PowerShell 默认执行策略是Restricted也就是说任何脚本都不允许执行。npm 的命令行入口是个.ps1文件所以 PowerShell 直接给你拦了。解决办法很简单以管理员身份打开 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserRemoteSigned的意思是本地创建的脚本可以运行从网上下载的脚本必须经过签名。这个策略足够日常开发使用又不至于完全放开限制算是安全和便利的平衡点。如果你不想改执行策略还有一个临时办法直接用 CMD命令提示符而不是 PowerShellCMD 里执行 npm 命令不会触发 PowerShell 的脚本策略。另外在 VS Code、Cursor 这类编辑器的终端里碰到这个报错只是因为你用的终端是 PowerShell处理方式完全一样。我推荐还是执行上面的策略修改一劳永逸。2.3 npm 镜像源、项目初始化与串口访问权限Node.js 装好之后建议先把 npm 镜像源换成国内镜像不然后面安装 node-modbusrtu、serialport 这些包的时候下载速度会让你怀疑人生。npm config set registry https://registry.npmmirror.com npm config get registry执行完npm config get registry能看到https://registry.npmmirror.com/说明镜像源生效了。接着初始化项目npm init -y npm install node-modbusrtu serialport这里单独说下 serialportnode-modbusrtu 的底层串口通信依赖 serialport安装时会下载预编译的二进制文件。如果安装过程报错大概率是网络问题镜像源配好之后基本能解决。如果你在 Linux 或者 macOS 上开发操作串口设备需要权限。Linux 下要把当前用户加进dialout用户组然后重新登录sudo usermod -a -G dialout $USERmacOS 一般不需要额外配置Windows 上用设备管理器确认串口号就行。我见过不少人在 Linux 上开发代码写完了发现打不开串口其实是忘了加用户组。3. 核心 API 解析串口参数与读写方法3.1 串口参数不是配置完就完事的四个数字所有 Modbus RTU 通信的第一步是打开一个串口。串口参数有四个波特率baudRate、数据位dataBits、停止位stopBits、校验位parity。这四个参数必须跟从站设备的设置完全一致否则你会收到一堆乱码或者干脆收不到任何数据。最常见的组合是9600, 8, 1, none也就是波特率 9600、数据位 8、停止位 1、无校验工业上简写为 8N1。但这不是绝对的有些进口仪表默认 19200有些 PLC 走 7E17 个数据位、偶校验配置之前一定要翻设备手册或者看设备的拨码开关。为什么参数不一致会导致通信失败因为串口是按位收发数据的发送方用规定的时间间隔逐位发出接收方用同样时间间隔采样。波特率不同等于双方说话速度不一致数据位、停止位、校验位不同等于双方对一句话怎么切分的标准不一样自然没法正确拼出报文。3.2 六个核心方法连接、读、写node-modbusrtu 的 API 设计得很直观。不同版本的模块方法签名可能会有细微差异但思路一致我以目前比较常见的一套写法来说明const ModbusRTU require(node-modbusrtu); const client new ModbusRTU({ port: COM3, // Windows 串口号 // port: /dev/ttyUSB0, // Linux 下通常是这个 baudRate: 9600, dataBits: 8, stopBits: 1, parity: none, }); client.connect() .then(() { // 读从站 1 的保持寄存器起始地址 0读 2 个寄存器 return client.readHoldingRegisters(1, 0, 2); }) .then((data) { console.log(data); // 例如 [250, 320] }) .catch((err) { console.error(读取失败:, err.message); });connect()打开串口并准备好通信链路返回 Promise。readHoldingRegisters(slaveId, address, length)的参数分别是从站地址、寄存器起始地址、寄存器数量返回值是寄存器值数组。写操作同样简单// 写单个寄存器从站 1地址 0写入 100 client.writeSingleRegister(1, 0, 100) .then(() console.log(写入成功)) .catch((err) console.error(err.message));实操中真正要注意的不是方法怎么调而是什么时候调。串口是半双工介质同一时刻只能有一个未完成的请求。如果你用Promise.all同时发好几个请求它们会在底层互相干扰导致响应错乱。node-modbusrtu 内部虽然做了串行化处理但在应用层人为控制请求顺序是让系统真正稳定的关键。3.3 超时、异常帧与事务机制要理解Modbus RTU 是请求-响应式协议一个请求发出后从站必须在一定时间内应答。如果从站没收到比如地址错了、收到了但处理不了比如寄存器地址超出范围或者线路有问题主机的请求就石沉大海了。node-modbusrtu 内置了超时机制。默认超时时间因模块版本而异一般在 1000ms 到 2000ms 之间。超过这个时间没有收到响应readHoldingRegisters返回的 Promise 就会 reject你可以在.catch里拿到超时错误。这里有个细节异常帧。如果从站收到了请求但请求本身不合法它会回一个异常帧把功能码的最高位置 1然后附带一个异常码。比如请求功能码 03异常帧的功能码是 0x83异常码 02 表示非法数据地址。模块收到异常帧后同样会 reject错误信息里会带上异常码。我建议你把自己设备常见的几个异常码记住01 非法功能码、02 非法数据地址、03 非法数据值、04 从站设备故障排查问题会快很多。4. 完整实操读取一台 RS485 从站的数据4.1 一个典型项目场景温湿度变送器拿实际项目举例。现场有一台温湿度变送器通过 RS485 接到上位机参数如下从站地址 1波特率 96008N1。设备手册里写寄存器映射地址 0 是温度数据类型 uint16除以 10 就是实际温度地址 1 是湿度uint16除以 10 就是实际湿度。这种场景在暖通、仓储、机房动环监控里太常见了。目标很简单定时把温度和湿度读出来存到数据库或者推给前端展示。4.2 先手动验证再写代码我的习惯是拿到设备后先不写代码用串口调试助手手动发一帧报文确认设备和线缆没问题。这样可以隔离问题——先证明硬件链路是通的再排查代码逻辑。先算出读请求的 CRC01 03 00 00 00 02 C4 0BCRC 可以用任意在线工具算。在串口调试助手里打开对应串口参数设成 9600、8N1以十六进制发送这一帧。如果设备正常你会收到类似下面的响应01 03 04 00 FA 01 40 7B 9E拆解一下01从站地址03功能码04后面数据长度是 4 个字节00 FA是温度十进制 250除以 10 就是 25.0 度01 40是湿度十进制 320除以 10 就是 32.0%RH最后两个字节是 CRC。确认能收到正确响应之后再进代码阶段。4.3 完整代码连接、读取、解析const ModbusRTU require(node-modbusrtu); const client new ModbusRTU({ port: COM3, baudRate: 9600, dataBits: 8, stopBits: 1, parity: none, }); async function readSensor() { try { const data await client.readHoldingRegisters(1, 0, 2); const temperature data[0] / 10; const humidity data[1] / 10; console.log(温度: ${temperature.toFixed(1)} °C); console.log(湿度: ${humidity.toFixed(1)} %RH); return { temperature, humidity }; } catch (err) { console.error(读取失败:, err.message); } } client.connect() .then(() { console.log(串口已打开); // 每 5 秒读一次 setInterval(readSensor, 5000); }) .catch((err) { console.error(连接失败:, err.message); process.exit(1); });这段代码看起来简单但已经覆盖了一个采集服务的核心骨架打开串口、定时轮询、异常捕获。实际项目里我会再加一层把采集结果包一层 JSON通过 WebSocket 或者 HTTP 接口交给前端展示或者写进 InfluxDB 这类时序数据库。4.4 数据解析的坑大小端、缩放因子、32 位数据Modbus 寄存器的数据解析是新手最容易翻车的环节。一个寄存器是 16 位读取后拿到的是 0 到 65535 之间的整数。但设备和设备之间的规矩不一样大端模式Big Endian高字节在前这是 Modbus 协议默认的数据排列方式大部分设备都这样小端模式Little Endian低字节在前部分设备不按套路出牌32 位数据如浮点数、长整数需要两个寄存器拼起来拼接顺序可能是 ABCD也可能是 CDAB、BADC 等变体遇到 32 位数据模块读回来的数组是两个相邻寄存器的值比如[0x41A0, 0x0000]。如果你不太确定设备的字节序可以拿一个已知值去试设备设成输出一个固定数值然后看你读回来的数据需不需要交换字节。我有一台流量计就是反过来的当时排查了很久最后把两个寄存器的字节交换一下就对了。缩放因子同样要留意。很多传感器为了保留精度会把真实值放大 10 倍、100 倍甚至 1000 倍存在寄存器里。设备手册写分辨率 0.1意味着你读到的 250 对应的真实值是 25.0。忘了除以缩放因子数据偏差 10 倍这种错误在数据展示时一时半会儿看不出来但会给后续分析埋雷。4.5 多从站轮询与并发控制一个串口带多台从站设备是常态。假设你接了 3 台设备从站地址分别是 1、2、3轮询逻辑可以这样写const SLAVES [1, 2, 3]; async function pollAll() { for (const id of SLAVES) { try { const data await client.readHoldingRegisters(id, 0, 2); console.log(从站 ${id}:, data); } catch (err) { console.error(从站 ${id} 读取失败:, err.message); } } } client.connect() .then(() setInterval(pollAll, 3000)) .catch((err) console.error(err.message));注意这里用for循环串行读取而不是Promise.all。为什么因为串口是半双工的一个请求的响应还没回来就发下一个请求会把总线上的数据搅成一锅粥。虽然模块内部大概率做了排队但你控制好应用层的节奏系统才更可控。轮询间隔也要结合实际从站响应速度、总线上的设备数量、你需要的实时性。设备多的时候轮询一圈的时间会比单台设备长很多间隔设太短会导致请求永远在排队CPU 空转但数据一直刷新不出来。5. 常见问题与排查技巧实录5.1 高频问题速查表我把实际项目中踩过的、以及同行交流中最常出现的问题整理成一个表现象可能原因解决方案请求一直超时从站地址错误、波特率不匹配、RS485 的 A/B 线接反核对设备手册参数调换 A/B 线测试能通但收到 CRC 错误通信线接触不良、距离过长、现场干扰严重检查接线端子换成屏蔽双绞线降低波特率读到的数据明显不对寄存器地址错误、大小端反了、忘了缩放因子再读一遍手册用已知值验证解析逻辑轮询一段时间后卡死上层请求堆积、串口缓冲区溢出检查是否并发发请求适当加大轮询间隔程序报端口被占用串口被串口调试助手或其他程序打开关闭所有占用串口的程序重新插拔 USB 转串口RS485 的 A/B 线接反是特别容易犯的错。接反之后设备不会烧坏但会完全收不到数据或者收到一堆乱码。发现通信不上第一件事就是查线序这比查代码快得多。5.2 调试三板斧手动发包、模拟器、原始日志代码调试永远遵循一个原则先证明链路通再查业务逻辑。我的调试流程分三步。第一步用串口调试助手手动发原始帧。这一步能确认设备本身是好的、线是好用的。串口调试助手推荐用 SSCOM 或者 MThings简单够用。发一帧01 03 00 00 00 02 C4 0B看设备有没有正常响应。如果手动发都收不到正确响应别折腾代码先去查硬件。第二步用 Modbus 模拟器验证代码。如果你手头暂时没有硬件设备可以用 Modbus Slave 模拟器比如 ModRSsim2在电脑上虚拟一个从站然后跑代码去读它。这样能把代码逻辑先调通拿到现场再接真设备问题定位就会精准很多。第三步在代码里打原始报文日志。node-modbusrtu 通常有启用调试日志的选项或者你可以手动打印每个请求和响应的十六进制帧。排查 CRC 错误、数据错位这类问题看到原始报文基本一眼就能定位。光看解析后的数据是看不出问题的因为你不知道数据是在哪一步坏的。5.3 生产环境里必须注意的几件事开发环境能跑和生产环境稳定跑是两回事。我总结几个生产运行的关键点第一掉线重连机制。USB 转串口设备在长时间运行后可能掉线特别是工业现场电压不稳、USB 口供电不足的情况。程序一旦检测到串口错误不能直接崩溃要自动关闭串口、等待几秒、重新 connect。如果使用的是setInterval定时轮询重连成功后要防止出现多个轮询循环叠加——这是非常隐蔽的 bug重连几次后采集频率翻了几倍。第二串口排他性。串口同一时间只能被一个进程打开。生产环境里如果发现端口被占用多半是之前异常退出的进程没释放端口或者残留的调试工具还开着。Windows 上可以用设备管理器确认Linux 上lsof /dev/ttyUSB0能查到占用进程。第三日志一定要分级。正常运行时的采集数据、错误信息、重连事件分开记录。现场问题排查基本全靠日志没有日志等于盲人摸象。第四考虑把采集层和业务层分离。node-modbusrtu 只负责和设备通信采集到的数据不要直接在里面做业务处理而是通过事件或接口传给上层。这样设备通信出问题时业务逻辑不会跟着崩上层要加功能时也不用动通信模块。6. 一点个人体会我实际用 node-modbusrtu 部署过的项目里最典型的是一套十几台仪表的能耗采集系统。Node.js 进程跑在工控机上通过两个 USB 转 RS485 串口分两条总线轮询所有仪表采集到的数据通过 WebSocket 推给前端大屏。整套系统跑了半年多除了 USB 转串口偶尔需要重启之外基本没出过问题。这个稳定性其实很大程度要归功于先把环境弄干净、把参数弄对、把轮询节奏控制好。最后再分享一个小技巧如果你打算做类似的前后端联动项目可以把 node-modbusrtu 封装成一个独立的 WebSocket 网关服务——串口采集、数据缓存、前端订阅各司其职。这样以后不管前端用 Vue、React 还是小程序都只需要连 WebSocket 拿数据不用重复折腾串口通信。这个思路我在后面几个项目里沿用下来省了不少事。本文还有配套的精品资源点击获取