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

Материал из WebHMI Wiki
Перейти к: навигация, поиск
На этой странице были произведены изменения, не отмеченные для перевода.

Другие языки:
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_
------------------------------