软件开发中的抽象胡言乱语。实时






4.90/5 (35投票s)
抽象方法在实时中的应用。
- 下载航空项目源代码 4.9 MB
- 下载附加文件 352 KB
- 下载音频文件 352 KB
- 下载实时控制系统示例 5.2 KB
- 下载实时控制系统 Windows Forms 应用程序源代码 727 KB
- 下载远程控制示例 144 KB
- 下载远程控制 Windows Forms 应用程序源代码 737 KB
- 下载视频 + 音频示例 7.5 MB
- 下载视频 + 音频 WPF 应用程序源代码 8.4 MB
- 下载实时卫星动画示例 495 KB
- 下载实时卫星动画 WPF 应用程序源代码 1.53 MB
有用链接
- 通用工程软件主页
- 原文
- Microsoft Visual Studio Express
- OPC 基金会
- Weifen Luo 主页
- 范畴论
- 一个简化的非主题事件通知服务器,支持多种协议的 WCF
- 俄罗斯水文气象中心
- .NET 的 Zip、GZip、BZip2 和 Tar 实现
- 开源 .NET SCADA 框架
- 适用于 Silverlight 3 和 WPF 的圆形仪表自定义控件
- Laurence Chisholm Young。变分法和最优控制理论讲座
- 非交换几何和量子群讲义
- NASA 地球观测
- NORAD 两行元素集
- HTML 到 XAML 转换器
- Celestia。实时 3D 空间可视化
- 从地面到太空的地球大气层经验全球模型 (NRLMSISE-00) 源代码
1 简介。
本文包含我上一篇文章的进一步发展。数学中的抽象胡言乱语(范畴论)是不同数学分支的通用语言,可以看作是代码复用。同样,我的程序组件数量随着功能的增加而减少。2007 年至 2010 年期间,我开发了SCADA系统。我发现OPC 基金会是一项出色的实时技术。然而,许多航空航天公司不理解这个事实。很多人认为工业企业的实时控制与航空航天控制有本质区别。结果,一些公司使用套接字而不是OPC 基金会。本文包含一种统一的实时方法,并附有工业控制和航空航天示例。最近我参加了一个关于Simulink应用的研讨会。讲师说了以下内容。
++++++++++
如果您想开发控制系统,那么我们公司将为您提供帮助。++++++++++
这意味着Simulink仅对Simulink的开发人员来说是相当清楚的。Simulink是一个功能强大且设施庞大的产品。然而,这种情况使得许多人无法使用Simulink。我认为可以减少Simulink组件的数量,从而使Simulink支持以前的功能,但变得更清晰和可用。我想让我的软件更清晰和可用。我发现 SCADA 系统的组件数量相对较少。然而,少量的组件提供了非常丰富的功能。
Laurence Chisholm Young 写道。
+++++++++++++++++++++
有时,一些著名的数学家会表达这样一种观点:一段数学的价值和趣味性与其所投入的真正艰苦工作量直接相关。这无疑是非常正确的,但必须记住,这种艰苦工作最好在幕后完成,其中大部分工作都是为了将材料整理成易于理解的形式。如果读者只重视他觉得难以理解的东西,他很可能会对今天的数学产生非常扭曲的看法。同样,早期驾驶汽车的人可能会通过汽车发出的噪音和扬起的灰尘来判断汽车的动力,但这在我们这个时代并不适用。在数学领域,那些以证明页数或用秒表测量理解证明所需时间为标准来衡量结果价值的人,在现代经历了一系列的冲击,因为以前需要冗长复杂论证的定理找到了简单的证明。事实是,数学真的像一件艺术品:它需要无限的努力,但最终结果却丝毫没有展现这些努力,没有任何东西能分散观者对其带来的深刻理解和洞察力的注意力。
+++++++++++++++++++++
同样,我想开发“轻量级”版本的 Simulink。
2 背景
我发现OPC Foundation是一项出色的实时技术。然而,OPC Foundation是一个商业产品,我无法将其作为开源共享。任何开发人员都可以将OPC Foundation集成到我的软件中。为了使我的软件免版税,我开发了OPC Foundation的代理。与OPC Foundation一样,我的代理包含以下要素:
- (远程)事件
- (远程)数据流
抽象胡言乱语模式用于实时。下图解释了此模式在实时中的应用。
事件对象引发事件,事件处理程序处理事件。此示例为空。以下电影不为空,并展示了一些定性 SCADA 功能(事件、数据指示)。
Input 对象生成两个与时间相关的函数。
Event 对象引发事件。Event handler 对象处理事件,即它从 Input 获取信息并进行指示。
实时领域包含以下基本类型。
IEvent
:任何引发事件的对象都实现此接口。IEventHandler
:任何事件处理程序都实现此接口。EventLink
:事件和事件处理程序之间的链接。此链接的源(或目标)应实现IEvent
(或IEventHandler
接口)。
事件及其处理程序之间存在多对多关系。以下代码包含这些基本类型。
/// <summary> /// Event /// </summary> public interface IEvent { /// <summary> /// Raised event /// </summary> event Action Event; /// <summary> /// Is enabled sign /// </summary> bool IsEnabled { get; set; } }
/// <summary> /// Event handler /// </summary> public interface IEventHandler { /// <summary> /// Adds event /// </summary> /// <param name="ev">The event to add</param> void Add(IEvent ev); /// <summary> /// Removes event /// </summary> /// <param name="ev">The event to remove</param> void Remove(IEvent ev); /// <summary> /// The On Add event /// </summary> event Action<IEvent> OnAdd; /// <summary> /// The On Remove event /// </summary> event Action<IEvent> OnRemove; }
/// <summary> /// Link between event and its handler /// </summary> [Serializable()] public class EventLink : CategoryArrow, IRemovableObject { #region Fields IEvent target; IEventHandler source; #endregion #region Ctor /// <summary> /// Default constructor /// </summary> public EventLink() { } /// <summary> /// Deserialization constructor /// </summary> /// <param name="info">Serialization info</param> /// <param name="context">Streaming context</param> protected EventLink(SerializationInfo info, StreamingContext conext) { } #endregion #region Overriden Members /// <summary> /// The source of this arrow /// </summary> public override ICategoryObject Source { get { return source as ICategoryObject; } set { source = value.GetSource<IEventHandler>(); } } /// <summary> /// The target of this arrow /// </summary> public override ICategoryObject Target { get { return target as ICategoryObject; } set { target = value.GetTarget<IEvent>(); source.Add(target); } } #endregion #region IRemovableObject Members void IRemovableObject.RemoveObject() { if ((source == null) | (target == null)) { return; } source.Remove(target); source = null; } #endregion }以下文本包含此范例应用的示例。
3 基本事件
3.1 强制事件
许多工程系统需要人机界面。ForcedEvent
就是为此功能而设计的。以下代码解释了此类的业务逻辑。
/// <summary> /// Forced event /// </summary> [Serializable()] public class ForcedEvent : CategoryObject, ISerializable, IEvent { #region Fields //.... event Action ev = () => { }; #endregion #region IEvent Members event Action IEvent.Event { add { ev += value; } remove { ev -= value; } } //... #endregion #region Public Members /// <summary> /// Forces event /// </summary> public void Force() { ev(); } #endregion
Force
方法引发事件。以下电影解释了强制事件的工作原理。
3.2 计时器
计时器是一种对象,它以固定间隔引发不同的事件。.NET Framework 支持多种计时器。System.Windows.Forms.Timer
适用于 System.Windows.Forms
应用程序,而 System.Windows.Threading.DispatcherTimer
适用于 WPF。通用软件应支持任何类型的计时器。为此目的开发了以下两个接口:
/// <summary> /// Timer event /// </summary> public interface ITimer : IEvent { /// <summary> /// Time span /// </summary> TimeSpan TimeSpan { get; set; } }
/// <summary> /// Timer factory /// </summary> public interface ITimerFactory { /// <summary> /// New timer /// </summary> ITimer NewTimer { get; } }
这些接口的 Windows Forms 实现如下所示。
/// <summary> /// Windows Forms Timer /// </summary> public class Timer : ITimer, IDisposable { #region Fields event Action ev = () => { }; System.Windows.Forms.Timer timer = new System.Windows.Forms.Timer(); bool isEnabled = false; TimeSpan timeSpan = new TimeSpan(); #endregion #region Ctor internal Timer() { timer.Tick += (object sender, EventArgs e) => { ev(); }; } #endregion #region ITimer Members TimeSpan ITimer.TimeSpan { get { return timeSpan; } set { timeSpan = value; if (value.TotalMilliseconds <= 0) { timer.Interval = 1; return; } timer.Interval = (int)value.TotalMilliseconds; } } #endregion #region IEvent Members event Action Interfaces.IEvent.Event { add { ev += value; } remove { ev -= value; } } bool Interfaces.IEvent.IsEnabled { get { return isEnabled; } set { if (value == isEnabled) { return; } isEnabled = value; timer.Enabled = value; if (value) { timer.Start(); } else { timer.Stop(); } } } #endregion #region IDisposable Members void IDisposable.Dispose() { timer.Dispose(); } #endregion }
/// <summary> /// Factory of windows timers /// </summary> public class WindowsTimerFactory : ITimerFactory { #region Fields /// <summary> /// Singleton /// </summary> public static WindowsTimerFactory Singleton = new WindowsTimerFactory(); #endregion #region Ctor private WindowsTimerFactory() { } #endregion #region ITimerFactory Members ITimer ITimerFactory.NewTimer { get { return new Timer(); } } #endregion }
3.3 事件的“直和”
3.3.1 范畴论类比
数学中的抽象胡言乱语(范畴论)为不同的问题提供了统一的方法。这种方法非常富有成效。范畴论促进了数学家之间的更好理解,也促进了软件开发中的设计模式。例如,范畴论处理抽象直积,如下所示。
上图意味着存在一个唯一的虚线箭头,使得上图是可交换的。如果我们有一个集合范畴,A(或 B)是一个有 m(或 n)个元素的有限集,那么 A×B 是一个有 mn 个元素的集合。然而,在线性空间的情况下,如果 A 是 1D 空间,B 是 2D 空间,那么 A×B 是一个 3D 空间。直和的概念是通过反转箭头获得的。
如果我们有一个集合范畴,A(或 B)是一个有 m(或 n)个元素的有限集,那么 A+B 是一个有 m + n 个元素的集合。此外,这些范畴定义为我们理解最初的数学概念提供了新的见解,例如自然数的乘法和加法,集合的交集、积和并集,以及数理逻辑中的合取和析取。特别是,它们使加法与乘法对偶,并使不相交并集比普通并集更自然。简单来说,每个人都知道,例如,
a + b = b + a 和 ab = ba(对于自然数 a 和 b),
但只有范畴论告诉我们这些等式是单个结果的特例!(上述引文来源)
让我们考虑 3D 图形中的“直和”。以下两张图片展示了直升机复杂的运动
所以上面的直升机包含 7 个 3D 形状(机身、尾桨和主旋翼的 5 个叶片)。这些形状的“直和”是整个直升机,如下所示。
L 1 - L 5、Fuselage、Tail rotor 分别是叶片 1-5、机身和尾桨的 3D 形状。Full helicopter 3D 是这些形状的“直和”
因为它通过链接与其他形状连接。否则,五个虚拟摄像头通过 5 个箭头连接到 Full helicopter 3D。如果我们将 5 个摄像头连接到 7 个 3D 形状,我们需要 5 * 7 = 35 个箭头。结果,我们有 7 + 5 = 12 个箭头,而不是 5 * 7 = 35 个。
3.3.2 事件的“直和”
与 3D 图形一样,事件也具有直和。下图包含两个基本事件 Forced 和 Timer。
Event collection 对象是这些基本事件的“直和”。Event collection 是 EventCollection
类的一个对象。以下代码解释了它的工作原理:
#region IEvent Members event Action IEvent.Event { add { foreach (IEvent e in ev) { e.Event += value; } } remove { foreach (IEvent e in ev) { e.Event -= value; } } } bool IEvent.IsEnabled { get { return isEnabled; } set { if (isEnabled == value) { return; } foreach (IEvent e in ev) { e.IsEnabled = value; } isEnabled = value; } } #endregion
4 事件 + 信息流
任何实时系统都支持信息流。某些实时对象同时是事件和信息源。例如,任何OPC 订阅都提供数据并引发 ItemChanged
事件(参见此处)。我的框架也支持信息流。这里我们考虑那些同时是数据流元素和事件的对象。
4.1 强制数据事件
4.1.1 展望
此对象用于人机界面。此对象可用于 SCADA 系统和航空模拟器。强制事件数据对应于实现 IEvent
和 IMeasurements
的 ForcedEventData
类。因此,它引发事件,并且是信息源。以下代码解释了此类的逻辑。
/// <summary> /// Forced event data /// </summary> [Serializable()] public class ForcedEventData : CategoryObject, ISerializable, IMeasurements, IEvent, IAlias, IStarted { #region Fields /// <summary> /// Data /// </summary> object[] data; object[] initial; event Action ev = () => { }; Action force = () => { }; //... #endregion #region IEvent Members event Action IEvent.Event { add { ev += value; } remove { ev -= value; } } bool IEvent.IsEnabled { get { return isEnabled; } set { if (isEnabled == value) { return; } isEnabled = value; force = value ? ev : () => { }; } } #endregion #region Public Members /// <summary> /// Data /// </summary> public object[] Data { get { return data; } set { data = value; force(); } } //... #endregion //...
以上代码意味着数据的改变会引发一个事件。强制事件具有以下用户界面。
该接口使我们能够设置数据元素的数量及其类型。此外,该接口使我们能够更改数据的初始值和当前值。
4.1.2 在控制系统中的应用
让我们考虑由以下方程描述的控制系统。
其中
- x - 物理参数的实际值;
- y - 物理参数的所需值;
- c - 常数;
物理参数的所需值由人设定。下图附带的视频解释了这一现象。
Forced 是 ForcedEventData
类型的一个对象。人使用此对象输入所需值。Timer 是一个计时器事件生成器。Event collection 是 Timer 和 Forced 事件的“直和”。Equation 包含控制系统的微分方程。Equation 的属性如下所示。
4.1.4 用户界面
以上用户界面对设计很有用,但对工作不方便。以下用户界面适合工作。
用户界面问题在下面解释。
4.2 远程事件
4.2.1 客户端和服务器
任何高级实时系统都应支持远程事件。我使用了“一个简化的非主题事件通知服务器,支持多种协议的 WCF”作为远程事件的原型。远程对象包含服务器和客户端。服务器引发事件并提供信息。客户端处理事件并接收信息。以下状态图解释了客户端-服务器互操作性:
Register
方法具有以下签名。
/// <summary> /// This is the Service contract for Subscription Service /// </summary> [ServiceContract(CallbackContract = typeof(IEvent))] public interface IRegistration { /// <summary> /// Registers itself /// </summary> /// <returns>Values of output names and types</returns> [OperationContract] string[] Register(); /// <summary> /// Unregisters itself /// </summary> [OperationContract] void UnRegister(); }
Register
方法返回一个字符串数组。此数组的奇数元素是参数名称,偶数元素是相应的类型名称。此远程方法在初始化时由客户端调用。服务器调用 OnEvent
远程方法。
/// <summary> /// This is the service contract of Publish Service /// </summary> [ServiceContract] interface IEvent { /// <summary> /// Event handler /// </summary> /// <param name="data">Alert data</param> [OperationContract(IsOneWay = true)] void OnEvent(AlertData data); } /// <summary> /// Alert data /// </summary> [DataContract] [KnownType(typeof(object[]))] public class AlertData { /// <summary> /// Data /// </summary> [DataMember] public object[] Data { get; set; } }
以上代码表示服务器将一个对象数组传输给客户端。此元素的逻辑与OPC 服务器组相似。
下图显示了服务器示例。
上图表示 Server 的输出参数是数据 Data 的 Formula_1 和 Formula_2,Server 使用进程间通信连接,连接的URL是 "net.pipe:///Data"。Data 的布尔参数 Formula_3 是事件条件,即当且仅当此参数等于 true
时才引发事件。Data 的属性如下所示:
数据包含三个输出参数。
N | 名称 | 类型 |
1 | Formula_1 | double |
2 | Formula_2 | 字符串 |
3 | Formula_3 | bool |
第一个和第二个参数由 Server 导出,第三个是事件条件。导出的数据由客户端使用,如下所示。
Client 通信参数与服务器参数一致。Client 执行两个功能:
- 它提供数据;
- 它引发事件。
导入的参数 Formula_1 和 Formula_2 由 DataConsumer 使用。DataConsumer 既是
- 数据消费者;
- 事件处理程序。
DataConsumer 通过 Data 箭头连接到 Client 作为数据消费者,并通过 Event 箭头连接作为事件处理程序。
4.2.2 多个应用程序的启动
以上示例暗示服务器和客户端作为不同的应用程序运行,如下所示:
这些应用程序之间存在互操作性。我们需要使用不同的文件启动应用程序的多个实例。但是,如果文件被压缩成单个文件,那么将单个 zip 文件拖放到桌面会自动启动多个实例。我为此目的使用了.NET 的 Zip、GZip、BZip2 和 Tar 实现库。
4.2.3 在温度控制中的应用
让我们考虑一个温度控制任务。温度由加热器控制,加热器有“开”和“关”位置。这种物理现象由以下常微分方程描述。
其中
- z 是加热参数。(z = (加热器“关闭”) ? 0 : d;
- a, b - 正实常数 s;
- T(或 Texternal)受控温度(或外部温度)。
下图表示此现象的模拟器:
Temperature 对象提供来自俄罗斯水文气象中心的信息。此对象的属性如下所示。
.
以上组件提供以下参数。
N | 参数名称 | 参数类型 | 参数当前值 |
1 | 大气压,毫米汞柱 | double | 760 |
2 | 温度,°C | double | -1.8 |
3 | 最低温度,°C | double | - |
4 | 最高温度,°C | double | - |
5 | 注释 | 字符串 | 雪强弱 |
6 | 风向 | 字符串 | S |
7 | 风速,米/秒 | double | 2 |
1 | 总云量值 | double | 10 |
9 | 水平能见度,公里 | double | 4 |
此对象仅用作外部温度计。Control Signal 是一个远程客户端,提供加热参数。Timer 和 Control Signal 都是事件生成器。Event collection 是这些事件的组合。Equation 对象提供必要的微分方程。
Recursive 用于引发OPC DataChange 事件的模拟,此对象的属性如下所示。
Recursive 的参数 d 是阈值,b 是 Server 对象的事件条件。我们使用开关控制律,其表达如下。
下图展示了控制器。
控制器和模拟器之间存在反馈,如下表所示。
N | URL | 服务器端 | 服务器对象 | 客户端侧 | 客户端对象 |
1 | net.pipe:///Sensor | 模拟器 | Sensor | 控制器 (Controller) | Sensor |
2 | net.pipe:///ControlSignal | 控制器 (Controller) | 服务器 | 模拟器 | 控制信号 |
Required 对象使我们能够设置所需的温度值,它引发一个 OnChange
事件。Turn_off_control 实现控制律,此对象的属性如下所示。
All events 对象是 Sensor 和 Required 引发的事件的组合(“直和”),即,如果所需温度或实际温度值发生实质性变化,则 All events 被引发。如果控制器输出值发生变化,则 Server 引发远程事件并导出控制器输出值。
除了进程间通信,所有客户端和服务器都支持TCP和/或HTTP通信。因此,现象模拟器和控制器可以安装在不同的计算机上。下图展示了 TCP 通信。
此示例需要附加文件。
5 用户界面
任何 SCADA 系统都包含用户界面设计器,下图展示了此类设计器的示例。
然而,Visual Studio 可以用作 SCADA 用户界面的设计器。
5.1 Windows Forms 设计器
我使用了开源 .NET SCADA 框架作为原型。下图包含温度控制示例的用户界面设计。
上图表示被编辑的对象是 Timer 的事件处理程序,即被编辑的对象仅当 Timer 引发事件时才改变值。被编辑对象的 Output 是 Sensor 的 Reqursive_x 参数。类似的图,下图包含滑块的属性。
上图表示此滑块使我们能够输入 Required 的 Temperature。温度计和滑块都实现了以下接口:
/// <summary> /// Consumer of SCADA /// </summary> public interface IScadaConsumer { /// <summary> /// Scada interface /// </summary> IScadaInterface Scada { get; set; } /// <summary> /// Is enabled /// </summary> bool IsEnabled { get; set; } }
其中 IScadaInterface
是抽象 SCADA
/// <summary> /// Scada interface /// </summary> public interface IScadaInterface { /// <summary> /// Inputs /// </summary> Dictionary<string, object> Inputs { get; } /// <summary> /// Outputs /// </summary> Dictionary<string, object> Outputs { get; } /// <summary> /// Constants /// </summary> Dictionary<string, object> Constants { get; } /// <summary> /// Events /// </summary> List<string> Events { get; } /// <summary> /// Gets input /// </summary> /// <param name="name">Input name</param> /// <returns>Input</returns> Action<object> GetInput(string name); /// <summary> /// Gets inputs /// </summary> /// <param name="names">Input names</param> /// <returns>Input names</returns> Action<object[]> GetInput(string[] names); /// <summary> /// Gets constant /// </summary> /// <param name="name">Constant</param> /// <returns>Constant</returns> Action<object> GetConstant(string name); /// <summary> /// Gets output /// </summary> /// <param name="name">Name</param> /// <returns>Output</returns> Func<object> GetOutput(string name); /// <summary> /// Gets outputs /// </summary> /// <param name="names">Names</param> /// <returns>Outputs</returns> Func<object[]> GetOutput(string[] names); /// <summary> /// Gets event /// </summary> /// <param name="name">Event name</param> /// <returns>The event</returns> IEvent this[string name] { get; } /// <summary> /// Gets object of type /// </summary> /// <typeparam name="T">Type</typeparam> /// <param name="name">Object name</param> /// <returns>The object</returns> T GetObject<T>(string name) where T : class; /// <summary> /// The "is enabled" sign /// </summary> bool IsEnabled { get; set; } /// <summary> /// On start event /// </summary> event Action OnStart; /// <summary> /// On Stop event /// </summary> event Action OnStop; /// <summary> /// Create XML event /// </summary> event Action<XmlDocument> OnCreateXml; /// <summary> /// Xml document /// </summary> XmlDocument XmlDocument { get; } /// <summary> /// Error Handler /// </summary> IErrorHandler ErrorHandler { get; set; } /// <summary> /// Refresh /// </summary> void Refresh(); /// <summary> /// On refresh event /// </summary> event Action OnRefresh; }
滑块和温度计的 IScadaConsumer
实现如下所示。
// Slider #region Fields string inputString; Action<float> input; #endregion [DefaultValue("")] [Editor(typeof(ListGridComboBox), typeof(UITypeEditor))] [DataList("GetInputs")] [TypeConverter(typeof(ListExpandableConverter))] [Category("SCADA"), Description("Input name"), DisplayName("Input")] public string Output { get { return inputString; } set { inputString = value; } } #region IScadaConsumer Members IScadaInterface IScadaConsumer.Scada { get { return scada; } set { if (value == null) { return; } scada = value; input = GetFloatInput(scada, inputString); } } bool IScadaConsumer.IsEnabled { get { return isEnabled; } set { if (isEnabled == value) { return; } isEnabled = value; if (value) { this.ValueChanged += Slider_ValueChanged; } else { this.ValueChanged += Slider_ValueChanged; } } } #endregion #region Public Membres /// <summary> /// Gets float input function /// </summary> /// <param name="scada">The SCADA</param> /// <param name="name">The function name</param> /// <returns>The function</returns> public static Action<float> GetFloatInput(this IScadaInterface scada, string name) { Action<object> action = scada.GetInput(name); return GetFloatInput(action, scada.Inputs[name]); } #endregion #region Private Members static Action<float> GetFloatInput(Action<object> action, object type) { Type t = type.DetectType(); if (t.Equals(typeof(float))) { return (float x) => { action(x); }; } else { return (float x) => { double a = (double)x; action(a); }; } } void Slider_ValueChanged(object sender, EventArgs e) { input(_val); } #endregion
// Thermometer #region Scada Input Fields string eventString; string outputString; Func<float> output; IScadaInterface scada; IEvent eventObject; bool isEnabled; #endregion #region Public Members /// <summary> /// Event string /// </summary> [DefaultValue("")] [Editor(typeof(ListGridComboBox), typeof(UITypeEditor))] [DataList("GetEvents")] [TypeConverter(typeof(ListExpandableConverter))] [Category("SCADA"), Description("Event name"), DisplayName("Event")] public string Event { get { return eventString; } set { eventString = value; } } /// <summary> /// Output string /// </summary> [DefaultValue("")] [Editor(typeof(ListGridComboBox), typeof(UITypeEditor))] [DataList("GetOutputs")] [TypeConverter(typeof(ListExpandableConverter))] [Category("SCADA"), Description("Output name"), DisplayName("Output")] public string Output { get { return outputString; } set { outputString = value; } } #endregion #region IScadaConsumer Members IScadaInterface IScadaConsumer.Scada { get { return scada; } set { if (value == null) { return; } scada = value; eventObject = scada[eventString]; output = GetFloatOutput(scada, outputString); } } bool IScadaConsumer.IsEnabled { get { return isEnabled; } set { if (isEnabled == value) { return; } isEnabled = value; if (value) { eventObject.Event += Set; } else { eventObject.Event -= Set; } } } #endregion #region Private Members void Set() { Set(output()); } public void Set(float val) { if (base.Enabled) { float temp = _val; if (val > _max) val = _max; if (val < _min) val = _min; _val = val; if (temp != _val) { OnValueChanged(); base.Invalidate(); base.Update(); } } } static Func<float> GetFloatOutput(IScadaInterface scada, string name) { Func<object> f = scada.GetOutput(name); return GetFloatOutput(f, scada.Outputs[name]); } static Func<float> GetFloatOutput(Func<object> func, object type) { Type t = type.DetectType(); if (t.Equals(typeof(float))) { return () => { return (float)func(); }; } else { return () => { double a = (double)func(); return (float)a; }; } } #endregion
SCADA 与 Windows Forms 的互操作性通过递归实现,如下所示。
#region Public Members /// <summary> /// Recursive action of control /// </summary> /// <param name="control">The control</param> /// <param name="action">The action</param> public static void RecursiveAction(this Control control, Action<Control> action) { action(control); foreach (Control c in control.Controls) { c.RecursiveAction(action); } } /// <summary> /// Sets scada intrerface to control /// </summary> /// <param name="control">The control</param> /// <param name="scada">The scada</param> public static void Set(this Control control, IScadaInterface scada) { control.SetPrivate(scada); scada.OnRefresh += () => { control.SetPrivate(scada); }; scada.OnStart += () => { control.SetScadaEnabled(true); }; scada.OnStop += () => { control.SetScadaEnabled(false); }; } /// <summary> /// Sets "is enabled" sign to control /// </summary> /// <param name="control">The control</param> /// <param name="isEnabled">The is enabled sign</param> public static void SetScadaEnabled(this Control control, bool isEnabled) { control.RecursiveAction((Control c) => { if (c is IScadaConsumer) { (c as IScadaConsumer).IsEnabled = isEnabled; } }); } #endregion #region Private & Internal Members private static void SetPrivate(this Control control, IScadaInterface scada) { control.RecursiveAction((Control c) => { if (c is IScadaConsumer) { (c as IScadaConsumer).Scada = scada; } }); } #endregion
下图附带视频展示了温度控制示例。
上图表示两个应用程序
- 物理现象模拟器;
- 控制器。
启动包括以下两个步骤。
1. 在设计模式下启动模拟器。用户应启动 Aviation.exe
,并从ipc_control.zip 144 KB中打开 sensor.cfa
文件
2. 启动控制器应用程序。
5.2 WPF 用户界面
我认为 WPF 技术比 Windows Forms 更接近 SCADA。System.Windows.Forms
暗示了一个常见的事件循环。然而,任何 WPF UI 元素都有自己的线程,因此也有自己的调度程序。所以我们可以将不同的 WPF UI 元素与不同的事件关联起来。一些 SCADA 系统不包含用户界面,因为它们提供没有 HMI 的控制。不同类型的 SCADA 意味着不同类型的计时器。如果我们使用 WPF,我们应该用以下计时器替换Windows Forms 计时器。
////// WPF timer /// public class Timer : ITimer, IDisposable { #region Fields event Action ev = () => { }; DispatcherTimer timer = new DispatcherTimer(); bool isEnabled = false; TimeSpan timeSpan = new TimeSpan(); #endregion #region Ctor internal Timer() { timer.Tick += (object sender, EventArgs e) => { ev(); }; } #endregion #region ITimer Members TimeSpan ITimer.TimeSpan { get { return timeSpan; } set { timeSpan = value; timer.Interval = value; } } #endregion #region IEvent Members event Action Interfaces.IEvent.Event { add { ev += value; } remove { ev -= value; } } bool Interfaces.IEvent.IsEnabled { get { return isEnabled; } set { if (value == isEnabled) { return; } isEnabled = value; timer.IsEnabled = value; if (value) { timer.Start(); } else { timer.Stop(); } } } #endregion #region IDisposable Members void IDisposable.Dispose() { try { if (timer != null) { timer.Stop(); } } catch (Exception) { } timer = null; } #endregion }
5.4.1 飞行模拟器
任何飞行模拟器实际上都是一个SCADA系统,辅以高级3D图形、音频支持和特殊设备。我认为SCADA技术(例如OPC)对于飞行模拟器非常有用。我写了一篇文章,专门介绍音频支持及其在飞行模拟器中的应用。下图解释了此任务。
与温度控制示例一样,此示例使用气象组件。因此,此组件也提供风向角。风速和风向的用法是双重的。首先,这些参数用于ATIS音频消息中。此外,这些参数还用于运动方程中。音频消息的时间取决于模拟飞机的运动。ATIS 消息时间取决于到机场的距离。高度(或速度)消息在高度(或速度)过渡时间播放。此处详细描述了此任务。下图(含视频+音频)显示了虚拟摄像头。
安装音频文件包括以下步骤。
步骤 1. 将音频压缩包解压到任意目录。
我们想为这个任务开发用户界面。我使用Silverlight 3 和 WPF 的圆形仪表自定义控件来指示高度和速度。CircularGaugeControl
通过以下方式进行了扩展。
/// <summary> /// Represents a Circular Gauge SCADA control /// </summary> [TemplatePart(Name = "LayoutRoot", Type = typeof(Grid))] [TemplatePart(Name = "Pointer", Type = typeof(Path))] [TemplatePart(Name = "RangeIndicatorLight", Type = typeof(Ellipse))] [TemplatePart(Name = "PointerCap", Type = typeof(Ellipse))] public class ScadaCircularGaugeControl : CircularGauge.CircularGaugeControl, IScadaConsumer { #region Fields #region Scada Input Fields Func<double> output; IScadaInterface scada; IEvent eventObject; bool isEnabled; #endregion #region Ctor public ScadaCircularGaugeControl() { Unloaded += (object sender, RoutedEventArgs e) => { (this as IScadaConsumer).IsEnabled = false; }; } #endregion #endregion #region IScadaConsumer Members IScadaInterface IScadaConsumer.Scada { get { return scada; } set { if (value == null) { return; } scada = value; if (eventObject != null) { if (isEnabled) { eventObject.Event -= Set; } } eventObject = scada[Event]; output = scada.GetDoubleOutput(Output); } } bool IScadaConsumer.IsEnabled { get { return isEnabled; } set { if (isEnabled == value) { return; } isEnabled = value; if (value) { eventObject.Event += Set; } else { eventObject.Event -= Set; } } } #endregion #region Public Members /// <summary> /// Event /// </summary> [Browsable(true)] [TypeConverter(typeof(EventConverter))] [Category("SCADA"), Description("Event name"), DisplayName("Event")] public string Event { get; set; } /// <summary> /// Output /// </summary> [Browsable(true)] [TypeConverter(typeof(OutputRealConverter))] [Category("SCADA"), Description("Output name"), DisplayName("Output")] public string Output { get; set; } #endregion #region Private Methods void Set() { base.CurrentValue = output(); } #endregion }
此组件的属性编辑器如下所示。
此控件的属性类似于温度计的属性。WPF SCADA 包含一个用于虚拟 3D 摄像头的特殊用户控件(UserControlCamera)
。此控件的属性如下所示。
附带视频和音频的完整应用程序如下所示。
sounds
目录(来自sounds.zip压缩包)应复制到 Scada.WPF.Sound.Sample.exe
文件所在的位置。
5.4.2 实时卫星 3D 可视化
我写了一篇关于确定人造卫星轨道的文章。然而,我们可以通过实时可视化扩展功能。我们希望使用当前卫星的参数并显示从虚拟卫星观测到的不同地球图像。类似的任务在我的上一篇文章中有所描述。但是,之前我没有考虑实时性。现在,此任务通过以下功能实现:
- 计时器(用于实时);
- NORAD 两行元素集网站的包装器,它提供卫星参数。
NORAD 两行元素集网站提供了大多数低地球轨道卫星的开普勒元素。我使用HTML 到 XAML 转换器来开发包装器。下图显示了该网站及其包装器。
网站
WPF 包装器
我还使用了Celestia。实时 3D 空间可视化,我将一些类从 C++ 转换为 C#。此外,我开发了地球大气层从地面到太空的经验全球模型 (NRLMSISE-00) 的面向对象包装器。结果我们得到了下图。
Timer 是用于实时的计时器。NASA Image 对象用于纹理。我们可以输入来自NASA 地球观测网站的任何 URL。
Celestrak 是NORAD 两行元素集网站的包装器。
所以我们有了业务逻辑,现在我们想开发用户界面。我们使用UserControlCamera 控件来可视化摄像头。我们还使用 GridTextOutput
和 UserControlChartGroup
来输出数字参数。这些组件如下所示。
UserControlWebPage
用于更改纹理
UserControlCelestrak
用于卫星选择。
结果我们得到了以下应用程序。
关注点
一个人对我说:“我比你懂得多,因为我在一家公司工作了 50 年。”我回答:“我比你懂得多,因为我在很多公司工作过。”