Создание расписания для SCADA системы на основе cron

в 0:38, , рубрики: C#, scada, расписание, системное программирование

В продолжение о SCADA системе моего «любимого» торгового центра

Я думаю не мало инженеров в АСУТП сталкивались с требованием заставить «что-то» работать по расписанию. Покажу как реализовал сделал расписание в составе сервера SCADA.

В основном расписания я видел в ПЛК. И там обычно это расписание недельное. Несколько точек переключения на день.
Для ПЛК такую реализацию можно понять. Ограниченная память, например. Да и не особо надо морочиться больше.
Но все же, так не сделаешь каких-нибудь мудреных условий работы. Типа «включится в праздничные дни».

Идея

Есть такая утилита в linux — cron. «Для периодического выполнения заданий в определённое время». Инструкции в cron пишутся в таком виде

минута час день_месяца месяц день_недели команда

  • День недели (0 — 7) (Воскресенье =0 или =7)
  • Месяц (1 — 12)
  • День (1 — 31)
  • Час (0 — 23)
  • Минута (0 — 59)

Например

0 0 * * 1 — Каждый понедельник в 0:00 минут
где * — означает любое значение

У cron еще куча фишек. За пикантными подробностями можно в википедию
А нам и этого вполне хватит.
Только в нашем случае нужен отрезок времени, а не конкретный момент времени. Ну и приделаем к записи еще и год (чтоб не мелочиться). Получим такую запись:

<Минуты> <Часы> <Дни_месяца> <Месяцы> <Дни_недели> <Годы> <Отрезок времени в минутах>

Еще понадобится «приоритет». Ведь может быть, что одна инструкция перекроет другую.

Реализация

На первом этапе в SCADA системе все было в xml файле:

<?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>

Где

«Реж*ИМП; ПВ*80; ВВ*80; У*21» — импульсный режим работы, 80% скорости приточного вентилятора, 80% скорости вытяжного вентилятора, уставка температуры по помещению — 21°С

Все делалось под вентиляционные установки. Для примера показано 2 правила.
Одно — постоянный «стоп» системы. Второе каждый день в 7 утра запустит вент установку на 1 час.

Диспетчеризация построена в SCADA+. Эта среда поддерживает скрипты С#. Скрипты формируются как объекты с выходными переменными (свойствами). Для нашего скрипта чтения расписания переменные выглядят так:

image

Задача скрипта — сформировать выходной массив типа ArrayList. Он будет содержать строки типа

«ПВ1>СТОП»
«ПВ2>СТОП»
«ПВ3>СТОП»

А уже дальше подпрограмма, отвечающая за конкретную вентиляционную установку, найдет себя в массиве и изменит (если надо) режим работы вентустановки.

А теперь код

На красоту и правильность кода не претендую. Так же некоторые решения тут вызваны самой средой исполнения.
Функция для чтения конфигурационного файла и формирования выходного массива режимов:

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;
        }

И функция innerInterval. Она определяет попадает ли установка в конкретный отрезок времени:

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;
        }
Ну и полный код, кому интересно

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;
        }
    }
}

Касается SCADA+

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

По текущей реализации. Заказчик затребовал возможность самим настраивать режимы. Понятное дело, от редактирования xml файла он отказался. Перевели все с xml на таблицу в MySQL (чтоб можно было редактировать с АРМа, т.к. сервер c SCADA находится удаленно) и сделали простенькую программу для редактирования

image

На этом все. Интересных вам проектов.

P.S.

Подобное расписание можно поднять и на MasterSCADA — она тоже поддерживает C#. Можно даже подключить Visual Studio для более удобной отладки (насчет SCADA+ не знаю).

Сейчас руки зачесались реализовать эту идею на ST для Codesys. Конкретно для ОВЕН ПЛК63. Если получится, напишу продолжение.

Автор: Фоменко Лев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js