Полезные советы
Содержание
- 1 Запуск скрипта по фронта или срезу дискретного сигнала
- 2 Создание переключателя
- 3 Реализация таймера - задержки включения (TON)
- 4 Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи
- 5 Скользящее среднее
- 6 ПИД - регулятор
- 7 Счетчик моточасов
- 8 Отладка сложных скриптов
- 9 Формирование суточных, недельных и т.п. отчетов
- 10 Преобразование числа в битовую табличку
- 11 Преобразование битовой таблички в число
- 12 Обработка чисел с плавающей точкой двойной точности
Запуск скрипта по фронта или срезу дискретного сигнала
Скрипт нужно вызываеть по изменению регистра. Внутри скрипта нужно сделать проверку текущего состояния этого регистра и выполнять соответветсующие действия либо по фронту (текущее состояние =1), либо по срезу (=0). Пример скрипта:
Создание переключателя
Элемент слайдер можно использовать для управления типа открыть- закрыть, вкл.-выкл. и т.п. Для этого нужно "привязать" его к битовому регистру и указать опцию "user can change value" на дешборде. Также такой слайдер наглядно может отобразить положение переключателя (ручн. - акт., местное - дистанционное управение), заслонки , шибера и т.д.
Реализация таймера - задержки включения (TON)
Таймер TON начинает отчет пока вход = 1, по истечении времени задержки выход тоже устанавливается в "1".
TIMER_DELAY = 20; -- задержка таймера 20 сек.
tmr = false; -- начальное состояние - отсчет времени не идет
tmrStartTime = 0; -- время начала работы таймера
--
function main (userId)
-- переменные
local in_value = (GetReg(212) == 1); -- Просто бит (D301@WebHMI)
local now = GetReg(28); -- Текущее время (T0@WebHMI)
-- ПРОВЕРЯЕМ УСЛОВИЕ - СОСТОЯНИЕ ВХОДА ----------
if in_value then
if not tmr then
tmr = true;
tmrStartTime = now; -- запомнить время начала отсчета
else
if ( now - tmrStartTime ) > TIMER_DELAY then
-- действие по истечении таймера
WriteReg("TON_out", 1); -- сигнал таймера , битовый регистр с псевдонимом "TON_out"
end
end
else
tmr = false;
tmrStartTime = 0;
WriteReg("TON_out", 0); -- сигнал таймера
end
-- КОНЕЦ
end
Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи
Можно анализировать скриптом время скана и при выходе его за допустимый предел, сигнализировать об этом разными способами. Ниже приведен пример обработки большого времени скана с сигнализацией в буфер сообщений и по Viber.
cntdownFlag = false; -- флаг обратного отчета таймера
timeStmp = 0; -- метка времени
msgSent = false; -- флаг отправки сообщения
function main (userId)
-- Читаем входные
local scan = GetReg(34); -- время скана
local c0 = GetReg(42); -- номер неработающего соединения
local SCANLIMIT = GetReg(886); -- предел времения скана
local SCANDELAY = GetReg(887); -- задержка реагирования на ошибку
--
if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then
ERROR("scan / c0 was read as nil");
return 0;
end
-- читаем время
local now = os.time();
-- шаблон сообщения
local msg1 = "Cкан тайм большой "..tostring(scan).." ".."ms, ошибка в соед. "..tostring(c0);
--
if (scan > SCANLIMIT) then -- скан выше нормы
if not cntdownFlag then
cntdownFlag = true;
timeStmp = now;
else
if (now - timeStmp) > SCANDELAY then
if not msgSent then
AddAlertMessage(msg1);
SendViberMessage(398044391, msg1); -- Женя
-- SendViberMessage(642997589, msg1); -- Игорь
SendViberMessage(335584075, msg1); -- Костя
msgSent = true;
end
end
end
else -- скан в норме
if (cntdownFlag == true) and msgSent then
AddInfoMessage("Скан вернулся к норме ");
SendViberMessage(398044391, "Скан вернулся к норме ");
SendViberMessage(335584075, "Скан вернулся к норме ");
msgSent = false;
end
cntdownFlag = false;
end
end -- main
В WebHMI имеются buzzer для подачи звукового сигнала, и выходные реле 2 шт. , которыми можно управлять для сигнализации (выдать на сигнальную колонну либо в ПЛК сигнал о проблеме).
Скользящее среднее
Скользящее среднее полезно для сглаживания значений параметров, имеющих шумы, пульсации.
Алгоритм скользящего среднего:
в начале работы фильтра на выборке в N значений идет подсчет арифметического среднего значения, по достижении конца выборки один элемент отбрасывается (путем деления суммы на длину очереди), вместо него добавляется новый и сумма снова делится на длину очереди.
-- глобальные переменные, сохраняют значения между вызовами программы
mav_len = 20; -- длина очереди
queue_fill = 0; -- индекс заполнения очереди
av_sum = 0; -- аккумулятор ск. среднего
function main (userId)
local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- читаем значение параметра
if (queue_fill < mav_len) then -- очередь не заполнена
av_sum = av_sum + in_value; -- накапливаем сумму
queue_fill = queue_fill +1; -- и индекс
else -- очередь полная дальше будет движение по очереди
tmp_var = av_sum / mav_len; -- запомнить один элемент
av_sum = av_sum - tmp_var + in_value; -- вычесть его и добавить новый
end
--
if (queue_fill == mav_len ) then
out_value = av_sum / mav_len; -- посчитать ск. среднее
else
out_value = av_sum / queue_fill; -- среднее арифм.
end
WriteReg("Tout_mav", out_value); -- Наружная температура среднее
end
ПИД - регулятор
Пример реализации ПИД регулятора в WebHMI:
-- глобальные переменные, сохраняются между вызовами скрипта
Kp = 1; -- пропорциональная составляющая
Ti = 0.9; -- инт. составляющая
Td = 1; -- дифф. составляющая
SampleTime = 10 ; -- время цикла ПИД
TimeStamp = 0; -- метка для запоминания времения последнего вызова
Limit = 100; -- ограничение выхода регулятора
Int_sum = 0; -- интегральный накопитель
--
function main (userId)
-- локальные переменные
local now = GetReg("SysTime"); -- Время
local PV = GetReg("PID_PV"); -- PV (D14@Тест) обратная связь
local Sp = GetReg("PID_Sp"); -- Sp (D10@Тест) задание
local prevErr = 0.0; -- предыдущая ошибка для вычисления дифф. составляющей
--
if (TimeStamp == 0) then
TimeStamp = now; -- запомнить время входа в цикл
--
local Err = PV - Sp; -- вычисляем ошибку
local dErr = Err - prevErr; -- вычисляем производную ошибки
-- проверяем интегральное насыщение
local iSum_Limit = Limit * Ti / (Kp);
if (Int_sum <= iSum_Limit) and (Int_sum >= 0.0) then
Int_sum = Int_sum + Err; -- накапливаем интеграл ошибки
elseif Int_sum < 0 then
Int_sum = 0;
else
Int_sum = iSum_Limit; -- ограничиваем интегральную составляющую
end;
-- ПИД - регулятор
G = Kp * (Err + (1/Ti)*Int_sum + Td*dErr);
-- проверка выхода за диапазон
if G < 0 then
G = 0;
end
if G > Limit then
G = Limit;
end
prevErr = Err; -- запомнить предыдущую ошибку для след. скана
WriteReg("PID_out", G); -- Выход ПИД (D0@Тест) записать в регистр
else
if (now - TimeStamp > SampleTime ) then -- проверка начала цикла работы
TimeStamp = 0;
end
end
end
Данный алгоритм является типовым для применения в ПЛК. Поскольку регулятор выполняется через равные интервалы времени, т.е. дифф. и инт. составляющие всегда вычисляются в одном мастштабе времени, поэтому делить и умножать их на время для получения производной и интеграла необязательно, можно подбирать постоянные времени Ti, Td. В данном алгоритме Ti является обратной величиной (чем больше ее величина, тем меньше вклад интегральной ошибки)
Счетчик моточасов
Счетчик моточасов удобен для автоматической генерации сообщения о необходимости регламенных работ для узла оборудования, смены ведущего насоса в насосной группе для выравнивания наработки и т.п.
Пример реализации счетчика моточасов на Lua в WebHMI (программа выполняется в каждом скане):
-- глобальные переменные, сохраняются между вызовами скрипта
run_state = false; -- для запоминания текущего состояния
function main (userId)
-- локальные переменные
local check_mask = tonumber("0000100000000000",2); -- маска для проверки бита вращения в частотном приводе FC 51 Danfoss
local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- результат проверка как переменная типа bool
local now = os.time(); -- текущее время системы
local time_diff = 0; -- разница во времени между текущим временем и временем последнего вызова
-- ловим фронт события включения механизма для инициализации
if (not run_state) and run_status then
WriteReg("P43StartTime", now); -- Время старта привода №П43
-- считаем время
if run_state then
time_diff = (now - GetReg("P43StartTime")); -- посчитать разницу времени
WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- увеличить счетчик моточасов
WriteReg("P43StartTime", now); -- переписать начальную точку времени
end
run_state = run_status;
end
Регистры хранения моточасов и метки времени нужно делать энергонезависимыми.
Отладка сложных скриптов
Инициализация проекта
Энергонезависимые регистры
Следует помнить что регистры типа Dхх, Sxx и некоторые другие внутренние регистры, кроме DSxx возвращаются в начальное состояние (как правило в 0) после инициализации проекта (что происходит при любом редактировании элементов проекта - скриптов, регистров, соединений и т.д.). Если такие регистры используются как переменные для формирования триггеров событий или условий выполнения других скриптов, это может приводить к нарушению логики выполнения. Следует учитывать эту особенность при выборе регистров как входных и выходных переменных скриптов.
Глобальные переменные
Переменные в скриптах, объявленные до функции main сохраняют свои значения между вызовами скрипта, но при инициализации также принимают начальные значения. Их можно использовать для хранения констант, коэффициентов и др. подобных величин.
Первый скан
Скрипту можно назначить способ запуска на первом скане при инициализации проекта и сделать некую инициализацию переменных из одного места. Иногда может понадобится определить внутри самого скрипта, был ли это первый вызов после инициализации. Можно создать глобальную переменную first_scan:
first_scan = true;
function main (userId)
if first_scan then
first_scan = false;
--[[
Действия при инициализации проекта
--]]
end --if
end -- main
Следует помнить что регистры типа Dхх, CDxx и другие внутренние регистры, кроме DSxx возвращаются в начальное состояние (как правило в 0) после инициализации проекта (что происходит при любом редактировании элементов проекта - скриптов, регистров, соединений и т.д.). Если такие регистры используются как переменные для формирования триггеров событий или условий выполнения других скриптов, это может приводить к нарушению логики выполнения. Следует учитывать эту особенность при выборе регистров как входных и выходных переменных скриптов.
Модульность
Рекомендуется разбивать сложные скрипты на более простые и часто используемые функции, которые можно использовать повторно. Разбивка на более простые части, расположение их в нужном порядке и группировка помогает контролировать логику работы системы и легче наладить систему. Например, если есть задача управления положением задвижки, можно выделить такие части:
- блок "захвата" момента включения привода, для инициализации начальной позиции и времени начала движения
- блок расчета текущей позиции в движении (который определяет время между вызовами скрипта и вычисляет пройденный путь штока)
- блок авт. управления (проверяет рассогласование между уставкой и тек. положением, определяет направление и дает команду, если нет других блокировок)
- блок ручного управления
- блок блокировок по наезду на конечные выключатели - для снятия текущей команды и установки позиции в 0/100%
Сложные функции перед вложением их другие скрипты можно отладить отдельно в пользовательском скрипте, выделив часть внутренних регистров под входные переменные, часть под выходные. Эти же регистры вынести на дешборд, вместе с кнопкой запуска отлаживаемого скрипта. Тогда меняя входные наборы данных удобно видеть тут же результат выполнения. Также можно использовать отдельную IDE типа Eclipse для Lua или онлайн версию, чтобы удобно и быстро отлаживать маленькие фрагменты, изучать работу новых функций и т.д.
Отличия записи во внутренние регистры от регистров в устройствах
Есть некоторые отличия в работе функций SetReg и WriteReg применительно к внутренним регистрам (Dxx, DSxx). Эти функции непосредственно меняют значения внутренних регистров внутри скана, а не откладывают запись WriteReg на следующий скан. Таким образом, в конце скана внутренний регистр может иметь значение, отличное от того которые было на входе в скан. Тогда, например возможна ситуация, когда:
- скрипт 1 меняет значение некоего регистра Dn. (выполняется в каждом скане)
- скрипт 2 работает по изменению этого регистра Dn. (выполняется по изменению регистра)
Если порядок выполнения скриптов будет 1 - 2, то все будет работать, потому что на входе в скан скрипт 2 видел одно значение, и перед своим выполнением другое (которое успел изменить скрипт 1), и отработает "по изменению". Если же порядок выполнения скриптов поменять местами, то скрипт 2 перестанет, работать, так как на входе в текущий скан он будет видеть измененное значение, а новое изменение произойдет после скрипта 2 в скрипте 1.
Отладочная печать
Желательно сразу ставить после ключевых моментов логики в скриптах отладочную печать функциями TRACE, c DEBUG c номером скрипта или названием функции. Тогда эти фрагменты удобно искать и анализировать в коммуникационном логе. Однако в большой системе, когда отладочной печати становится много, становится неудобно искать необходимые данные. Можжно поступить следующим образом - назначить свою функцию отладочной печати, которая будет вызываться только, если отладочная печать в этом скрипте разрешена. Например, можно в регистр "debug_ID" записывать id скрипта в котором нужна отладночная печать, а функция внутри скрипта будет смотреть на этот номер. Например:
thisScriptID = 15;
function main (userId)
MyDebugPrint("Программа симуляции теплосчетчка");
local VLVpos = GetReg("curPosition"); -- Текущее положение клапана (DS1005@WebHMI 3point control)
--[[
code
--]]
end
function MyDebugPrint(str)
local debug_id = GetReg("debug_ID");
if (debug_id == thisScriptID) then
ERROR(str);
end
end
Внутри функции задавать уровень лога - INFO, ERROR и т.д. а в настройках системы временно отключать неиспользуемые уровни, чтобы сфокусироваться только на нужной отладочной информации. Можно развить вариант до одновременного вывода в нескольких скриптах, если debug_ID сделать строкой вида "25_26_27", т.е. перечень скриптов с включенной отладкой. Номера можно быстро выделять используя строковые функции поиска паттернов:
------- debug printing -------
function DEBUG_(str)
local i = 0;
local tmp = "";
while true do
i,_, tmp = string.find(str, "(%d+)%s+",i+1);
if i == nil then break end -- не найдено
if (tmp ~= "0") then
-- найдено проверить на совпадение
if (tmp == tostring(thisScriptID)) then
ERROR(str);
end
end
end
end -- DEBUG_
------------------------------
Формирование суточных, недельных и т.п. отчетов
Для оптимизации производительности устройства данные для отчетов целесообразно регистрировать "в потоке". В WebHMI для этого есть механизм событий, которые после регистрации фактически дают готовые отчеты.
- Необходимые переменные, скрипты и события -
- Необходимые переменные, скрипты и события -
- скрипт, выдающий номер дня, месяца, недели, квартала и т.п. из системного времени во внутренний регистр "день недели", "час", "секунда"
- скрипт подсчета разницы "текущие показание - предыдущие", и фиксации результата в регистре "счетчик" (скрипт работает также по изменению регистра "минута")
- регистр-флаг генерации события, однократного, достаточного для записи текущего посчитанного счетчика и метки времени
- скрипт, снимающий флаг, когда есть событие (т.е. данные уже зафиксированы)'
- событие для регистрации
Для демонстрации можно использовать пример, в котором на каждой секунде "виртуальный" счетчик увеличивается на 2. Другие скрипты реализуют поминутную запись разницы его показаний в отчет. Для добавления других параметров, необходимо по аналогии добавить их внутрь соответствующих скриптов и событий. Скрипт, генерирующий номера минут:
function main (userId)
-- получить номер минуты
local min = os.date("%M",os.time());
-- записать во внутренний регистр
WriteReg("minute", min); -- Минуты
end
Скрипт, работающий на изменение регистра "minute":
function main (userId)
local current = GetReg("FlowMeter"); -- текущие показания
local prevMin = GetReg("pMinTotal"); -- предыдущее значение
local cnt = current - prevMin; -- посчитать разницу
WriteReg("MinuteFlow", cnt); -- записать счетчик во внутр. регистр. он используется в событии
WriteReg("pMinTotal", current); -- записать текущее в предыдущее для след. периода
-- поднять флаг для отчета
WriteReg("EventFlag", 1); -- Установить флаг для события
end
Скрипт, снимающий флаг и соответственно событие после записи:
function main (userId)
-- проверяем выполняется ли событие
local event_state = (GetReg("ES1") == 1); -- Событие 1 выполняется (ES1@Internal register)
-- и сбрасываем флаг
if event_state then
WriteReg("EventFlag", 0); -- Флаг минутного отчета
end
end
Настройки события для данного отчета:
Результатом будет являться отчет такого вида:
При поминутной регистрации данные результаты получаться, если время скана будет меньше чем секунда. Тогда "виртуальный" электросчетчик не пропустит своих секунд.
Преобразование числа в битовую табличку
В lua (в версии, используемой в WebHMI) нет встроенной операции получения строкового представления двоичного числа. Однако эта функция очень полезна в пользовательсиких протоколах и др. задачах, где требуется перераспределить или каким то образом обработать отдельные биты числа. Текст варианта такой функции:
function getBits(input_num,length) -- работает с заданной длиной
local tab = {}; -- пустая табличка для ответа
local max_i = length - 1;
local remainder = input_num; -- остаток порязрядного взешивания
for i=max_i,0,-1 do
if remainder - 2^i >= 0 then
table.insert(tab, "1") ;
remainder = remainder - 2^i;
else
table.insert(tab, "0") ;
end
end
return tab;
end -- getBits
Далее можно переставить нужные биты местами и т.п. и получить нужное число. Ниже приведен пример перестановки битов 31 и 23 битов (при нумерации битов с 0) в ответе счетчика расходомера ВЛР 2301/2304 производства Асвега-У
function main (userId)
local input_num = GetReg(3); -- прочитать регистр с числом
local bitTable = getBits(input_num,32); -- таблица для обработки результата
local tmp_bit = ""; -- вспомогательный бит
tmp_bit = bitTable[1];
bitTable[1] = bitTable[9]; -- 31 бит это 1 элемент так как таблица развернута слева-направо, номера элементов табл.в lua c 1
bitTable[9] = tmp_bit;
-- конкатенация готовой таблицы в строку и преобразование в число из строки двоичного представления
WriteReg(1, tonumber(table.concat(bitTable),2));
end
Преобразование битовой таблички в число
Данная функция может быть полезна для нестандартных преобразований, когда число хранится в специфичном формате и его необходимо обработать по частям, используя заданную позицию 1-го бита и длину поля значения.
function getNumberFromTab(tab,start,length) -- получить число из таблички
local result_str = "";
for i=start,(start+length-1) do
result_str = result_str..tostring(tab[i]);
end
return tonumber(result_str,2);
end -- getNumberFromTab
Обработка чисел с плавающей точкой двойной точности
Некоторые устройства могут хранить данные в формате с повышенной точностью double float. В WebHMI текущей версии поддерживаются только 32 битные регистры, поэтому чтобы обработать 64 битное число необходимо будет использовать скрипт, которые "сцепит" два регистра в исходное число, а затем преобразует и запишет в обратно в float. Регистры должны быть типа double uint. Для "сцепки" чисел можно использовать описанную выше функцию getBits чтобы получить 2 таблицы, а затем дополнить первую таблицу битами из второй используя table.insert. В этом примере в качестве проверочного числа для простоты используется константа.
--------------------------------------
local test_var = 0xC1D312D000000000;
local NaN = tonumber("11111111111111111111111111111111",2);
--------------------------------------
function main (userId)
-- получить результирующую табличку "0" "1"
local result_tab = getBits(test_var,64);
WriteReg(11,table.concat(result_tab)); -- отладочная печать в регстр - строку
local result_num = 0.0; -- для хранения результата
local sign,exp,mantissa = 0,0,0;
local fraction_table = {}; -- табл. для дробной части
-- определить знак
if result_tab[1] == "1" then
sign = -1;
else
sign = 1;
end
-- экспоненту
exp = getNumberFromTab(result_tab,2,11);
DEBUG("exp = "..tostring(exp)); -- отл. печать
-- мантиссу
for i=13,64 do
table.insert(fraction_table,result_tab[i]);
end
-- посчитать мантиссу по-разрядно !!!
for j=1,52 do
if fraction_table[j]=="1" then
mantissa = mantissa +(2^(-1*j));
end
end
mantissa = mantissa +1;
DEBUG("m = "..tostring(mantissa)); -- отл. печать
result_num = sign*(2^(exp - 1023))*mantissa;
-- Обработка исключений
if exp == 0 then -- subnormals
result_num = sign*(2^(-1022))*(mantissa-1);
end
if exp == 0x7ff then -- nan
result_num = NaN;
end
-- Вывод результата в регистр типа float
WriteReg(10, result_num);
end -- main