几年以来,个人对监控系统的接触比较多,像电力scada系统,自动化设备的上位机系统,无人机地面站等,到后来独立开发监控系统,慢慢的形成了自己对监控系统通用实现的一种理解。其实做这行软件开发的也都有一个架构上的共识。这篇文字总结一下监控系统的通用实现,也可以说得上是架构吧。
监控系统首先需要设计的就是通信规约,通信规约定义设备和监控系统的对话方式。设备和系统的通信是以一包包数据为最小单元来通信的,这一包数据也叫做“帧”,定义通讯规约首先就要定义“帧”的基本格式,下面是一个帧格式的简单例子
接着就要定义各种指令内容,像下面我定义的一个和stm32通信监控电机的指令集,里面包含两条指令,#1号指令用来从系统往下面发,用来控制电机转速;#2号指令由下面以给定间隔连续的往系统发,用来反馈电机转速和码盘值。
规约定好了,软件怎么写?假如我们的系统是有一个简单的界面,上面有个控制转速的按钮,和显示电机转速的标签。考虑到以后可能会扩充指令,进行运行报警、历史数据存储,数据回放,甚至系统可能会分布式。我们代码怎么写才会bug少,具备良好的扩充性同时又很优雅?
基本架构
1、协议公共包
协议公共包用来定义帧常量、指令id及其结构体及用到的各种枚举。像上面的协议,我们可以在这个包里定义 帧头1、帧头2常量,定义消息集枚举、分别定义#1号消息结构体和#2号消息结构体等,凡是用到的公共定义都写到这里面,避免后面封包解包直接硬编码。2、数据流
这里的数据流其实是一个通信接口,他是串口、tcp、udp等各种通信方式的一个抽象,程序上表现为一个接口或者抽象类,包含基本的读、写方法,在具体的应用上进行实现就可以了。数据流的一个基本特点就是不可以多个组件同时读,或者多个组件同时写,但是读写可以同时进行。
如果你使用的是异步socket通信方式,这时候还是可以适用的,实现上设置一个接收和发送缓冲区,每次有socket通知有数据的时候,将数据放到接收缓冲区,发送数据的时候放到发送缓冲区,同时循环发送就可以了。通过这种方式转换成了上面的串行数据流的形式。
3、协议接口
这个包用来封包、解包,提供协议上的一些公共方法,比方说上面的协议,提供一个设置电机速度的方法setSpeed,这个设置速度的接口内部实现首先进行封包,发送。有些协议可能还需要回复,这时候就需要读包,等待确认指令再返回。我们将这一个基本的通信对话叫做事物。
事物的基本要求是要对信道的独占使用,比方我发送一个电机速度控制指令,这时候我就需要等待回复。在这段时间内,系统内其他组件不能够写也不能够读,收到的所有包如果不是我需要的确认指令,要么丢弃,或做其他处理(存起来)
因此,需要在这个包里面有一个对对数据流资源的一个控制,需要拥有一个读互斥锁、写互斥锁和独占标志。
读互斥锁:用来安排多个线程同时读包的请求
写互斥锁:用来安排多个线程同时发送包的请求
独占标志:用来标志是否当前正在进行一个事物。
如果一个数据流上有很多个设备同时通信,每个设备通过一个id来区分。这可以通过修改上面的协议的帧格式,在里面加一个id字段就可以实现。考虑到这种情况,因此协议接口包还包含着多个会话状态,每个会话状态相当于一个设备,保存着通信和设备的相关状态信息。
4、会话状态
每个会话状态对应于一个设备,会话状态里面包括设备状体实时库,像上面的例子,设备的状态就就包括电机的转速、码盘值等。还包括一个包存储器和其他的一些消息存储器。考虑到有一些主动上送的协议,设备会定时将自己的状态上送过来,但是实时库可能还没有达到刷新周期,这时候可以先将这些包存起来到一个字典中,键为消息id,值为消息体。当实时库刷新周期到了以后,自动取这里存放的包就可以了,如果需要发送召唤就直接发送就可以了。这样实时库的刷新就无需进行等待。
5、设备状态库
设备状态库简单上说,就是一个类,对设备进行建模,包含设备的各种状态数据。也叫实时库。有时候系统可能会分布式,有服务器有客户端,各个节点都要共享设备的数据,这时候实时库也需要支持分布式,因此比简单的类建模要复杂很多,这要是将设备状态库叫做实时库的一个原因吧。
6、包存储器
这个主要用于实现实时库的高效刷新。实时库的的刷新可以设置成以固定频率进行,实现上设置一个刷新线程,以固定的周期读取包存储器里面的包,解析,将数据刷往实时库;如果需要召唤,直接调用协议接口包进行发召唤。但如果采用常规的方式,先独占数据流,发送召唤,等待,读数据,解包,刷新,释放数据流的方式,这样通信效率会很低。
7、驱动线程
现在整个基本架构已经有了,但还需要一个总的线程来进行驱动。在这个线程循环里面,轮询设备,定时发送心跳包(如果协议有要求)、刷新实时库,
8、设备报警
如果设备某个状态位发生改变,实时库需要发出通知,可以在通过设置回调,在刷新方法内判断并进行调用。
9、历史数据保存
编写一个历史存储器,注册实时库的刷新回调,将数据刷新到存储器的缓冲区,达到一定数据后,触发保存操作。所有的保存操作在自己的线程里面执行,不要阻塞实时库的刷新线程。