Полезные программы — различия между версиями

Материал из WebHMI Wiki
Перейти к: навигация, поиск
 
(не показаны 22 промежуточные версии этого же участника)
Строка 1: Строка 1:
 
+
<languages/>
== Полезные программы ==  
+
<translate>
 +
== Полезные программы == <!--T:1-->
 
=== Запуск скрипта по фронта или срезу дискретного сигнала ===
 
=== Запуск скрипта по фронта или срезу дискретного сигнала ===
 
Скрипт нужно вызываеть по изменению регистра. Внутри скрипта нужно сделать проверку текущего состояния этого регистра и выполнять соответветсующие действия либо по фронту (текущее состояние =1), либо по срезу (=0).
 
Скрипт нужно вызываеть по изменению регистра. Внутри скрипта нужно сделать проверку текущего состояния этого регистра и выполнять соответветсующие действия либо по фронту (текущее состояние =1), либо по срезу (=0).
Строка 6: Строка 7:
 
[[Файл:Rising edge.png | 1000 px | left]] <br clear = all >
 
[[Файл:Rising edge.png | 1000 px | left]] <br clear = all >
  
=== Реализация таймера - задержки включения (TON) ===  
+
=== Реализация универсального таймера (TON TOFF) === <!--T:2-->
Таймер TON начинает отчет пока вход = 1, по истечении времени задержки выход тоже устанавливается в "1".  
+
Таймер сравнивает свое текущее состояние с состоянием на входе:
 +
* текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0.
 +
* текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0.  
  
 +
<!--T:3-->
 +
Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера.
  
 
<syntaxhighlight lang="Lua">
 
<syntaxhighlight lang="Lua">
-- глобальные
 
TIMER_DELAY = 20; -- задержка таймера 20 сек.
 
tmrStartTime = 0; -- время начала работы таймера
 
  
function main (userId)
+
function TimerOnOff(bool_input, onDelay, offDelay, tmrAlias)
  -- лок. переменные
+
--                   bool    seconds  seconds  string
local now = os.time(); -- Текущее время
+
    local outAlias = tmrAlias.."_out"
local startBit = (GetReg("tmrStartBit") == 1); -- Просто бит (D301@WebHMI)
+
    local curTmrState = (GetReg(outAlias) == 1) ; DEBUG("curTmr  State "..tostring(curTmrState))
 +
                                                  DEBUG("bool_input = "..tostring(bool_input))
 +
                                               
 +
    if (bool_input == curTmrState) then
 +
        WriteReg(tmrAlias, now) ;                 DEBUG("timer input match state ")
 +
        return curTmrState  -- as bool
 +
    elseif bool_input then
 +
                                                    DEBUG("countdown for onDelay = "..(onDelay - (now - GetReg(tmrAlias))))
 +
        if now - GetReg(tmrAlias) > onDelay then                                             
 +
                WriteReg(outAlias, 1)
 +
                                                    DEBUG("detected ON state after delay ")
 +
                return true
 +
          end
 +
    else
 +
                                                    DEBUG("countdown for offDelay = "..(offDelay - (now - GetReg(tmrAlias))))
 +
        if (now - GetReg(tmrAlias) > offDelay) then                                             
 +
                WriteReg(outAlias, 0)
 +
                                                    DEBUG("detected OFF state after delay ")
 +
                return false 
 +
        end   
 +
    end  --
 +
    return  curTmrState  -- output rerurned as bool for simple usage in IFs
  
 +
<!--T:4-->
 +
end -- function
 +
</syntaxhighlight>
  
-- ПРОВЕРЯЕМ УСЛОВИЕ ----------
+
Пример использования:
if not startBit then
+
 
    tmrStartTime = now; -- здесь сохранится время начала отсчета
+
Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if.
    WriteReg("TON_out", 0); -- сигнал таймера , битовый регистр с именем "TON_out"
+
 
    return 0 ;
+
 
else
+
<!--T:6-->
    if (now - tmrStartTime) > TIMER_DELAY then
+
<syntaxhighlight lang="Lua">
      -- действия по истечении таймера
+
                                              DEBUG(" hotWater timer call ")
            WriteReg("TON_out", 1);
+
local DI2 = (GetReg(39) == 1) -- hotwater low pressure 
    end  -- if 
+
local DI3 = (GetReg(40) == 1) -- hotwater high pressure 
end -- if
+
local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off . 
end -- main
+
 
 +
<!--T:7-->
 +
-- or like this
 +
if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then
 +
  -- do something
 +
end -- simple timer in one row !
 +
 
 +
<!--T:8-->
 
</syntaxhighlight>
 
</syntaxhighlight>
  
===  Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи ===  
+
===  Cигнализация (Звуковая, релейная, sms, viber, telegram) об ошибках связи === <!--T:6-->
  
 +
<!--T:7-->
 
Можно анализировать скриптом время скана и при выходе его за допустимый предел, сигнализировать об этом разными способами. Ниже приведен пример обработки большого времени скана с сигнализацией в буфер сообщений и по Viber.  
 
Можно анализировать скриптом время скана и при выходе его за допустимый предел, сигнализировать об этом разными способами. Ниже приведен пример обработки большого времени скана с сигнализацией в буфер сообщений и по Viber.  
  
  
 +
<!--T:8-->
 
<syntaxhighlight lang= "lua">
 
<syntaxhighlight lang= "lua">
 
cntdownFlag = false; -- флаг обратного отчета таймера  
 
cntdownFlag = false; -- флаг обратного отчета таймера  
Строка 46: Строка 81:
  
  
 +
<!--T:9-->
 
function main (userId)
 
function main (userId)
 
   -- Читаем входные
 
   -- Читаем входные
Строка 88: Строка 124:
 
end  
 
end  
  
 +
<!--T:10-->
 
end -- main  
 
end -- main  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
  
 +
<!--T:11-->
 
В WebHMI имеются buzzer для подачи звукового сигнала, и выходные реле 2 шт. , которыми можно управлять для сигнализации (выдать на сигнальную колонну либо в ПЛК сигнал о проблеме).
 
В WebHMI имеются buzzer для подачи звукового сигнала, и выходные реле 2 шт. , которыми можно управлять для сигнализации (выдать на сигнальную колонну либо в ПЛК сигнал о проблеме).
  
=== Скользящее среднее ===  
+
=== Скользящее среднее === <!--T:12-->
 
Скользящее среднее полезно для сглаживания значений параметров, имеющих шумы, пульсации.  
 
Скользящее среднее полезно для сглаживания значений параметров, имеющих шумы, пульсации.  
 
Алгоритм скользящего среднего: <br>
 
Алгоритм скользящего среднего: <br>
Строка 105: Строка 143:
  
  
 +
<!--T:13-->
 
function main (userId)
 
function main (userId)
  
  
 +
<!--T:14-->
 
local in_value, tmp_var, out_value  = GetReg(26), 0, 0; -- читаем значение параметра
 
local in_value, tmp_var, out_value  = GetReg(26), 0, 0; -- читаем значение параметра
 
    
 
    
Строка 125: Строка 165:
 
WriteReg("Tout_mav", out_value); -- Наружная температура среднее  
 
WriteReg("Tout_mav", out_value); -- Наружная температура среднее  
  
 +
<!--T:15-->
 
end
 
end
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== ПИД - регулятор ===  
+
=== ПИД - регулятор === <!--T:16-->
 
Пример реализации ПИД регулятора в WebHMI:
 
Пример реализации ПИД регулятора в WebHMI:
  
  
 +
<!--T:17-->
 
<syntaxhighlight lang = "lua">
 
<syntaxhighlight lang = "lua">
 
G_LIMIT = 100 -- ограничение выхода регулятора
 
G_LIMIT = 100 -- ограничение выхода регулятора
Строка 221: Строка 263:
 
end -- if auto  
 
end -- if auto  
  
 +
<!--T:18-->
 
end -- main  
 
end -- main  
  
 +
<!--T:19-->
 
-- округлление  
 
-- округлление  
 
function Round(var)
 
function Round(var)
Строка 235: Строка 279:
 
end  
 
end  
  
 +
<!--T:20-->
 
------ debug printing ------
 
------ debug printing ------
 
thisScriptID = 45
 
thisScriptID = 45
  
 +
<!--T:21-->
 
function DEBUG_(str)
 
function DEBUG_(str)
 
                 --ERROR("entered DEBUG_ in"..thisScriptID.." script");
 
                 --ERROR("entered DEBUG_ in"..thisScriptID.." script");
Строка 245: Строка 291:
 
local debug_id = GetReg("debug_IDs");
 
local debug_id = GetReg("debug_IDs");
  
 +
<!--T:22-->
 
local capture_mask = "%s+(%d+)%s+"
 
local capture_mask = "%s+(%d+)%s+"
  
 +
<!--T:23-->
 
while true do  
 
while true do  
 
i,_, tmp = string.find(debug_id,capture_mask,i+1)
 
i,_, tmp = string.find(debug_id,capture_mask,i+1)
  
     if (i == nil)  then  
+
     <!--T:24-->
 +
if (i == nil)  then  
 
         break -- не найдено  
 
         break -- не найдено  
 
     end   
 
     end   
Строка 263: Строка 312:
 
end  
 
end  
  
 +
<!--T:25-->
 
end -- DEBUG_
 
end -- DEBUG_
 
------------------------------
 
------------------------------
 
</syntaxhighlight>
 
</syntaxhighlight>
  
 +
<!--T:26-->
 
Данный алгоритм является типовым для применения в ПЛК. Поскольку регулятор выполняется через равные интервалы времени, т.е. дифф. и инт. составляющие всегда вычисляются в одном масштабе времени, поэтому делить и умножать их на время для получения производной и интеграла необязательно, можно подбирать постоянные времени Ti, Td. В данном алгоритме Ti является обратной величиной (чем больше ее величина, тем меньше вклад интегральной ошибки)
 
Данный алгоритм является типовым для применения в ПЛК. Поскольку регулятор выполняется через равные интервалы времени, т.е. дифф. и инт. составляющие всегда вычисляются в одном масштабе времени, поэтому делить и умножать их на время для получения производной и интеграла необязательно, можно подбирать постоянные времени Ti, Td. В данном алгоритме Ti является обратной величиной (чем больше ее величина, тем меньше вклад интегральной ошибки)
  
=== Счетчик моточасов ===  
+
=== Счетчик моточасов === <!--T:27-->
  
  
 +
<!--T:28-->
 
Счетчик моточасов удобен для автоматической генерации сообщения о необходимости регламенных работ для узла оборудования, смены ведущего насоса в насосной группе для выравнивания наработки и т.п.  
 
Счетчик моточасов удобен для автоматической генерации сообщения о необходимости регламенных работ для узла оборудования, смены ведущего насоса в насосной группе для выравнивания наработки и т.п.  
  
  
 +
<!--T:29-->
 
Пример реализации счетчика моточасов на Lua в WebHMI (программа выполняется в каждом скане):
 
Пример реализации счетчика моточасов на Lua в WebHMI (программа выполняется в каждом скане):
  
 +
<!--T:30-->
 
<syntaxhighlight lang = "lua">
 
<syntaxhighlight lang = "lua">
 
-- глобальные переменные, сохраняются между вызовами скрипта
 
-- глобальные переменные, сохраняются между вызовами скрипта
Строка 288: Строка 342:
  
  
 +
<!--T:31-->
 
-- ловим фронт события включения механизма для инициализации
 
-- ловим фронт события включения механизма для инициализации
 
   if (not run_state) and run_status then  
 
   if (not run_state) and run_status then  
 
       WriteReg("P43StartTime", now); -- Время старта привода №П43
 
       WriteReg("P43StartTime", now); -- Время старта привода №П43
 +
  end
  
 
+
<!--T:32-->
 
-- считаем время  
 
-- считаем время  
 
  if run_state then  
 
  if run_state then  
Строка 304: Строка 360:
  
  
 +
<!--T:33-->
 
Регистры хранения моточасов и метки времени нужно делать энергонезависимыми.
 
Регистры хранения моточасов и метки времени нужно делать энергонезависимыми.
  
=== Алгоритм чередования по времени (циркуляция с функцией АВР) ===  
+
=== Алгоритм чередования по времени (циркуляция с функцией АВР) === <!--T:34-->
 
Данный алгоритм используется в системах, где необходимо чередовать работу механизмов (насосы, вентиляторы, кондиционеры) по времени, либо по моточасам (наработке).
 
Данный алгоритм используется в системах, где необходимо чередовать работу механизмов (насосы, вентиляторы, кондиционеры) по времени, либо по моточасам (наработке).
 
Для примера используется набор из 2-х установок, которые необходимо чередовать по времени. Если на какой-то установке возникает ошибка, тогда алгоритм начинает работать только по рабочей (функция АВР).
 
Для примера используется набор из 2-х установок, которые необходимо чередовать по времени. Если на какой-то установке возникает ошибка, тогда алгоритм начинает работать только по рабочей (функция АВР).
Строка 317: Строка 374:
  
  
 +
<!--T:35-->
 
function main (userId)
 
function main (userId)
 
   --[[
 
   --[[
Строка 329: Строка 387:
  
  
 +
<!--T:36-->
 
if (not acError1) and (not acError2) then  
 
if (not acError1) and (not acError2) then  
 
     -- работаем по циркуляции  
 
     -- работаем по циркуляции  
Строка 348: Строка 407:
  
  
 +
<!--T:37-->
 
end -- main  
 
end -- main  
 
</syntaxhighlight>
 
</syntaxhighlight>
  
  
 +
<!--T:38-->
 
Второй скрипт смотрит какой кондиционер сейчас активен, и выполняет необходимые действия. В скрипте это просто отладочная печать, но здесь могут быть команды управления инфракрасным передатчиком для выдачи нужной команды, запись в журнал сообщений и переключении и т.п.
 
Второй скрипт смотрит какой кондиционер сейчас активен, и выполняет необходимые действия. В скрипте это просто отладочная печать, но здесь могут быть команды управления инфракрасным передатчиком для выдачи нужной команды, запись в журнал сообщений и переключении и т.п.
  
  
 +
<!--T:39-->
 
<syntaxhighlight lang = "lua">
 
<syntaxhighlight lang = "lua">
 
function main (userId)
 
function main (userId)
Строка 363: Строка 425:
  
  
 +
<!--T:40-->
 
if (pointer==0) then  
 
if (pointer==0) then  
 
       DEBUG("все выключаем");
 
       DEBUG("все выключаем");
Строка 377: Строка 440:
 
Также здесь нужен будет скрипт, который будет выставлять флаги ошибок работы по неким условиям, читаю состояние автоматов защиты, регистры ошибок по интерфейсу и т.п.
 
Также здесь нужен будет скрипт, который будет выставлять флаги ошибок работы по неким условиям, читаю состояние автоматов защиты, регистры ошибок по интерфейсу и т.п.
  
=== 3-х точечное управление клапанами, сервоприводами и др. ===
+
=== 3-х точечное управление клапанами, сервоприводами и др. === <!--T:41-->
  
  
 +
<!--T:42-->
 
3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию.  
 
3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию.  
 
<p>Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем.  
 
<p>Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем.  
Как упоминалось выше, относительно сложные и разветвленные алгоритмы в WebHMI лучше разбить на функционально законченные и простые программы, взаимодействие и синхронизацию между которыми можно делать используя внутренние регистры.</p>
+
</p>
 
<br>
 
<br>
 
Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками.
 
Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками.
Программа разбита на 6 частей:
+
<syntaxhighlight lang = "lua">
 +
R = GetReg  -- redefine function names
 +
WR = WriteReg
  
[[Файл:3-point control.png|800px|left]]
 
<br clear = all>
 
::'''v3 OpenClose Valve Manual''' - скрипт управляет приводом в ручном режиме. Запускается по изменению номера нажатой кнопки с дешборда.
 
::'''v4 Auto Valve Control''' - основной скрипт управления в авт. режиме. Он автоматически выполняет первую инициализацию, и при несовпадении заданной координаты от текущей включает привод в нужном направлении, по достижении позиции останавливает. Также при совпадении заданной позиции как крайней (0,100), скрипт продолжает держать команду, пока не произойдет наезд на концевой выключатель, таким образом выполняя периодическую синхронизацию расчетной позиции с реальной.
 
::'''v5 LatchStart_Time''' - скрипт "захвата" начальной позиции движения, выполняется по изменению флага inMotionFlag, который устанавливают предыдущие программы v3 или v4.
 
::'''v6 CalcNewPosition''' - выполняется 1 раз в сек. (по изменению системного времени), скрипт работает, пока есть флаг inMotionFlag и пересчитывает текущее время, пройденное с начала движения в текущую позицию (которая в свою очередь используется программой v4).
 
::'''v7 Limit sw reaction''' - по достижении конечных выключателей снимает команды, а также снимает запрос от кнопок ручного управления.
 
::'''v8 Drop Cur Cmd on Manual mode''' - при переходе в ручной режим выключает текущую команду.
 
 
 
В начале программ обозначен порядок требуемый порядок выполнения (v3..v8), поскольку необходимый порядок выполнения программ может измениться нежелательным образом, например при сортировке программ и неточном "перетаскивании" программ в списке. Таким образом префикс напоминает о нужном порядке, также он может отражать функциональную принадлежность скрмпта - "v" Valves, "t"  - temperature control и т.д. чтобы удобнее ориентироваться в больших списках и ссылаться на него.
 
<p>Исходные тексты скриптов:</p>
 
'''v3'''
 
<syntaxhighlight lang = lua>
 
------- РУЧНОЕ УПРАВЛЕНИЕ С КНОПОК НА ЭКРАНЕ OpenClose Valve Manual -------------------
 
 
function main (userId)
 
function main (userId)
 
 
  local button_value = GetReg("valveMan_code"); -- номер от кнопок нажатия ОТКР / ЗАКР.
 
  local manual_mode = (GetReg("auto_mode") == 0) ; -- АВТ. РЕЖИМ ВКЛЮЧЕН (ds1176@WH Global)
 
  
 +
now = os.time() -- global
  
if manual_mode then
+
local manOpenCmd, manCloseCmd = R("manOpenCmd"), R("manCloseCmd") -- separate flags for manual control
 +
local autoOpenCmd, autoCloseCmd = R("autoOpenCmd"), R("autoCloseCmd") --  system's output to valve
 +
local openSw, closeSw = (R("valveOpenSw") == 1) , (R("valveCloseSw") == 1)
 +
local pullUpFlag, pullDownFlag = (R("pullUpFlag") == 1 ), (R("pullDownFlag") == 1 )  ;
  
 +
local curPosition = Round(R("valveCurPos") + MotionTimer(autoOpenCmd, autoCloseCmd, "Tmr5")) ; DEBUG_("curPosition calc. as "..curPosition)
  
if (button_value == 10) then
+
                -- filter and check limit sw
        WriteReg("openCmd", 1); -- Команда "Открытие" (DS1007@WH Valve control)
+
if (curPosition >= 100) or openSw then  
        WriteReg("closeCmd", 0);
+
     curPosition = 100
    elseif (button_value == 5) then  
+
         if openSw then  
          WriteReg("closeCmd", 1); -- Команда "Закрытие" (DS1008@WH Valve control)
+
            WR("pullUpFlag", 1 )
          WriteReg("openCmd", 0);
+
            pullUpFlag = true
     else
+
         end  
         -- на недопустимое значение снимаем команды
+
end  
                                    DEBUG("Read button value as "..tostring(button_value));
+
        WriteReg("openCmd", 0);
+
        WriteReg("closeCmd", 0);
+
        WriteReg("inMotion_flag",0); --
+
    end --if  
+
   
+
    -- Фиксация начальной позиции, времени начала движения для скрпита обсчета текущей позиции
+
    if (button_value == 10) or (button_value == 5) then  
+
        WriteReg("startPosition", GetReg("curPosition")); -- Начальная позиция движения  (DS1020@WebHMI 3point control)
+
        WriteReg("startPosTime", os.time()); -- Метка времени начала движения  (DS1012@WebHMI 3point control)
+
         WriteReg("inMotion_flag",1);
+
    end
+
end -- manual_mode
+
  
 +
if ((curPosition < 0) or closeSw) then
 +
    curPosition = 0
 +
        if closeSw then
 +
            WR("pullDownFlag", 1 )
 +
            pullDownFlag = true
 +
        end
 +
end
 +
                            -- pulling
 +
if (curPosition == 100) and not (openSw) and not pullUpFlag then
 +
    curPosition = 99  ;                                            DEBUG_(" pulling up ")
 +
end
 +
if (curPosition == 0 ) and not (closeSw) and not pullDownFlag then
 +
    curPosition = 1  ;                                            DEBUG_(" pulling down ")
 +
end
  
end -- main -----------------------
+
WR("valveCurPos", curPosition) -- store new position
</syntaxhighlight>
+
  
 +
                                    -- AUTO MODE ----
 +
local valveSp = R("valveSp")  ;                                    DEBUG_("valveSp  "..valveSp)                                   
 +
local positionError = (curPosition - valveSp) 
  
'''v4'''
+
if (positionError ~= 0) and ( autoOpenCmd ~= 1) and (autoCloseCmd ~= 1) then  
<syntaxhighlight lang = lua>
+
-- reset pulling flags on new cycle of motion
-- АВТ. УПРАВЛЕНИЕ ДВИЖЕНИЕМ КЛАПАНА Auto Valve Control ----
+
    WR("pullDownFlag" , 0 ); WR("pullUpFlag", 0)
function main (userId)
+
end  
   
+
  local now = os.time(); -- текущее время
+
  local timeStmp = GetReg("endPosTime"); -- Метка времени окончания движения  (D1001@WebHMI Kitothemr)
+
  local auto_mode = (GetReg("auto_mode")==1);
+
 
+
if auto_mode then
+
  -- проверка на самое первое включение, когда метка времения = 0
+
  if (timeStmp == 0) and (not home_mode) then
+
      WriteReg("homingBit",1);
+
                                DEBUG("timeStmp = 0, homing needed! ");
+
  end  
+
 
+
  local home_mode = (GetReg("homingBit") == 1); -- Бит репозиционирования  (D1006@WebHMI Kitothemr)
+
  local full_close = GetReg("LLsw"); -- Датчик полного закрытия (D1010@WebHMI Kitothemr)
+
  
  
if home_mode then
+
if (R("distribAutoMode") == 1) then  
      -- едем вниз до упора чтобы определить позицию
+
              DEBUG("giving home_mode close cmd ! ");
+
      WriteReg("closeCmd", 1); -- Команда "Закрытие" (D1008@WebHMI Kitothemr)
+
     
+
          if (full_close == 1) then  
+
                  DEBUG("full close ! ");
+
               
+
                WriteReg("closeCmd",0);
+
                WriteReg("endPosTime", now); -- чтобы избежать повторного home_mode
+
                WriteReg("homingBit",0);
+
                WriteReg("curPosition", 0); -- Текущее положение клапана (D1005@WebHMI Kitothemr)
+
          end
+
      return 0; -- выходим пока не закончим выход в 0
+
  end -- home mode
+
 
+
  -- ОСНОВНАЯ ЧАСТЬ ----
+
                          DEBUG("-------- auto valve control ");
+
 
+
  local openingStatus = (GetReg("openCmd") == 1); -- Команда "Открытие" (D1007@WebHMI Kitothemr)
+
  local closingStatus = (GetReg("closeCmd") == 1); -- Команда "Закрытие" (D1008@WebHMI Kitothemr)
+
  local inMotionFlag = openingStatus or closingStatus;
+
 
+
  local target_dir= 0; -- знак движения
+
  local posSV = GetReg("posSetpoint"); -- Уставка положения клапана (D1000@WebHMI Kitothemr)
+
  local posPV = GetReg("curPosition"); -- текущее положение
+
 
+
    -- определить начальные время и позицию
+
  local startTime = GetReg("startPosTime");
+
  local startPos = GetReg("startPosition");
+
  local pathdone = 0; -- обсчета пройденного пути
+
 
+
  local OpenSw = GetReg("HLsw"); -- Датчик полного открытия (D1009@WebHMI Kitothemr)
+
  local CloseSw = GetReg("LLsw"); -- Датчик полного закр.  (D1009@WebHMI Kitothemr)
+
 
+
                            DEBUG("posPV  "..tostring(posPV).." pos SV"..tostring(posSV));
+
      -- Уже едем
+
      if inMotionFlag then
+
                            DEBUG("Мы уже в движении ");
+
        -- доехали вниз ?
+
          if (closingStatus) and (posPV <= posSV) then
+
              -- если задан 0 как позицию, то едем до нижнего конечника, чтобы синхронизировать позицию с реальным положением
+
              if (posPV == 0) and (not CloseSw) then
+
                  WriteReg("closeCmd", 1);
+
                  return 0; -- выходим, команду сбросит скрипт Limit Sw Reaction
+
              end
+
                  WriteReg("closeCmd", 0);
+
                  WriteReg("inMotion_flag",0);
+
            end
+
        -- доехали вверх? 
+
          if (openingStatus) and (posPV >= posSV) then
+
              -- если задан верхний предел, то едем до конечника чтобы синхронизировать позицию с реальным положением
+
                  if (posPV == 100) and (not OpenSw) then
+
                      WriteReg("openCmd", 1);
+
                      return 0; -- выходим, команду сбросит скрипт Limit Sw Reaction
+
                  end
+
              WriteReg("openCmd", 0);
+
              WriteReg("inMotion_flag",0);
+
          end
+
      -- позиция не доехали ?
+
      else
+
          if (posSV ~= posPV) then
+
              -- уставка поменялась ?
+
            target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- определить знак
+
                                            DEBUG_("target_dir =  "..tostring(target_dir));
+
              if (target_dir > 0) then
+
                  WriteReg("openCmd" , 1);
+
                                            DEBUG_("will open...");
+
              else
+
                  WriteReg("closeCmd" , 1);
+
                                            DEBUG_("will close...");
+
              end
+
            WriteReg("inMotion_flag", 1); -- уст. флаг для скриптов работающих по началу движения                                   
+
          end
+
  end -- auto mode motion control
+
 
+
 
+
end -- auto_mode
+
end -- main
+
 
+
 
+
</syntaxhighlight>
+
 
+
 
+
'''v5'''
+
<syntaxhighlight lang = lua>
+
-- Захват позиции LatchStart_Time ---
+
function main (userId)
+
 
      
 
      
                DEBUG("Entered  latch start time and position");
+
    WR("manOpenCmd", 0 ) -- clear manual cmds
  -- Add your code here
+
    WR("manCloseCmd", 0)    
  local flag = (GetReg("inMotion_flag") == 1) ; -- Флаг "в движении" (DS1016@WebHMI 3point control)
+
                DEBUG("motion flag = "..tostring(flag));
+
 
+
 
+
  if flag then
+
                  DEBUG("now flag = 1");
+
      WriteReg("startPosition", GetReg("curPosition")); -- Начальная позиция движения  (DS1020@WebHMI 3point control)
+
      WriteReg("startPosTime", os.time()); -- Метка времени начала движения  (DS1012@WebHMI 3point control)
+
               
+
  end
+
 
+
 
+
end
+
</syntaxhighlight>
+
'''v6'''
+
<syntaxhighlight lang=lua>
+
--- Расчет новой позиции CalcNewPosition
+
-- константы
+
FULLPATH = 127 ; -- время полного открытия в сек.
+
K = 100 / FULLPATH ; -- коэф. пересчета % открытия в временной интервал
+
 
+
 
+
function main (userId)
+
 
      
 
      
                            DEBUG("Entered #5 script, calc. cur position");
+
    if (positionError == 0)  then
  local now = os.time(); -- текущее время
+
        WR("autoOpenCmd", 0 )
   local open_sts = (GetReg("openCmd") == 1); -- Команда "Открытие" (D1007@WebHMI Kitothemr)
+
        WR("autoCloseCmd", 0) -- clear cmds no error
  local close_sts = (GetReg("closeCmd") == 1); -- Команда "Закрытие" (D1008@WebHMI Kitothemr)
+
    elseif (positionError > 0)   then
  local inMotionFlag = (open_sts or close_sts) ; -- Команда "Закрытие" (D1008@WebHMI Kitothemr)
+
                                    DEBUG_("GO DOWN because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
                            DEBUG("in motion flag = "..tostring(inMotionFlag));
+
        WR("autoOpenCmd", 0 )
 
+
        WR("autoCloseCmd", 1) -- go down
  local cur_dir = 0; -- знак направления движения
+
       
  local posPV = GetReg("curPosition"); -- текущая позиция
+
    elseif (positionError < 0) then
 +
                                    DEBUG_("GO UP because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
 +
        WR("autoOpenCmd", 1 )
 +
        WR("autoCloseCmd", 0) -- go up
 +
    else
 +
                                    DEBUG_("Undefined if !!! in valve control ")
 +
    end
 +
                                  --- MANUAL MODE -----
 +
else               
 +
    --  just redirect manual cmd
 +
    WR("autoOpenCmd", manOpenCmd )
 +
    WR("autoCloseCmd", manCloseCmd)
 +
 
 +
end
  
 
-- определить начальные время и позицию
 
  local startTime = GetReg("startPosTime");
 
  local startPos = GetReg("startPosition");
 
  local pathdone = 0; -- пройденный путь
 
 
 
  -- Уже едем
 
  if inMotionFlag then
 
      -- определяем направление движения
 
      if open_sts then
 
            cur_dir = 1;
 
      else
 
            cur_dir = -1;
 
      end
 
  pathdone = GetPathDone(startTime, now); -- вычисляем пройеднный путь
 
  varPos = startPos + cur_dir*pathdone; -- теперь текущую координату
 
     
 
      -- проверка выхода текущей позиции за допустимые границы
 
      if (varPos < 0) or (varPos > 100) then
 
                            DEBUG("new varPos calculaed outside limits, cur value "..tostring(varPos));
 
        if (varPos < 0) then varPos = 0 end
 
        if varPos > 100 then varPos = 100 end
 
      end
 
     
 
                      DEBUG("startPos , pathdone = , new varPos  "..tostring(startPos).." "..tostring(pathdone).." "..tostring(varPos));
 
      WriteReg("curPosition", varPos); -- пишем текущую позицию
 
  end -- auto mode motion control
 
 
end -- main  
 
end -- main  
  
 +
function MotionTimer(openCmd, downCmd, tmrAlias)
 +
--[[   
 +
remembers last call time
 +
return motion quant passed since last call if there were motion
 +
then quant added to current position
 +
--]]
  
------------- Функция вычисления остатка движения ---------------   
+
local FULL_PATH_TIME = 180 -- sec.
function GetPathDone(startTime, curTime)
+
local KOEF = 100 / FULL_PATH_TIME
     -- смотрим на тек. время и высчитываем %
+
local quant = 0
     local curPos = (curTime - startTime) * K;
+
                                               
     local remainder = curPos - math.floor(curPos);
+
    local motion = { flag = ((openCmd == 1) or (downCmd == 1)), dir = (openCmd == 1), lastTimeStamp = R(tmrAlias)}
    if (remainder >= 0.5) then  
+
     DEBUG_(" motion.flag = "..tostring(motion.flag).." dir  "..tostring(motion.dir)..os.date("%c", lastTimeStamp))
        -- округл. вниз
+
     local outAlias = tmrAlias.."_out"
        curPos = math.floor(curPos) + 1;
+
     local curTmrState = (R(outAlias) == 1) ; DEBUG_("cur "..tmrAlias.." State = "..tostring(curTmrState))
 +
                                               
 +
    if motion.flag then  
 +
      quant = (now - R(tmrAlias)) * KOEF
 +
          if not motion.dir then
 +
              quant = quant * (-1)  
 +
          end
 +
                                                    DEBUG_("quant calculated as = "..quant)
 +
      WR(outAlias, 1) -- in motion now
 +
                                                   
 
     else  
 
     else  
        curPos = math.floor(curPos) ;
+
                    DEBUG_("no motion lasts = ")
    end
+
      WR(outAlias, 0)  
     return curPos;
+
      quant = 0
end  
+
     end -- if
</syntaxhighlight>
+
'''v7'''
+
<syntaxhighlight lang=lua>
+
-- Limit sw reaction------------
+
function main (userId)
+
 
      
 
      
  local now = os.time();
+
WR(tmrAlias, now)    -- store last call time
  local open_sts = (GetReg("openCmd") == 1); -- Команда "Открытие" (D1007@WebHMI Kitothemr)
+
return quant
  local close_sts = (GetReg("closeCmd") == 1); -- Команда "Закрытие" (D1008@WebHMI Kitothemr)
+
 
+
  local full_close = (GetReg("LLsw") == 1); -- Датчик полного закрытия (D1010@WebHMI Kitothemr)
+
  local full_open = (GetReg("HLsw") == 1); -- Датчик полного закрытия (D1010@WebHMI Kitothemr)
+
 
+
  if (full_open or full_close) then
+
    --  проверить наезд на конечники
+
          if full_open then
+
              -- посчитать новую позицию
+
              WriteReg("curPosition", 100);
+
              WriteReg("openCmd", 0);
+
              WriteReg("valveMan_code", 0); -- для ручнго режима
+
          end
+
         
+
          if full_close then
+
              WriteReg("curPosition", 0);
+
              WriteReg("closeCmd", 0);
+
              WriteReg("valveMan_code", 0); -- для ручного режима
+
          end
+
         
+
      WriteReg("inMotion_flag", 0);
+
      WriteReg("endPosTime", now); -- Метка времени окончания движения  (DS1001@WH Valve control)
+
      
+
  end
+
 
+
  -- индикация соcтояния привода
+
  if (not open_sts) and (not close_sts) then
+
      WriteReg("valveStatus", 0); -- Стоим
+
  elseif open_sts then
+
      WriteReg("valveStatus", 1); -- Открытие
+
  else
+
      WriteReg("valveStatus", 2); -- Закрытие
+
  end
+
 
+
end -- main --------------------------------
+
</syntaxhighlight>
+
  
 +
end -- function
  
'''v8'''
+
-- округлление
<syntaxhighlight lang=lua>
+
function Round(var)
------ v8 Drop Cur Cmd on Manual mode ----  
+
function main (userId)  
+
    local auto_on = (GetReg("auto_mode")==1); -- работаем по изменению регистра режима
+
 
      
 
      
     if not auto_on then
+
     local integer, fraction = math.modf(var)
                                    DEBUG_("Dropped cmds in manua mode ! ");
+
        WriteReg("openCmd", 0);
+
        WriteReg("closeCmd", 0);
+
        WriteReg("inMotion_flag",0);
+
    end
+
    prev_mode = auto_on;
+
end -- main 
+
 
+
 
+
</syntaxhighlight>
+
 
+
=== Битовые операции ===
+
 
+
==== Удобные битовые операции ====
+
Можно определить несколько удобных битовых операций, помня о том что в lua биты нумеруются с 1. Ссылка взята [http://lua-users.org/wiki/BitwiseOperators отсюда], там же есть и другие полезные функции.
+
<syntaxhighlight lang = lua>
+
 
+
 
+
function bit(n)
+
  return 2 ^ (n - 1)  -- возвращает число - вес разряда номер n
+
end
+
 
+
 
+
function hasbit(x, p)
+
  return x % (p + p) >= p  -- возвращает состояние бита p как true/false; if hasbit(value, bit(2)) then ...
+
end
+
 
+
 
+
function setbit(x, p)
+
  return hasbit(x, p) and x or x + p -- установить бит № p в х Пример использования:  х = setbit(х, bit(p))
+
end
+
 
+
 
+
function clearbit(x, p)
+
  return hasbit(x, p) and x - p or x -- тоже только снять бит
+
end
+
</syntaxhighlight>
+
 
+
==== Преобразование битовой таблички в число ====
+
 
+
Данная функция может быть полезна для нестандартных преобразований, когда число хранится в специфичном формате и его необходимо обработать по частям, используя заданную позицию 1-го бита и длину поля значения.
+
 
+
 
+
<syntaxhighlight lang = "lua">
+
 
+
 
+
function getNumberFromTab(tab,start,length) -- получить число из таблички
+
    local result_str = "";
+
 
      
 
      
     for i=start,(start+length-1) do
+
     if fraction >= 0.5 then
            result_str = result_str..tostring(tab[i]);
+
        integer = integer + 1
 
     end  
 
     end  
    return tonumber(result_str,2);
 
end -- getNumberFromTab
 
 
 
</syntaxhighlight>
 
 
==== Преобразование числа в битовую табличку ====
 
 
 
В lua (в версии, используемой в WebHMI) нет встроенной операции получения строкового представления двоичного числа.
 
Однако эта функция очень полезна в пользовательсиких протоколах и др. задачах, где требуется перераспределить или каким то образом обработать отдельные биты числа.
 
Текст варианта такой функции:
 
 
 
<syntaxhighlight lang = "lua">
 
 
 
function getBits(input_num,length)        -- работает с заданной длиной
 
 
      
 
      
local tab = {};                          -- пустая табличка для ответа
+
    return math.floor(integer)
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
+
</syntaxhighlight>
+
 
+
 
+
Далее можно переставить нужные биты местами и т.п. и получить нужное число. Ниже приведен пример перестановки битов 31 и 23 битов (при нумерации битов с 0) в ответе счетчика расходомера ВЛР 2301/2304 производства Асвега-У
+
 
+
 
+
<syntaxhighlight lang = "lua">
+
 
+
 
+
 
+
function main (userId)
+
 
      
 
      
  local input_num = GetReg(3);              -- прочитать регистр с числом
+
end
  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));     
+
  
 +
thisScriptID = 14
 +
function DEBUG_(str)
 +
                        -- ERROR("entered DEBUG_ in 35 script")
 +
local i = 0
 +
local tmp = ""
 +
local debug_id = R("debug_IDs")
 +
                        -- ERROR(debug_id)
 +
local capture_mask = "%s+(%d+)%s+"
  
end
+
while true do
</syntaxhighlight>
+
i,_, tmp = string.find(debug_id, capture_mask, i+1)
  
=== Обработка чисел с плавающей точкой двойной точности ===
+
    if i == nil  then
 
+
                        --     ERROR("not found !")
 
+
        break
Некоторые устройства могут хранить данные в формате с повышенной точностью double float. В WebHMI текущей версии поддерживаются только 32 битные регистры, поэтому чтобы обработать 64 битное число необходимо будет использовать скрипт, которые "сцепит" два регистра в исходное число, а затем преобразует и запишет в обратно в float. Регистры должны быть типа double uint.
+
    end  -- не найдено
Для "сцепки" чисел можно использовать описанную выше функцию getBits чтобы получить 2 таблицы, а затем дополнить первую таблицу битами из второй используя table.insert. В этом примере в качестве проверочного числа для простоты используется константа.
+
    if (tmp ~= "0") then
 
+
        -- найдено проверить на совпадение
 
+
        if (tmp == tostring(thisScriptID)) then  
<syntaxhighlight lang = "lua">
+
                            DEBUG(str)
--------------------------------------
+
                            -- ERROR("found own id , tmp = "..tmp)
local test_var = 0xC1D312D000000000;
+
            return 0
local NaN = tonumber("11111111111111111111111111111111",2);
+
        end  
--------------------------------------
+
 
+
 
+
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  
 
end  
 
end  
 
+
   
 
+
end -- DEBUG_
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
+
 
+
 
+
 
</syntaxhighlight>
 
</syntaxhighlight>
  
=== Формирование общего регистра аварий по разным условиям ===
+
</translate>
 
+
 
+
В WebHMI есть механизм аварий. Т.е. по состоянию бита в регистре можно автоматически генерировать записи в журнале о моменте возникновения, снятия аварии, квитировании ее оператором.
+
 
+
Однако в некоторых случаях использовать данный механизм непосредственно, как есть, может не совсем быть удобно. Например - аварийным считается определенный бит или их комбинация, разные коды ошибок (и их надо отличать друг от друга), значение больше или меньше порога и т.д. Если регистр имеет уже настроенные состояния для аварийной ситуации, используемое для отображения, то его нужно просто продублировать, выставив бит в общем аварийном регистре. Также может потребоваться введение задержки на срабатывание аварии, так как значение может иметь "дребезг" который надо фильтровать, прежде чем генерировать аварию в журнале.
+
Кроме этого удобнее, когда все аварии описаны в одном месте, а не разбросаны по сотням регистров. Т.е. рациональнее использовать один или несколько несколько 32-битных регистров для описания всех возможных аварий, и скрипт в котором эти аварии будут записываться в этот регистр по разным условиям. Ниже приведен пример такого скрипта:
+
 
+
 
+
Допустим, есть некая функциональная единица, например приточная вентиляционная установка, в которой есть некий общий признак аварии, есть несколько регистров с кодами текущих ошибок от частотных преобразователей и ситуация, когда один из регистров (влажность, СО2 и др.) вышел за допустимый диапазон, что является также аварийной ситуацией.
+
[[Файл:Alerts.png | 800px | left]]
+
<br clear = all>
+
 
+
Тогда скрипт, который обработает все эти ситуации и сформирует нужные флаги на своих местах может выглядеть так:
+
<syntaxhighlight lang = lua>
+
allAlerts = {          -- регистр    бит - код ошибки    тип операции
+
              {srcAlias = "pv1Status", bits = {[1] = 1}, "bit9"},
+
              {srcAlias = "vfdStatus_1", bits = {[2] =  10, [3] = 20, [4] = 30, [5] = 40}, "=="},
+
              {srcAlias = "vfdStatus_2",  bits = {[6] =  10, [7] = 20, [8] = 30, [9] = 40}, "=="},
+
              {srcAlias = "pv1ZoneHumidity", bits = {[10] = 90}, ">="}
+
}
+
 
+
intAlertReg = "alertsReg" -- ОБЩИЙ  РЕГИСТР АВАРИЙ
+
 
+
function main (userId)
+
 
+
local alertInput, alertOut = 0,0 -- временные переменные для чтения аварий и формирования результата
+
local digit = 0 -- номер бита в исходном регистре, указывающий на аварию..
+
--[[
+
Если назвать единообразно все  параметры,
+
из которых надо формировать аварии и сложить их в одну структуру,
+
тогда можно использовать только один цикл for, например так:
+
--]]
+
  for i,v in pairs(allAlerts) do
+
        alertInput = GetReg(v.srcAlias) -- читаем регистр
+
 
+
 
+
for j,k in pairs(v.bits) do -- выполнится по числу элементов в под-табличке bits
+
                          -- проверяем тип операции - точное совпадение или другое условие
+
                if (v[3] == "==") then
+
 
+
 
+
-- проверяем регистр на один из кодов ошибок
+
                  if (alertInput == k) then
+
                      alertOut = setbit(alertOut, j)
+
                  else
+
                      alertOut = clearbit(alertOut, j)
+
                  end
+
 
+
 
+
-- проверяем определенный бит
+
                elseif (string.find(v[3], "bit%d+") ~= nil) then              -- %d+    паттерн любая последовательность цифр
+
                        _,_,digit = tonumber(string.find(v[3], "bit(%d+)"))  -- выделить из паттерна цифру и преобразовать в число
+
                      if hasbit(alertInput, bit(digit)) then                -- если заданный бит установлен во входном регистре
+
                          alertOut = setbit(alertOut, j)
+
                      else
+
                          alertOut = clearbit(alertOut, j)
+
                      end
+
                else
+
                    --- другие операции
+
                end 
+
        end -- for bits inside
+
 
+
 
+
end -- for allAlerts
+
                    -- теперь просто записать то что получилось в общий регистр аварий
+
WriteReg(intAlertReg,  alertOut)
+
 
+
 
+
end -- main
+
 
+
 
+
----------------- ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ ----------------------------------
+
 
+
 
+
function bit(n)
+
  return 2 ^ (n - 1)  -- возвращает число - вес разряда номер n
+
end
+
 
+
 
+
function hasbit(x, p)
+
  return x % (p + p) >= p  -- возвращает состояние бита p как true/false; if hasbit(value, bit(2)) then ...
+
end
+
 
+
 
+
function setbit(x, p)
+
  return hasbit(x, p) and x or x + p -- установить бит № p в х Пример использования:  х = setbit(х, bit(p))
+
end
+
 
+
 
+
function clearbit(x, p)
+
  return hasbit(x, p) and x - p or x -- тоже только снять бит
+
end
+
 
+
 
+
</syntaxhighlight>
+
 
+
 
+
Если различных операций над значениями много: не только сравнение, но и например проверка конкретного бита и т.п. то можно детали реализации спрятать в функцию, которая получит на вход табличку с типом операции (установлен ли конкретный бит, код ошибки, параметр в границах и т.п.) и входным значением и поставит или сбросит нужный бит.
+
 
+
 
+
<syntaxhighlight lang = lua>
+
 
+
for i,v in pairs(allAlerts) do
+
    alertInput = GetReg(v.srcAlias) -- читаем регистр
+
    alertOut = MyGetAlerts(alertInput, alertOut, v) -- вызываем функцию, которая посмотрит что надо сделать по полю v[3], т.е. allAlerts[3]
+
end -- for
+
</syntaxhighlight>
+

Текущая версия на 10:34, 5 октября 2018

Другие языки:
English • ‎русский

Полезные программы

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

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

Rising edge.png

Реализация универсального таймера (TON TOFF)

Таймер сравнивает свое текущее состояние с состоянием на входе:

  • текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0.
  • текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0.

Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера.

function TimerOnOff(bool_input, onDelay, offDelay, tmrAlias)
--                    bool     seconds   seconds   string 
    local outAlias = tmrAlias.."_out"
    local curTmrState = (GetReg(outAlias) == 1) ; DEBUG("curTmr  State "..tostring(curTmrState))
                                                  DEBUG("bool_input = "..tostring(bool_input))
                                                
    if (bool_input == curTmrState) then 
        WriteReg(tmrAlias, now) ;                 DEBUG("timer input match state ") 
        return curTmrState  -- as bool 
    elseif bool_input then 
                                                    DEBUG("countdown for onDelay = "..(onDelay - (now - GetReg(tmrAlias))))
         if now - GetReg(tmrAlias) > onDelay then                                              
                WriteReg(outAlias, 1)
                                                    DEBUG("detected ON state after delay ")
                return true 
          end 
    else 
                                                    DEBUG("countdown for offDelay = "..(offDelay - (now - GetReg(tmrAlias))))
        if (now - GetReg(tmrAlias) > offDelay) then                                              
                WriteReg(outAlias, 0)
                                                    DEBUG("detected OFF state after delay ")
                return false  
        end     
     end  -- 
    return  curTmrState  -- output rerurned as bool for simple usage in IFs

end -- function

Пример использования:

Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if.


                                               DEBUG(" hotWater timer call ")
local DI2 = (GetReg(39) == 1) -- hotwater low pressure   
local DI3 = (GetReg(40) == 1) -- hotwater high pressure   
local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off .   

-- or like this 
if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then 
   -- do something 
end -- simple timer in one row !

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:


G_LIMIT = 100 -- ограничение выхода регулятора
--
function main (userId)
  -- локальные переменные 
  local now = os.time()
  local nexTime = GetReg("nextPidTime")
  local CYCLE_TIME = GetReg("pidCycleTime")
  
  -- параметры регулятора 
  local Kp = GetReg("Kp") -- Пропорц. коэф. (DS1400@WH Valve control)
  local Ti = GetReg("Ki") -- Интегральный коэф. (DS1404@WH Valve control)
  local Td = GetReg("Kd") -- Дифф. коэф.  (DS1408@WH Valve control)
  local Err, dErr, iSum_Limit = 0, 0, 0 
  local Int_sum = GetReg("pidIntegral") -- интегральный накопитель 
  local intPart = 0 -- инт. часть формулы 
  local G = GetReg("pidOut") -- выход регулятора
  
  -- процесс 
  local PV = GetReg(1436) -- Мощность (PWR0@Scylar 8 INT)
  local Sp = GetReg("targetPowerSp") 
  
                                    DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now))
    -- условие работы             
  local auto = (GetReg("auto_mode") == 1) -- АВТ. РЕЖИМ ВКЛЮЧЕН (ds1176@WH Global)
  local heatDemand = (GetReg("heatDemand") == 1)
  
if auto then 
  
  if heatDemand then 
                                    -- РЕГУЛЯТОР
          if (now >= nexTime) then 
                                                          DEBUG_("PID compute cycle")
                WriteReg("nextPidTime", now + CYCLE_TIME)
                Err =  Sp - PV -- вычисляем ошибку 
                                                    DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err)
                dErr = Err - GetReg("pidPrevError") -- вычисляем производную ошибки 
                                                    DEBUG_("dErr = "..dErr)
                    -- проверяем интегральное насыщение 
                iSum_Limit = (G_LIMIT * Ti / Kp) / 5
                                                            DEBUG_("iSum_Limit = "..iSum_Limit)
                
                            -- ПИД - регулятор
                    --проверка на 0 инт. составляющей 
                                                            DEBUG_("prev Int_sum = "..Int_sum)
                                                            if (intPart <= iSum_Limit) and (intPart >= 0.0) then
                        Int_sum = Int_sum + Err -- накапливаем интеграл ошибки 
                                                   DEBUG_("added error to Int_sum ")
                                                            
                    elseif Int_sum < 0 then
                        Int_sum = 0
                    else
                        Int_sum = iSum_Limit -- ограничиваем интегральную составляющую
                    end
                    if (Ti == 0) then 
                        intPart = 0 
                    else 
                        intPart = (1/Ti)*Int_sum
                    end 
                    
                    
                                                            DEBUG_("new Int_part = "..intPart)
                G = Kp * (Err + intPart + Td*dErr)
                                                            DEBUG_("Calculated G as "..G)
                G = Round(G)
                                                            DEBUG_("Rounded G as "..G)
                    -- проверка выхода за диапазон 
                if G < 0 then
                    G = 0  
                end
                if G > G_LIMIT then
                    G = G_LIMIT
                end
                
                WriteReg("pidPrevError", Err) -- запомнить предыдущую ошибку для след. скана
                WriteReg("dErr", dErr) -- запомнить предыдущую ошибку для след. скана
                WriteReg("pidIntegral", intPart) -- запомнить интегральную составляющую
                WriteReg("pidOut", G) 
                WriteReg("posSPinput", G) -- дать уставку на клапан 
        end -- time stamp 
  else 
                DEBUG_("no heatDemand") -- вывести ПИД выход 
      G = 0 
  end -- heatDemand 
                -- DEBUG_("PID_out = "..G) -- вывести ПИД выход 
                WriteReg("pidOut", G) 
                WriteReg("posSPinput", G) -- дать уставку на клапан 
end -- if auto 

end -- main 

-- округлление 
function Round(var)
    
    local integer, fraction = math.modf(var)
    
        if fraction >= 0.5 then 
            integer = integer + 1
        end 
    
    return math.floor(integer)
end 

------ debug printing ------
thisScriptID = 45

function DEBUG_(str)
                --ERROR("entered DEBUG_ in"..thisScriptID.." script");
local i = 0;
local tmp = "";
local id = tostring(thisScriptID);
local debug_id = GetReg("debug_IDs");

local capture_mask = "%s+(%d+)%s+"

while true do 
i,_, tmp = string.find(debug_id,capture_mask,i+1)

    if (i == nil)  then 
        break -- не найдено 
    end  
    -- найдено
    if (tmp ~= "0") then 
        -- найдено проверить на совпадение 
        if (tmp == tostring(thisScriptID)) then 
             DEBUG(str)
             return 0
        end 
    end 
end 

end -- DEBUG_
------------------------------

Данный алгоритм является типовым для применения в ПЛК. Поскольку регулятор выполняется через равные интервалы времени, т.е. дифф. и инт. составляющие всегда вычисляются в одном масштабе времени, поэтому делить и умножать их на время для получения производной и интеграла необязательно, можно подбирать постоянные времени 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
  end 

-- считаем время 
 if run_state then 
     time_diff = (now - GetReg("P43StartTime")); -- посчитать разницу времени
     WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- увеличить счетчик моточасов
     WriteReg("P43StartTime", now); -- переписать начальную точку времени 
 end 
 run_state = run_status; 
end


Регистры хранения моточасов и метки времени нужно делать энергонезависимыми.

Алгоритм чередования по времени (циркуляция с функцией АВР)

Данный алгоритм используется в системах, где необходимо чередовать работу механизмов (насосы, вентиляторы, кондиционеры) по времени, либо по моточасам (наработке). Для примера используется набор из 2-х установок, которые необходимо чередовать по времени. Если на какой-то установке возникает ошибка, тогда алгоритм начинает работать только по рабочей (функция АВР). Пример настройки необходимых регистров приведены ниже:

Circ algorithm regs.png

Для простоты и наглядности лучше разбивать скрипты на функциональные модули, которые можно быстро проанализировать и расположить их выполнение в нужном порядке в списке программ. Первый скрипт смотрит на ошибки и если их нет, чередует работу кондиционеров по времени.

CIRCULATION_TIME = 30; -- на время отладки время = 30 сек. 


function main (userId)
  --[[
  если нет ошибок, то чередуем работу по времени циркуляции
  если есть ошибка на одном из кондиционеров, то он исключается из чередования 
  если ошибки на обоих, то стоим на месте 
  --]]
local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- Кондиционер 1 ошибка (DS101@webmi)
local switchTime = GetReg("switchTime"); -- Время следующего переключения (DS103@webmi)
local now = os.time();
local curActiveAC = GetReg("activeAC"); -- Активный кондиционер (DS100@webmi)


if (not acError1) and (not acError2) then 
    -- работаем по циркуляции 
    if (now >= switchTime) then 
        if (curActiveAC == 1) then 
            WriteReg("activeAC", 2);
        else 
            WriteReg("activeAC", 1);
        end
        WriteReg("switchTime", now + CIRCULATION_TIME);
    end 
elseif acError1 and (not acError2) then 
        WriteReg("activeAC", 2);
elseif acError2 and (not acError1) then 
        WriteReg("activeAC", 1);
else
        WriteReg("activeAC", 0);
end -- if no errors 


end -- main


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


function main (userId)
  --[[
  включает выбранный кондицинер, в зависимости от указателя 
  --]]
  local pointer = GetReg("activeAC"); -- Активный кондиционер (DS100@webmi)


if (pointer==0) then 
      DEBUG("все выключаем");
      return 0;
  elseif 
      (pointer==1) then 
          DEBUG("включаем 1-й кондиционер");
  else 
          DEBUG("включаем 2-й кондиционер");
      
  end -- if 
end

Также здесь нужен будет скрипт, который будет выставлять флаги ошибок работы по неким условиям, читаю состояние автоматов защиты, регистры ошибок по интерфейсу и т.п.

3-х точечное управление клапанами, сервоприводами и др.

3-х точечным называется способ управления позицией клапана, сервопривода, задвижки и т.п., когда для управления приводом используются 3 провода - "общий", "питание - ВВЕРХ", " питание - ВНИЗ". Такие приводы могут быть (а могут и нет) оснащены также концевыми датчиками положения. Иногда при отсутствии датчиков положения и низких требованиях к точности позиционирования может применяться алгоритм, когда привод уезжает вниз или вверх (либо по 1 датчику положения либо подачей одной команды на время превышающее время полного хода клапана), инициализирует координату, а потом отрабатывает заданную позицию.

Для определения промежуточных положений используется расчетное значение, определяемое из характеристики исполнительного механизма "время полного хода", которое можно определить и экспериментальным путем.


Ниже приведен вариант 3-х точечного управления для привода с 2-мя концевыми датчиками.

R = GetReg   -- redefine function names
WR = WriteReg

function main (userId)

now = os.time() -- global 

local manOpenCmd, manCloseCmd = R("manOpenCmd"), R("manCloseCmd") -- separate flags for manual control 
local autoOpenCmd, autoCloseCmd = R("autoOpenCmd"), R("autoCloseCmd") --  system's output to valve
local openSw, closeSw = (R("valveOpenSw") == 1) , (R("valveCloseSw") == 1)
local pullUpFlag, pullDownFlag = (R("pullUpFlag") == 1 ), (R("pullDownFlag") == 1 )  ; 

local curPosition = Round(R("valveCurPos") + MotionTimer(autoOpenCmd, autoCloseCmd, "Tmr5")) ; DEBUG_("curPosition calc. as "..curPosition)

                -- filter and check limit sw 
if (curPosition >= 100) or openSw then 
    curPosition = 100 
        if openSw then 
            WR("pullUpFlag", 1 )
            pullUpFlag = true 
        end 
end 

if ((curPosition < 0) or closeSw) then 
    curPosition = 0 
        if closeSw then 
            WR("pullDownFlag", 1 ) 
            pullDownFlag = true 
        end 
end 
                            -- pulling 
if (curPosition == 100) and not (openSw) and not pullUpFlag then 
    curPosition = 99  ;                                             DEBUG_(" pulling up ") 
end 
if (curPosition == 0 ) and not (closeSw) and not pullDownFlag then 
    curPosition = 1   ;                                             DEBUG_(" pulling down ") 
end 

WR("valveCurPos", curPosition) -- store new position

                                    -- AUTO MODE ---- 
local valveSp = R("valveSp")  ;                                     DEBUG_("valveSp  "..valveSp)                                     
local positionError = (curPosition - valveSp)   

if (positionError ~= 0) and ( autoOpenCmd ~= 1) and (autoCloseCmd ~= 1) then 
 -- reset pulling flags on new cycle of motion 
    WR("pullDownFlag" , 0 ); WR("pullUpFlag", 0)
end 


if (R("distribAutoMode") == 1) then 
    
    WR("manOpenCmd", 0 ) -- clear manual cmds
    WR("manCloseCmd", 0)     
    
    if (positionError == 0)  then 
        WR("autoOpenCmd", 0 )
        WR("autoCloseCmd", 0) -- clear cmds no error 
    elseif (positionError > 0)   then 
                                    DEBUG_("GO DOWN because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
        WR("autoOpenCmd", 0 )
        WR("autoCloseCmd", 1) -- go down 
        
    elseif (positionError < 0)  then 
                                    DEBUG_("GO UP because positionError = "..positionError) ; DEBUG_(" math.abs error <= 0.5 "..math.abs(positionError))
        WR("autoOpenCmd", 1 )
        WR("autoCloseCmd", 0) -- go up 
    else 
                                    DEBUG_("Undefined if !!! in valve control ")
    end 
                                  --- MANUAL MODE ----- 
else                
    --  just redirect manual cmd 
    WR("autoOpenCmd", manOpenCmd )
    WR("autoCloseCmd", manCloseCmd)
   
end 

end -- main 

function MotionTimer(openCmd, downCmd, tmrAlias)
--[[    
remembers last call time 
return motion quant passed since last call if there were motion 
then quant added to current position
--]]

local FULL_PATH_TIME = 180 -- sec. 
local KOEF = 100 / FULL_PATH_TIME
local quant = 0 
                                                 
    local motion = { flag = ((openCmd == 1) or (downCmd == 1)), dir = (openCmd == 1), lastTimeStamp = R(tmrAlias)}
    DEBUG_(" motion.flag = "..tostring(motion.flag).." dir  "..tostring(motion.dir)..os.date("%c", lastTimeStamp)) 
    local outAlias = tmrAlias.."_out"
    local curTmrState = (R(outAlias) == 1) ; DEBUG_("cur "..tmrAlias.." State =  "..tostring(curTmrState))
                                                
    if motion.flag then 
      quant = (now - R(tmrAlias)) * KOEF
          if not motion.dir then 
              quant = quant * (-1) 
          end 
                                                     DEBUG_("quant calculated as = "..quant)
      WR(outAlias, 1) -- in motion now 
                                                    
    else 
                    DEBUG_("no motion lasts = ")
      WR(outAlias, 0) 
      quant = 0 
    end  -- if 
    
WR(tmrAlias, now)     -- store last call time 
return quant

end -- function 

-- округлление 
function Round(var)
    
    local integer, fraction = math.modf(var)
    
    if fraction >= 0.5 then 
        integer = integer + 1
    end 
    
    return math.floor(integer)
    
end 

thisScriptID = 14 
function DEBUG_(str)
                        -- ERROR("entered DEBUG_ in 35 script")
local i = 0
local tmp = ""
local debug_id = R("debug_IDs")
                         -- ERROR(debug_id)
local capture_mask = "%s+(%d+)%s+"

while true do 
i,_, tmp = string.find(debug_id, capture_mask, i+1)

    if i == nil  then 
                        --     ERROR("not found !")
        break 
    end  -- не найдено 
    if (tmp ~= "0") then 
        -- найдено проверить на совпадение 
        if (tmp == tostring(thisScriptID)) then 
                             DEBUG(str) 
                            -- ERROR("found own id , tmp = "..tmp)
             return 0 
        end 
    end 
end 
    
end -- DEBUG_
------------------------------