众所周知Node.js 模块库种类丰富并且社区很健康,类似MODBUS 协议就有不亚于3种实现。根据颜值我选择了"jsmodbus v4.0.0" 作为通信连接驱动; 从尽量轻量和无编码角度出发,我只需要简单封装成Promise 以闭包形式实现语言的异步执行环境到访问资源的同步执行;日志采用 “log4js v6.3.0” 默认配置成开发者模式将日志保存到硬盘的public 目录(专家评价一下这是不是一个好主意)。前端利用ejs 模板来动态解析访问到的数据点位。
只是用Promise 同步化一下jsmodbus模块。我能想到的另外一个处理办法也许是基于emitter 的版本,有空我可以写一个那样的。对于通常的访问远端设备,公开的通信协议和专有的物理地址空间是两个正交量。于是每一种设备都不同。MODBUS 协议在工业领域也用了好久,比较稳定。同样意味它过于古董而不受IoT 业界的看好。最大的制约是对事件和pub/sub 机制的支持问题。
通信的本质是get 到对端的 response,对于TCP/IP 类型的连接,用IP 地址和端口打开对端的socket 就可以读写访问,用完以后记得关闭。于是建模时我把一台设备的访问参量抽象成:
-
公共参量例如对端设备类型识别码和采样周期
-
协议连接参量例如IP地址、端口和用户名、密码
-
专有地址空间例如寄存器起始地址和数目
这些参量确定了对端设备的模型最小可用集。把系统最小化会方便调试和找出问题的本质。
再回到一个实用系统对Event 的支持话题上来,这是任何一个成熟软件绕不过的考核。MODBUS 协议本身是基于poll 的,70年代那是人们人为类似串口的轮询已经非常工业了,没有必要再快了。后来出现了工业以太网等概念就把这些概念给挑战了。 虽然基于MQTT 或者Redis 都能快速实现一个pub/sub 体系,但是MODBUS 还是需要理解一下这当中的距离。我们这里说的事件响应就仅仅是近实时的,例如过载脱扣这样的事故需要软件系统发出实时告警,轮询就差了,甚至有可能还会漏掉告警。怎么办? 这里的事件告警类似电站系统的遥信概念,就是数字量感知,怎么做呢? 就是不停轮询设备状态字的特定Status bit位,发现它变化了就触发事件(接下来可以通过邮件或者MQTTs 等传递到上层stakeholder );如果是关键事件或者有害事件则发出告警或者控制其他机构动作,总体而言是遥控(有时对象是机器有时是人),这就是类似的MODBUS 或者RS485 总线上实现时髦的pub/sub 机制的本质。客户肯定会问这种做法可靠吗? 答案也是用数据来说,一个状态位更新如果是亚毫秒量级,这就 满足大部分需求了;如果事件响应需要几分钟或者几十秒钟则会受到选型时的拷问和质疑。在普通局域网中访问寄存器的速度到底有多快?我在代码中简单Profiling 了轮询一次920 寄存器的时间统计,很鼓舞的结果190~220ms。 即使全部更新一次Breaker 的1840 byte 的状态字(其实远没有这么多)也耗时不到1 second,非常值得尝试。这也是下文谈论实现细节时提到实现采样值的二进制字符串显示的话题。
再进一步,在软件便利性上我们还能做什么呢? 对了,类似OPC UA 和IEC 61850 那样的模型discovery 功能。 就是在采样Measurement 之前通过接口去查询设备变量结构树,借鉴IEEE488.2 技术标准,就是实现对设备的*IDN? 的访问接口。这样做有什么好处呢? 可以帮厂商多卖设备,因为编程接口开发变得准确了。 不同firmware 里变量的寄存器地址可能增加和升级,但是只要有层级树能访问到它们的地址集,并用可读的方式(例如定义一个description 字段帮助理解寄存器含义)返回查询结果,能大大地增强开发效率。可惜的是目前只有精密测量设备具备这样的接口,工业和电力设备除了61850 电站用电力设备外其它没有这样的现成接口,需要工程师自己来开发。这样一来不仅能提高测量参量开发,也能增强设备的事件上报能力。 顺便提一句488.2 协议真是一种自动化测量非常靠谱的参考标准。 经过了30 来年的现场验证,绝对值得一读。
展示数据的功能必须是异步的。也就是HTTP 请求不应该去依赖我的测量和采样动作,因为那样多用户局域网并发访问就会发生数据更新的不一致-我们要尽量降低HTTP 请求里的操作时延。 和前面南向数采函数同步化(因为网络资源是唯一的互斥的不存在并行的条件)不同,多用户通过网页查询后台数据需要异步。try the best 这才是互联网的本质。没有强制也没有保证,当前有什么样的数据就展示什么样的结果,每个用户都可以在内存中得到一个样本拷贝,可能是一样的,也可能完全不一样。
寄存器读出来的数据在Node.js 里是用Buffer 这种字节流来表示的(读者可以思考一下为什么不用字符串?)刚开始我显示的只是类似65535 这样的数值,可是很快发现HEX 格式似乎更容易理解; 做完后又得到需求Binary 似乎才是状态字类型的最好格式。于是同一个寄存器值的present 扩展到3 种格式-对了,还有一种是字节流的ASCII 码显示,因为某些寄存器会存储设备序列号和铭牌信息或者时间戳字符串。这样的展示极其容易在一大堆怪字符里发现你想要的关键信息。 对了,大数据不仅仅是数据多和杂,关键还在于数据的理解,这里提到的16进制 2 进制展示,以及ASCII 字符串展示在实际工作中往往一下子就能回答我们抓取的数据值是不是对的,解决我对程序正确性的无端担忧。数据展示和理解绝对是大数据推广的前置条件。
第一步是看程序跑起来会不会崩溃。这不是一个无聊的假设,因为我就尝试了3次,第一次是一个循环变量改过以后循环体里忘记改了;第二次是配置文件的IP 地址配错了; 第三次是ejs 模板缺少了header 和 footer 造成网页无法展示虽然trace 显示的访问南向设备是OK 的。
如果有断路器实物我们可以直接用网线连接进行数据点位采集; 可是万一没有就只能基于历史记录或者其它类似的MODBUS 协议设备来调试程序。或者大家都认为基于历史记录很low,但是如果有一个恰当的记录文件,我们可以用在很多调试中- 例如response 的数据layout 和 采集到的Buffer 的长度。尤其是Buffer 的处理,有了一些基线数据例如时间戳共调试以后,这部分可以解决掉绝大部分的展示网页的调试。
Profiling 意味剖析瓶颈-如果没有瓶颈或者性能捉急的地方也就不用调优了,所以性能切片是调优的前期判断。这里我们只需要在采集数据的循环上利用日志打印出取到545 个寄存器的总体时间。
当我们用这段程序访问一台不存在的设备时会发生连接超时错误,底层送上来ERRORCONNECT 这是容易理解的,但是用户看到这段这个报错码会丈二金刚摸不着脑袋 - 跟他的配置有什么关系吗? 所以在该类型错误对象上附加必要的地址信息会非常容易排查:
socket.on('error', (error)=>{
socket.end()
error.ip = addressSpace.protocolData.ip
error.port = Number(addressSpace.protocolData.port)
tracer.error(error)
reject("socket: " + error)
就是这么简单地添加上两个参量就能拯救80% 的尴尬 出了错不知道错误原因在哪。有时是配置从别的机器拷贝过来未经验证;有的是端口改了;有的是设备关机了。