Полезные советы

Материал из WebHMI Wiki
Версия от 15:53, 29 сентября 2017; Evgeniy.mozoliak (обсуждение | вклад) (Модульность)

Перейти к: навигация, поиск

Запуск скрипта по фронта или срезу дискретного сигнала

Скрипт нужно вызываеть по изменению регистра. Внутри скрипта нужно сделать проверку текущего состояния этого регистра и выполнять соответветсующие действия либо по фронту (текущее состояние =1), либо по срезу (=0). Пример скрипта:

Rising edge.png

Создание переключателя

Элемент слайдер можно использовать для управления типа открыть- закрыть, вкл.-выкл. и т.п. Для этого нужно "привязать" его к битовому регистру и указать опцию "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хх, CDxx и другие внутренние регистры, кроме 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", т.е. перечень скриптов с включенной отладкой. Номера можно быстро выделять используя строковые функции поиска паттернов.

Формирование суточных, недельных и т.п. отчетов

Для оптимизации производительности устройства данные для отчетов целесообразно регистрировать "в потоке". В 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

Настройки события для данного отчета:
Rep example2.png
Rep example3.png


Результатом будет являться отчет такого вида:
Rep example.png
При поминутной регистрации данные результаты получаться, если время скана будет меньше чем секунда. Тогда "виртуальный" электросчетчик не пропустит своих секунд.

Преобразование числа в битовую табличку

В 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