В продолжение о SCADA системе моего «любимого» торгового центра
Я думаю не мало инженеров в АСУТП сталкивались с требованием заставить «что-то» работать по расписанию. Покажу как реализовал сделал расписание в составе сервера SCADA.
В основном расписания я видел в ПЛК. И там обычно это расписание недельное. Несколько точек переключения на день.
Для ПЛК такую реализацию можно понять. Ограниченная память, например. Да и не особо надо морочиться больше.
Но все же, так не сделаешь каких-нибудь мудреных условий работы. Типа «включится в праздничные дни».
Есть такая утилита в linux — cron. «Для периодического выполнения заданий в определённое время». Инструкции в cron пишутся в таком виде
Например
У cron еще куча фишек. За пикантными подробностями можно в википедию
А нам и этого вполне хватит.
Только в нашем случае нужен отрезок времени, а не конкретный момент времени. Ну и приделаем к записи еще и год (чтоб не мелочиться). Получим такую запись:
Еще понадобится «приоритет». Ведь может быть, что одна инструкция перекроет другую.
На первом этапе в SCADA системе все было в xml файле:
Где
Все делалось под вентиляционные установки. Для примера показано 2 правила.
Одно — постоянный «стоп» системы. Второе каждый день в 7 утра запустит вент установку на 1 час.
Диспетчеризация построена в SCADA+. Эта среда поддерживает скрипты С#. Скрипты формируются как объекты с выходными переменными (свойствами). Для нашего скрипта чтения расписания переменные выглядят так:

Задача скрипта — сформировать выходной массив типа ArrayList. ��н будет содержать строки типа
А уже дальше подпрограмма, отвечающая за конкретную вентиляционную установку, найдет себя в массиве и изменит (если надо) режим работы вентустановки.
На красоту и правильность кода не претендую. Так же некоторые решения тут вызваны самой средой исполнения.
Функция для чтения конфигурационного файла и формирования выходного массива режимов:
И функция innerInterval. Она определяет попадает ли установка в конкретный отрезок времени:
По текущей реализации. Заказчик затребовал возможность самим настраивать режимы. Понятное дело, от редактирования xml файла он отказался. Перевели все с xml на таблицу в MySQL (чтоб можно было редактировать с АРМа, т.к. сервер c SCADA находится удаленно) и сделали простенькую программу для редактирования

На этом все. Интересных вам проектов.
Подобное расписание можно поднять и на MasterSCADA — она тоже поддерживает C#. Можно даже подключить Visual Studio для более удобной отладки (насчет SCADA+ не знаю).
Сейчас руки зачесались реализовать эту идею на ST для Codesys. Конкретно для ОВЕН ПЛК63. Если получится, напишу продолжение.
Я думаю не мало инженеров в АСУТП сталкивались с требованием заставить «что-то» работать по расписанию. Покажу как реализовал сделал расписание в составе сервера 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+. Эта среда поддерживает скрипты С#. Скрипты формируются как объекты с выходными переменными (свойствами). Для нашего скрипта чтения расписания переменные выглядят так:

Задача скрипта — сформировать выходной массив типа 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+
Еще нужно будет указать какой метод вызывать при пересчете программы.


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

На этом все. Интересных вам проектов.
P.S.
Подобное расписание можно поднять и на MasterSCADA — она тоже поддерживает C#. Можно даже подключить Visual Studio для более удобной отладки (насчет SCADA+ не знаю).
Сейчас руки зачесались реализовать эту идею на ST для Codesys. Конкретно для ОВЕН ПЛК63. Если получится, напишу продолжение.
