В прошлый раз мы закончили на создании типа точки данных (DPT Flap) и трех экземпляров точки данных (DPs Flap1, Flap2, Flap3). Пора переходить к визуальной составляющей операторского интерфейса. Открываем модуль gedi. В gedi мы видим название нашего проекта в дереве и его составные части. Нас сейчас интересуют «панели», поэтому разворачиваем их.

Определенная структура панелей в проекте уже имеется, но для упрощения мы будем работать в корне. Создадим новую панель, для чего необходимо выполнить правый клик мышкой и нажать пункт меню «Add new panel».

Зададим имя панели — Flap.pnl, после чего панель появляется в структуре проекта.

Все «панели» в проекте — это тоже текстовые файлы, только они бывают двух типов: pnl или xml. Xml в силу своей структурированности использовать в боевых проектах гораздо удобнее (это же заявляют и люди, работающие с WinCC OA профессионально), его удобнее редактировать в сторонних редакторах. Мы же в рамках базы работаем только в gedi, по этой причине нам достаточно и базового формата. Для изменения формата панели необходимо выполнить «save as» (сохранить как — пункт в меню, потом надо выбрать другой формат данных). Привожу скриншот содержимого пустой панели Flap.pnl:

Теперь открываем созданную пустую панель двойным кликом и немного меняет ее геометрию, примерно, как на этом экране

В верхней части редактора gedi есть графические примитивы, выбираем и рисуем на панели прямоугольник.

Обратите внимание на окно свойств и событий графического объекта, находящееся слева от самой панели, оно нам понадобится. К примеру, для изменения цвета фона объекта необходимо найти свойство «Background color».

При двойном клике на этом свойстве появляется таблица цветов.

Система, позволяет не только выбрать цвет из готового списка, но и использовать цвета в формате RGB. Для создания своего цвета необходимо открыть палитру цветов, задать цвет и его имя, после чего этот он появится в проекте. Для того, чтобы задать цвет RGB, необходимо в окне ниже нажать кнопку More, вбить цвет числами или выбрать его мышью (справа) и убедиться, что он меняется в левой части экрана. Ограничимся лишь выбором цвета blue.

Выбираем цвет blue
Выбираем цвет blue
"Труба с выбранным цветом"
"Труба с выбранным цветом"

Сделаем копи-паст одной «трубы»

Сделаем еще один копи-паст. Это будет сама задвижка (первые два прямоугольника — это труба).

Этот третий объект необходимо повернить вертикально (считается, что задвижка в нормальном положении закрыта). Тут появляется еще один нюанс. Начало системы координат графического объекта на панели определяется т.н. «реперной точкой» (reference point). По умолчанию в нашем случае эта точка располагается в верхнем левом углу (на скриншоте она обозначена, как едва заметный желтый круг). Если прямо сейчас найти свойство «Rotation» и задать угол 90, то мы получим следующее изображение.

Как видно, реперная точка теперь находится в левом нижнем углу (на самом деле референсная точка осталась на своем месте, а относительно нее и произошло вращение). Обнулю свойство Rotation и перенесу реперную точку в середину прямоугольника. Сдвигать reference point необходимо в окне свойств объекта, непосредственно мышью перемещать эту точки нельзя.

Теперь можно спокойно задать свойство Rotation. И немного скорректировать размер «задвижки», чтобы она помещалась в «трубу».

Рядом с кнопкой Save есть кнопка Save and run, нажмем ее и увидим графическое изображение нашей задвижки в режиме исполнения.

Приступим к динамизации изображения — пусть задвижка вращается в зависимости от DPE Position нашей DP Flap. Выберем графический примитив «задвижка», найдем в списках событий (под графическими свойствами) событие Initialize и ждем кнопку «Open property wizard» этого события.

Выбираем Rotate object

Выберем DPE Flap1.Inputs.Position, зададим значения «минимум» и «максимум», нажмем Next, а в следующем окне «Finish»

Нажмем Save and run и понаблюдаем поворот задвижки. В моем случае еще с прошлого раза значение DPE Flap1.Inputs.Position было равно 11, что мы и наблюдаем сейчас визуально.

Если открыть сейчас модуль para и для конфига Flap1.Inputs.Position._original дать значение 90, задвижка полностью «откроется»

Теперь давайте вернемся к событию «Initialize» для графического примитива «задвижка». Только в этот раз нажмем кнопку Open script editor:

// SimpleCtrlScriptStart {valid}
main()
{
  EP_setRotation();
}

void EP_setRotation()
{
  dyn_errClass err;

  if( !dpExists( "System1:Flap1.Inputs.Position:_online.._value") )
  {
    setValue("", "color", "_dpdoesnotexist");
    return;
  }

  dpConnect("EP_setRotationCB",
            "System1:Flap1.Inputs.Position:_online.._value");
  err = getLastError();
  if (dynlen(err) > 0)
    setValue("", "color", "_dpdoesnotexist");

}


void EP_setRotationCB(string dp1, int iNewValue)
{
  float MIN_VALUE = 0;
  float MAX_VALUE = 90;
  float MIN_ROTATION = 0;
  float MAX_ROTATION = 90;

  float fRotation;
  fRotation = ( 1.0 * (MAX_ROTATION - MIN_ROTATION) / (MAX_VALUE - MIN_VALUE)) * 
              (iNewValue - MIN_VALUE) + MIN_ROTATION;
  if (fRotation > MAX_ROTATION) fRotation = MAX_ROTATION;
  else if (fRotation < MIN_ROTATION) fRotation = MIN_ROTATION;

  setValue("", "rotation", fRotation);
}

// SimpleCtrlScript {EP_setRotation}
// DP {System1:Flap1.Inputs.Position}
// DPConfig {:_online.._value}
// DPType {int}
// PVSSRange {0}
// Min {0}
// Max {90}
// MinRotation {0}
// MaxRotation {90}
// SimpleCtrlScriptEnd {EP_setRotation}

Собственно, как я и угрожал — везде текстовые файлы. В данном случае мы видим скрипт, который был создан автоматически средствами Wizard. Да, визарды есть в системе WinCC OA, но по сути их назначение — вызов шаблона, «рыбы», которую мы в дальнейшем будем править руками. Как видно скрипт написан на языке, очень похожем на C. В WinCC OA встроенный язык называется Control.

Давайте разберем и попробуем понять это скрипт. В первую очередь, присутствует функция main(), это «точка входа» в программу, исполнение всегда начинается с нее.

main()
{
  EP_setRotation();
}

Функция main() содержит единственную строчку — вызов функции EP_SetRotation(). Посмотрим на ее содержимое.

void EP_setRotation()
{
  dyn_errClass err;

  if( !dpExists( "System1:Flap1.Inputs.Position:_online.._value") )
  {
    setValue("", "color", "_dpdoesnotexist");
    return;
  }

  dpConnect("EP_setRotationCB",
            "System1:Flap1.Inputs.Position:_online.._value");
  err = getLastError();
  if (dynlen(err) > 0)
    setValue("", "color", "_dpdoesnotexist");

}

В первую очередь эта функция выполняет проверку, существует ли в системе точки данных. Если точки данных (точнее, DPE) не существует, то функция прекращает свою работу и выставляет цвет с именем «точкаданныхнесуществует». Далее идет вызов dpConnect. Нет, не так…

ДАЛЕЕ ИДЕТ ВЫЗОВ ФУНКЦИИ dpConnect!!!

Я намеренно выделяю эту мысль. Функция dpConnect и сам механизм «подписки» — основопологающий механизм всей системы WinCC OA. В обязательном порядке необходимо понять действие этого механизма, и правильно им пользоваться. В неумелых руках, при недальновидности, при глупости или при лени у вас получится не нормальный программный продукт в виде легкой и надежной операторской системы, а одно огромное разочарование, состоящее из глюков, тормозов и костылей. И виной тому будет не среда разработки WinCC OA, а чьи-то кривые руки. Запомните, я предупрежал!

Что же делает функция dpConnect в нашем случае?

dpConnect("EP_setRotationCB", "System1:Flap1.Inputs.Position:_online.._value");

Во-первых, эта функция сообщает системе о наличии Callback-функции EP_setRotationCB (она описана ниже). Во-вторых, вызов колбэк-функции EP_setRotationCB осуществляется по событию System1:Flap1.Inputs.Position:_online.._value, то есть, по изменению элемента точки данных. Ответственным за раздачу сообщений об изменении точки данных у нас, как вы помните, является менеджер событий (EV). Таким образом, изменение угла наклона прямоугольника у нас происходит тогда и только тогда, когда DPE Position точки данных Flap1 меняет свое значение. Как я говорил ранее, постоянного поллинга данных у нас нет, вся система событийная.

Можно провести некую аналогию с протоколом MQTT, работу которого я недавно разбирал на примере S7-1200. Менеджер событий у нас является брокером в этой аналогии. Драйвер связи с контроллером является издателем. А пользовательский интерфейс — подписчиком. Драйвер подключается к брокеру и публикует сообщение. UI подписывается на изменение точки данных (при помощи функции dpConnect). И как только это изменение произошло, вызывает обработчик этого события в виде callback-функции. В обычном состоянии подписчик не опрашивает брокера постоянно, а лишь слушает его в фоновом режиме, что и обеспечивает событийность любой обработки. Поскольку вся визуализация только тем и занимается, что меняет цвета, картинки, положение и т.д. в зависимости от значений переменных (тэгов), то этих вызовов dpConnect у вас будет очень много.

Причем, обратите внимание. Функция main состоит только из вызова функции EP_SetRotation. После этого вызова работа функции main прекращается. А вот функция EP_SetRotation, в свою очередь, осуществляет привязку изменений DPE к другой функции, и тоже прекращает свою работу. Возникает закономерный вопрос — а кто работать-то будет? Ответ на этот вопрос простой — dpConnect создает отдельный поток (подпроцесс) на компьютере, который и будет обрабатывать изменение графического элемента, и это в-третьих. В нормальном режиме, как я уже говорил, этот подпроцесс спит и ничего не делает, включаясь только по событию. Из этого делаем вывод, что программирование в WinCC OA — это многопоточное программирование, и это тоже надо иметь в виду. В реальной работе при таком подходе потребуются механизмы разделения, такие, как семафоры, или иные методы синхронизации.

Посмотрим саму callback-функцию

void EP_setRotationCB(string dp1, int iNewValue)
{
  float MIN_VALUE = 0;
  float MAX_VALUE = 90;
  float MIN_ROTATION = 0;
  float MAX_ROTATION = 90;

  float fRotation;
  fRotation = ( 1.0 * (MAX_ROTATION - MIN_ROTATION) / (MAX_VALUE - MIN_VALUE)) * 
              (iNewValue - MIN_VALUE) + MIN_ROTATION;
  if (fRotation > MAX_ROTATION) fRotation = MAX_ROTATION;
  else if (fRotation < MIN_ROTATION) fRotation = MIN_ROTATION;

  setValue("", "rotation", fRotation);
}

Первый параметр этой функции — точка данных (символьное имя точки данных), генерирующая изменения. Второй параметр — значение этой точки данных. Тип этого параметра зависит от типа DPE, на который происходит подписка.

Кроме проверки значений параметров эта функция содержит вызов setValue. Ее параметры:

первый (у нас пустой) — имя объекта, значение которого мы меняем. В данном случае пустое имя подразумевает текущий объект (наш прямоугольник);

второй (rotation) — имя свойства, которое мы меняем, а меняем мы угол наклона, это имя свойства берется из документации и из графического редактора свойств объекта… только «программное» имя свойства и «графическое» далеко не всегда совпадает (программисты классического WinCC меня поймут);

третий (fRotation) — значение свойства.

Возможно так же работать со свойствами в объектно-ориентированной нотификации, создавая объект типа Shape, привязываясь к существующему объекту (GetShape) и так далее. Это уже продвинутый уровень.

Надеюсь, написанного выше разъяснения, достаточно теперь для того,чтобы понять, почему этот скрипт написан для события инициализации.

Добавим еще на панель пару кнопок — открыть и закрыть. В настоящий момент мы работаем исключительно в WinCC OA, поэтому реакцию системы на команды реализуем тоже средствами этой системы. Перетаскиваем элемент Push Button на экран и меняем его свойство «Button label»

Далее необходимо запрограммировать нажатие кнопки Open. Для этого в списке событий выбираем Clicked и вызываем Script Editor. В редакторе я набираю вызов функции dpSet, это еще одна очень полезная функция, которая нам сильно пригодится в работе:

Параметры этой функции — вначале имя DPE, далее идет значение DPE. В меню Tools>Datapoint Selector есть возможность вызвать список всех точек данных проекта.

Выбираю там Flap1.Inputs.Position. Это делается осознанно, в качестве самого примитивного примера, далее проект будем улучшать. Значение «положения» в открытом состоянии — 90 (в соответствии с нашей мощной моделью данных). Редактор скриптов ловит только самые грубые синтаксические ошибки. Ошибки семантики не определяются. Например, очень распространенная ошибка — некорректная точка данных не будет определена на этом этапе. Вывод? Не забываем про отлов ошибок в самом скрипте. В этом скрипте мы ошибки не ловим никак, и это неправильно, в реальных проектах надо, как минимум, выполнить проверку dpExists.

Аналогично описываем обработчик события нажатия кнопки Close. С одной лишь разницей — в DPE Position записываем не 90, а 0. Теперь можно нажать на кнопку Save and run и проверить работу панели.

Нажали "Закрыть"
Нажали "Закрыть"
Нажали "Открыть"
Нажали "Открыть"

Сейчас необходимо обратить внимание на разумное использование dpSet. Эта функция, по сути, выполняет операцию присвоения. Смысл вызова

dpSet("System1:Flap1.Inputs.Position", 90);

…заключается в (привожу аналог)

System1:Flap1.Inputs.Position:= 90;

Но теперь давайте вспомним структуру системы. Сам скрипт присвоения выполнятся в менеджере ui. Значение DPE расположено в EV. Взаимодействие между ui и EV осуществляется по протоколу TCP/IP. Один вызов dpSet — это одно событие, передающееся от ui в EV. Один вызов (одно присвоение) — это ерунда. А если присвоений будет существенно больше? Например, два? ("Саша, существенно больше" (с) День Радио). Два миллиона? Или двести тысяч? Что произойдет в этом случае? А в этом случае от ui в сторону EV полетит не одно сообщение, а двести тысяч сообщений. Или два миллиона. И это может легко привести к переполнению очереди событий. От чего система гарантированно умрет. Вообще, как уже было замечено выше, WinCC OA в ленивых руках становится опасной, но при грамотном вдумчивом уважительном подходе способна на очень многое.

Как же поступать правильно? Правильно отправлять множество присвоений путем одного вызова функции dpSet. Например, вместо трех вызовов:

dpSet("System1:Flap1.Inputs.Position", 90);

dpSet("System1:Flap2.Inputs.Position", 90);

dpSet("System1:Flap3.Inputs.Position", 90);

делать один вызов

dpSet("System1:Flap1.Inputs.Position", 90, "System1:Flap2.Inputs.Position", 90, "System1:Flap3.Inputs.Position", 90);