Schedule creation for cron-based SCADA systems

    In continuation of the SCADA system of my “beloved” shopping center,

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


    I mainly saw schedules in the PLC. And there usually is a weekly schedule. Several switching points per day.
    For a PLC, such an implementation can be understood. Limited memory, for example. And you don’t really have to fool around anymore.
    But still, you won’t do any tricky working conditions. Type "will be included in the holidays."

    Idea


    There is such a utility in linux - cron. "For periodic tasks at certain times." 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 still has a bunch of chips. For spicy details, you can go to Wikipedia.
    And this is quite enough for us.
    Only in our case we need a period of time, and not a specific moment in time. Well, we also attach a year to the record (so as not to be trifled). We get the following record:
    <Minutes> <Hours> <Month_days> <Months> <Week_weeks> <Years> <Time span in minutes>

    Still need "priority." After all, it may be that one instruction will block another.

    Implementation


    At the first stage in the SCADA system, everything was in the xml file:
    <?xml version="1.0" encoding="utf-8" ?><timemode><deviceID="ПВ1" ><modetimeperiod="* * * * * * 5"type="СТОП"priority="0" /><modetimeperiod="0 7 * * * * 60"type="Реж*ИМП;ПВ*80;ВВ*80;У*21"priority="1" /></device></timemode>

    Where
    "Dir * IMP; PV * 80; BB * 80; U * 21 ”- pulse operation, 80% of the supply fan speed, 80% of the exhaust fan speed, room temperature setting - 21 ° C

    Everything was done for ventilation installations. For example, 2 rules are shown.
    One is the constant “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 type ArrayList. It will contain strings like
    "PV1> STOP"
    "PV2> STOP"
    "PV3> STOP"

    Further on, the subroutine responsible for a specific ventilation installation will find itself in the array and change (if necessary) the operation mode of the ventilation installation.

    And now the code


    I do not pretend to the beauty and correctness of the code. Also, some decisions here are caused by the runtime itself.
    Function for reading the configuration file and forming the output array of modes:
    publicvoidXMLread() {
                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:
    intinnerInterval(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)
                            {
                                return0;
                            }
                            return1;
                        }
                    }
                    catch (FormatException)
                    {
                        return-1;
                    }
                    catch
                    {
                        return-10;
                    }
                }
                return-1;
            }


    Well, the full code, who are interested
    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;
    namespaceClassLibrary
    {
        publicclassMyClass
        {
    			ArrayList ArrayListVentModes_local;
        		public ArrayList ArrayListVentModes{
        		    get{
        		      return ArrayListVentModes_local;
        		    }
        		}    
        		publicstring FilePath{
        			set{ this.filepath_local = value ; }
        		}
        		publicstring Massege_str{
        		    get; set;
        		}
        		publicstring CountElement{
        		    get; set;
        		}
        		string filepath_local ;
        		ArrayList arr;
                intinnerInterval(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)
                            {
                                return0;
                            }
                            return1;
                        }
                    }
                    catch (FormatException)
                    {
                        return-1;
                    }
                    catch
                    {
                        return-10;
                    }
                }
                return-1;
            }
            int clamp = 0;  
                    publicvoidmain_metod() {
                   if (this.clamp != 1)  {
                 Thread tRec = new Thread(new ThreadStart(XMLread));
                 tRec.Start();
                 this.clamp = 1 ; 
                  }
              }  
                publicvoidXMLread() {
                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;
            }
        }
    }


    Affects SCADA +
    Еще нужно будет указать какой метод вызывать при пересчете программы.
    image


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

    image

    That's all. Interesting projects for you.

    PS


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

    Now my hands are combing to realize this idea on ST for Codesys. Specifically for ARIES PLC63. If possible, I’ll write a sequel.

    Also popular now: