В продолжение о 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+. Эта среда поддерживает скрипты С#. Скрипты формируются как объекты с выходными переменными (свойствами). Для нашего скрипта чтения расписания переменные выглядят так:
Задача скрипта — сформировать выходной массив типа 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;
}
}
}
По текущей реализации. Заказчик затребовал возможность самим настраивать режимы. Понятное дело, от редактирования xml файла он отказался. Перевели все с xml на таблицу в MySQL (чтоб можно было редактировать с АРМа, т.к. сервер c SCADA находится удаленно) и сделали простенькую программу для редактирования
На этом все. Интересных вам проектов.
P.S.
Подобное расписание можно поднять и на MasterSCADA — она тоже поддерживает C#. Можно даже подключить Visual Studio для более удобной отладки (насчет SCADA+ не знаю).
Сейчас руки зачесались реализовать эту идею на ST для Codesys. Конкретно для ОВЕН ПЛК63. Если получится, напишу продолжение.
Автор: Фоменко Лев