FreeSCADA 2:基于.NET的开源 SCADA 系统
项目简介
FreeSCADA 2 是一款基于微软技术栈的开源 SCADA(监控与数据采集)系统,采用 .NET/C#/WPF/XAML 等技术构建。它支持使用纯 XAML 语法定义矢量图形,并可将 XAML 属性直接绑定到标签值,实现动态数据的可视化展示。此外,还内置了 OPC、ModBus 和 SNMP 等多种通信协议的驱动程序,在工业自动化领域具有广泛应用潜力。
克隆项目
打开命令行工具,执行以下命令以克隆FreeSCADA项目到本地:
1 |
git clone https://github.com/AlexDovgan/FreeSCADA.git |
项目架构
- 整体架构 :FreeSCADA 2 的架构主要由 Designer 和 Run Time 两个核心模块以及其他辅助模块和插件等组成。
- Designer :是用于创建和配置项目的开发工具,提供了可视化界面,允许用户定义与数据源的链接、设置归档规则、声明报警及其预期用户反应、创建可视化方案和报告模板、设置报告生成计划等功能。例如,用户可以在 Designer 中通过简单的拖拽操作和配置,建立起与 PLC 等设备的连接,并设计出相应的监控画面和数据展示布局。
- Run Time :负责项目的实际运行和监控,实现数据的实时采集、处理、存储、归档,以及报警生成、数据可视化、报告生成等功能。它能够将 Designer 中配置好的项目部署到实际运行环境中,并按照设定的规则进行数据采集和处理,同时以图形化的方式将数据展示给用户。例如,在工业生产过程中,Run Time 可以实时采集生产数据,并在界面上动态显示生产状态和相关参数,当出现异常情况时及时发出报警。
- 数据流架构 :数据采集模块从各种设备和数据源中获取数据,然后通过数据处理模块进行清洗、转换和计算等操作,再将处理后的数据存储到数据库中。数据存储模块为数据查询和分析模块提供支持,以便用户能够快速检索和分析历史数据。同时,系统还会根据配置的规则生成相应的报警信息,并通过报警管理模块通知用户。在数据展示方面,系统利用 WPF 和 XAML 的强大功能,将实时数据和历史数据以图表、趋势图、仪表盘等多种形式直观地展示给用户。
- 通信架构 :FreeSCADA 2 内置了多种通信协议的驱动程序,如 OPC、ModBus 和 SNMP 等,通过这些协议,系统能够与各种工业设备和系统进行通信,实现数据的采集和交互。此外,它还提供了通信插件机制,允许用户根据实际需求扩展和定制通信协议,以满足不同设备和场景的通信要求。
主要代码分析
核心模块代码
Designer 模块
位于项目的 Designer
目录下,其核心代码主要包括 MainForm.cs
等文件。MainForm.cs
是 Designer 的主窗口代码,实现了界面的初始化、菜单和工具栏事件处理、项目加载和保存等功能。例如,在项目加载过程中,它会读取项目的配置文件,解析项目的结构和各个组件的配置信息,并在界面上进行相应的显示和初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
// 定义一个静态的WindowManager实例,用于管理窗口 static WindowManager windowManager; // 定义一个私有成员变量,用于存储启动画面 private SplashScreen _splashScreen; /// <summary> /// 构造器 /// </summary> public MainForm() { // 初始化组件,包括窗体和控件的创建 InitializeComponent(); // 设置自动缩放模式为DPI,以适应不同分辨率的屏幕 AutoScaleMode = AutoScaleMode.Dpi; // 设置启动画面 SetSplashScreen(); // 初始化环境,传入当前窗体、主菜单、主工具栏和环境模式 Env.Initialize(this, mainMenu, mainToolbar, FreeSCADA.Interfaces.EnvironmentMode.Designer); // 初始化归档系统 ArchiverMain.Initialize(); // 初始化文件操作的命令上下文 CommandManager.fileContext = new BaseCommandContext(fileToolStripMenuItem.DropDown, mainToolbar); // 初始化视图操作的命令上下文 CommandManager.viewContext = new BaseCommandContext(viewSubMenu.DropDown, mainToolbar); // 初始化文档编辑的命令上下文 CommandManager.documentContext = new BaseCommandContext(editSubMenu.DropDown, mainToolbar); // 添加帮助按钮到主菜单 ToolStripMenuItem newItem = new ToolStripMenuItem(StringResources.CommandContextHelp); mainMenu.Items.Add(newItem); // 初始化帮助操作的命令上下文 CommandManager.helpContext = new BaseCommandContext(newItem.DropDown, null); // 添加检查更新的命令到帮助上下文 CommandManager.helpContext.AddCommand(new CheckForUpdatesCommand()); // 添加最近使用的文件列表到菜单栏 MRUManager mruManager = new MRUManager(mRU1ToolStripMenuItem, toolStripSeparator2); // 创建并初始化窗口管理器,传入停靠面板和最近使用的文件管理器 windowManager = new WindowManager(dockPanel, mruManager); // 注册项目加载完成的事件处理程序 Env.Current.Project.ProjectLoaded += new EventHandler(OnProjectLoaded); // 更新窗体标题和命令状态 UpdateCaptionAndCommands(); } |
RunTime 模块
主要代码集中在 Runtime
目录中,MainForm
.cs
是其核心文件之一,负责 RunTime 的启动、初始化和运行时管理。它会加载项目的配置,启动数据采集和处理线程,初始化通信协议驱动等,并在运行过程中实时监控系统的状态和数据变化,确保系统的稳定运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// 定义一个WindowManager类型的变量windowManager,用于管理窗口 WindowManager windowManager; // MainForm的构造函数,用于初始化窗体 public MainForm() { // 调用InitializeComponent方法,初始化窗体组件 InitializeComponent(); // 初始化Env环境,传入当前窗体、主菜单、主工具栏和运行模式 Env.Initialize(this, mainMenu, mainToolbar, FreeSCADA.Interfaces.EnvironmentMode.Runtime); // 初始化ArchiverMain,可能是用于数据归档或日志记录 ArchiverMain.Initialize(); // 设置CommandManager的viewContext,传入视图子菜单的DropDown和主工具栏 CommandManager.viewContext = new BaseCommandContext(viewSubMenu.DropDown, mainToolbar); // 创建一个MRUManager对象,用于管理最近使用的文件,传入最近使用文件菜单项和分隔符 MRUManager mruManager = new MRUManager(mRUstartToolStripMenuItem, toolStripSeparator3); // 创建一个WindowManager对象,传入停靠面板和MRUManager,用于管理窗口 windowManager = new WindowManager(dockPanel, mruManager); // 更新窗体的标题 UpdateCaption(); } // MainForm的另一个构造函数,用于初始化窗体并加载指定的文件 public MainForm(string fileToLoad) { // 调用InitializeComponent方法,初始化窗体组件 InitializeComponent(); // 初始化Env环境,传入当前窗体、主菜单、主工具栏和运行模式 Env.Initialize(this, mainMenu, mainToolbar, FreeSCADA.Interfaces.EnvironmentMode.Runtime); // 初始化ArchiverMain,可能是用于数据归档或日志记录 ArchiverMain.Initialize(); // 设置CommandManager的viewContext,传入视图子菜单的DropDown和主工具栏 CommandManager.viewContext = new BaseCommandContext(viewSubMenu.DropDown, mainToolbar); // 创建一个MRUManager对象,用于管理最近使用的文件,传入最近使用文件菜单项和分隔符 MRUManager mruManager = new MRUManager(mRUstartToolStripMenuItem, toolStripSeparator3); // 创建一个WindowManager对象,传入停靠面板和MRUManager,用于管理窗口 windowManager = new WindowManager(dockPanel, mruManager); // 如果传入的文件路径不为空,则加载该文件 if (fileToLoad != "") windowManager.LoadProject(fileToLoad); // 更新窗体的标题 UpdateCaption(); } |
Common模块
Common 模块是 FreeSCADA 2 的核心基础模块,它为整个系统提供了通用的接口、数据模型、工具类和配置管理等功能,是其他模块(如 Designer、Run Time 等)正常运行所依赖的基石。它就像是一个公共工具箱,里面包含了各种各样的工具和资源,供系统的不同部分在需要时调用和使用。
主要功能与代码细节
- 接口定义
- 数据接口 :Common 模块定义了一系列与数据相关的接口,比如
IDataSource
接口。这个接口规定了数据源的基本操作规范,包括数据的读取、写入、连接和断开等方法。例如,Read
方法用于从数据源读取数据,Write
方法用于向数据源写入数据。这些接口使得不同类型的设备和协议驱动(如 OPC、ModBus 等)能够以统一的方式进行数据交互。当开发者需要添加一个新的数据源驱动时,只需实现IDataSource
接口,就可以确保它能够与系统的其他部分无缝集成。 - 通信接口 :
ICommunicationDriver
是通信相关的接口,它规定了通信驱动的基本行为,例如建立通信连接、发送和接收数据等。这使得系统能够轻松地扩展对不同通信协议的支持。例如,如果要添加一个新的无线通信协议驱动,开发者可以基于ICommunicationDriver
接口进行实现,而无需修改系统的其他核心逻辑。
- 数据接口 :Common 模块定义了一系列与数据相关的接口,比如
- 数据模型
- 标签数据模型(Tag) :在 Common 模块中,标签(Tag)是数据的基本单位。
Tag
类定义了标签的属性,如名称、数据类型、描述、当前值、质量(表示数据的可靠性,如好、坏、可疑等)、时间戳等。例如,一个温度传感器的标签可能有名称为 “TemperatureSensor1”,数据类型为浮点数,当前值为 25.5℃,质量为 “Good”,时间戳为当前系统时间。这些标签数据模型为系统中的数据采集、存储、处理和可视化提供了统一的数据结构。
- 标签数据模型(Tag) :在 Common 模块中,标签(Tag)是数据的基本单位。
- 工具类
- 日志工具类(Logger) :日志记录对于系统的调试、监控和维护至关重要。
Logger
类提供了方便的日志记录功能,包括记录不同级别的日志(如信息、警告、错误等)。例如,当系统成功连接到一个设备时,可以记录一条信息级别的日志;当数据采集出现异常时,记录一条错误级别的日志。这有助于开发者快速定位问题所在,了解系统的运行状态。
- 日志工具类(Logger) :日志记录对于系统的调试、监控和维护至关重要。
- 配置管理
- 环境类(Env),它实现了
IEnvironment
接口。这个类主要用于管理和初始化FreeSCADA环境,包括版本信息、命令、主窗口、项目、环境模式、日志记录、脚本管理以及通信和可视化插件等。Env
类使用了单例模式,确保在整个应用程序中只有一个Env
实例。
- 环境类(Env),它实现了
在系统中的作用与与其他模块的交互
- 在系统中的作用 :Common 模块是整个 FreeSCADA 2 系统的 “粘合剂”。它为其他模块提供了统一的接口规范、数据模型和工具,确保了系统的各个部分能够以协调一致的方式工作。没有 Common 模块,其他模块之间就难以进行有效的通信和数据共享。
- 与其他模块的交互 :Designer 模块在创建和配置项目时,会频繁地调用 Common 模块中的接口和工具类。例如,在定义标签时,会使用
Tag
类来设置标签的属性;在配置通信协议时,会参考ICommunicationDriver
接口的规范。Run Time 模块在运行项目时,会根据ProjectConfig
类中的配置信息来初始化数据源和通信连接,并利用DataProcessorUtils
类对采集到的数据进行处理。同时,Run Time 模块也会通过Logger
类记录运行过程中的各种事件和状态信息。
数据采集
在 Communication
目录下,包含了各种通信协议驱动的实现代码。比如在 FreeSCADA 2 的 Communication.OPCPlug 模块中,ICommunicationDriver 接口发挥着关键作用,它定义了通信驱动的基本规范,使得 OPCPlug 模块能够与系统其他部分高效协同,实现数据的精准传递。
IChannel
Interface
定义了通道(Channel)的基本功能和属性,如名称(Name
)、插件标识(PluginId
)、值(Value
)、是否只读(IsReadOnly
)、状态标志(StatusFlags
)等。
提供了事件,如PropertyChanged
和ValueChanged
,用于通知其他部分通道属性或值的变化。
是一个契约,规定了通道类需要实现的成员。
BaseChannel
Abstract Class
实现了IChannel
接口,提供了通道的基本功能和公共实现。
包含了通道的核心属性和方法,如获取通道名称、类型、值,以及更新值和状态的方法。
提供了抽象方法DoUpdate()
和虚方法DoUpdate(object value)
、DoUpdate(object value, DateTime externalTime, ChannelStatusFlags status)
,供子类根据具体通信协议实现数据更新逻辑。
具备线程安全机制,使用lock
关键字确保在多线程环境下数据的一致性和完整性。
是一个抽象基类,不能直接实例化,需要由子类继承并实现抽象方法。
OPCBaseChannel
Class
继承自BaseChannel
抽象类,是针对OPC(OLE for Process Control)协议通信的具体实现。
包含了与OPC服务器通信相关的属性,如OPC通道名称(OpcChannel
)、OPC服务器名称(OpcServer
)、OPC主机(OpcHost
)、连接组(opcConnection
)和OPC句柄(opcHandle
)。
重写了Value
属性的set
访问器,在设置新值时,会通过OPC连接将值写入OPC服务器,并调用基类的DoUpdate
方法更新本地值。
提供了Connect
和Disconnect
方法,用于与OPC服务器建立和断开连接。
重写了DoUpdate
方法,目前为空,需要根据具体OPC通信逻辑进行实现。
数据流向
数据从OPC服务器流向OPCBaseChannel
:
OPCBaseChannel
通过Connect
方法与OPC服务器建立连接,并获取到OPC句柄opcHandle
。
当需要从OPC服务器读取数据时,OPCBaseChannel
通过其内部的OPC连接(opcConnection
)从OPC服务器读取数据。
读取到的数据会更新到OPCBaseChannel
的Value
属性中。
更新Value
属性会触发基类的FireValueChanged
方法,进而引发PropertyChanged
和ValueChanged
事件,通知其他订阅者数据已更新。
数据从OPCBaseChannel
流向OPC服务器:
当OPCBaseChannel
的Value
属性被设置时,如果通道不是只读的,并且与OPC服务器的连接已建立,它会通过opcConnection.WriteChannel(opcHandle, value)
将新值写入OPC服务器。
同时,它会调用基类的DoUpdate
方法更新本地值和状态。
数据在OPCBaseChannel
内部流动:
通道的状态(StatusFlags
)可以通过StatusFlags
属性设置,这会更新modifyTime
并触发FireValueChanged
方法,进而引发相关事件。
通道的重置操作(Reset
方法)会将值恢复为默认值,并更新状态和时间。
数据从OPCBaseChannel
流向其他部分:
通过PropertyChanged
事件,当通道的Value
、ModifyTime
、Status
或StatusFlags
属性发生变化时,会通知其他订阅者(如用户界面或数据处理模块),使它们能够获取到最新的数据。
通过ValueChanged
事件,当通道的值发生变化时,会通知其他订阅者,使它们能够对值的变化做出响应。
总结
这三个类的关系和数据流向如下:
IChannel
定义了通道的契约,BaseChannel
实现了这个契约并提供了通用功能,OPCBaseChannel
继承自BaseChannel
并实现了针对OPC协议的具体功能。
数据从OPC服务器流向OPCBaseChannel
,更新其值和状态;OPCBaseChannel
内部处理数据的更新和状态变化,并通过事件将数据变化通知给其他部分。
这种设计通过接口和抽象类实现了代码的复用和扩展性,使得可以方便地添加对其他通信协议的支持,只需继承BaseChannel
并实现相应的通信逻辑即可。
自定义控件
VisualControls.FS2EasyControls 模块是 FreeSCADA 2 系统中用于提供可视化控件的核心组件库,旨在为用户提供更丰富、便捷的图形化界面元素,用于直观地展示和操作工业自动化过程中的各类数据。这些控件涵盖了从基本的数据显示(如数值、文本)、状态指示(如指示灯、报警器),到复杂的交互式图表(如趋势图、棒图)等多种类型,是实现系统人机交互界面(HMI)的关键。
IVisualControlsPlug 接口在 FS2EasyControls 中的实现
接口方法实现
数据源绑定
BindDataSource
方法:这是实现控件与数据源关联的核心方法。它接收数据源标识符(如标签名称)和数据绑定规则(如数据类型转换、数据更新频率等)作为参数。FS2EasyControls 模块中的每个可视化控件都通过这个方法与 FreeSCADA 2 系统中的特定数据(如从 OPC 服务器、ModBus 设备等采集的数据)建立联系。
例如,对于一个用于显示温度的数字显示屏控件,通过 BindDataSource
方法将其与表示温度传感器数据的标签 “Temperature.Sensor1” 绑定。在绑定过程中,可以指定数据类型为浮点数,更新频率为每秒一次等规则。这样,控件就能够实时获取该温度数据的变化,并在界面上进行相应更新。
数据更新与刷新
UpdateData
方法:用于将最新的数据从数据源拉取到控件,并触发控件的刷新操作。当系统中的数据(如通过 Communication 模块从设备采集到的新数据)发生变化时,FS2EasyControls 模块会调用控件的 UpdateData
方法。该方法内部会根据控件的类型和绑定规则,对数据进行适当的处理(如数值修约、单位转换等),然后更新控件的显示内容。
比如,在一个模拟压力表控件中,当 UpdateData
方法被调用并接收到新的压力值数据时,它会根据压力值计算指针的旋转角度,重新绘制压力表的图形界面,使指针指向对应的压力刻度位置,从而直观地反映当前压力的变化情况。
用户交互响应
OnUserInteraction
方法:用于处理用户与控件之间的交互操作,如点击按钮、调整滑块、输入文本等。当用户对可视化控件进行操作时,FS2EasyControls 模块会捕捉到这些操作事件,并通过 OnUserInteraction
方法进行相应处理。
例如,在一个用于控制电机启停的按钮控件中,当用户点击启动按钮时,OnUserInteraction
方法会识别这个点击事件,然后根据预先定义的控制逻辑(如向 PLC 发送电机启动命令),通过 FreeSCADA 2 系统的通信模块将控制指令发送到相应的设备。同时,它还可以更新按钮本身的显示状态(如改变按钮颜色为绿色表示电机已启动)。
数据封装与传递
数据封装
在 FS2EasyControls 模块中,数据从数据源传递到控件时,会按照 FreeSCADA 2 系统统一的数据模型(如 Common 模块中的 Tag
类)进行封装。控件接收到的数据包含数据值、质量(如好、坏、可疑等)、时间戳等关键信息。例如,从 OPC 服务器获取的流量数据 “FlowMeter1” 会封装为一个 Tag
对象,其 Value 属性为当前流量值(如 120m³/h),Quality 属性为数据质量(假设为 “Good”),TimeStamp 属性为数据更新时间。
数据传递流程
当系统中的数据源(如通过 OPCPlug 模块连接的 OPC 服务器)有数据更新时,数据首先会按照 ICommunicationDriver 的规范传递到 FreeSCADA 2 的数据处理层。然后,数据处理层根据可视化界面的配置(如哪个控件绑定了该数据源),调用 FS2EasyControls 模块中相应控件的 UpdateData
方法。控件接收到数据后,根据自身类型和绑定规则进行处理,并更新显示内容。同时,控件在处理数据过程中,也可以将一些状态信息(如数据是否在正常范围内、是否触发报警等)反馈给系统,以便系统进行进一步的处理(如生成报警信息推送等)。