This is an old revision of the document!
Table of Contents
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
Debug printing
Standard functions (INFO and other ) for writing to communication log and lua console has some drawbacks:
- they expect only one parameter of a string type, so you have to concatenate multiple parameters with '..' operator and add spaces between values
- they don't check input parameters for a nil, so the script won't run after the error line if the 'nil' is met in the parameters being concatenated.
To enhance console and communication log output you may use your own versions based on these standard functions:
function DBG(...) local tc = tabToStr function align2s(s1, s2) -- appends space to smaller string local d, s = (#s1 - #s2), ' ' ; local ad = math.abs(d) if (d ~= 0) then local Lp, Rp = 0, 0 if ((ad % 2) == 0) then 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 return (((d > 0) and {s1, s:rep(Lp) .. s2 .. s:rep(Rp)}) or ({s:rep(Lp) .. s1 .. s:rep(Rp), s2})) else return {s1, s2} end end if ENABLE_DEBUG then local arg_str = tc(arg) local t_s_pos = string.find(arg_str, "|") if not t_s_pos then INFO(arg_str) -- Just printing single line else 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 function tabToStr(t) -- glue all and add spaces, use tostring to protect from nil and bool argument local s = "" for i = 1, #t do local e = t[i] ; if (e == nil) then e = 'nil' end s = s .. tostring(e) .. ' ' end -- for return s 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
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.
Universal Timer (TON / TOFF)
The timer compares its current state with the input condition:
- current state = 0, input = 1 - the timer will switch on its output after onDelay time. If the offDelay parameter = 0, then it will be “pure” TON timer
- current state = 1, input = 0 - the timer will switch off its output after offDelay time. If the onDelay parameter = 0, then it will be “pure” TOFF timer
Two registers are needed for this function:
- a register with “timer_name” script alias of a Unix Time type - to store the time stamp of the rising / falling edge on the input
- a register with “timer_name_out” script alias of of a Bit type - to keep timer state between calls (in case of project restart etc. )
Use sensible names for the register script aliases - to associate clearly aliases with the “real” equipment. This will greatly improve code readability. E.g. if you want to use timer to mask some alert condition from pressure sensor #1, you might use “lowP1_AlertTmr”and “lowP1_AlertTmr_out” aliases.
function Timer (bool_input, onDelay, offDelay, tmrAlias) -- bool sec to ON sec to OFF string alias local now , nowString, curTimeStamp, curTmrState = os.time(), 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
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”
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:
CIRCULATION_TIME = 30; -- for tests circulation time is short function main (userId) --[[ if there are no errors, then circulate over time If there is an error on one of the air conditioners, it is excluded from the rotation if there are errors on both, then we stand --]] local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- errro on a/c #1 (DS101@webmi) local switchTime = GetReg("switchTime"); -- next switch over time (DS103@webmi) local now = os.time() local curActiveAC = GetReg("activeAC"); -- active a/c (DS100@webmi) if (not acError1) and (not acError2) then -- work on circulation if (now >= switchTime) then if (curActiveAC == 1) then WriteReg("activeAC", 2); else WriteReg("activeAC", 1); end WriteReg("switchTime", now + CIRCULATION_TIME); end elseif acError1 and (not acError2) then WriteReg("activeAC", 2); elseif acError2 and (not acError1) then WriteReg("activeAC", 1); else WriteReg("activeAC", 0); end -- if no errors end -- main
For simplicity and clarity, it is better to split the scripts into functional modules that can be quickly analyzed and placed in the right order in the program list. The first script looks at the errors and if they do not exist, the air conditioners alternate in time
The second script check which conditioner is now active, and performs the necessary actions. In a script, this is just debugging, but there may be commands for controlling the infrared transmitter for issuing the desired command, writing to the message log and switching, etc.
function main (userId) --[[ turn on selected a/c depending on pointer --]] local pointer = GetReg("activeAC"); -- active conditioner (DS100@webmi) if (pointer==0) then DEBUG("all off") return 0 elseif (pointer==1) then DEBUG("turn on a/c #1"); else DEBUG("turn on a/c #2"); end -- if 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 UpdateReg(reg, value) -- avoids unnecessary writing to a register (write on change) if R(reg) ~= value then -- then use shortcut WriteReg = UpdateReg, then all WriteReg fs become on demand W(reg, value) else -- DEBUG("won't rewrite thie register") 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 risging or falling state of the var -- false for nil input -- prev. var value stored in previous alias register DEBUG("P_TRIG was called with this " .. var .. " " ..previous) local curVar = R(var) if not curVar then return false end local prevVar = R(previous) ; DEBUG(" p_trig " .. curVar .. " " .. prevVar) W(previous, curVar) if (curVar ~= prevVar) then if (curVar > prevVar) then return "positive" else return "negative" end end return false -- nothing happened end -- P_TRIG
Curve handler
Library
- - 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
Processing double float numbers
function d2float(n) ----------------------- CONVERT DOUBLE FLOAT TO FLOAT --------------------------- local function getBits(input_num, length) -- get number as table of bits -- works with number and len local tab = {} local max_i = length - 1 local remainder = input_num -- rem of bitwise weighing for i = max_i, 0, -1 do local bit_ = (remainder - 2^i >= 0) and '1' or '0' table.insert(tab, bit_) end return tab end 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 NaN = tonumber("11111111111111111111111111111111", 2) local result_tab = {} -- get a table of "0" & "1" if (type(n) == "number") then result_tab = getBits(n, 64) elseif (type(n) == "table") then local tmpS = '' for i = 1, #n do tmpS = tmpS .. table.concat(getBits(n[i],8)) end for j=1, #tmpS do table.insert(result_tab, string.sub(tmpS, j, j)) end else ERROR("Unknown type for the d2float!") 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