简介:HmiFuncDesigner是一款将HMI(人机界面)与数据采集功能集成于一体的软件工具,主要面向工业自动化、上位机开发及设备监控相关工程师。资源以zip压缩包形式提供,包体大小约12.22MB,内容覆盖Modbus协议通信、JavaScript脚本解析、画面功能编辑等核心模块,适合需要快速搭建HMI交互界面或实现设备数据采集与协议对接的开发者参考学习。目前已有84人浏览学习,可用于了解该软件的基本功能布局与典型应用流程。通过本资源,读者可以获得HmiFuncDesigner的实际软件包,便于在本地环境中安装体验,并结合Modbus协议完成从界面绘制、脚本逻辑编写到数据联调的整体实践。对于正在选型或评估轻量化HMI解决方案的工程师而言,这份资源也具备一定的功能验证与上手参考价值。
1. HmiFuncDesigner 解决的不只是画面:数据采集与脚本解析要放在同一个运行时里
一条产线上十几台走 Modbus RTU 的仪表,要在一台触摸屏上完成显示、参数设定和报警联动。老办法是组态软件画画面、网关做协议转换、上位机再跑一段逻辑,三套工程三套变量表,现场调试光对地址就要浪费半天。HmiFuncDesigner 这类软件把 HMI 画面编辑、Modbus 数据采集和 JS 解析收进同一个工程,点位直接进画面,联动逻辑用标准 JavaScript 写,因此特别适合中小型设备的面板开发、数据汇总屏和边缘侧数据预处理节点。下面按一个多从站项目的推进路线,讲清楚 Modbus 通道怎么配、寄存器映射表怎么建、JS 脚本挂在哪、画面绑定怎么做。
2. HmiFuncDesigner 的运行时架构:画面线程、Modbus 轮询与 JS 引擎的边界
如果界面重绘和设备网络轮询挤在同一个线程里,Modbus 请求发出后等待响应的几十毫秒会直接表现为画面拖动卡顿、按钮点击无反馈;反过来,画面趋势图的重绘也会延迟寄存器读取,让变量更新时间出现锯齿状跳动。常见的处理方式是拆成三个模块:画面渲染只负责图元绘制,采集调度器按固定周期发送 Modbus 请求并更新变量表,JS 引擎在独立上下文里执行回调,不直接碰界面对象。三者之间用一张共享变量表通信,画面通过订阅变量变化决定哪些图元需要重绘。
这类软件的配置界面通常把设备、变量、画面、脚本拆成四个独立编辑区域,HmiFuncDesigner 也符合这条规律。配置的时候,画面上的数值框绑定变量表中的逻辑名,而不是直接写“COM3 上某个寄存器”,所以后面更换采集通道或调整寄存器地址,画面文件基本不用动。这一点在多设备、多通道项目里价值很大:变量名稳定之后,画面编辑和驱动调试可以并行推进。
2.1 线程与内存边界:为什么 JS 不能直接操作画面控件
某些组态软件把脚本和画面放在同一个线程,脚本里再做一个几百毫秒的循环,整个界面就僵住了。HmiFuncDesigner 这类含 JS 解析的软件,脚本引擎与画面线程之间通常只有受限桥接对象(如 hmi.get/set 方法),脚本不能遍历控件的内部句柄。这条边界初看是限制,实际是保护:即使脚本写了一个死循环,UI 线程还能响应触摸和鼠标操作,上位机不至于黑屏。
在边界设计上,变量表是三个模块的唯一数据契约。采集驱动写入值,JS 脚本读写值,画面控件订阅值,三者都不直接持有对方的指针。调试时应坚持“变量表里看到什么,画面就显示什么”,不要在脚本里缓存一份变量值副本超过一个采集周期,否则会出现设备和界面数据不一致这种最难查的现场问题。
2.2 主站与从站:HmiFuncDesigner 在 Modbus 网络里的两种位置
HmiFuncDesigner 最常见的形态是 Modbus 主站,主动读取 PLC、变频器、温控表、电表的数据;此时要关心轮询周期、超时、重试,以及 485 总线上从站地址的分配。另一种形态是把它当 Modbus 从站,自己维护一张寄存器缓冲表给上位 SCADA 或 MES 来读,画面和 JS 仍可修改这张表,相当于在上位机与现场设备之间加了一层带界面的数据网关。
两种角色的配置方向完全不同。主站模式关注“轮询策略”,重点是请求串行带来的叠加耗时;从站模式关注“地址唯一性”,重点是单元号冲突和寄存器区间重叠。我一般建议一个工程里把低速 485 仪表和以太网 PLC 分成两个通道,不要塞进同一个轮询循环,这样总线故障的影响范围可以控制在单一区域内。
2.3 数据块、变量表与采集周期的落地关系
2.3.1 用数据块表达连续寄存器区
读到这里需要一个实际形态的示例。工程解包后,通道与数据块配置通常是结构化文件,下面是一段常见的 JSON 结构:
{ "channels": [ { "name": "CH1_RS485", "port": "COM3", "baudrate": 9600, "dataBits": 8, "stopBits": 1, "parity": "even", "slaves": [ { "address": 1, "timeout": 200, "retries": 2, "blocks": [ { "name": "B1_Temp", "fc": 3, "start": 0, "length": 20, "poll": 500 }, { "name": "B1_Status", "fc": 2, "start": 0, "length": 8, "poll": 300 } ] } ] } ] }这段配置表达的是:CH1_RS485 通道挂在 COM3 上,串口采用 8 数据位、偶校验、1 停止位;从站地址 1 的请求超时 200ms,最多重试 2 次;两个数据块分别用功能码 03 读取 0 号保持寄存器连续 20 个字,用功能码 02 读取 0 号离散输入连续 8 位。注意 poll 是“这一帧请求的最短间隔”,不是单个变量的刷新率,一个数据块里所有变量在同一帧响应中更新时间一致。
字段设置没有标准答案,但有一条经验值得记下来:
| 字段 | 含义 | 常见设置 |
|---|---|---|
| name | 数据块名称,变量通过它定位 | 带设备前缀的语义名 |
| fc | 功能码 01/02/03/04 | 按寄存器类型 |
| start / length | 寄存器起始地址与个数 | 连续合并,减少帧数 |
| poll | 轮询间隔(毫秒) | 不低于单请求 RTT 的 2-3 倍 |
| timeout | 等待响应上限 | 单请求 RTT 的 3 倍以上 |
| retries | 失败重试次数 | 0-2,多了拖慢总线 |
2.3.2 轮询周期按“最坏耗时”估算,不是填越小越好
Modbus 是严格的请求-响应协议,一条总线上同一时刻只能有一个未完成的请求。把 poll 从 500ms 改到 50ms 不会让设备响应变快,只会提高总线冲突率,最终表现为变量突然跳到错误值后再弹回来。估算单请求耗时的方法:读 20 个保持寄存器,请求帧约 8 字节,9600bps 下约 8.3ms;响应帧约 47 字节,约 49ms;加上从站内部处理时间,单请求最坏约 80ms。一个从站三个数据块串行,一轮就是 240ms,poll 设在 300-500ms 之间是合理的。
提示:如果画面上要求温度变量刷新率达到 100ms 级,不要靠缩短 poll 实现。改走 Modbus/TCP 并用支持多线程事务的从站,或拆两个通道分别轮询不同寄存器组,效果更可控。
3. 在 HmiFuncDesigner 里接入 Modbus 协议:从通道参数到寄存器映射
3.1 先选协议变体:RTU、TCP、ASCII 什么时候用
Modbus 家族里最容易混淆的不是功能码,而是协议变体。RTU 面向 485/232 串口,二进制帧加 CRC 校验,现场绝大多数仪表、变频器、温控器走的就是它;TCP 面向以太网,端口 502,在帧前加了 6 字节 MBAP 头,用单元号(Unit ID)代替从站地址;ASCII 把帧内容转成十六进制字符,效率只有 RTU 的一半,只在无线电台和老式设备上还能见到。HmiFuncDesigner 的通用驱动通常是在一个通道下选变体,选错的表现往往不是完全连不上,而是偶发跳变或隔一段时间报一次超时,排查时要把接线、串口参数和协议变体放在一起看。
我选型的经验是:设备有网口就用 Modbus/TCP,省掉 485 转接器也减少共地干扰;设备只有 RS485 且距离超过几十米,优先 RTU,A/B 用双绞线、屏蔽层单端接地、末端并 120Ω 终端电阻;波特率从 9600 起调,超过 38400 时帧间隙接近物理极限,对从站响应时间要求苛刻,反而容易误判超时。
3.2 四种寄存器区和变量映射表该怎么建
Modbus 的数据模型分成四个区,映射表就是把这四个区翻译成 HMI 变量的中介。对着一张空白点表,先按下面的对应关系定列:
| 地址空间 | 功能码 | 位 / 字 | 典型内容 | 变量建议 |
|---|---|---|---|---|
| 线圈 Coil | 01 读,05 写单,15 写多 | 1 位 | 启停、复位、使能 | BOOL |
| 离散输入 Discrete Input | 02 读 | 1 位 | 限位开关、故障触点 | BOOL 只读 |
| 保持寄存器 Holding Register | 03 读,06 写单,16 写多 | 16 位 | 设定值、运行参数 | INT16 / UINT16 / INT32 / FLOAT32 |
| 输入寄存器 Input Register | 04 读 | 16 位 | 模拟量采集值 | UINT16 / INT32,带缩放系数 |
映射表建好之后,真正容易翻车的是起始编号。设备手册给 40001 表示第一号保持寄存器,Modbus 帧里的偏移却是从 0 开始,二者差 1。HmiFuncDesigner 里一般填协议偏移量,如果照抄手册的 40001,访问到的就是第二个寄存器。做第一版点表前先确认设备手册的编号起点,比现场拿万用表怀疑人生快得多。
3.3 最小可复现的接入步骤:先测链路,再进 HmiFuncDesigner
我会在往软件里配通道之前,先用命令行工具把通信链路单独验一遍,避免把接线问题和软件配置问题混在一起。Linux 下装好 mbpoll,执行:
# 读取地址 1 的从站: 功能码 03, 起始寄存器 0, 连续 4 个, 9600 偶校验 mbpoll -a 1 -t 3 -r 0 -c 4 -b 9600 -p even /dev/ttyUSB0参数含义:-a 指从站地址,-t 3 表示功能码 03 读保持寄存器,-r 为起始寄存器偏移,-c 为连续寄存器个数,-b 覆盖波特率,-p 覆盖校验位,设备节点是 /dev/ttyUSB0。如果返回数值和仪表面板一致,后面所有问题都从软件侧找;如果超时,优先看 A/B 是否接反、终端电阻是否到位,不要急着反复重启工程。
Windows 开发机上没有 mbpoll 时,用 pymodbus 写个最小客户端做同样的事:
from pymodbus.client import ModbusSerialClient client = ModbusSerialClient( port="COM3", baudrate=9600, bytesize=8, parity="E", stopbits=1, timeout=0.2, ) if client.connect(): resp = client.read_holding_registers(0, 4, slave=1) if not resp.isError(): print("registers:", resp.registers) client.close()这段代码的关键在 timeout=0.2:它是单个请求的等待上限,不是整体缓冲时间。如果设备本身要 300ms 才回应,必须把它放大到 0.5,否则程序会把正常响应当超时丢掉。从这里拿到的响应值,要与后面在 HmiFuncDesigner 变量监视窗口里看到的数值完全一致,才算链路打通。
3.4 485 与 Modbus 调试的高频故障点
485 接线问题的表现很典型:A/B 接反通常完全无响应;屏蔽层两端接地可能因电位差引入地环流,产生偶发错误帧;缺终端电阻短距离能通,距离一长就随机丢包。建议把这几项写进设备交付检查单,现场按顺序排除。另一个高频问题是从站地址重复:两个从站都设成 1 时,主站请求发出后两个设备同时回帧,数据线上出现帧叠加,CRC 几乎必然失败,但单看每一台设备又都在正常应答。这种故障只能逐台断电定位。
还有一类“读回全 0”的坑。有些设备把真实数据放在输入寄存器区(功能码 04),手册却把地址写得像保持寄存器(功能码 03),按手册填完读回的全是 0。遇到全 0,先换寄存器区再试一次,通常当场就能定位。调试中“读回全 0”和“读回错误数值”要分开处理:前者指向功能码或地址空间错位,后者指向数据类型解析错误,比如把 FLOAT32 按 INT16 拆开读。
4. 用 JS 解析把画面功能编辑做厚:变量回调、配方切换与控件绑定
4.1 从动作脚本到 JS 解析,HmiFuncDesigner 绕开了什么
传统组态软件的事件脚本通常是一行行的“变量 = 值”或“IF…THEN…”,做按钮互锁够用,一旦遇到配方计算、数组遍历、字符串拼接、和 HTTP 服务对接,就会变成一坨难以维护的分支。HmiFuncDesigner 内置 JS 解析后,界面和逻辑被拆开了:画面只负责展示和收集输入,JS 负责数据处理与设备交互。这个边界意味着可以在不重画面布局的情况下,只替换脚本文件就改变设备行为,现场迭代速度完全不一样。
嵌入式设备上的 JS 引擎并不是把整个浏览器搬进来,而是选 QuickJS 或 Duktape 这类可裁剪的引擎,内存占用在几百 KB 到几 MB 级别。脚本风格要适应这个环境:不写庞大的字符串转换,不依赖 setTimeout 做精确计时,不用严格相等去比较浮点运算结果。HmiFuncDesigner 脚本上下文里通常暴露一组 hmi. 前缀的桥接方法,用于读变量、写寄存器、改控件属性,这也是脚本与画面交互的唯一通道。
4.2 脚本挂载点与变量变化回调
常见的挂载点有四类:画面加载完成后、控件事件触发时、变量值变化时、周期定时器。其中变量变化回调是最能体现“数据驱动界面”的位置——采集链路每更新一个值,脚本就被触发一次,界面上与该值相关的一切都会被同步刷新。下面是典型的回调骨架:
// 变量名: Turbine_Temp, 来自保持寄存器区 // 每次采集更新到该变量时自动调用 function onVarChange_Turbine_Temp(ctx) { var v = parseFloat(ctx.value); if (isNaN(v)) { hmi.setVisible("Temp_Abnormal", true); return; } var limit = parseFloat(hmi.getValue("Temp_Limit")); hmi.setVisible("Temp_Abnormal", v > limit); hmi.setValue("Temp_Display", v.toFixed(1)); }参数说明:ctx.value 是带工程单位的采集值,转浮点后先做 NaN 判断,因为寄存器掉线时常见 65535 这类满量程值,不做判断会被当真温度;hmi.getValue("Temp_Limit") 读的是另一个画面变量的当前值,它可能已经经过缩放;hmi.setVisible 和 hmi.setValue 改的是控件状态与变量表,不会向设备发送写指令。沿着这条线排错,90% 的脚本不生效问题都能归结为“变量没有出现在变量表”或“上下文里没有这个值”。
4.3 配方切换:一个可以直接复用并改写的 JS 脚本
配方的本质是一批设定值寄存器的一次性批量写入。画面编辑器里逐条写动作,5 组配方十几条指令,新增配方还要动画面;放进 JS 数组之后,新增配方只改数据不动画面。下面是按设备地址、寄存器区、寄存器偏移组织的一次写入示例:
// 三组配方,每组对应保持寄存器 10/11/12 const RECIPES = [ { no: 1, temp: 80, speed: 500, hold: 120 }, { no: 2, temp: 90, speed: 450, hold: 150 }, { no: 3, temp: 100, speed: 400, hold: 180 } ]; function applyRecipe(index) { if (index < 0 || index >= RECIPES.length) { hmi.setValue("Recipe_Error", 1); return; } const r = RECIPES[index]; hmi.writeRegister(1, "Hold", 10, r.temp); hmi.writeRegister(1, "Hold", 11, r.speed); hmi.writeRegister(1, "Hold", 12, r.hold); hmi.setValue("Recipe_No", r.no); hmi.setValue("Recipe_Error", 0); } // 画面按钮"下一组"的事件里调用 function onNextRecipe() { var cur = parseInt(hmi.getValue("Recipe_No"), 10) || 0; applyRecipe(cur % RECIPES.length); }hmi.writeRegister 在不同小版本里名字可能不同,但参数顺序稳定:从站地址、寄存器区(Hold/Input/Coil/Discrete)、寄存器偏移、写入值。值得强调:hmi.setValue 只改内存变量表,hmi.writeRegister 才会走驱动线程发 Modbus 写请求。两者混用造成的“画面显示正常但设备没动作”,是脚本阶段最常出现的误解。
4.4 画面功能编辑里的数据绑定才是主体,脚本只是补充
画面功能编辑的核心不是画画,而是把控件属性绑定到变量表。绑定关系建立之后,刷新由采集驱动,脚本不需要主动轮询。下表是常见的绑定维度:
| 控件 | 可绑定属性 | 典型场景 |
|---|---|---|
| 数值框 | 值、可见性、使能 | 温度显示与输入限幅 |
| 指示灯 | 颜色、可见性 | 越限变色、状态闪烁 |
| 仪表盘 | 指针角度 | 压力、转速实时指示 |
| 趋势曲线 | 数据源列表 | 多个变量的历史走势 |
| 表格 | 单元格值 | 配方表、报警记录 |
在 HmiFuncDesigner 的画面编辑器里,一般流程是先选中控件,在属性面板指定“绑定变量”,再为“数值越界”这类条件配置颜色或可见性动画,最后才在脚本里引用同名变量。顺序不能反:脚本依赖的 hmi.getValue 只有变量存在于变量表时才有效,直接拿控件名拼脚本,画面重载时就可能报空引用。把这个顺序记牢,画面功能编辑的大部分问题都可以避免。
5. 基于 HmiFuncDesigner 的工程验证与排查:模拟器、日志与刷新节奏
5.1 用 Modbus 模拟器切断“设备问题”和“软件问题”
设备还没到场时,用 pymodbus 起一个最小从站,是验证 HmiFuncDesigner 配置最直接的手段。下面的脚本监听本机 502 端口,保持寄存器 0 到 9 初始值为 1 到 10:
# 最小 Modbus/TCP 从站 from pymodbus.server import StartTcpServer from pymodbus.datastore import ( ModbusSlaveContext, ModbusDataBlock, ModbusServerContext, ) slave = ModbusSlaveContext( hr=ModbusDataBlock.create([i + 1 for i in range(10)]) ) StartTcpServer( context=ModbusServerContext(slaves=slave, single=True), address=("127.0.0.1", 502), )跑起来后,把 HmiFuncDesigner 的 Modbus/TCP 通道指向 127.0.0.1:502,画面应显示 1 到 10。这一步通过,后面现场接线的故障就能直接从软件流程里排除。
5.2 三层日志分别看什么
按驱动层、脚本层、画面层来切分问题。驱动层看每一帧请求是否发出、有没有响应帧、CRC 是否通过,解决“设备不回答”;脚本层看变量回调是否被触发、JS 是否抛异常,解决“逻辑没跑起来”;画面层看绑定是否失效、图元是否重绘,解决“显示不更新”。HmiFuncDesigner 开发模式一般有日志窗格,捕获到问题后先对齐三层日志的时间戳,再决定动哪一块配置。
5.3 把采集更新和界面重绘解耦,避免刷新卡顿
采集回调按变量粒度触发,画面如果每来一次就全量重绘,曲线和动画必然掉帧。常见做法是采集回调里只打脏标记,由 200ms 周期的定时器统一重绘:
var dirty = false; function onVarChange_Turbine_Temp(ctx) { dirty = true; } function onTimer_200ms() { if (dirty) { hmi.redraw("Trend_Temp"); dirty = false; } }最后一个值得落地的技巧:把变量更新时间戳暴露到画面调试页。连续 5 个采集周期里时间戳都不变化,基本可以判定驱动或链路已经停止更新,而不是数据碰巧没变化。用时间戳当通信故障判据,比死盯数值可靠得多。
本文还有配套的精品资源,点击获取