65.9K
CodeProject 正在变化。 阅读更多。
Home

SPADA - 小型 SCADA, 用于 Arduino

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (10投票s)

2014 年 3 月 24 日

CPOL

9分钟阅读

viewsIcon

48977

downloadIcon

3354

用于 ARDUINO 的监督控制和数据采集

简介

为了构建一个小型工厂,我需要对其进行控制。具有了基础SCADA的所有必需功能。我想,你们中的许多人,出于研究、学校、业余爱好等目的,需要监控、控制以及当然是采集系统的数据。

安全和功能需求要求:

  • 具有滞后控制(开启阈值 != 关闭阈值)的变量控制
  • 带反馈的变量控制
  • 通过PC设置开启或关闭的阈值以及反馈的“设定点”的运行时值
  • 执行校正
  • 检测本地操作(例如按下按钮,例如:水箱满时,或者我想排空它而按下按钮时,会激活水泵,并且我也可以从PC激活水泵)
  • 如果手动激活了某项,则显示警告
  • 并且重要的是,为了改进而保存到数据库。

背景

为了允许我的PC和我的控制器之间进行通信,需要映射变量。

我们使用一个数组自动为每个变量分配一个编号。但哪些变量呢? 所有我们感兴趣的用于设置或读取的变量。

因此,变量寄存器中包含:

  • 阈值限制和设定点
  • 状态(开启/关闭)
  • 模拟和数字读取
  • 每个传感器

Architecture

现在我们组织通信。

我需要一个协议。它定义了一个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值,并且始终递增。如果两个字段具有相同的值,可能会引发异常。

那么其他列的顺序并不重要,它将为每个数值列绘制一个图形。

绘制更多变量和绘制数据表的需求是后期出现的,因此该类并非为此目的而优化。我只是调整了旧类以使其适用于更多图形,但仍有改进的空间。

数据库

数据库是FIREBIRD。

我认为没有哪个数据库更适合这个目的。嵌入式(无需安装任何东西)、免费、功能强大,并且通常会让你说:“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。

使用程序

© . All rights reserved.