SPADA - 小型 SCADA, 用于 Arduino
用于 ARDUINO 的监督控制和数据采集
简介
为了构建一个小型工厂,我需要对其进行控制。具有了基础SCADA的所有必需功能。我想,你们中的许多人,出于研究、学校、业余爱好等目的,需要监控、控制以及当然是采集系统的数据。
安全和功能需求要求:
- 具有滞后控制(开启阈值 != 关闭阈值)的变量控制
- 带反馈的变量控制
- 通过PC设置开启或关闭的阈值以及反馈的“设定点”的运行时值
- 执行校正
- 检测本地操作(例如按下按钮,例如:水箱满时,或者我想排空它而按下按钮时,会激活水泵,并且我也可以从PC激活水泵)
- 如果手动激活了某项,则显示警告
- 并且重要的是,为了改进而保存到数据库。
背景
为了允许我的PC和我的控制器之间进行通信,需要映射变量。
我们使用一个数组自动为每个变量分配一个编号。但哪些变量呢? 所有我们感兴趣的用于设置或读取的变量。
因此,变量寄存器中包含:
- 阈值限制和设定点
- 状态(开启/关闭)
- 模拟和数字读取
- 每个传感器
现在我们组织通信。
我需要一个协议。它定义了一个8字节令牌的含义。通过这个令牌,控制器和PC进行通信。
该协议包含在源代码的“Sprotocol”类中。
Arduino
SCADA系统,尤其是SPADA,深受监控系统本身的影响。在某种程度上,你必须认识到你变量的性质。但是可以定义一个模板:这是一个Arduino的代码模板。
#include <EEPROM.h> //这些是变量寄存器(数组)的索引。例如: enum variables{ SYSTEM=0, LIGHT_ON=1, //开启阈值 LIGHT_OFF=2 //关闭阈值, LIGHT_READ=3, //模拟输入的读取,(也存储在变量中) L1=4,//LED的状态(我的水泵) }; #define VARIABLES_COUNT 10 enum msg_Type // 命令、错误、报告、类型等 { }; struct register_record { long value; byte warnings; //为了节省空间,警告存储在一个字节中。8个字节 //就像8个布尔值。因此,每个变量可以附加8个警告 //警告的类型取决于物理变量寄存器的性质 register_record() { value=0; warnings=0; } boolean GetWarning(byte i) { return (warnings & (1 << i))>0; } void SetWarning(byte i) { warnings= warnings ^ (1 << i) ; } }v[VARIABLES_COUNT ]; // 声明在这里 //发送消息 void SendMessage(byte a, byte *value ,byte msg_type) { Serial.write(cnctBegin); .... ); }
在loop函数中,可以清晰地识别出流程,如图所示:
- 处理来自PC的输入,并将其存储在寄存器中
- 捕获本地输入(本地输入是基于loco构建的,代码取决于你的系统,但输入也必须存储在一个变量中)
- 读取传感器、液位等,并存储在一个变量中
- 现在处理变量,执行操作,并将结果存储在一个变量中
- 发送报告(所有值、读取、液位、输入)
//处理传入的消息(如果字节数为8,则表示一个完整的令牌已准备好读取) //例如,令牌 F0 01 03 00 00 00 02 F7 if (Serial.available()>7 ) { if (Serial.read()==cnctBegin) //cnctBegin 是我的起始令牌 F0 { action=Serial.read();//这是指令(1=设置等) i=Serial.read();//这是变量,第三个字节我称之为“i”,因为它 //将是这个情况下的索引(3=LIGHT_ON) bi[0]=Serial.read();//这些字节是值 bi[1]=Serial.read(); bi[2]=Serial.read(); bi[3]=Serial.read(); what =bToI(bi); //bToI将4个字节转换为Long,另一个函数iToB将Long转换为字节,准备发送。VB中也有相同的函数。为了用相同的算法加密和解密... //你的代码,读取输入,
Visual Basic
Arduino将因此在每“延迟
”毫秒内发送寄存器的状态给我。Arduino的频率与数据库的保存频率不同。谁需要每秒10个保存值?在程序中,有一个计时器,现在设置为1秒,它打开到数据库的“门”。
PC捕获这些变量,并可以理解控制器中发生了什么,如果它将它们存储在缓冲区中,就可以绘制时间图。
- VB可以使用System.IO.Ports.SerialPort,我创建了一个派生类Sprotocol。
- SerialPort在消息传入时会引发一个事件。我捕获它,并重新构建我的字节
- 我引发一个新事件,使“可读消息”在类外部可用
Public Class SProtocol Inherits System.IO.Ports.SerialPort ... Private Sub SProtocol_DataReceived(value As Object, e As System.IO.Ports.SerialDataReceivedEventArgs) Handles Me.DataReceived '在此例程中,我将重建消息并引发自己的事件... RaiseEvent MessageReceived(MainBuffer(1), MainBuffer(2), v) ... End Sub End Class
例如,在我的窗体中:我会得到;
If msg_type = SProtocol.msg_Type.ask_REPORT Then Select Case var Case variables.LIGHT_OFF nuOff.Value = value '这是我的窗体控件 dImg.NewPoint(variables.LIGHT_OFF, Now(), value)'这是我的图形 Case variables.LIGHT_ON nuOn.Value = value dImg.NewPoint(variables.LIGHT_ON, Now(), value) end select end if
重要的是要说明:SerialPort是线程安全的,这意味着处理事件的函数不能与窗体类“通信”。
这可以通过Invoke函数完成,该函数最终会将事件发送到我的ProcessMessage函数:
Private Sub com_MessageReceive(var As SProtocol.variables, type As SProtocol.msg_Type, value As Long) Handles com.MessageReceive Invoke(New MsgRecived(AddressOf ProcessMessage), var, type, value) End Sub
图形
基础类取自Carl Morey
https://codeproject.org.cn/Articles/42557/Arduino-with-Visual-Basic
我做了很多改进。现在完全可调整大小,并允许在同一图形中绘制更多变量。
如何使用
在其他地方声明一个普通的PictureBox控件,然后声明一个变量,例如
dim dImg as Display
dImg = New Display(Me.PictureBox1.Width, Me.PictureBox1.Height)
Me.PictureBox1.Image = dImg.GetImage 'dImg will create a Image and attach to it
每一行都需要自己的缓冲区。因此,我们必须通过以下方式传递这些信息
dImg.AddVariable(variables.LIGHT_READ, Color.Lime , "Light Read")
此时,图形已准备好接收值
dImg.NewPoint(variables.LIGHT_OFF, Now(), value) 'In order, the index,the time and the value
而不是点,它也可以接受数据表,例如来自查询
img.setDataTable(dt)' where dt is a DataTable
它将识别带有数值的列并相应地绘制图形。
重要提示:数据表的第一列必须是datetime值,并且始终递增。如果两个字段具有相同的值,可能会引发异常。
那么其他列的顺序并不重要,它将为每个数值列绘制一个图形。
绘制更多变量和绘制数据表的需求是后期出现的,因此该类并非为此目的而优化。我只是调整了旧类以使其适用于更多图形,但仍有改进的空间。
数据库
我认为没有哪个数据库更适合这个目的。嵌入式(无需安装任何东西)、免费、功能强大,并且通常会让你说:“uff”的情况(例如大小写敏感性),当你处理数字时,就不会烦人。所以让我们享受这个令人难以置信的强大数据库。
图形用户界面
GUI只是一个文本框,用于编写select、insert、update和delete的查询。
如前所述,图形将读取数据表(记住第一列是时间),并跟踪相应的图形。
该程序的目的是面向有编程技能的合格人员。我认为“通过SQL”与程序“对话”不是问题。更好的是,它非常强大。你可以进行任何类型的设置。
下面我将向你展示一些重要的查询,例如图片中的那个,它允许你重建某个时刻的元组。并在某个时刻确定整个系统。
功能包括
1)查询:在文本框中输入SQL指令
Select trim(name),qryString from queries
此指令调用`queries`表,你可以在其中选择一个已保存的命令,复制粘贴即可执行。
2)运行:它执行文本框中的SQL指令
3)引用查询:它准备框中的文本,使其准备好被插入到数据库中,并带有引号和“INSERT”子句。例如,你输入
Select t , val as Pressure from dates
如果你点击引用查询,你的框中将出现
insert into queries(name,qrystring)
values('NAME HERE',
'Select t , val as Pressure from dates ;');
你需要在指定位置填写“名称”,然后点击运行。
4)备份:它会备份你的数据库到一个.sql文件:仅仅一个文本文件,其中包含重建数据库的SQL指令(例如CREATE TABLE...bla bla)。如果你打开这个文件,你可能会被开头那些奇怪的指令吓到。你可以在我的网站上找到解释。它会备份整个数据库。
5)导出到Excel:是的,它导出到Excel。但是,只导出文本框中的查询(无需运行)。
SQL入门
如果你想开始编写查询,你必须了解结构。
只有三张表,而且非常简单。两张用于数据记录,第三张用于存储查询。
重要提示:Firebird没有自动增量值。但它使用触发器和生成器。 此处有相关信息。`variables`和`queries`表有一个名为`i`的列,该列带有用于自动增量的触发器。备份也将包含这些指令。
CREATE TABLE variables (
i INTEGER NOT NULL, -- this is a value, where is associated a trigger
name VARCHAR(255) ,
description BLOB SUB_TYPE 1 ,
um VARCHAR(5) ,
CONSTRAINT PRIMARY KEY(i)
);
`Variables`包含变量的描述性定义...例如,名称和描述。它有一个与`i`关联的触发器,而`i`也是它的主键(如果你设置正确,`i`将是你发送给Arduino以请求其寄存器的值)。
CREATE TABLE dates (
var INTEGER NOT NULL,
t TIMESTAMP NOT NULL,
val BIGINT ,
CONSTRAINT PRIMARY KEY(var,t)
);
CREATE INDEX ixVal on table dates(val);
`Dates`是工作范围,包含来自Arduino的元组。`var`是引用`Variables`表中`i`的外键。`T`是时间戳。`val`是值。
`Dates`已完全索引。主键由列(`var`,`t`)组成,并且还有一个附加的`val`索引。
`Dates`可能变得非常大。这些索引很重要。
还有第三个表叫做`queries`,存储我们的SQL指令。
CREATE TABLE queries (
i INTEGER NOT NULL,
name VARCHAR(255) ,
qrystring BLOB SUB_TYPE 1 ,
CONSTRAINT PRIMARY KEY(i)
);
认真学习SQL
同时查看所有变量的所有图形很重要,以便识别例如原因和结果之间的相关性,或者你的系统何时启动,何时发生错误。所以我们需要同一时间的所有变量的元组。变量的数量可能非常大,并且无法在开发时确定。与其在表中添加更多列,我只添加一列,但每个元组都有相同的插入时间。实际上,这就像系统在同一毫秒内同时插入所有变量。(VB就是这样做的,在源代码中有注释)
重建元组的查询如下。
SELECT a.T, a.VAL as Light_on,b.val as Light_off , c.val as Light_read,(500*d.val) as L1,e.val as pressure_on, f.val as pressure_off, g.val as pressure_setpoint FROM DATES a inner join DATES B on a.t=b.t inner join DATES C on a.t=c.t inner join DATES D on a.t=d.t inner join DATES E on a.t=e.t inner join DATES F on a.t=f.t inner join DATES G on a.t=g.t where a.t between '23.03.2014 14:00:00' and '23.03.2014 18:00:00' AND a.var = 1 AND b.var = 2 AND C.var = 3 AND D.var = 4 AND E.var =5 AND F.var=6 AND G.var=7 order by a.t;
实际上,我将`dates`表进行了尽可能多的JOIN,数量等于我想显示的变量数量。
技巧
- 为列设置别名,你将在图例中看到它。
- 要缩放图形,请在select中乘以一个因子(对值为1/0的情况很有用)。
JOIN条件中的严格“=”以及索引允许我们获得良好的响应时间。
你可以使用下面的块执行相同的操作。
--SET TERM !!; -- deactivate the comments with other GUI's Ex: Flamerobin
EXECUTE BLOCK --ix= 1 2 3 4 5 6 7 ==>11111110b =254d
returns( t timestamp,a bigint,b bigint,c bigint, d bigint,e bigint,f bigint,g bigint,n integer)
AS
declare tmpt2 timestamp;
declare tmpVar integer;
declare tmpVar2 integer;
declare tmpVal bigint;
declare tmpVal2 bigint;
declare ctrlFlag integer;
DECLARE cur cursor FOR
/*********************************************************/
/*** MODIFY THE QUERY **/
/*********************************************************/
(SELECT var,t, val from dates
where t> '22.03.2014 00:00:00' --between '23.03.2014 00:00:00' and '24.03.2014 15:00:00'
order by t,var ); --important, order first for time than for var
BEGIN
n=0;
ctrlFlag=254;
OPEN cur;
fetch cur INTO tmpVar,t,tmpVal;
IF (ROW_COUNT = 0) THEN exit;
while (1=1) do
BEGIN
ctrlFlag=bin_xor(ctrlFlag,bin_shl(1,tmpVar));
fetch cur INTO tmpVar2,tmpt2,tmpVal2;
IF (ROW_COUNT = 0) THEN leave;
IF (tmpVar=1) THEN a=tmpVal;
IF (tmpVar=2) THEN b=tmpVal;
IF (tmpVar=3) THEN c=tmpVal;
IF (tmpVar=4) THEN d=tmpVal;
IF (tmpVar=5) THEN e=tmpVal;
IF (tmpVar=6) THEN f=tmpVal;
IF (tmpVar=7) THEN g=tmpVal;
if (tmpt2<>t ) then
BEGIN
if (ctrlFlag=0) then
begin
n=n+1;
suspend;
end
ctrlFlag=254;
END
tmpvar=tmpVar2;
t=tmpt2;
tmpVal=tmpVal2;
END
if (ctrlFlag=0) then
begin
n=n+1;
suspend;
end
close cur;
END
--SET TERM; !! -- deactivate the comments with GUI Ex: Flamerobin
当我谈到Firebird的强大时,这就是一个例子。Firebird支持一种完整而强大的过程语言,你可以用它做很多事情。这是一个EXECUTE BLOCK。它允许在不保存任何过程的情况下执行一段指令。MySQL不支持它。这个块的结果比SELECT快一点:SELECT是0.160毫秒,BLOCK是0.120毫秒,查询中有1700个元素/列(所以大约11900个元素)。
也许我应该花点时间谈谈`ctrlFlag`。
处理元组需要检查是否所有变量都已设置、保存或已保存。每个变量都有一个索引。如果我在所需变量的位置设置一个标志1,我可以正确地激活或停用其标志。例如:
SYSTEM | LIGHT_ON | LIGHT_OFF | LIGHT_READ | L1 | PRESSURE_ON | P_OFF | P_SETPOINT | |
index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
我不要求系统,所以是0。我要求所有其他的,都是1。
结果是11111110b=254
要将索引的值设置为0,我执行`ctrlFlag`=`ctrlFlag` XOR (1<<`index`)。此指令只会关闭其自己的位。
最后,如果所有值都设置为零,`ctrlFlag`将等于0。
使用程序
它是特定于某个应用程序的,目的是理解我如何控制一个系统,它没有针对速度或设置进行优化。
同样的目的,如果你想学习控制一个系统,步骤如下:
- 连接(会连接,但不会开始消息传递)
- 开始消息,将开始消息传递(如果不起作用,多点击几次)
在关机前:
- 停止消息,它将停止控制器发送消息
- 现在你可以安全地断开连接(就像断开USB一样 :-))
主要问题是控制器和PC之间对话开始时的正确同步。这带来了一些我需要解决的麻烦。