📜 ⬆️ ⬇️

Creating a schedule for SCADA systems based on cron

In continuation of the SCADA system of my "favorite" shopping center

I think quite a few engineers in an automated process control system were faced with the requirement to make “something” work on a schedule. I will show how I implemented the made schedule as part of the SCADA server.


Basically the schedules I saw in the PLC. And there is usually a weekly schedule. Several switching points per day.
For PLC, this implementation can be understood. Limited memory, for example. Yes, and not really need to fool more.
But nevertheless, you will not do so some tricky working conditions. Type "included in the holidays."
')

Idea


There is such a utility in linux - cron. "For the periodic performance of tasks at a certain time." Instructions in cron are written in this form
minute hour day_month month day_week team
  • Day of the week (0 - 7) (Sunday = 0 or = 7)
  • Month (1 - 12)
  • Day (1 - 31)
  • Hour (0 - 23)
  • Minute (0 - 59)

for example
0 0 * * 1 - Every Monday at 0:00 minutes
where * means any value

Cron has a lot of chips. For spicy details you can in Wikipedia
And that's enough for us.
Only in our case we need a period of time, and not a specific point in time. Well, we attach to the record also a year (so as not to waste time on trifles). We get the following entry:
<Minutes> <Hours> <Months_Months> <Months> <Days_Week>> Years> <Time span in minutes>

Still need a "priority." After all, it may be that one instruction will override the other.

Implementation


At the first stage in the SCADA system everything was in the xml file:
<?xml version="1.0" encoding="utf-8" ?> <timemode> <device ID="1" > <mode timeperiod="* * * * * * 5" type="" priority="0" /> <mode timeperiod="0 7 * * * * 60" type="*;*80;*80;*21" priority="1" /> </device> </timemode> 

Where
“Dir * IMP; PV * 80; BB * 80; Y * 21 "- pulse operation mode, 80% of the speed of the supply fan, 80% of the speed of the exhaust fan, the temperature set point for the room is 21 °

Everything was done under the ventilation system. For example, 2 rules are shown.
One is the permanent “stop” of the system. The second every day at 7 am will start the ventilation installation for 1 hour.

Dispatching is built in SCADA + . This environment supports C # scripts. Scripts are formed as objects with output variables (properties). For our schedule reading script, the variables look like this:

image

The task of the script is to form an output array of the ArrayList type. It will contain strings of type
"PV1> STOP"
"PW2> STOP"
“PV3> STOP”

And already further the subroutine, which is responsible for a specific ventilation installation, will find itself in the array and change (if necessary) the mode of operation of the ventilation unit.

And now the code


I do not pretend to the beauty and correctness of the code. Also, some decisions here are caused by the execution environment itself.
The function for reading the configuration file and forming the output array of modes:
 public void XMLread() { arr = new ArrayList(); int prio; XmlDocument xmlDocument = new XmlDocument(); try{ xmlDocument.Load(filepath_local); Massege_str = " " ; } catch { Massege_str = "  " ; return; } foreach (XmlNode device in xmlDocument.SelectNodes("/timemode/device")) { prio = -1; string strmode = "???" ; foreach (XmlNode mode in xmlDocument.SelectNodes("/timemode/device[@ID=\"" + device.Attributes["ID"].Value + "\"]/mode")) { int prioNew = Convert.ToInt16(mode.Attributes["priority"].Value) ; if ((innerInterval(mode.Attributes["timeperiod"].Value) > 0 ) && prio < prioNew) { strmode = mode.Attributes["type"].Value; prio = prioNew ; } } string newmes = (device.Attributes["ID"].Value + ">" + strmode); arr.Add(newmes); } ArrayListVentModes_local = arr; CountElement = ">" + ArrayListVentModes.Count.ToString(); Thread.Sleep(5000); clamp = 0; } 

And the innerInterval function. It determines whether the installation falls into a specific period of time:
 int innerInterval(string CronFormatStr){ string[] word = CronFormatStr.Split(' '); DateTime dt = DateTime.Now; if (word.Length == 7) { try { int dayOfWeekArray = 0; if (word[4] != "*") { dayOfWeekArray = Convert.ToInt32(word[4]); } DateTime dt_start = new DateTime( (word[5] == "*") ? dt.Year : Convert.ToInt32(word[5]), (word[3] == "*") ? dt.Month : Convert.ToInt32(word[3]), (word[2] == "*") ? dt.Day: Convert.ToInt32(word[2]), (word[1] == "*") ? dt.Hour : Convert.ToInt32(word[1]), (word[0] == "*") ? 0 : Convert.ToInt32(word[0]), 0 ); DateTime dt_end = dt_start.AddMinutes(Convert.ToInt32(word[6])); if (dt >= dt_start && dt <= dt_end) { if (dayOfWeekArray != Convert.ToInt32(dt.DayOfWeek) && dayOfWeekArray > 0) { return 0; } return 1; } } catch (FormatException) { return -1; } catch { return -10; } } return -1; } 


Well, the full code, who cares
 using System; using System.Collections; using System.Collections.Generic; //using System.Linq; using System.Text; using System.Xml; using System.Threading; using System.Xml.Linq; namespace ClassLibrary { public class MyClass { ArrayList ArrayListVentModes_local; public ArrayList ArrayListVentModes{ get{ return ArrayListVentModes_local; } } public string FilePath{ set{ this.filepath_local = value ; } } public string Massege_str{ get; set; } public string CountElement{ get; set; } string filepath_local ; ArrayList arr; int innerInterval(string CronFormatStr){ string[] word = CronFormatStr.Split(' '); DateTime dt = DateTime.Now; if (word.Length == 7) { try { int dayOfWeekArray = 0; if (word[4] != "*") { dayOfWeekArray = Convert.ToInt32(word[4]); } DateTime dt_start = new DateTime( (word[5] == "*") ? dt.Year : Convert.ToInt32(word[5]), (word[3] == "*") ? dt.Month : Convert.ToInt32(word[3]), (word[2] == "*") ? dt.Day: Convert.ToInt32(word[2]), (word[1] == "*") ? dt.Hour : Convert.ToInt32(word[1]), (word[0] == "*") ? 0 : Convert.ToInt32(word[0]), 0 ); DateTime dt_end = dt_start.AddMinutes(Convert.ToInt32(word[6])); if (dt >= dt_start && dt <= dt_end) { if (dayOfWeekArray != Convert.ToInt32(dt.DayOfWeek) && dayOfWeekArray > 0) { return 0; } return 1; } } catch (FormatException) { return -1; } catch { return -10; } } return -1; } int clamp = 0; public void main_metod() { if (this.clamp != 1) { Thread tRec = new Thread(new ThreadStart(XMLread)); tRec.Start(); this.clamp = 1 ; } } public void XMLread() { arr = new ArrayList(); int prio; XmlDocument xmlDocument = new XmlDocument(); try{ xmlDocument.Load(filepath_local); Massege_str = " " ; } catch { Massege_str = "  " ; return; } foreach (XmlNode device in xmlDocument.SelectNodes("/timemode/device")) { prio = -1; string strmode = "???" ; foreach (XmlNode mode in xmlDocument.SelectNodes("/timemode/device[@ID=\"" + device.Attributes["ID"].Value + "\"]/mode")) { int prioNew = Convert.ToInt16(mode.Attributes["priority"].Value) ; if ((innerInterval(mode.Attributes["timeperiod"].Value) > 0 ) && prio < prioNew) { strmode = mode.Attributes["type"].Value; prio = prioNew ; } } string newmes = (device.Attributes["ID"].Value + ">" + strmode); arr.Add(newmes); } ArrayListVentModes_local = arr; CountElement = ">" + ArrayListVentModes.Count.ToString(); Thread.Sleep(5000); clamp = 0; } } } 


Applies to SCADA +
You will also need to specify which method to call when recalculating the program.
image


According to the current implementation. The customer requested the ability to customize the modes. Understandably, he refused to edit the xml file. Translated everything from xml to a table in MySQL (so that you can edit from AWP, because the server with SCADA is located remotely) and made a simple program for editing

image

That's all. Interesting to you projects.

PS


A similar schedule can be raised on MasterSCADA - it also supports C #. You can even connect Visual Studio for easier debugging (I don’t know about SCADA +).

Now my hands were itching to implement this idea on ST for Codesys. Specifically for ARIES PLC63. If possible, I will write a sequel.

Source: https://habr.com/ru/post/333412/


All Articles