В процессе создания модели поведения клапанов мы создали (и в прошлой части модифицировали его) скрипт Model, в котором было несколько вызовов dpConnect и несколько callback-функций. Тогда я писал, что это «быстрый, но неправильный способ». Эта пауза была необходима, чтобы предварительно ознакомиться с функцией dpQuery. Предлагаю вернуться немного назад и реализовать модель медленно и очень занудно правильно, теперь при помощи функций семейства dpQueryConnect. Откроем наш скрипт Model.
// $License: NOLICENSE
//--------------------------------------------------------------------------------
/**
@file $relPath
@copyright $copyright
@author akcou
*/
//--------------------------------------------------------------------------------
// Libraries used (#uses)
//--------------------------------------------------------------------------------
// Variables and Constants
//--------------------------------------------------------------------------------
/**
*/
main()
{
dpConnect("OnOpen_CB1", "System1:Flap1.Commands.Open");
dpConnect("OnOpen_CB2", "System1:Flap2.Commands.Open");
dpConnect("OnOpen_CB3", "System1:Flap3.Commands.Open");
for (;;) {
dpSet("System1:Flap1.Inputs.Flow", rand());
dpSet("System1:Flap2.Inputs.Flow", rand());
delay(1);
}
}
void OnOpen_CB1(string dp1, bool bNewValue)
{
if (bNewValue) {
dpSet("System1:Flap1.Inputs.Position", 90);
} else {
dpSet("System1:Flap1.Inputs.Position", 0);
}
}
void OnOpen_CB2(string dp1, bool bNewValue)
{
if (bNewValue) {
dpSet("System1:Flap2.Inputs.Position", 90);
} else {
dpSet("System1:Flap2.Inputs.Position", 0);
}
}
void OnOpen_CB3(string dp1, bool bNewValue)
{
if (bNewValue) {
dpSet("System1:Flap3.Inputs.Position", 90);
} else {
dpSet("System1:Flap3.Inputs.Position", 0);
}
}
В функции dpConnect мы явно указываем одну или несколько DPE, на которую происходит подписка. Функциями типа dpQueryConnect мы подписываемся на неопредленное явно количество точек данных. На те точки данных, которые попадают под указанный SQL-запрос. Очевидно, что в нашем случае необходимо подготовить SQL-запрос, который возвращает все наши клапаны: Flap1, Flap2 и Flap3. Существует два вида функций — dpQueryConnectSingle и dpQueryConnectAll. Первая осуществляет передачу коллбэк-функции только одного значение. Того, которое изменилось. Например, если изменение коснулось только второго клапана, то передается значение DPE именно второго клапана. При использовании второй функции в обработчик прилетают значения со всех «подписанных» клапанов, даже если они не изменялись.
Необходимо так же иметь в виду, что в рамках системы WinCC OA у нас есть возможность создавать точки данных динамически, в том числе — и через программный код, не перезапуская при этом рантайм целиком. Например, сейчас у нас есть три клапана, и посредством dpQueryConnectSingle произошла подписка. Все работает. Не останавливая систему, мы создаем Flap4. И вот в этом случае подписки на Flap4 не произойдет, так как эта точка данных была добавлена после старта скрипта и выполнения подписки. Такая ситуация не является безвыходной, есть решение. Необходимо подписаться на событие «добавление точки данных». Если произошло такое событие, смотрим, какая точка добавлена. Если добавлена точка данных «клапан», выполняем вначале отписку от изменений (от Flap1, Flap2, Flap3), после чего подписываемся заново (уже на все 4 клапана). Проблема решена.
Продемонстрируем «правильную» подписку на изменения. Для начала откроем снова окно SQL-query и составим следующий запрос.
Откроем скрипт Model и приведем функцию main к следующему виду
main()
{
dpQueryConnectSingle("OnOpenAll_CB",FALSE, "", "SELECT '_original.._value' FROM 'Flap*.Commands.Open'");
//dpConnect("OnOpen_CB1", "System1:Flap1.Commands.Open");
//dpConnect("OnOpen_CB2", "System1:Flap2.Commands.Open");
//dpConnect("OnOpen_CB3", "System1:Flap3.Commands.Open");
for (;;) {
dpSet("System1:Flap1.Inputs.Flow", rand());
dpSet("System1:Flap2.Inputs.Flow", rand());
delay(1);
}
}
Отдельные dpConnect исключаем из программы. Используем dpQueryConnectSingle. Обработчиком будет функция OnOpenAll_CB.
Привожу текст обработчика. Описание работы callback-функции находится в комментариях к программному коду. На вход функции мы получаем динамический массив, первая строка которого нам неинтересна, она содержит заголовок выполнения SQL-запроса. Нам, по сути, интересна только вторая строка и не более, но, зачем-то, я обрабатываю все строки в цикле, хотя в этом конкретном случае строк будет всего две.
Первый элемент второй строки — имя точки данных команды Открыть для задвижки. Имя содержит в том числе и FlapX, где X — это номер задвижки. Для того, чтобы задать Position того или иного клапана нам необходимо вычленить этот FlapX. А потом, зная новое значение этой «команды» мы задаем фактическое положение клапана. Новое значение — это второй элемент второй строки. Итого
sDPE — получаем имя точки данных команды, например System1:Flap2.Commands.Open
bNewValue — получаем значение этой команды, true или false
split — разбиваем строку с DPE на подстроки, нам интересна только первая подстрока, которая содержит в себе явное имя клапана, например System1:Flap2
Далее в зависимости от состояния команды задаем «процент открытия» клапана, зная точно, какой это клапан.
OnOpenAll_CB(string s, dyn_dyn_anytype ddaTab)
{ //ddaTab contains DPE and the changed value
int z; //вспомогательная переменная
string sDPE; //элемент точки данных, который инициировал скрипт
dyn_string split; //для парсинга DPE
bool bNewValue; //значение команды задвижки
for(z=2;z<=dynlen(ddaTab);z++) //цикл тут необязателен, это я делаю копи-паст из примера справки
{
sDPE = ddaTab[z][1]; //получить DPE, по которому вызвался обработчик - это DPE команды, типа System1:FlapX.Commands.Open
bNewValue = ddaTab[z][2]; //получить значение коменды, тру или фолс
split = strsplit(sDPE, "."); //разбить имя DPE на составляющие части, нам нужна первая часть, типа System1:FlapX
//DebugN("AGK", sDPE, bNewValue);
//DebugN("AGK", split);
if (bNewValue) { //если команда "открыть"
dpSet(split[1] + ".Inputs.Position", 90); //дать значение Position в 90 градусов
} else {
dpSet(split[1] + ".Inputs.Position", 0); //иначе в ноль градусов
}
}
}
Сохраним скрипт, перезапустим его менеджер, откроем в исполнении панель Main и убедимся, что модель работает.
В настоящий момент осталось лишь разобраться с тем, как полноценно запускать пользовательский интерфейс. Выполнение панели Main через QuickTest годится только для отладочных целей, но вряд ли подходит для операторских систем. Конечному заказчику необходим запуск пользовательского интерфейса, а не среды разработки. Для этого необходимо в консоли добавить ui-менеджер.
Нам необходим User Interface. В опциях указываем номер 2 (-num 2), потому как первый ui — это редактор, и он уже есть. Вторая опция, «-p Main.pnl» — это имя панели, которая будет открываться в менеджере. Дополнительно я выбрал автоматический запуск менеджера (always). Нажимаем ОК и наблюдаем за запуском ui.