User Tools

Site Tools

This is an old revision of the document!

Useful programs

Bit functions

You can use Lua BitOp extension functions in your programs:

bit.tobit(x)          -- normalize number to the numeric range of
                      -- bit operations (all bit ops use this implicitly)
bit.tohex(x[,n])      -- convert x to hex with n digits (default 8)
bit.bnot(x)           -- bitwise not of x[,x2...])  -- bitwise and of x1, x2, ...
bit.bor(x1[,x2...])   -- bitwise or of x1, x2, ...
bit.bxor(x1[,x2...])  -- bitwise xor of x1, x2, ...
bit.lshift(x, n)      -- left-shift of x by n bits
bit.rshift(x, n)      -- logical right-shift of x by n bits
bit.arshift(x, n)     -- arithmetic right-shift of x by n bits
bit.rol(x, n)         -- left-rotate of x by n bits
bit.ror(x, n)         -- right-rotate of x by n bits
bit.bswap(x)          -- byte-swap of x (little-endian <-> big-endian)

To access idividual bits, you can use following handy functions:

function bw(n)
  return 2 ^ (n - 1)  -- returns weight of the bit in pos. n
function hasbit(x, b) 
  local p = bw(b)
  return x % (p + p) >= p  -- returns if b is true/false; if hasbit(value, b) then ...
function setbit(x, b) 
  return hasbit(x, b) and x or x + bw(b) -- sets bit b in х example:  х = setbit(х, b))
function clearbit(x, b)
  return hasbit(x, b) and x - bw(b) or x --  clears bit b in х 
function togglebit(x, b) -- toggles bit b in x 
    if hasbit(x, b) then 
        return clearbit(x, b)
        return setbit(x, b)
function outbit(condition, x, b) -- output bool condition as 0 /1 into a bit 
     if condition then     
         return hasbit(x, b) and x or x + bw(b) 
         return hasbit(x, b) and x - bw(b) or x 
function outBit(condition, alias, b) -- output bool condition as 0 /1 into a bit of a internal register 
     local new_value = outbit(condition, R(alias), b)
     return new_value
function setBit(alias, b) -- sets bit in the register using alias or id   
    local old_value = R(alias) 
    local new_value = setbit(old_value, b) 
    if (new_value ~= old_value) then 
        WriteReg(alias, new_value)  -- to prevent unnecessary writing
function clearBit(alias, b) -- uses alias or id and crears bit in it 
    local old_value = R(alias)
    local new_value = clearbit(old_value, b)
    if (new_value ~= old_value) then 
        WriteReg(alias, new_value)
function toggleBit(alias, b) -- toggles bit b in reg by alias or id 
     local cur_value = R(alias)
    if hasbit(cur_value, b) then 
        clearBit(alias, b)
        setBit(alias, b)
    return true 
-- return bit from the position as a  1 / 0 
function getBit(alias, b)
    if hasbit(R(alias), b) then 
        return 1 
        return 0 

To enhance readablity of complex program you may wrap actions made with the register of a bit type to their handy analogs. Then in a complex program you can quickly determine where bit varialbe is changed, checked etc.

-- this function to make shorter ifs operator for bit registes
function TRUE(reg)
    return (R(reg)== 1) 
function NOT(reg)
    return (R(reg) == 0) 
function SET(reg)
    W(reg, 1)   
function RESET(reg)
    W(reg, 0)   
function OUT(condition, reg)
-- shorter analog of setting / resetting bit register dependnig on condition
      if condition then 
        W(reg, 1)   
        W(reg , 0)   

Debug printing

Standard functions (INFO and other ) for writing to communication log and lua console has some drawbacks:

  • they expect only one parameter of a string type, so you have to concatenate multiple parameters with '..' operator and add spaces between values
  • they don't check input parameters for a nil, so the script won't run after the error line if the 'nil' is met in the parameters being concatenated.

To enhance console and communication log output you may use your own versions based on these standard functions:

function DBG( ...) -- ... accepts multiple arguments in a table arg 
local table_sign = '|' -- if there is divider sign, the values will be grouped in 2 rows
local table_sign_found = false        
local tab_s_index = 0 
if ENABLE_DEBUG then -- should be global in the calling script 
    -- find divider
    for i = 1, #arg do 
        if arg[i] == table_sign then 
            table_sign_found = true 
            tab_s_index = i 
    if not table_sign_found then  
        INFO(tabToStr(arg)) -- outputs table values in a single row 
        local header_t, value_t = {}, {} -- prepare header and value rows
        for k = 1, tab_s_index - 1 do 
            header_t[k] = arg[k]
        for j = tab_s_index + 1, #arg do 
            value_t[j - tab_s_index] = tostring(arg[j])
        if (#header_t ~= #value_t) then 
             ERROR("column count differs!")
            for g = 1, #header_t do 
                local delta = #header_t[g] - #value_t[g] -- makes aligment by adding spaces to shorter strings...
                if delta ~= 0 then 
                    if (delta > 0) then 
                        value_t[g] = value_t[g] .. string.rep(' ', delta) 
                        header_t[g] = header_t[g] .. string.rep(' ', math.abs(delta)) 
            end -- for 
            INFO(tabToStr(header_t)) ; INFO(tabToStr(value_t))
end -- if ENABLE_DEBUG
return true 
end -- DBG
function tabToStr(t)  -- glue all and add spaces, use tostring to protect from nil and bool argument 
    local s = ""            
    for i = 1, #t do 
        s = s .. tostring(t[i]) .. ' ' 
    end -- for 
    return s 

Then the print output can be enhanced like this:

The ENABLE_DEBUG flag should be global boolean variable in the calling script. You may just to set it when needed and save the script.

ENABLE_DEBUG = true -- false will disable debug print

Sometimes you need to trace how a number of different scripts are running together, but want to filter out unneccesary lines in the communication log. Then you can use internal register of a string type for storing IDs of scripts where you want to enable log output. Inside each script you can check if its ID exists in the “enabled” list or no and control ENABLE_DEBUG variable. E.g.

thisScriptID = 43 -- ID is shown in the script list
local i,_,_ = string.find (                     -- use this function to find script ID in the list
                            R ("enDebugList"),  -- read enable string from the int. register
                            "%s+("..  thisScriptID ..")%s+") -- find a pattern of digit(s) inside spaces 
  if not i then -- equals to if ( i == nil ) 
     ENABLE_DEBUG = false 
     ENABLE_DEBUG = true

Running hours meter

Running hours meter accumulates the time while some equipment has been turned on.

function main (userId)
  runHoursMeter ( R ( "runHours_condition" ) == 1  , "runHours")
function runHoursMeter ( condition , meter_alias)
--[[ strores the time of prevous call in a _prevTime
when called, if condition is met adds diff. between current time and prevTime to _Acc
recieves prefix (meter_alias) and generates needed register aliases by adding suffix --]] 
    local now = os.time ()
    if condition then 
        local curAcc = R ( meter_alias .. "_Acc" ) 
        W (meter_alias .. "_Acc" , curAcc + ( now - R ( meter_alias .. "_prevTime" ) ) )
    W ( meter_alias .. "_prevTime", now ) -- store previous call time 

Three registers were used to demonstrate its work:

  • runHours_prevTime - this register stores the time of the previous function call. Type - Unix Type
  • runHours_Acc - accumulator for the running hours. Type - Double Word UInt, with the “Display as time duration” checkbox set
  • runHours_condition - to test how the function works from the interface this register is used. Type - Bit.

Universal Timer (TON / TOFF)

The timer compares its current state with the input condition:

  • current state = 0, input = 1 - the timer will switch on its output after onDelay time. If the offDelay parameter = 0, then it will be “pure” TON timer
  • current state = 1, input = 0 - the timer will switch off its output after offDelay time. If the onDelay parameter = 0, then it will be “pure” TOFF timer

Two registers are needed for this function:

  • a register with “timer_name” script alias of a Unix Time type - to store the time stamp of the rising / falling edge on the input
  • a register with “timer_name_out” script alias of of a Bit type - to keep timer state between calls (in case of project restart etc. )

Use sensible names for the register script aliases - to associate clearly aliases with the “real” equipment. This will greatly improve code readability. E.g. if you want to use timer to mask some alert condition from pressure sensor #1, you might use “lowP1_AlertTmr”and “lowP1_AlertTmr_out” aliases.

 function Timer (bool_input, onDelay, offDelay, tmrAlias)
--                bool      sec to ON  sec to OFF  string alias
local now , nowString, curTimeStamp, curTmrState =  os.time(), 
                                          "%c", now), 
                                                    TRUE(tmrAlias .."_out")     
DEBUG("curTimeStamp - ","%c", curTimeStamp) )
DEBUG ("cur "..tmrAlias.." State =  "..tostring(curTmrState))                 
-- protects from malfunctions on very first run 
if curTimeStamp == 0 then 
    DEBUG("curTimeStamp was zero in this timer!")
    WriteReg(tmrAlias, now)
    return nil, 0 -- countdown 
    -- in and output are equal                                                     
    if (bool_input == curTmrState) then 
        WriteReg(tmrAlias, now)                           DEBUG("timer input match state ") 
        return curTmrState, 0   -- as bool 
    -- TON     
    elseif bool_input then 
         if ((now - GetReg(tmrAlias)) > onDelay) then 
             WriteReg(tmrAlias .."_out", 1) 
                                                        DEBUG("detected ON state after delay (input, now, tmr, onDelay ):", bool_input, nowString, onDelay)
             return true, 0  
            local countDown = onDelay - (now - curTimeStamp)
            DEBUG("countdown to On in " .. tmrAlias .. " ".. countDown)
            return curTmrState, countDown
    -- TOFF     
    elseif not bool_input then 
        if (now - GetReg ( tmrAlias ) > offDelay) then   
            WriteReg (tmrAlias .."_out", 0)
            DEBUG("detected OFF state after delay (input, now, tmr, offDelay ):", bool_input, nowString, offDelay)
            return false  
            local countDown = offDelay - (now - curTimeStamp)
            DEBUG ("countdown to OFF in " .. tmrAlias .." ".. countDown)
            return curTmrState, countDown
end -- Timer func 
Note: This function should be called in every scan, i.e. don't put it inside "ifs", otherwise it won't update the 
timestamp and may not work properly.

Generating alert messages with time - delay

Generating alerts is a common thing in remote monitoring application. Becuase the system being monitoring can “balance” on the “alert / normal” point there should be made some time delay to filter alerts, especially when they are sent via messaging systems (e-mails, viber, telegram, sms)

PRESSURE_AL_DELAY = 60 -- seconds
function main(userID)
   local error_condition = R("pressure") >= PRESSURE_HIGH_LEVEL
   local alTmrState = R("pAlerTmr_out") -- remember current timer state before calling a Timer function
   if Timer(error_condition, PRESSURE_AL_DELAY, 0, "pAlerTmr") then 
        if alTmrState == 0 then -- alert rise
            SendSMS("380679999999", "Pressure is over High Limit!")
        if alTmrState == 1 then -- alert fall
            SendSMSMessage("380679999999", "Pressure is normal now.")

Moving average

The moving average is useful for smoothing the values ​​of parameters that have noises, pulsations so to avoid unneccessary control outputs reacting to unwanted noise.

Algorithm of the moving average: at the beginning of the filter on the sample, N values ​​are counted by the arithmetic mean, after reaching the end of the sample, one element is discarded (by dividing the sum by the length of the queue), a new one is added instead of it, and the amount is again divided by the length of the queue.

function MovingAverage(reg, filter)
    local Q_DEPTH , queue_fill, av_sum  = 10 , 
                                          R(filter .. "_fill"), 
                                          R(filter .. "_sum")
    local in_value, tmp_var, out_value  = R(reg), 0, 0 -- read input and initialize some vars
    if (queue_fill < Q_DEPTH) then           -- queue not filled yet
        av_sum = av_sum + in_value           -- accumulator
        queue_fill = queue_fill + 1          -- inc. index
    else                                     -- now the queue full
        tmp_var = av_sum / Q_DEPTH           -- calc. weight of the one element of the queue
        av_sum = av_sum - tmp_var + in_value -- subtract it and add new input value 
    if (queue_fill == Q_DEPTH ) then         -- if filled 
          out_value = av_sum / Q_DEPTH       -- calc. as current accumulator / queue lentgh, moving average
          out_value = av_sum / queue_fill    -- not filled then mean average 
                                             -- remember state
    W (filter .. "_sum", av_sum) 
    W (filter .. "_fill" , queue_fill) 
    return out_value
end -- MovingAverage 

Two registers are required for this script -

  • accumulator for the sum, e.g. “meanOutsideT_sum
  • current queue index “meanOutsideT_fill

PID - control

An example of implementing a PID controller in WebHMI:

function PID ( sp , pv , c_alias)
                                                    DBG ( "Entered PID for ", c_alias )
  local now, nexTime = os.time() , R ( c_alias .. ".nextPidTime" )
  local CYCLE_TIME = R ( c_alias..".pidCycleTime" )
  local ZONE = 0.2                -- can alse be an external setting 
  -- параметры регулятора 
  local Kp = R ( c_alias..".Kp" ) 
  local Ti =  R (c_alias..".Ki" ) 
  local Td = R (c_alias..".Kd" ) 
  local Int_sum = R ( c_alias..".pidIntegral" ) -- integral part accumulator
  local G = R (c_alias .. ".pidOut") 
  local  Err =   sp - pv           ;                DBG ( "current Error = ", Err )
     -- add some tolerance for error 
  if ( math.abs ( Err ) <= ZONE ) then Err = 0 end 
                                                    DBG(" seconds left for PID cycle = "..tostring(nexTime - now))
  if ( now >= nexTime ) then 
        W ( c_alias..".nextPidTime", now + CYCLE_TIME)   -- calc. next cycle time 
        local dErr = Err - R (c_alias..".pidPrevError" ) 
            -- calc. integral limit 
        local iSUM_LIMIT = G_LIMIT * (Ti / Kp)
   --integral part check 
        if (Ti == 0) then 
            Int_sum = 0 
            Int_sum = Int_sum + Err 
            -- check limits of integral part 
                if Int_sum >= iSUM_LIMIT then 
                    Int_sum = iSUM_LIMIT 
                elseif Int_sum < 0 then
                    Int_sum = 0 
                    -- undefined if  
        G = Round(Kp * (Err + (Int_sum / Ti) + Td * dErr)) 
        -- check output and limit 
        if (G < 0) then G = 0  end
        if (G > G_LIMIT) then G = G_LIMIT end
        -- refresh outputs 
        W (c_alias..".pidPrevError", Err) 
        W (c_alias..".dErr", dErr) 
        W (c_alias.. ".pidIntegral", Int_sum) 
        W (c_alias..".pidOut", G) 
   end -- time stamp 
   return G 
end  -- PID 

This algorithm is typical for use in PLCs. Because the regulator is run at regular intervals, i.e. diff. and int. the components are always computed on the same time scale, so it is not necessary to divide and multiply them by time to obtain the derivative and integral, we can select the time constants Ti, Td. In this algorithm, Ti is an inverse quantity (the larger its value, the smaller the contribution of the integral error).

In this example the register are addressed with “connection.alias”, i.e. the script will look for register with name “alias” in the connection “connection”.

Fixed quantity circulation with PID

This algorithm allows to precisely control process variable (temperature, pressure etc. ) with PID control which transfers its output for smooth control to only one power unit (current active unit - A/C, heater, fan section etc.), while other units work at maximal or minimal output.

function CircControl ( sp, pv, alias )
    local TOTAL_CH = 3                                     -- total channels 
    local G_MIN, G_MAX = 0 , 100                           -- limits of pid output
    local tmrSwDelay = R ( alias .. "pidCycleTime" ) * 3 
    local G = PID ( sp, pv, alias)                   DBG ( "CircControl pidOut = ", G )
    local curCh = R ( alias .. "curCh")              DBG ( "curCh", curCh )
    -- filtering small error 
    local err = sp - pv 
    if ( math.abs ( err ) <= 0.2 ) then err = 0 end 
    local fwTimer = alias .. "fw_Tmr"
                                                     DBG ( "fw pre-cond. - ", NOT ( fwTimer .. "_out") , (err < 0), ( G == G_MIN ) )
    if  Timer ( ( err < 0 ) and ( G == G_MIN ) , tmrSwDelay , 0 , fwTimer ) then 
                                                 -- if stay too long with negarive error, decrease cnannel            
            if ( curCh + 1 ) <= TOTAL_CH then 
                                                    DBG ( "fw condition detected!, curCh now = ", curCh )
                curCh = curCh + 1 
                W ( alias .."curCh", curCh ) 
                                                 -- and set max. output for the lower channel 
                G = 100                          -- for smooth switching
                W (alias .. "pidOut", G  )
                W (alias .. "pidIntegral", 1000) 
    local backTimer = alias .. "back_Tmr"
                                                     DBG ( "back pre-cond. - ", NOT ( backTimer .. "_out") , (err > 0), ( G == G_MAX ) )
                                                     -- if stay too long with positive error, increase cnannel   
    if Timer ( ( err > 0 ) and ( G == G_MAX ) , tmrSwDelay , 0 , backTimer ) then 
        if (curCh - 1) >= 1 then 
                                                     DBG ( "back condition detected!, curCh now = ", curCh )
            curCh = curCh - 1 
            W ( alias .."curCh", curCh ) 
                                                   -- and set low output value for the next channel
                                                   -- for smooth switching
            G = 0 
            W (alias .. "pidIntegral", G ) 
            W (alias .. "pidOut", G )
    -- pid output transferred to current channel, lower channels work at minimal, higher 
    -- channels at maximal output
    local ch_values = { 0 , 0 , 0 } 
    for i = 1 , 3 do 
        if ( i < curCh ) then     
            ch_values [i] = G_MIN 
        elseif ( i == curCh )  then 
            ch_values [i] = G 
        elseif ( i > curCh ) then 
            ch_values [i] = G_MAX
            DBG ("Undefined if in ch_values assignment, Line:94 ")
    end -- for 
                                                    ; DBG ( "now channels are:", ShowTable ("%f" , ch_values) )
    return ch_values[1], ch_values[2], ch_values[3]

Time Circulation algorithm (together with redundancy function)

This algorithm is used in systems where it is necessary to alternate the operation of mechanisms (pumps, fans, air conditioners) over time, or on the run hour meters. For example, a set of 2 units is used, which must be alternated in time. If an error occurs on some unit, then the algorithm starts working only on the working (redundancy function). An example of setting the required registers is given below:

CIRCULATION_TIME = 30; -- for tests circulation time is short 
function main (userId)
  if there are no errors, then circulate over time 
  If there is an error on one of the air conditioners, it is excluded from the rotation
  if there are errors on both, then we stand 
local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- errro on a/c #1 (DS101@webmi)
local switchTime = GetReg("switchTime"); -- next switch over time (DS103@webmi)
local now = os.time()
local curActiveAC = GetReg("activeAC"); -- active a/c (DS100@webmi)
if (not acError1) and (not acError2) then 
    -- work on circulation
    if (now >= switchTime) then 
        if (curActiveAC == 1) then 
            WriteReg("activeAC", 2);
            WriteReg("activeAC", 1);
        WriteReg("switchTime", now + CIRCULATION_TIME);
elseif acError1 and (not acError2) then 
        WriteReg("activeAC", 2);
elseif acError2 and (not acError1) then 
        WriteReg("activeAC", 1);
        WriteReg("activeAC", 0);
end -- if no errors 
end -- main

For simplicity and clarity, it is better to split the scripts into functional modules that can be quickly analyzed and placed in the right order in the program list. The first script looks at the errors and if they do not exist, the air conditioners alternate in time

The second script check which conditioner is now active, and performs the necessary actions. In a script, this is just debugging, but there may be commands for controlling the infrared transmitter for issuing the desired command, writing to the message log and switching, etc.

function main (userId)
     turn on selected a/c depending on pointer
  local pointer = GetReg("activeAC"); -- active conditioner (DS100@webmi)
if (pointer==0) then 
      DEBUG("all off")
      return 0
      (pointer==1) then 
          DEBUG("turn on a/c #1");
          DEBUG("turn on a/c #2");
  end -- if 

Also here you need a script that will expose the flags of errors of work on certain conditions, read the status of the protection devices, the error registers on the interface, and so on.

3-point control for a valve or servo

A 3-point method is used to control the position of the valve, servo, gate valve, etc., when 3 wires are used to control the drive - 'common', 'power UP', 'power - DOWN'. Such drives may or may not be equipped with end position sensors. Sometimes, in the absence of position sensors and low requirements for positioning accuracy, an algorithm can be used when the drive leaves down or up (either one position sensor or one command for a time longer than the valve's full travel time), initializes the coordinate, and then go to the specified position.

To determine intermediate positions, a calculated value is used, determined from the characteristics of the 'full path time', which can also be determined experimentally.

Below is a variant of 3-point control for a valve with 2 limit switches.

function main (userId)
       -- copy desired Tfeed to valve PID target temp.
    W ( "HeatDistribution.targetTemperature",  R ( "recalcFeedTemp" ))     
now = os.time() -- global 
local manOpenCmd, manCloseCmd = R("manOpenCmd"), R("manCloseCmd")     -- manual run flags
local autoOpenCmd, autoCloseCmd = R("autoOpenCmd"), R("autoCloseCmd") -- cmds to control 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)
                -- filtering cur position and check limit sw
if (curPosition >= 100) or openSw then 
    curPosition = 100 
        if openSw then 
            W ("pullUpFlag", 1 )
            pullUpFlag = true 
if ((curPosition < 0) or closeSw) then 
    curPosition = 0 
        if closeSw then 
            W ("pullDownFlag", 1 ) 
            pullDownFlag = true 
                            -- pulling
if (curPosition == 100) and not (openSw) and not pullUpFlag then 
    curPosition = 99  ;                                             DEBUG(" pulling up") 
if (curPosition == 0 ) and not (closeSw) and not pullDownFlag then 
    curPosition = 1   ;                                             DEBUG(" pulling down") 
W ("valveCurPos", curPosition) -- renew current 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 befor start 
    W ("pullDownFlag" , 0 ); W ("pullUpFlag", 0)
if ( R ( "distribAutoMode") == 1) then 
    W ("manOpenCmd", 0 ) -- clear manual cmds
    W ("manCloseCmd", 0)     
    if (positionError == 0)  then 
        W ("autoOpenCmd", 0 )
        W ("autoCloseCmd", 0) 
    elseif (positionError > 0)   then 
                                    DEBUG("GO DOWN because positionError = "..positionError) ; DEBUG(" math.abs error <= 0.5 "..math.abs(positionError))
        W ("autoOpenCmd", 0 )
        W ("autoCloseCmd", 1) -- GO DOWN
    elseif (positionError < 0)  then 
                                    DEBUG("GO UP because positionError = "..positionError) ; DEBUG(" math.abs error <= 0.5 "..math.abs(positionError))
        W ("autoOpenCmd", 1 ) -- GO UP
        W ("autoCloseCmd", 0) 
                                    DEBUG("Undefined if !!! in valve control ")
                                  --- MANUAL MODE ----- 
                                  -- just copy manual cmd to valve auto cmd
    W ("autoOpenCmd", manOpenCmd )
    W ("autoCloseCmd", manCloseCmd)
     if not openSw then 
        W ("autoOpenCmd", manOpenCmd ) 
        W ("manOpenCmd", 0 )
        W ("autoOpenCmd", 0 )
        W ("valveCurPos", 100) -- renew position after open limit sw
    if not closeSw then 
        W ("autoCloseCmd", manCloseCmd) 
        W ("manCloseCmd", 0) 
        W ("autoCloseCmd", 0) 
        W ("valveCurPos", 0) -- renew position after close limit sw
end -- main 
function MotionTimer(openCmd, downCmd, tmrAlias)
local FULL_PATH_TIME = 180 -- sec. full path time
local KOEF = 100 / FULL_PATH_TIME
local quant = 0 
--[[                 bool        string 
remembers start time
recalc. micro path from the previous call if was in motion
adds micro path to current position 
--                                                  open = true                                       ]] 
    local motion = { flag = ((openCmd == 1) or (downCmd == 1)), dir = (openCmd == 1), lastTimeStamp = R(tmrAlias)}
    DEBUG(" motion.flag = "..tostring(motion.flag).." dir  "..tostring(motion.dir)"%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) 
                                                     DEBUG("quant calculated as = "..quant)
      W (outAlias, 1) -- "in motion" flag
                    DEBUG("no motion lasts = ")
      W (outAlias, 0)  
      quant = 0 
    end  -- if 
W (tmrAlias, now)     -- rememeber last call time 
return quant
end -- function 

Other handy functions

-- round a value with set precision 
function Round (num, numDecimalPlaces)
  local mult = 10^(numDecimalPlaces or 0)
  return math.floor(num * mult + 0.5) / mult
-- checks if value within range 
function InRange (min, max, value)
    if (value >= min) and (value <= max) then 
        return true 
        return false 
-- scaling based on y = kx + b formula 
function linearCalc ( value, x1, x2 , y1, y2 )
    local k = (y1 - y2 ) / (x1 - x2 )   
    local b = y1 - k * x1               
    local y = k * value + b             
    return y 
function GetTimeFromHHMM(arg1, arg2 , type, incYearFlag) -- get UNIX time from the month day hour minute
    -- type HHMM - using hour minutes
    -- type mmdd - using day month
    local dateTable ="*t", now) 
    local result = 0 
    produces the table
    {year = 1998, month = 9, day = 16, yday = 259, wday = 4,
     hour = 23, min = 48, sec = 10, isdst = false}
    if (type == "HHMM") then 
            dateTable.hour = arg1
            dateTable.min = arg2
            dateTable.sec = 0 
            return os.time(dateTable)
    elseif (type == "mmdd") then 
   = arg1
            dateTable.month = arg2 
            dateTable.sec = 0 
            if incYearFlag then 
                dateTable.year = dateTable.year + 1 
            result = os.time(dateTable)
             DEBUG("GetTimeFromHHMM going to return this - ""%c",result))
            return result
        ERROR("Undefined if in GetTimeFromHHMM")
end -- GetTimeFromHHMM
function UpdateReg(reg, value) -- avoids unnecessary writing to a register (write on change)
    if R(reg) ~= value then    -- then use shortcut WriteReg = UpdateReg, then all WriteReg fs become on demand
        W(reg, value)
        -- DEBUG("won't rewrite thie register")
function P_TRIG(var, previous)
    -- возваращет pos / neg  если было обнаружено изменение 
    -- false - если не было зименения 
    -- пред. состояние запоминается в previous
    DEBUG("P_TRIG was called with this " .. var .. " " ..previous)
    local curVar = R(var)       
    if not curVar then return false end 
    local prevVar = R(previous) ; DEBUG(" p_trig " .. curVar .. " " .. prevVar)
    W (previous, curVar)
    if (curVar ~= prevVar) then 
        if curVar > prevVar then 
            return "positive" 
            return "negative"
            return false -- nothing happened 
end -- P_TRIG 

PLC - like control

If you are an experienced user of PLC and get used to Ladder Diagram or FBD languge, in the beginning you may feel difficult to adapt to writing the same algorithms in WebHMI script editor.

But using above mentioned handy funtions and writing your own analogs of your PLC function you can get almost the same short and clear code almost identical to ladder diagram.

E.g. let's consider this ladder network from Siemens Tia Portal, which enables running Pump 1 and Pump 2 depending on day time and other conditions:

The same logic could be written like this -

function main (userId)
  -- Pump 1 & 2 logic 
local  if_pump1_2_run = (TRUE("nightTime")  
                          TRUE("halfPeadEnable") and InRange(R("hPeakStart"), R("hPeakEnd"), os.time ())
                          TRUE("enableCheckPumps")) and TRUE("genAutoMode") and NOT("criticalAlert") 
  OUT(if_pump1_2_run, "pump1_OnCmd") 
  OUT(if_pump1_2_run, "pump2_OnCmd")  

Self-masking filter for the physical button (one-shot)

Sometimes it is important to control a series of actions with a guaranteed delay between them, i.e.avoid “sticking” action together when the actions are initiated with a button.

The button or other signal may have duration in time, but this signal is used for one-shot pulse to trigger next action. In the ladder program, this could be done like this:

Using handy lua functions from above, it could be written in this way:

function main (userId)
  -- after pressing physical button 
  -- generate one-scan event and then block itself for set delay 
  local SELF_MASK_DELAY = 8 -- seconds 
  local enCond = TRUE("button") and NOT ("lockTmr_out") ;  OUT(enCond , "evTrigger")
  if enCond then
      AddInfoMessage("triggered button and masked itself for " .. SELF_MASK_DELAY .. ' ' .. "seconds") 
  Timer(TRUE("button" ), 0 , SELF_MASK_DELAY , "lockTmr") 

The video how this code works on registers:

You can check how the script worked in the messages log (the second message was generated from the event with enabled 'Add log message' option):

Detecting rise / fall edge of the signals

function P_TRIG(var, previous)
    -- detect risging or falling state of the var
    -- false for nil input 
    -- prev. var value stored in previous alias register 
    DEBUG("P_TRIG was called with this " .. var .. " " ..previous)
    local curVar = R(var)       
    if not curVar then return false end 
    local prevVar = R(previous) ; DEBUG(" p_trig " .. curVar .. " " .. prevVar)
    W(previous, curVar)
    if (curVar ~= prevVar) then 
        if (curVar > prevVar) then 
            return "positive" 
            return "negative"
            return false -- nothing happened 
end -- P_TRIG 

Page Tools