User Tools

Site Tools


useful_programs

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
useful_programs [2020/06/03 12:01] – [Other handy functions] emozolyakuseful_programs [2024/03/19 09:39] (current) – [Detection of change of state] emozolyak
Line 1: Line 1:
 ====== Useful programs ====== ====== Useful programs ======
 +
 +
  
 ===== Bit functions ===== ===== Bit functions =====
Line 131: Line 133:
  
 </code> </code>
-===== Debug printing ===== 
  
-Standard functions (INFO and [[ http://docs.webhmi.com.ua/scripts#writing_to_communication_log | 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:+==== 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. 
  
 <code lua> <code lua>
-function DBG(...) +-- sets bit in certain register depending on condition  
-local tc tabToStr+include "bit.lib"  
 + 
 +function main (userId) 
 +    outBit(not R("someRegister"), "alertMask", 15) 
 +end 
 +</code> 
 + 
 + 
 + 
 + 
 +===== Table functions ===== 
 + 
 +<code lua> 
 +-- 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) 
    
-function align2s(s1s2) -- appends space to smaller string  +    for tabIndex, _ in ipairs(t) do  
-    local d, s = (#s1 #s2), ' ' ; local ad math.abs(d)+        local matchFlag = true  
 +  
 +        for seqIndexsequenceByte in ipairs(pttrndo  -- 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 kv 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  
 + 
 +</code> 
 + 
 +===== Compare and trigger ===== 
 + 
 +This is used to detect when the value reach certaint value and make one-time action.  
 +<code lua> 
 +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 (d ~= 0) then  +    if helper(regValue, self.level) then  
-         +        if (not self.flag) then  
-        local Lp, Rp = 0, 0 +            self.flag true  
-         +            DEBUG("got it !") 
-        if ((ad % 2) == 0) then  +            return true 
-            Lp ad / 2 ; Rp = Lp ; INFO("even parts, L R = .. Lp .. s .. Rp) +
-        else  +
-            Lp = math.floor(ad / 2) ; Rp = Lp + 1 ; INFO("not even L R " .. Lp .. s .. Rp)+
         end          end 
-        return (((d > 0) and {s1, s:rep(Lp) .. s2 .. s:rep(Rp)})  
-                        or 
-             ({s:rep(Lp) .. s1 .. s:rep(Rp), s2})) 
     else      else 
-        return {s1, s2}+        self.flag = false 
     end      end 
 +    
 +    return false     
 end  end 
  
-    if ENABLE_DEBUG then  +function greaterThan(arg1arg2
-        local arg_str = tc(arg) +    return arg1 >arg2 
-        local t_s_pos = string.find(arg_str"|")  +end  
-  + 
-        if not t_s_pos then  +function main (userId
-            INFO(arg_str)  -- Just printing single line  +    if (not detect) then  
-        else  +       detect = cmp new ("dBit2"1)
-            local h_row, v_row {}, {}  +
-            -- header  +
-            for w in string.gmatch(arg[1], "%S+") do +
-                h_row[#h_row + 1] = w       +
-            end  +
-            -- values  +
-            for i = 3, #arg do  +
-                v_row[#v_row + 1] = tostring(arg[i]+
-            end  +
-  +
-            if (#h_row ~= #v_row) then  +
-                ERROR("Inconsistent header and value rows in DBG!"+
-                INFO("h_row:" .. tc(h_row)) ; INFO("v_row.. tc("v_row")) +
-                return  +
-            else  +
-                for g = 1, #h_row do  +
-                    h_row[g], v_row[g] = unpack(align2s(h_row[g], v_row[g])+
-                end  +
-            end  +
-            INFO(tc(h_row)) ; INFO(tc(v_row)) +
-        end    +
     end      end 
-end  +     
 +    if detect(greaterThan) then  
 +       -- SendTelegramMessage() 
 +    end  
 +end 
 +</code>
    
-function tabToStr(t)  -- glue all and add spaces, use tostring to protect from nil and bool argument  +===== Iteraion over indexed registers ===== 
-    local s = ""             + 
-    for i = 1#t do  +You can iterate your register in loogs using their alias names, which contain the index, e.g.: 
-        local e = t[i; if (e == nilthen e = 'nil' end  + 
-        s .tostring(e) .. ' '  +<code lua> 
-    end -- for  +--[[ 
-    return s +    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 end
 +</code>
  
 +The result is shown on the following picture: 
 +
 +{{ :lua:iterating_regs_demo.gif |}}
 +
 +===== Debug printing =====
 +
 +Standard functions (INFO and [[ http://docs.webhmi.com.ua/scripts#writing_to_communication_log | 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:
 +
 +<code lua>
 +--[[  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 
 </code> </code>
 Then the print output can be enhanced like this: Then the print output can be enhanced like this:
-{{ :dbg_table_out.png?direct&600 |}}+{{ network:dbg_table_out.png?direct&600 |}}
  
 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.  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. 
Line 248: Line 452:
 end -- tprint  end -- tprint 
 </code> </code>
 +
 +===== Date & Time operations =====
 +When you need to select different date - time numbers, you can use the following example: 
 +
 +<code lua>
 +function main (userId)
 +  
 +  local dateTimeTable = os.date("*t", os.time())
 +  
 +  INFO(dateTimeTable.hour)
 +  INFO(dateTimeTable.min)
 +  
 +end
 +</code>
 +
 +<code lua>
 +-- converting utc timestamp in human readable string
 +function getDateString(utc)
 +    return os.date("%c", utc)
 +end 
 +</code>
 +
 +Please refer to the [[ https://www.lua.org/pil/22.1.html| Lua documentation site]].
  
 ===== Running hours meter ===== ===== Running hours meter =====
Line 282: Line 509:
   * **runHours_condition** - to test how the function works from the interface this register is used. Type - Bit.   * **runHours_condition** - to test how the function works from the interface this register is used. Type - Bit.
  
-{{::run_hours_regs.gif?direct}}+{{network:run_hours_regs.gif?direct}}
  
  
-===== Universal Timer (TON / TOFF)=====+===== 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 ==== 
 +<code lua> 
 +-- 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  
 +</code> 
 + 
 +==== Simple TOFF timer ==== 
 + 
 +<code lua> 
 +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 
 +</code> 
 + 
 +==== Universal Timer (TON / TOFF) ====
 The timer compares its current state with the input condition: The timer compares its current state with the input condition:
  
Line 291: Line 583:
   * 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    * 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:+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" 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. )   * 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. )
Line 343: Line 635:
   Note: This function should be called in every scan, i.e. don't put it inside "ifs", otherwise it won't update the    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.   timestamp and may not work properly.
 +
 +
  
 ===== Generating alert messages with time - delay ===== ===== Generating alert messages with time - delay =====
Line 369: Line 663:
  
 </code> </code>
 +
 +This one uses simper timer: 
 +<code lua>
 +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 
 +
 +</code>
 +
 +===== 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 : 
 +
 +<code lua>
 +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
 +
 +</code>
 +
  
 ===== Moving average ===== ===== Moving average =====
Line 412: Line 794:
   * accumulator for the sum, e.g. "//meanOutsideT_sum//"   * accumulator for the sum, e.g. "//meanOutsideT_sum//"
   * current queue index "//meanOutsideT_fill//"   * current queue index "//meanOutsideT_fill//"
 +
 +===== ON-OFF control =====
 +
 +==== Function based  ====
 +
 +<code lua>
 +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
 +</code>
 +
 +==== Object programming style ====
 +
 +<code lua>
 +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
 +</code>
 +
 +==== Both examples use the following library ====
 +<code lua>
 +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 
 +</code>
 +
 +Another way of creating objects: 
 +<code lua>
 +TimerConstructor = function () 
 +return setmetatable({}, {
 +        __call = function(self, delay) 
 +                local now = os.time() 
 +                if (not self.stamp or now - self.stamp >= delay) then 
 +                    self.stamp = now 
 +                    return true 
 +                else 
 +                    return false 
 +                end 
 +             end 
 +}) end  
 +
 +
 +-- usage  
 +
 +function main (userId)
 +    --  creating timer   
 +    if (not timer) then 
 +        timer = TimerConstructor()
 +    end 
 +    
 +    if timer (60) then 
 +        INFO('timer!')
 +        for i = 1, 8 do 
 +            toggle('synapse.do' .. i)
 +        end 
 +    end
 +end
 +
 +function toggle(v)
 +    W(v, 1 - R(v))
 +end 
 +</code>
 +
  
 ===== PID - control ===== ===== PID - control =====
Line 559: Line 1101:
  
 <code lua> <code lua>
-CIRCULATION_TIME = 30-- for tests circulation time is short +include "common.lib" include "bit.lib"  ; ENABLE_DEBUG = true 
  
-function main (userId) +AL {LOW_P_BIT 3DRY_RUN_BIT 2}
-  --[[ +
-  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)+
  
 +function main (userId)      
 +    
 +    local PAUSE_BEFORE_SW_ON = 10                        -- sec. 
 +    local C_TMR_ON_DELAY     = R("pumpCircTime") * 3600  -- 30 sec. 
  
-if (not acError1) and (not acError2then  +    local alerts      = R("alertsRegister"
-    -- work on circulation +    local pressure_Ok = not hasbit(alerts, AL.LOW_P_BIT) and not hasbit(alerts, AL.DRY_RUN_BIT
-    if (now >switchTime) then  +    local winter      TRUE('winter_season_flag'
-        if (curActiveAC == 1then  +     
-            WriteReg("activeAC", 2); +    local p1_Ok = NOT("oL_relay"  and TRUE("p1Auto"and  winter and pressure_Ok 
-        else  +    local p2_Ok = NOT("p2OL_relay") and TRUE("p2Auto"and  winter and pressure_Ok 
-            WriteReg("activeAC", 1)+     
-        end +    local leadPump = R('abLeadPump'   -- active pump 
-        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 +    if not p1_on_tmr then               -- sw on timers  
-</code> +        p1_on_tmr = ton_proto.new() ; p2_on_tmr = ton_proto.new()  
- +    end  
-For simplicity and clarityit 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 existthe air conditioners alternate in time +     
- +    local p1_cmd   = p1_on_tmr(p1_Ok and (leadPump == 1)PAUSE_BEFORE_SW_ON)  
-The second script check which conditioner is now activeand performs the necessary actions. In a scriptthis is just debuggingbut there may be commands for controlling the infrared transmitter for issuing the desired commandwriting to the message log and switchingetc. +    OUT(p1_cmd, "pumpOnOff"                
- +     
-<code lua> +    local p2_cmd   = p2_on_tmr(p2_Ok and (leadPump == 2)PAUSE_BEFORE_SW_ON)  
-function main (userId+    OUT(p2_cmd, "p2OnOff") 
-  --[[ +     
-     turn on selected a/c depending on pointer +    DBG("winter alerts pressure_Ok p1_Ok p2_Ok p1_cmd p2_cmd"'|' 
-  --]] +         winteralertspressure_Okp1_Ok, p2_Ok, p1_cmd, p2_cmd) 
-  local pointer GetReg("activeAC"); -- active conditioner (DS100@webmi+     
- +     
- +    local circTmr = Timer(p1_Ok and p2_Ok, C_TMR_ON_DELAY, 0, "pumpCirculationTmr"
-if (pointer==0) then  +     
-      DEBUG("all off"+    if circTmr then  
-      return +        leadPump (leadPump == 1) and 2 or 1  
-  elseif  +        RESET("pumpCirculationTmr_out") 
-      (pointer==1) then  +    end  
-          DEBUG("turn on a/c #1"); +     
-  else  +    if (p1_Ok and not p2_Ok) then leadPump = 1 end  
-          DEBUG("turn on a/c #2"); +    if (p2_Ok and not p1_Okthen leadPump = 2 end  
-       +    if not (p1_Ok or p2_Ok)  then leadPump = end  
-  end -- if  +     
-end+    if (p1_Ok and p2_Ok and (leadPump == 0)) then  
 +        leadPump = 
 +    end  
 +    UpdReg("abLeadPump", leadPump)  
 +     
 +end 
 </code> </code>
  
Line 739: Line 1272:
 end -- GetTimeFromHHMM end -- GetTimeFromHHMM
  
-function UpdateReg(reg, value) -- avoids unnecessary writing to a register (write on change) +function U(reg, new_value    -- U = UpdateReg 
-    if R(reg) ~= value then    -- then use shortcut WriteReg UpdateReg, then all WriteReg fs become on demand +    local cur_value = R(reg) 
-        W(reg, value)+     
 +    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      else 
-        -- DEBUG("won't rewrite thie register")+        ERROR("Could not update value in reg.. reg .. ' with a value of ' .. tostring(new_value))
     end      end 
 end  end 
Line 795: Line 1337:
 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: 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:
  
-{{ ::pump1-2_condition.png?direct&700 |}}+{{ network:pump1-2_condition.png?direct&700 |}}
  
 The same logic could be written like this -  The same logic could be written like this - 
Line 819: Line 1361:
 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: 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:
  
-{{ ::self_mask_ladder.png?direct&700 |}}+{{ network:self_mask_ladder.png?direct&700 |}}
  
 Using handy lua functions from above, it could be written in this way: Using handy lua functions from above, it could be written in this way:
Line 844: Line 1386:
 The video how this code works on registers: The video how this code works on registers:
  
-{{ ::self_lock_demo.gif |}}+{{ network:self_lock_demo.gif |}}
  
 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): 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):
  
-{{ ::messages_one_shot.png?direct&814 |}}+{{ network:messages_one_shot.png?direct&814 |}}
  
 ==== Detecting rise / fall edge of the signals  ==== ==== Detecting rise / fall edge of the signals  ====
Line 854: Line 1396:
 <code lua> <code lua>
 function P_TRIG(var, previous) function P_TRIG(var, previous)
-    -- detect risging or falling state of the var+    -- detect rising or falling state of the register var
     -- false for nil input      -- false for nil input 
-    -- prev. var value stored in previous alias register +    -- previous regoster value stored in previous register (id or alias)
     DEBUG("P_TRIG was called with this " .. var .. " " ..previous)     DEBUG("P_TRIG was called with this " .. var .. " " ..previous)
    
Line 875: Line 1417:
             return false -- nothing happened              return false -- nothing happened 
 end -- P_TRIG  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 
 </code> </code>
  
 +===== Curve handler =====
 +This is the library. For more information about the what it is and how to use it click here: [[curves|Curves]]
 +<code lua - 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,
 +end
 +</code>
 +Example of usage:
 +<code lua - 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
 +</code>
 +
 +===== Detection of change of state =====
 +
 +Sometimes you may need take actions upon changing any of the registers in a set. The following function returns true at the moment (scan) the value differs from previous value or false if no changes. Also, it can call callback function to make your code more readable.
 +
 +<code lua>
 +OnChange = {} ; setmetatable(OnChange, {__call = function(self) 
 +                                    return function(v, cb) 
 +                                          local out = false 
 +                                          if (not self.prev) then
 +                                              self.prev = v 
 +                                          end 
 +                                          if (self.prev and v ~= self.prev ) then 
 +                                              out = true 
 +                                              if cb then cb() end 
 +                                          end 
 +                                          self.prev = v 
 +                                          return out 
 +                                    end 
 +end 
 +})
 +
 +function main (userId)
 +  if (not onchange1) then 
 +      onchange1 = OnChange()
 +  end 
 +  
 +  onchange1(R(110), function() INFO("true!") end )
 +    ------- Detecting changes of a set of registers ----------------
 +  local regSet = {{110, function() 
 +                            INFO("I'm a callback for reg 110")
 +                    end },
 +                   {1, function() 
 +                       INFO("I'm a callback for reg 1")
 +                    end }, 
 +                    {200, function() 
 +                        INFO("I'm a callback for reg 200")
 +                    end }, 
 +                 
 +   }
 +  
 +  local handlers = {} 
 +  
 +  -- registereing handlers 
 +  if (not handlers[regSet[1][1]]) then 
 +      for _, s in ipairs(regSet) do
 +          handlers[s[1]] = OnChange()
 +      end 
 +  end 
 +   
 +  -- using  handlers 
 +  for _, s in ipairs(regSet) do
 +      local reg, func   = s[1], s[2]
 +      handlers[reg](R(reg), func())    
 +  end 
 +end
 +</code>
 +
 +
 +===== 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:
 +  - use the table with pairs of `register to be filetered - its safe copy` and respective counter
 +  - if the value is read just copy it to the safe copy, 
 +  - otherwise, start counting errors for this particular register
 +  - write invalid value after a nubmer of failed attempts
 +
 +<code lua>
 +READ_ATTEMPTS_BEFORE_ERR      10     -- scan count before setting invalid value 
 +INVALID_VALUE                = -100000 -- invalid value sign 
 + 
 +filter = { {reg = 1, safeCopy = 2, errCount = 0},  -- reg - source register 
 +           {reg = 3, safeCopy = 4, errCount = 0} } -- safeCopy - last read ok value 
 + 
 +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 
 +</code>
 +
 +===== Processing double float numbers =====
 +
 +<code lua>
 +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 
 +
 +</code>
 +
 +You can test the example above [[https://www.binaryconvert.com/result_double.html?decimal=048046049050051052050051052|here]]
 +
 +{{network:d2float_test_screen.png?800|}}
 +
 +{{network:d2float_test_scree2.png?800|}}
  
useful_programs.1591185698.txt.gz · Last modified: 2020/06/03 12:01 by emozolyak

Donate Powered by PHP Valid HTML5 Valid CSS Driven by DokuWiki