OpenPLC Editor v4 Modbus 配置 OpenPLC 编辑器使用基于 Modbus 的自定义调试协议作为其主要机制以实现实时的 PLC 变量检查与强制写入。与标准的 Modbus读取线圈和寄存器不同编辑器在 Modbus PDU 层注入了五个专有功能码0x41–0x45支持在 TCP 和 RTU 两种传输实现中进行程序验证、批量变量读取和写入强制。本文档将深入剖析该协议的架构、双传输客户端的实现以及将前端与特定传输层细节隔离的端口抽象机制。自定义功能码协议OpenPLC 运行时通过五个非标准 Modbus 功能码暴露调试功能这些功能码运行在标准 Modbus ADU/TDU 帧结构之上。TCP 和 RTU 两种传输方式共享完全相同的功能码仅帧结构和错误检测层有所不同。功能码十六进制名称方向用途DEBUG_INFO0x41调试信息请求保留用于信息查询DEBUG_SET0x42调试设置请求→响应在目标上写入或强制修改变量值DEBUG_GET0x43调试获取请求→响应通过索引读取单个变量DEBUG_GET_LIST0x44调试获取列表请求→响应通过索引数组批量读取多个变量DEBUG_GET_MD50x45调试获取 MD5请求→响应获取运行中程序的 MD5 哈希值以进行验证运行时使用从ModbusDebugResponse枚举中提取的状态字节进行响应状态码十六进制含义SUCCESS0x7E操作成功完成ERROR_OUT_OF_BOUNDS0x81变量索引超出程序的变量表范围ERROR_OUT_OF_MEMORY0x82目标无法为响应分配内存功能码 0x41–0x45 占据了 Modbus 规范中的“用户定义”范围标准用户定义范围为 0x65–0x6F但 OpenPLC 运行时接受 0x41 及以上的自定义代码。请确保任何中间 Modbus 网关或防火墙配置为允许这些非标准代码通过而不进行过滤。双传输架构编辑器实现了两种传输客户端它们共享相同的 PDU 层协议但在帧结构、连接语义和错误检测方面存在根本差异适配器层在运行时根据当前设备配置选择合适的传输方式 —— 网络连接的运行时使用 TCP串口/USB 连接的设备使用 RTU集成的 AVR 模拟器则使用注入的虚拟串口。ModbusTcpClient — 网络传输ModbusTcpClient封装了原生的 Node.jsnet.Socket通过标准 Modbus TCP 帧结构与 OpenPLC 运行时通信。每个请求都被包装在Modbus TCP 应用报文头 (MBAP)中包含 2 字节的事务 ID、2 字节的协议 ID始终为0x0000、2 字节的长度字段和 1 字节的单元 ID。连接与并发模型客户端通过 Promise 链互斥锁 (sendRequestMutex) 强制执行严格的请求串行化。这可以防止共享 TCP 套接字上出现响应交错 —— 这是一个关键问题因为 Modbus TCP 协议除了事务 ID 之外没有内在的请求-响应关联机制。互斥锁将每次sendTcpRequestImpl调用链接起来确保同一时间只存在一个进行中的请求sendRequestMutex sendRequestMutex.then( () sendTcpRequestImpl(request).then(resolve, reject), () sendTcpRequestImpl(request).then(resolve, reject) )事务 ID 会以模运算方式自动递增 ((id 1) % 65536)并在每次响应时进行验证以检测过期或不匹配的回复。关键操作MD5 验证(getMd5Hash)构建一个带有字节序校验值 (0xDEAD) 的 12 字节 MBAP 帧请求。响应包含编译后的 ST 程序的 MD5 哈希值以 UTF-8 字符串形式使编辑器能够在启动调试会话之前验证正在运行的程序是否与当前项目匹配。批量变量读取(getVariablesList)接受一个变量索引数组并将它们打包到单个请求中。响应包含 PLC 滴答计数4 字节UInt32BE、最后处理的索引2 字节UInt16BE、响应大小字段和原始变量数据缓冲区。这种批处理对于高效轮询至关重要 —— 调试器可以在单次往返中读取所有监视的变量而无需为每个变量单独发出请求。变量写入/强制(setVariable)发送变量索引、强制标志1 覆盖 PLC 逻辑0 释放强制、数据长度和原始值字节。当force为false且未提供valueBuffer时将写入单个零字节作为占位符从而有效地释放对该变量的任何活动强制。ModbusRtuClient — 串口传输ModbusRtuClient使用 Modbus RTU 帧结构通过串口进行通信其中每一帧都以通过预计算查找表256 项高低字节表计算的CRC-16校验和作为结束。此客户端处理与 Arduino 及类似开发板的物理串口连接以及 AVR 模拟器注入的虚拟串口。CRC-16 计算CRC 使用标准的 Modbus RTU 多项式与两个 256 字节的查找表 (CRC_HI_TABLE和CRC_LO_TABLE) 进行计算。该算法处理帧的每个字节将高 CRC 字节与数据字节进行异或运算以索引两个表然后交换高低字节。生成的 16 位 CRC 以大端序附加到帧的末尾。当 RTU 客户端收到响应时它会验证 CRC但将不匹配视为非致命错误—— OpenPLC 调试器故意忽略 CRC 错误因为基于 Arduino 的目标设备可能会产生边缘串口时序导致偶尔的 CRC 不匹配而不会造成数据损坏。这是一个权衡调试可靠性而牺牲严格协议合规性的有意设计取舍。帧组装与检测与 TCP 中 MBAP 头提供显式长度信息不同RTU 帧没有长度字段。客户端使用基于静默的超时来检测帧完成在接收数据后它会等待FRAME_COMPLETE_TIMEOUT_MS(10ms) 以获取额外字节。当在此时间窗口内没有更多字节到达时即认为帧已完成剥离 CRC并在剩余 PDU 的前面填充 6 个零字节以创建与 TCP 客户端 MBAP 风格解析兼容的统一响应格式const paddedResponse Buffer.alloc(6 responseWithoutCrc.length) paddedResponse.fill(0, 0, 6) responseWithoutCrc.copy(paddedResponse, 6)这种填充技巧允许两种传输方式在适配器层中共享相同的 PDU 解析逻辑。Arduino 引导程序延迟当连接到物理 Arduino 开发板时构造函数会在串口打开后引入ARDUINO_BOOTLOADER_DELAY_MS(2500ms) 的延迟。此延迟允许 Arduino 引导程序超时并在发送第一个 Modbus 请求之前将控制权转移给用户程序 —— 如果没有此延迟早期请求将被引导程序消耗并静默丢弃。MD5 重试策略getMd5Hash方法实现了一个重试循环包含MD5_REQUEST_MAX_RETRIES3 次尝试和MD5_REQUEST_RETRY_DELAY_MS尝试间隔 500ms。这弥补了串口通信相比 TCP 更高的错误率 —— 连接后的第一个 MD5 请求可能会因目标仍在初始化而失败因此重试提供了优雅的恢复机制而不会将错误暴露给 UI。用于模拟的注入串口RTU 客户端支持通过ModbusRtuClientOptions中的serialPort选项依赖注入预构建的串口。提供此选项时客户端跳过SerialPort的构建直接使用注入的端口。这就是 AVR 模拟器集成提供虚拟串口的机制 —— 模拟器创建一对VirtualSerialPort将一端注入 RTU 客户端并用模拟的 PLC 响应驱动另一端。DebuggerPort — 传输抽象端口层中的DebuggerPort接口将 TCP/RTU 的区别从前端完全隐藏。它定义了映射到底层 Modbus 功能码的七个操作以及连接生命周期和事件订阅方法端口方法底层 Modbus 操作关键返回字段connect()打开 TCP 套接字或串口{ success, error? }disconnect()关闭套接字/端口{ success }getVariablesList(indexes)DEBUG_GET_LIST(0x44){ success, tick, lastIndex, data, error? }setVariable(index, force, buffer)DEBUG_SET(0x42){ success, error? }verifyMd5(expectedMd5)DEBUG_GET_MD5(0x45){ match, actualMd5, error? }readProgramMd5(path, board)读取磁盘上编译的 ST 产物{ success, md5?, error? }readDebugFile(path, board)读取磁盘上的.dbg变量映射{ success, content?, error? }onDisconnected(callback)套接字/端口关闭事件Unsubscribe函数isConnected()套接字/端口状态检查boolean编辑器适配器通过 Electron IPC Bridge 将这些调用路由到实际ModbusTcpClient或ModbusRtuClient实例所在的主进程。Web 适配器将通过带有 WebRTC 或 HTTP 传输的DebugBridge单例进行路由。这种架构分离意味着前端调试器 UI 完全不知道目标是经由 TCP、串口还是模拟器到达的。前端调试器集成调试器有机组件通过 Zustand 工作区存储消费 Modbus 数据管道。两个关键的存储字段驱动调试显示debugTick由getVariablesList返回的 PLC 循环计数器用于将变量快照与执行进度相关联。debugDataStale指示轮询数据可能过时的标志例如重新连接后。该组件使用两个 Hook ——useDebugBoolValuesMap和useDebugNonBoolValuesMap—— 将来自getVariablesList的原始Buffer数据解析为类型化的 JavaScript 值。布尔变量映射为0/1用于图表渲染而数值变量则根据其 IEC 61131-3 数据类型进行解析。调试器为每个变量维护一个滚动时间序列缓冲区最多可达MAX_BUFFER_SECONDS600 秒 / 10 分钟并具有可配置的显示范围1s、10s、30s、1m、5m、10m。暂停/恢复控制在不断开 Modbus 会话的情况下停止数据摄取 —— 暂停时追加新数据点的useEffect会提前返回但适配器层中的轮询循环会继续从目标读取数据以防止套接字缓冲区累积。传输对比特性ModbusTcpClientModbusRtuClient传输方式TCP 套接字 (net.Socket)串口 (serialport) 或虚拟端口帧结构MBAP 头 (7 字节: TxID ProtocolID Length UnitID)RTU 帧 (SlaveID FC Data CRC16)错误检测TCP 校验和由操作系统处理基于查找表的 CRC-16不匹配时为非致命错误帧定界MBAP 头中的显式长度基于静默的超时 (10ms 字符间间隙)并发Promise 链互斥锁 (以 TxID 验证作为备份)Promise 链互斥锁 输入缓冲区刷新连接延迟无2500ms Arduino 引导程序延迟MD5 重试无单次尝试3 次重试间隔 500ms响应标准化原始 MBAP 响应6 字节零填充前缀用于 MBAP 兼容性典型目标网络上的 OpenPLC 运行时Arduino/串口设备或 AVR 模拟器