User Tools

Site Tools


useful_programs

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
bit.band(x1[,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
end
 
function hasbit(x, b) 
  local p = bw(b)
  return x % (p + p) >= p  -- returns if b is true/false; if hasbit(value, b) then ...
end
 
function setbit(x, b) 
  return hasbit(x, b) and x or x + bw(b) -- sets bit b in х example:  х = setbit(х, b))
end
 
function clearbit(x, b)
  return hasbit(x, b) and x - bw(b) or x --  clears bit b in х 
end
 
function togglebit(x, b) -- toggles bit b in x 
    if hasbit(x, b) then 
        return clearbit(x, b)
    else 
        return setbit(x, b)
    end 
end 
 
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) 
     else 
         return hasbit(x, b) and x - bw(b) or x 
     end 
end 
 
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)
     W(alias, new_value)
end 
 
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
    end 
end 
 
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)
    end 
end 
 
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)
    else 
        setBit(alias, b)
    end 
    return true 
end 
 
-- return bit from the position as a  1 / 0 
function getBit(alias, b)
 
    if hasbit(R(alias), b) then 
        return 1 
    else 
        return 0 
    end 
end 

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) 
end 
 
function NOT(reg)
    return (R(reg) == 0) 
end 
 
function SET(reg)
    W(reg, 1)   
end 
 
function RESET(reg)
    W(reg, 0)   
end 
 
function OUT(condition, reg)
-- shorter analog of setting / resetting bit register dependnig on condition
      if condition then 
        W(reg, 1)   
      else 
        W(reg , 0)   
      end   
end

Examples of using bit functions

Writing condition value into some bit

In the following example, a condition result is written right into the register mask. This could be used e.g. for status register, where each bit corresponds to some system alert while providing overall system status as 0 / >1 (no bit set(no alert), some bit set (there is an alert.)). This status register can be used in Level2 system to show the node's state.

-- sets bit in certain register depending on condition 
include "bit.lib" 
 
function main (userId)
    outBit(not R("someRegister"), "alertMask", 15)
end

Table functions

-- converts bytes to integer value 
table.int = function(t) 
   local HEX_NUMBERING = 16
   return tonumber(table.hexStr(t), HEX_NUMBERING) 
end
 
-- converts a table to hex view 
function table.hexStr(hextab, spacer) 
    local hex = {} 
    for _, hexbyte in ipairs(hextab) do 
        table.insert(hex, getHexByteAsStr(hexbyte))
    end 
    return table.concat(hex, spacer)
end 
 
-- converts tab to bcd number 
table.bcd = function(t)
    local hex = table.hexStr(t)
    local out = tonumber(hex) 
    return out 
end 
 
-- gets 2-char hex string from a byte 
function getHexByteAsStr(inputByte) 
    local strByte = string.format("%X", inputByte)    
    return (#strByte == 1 and '0' .. strByte) or strByte
end 
 
 
 
-- reverses a table 
table.reverse = function (tab) 
    local outTable = {}
    for i = #tab, 1, -1 do 
        table.insert(outTable, tab[i])
    end 
    return outTable
end 
 
--[[  find an element in a table 
    @ t - input table , v - value to find 
    --> value index of false (if not found)
--]] 
function table.find(t, v)
    for i, tv in pairs(t) do 
        if (tv == v) then 
            return i 
        end 
    end 
    return false 
end
 
--[[  finds pattern in a table 
    @ t - input table , pttrn - pattern to find 
    --> index of the first byte following the pattern
--]] 
function table.findPattern(t, pttrn) 
 
    for tabIndex, _ in ipairs(t) do 
        local matchFlag = true 
 
        for seqIndex, sequenceByte in ipairs(pttrn) do  -- checking pttrn inside a t 
            matchFlag = matchFlag and (sequenceByte == t[tabIndex + seqIndex - 1])
            if (not matchFlag) then 
                matchFlag = false 
                break
            end 
        end
        if matchFlag then 
            return (tabIndex + #pttrn) 
        end 
    end 
    return false  
end 
 
--[[  checks if the table has an elem 
    @ t - table , test 
    --> index of false (if not found)
--]] 
function table.some(t, test)
    for i, tv in pairs(t) do 
        if (test(tv)) then 
            return true 
        end 
    end 
    return false 
end
 
function table.sub(t, s, e) 
    local newTab = {}
    if (s and e) then 
        for i = s, e do 
            table.insert(newTab, t[i])
        end 
    end 
    return newTab 
end 
 
--[[ convenient print function for tables 
@ t - table, indent - indent between key & value
--]] 
function tprint(t, indent)
    if not indent then indent = 0 end 
    for k, v in pairs(t) do 
        local formatting = string.rep(' ', indent) .. k .. ': '
        if type(v) == "table" then 
            ERROR(formatting) 
            tprint(v, indent + 1) -- recursive call 
        else
            if type(v) == "boolean" then 
                v = v and "TRUE" or "FALSE"
            end 
            ERROR(formatting .. v) 
        end 
    end 
end

Compare and trigger

This is used to detect when the value reach certaint value and make one-time action.

cmp = {}
function cmp : new(reg, level) 
    self.__index = self 
    return setmetatable({ reg = reg,
                          level = level, 
                          flag = false }, self)
end 
function cmp : __call(helper) 
    local regValue = R(self.reg)
 
    if helper(regValue, self.level) then 
        if (not self.flag) then 
            self.flag = true 
            DEBUG("got it !")
            return true 
        end 
    else 
        self.flag = false 
    end 
 
    return false     
end 
 
function greaterThan(arg1, arg2)
    return arg1 >= arg2
end 
 
function main (userId)
    if (not detect) then 
       detect = cmp : new ("dBit2", 1)
    end 
 
    if detect(greaterThan) then 
       -- SendTelegramMessage()
    end 
end

Iteraion over indexed registers

You can iterate your register in loogs using their alias names, which contain the index, e.g.:

--[[
    The program uses random number from 1 to 10 as a base to set 3 consequtive registers
    named "indexedRegister1", "indexedRegister2", "indexedRegister3"
--]]
function main (userId)
 
  local startValue = math.random(1, 10)
 
  for i = 1, 3 do
      W("indexedRegister" .. i, startValue)
      startValue = startValue + i 
  end 
 
end

The result is shown on the following picture:

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:

--[[  universal print function
      prints when global ENABLE_DEBUG is set 
    @ ... any list 
          if arguments have | symbol treats data as a 2-row data 
          "arg1, arg2, arg3 ... ", '|'
          , arg1, arg2, arg3 ... 
          if arg type == table print all using tprint 
          otherwise prints all arguments in a row 
--]] 
function DBG(...)
 
    if (not ENABLE_DEBUG) then return end 
 
    -- piped printing 
	local pipePosition = table.find(arg, '|')
    if (pipePosition) then 
        -- make header row 
        local th = {} 
        for word in string.gmatch(arg[1], "%S+") do 
		   table.insert(th, word) 
        end 
        -- make data row 
	    local td = table.sub(arg, pipePosition + 1, #arg)
        if (#th ~= #td) then 
            ERROR('th & td in DBG are not equal!') ; return 
        else 
            for i = 1, #th do 
                local thl, tdl = string.len(th[i]), string.len(td[i])
                local diff = math.abs(thl - tdl)
                if (thl > tdl) then 
                    td[i] = wrapToSpaces(td[i], diff)
                end 
                if (tdl > thl) then 
                    th[i] = wrapToSpaces(th[i], diff)
                end 
            end 
            DEBUG(table.concat(th, ' ')) ; DEBUG(table.concat(td, ' '))
        end 
        return 
    end
    -- table print 
    if table.some(arg, function (a) return type(a) == 'table' end ) then 
        tprint(arg) ; return 
    end  
    -- print in a row
    DEBUG(table.concat(arg, ' ')) 
end    
--[[ add extra spaces on both sides of string 
    @ s - string 
      n - number of spaces 
    --> string wrapped in spaces 
--]] 
function wrapToSpaces(s, n) 
    local spaces = string.rep(' ', math.floor(n / 2))
    return  spaces .. s 
            .. ((n % 2 == 0) and spaces or spaces .. ' ')
end 

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 
  else
     ENABLE_DEBUG = true
  end

For complex tables print, you can use the following function:

function tprint(t, indent)
    if not indent then indent = 0 end 
    for k, v in pairs(t) do 
        local formatting = string.rep(' ', indent) .. k .. ': '
        if type(v) == "table" then 
            ERROR(formatting) 
            tprint(v, indent + 1) -- recursive call 
        else
            if type(v) == "boolean" then 
                v = v and "TRUE" or "FALSE"
            end 
            ERROR(formatting .. v) 
        end 
    end -- for 
end -- tprint 

Date & Time operations

When you need to select different date - time numbers, you can use the following example:

function main (userId)
 
  local dateTimeTable = os.date("*t", os.time())
 
  INFO(dateTimeTable.hour)
  INFO(dateTimeTable.min)
 
end

Please refer to the Lua documentation site.

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")
 
end
 
 
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" ) ) )
    end 
 
    W ( meter_alias .. "_prevTime", now ) -- store previous call time 
end 

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.

Timers

You can create your timers preventing jittering unnecessary input, which leads to mulitple alert generation or other action, prolongate some states after they changed.

Below some examples are provided:

  • Simple - Ton/Toff timers. These ones keep their state in the script's global vars, so each time the project restarts, they will be initialized. For most cases that's enough.
  • Reset-Restart proof timers. These timer keep their state in the external registers, so they are tolerant to even project restart. Use these timers for critical control.

Simple TON timer

-- Timer constructor 
Timer = {} ; setmetatable(Timer, {__call = function(self) 
                                       local obj = {stamp = os.time(), out = false}
                                       return setmetatable(obj, {__index = self})
                                   end })
-- Timer loop 
function Timer : run(in_, delay) 
   local now = os.time() 
   if (not in_) then 
       self.out = false ; self.stamp = now 
   else 
       if (not self.out and (now - self.stamp) >= delay) then 
           self.out = true 
       end 
   end 
   return self.out 
end 
function Timer : reset() self.out = false ; self.stamp = os.time() end 

Simple TOFF timer

toff_proto = {}  toff_proto.__index = toff_proto
 
function toff_proto.new(delay)
    return setmetatable({d = delay,
                         stamp = os.time(), 
                         out    = false,
                        }, toff_proto)
end 
 
function toff_proto:__call(in_)
 
    local now = os.time()
 
    if (in_ ~= self.out) then 
        if in_ then 
            self.out = true                -- immediate ton!  
        else 
            local diff = now - self.stamp
            if diff < self.d then 
                DEBUG("going to sw off in " .. self.d - diff)
                return self.out             -- don't update stamp 
            else 
                DEBUG("Switched off!") ; self.out = false  
            end 
        end 
    end 
 
    self.stamp = now 
    return self.out 
end

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 external registers are needed for this function to work:

  • 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(), os.date("%c", now), R(tmrAlias), TRUE(tmrAlias .."_out")     
 
    DBG("Timer State Stamp", "|", tmrAlias, curTmrState, os.date("%c", curTimeStamp)) 
-- protects from malfunctions on very first run 
if curTimeStamp == 0 then 
    DBG("curTimeStamp was zero in this timer!")
    WriteReg(tmrAlias, now)
    return nil, 0 -- countdown 
end 
 
    -- in and output are equal                                                     
 
if (bool_input == curTmrState) then 
        WriteReg(tmrAlias, now) ; DBG("timer inputs match") 
        return curTmrState, 0   -- as bool 
 
                            -- TON     
elseif bool_input then 
         if ((now - curTimeStamp) > onDelay) then 
             SET(tmrAlias .."_out") ; WriteReg(tmrAlias, now) ; DBG("detected ON state after delay")
             return true, 0  
        else 
            local countDown = onDelay - (now - curTimeStamp) ; DBG("countdown to On in ", countDown)
            return curTmrState, countDown
        end 
 
                            -- TOFF     
elseif not bool_input then 
 
        if ((now - curTimeStamp) > offDelay) then   
            RESET(tmrAlias .."_out") ; WriteReg (tmrAlias, now) ; DBG("detected OFF state after delay")
            return false, 0   
        else 
            local countDown = offDelay - (now - curTimeStamp) ; DBG("countdown to OFF in ", countDown)
            return curTmrState, countDown
        end 
    end     
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_HIGH_LEVEL = 6.0 -- bar
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!")
        end 
   else 
        if alTmrState == 1 then -- alert fall
            SendSMSMessage("380679999999", "Pressure is normal now.")
        end 
   end 
 
end

This one uses simper timer:

include "timers"
CHAT_ID = 569335646
MSG_DELAY = 5 * 60 
 
function main (userId)
 
  reportConnError(2, MSG_DELAY, CHAT_ID, 'CONN STUCK ON NODE https://node-name.webhmicloud.com')
 
end
 
function reportConnError(connId, delay, chatId, msg)
 
    if (not tmr) then 
      tmr = Timer()
    end     
 
    if tmr : run(GetConnectionErrors(connId) > 0, delay) then 
        SendTelegramMessage(chatId, msg)    
        tmr : reset() 
    end 
end

Filtering frequent messaging

When you want to filter too frequent messaging from WebHMI, e.g. about alerts to the Telegram or else, you can use the following script :

TELEGRAM_BOT_ID = "974482918"                -- your telegram chat bot id 
-- TELEGRAM_BOT_DDS = "569335646" 
 
curAlertsNum = 0                            -- global counter for current active alerts
 
blockingTimer = {
                    turnedOn = false, 
                    timeStamp = 0,
                    -- ADJUST FILTER TIME HERE AS NUMBER_OF_MINUTES * NUMBER_OF_SECONDS
                    timerDelay = 5 * 60, 
                }
 
function blockingTimer : method(argument) 
 
    local now = os.time()
 
    if (self.timeStamp == 0) then 
        self.timeStamp = now      -- init for 1st call 
    end 
 
    if (argument == 'set') then 
        self.turnedOn = true 
        self.timeStamp = now 
    end 
 
    if self.turnedOn and (now - self.timeStamp > self.timerDelay) then 
        self.turnedOn = false 
    else 
        local countDown = self.timerDelay - (now - self.timeStamp) 
        DEBUG("masking new alerts with blockingTimer: " .. countDown) 
    end 
 
    return self.turnedOn
end 
 
function main (userId)
 
  local alerts = GetCurrentAlerts()
 
  if (not blockingTimer : method()) and (#alerts > curAlertsNum)  then -- number has increased, # - means count table elements
      local alertNames = {} ; DEBUG("Alert count has increased!")
 
      for alertIndex, astruc in ipairs(alerts) do 
          table.insert(alertNames, astruc.title)
      end 
      alertNames = table.concat(alertNames, ', ')
      SendTelegramMessage(TELEGRAM_BOT_ID, "Current alerts are: " .. alertNames)
      blockingTimer : method('set')
  end 
 
  curAlertsNum = #alerts
 
end

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 
    end
 
    if (queue_fill == Q_DEPTH ) then         -- if filled 
          out_value = av_sum / Q_DEPTH       -- calc. as current accumulator / queue lentgh, moving average
        else
          out_value = av_sum / queue_fill    -- not filled then mean average 
    end
                                             -- 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

ON-OFF control

Function based

include "LIB"
 
SP, ZONE        = 30, 0.5 -- °C 
PV_REG, OUT_REG = 21, 50  -- id of the registers with process value and output 
 
function tempControl(sp, pv, zone, out, inZone) -- on - off function ---
 
    local inZone_ = (pv >= (sp - zone)) and (pv <= sp) and TRUE(inZone)
 
    if (pv >= sp) then 
        SET(inZone)
    elseif (pv < (sp - zone) ) then 
        RESET(inZone)
    end 
    DEBUG("sp pv " .. sp .. ' ' .. pv)
    U(out, not (pv >= sp or inZone_) )
 
end 
 
 
function main (userId) ------- Main body ---------------------
 
    tempControl(SP, R(PV_REG), ZONE, OUT_REG, "inZoneControl")
 
end

Object programming style

include "LIB"
 
tempControl = {} -- on-off control obj ---------------------------------------
 
function tempControl : new (pvReg, outputReg, inZoneReg) -- constructor & init 
    self.__index = self 
    return setmetatable( { pvReg     = pvReg, 
                           out       = outputReg,
                           inZoneReg = inZoneReg
                         }, 
           self)
end 
 
function tempControl : __call (sp, zone)  -- controller 
 
    local pv = R(self.pvReg)  ; DEBUG("sp pv " .. sp .. ' ' .. pv)
    local inZone = (pv >= (sp - zone)) and (pv <= sp) and TRUE(self.inZoneReg)
 
    if (pv >= sp) then 
        SET(self.inZoneReg)
    elseif (pv < (sp - zone) ) then 
        RESET(self.inZoneReg)
    end 
 
    U(self.out, not (pv >= sp or inZone))
 
end 
 
-------------------------- Main ---------------------------------------------------------    
 
SP, ZONE        = 30, 0.5 -- °C 
PV_REG, OUT_REG = 21, 50  -- id of the registers 
 
function main (userId)
    -- creates obj 
    if (not control) then
        control = tempControl : new (PV_REG, OUT_REG, "inZoneControl") 
    end 
 
    control(SP, ZONE)
end

Both examples use the following library

function TRUE(reg)  -- checks if value is 1 
    return (R(reg) == 1)
end 
 
function NOT(reg) -- checks if value is 0 
    return (R(reg) == 0)
end 
 
function SET(reg) -- sets reg to 1 
    local curValue = R(reg)
 
    if curValue and (curValue ~= 1) then 
        W(reg, 1)
    end 
end 
 
function RESET(reg) -- sets reg to 0  
    local curValue = R(reg)
 
    if curValue and (curValue ~= 0) then 
        W(reg, 0)
    end 
end 
 
function U(reg, new_value) -- Updates values only when it differs from the target register content 
    local cur_value = R(reg)
 
    if (cur_value ~= nil and new_value ~= nil) then 
 
        if (type(new_value) == 'boolean') then -- protect from boolean value 
            new_value = new_value and 1 or 0         
        end 
 
        if (new_value ~= cur_value) then 
            W(reg, new_value)
        end 
    else 
        ERROR("Could not update value in reg" .. reg .. ' with a value of ' .. tostring(new_value))
    end 
end 

Another way of creating objects:

ObjConstructor = {}
setmetatable(ObjConstructor, {
            	__call = function(self, delay)  -- init method 
                    		local o = {
                    			delay = delay   -- put init data here 
                    		}
                    		-- now may add methods to the prototype 
                    		return setmetatable(o, {__index = self}) 
                    	end
})
 
function ObjConstructor : oneShot()
 
	local now = os.time()
 
	if (not self.stamp or (now - self.stamp >= self.delay) )  then 
	    self.stamp = now 
	    return true 
    else 
        return false 
    end 
 
end
 
function main (userId)
 
  if (not tmr) then 
      local ONE_SHOT_DELAY = 10 
      tmr = ObjConstructor(ONE_SHOT_DELAY)
  end 
 
  DEBUG( tostring(tmr : oneShot() )  )
 
end

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 
        else 
            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 
                else 
                    -- undefined if  
                end 
        end 
 
        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) 
              end 
    end 
 
    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 )
 
        end 
    end 
 
    -- 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
        else 
            DBG ("Undefined if in ch_values assignment, Line:94 ")
        end 
 
    end -- for 
                                                    ; DBG ( "now channels are:", ShowTable ("%f" , ch_values) )
    return ch_values[1], ch_values[2], ch_values[3]
 
end 

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:

include "common.lib" ; include "bit.lib"  ; ENABLE_DEBUG = true 
 
AL = {LOW_P_BIT = 3, DRY_RUN_BIT = 2}
 
function main (userId)      
 
    local PAUSE_BEFORE_SW_ON = 10                        -- sec. 
    local C_TMR_ON_DELAY     = R("pumpCircTime") * 3600  -- 30 sec. 
 
    local alerts      = R("alertsRegister")
    local pressure_Ok = not hasbit(alerts, AL.LOW_P_BIT) and not hasbit(alerts, AL.DRY_RUN_BIT)
    local winter      = TRUE('winter_season_flag')
 
    local p1_Ok = NOT("oL_relay")   and TRUE("p1Auto") and  winter and pressure_Ok
    local p2_Ok = NOT("p2OL_relay") and TRUE("p2Auto") and  winter and pressure_Ok
 
    local leadPump = R('abLeadPump')    -- active pump 
 
    if not p1_on_tmr then               -- sw on timers 
        p1_on_tmr = ton_proto.new() ; p2_on_tmr = ton_proto.new() 
    end 
 
    local p1_cmd   = p1_on_tmr(p1_Ok and (leadPump == 1), PAUSE_BEFORE_SW_ON) 
    OUT(p1_cmd, "pumpOnOff")                
 
    local p2_cmd   = p2_on_tmr(p2_Ok and (leadPump == 2), PAUSE_BEFORE_SW_ON) 
    OUT(p2_cmd, "p2OnOff")
 
    DBG("winter alerts pressure_Ok p1_Ok p2_Ok p1_cmd p2_cmd", '|', 
         winter, alerts, pressure_Ok, p1_Ok, p2_Ok, p1_cmd, p2_cmd)
 
 
    local circTmr = Timer(p1_Ok and p2_Ok, C_TMR_ON_DELAY, 0, "pumpCirculationTmr")
 
    if circTmr then 
        leadPump = (leadPump == 1) and 2 or 1 
        RESET("pumpCirculationTmr_out")
    end 
 
    if (p1_Ok and not p2_Ok) then leadPump = 1 end 
    if (p2_Ok and not p1_Ok) then leadPump = 2 end 
    if not (p1_Ok or p2_Ok)  then leadPump = 0 end 
 
    if (p1_Ok and p2_Ok and (leadPump == 0)) then 
        leadPump = 1
    end 
    UpdReg("abLeadPump", leadPump) 
 
end 

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 withoutlimit switches.

function valveControl(v)
 
    local FULL_PATH_TIME = R(v .. "pathTime") ; local KOEF = 100 / FULL_PATH_TIME
    local HOMING_DELAY = R(v .. "homingDelay") * 60 * 60 -- get seconds from hours  
 
    local auto, autoOpenCmd, autoCloseCmd, now = 
          TRUE("auto_mode"), TRUE(v .. "openCmd"), TRUE(v .. "closeCmd"), os.time()
 
local motionTmr = function() -- calc. path quant passed from last call 
 
    local quant = 0
    if (autoOpenCmd or autoCloseCmd) then 
          quant = (now - R(v .. "motionTmr")) * KOEF ; SET(v .. "valveStatus") -- opening 
 
          if autoCloseCmd  then  
              quant = quant * (-1)          -- closing 
              UpdReg(v .. "valveStatus", 2) 
          end 
    else 
          RESET(v .. "valveStatus")
          quant = 0 
    end  -- if motion 
    UpdReg(v .. "motionTmr", now)     -- remember last call 
    return quant
end -- motionTmr 
 
    local pos = Limiter(Round(R(v .. "curPosition")) + motionTmr(), 0, 100)
    local sp = R(v .. "posSetpoint")  
 
    local uShoot, oShoot = (pos < sp), (pos > sp) 
 
    local homingOut = TRUE(v .. "homingTmr_out")
    local homing = Timer(not homingOut, HOMING_DELAY, FULL_PATH_TIME * 2, v .. "homingTmr")
    if not homingOut and homing then 
        AddInfoMessage("Started homing for valve " .. v)
    end 
 
    OUT(auto and uShoot and not homing or (not auto and TRUE(v .. "manOpenCmd")), v .. "openCmd")
    OUT(auto and oShoot or homing or (not auto and TRUE(v .. "manCloseCmd")),  v .. "closeCmd")
    UpdReg(v .. "curPosition", pos)
 
    DBG("auto sp pos autoOpenCmd autoCloseCmd uShoot oShoot ", '|', 
           auto, sp, Round(pos, 1), autoOpenCmd, autoCloseCmd, uShoot, oShoot)
end

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
end
 
-- checks if value within range 
function InRange (min, max, value)
    if (value >= min) and (value <= max) then 
        return true 
    else 
        return false 
    end 
end 
 
-- 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 
end 
 
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 = os.date("*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 
            dateTable.day = arg1
            dateTable.month = arg2 
            dateTable.sec = 0 
 
            if incYearFlag then 
                dateTable.year = dateTable.year + 1 
            end 
 
            result = os.time(dateTable)
             DEBUG("GetTimeFromHHMM going to return this - "..os.date("%c",result))
            return result
    else 
        ERROR("Undefined if in GetTimeFromHHMM")
    end 
 
end -- GetTimeFromHHMM
 
function U(reg, new_value)     -- U = UpdateReg
    local cur_value = R(reg)
 
    if (cur_value ~= nil and new_value ~= nil) then 
 
        if (type(new_value) == 'boolean') then 
            new_value = new_value and 1 or 0         -- to update bit registers from boolean conditions exressions 
        end 
 
        if (new_value ~= cur_value) then 
            W(reg, new_value)
        end 
    else 
        ERROR("Could not update value in reg" .. reg .. ' with a value of ' .. tostring(new_value))
    end 
end 
 
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" 
        else 
            return "negative"
        end 
    end 
            return false -- nothing happened 
end -- P_TRIG 
 
-- multiple ids or aliases read, returns table, have to be unpack() 'ed
-- usage: v1, v2, v3... = unpack(mRead(1, 2, 3, ...))
function mRead(...) 
     local results = {} 
     for i = 1, #arg do 
         local param = arg[i]
        --  INFO("processing " .. param .. " paramter")
          results[i] = GetReg(param)
     end 
     if (#results == 1) then 
         return results[1]
     else 
         return results
     end 
end 

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")  
                                   or 
                          TRUE("halfPeadEnable") and InRange(R("hPeakStart"), R("hPeakEnd"), os.time ())
                                   or 
                          TRUE("enableCheckPumps")) and TRUE("genAutoMode") and NOT("criticalAlert") 
 
  OUT(if_pump1_2_run, "pump1_OnCmd") 
  OUT(if_pump1_2_run, "pump2_OnCmd")  
end

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") 
  end 
 
  Timer(TRUE("button" ), 0 , SELF_MASK_DELAY , "lockTmr") 
 
end

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 rising or falling state of the register var
    -- false for nil input 
    -- previous regoster value stored in previous register (id or alias)
    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" 
        else 
            return "negative"
        end 
    end 
            return false -- nothing happened 
end -- P_TRIG 
 
-- usage example 
function main(userId)
    if P_TRIG("pulseId", "pulsePreviousValue") == "positive" then 
        local counterNewValue = R("counterId") + 1
        W("counterId", counterNewValue)
    end 
end 
 
-- Another version using global callable object
P_TRIG = setmetatable({},  -- callable objects like P_TRIG_FOR_COUNTER ... 
         {__call = function(self, regToTrack) 
                      -- init section 
                      local newValue = R(regToTrack)
                      if (not self.previousValue) then 
                          self.previousValue = newValue  
                          return false 
                      end 
                      -- main logic 
                      local result 
                      if (self.previousValue ~= newValue) then 
                          if (newValue > self.previousValue) then 
                              result = 'rise'
                          else 
                              result = 'fall'
                          end 
                      else 
                          result = false 
                      end 
                      self.previousValue = newValue -- store previous value 
                      return result
                  end } )
 
-- usage 
function main(userId)
    local ptrig = P_TRIG("testReg2") -- only one call in scan is correct!
    if (ptrig == "rise") then 
        DEBUG("rise detected!")
    elseif (ptrig == "fall") then 
        DEBUG("fall detected!")
    else 
        DEBUG("nothing detected!")
    end 
end 

Curve handler

This is the library. For more information about the what it is and how to use it click here: Curves

- curves.lib
function GetCurveValue ( curve_register, x_to_find_y  )
    --[[   
    --Curve handler 
    INPUT:
        Put curve register as first argument and X coordinate as second.
    OUTPUT:
        Get as result status if it is inside curve range of outside(false) and the Y value as second output argument
 
    EXAMPLE:
        curve_status, value = GetCurveValue( "curve_for_current_hour", current_hour )
    --]]
    table = cjson.decode( R ( curve_register ) )
    -- inRangeCurve( table, x_to_find_y )
    for _, value in ipairs( table ) do --piecewise handler inside range of the curve
        if ( value.range[1] <= x_to_find_y and x_to_find_y < value.range[2] ) then 
            -- in_range_status, index_curve_piece, y = true, index, curveLinearCalc( x_to_find_y, value.k, value.b )
            in_range_status, y = true, ( x_to_find_y * value.k + value.b )
            return in_range_status, y
        end
    end
    if  x_to_find_y < table[1].range[1] then -- behaivior for outside left-sided
        -- in_range_status, index_curve_piece, y = false, 1, curveLinearCalc( table[1].range[1], table[1].k*0, table[1].b ) --table[1].b
        in_range_status, index_curve_piece, y = false, 1, ( table[1].range[1] * table[1].k + table[1].b ) --table[1].b
        return in_range_status, y
    elseif x_to_find_y > table[#table].range[2] then -- behaivior for outside right-sided
        -- in_range_status, index_curve_piece, y = false,#table,curveLinearCalc( table[#table].range[2], table[#table].k, table[#table].b ) --table[#table].b
        in_range_status,  y = false,#table,( table[#table].range[2] * table[#table].k + table[#table].b)
        return in_range_status, y
    end
 
    return in_range_status, y 
end

Example of usage:

- curves getter
include "curves.lib"
 
function main (userId)
 
    local t = os.date("*t", os.time()) 
    current_hour = tonumber(t.hour)
    curve_status, y = GetCurveValue( "curve_for_current_hour", current_hour )
    WriteReg("value_for_current_hour", y) 
 
end

Detection of change of state

Sometimes you may need take actions upon changing any of the registers in a set. The following script's idea is to use one function to track changes of the respective registers, looking at their prevous value stored in a global structure (table) which is keeping between script execution.

include "lib.lib"
 
function main (userId)
------------------------ INIT (create globals) ---------------------  
  if (not startedFlag) then  
 
        detect = { curMinute = {regId = 1, prevValue = 0},  -- register's context 
                   stateReg  = {regId = 2, prevValue = 0}   -- which registes to track 
                 }
        mt = {__call = function(self, row)                  -- one method can detect as many registes as set in 
                           local me = self[row]             -- the detect table above 
                           local curValue = R(me.regId)
 
                           if (not curValue) then return false end 
 
                           local result 
                           if (not startedFlag) then 
                               me.prevValue = curValue ; result = false 
                           else 
                               if (R(me.regId) ~= me.prevValue) then 
                                   DEBUG("change of state detected for " .. row)
                                   me.prevValue = curValue ; result = true 
                               else 
                                   me.prevValue = curValue ; result =  false 
                               end 
                            end 
                            return result
                   end
           }
      setmetatable(detect, mt)
      prevAlerts = #GetCurrentAlerts()  
      startedFlag = true  -- avoid this block further
  end 
 ----------------------- MAIN -----------------------
 
     local alerts = GetCurrentAlerts() 
     local alertCount = #alerts
 
     if (alertCount > prevAlerts) or detect("curMinute") or detect("stateReg") then -- run on any of changes 
         -- YOUR CODE FOR ACTIONS MIGHT BE HERE -- 
         local jsonToSend = cjson.encode({stateRegister = R(2), activeAlerts = (#alerts > 0) and 1 or 0})
         AddInfoMessage(jsonToSend)
 
         W(4, jsonToSend) -- to external register 
         W(6, jsonToSend) -- to custom protocol
 
     end 
 
     prevAlerts = alertCount
 
end

Filters

In some projects, due to bad quality of the comm. lines your values can flicker like “3.14” (good one) and “-” (no data). That may confuse the users of the system and spoil graph curves. To filter these values for the safe timeout, you can use the script below. The script shold be set as running on each scan and will do the following:

  1. use the table with pairs of `register to be filetered - its safe copy` and respective counter
  2. if the value is read just copy it to the safe copy,
  3. otherwise, start counting errors for this particular register
  4. write invalid value after a nubmer of failed attempts
READ_ATTEMPTS_BEFORE_ERR     =  10     -- scan count before setting invalid value 
INVALID_VALUE                = -100000 -- invalid value sign 
 
filter = { {reg = 1, safeCopy = 2, errCount = 0},  -- what to filter 
           {reg = 3, safeCopy = 4, errCount = 0} } -- and where to store 
 
function main (userId)
    runFilter(filter)  
end
 
function runFilter(f)
    for _, fstruc in ipairs(f) do 
         local curVal = R(fstruc.reg)
         if (curVal) then 
            W(fstruc.safeCopy, curVal) 
	    fstruc.errCount = 0 
         else 
	     if (fstruc.errCount >= READ_ATTEMPTS_BEFORE_ERR) then
		W(fstruc.safeCopy, INVALID_VALUE)
	     else 
		fstruc.errCount = fstruc.errCount + 1 		
	     end 
         end 
    end 
end 

Processing double float numbers

function main (userId)
 
    local test_hi =  0x3FBF98AD
    local test_lo =  0x0A8FA5FB  
    INFO("d2float = " .. d2float(test_hi, test_lo))
 
end
 
function d2float(hi, lo)  ----------------------- CONVERT DOUBLE FLOAT TO FLOAT ---------------------------
 
	local function getNumberFromTab(tab, start, length)  -- get a number from table 
		local result_str = ""
 
		for i = start, (start + length - 1) do 
			result_str = result_str .. tostring(tab[i])
		end 
		return tonumber(result_str,2)
	end 
	local function dw_to_bits(hex_dw)
 
        local binMapper = {[0] = '0000', [1] = '0001', [2] = '0010', [3] = '0011',
                           [4] = '0100', [5] = '0101', [6] = '0110', [7] = '0111',
                           [8] = '1000', [9] = '1001', [0xA] = '1010', [0xB] = '1011',
                           [0xC] = '1100', [0xD] = '1101', [0xE] = '1110', [0xF] = '1111'
                           }
 
        local hex_str = string.format("%X", hex_dw)
        local len_remainder = 8 - #hex_str
        hex_str = (len_remainder > 0 and string.rep('0', len_remainder) .. hex_str) or hex_str
 
        local out = ''
        for i = 1, #hex_str do 
            local d = tonumber(hex_str:sub(i, i), 16) 
            local tetra  = binMapper[d] 
            out = out .. tetra
        end 
        INFO(out)
        return out 
    end 
 
 
	local NaN = tonumber("11111111111111111111111111111111", 2)
	local result_str = dw_to_bits(hi), dw_to_bits(lo)
	local result_tab = {}
 
	for i = 1, #result_str do 
	    local b = result_str:sub(i, i) 
	    table.insert(result_tab, b)
    end 
 
	local sign, exp, mantissa = 0, 0, 0
	local fraction_table = {} 				        -- fraction part table 
 
	sign = ((result_tab[1] == "1") and -1) or 1     -- get sign 
	exp = getNumberFromTab(result_tab, 2, 11)       -- get exp 
 
	for i = 13, 64 do 
		table.insert(fraction_table, result_tab[i]) -- mantissa
	end 
 
	for j = 1, 52 do 
		if (fraction_table[j]== "1") then 
		mantissa = mantissa +(2 ^(-1 * j))   		-- calc. mantissa by summing individual bits 
		end 
	end 
	mantissa = mantissa + 1
	local result_num = sign * (2 ^ (exp - 1023)) * mantissa
 
	----------------------------------------- exceptions ----------------------------------
	if exp == 0 then -- subnormals
	   result_num = sign*(2^(-1022))*(mantissa-1)
	end 
 
	if exp == 0x7ff then -- nan 
	   result_num = NaN;
	end 
 
   return result_num
end

You can test the example above here