Полезные советы/en — различия между версиями
(Новая страница: «-- CHECKING CONDITION ---------- if not startBit then tmrStartTime = now -- here start time is stored WriteReg("TON_out", 0) -- timer output bit register…») |
|||
(не показано 38 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
== Useful programs == | == Useful programs == | ||
=== Running script upon rising or falling edge of the signal === | === Running script upon rising or falling edge of the signal === | ||
+ | <languages/> | ||
The script should be defined as running upon register change. | The script should be defined as running upon register change. | ||
Inside the script, there have to be checking of the current state and espective actions on rising edge (current state = 1) or falling edge (=0). | Inside the script, there have to be checking of the current state and espective actions on rising edge (current state = 1) or falling edge (=0). | ||
Строка 33: | Строка 34: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === | + | === Alarming (sound, relya, sms, viber, telegram) about connection error === |
− | + | It is possible to analyze the scan time by the script and if it exceeds the acceptable limit, signal it in different ways. Below is an example of processing a large scan time with signaling to the message buffer and Viber. | |
<syntaxhighlight lang= "lua"> | <syntaxhighlight lang= "lua"> | ||
− | cntdownFlag = false | + | cntdownFlag = false -- countdown flag |
− | timeStmp = 0 | + | timeStmp = 0 -- метка времени time stamp |
− | msgSent = false; -- | + | msgSent = false; -- message sent flag |
function main (userId) | function main (userId) | ||
− | -- | + | -- reading inputs |
− | local scan = GetReg(34); -- | + | local scan = GetReg(34); -- scan time |
− | local c0 = GetReg(42); -- | + | local c0 = GetReg(42); -- bad connection number |
− | local SCANLIMIT = GetReg(886); -- | + | local SCANLIMIT = GetReg(886); -- scan time limit |
− | local SCANDELAY = GetReg(887); -- | + | local SCANDELAY = GetReg(887); -- error reaction delay |
-- | -- | ||
if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then | if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then | ||
Строка 53: | Строка 54: | ||
return 0; | return 0; | ||
end | end | ||
− | -- | + | -- reading time |
− | local now = os.time() | + | local now = os.time() |
-- шаблон сообщения | -- шаблон сообщения | ||
− | local msg1 = " | + | local msg1 = "Scan time long "..tostring(scan).." ".."ms, error in conn. "..tostring(c0); |
-- | -- | ||
− | if (scan > SCANLIMIT) then -- | + | if (scan > SCANLIMIT) then -- scan above limit |
if not cntdownFlag then | if not cntdownFlag then | ||
− | cntdownFlag = true | + | cntdownFlag = true |
− | timeStmp = now | + | timeStmp = now |
else | else | ||
if (now - timeStmp) > SCANDELAY then | if (now - timeStmp) > SCANDELAY then | ||
if not msgSent then | if not msgSent then | ||
− | AddAlertMessage(msg1) | + | AddAlertMessage(msg1) |
− | SendViberMessage(398044391, msg1) | + | SendViberMessage(398044391, msg1) |
− | + | msgSent = true | |
− | + | ||
− | msgSent = true | + | |
end | end | ||
end | end | ||
end | end | ||
− | else -- | + | else -- scan is normal |
if (cntdownFlag == true) and msgSent then | if (cntdownFlag == true) and msgSent then | ||
− | AddInfoMessage(" | + | AddInfoMessage("Scan is normal now"); |
− | SendViberMessage(398044391, " | + | SendViberMessage(398044391, "Scan is normal now"); |
− | + | msgSent = false | |
− | msgSent = false | + | |
end | end | ||
− | cntdownFlag = false | + | cntdownFlag = false |
end | end | ||
Строка 87: | Строка 85: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | In WebHMI there is a buzzer for the sound signal, and 2 output relays, which can be controlled for signaling (send a signal to the signal column or the PLC about the problem). | |
− | === | + | === Moving average === |
− | + | The moving average is useful for smoothing the values of parameters that have noises, pulsations. | |
− | + | Algorithm of the moving average: <br> | |
− | + | at the beginning of the filtering, first N values of sample 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. | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | -- | + | -- globals |
− | mav_len = 20; -- | + | mav_len = 20; -- queue length |
− | queue_fill = 0; -- | + | queue_fill = 0; -- queue filling index |
− | av_sum = 0; -- | + | av_sum = 0; -- accumulator for the moving average |
function main (userId) | function main (userId) | ||
− | local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- | + | local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- reading parameter |
− | if (queue_fill < mav_len) then -- | + | if (queue_fill < mav_len) then -- queue is not filled |
− | av_sum = av_sum + in_value; -- | + | av_sum = av_sum + in_value; -- accumulating sum |
− | queue_fill = queue_fill +1; -- | + | queue_fill = queue_fill +1; -- and index |
− | else -- | + | else -- queue is full |
− | tmp_var = av_sum / mav_len; -- | + | tmp_var = av_sum / mav_len; -- store one element |
− | av_sum = av_sum - tmp_var + in_value; -- | + | av_sum = av_sum - tmp_var + in_value; -- subtract and add new |
end | end | ||
-- | -- | ||
if (queue_fill == mav_len ) then | if (queue_fill == mav_len ) then | ||
− | out_value = av_sum / mav_len; -- | + | out_value = av_sum / mav_len; -- calc. moving average |
else | else | ||
− | out_value = av_sum / queue_fill; -- | + | out_value = av_sum / queue_fill; -- mean average |
end | end | ||
− | WriteReg("Tout_mav", out_value); -- | + | WriteReg("Tout_mav", out_value); -- outside temperature |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === | + | === PID - control === |
− | + | An example of implementing a PID controller in WebHMI: | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
Строка 215: | Строка 213: | ||
end -- main | end -- main | ||
− | -- | + | -- rounding |
function Round(var) | function Round(var) | ||
Строка 243: | Строка 241: | ||
if (i == nil) then | if (i == nil) then | ||
− | break -- | + | break -- not found |
end | end | ||
-- найдено | -- найдено | ||
if (tmp ~= "0") then | if (tmp ~= "0") then | ||
− | |||
if (tmp == tostring(thisScriptID)) then | if (tmp == tostring(thisScriptID)) then | ||
DEBUG(str) | DEBUG(str) | ||
Строка 259: | Строка 256: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | 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) | |
− | === | + | === Running hour meter === |
− | + | The hour meter is convenient for automatically generating a message about the need for maintenance for the equipment unit, changing the master pump in group to equalize the operating time, and so on. | |
− | + | An example of the implementation of the running hour meter on Lua in WebHMI (the program runs in each scan): | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | -- | + | -- globals |
− | run_state = false; -- | + | run_state = false; -- to store current state |
function main (userId) | function main (userId) | ||
− | -- | + | -- locals |
− | local check_mask = tonumber("0000100000000000",2); -- | + | local check_mask = tonumber("0000100000000000",2); -- the mask to check running state bit in status register of the freq. drive FC 51 Danfoss |
− | local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- | + | local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- checking result as a bool var |
− | local now = os.time(); -- | + | local now = os.time(); -- current system time |
− | local time_diff = 0; -- | + | local time_diff = 0; -- time difference between calls |
− | -- | + | -- catching rising edge of the event |
if (not run_state) and run_status then | if (not run_state) and run_status then | ||
− | WriteReg("P43StartTime", now); -- | + | WriteReg("P43StartTime", now); -- Drive start time №43 |
− | -- | + | -- counting time |
if run_state then | if run_state then | ||
− | time_diff = (now - GetReg("P43StartTime")); -- | + | time_diff = (now - GetReg("P43StartTime")); -- get time difference |
− | WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- | + | WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- increase running hour meter |
− | WriteReg("P43StartTime", now); -- | + | WriteReg("P43StartTime", now); -- overwrite start time stamp |
end | end | ||
− | run_state = run_status | + | run_state = run_status |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | The timekeeping registers and time stamps should be made non-volatile. | |
− | === | + | === 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 running hours count. | |
− | + | 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: | |
[[Файл:Circ algorithm regs.png | 800 px|left]] <br clear = all> | [[Файл:Circ algorithm regs.png | 800 px|left]] <br clear = all> | ||
− | + | For simplicity and clarity, it is better to split the scripts into functional modules that can be quickly analyzed and placed in the desired order in the program list. | |
− | + | The first script looks at the errors and if they do not exist, the units (air conditioners) alternate in time. | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | CIRCULATION_TIME = 30; -- | + | CIRCULATION_TIME = 30; -- for testing purposed circulation time = 30 sec. |
function main (userId) | function main (userId) | ||
Строка 308: | Строка 305: | ||
если есть ошибка на одном из кондиционеров, то он исключается из чередования | если есть ошибка на одном из кондиционеров, то он исключается из чередования | ||
если ошибки на обоих, то стоим на месте | если ошибки на обоих, то стоим на месте | ||
+ | if there are no errors, then alternate the work on the time of circulation | ||
+ | If there is an error on one of the air conditioners, it is excluded from the circulation | ||
+ | if there are errors on both, then we stand in place | ||
--]] | --]] | ||
− | local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- | + | local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- error on a/c #1 |
− | local switchTime = GetReg("switchTime"); -- | + | local switchTime = GetReg("switchTime"); -- next switch over time (DS103@webmi) |
− | local now = os.time() | + | local now = os.time() |
− | local curActiveAC = GetReg("activeAC") | + | local curActiveAC = GetReg("activeAC") -- active a/c (DS100@webmi) |
if (not acError1) and (not acError2) then | if (not acError1) and (not acError2) then | ||
− | -- | + | -- working with circulation |
if (now >= switchTime) then | if (now >= switchTime) then | ||
if (curActiveAC == 1) then | if (curActiveAC == 1) then | ||
Строка 335: | Строка 335: | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | The second script looks at which a/c 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. | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
function main (userId) | function main (userId) | ||
--[[ | --[[ | ||
− | + | turn selected a/c on dependting on pointer | |
--]] | --]] | ||
− | local pointer = GetReg("activeAC"); -- | + | local pointer = GetReg("activeAC"); -- active a/c (DS100@webmi) |
if (pointer==0) then | if (pointer==0) then | ||
− | DEBUG(" | + | DEBUG("all off "); |
− | return 0 | + | return 0 |
elseif | elseif | ||
(pointer==1) then | (pointer==1) then | ||
− | DEBUG(" | + | DEBUG("turn on a/c #1") |
else | else | ||
− | DEBUG(" | + | DEBUG("turn on a/c #2") |
end -- if | end -- if | ||
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | + | Also here you need a script that will rise error flags upon on certain conditions, read the status of the protection devices, the error registers on the interface, and so on. | |
− | === 3- | + | === 3-point control for a valve or servo === |
− | 3- | + | 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 full valve travel time), initializes the coordinate, and then runs through the specified position. |
− | <p> | + | <p> To determine intermediate positions, a calculated value is used, determined from the characteristics of the actuator's 'full stroke time' , which can also be determined experimentally. |
− | + | As mentioned above, relatively complex and branched algorithms in WebHMI are better to divide into functionally complete and simple programs, the interaction and synchronization between which can be done using internal registers. </p> | |
<br> | <br> | ||
− | + | Below one of the examples for 3-point valve control with 2 limit switches is provided | |
− | + | The program split into 6 parts: | |
[[Файл:3-point control.png|800px|left]] | [[Файл:3-point control.png|800px|left]] | ||
<br clear = all> | <br clear = all> | ||
− | ::'''v3 OpenClose Valve Manual''' - | + | ::'''v3 OpenClose Valve Manual''' - The script controls the drive in manual mode. Started by changing the number of the pressed button from the dashboard |
− | ::'''v4 Auto Valve Control''' - | + | ::'''v4 Auto Valve Control''' - the main control script in the auto mode. It automatically performs the first initialization, and if the specified coordinate does not match the current one, it turns the drive in the desired direction, upon reaching the position, stops. Also, if the specified position is the same as the limit positions (0,100), the script continues to hold the command until the physical limit switch reached, thereby performing a periodic synchronization of the calculated position with the real position. |
− | ::'''v5 LatchStart_Time''' - | + | ::'''v5 LatchStart_Time''' - script 'capturing' the initial position of the movement, is executed by changing the flag inMotionFlag, which is set by the previous programs v3 or v4. |
− | ::'''v6 CalcNewPosition''' - | + | ::'''v6 CalcNewPosition''' - is executed 1 time per second. (on system time change), the script works as long as there is an inMotionFlag flag and re-counts the current time passed from the beginning of the movement to the current position (which in turn is used by the v4 program). |
− | ::'''v7 Limit sw reaction''' - | + | ::'''v7 Limit sw reaction''' -When the limit switches are reached, it removes commands, and also removes the request from the manual control buttons. |
− | ::'''v8 Drop Cur Cmd on Manual mode''' - | + | ::'''v8 Drop Cur Cmd on Manual mode''' - when switching to manual mode, turns off the current command. |
− | + | At the beginning of the program names, the order of the required execution order (v3..v8) is indicated, since the necessary order of program execution can change undesirably, for example, when sorting programs and inaccurate 'dragging' programs in the list. Thus, the prefix reminds you of the desired order, it can also reflect the functionality of the script - 'v' Valves, 't' - temperature control, etc. It is more convenient to navigate in large lists and refer to it. | |
− | <p> | + | <p> Text of the programs:</p> |
'''v3''' | '''v3''' | ||
<syntaxhighlight lang = lua> | <syntaxhighlight lang = lua> | ||
− | ------- | + | ------- MANUAL CONTROL FROM SCREEN BUTTONS OpenClose Valve Manual ------------------- |
function main (userId) | function main (userId) | ||
− | local button_value = GetReg("valveMan_code"); -- | + | local button_value = GetReg("valveMan_code"); -- key code from buttons open / close |
− | local manual_mode = (GetReg("auto_mode") == 0) ; -- | + | local manual_mode = (GetReg("auto_mode") == 0) ; -- auto mode is on (ds1176@WH Global) |
if manual_mode then | if manual_mode then | ||
if (button_value == 10) then | if (button_value == 10) then | ||
− | WriteReg("openCmd", 1); -- | + | WriteReg("openCmd", 1); -- open command (DS1007@WH Valve control) |
WriteReg("closeCmd", 0); | WriteReg("closeCmd", 0); | ||
elseif (button_value == 5) then | elseif (button_value == 5) then | ||
− | WriteReg("closeCmd", 1); -- | + | WriteReg("closeCmd", 1); -- close command (DS1008@WH Valve control) |
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
else | else | ||
− | -- | + | -- invalid value - switch every command off |
DEBUG("Read button value as "..tostring(button_value)); | DEBUG("Read button value as "..tostring(button_value)); | ||
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
− | WriteReg("closeCmd", 0) | + | WriteReg("closeCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end --if | end --if | ||
− | -- | + | -- capture start position and start time |
if (button_value == 10) or (button_value == 5) then | if (button_value == 10) or (button_value == 5) then | ||
− | WriteReg("startPosition", GetReg("curPosition")); -- | + | WriteReg("startPosition", GetReg("curPosition")); -- starting position (DS1020@WebHMI 3point control) |
− | WriteReg("startPosTime", os.time()); -- | + | WriteReg("startPosTime", os.time()); -- start time stamp (DS1012@WebHMI 3point control) |
− | WriteReg("inMotion_flag",1) | + | WriteReg("inMotion_flag",1) |
end | end | ||
end -- manual_mode | end -- manual_mode | ||
Строка 415: | Строка 415: | ||
'''v4''' | '''v4''' | ||
<syntaxhighlight lang = lua> | <syntaxhighlight lang = lua> | ||
− | -- | + | -- AUTO VALVE CONTROL ---- |
function main (userId) | function main (userId) | ||
− | local now = os.time(); -- | + | local now = os.time(); -- current time |
− | local timeStmp = GetReg("endPosTime"); -- | + | local timeStmp = GetReg("endPosTime"); -- end position time stamp (D1001@WebHMI Kitothemr) |
− | local auto_mode = (GetReg("auto_mode")==1) | + | local auto_mode = (GetReg("auto_mode")==1) |
if auto_mode then | if auto_mode then | ||
− | -- | + | -- check if very first run = 0 |
if (timeStmp == 0) and (not home_mode) then | if (timeStmp == 0) and (not home_mode) then | ||
− | WriteReg("homingBit",1) | + | WriteReg("homingBit",1) |
DEBUG("timeStmp = 0, homing needed! "); | DEBUG("timeStmp = 0, homing needed! "); | ||
end | end | ||
− | local home_mode = (GetReg("homingBit") == 1); -- | + | local home_mode = (GetReg("homingBit") == 1); -- homing bit (D1006@WebHMI Kitothemr) |
− | local full_close = GetReg("LLsw"); -- | + | local full_close = GetReg("LLsw"); -- full close limit switch (D1010@WebHMI Kitothemr) |
if home_mode then | if home_mode then | ||
− | -- | + | -- go down till close switch to determine position |
DEBUG("giving home_mode close cmd ! "); | DEBUG("giving home_mode close cmd ! "); | ||
− | WriteReg("closeCmd", 1 | + | WriteReg("closeCmd", 1) |
if (full_close == 1) then | if (full_close == 1) then | ||
− | DEBUG("full close ! ") | + | DEBUG("full close ! ") |
WriteReg("closeCmd",0); | WriteReg("closeCmd",0); | ||
− | WriteReg("endPosTime", now); -- | + | WriteReg("endPosTime", now); -- to avoid repeated homing |
WriteReg("homingBit",0); | WriteReg("homingBit",0); | ||
− | WriteReg("curPosition", 0); -- | + | WriteReg("curPosition", 0); -- current position |
end | end | ||
− | return 0 | + | return 0 -- exit while not the end |
end -- home mode | end -- home mode | ||
− | -- | + | -- MAIN PART ---- |
DEBUG("-------- auto valve control "); | DEBUG("-------- auto valve control "); | ||
− | local openingStatus = (GetReg("openCmd") == 1); -- | + | local openingStatus = (GetReg("openCmd") == 1); -- |
− | local closingStatus = (GetReg("closeCmd") == 1); -- | + | local closingStatus = (GetReg("closeCmd") == 1); -- |
− | local inMotionFlag = openingStatus or closingStatus | + | local inMotionFlag = openingStatus or closingStatus |
− | local target_dir= 0; -- | + | local target_dir= 0; -- moving sign |
− | local posSV = GetReg("posSetpoint"); -- | + | local posSV = GetReg("posSetpoint"); -- valve setpoint |
− | local posPV = GetReg("curPosition"); -- | + | local posPV = GetReg("curPosition"); -- current position |
− | -- | + | -- determine start position and time |
local startTime = GetReg("startPosTime"); | local startTime = GetReg("startPosTime"); | ||
local startPos = GetReg("startPosition"); | local startPos = GetReg("startPosition"); | ||
− | local pathdone = 0; -- | + | local pathdone = 0; -- path done value |
− | local OpenSw = GetReg("HLsw"); -- | + | local OpenSw = GetReg("HLsw"); -- full open switch |
− | local CloseSw = GetReg("LLsw"); -- | + | local CloseSw = GetReg("LLsw"); -- full close switch |
DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV)); | DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV)); | ||
− | -- | + | -- already in motion |
if inMotionFlag then | if inMotionFlag then | ||
− | DEBUG(" | + | DEBUG("in motion now") |
− | -- | + | -- full close ? |
if (closingStatus) and (posPV <= posSV) then | if (closingStatus) and (posPV <= posSV) then | ||
− | -- | + | -- if 0 set then wait close limit switch, auto calibrate |
− | if ( | + | if (posSV == 0) and (not CloseSw) then |
WriteReg("closeCmd", 1); | WriteReg("closeCmd", 1); | ||
− | return 0 | + | return 0 -- cmd will be off in limit switch reaction script |
end | end | ||
− | WriteReg("closeCmd", 0) | + | WriteReg("closeCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end | end | ||
− | -- | + | -- upper limit |
if (openingStatus) and (posPV >= posSV) then | if (openingStatus) and (posPV >= posSV) then | ||
− | -- | + | -- same as for close |
− | if ( | + | if (posSV == 100) and (not OpenSw) then |
WriteReg("openCmd", 1); | WriteReg("openCmd", 1); | ||
− | return 0; -- | + | return 0; -- cmd will be off in limit switch reaction script |
end | end | ||
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end | end | ||
− | + | else -- was stopped | |
− | + | ||
if (posSV ~= posPV) then | if (posSV ~= posPV) then | ||
− | -- | + | -- |
− | target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- | + | target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- get direction |
DEBUG_("target_dir = "..tostring(target_dir)); | DEBUG_("target_dir = "..tostring(target_dir)); | ||
if (target_dir > 0) then | if (target_dir > 0) then | ||
Строка 504: | Строка 503: | ||
DEBUG_("will close..."); | DEBUG_("will close..."); | ||
end | end | ||
− | WriteReg("inMotion_flag", 1); -- | + | WriteReg("inMotion_flag", 1); -- set the start flag for other scripts |
end | end | ||
end -- auto mode motion control | end -- auto mode motion control | ||
Строка 515: | Строка 514: | ||
'''v5''' | '''v5''' | ||
<syntaxhighlight lang = lua> | <syntaxhighlight lang = lua> | ||
− | -- | + | -- LatchStart_Time --- |
function main (userId) | function main (userId) | ||
DEBUG("Entered latch start time and position"); | DEBUG("Entered latch start time and position"); | ||
− | + | local flag = (GetReg("inMotion_flag") == 1) ; -- in motion flag @WebHMI 3point control | |
− | local flag = (GetReg("inMotion_flag") == 1) ; -- | + | |
DEBUG("motion flag = "..tostring(flag)); | DEBUG("motion flag = "..tostring(flag)); | ||
− | + | ||
− | + | ||
if flag then | if flag then | ||
DEBUG("now flag = 1"); | DEBUG("now flag = 1"); | ||
− | WriteReg("startPosition", GetReg("curPosition")); -- | + | WriteReg("startPosition", GetReg("curPosition")); -- start position stored (DS1020@WebHMI 3point control) |
− | WriteReg("startPosTime", os.time()); -- | + | WriteReg("startPosTime", os.time()); -- start time remembered (DS1012@WebHMI 3point control) |
end | end | ||
Строка 535: | Строка 532: | ||
'''v6''' | '''v6''' | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
− | --- | + | --- CalcNewPosition |
− | -- | + | -- constants |
− | FULLPATH = 127 ; -- | + | FULLPATH = 127 ; -- full path time |
− | K = 100 / FULLPATH ; -- | + | K = 100 / FULLPATH ; -- time to position k |
function main (userId) | function main (userId) | ||
DEBUG("Entered #5 script, calc. cur position"); | DEBUG("Entered #5 script, calc. cur position"); | ||
− | local now = os.time(); -- | + | local now = os.time(); -- |
− | local open_sts = (GetReg("openCmd") == 1 | + | local open_sts = (GetReg("openCmd") == 1) |
− | local close_sts = (GetReg("closeCmd") == 1 | + | local close_sts = (GetReg("closeCmd") == 1) |
− | local inMotionFlag = (open_sts or close_sts | + | local inMotionFlag = (open_sts or close_sts) |
DEBUG("in motion flag = "..tostring(inMotionFlag)); | DEBUG("in motion flag = "..tostring(inMotionFlag)); | ||
− | local cur_dir = 0 | + | local cur_dir = 0 |
− | local posPV = GetReg("curPosition"); | + | local posPV = GetReg("curPosition"); |
+ | |||
− | |||
local startTime = GetReg("startPosTime"); | local startTime = GetReg("startPosTime"); | ||
local startPos = GetReg("startPosition"); | local startPos = GetReg("startPosition"); | ||
local pathdone = 0; -- пройденный путь | local pathdone = 0; -- пройденный путь | ||
− | |||
if inMotionFlag then | if inMotionFlag then | ||
− | |||
if open_sts then | if open_sts then | ||
cur_dir = 1; | cur_dir = 1; | ||
Строка 565: | Строка 560: | ||
cur_dir = -1; | cur_dir = -1; | ||
end | end | ||
− | pathdone = GetPathDone(startTime, now) | + | pathdone = GetPathDone(startTime, now) |
− | varPos = startPos + cur_dir*pathdone | + | varPos = startPos + cur_dir*pathdone |
− | |||
if (varPos < 0) or (varPos > 100) then | if (varPos < 0) or (varPos > 100) then | ||
DEBUG("new varPos calculaed outside limits, cur value "..tostring(varPos)); | DEBUG("new varPos calculaed outside limits, cur value "..tostring(varPos)); | ||
Строка 576: | Строка 570: | ||
DEBUG("startPos , pathdone = , new varPos "..tostring(startPos).." "..tostring(pathdone).." "..tostring(varPos)); | DEBUG("startPos , pathdone = , new varPos "..tostring(startPos).." "..tostring(pathdone).." "..tostring(varPos)); | ||
− | WriteReg("curPosition", varPos); -- | + | WriteReg("curPosition", varPos); -- |
end -- auto mode motion control | end -- auto mode motion control | ||
end -- main | end -- main | ||
− | ------------- | + | ------------- calculates path done --------------- |
function GetPathDone(startTime, curTime) | function GetPathDone(startTime, curTime) | ||
− | + | ||
local curPos = (curTime - startTime) * K; | local curPos = (curTime - startTime) * K; | ||
local remainder = curPos - math.floor(curPos); | local remainder = curPos - math.floor(curPos); | ||
Строка 600: | Строка 594: | ||
local now = os.time(); | local now = os.time(); | ||
− | local open_sts = (GetReg("openCmd") == 1 | + | local open_sts = (GetReg("openCmd") == 1) |
− | local close_sts = (GetReg("closeCmd") == 1 | + | local close_sts = (GetReg("closeCmd") == 1) |
− | local full_close = (GetReg("LLsw") == 1 | + | local full_close = (GetReg("LLsw") == 1) |
− | local full_open = (GetReg("HLsw") == 1 | + | local full_open = (GetReg("HLsw") == 1) |
if (full_open or full_close) then | if (full_open or full_close) then | ||
− | + | ||
if full_open then | if full_open then | ||
− | |||
WriteReg("curPosition", 100); | WriteReg("curPosition", 100); | ||
WriteReg("openCmd", 0); | WriteReg("openCmd", 0); | ||
− | WriteReg("valveMan_code", 0) | + | WriteReg("valveMan_code", 0) |
end | end | ||
if full_close then | if full_close then | ||
− | WriteReg("curPosition", 0) | + | WriteReg("curPosition", 0) |
− | WriteReg("closeCmd", 0) | + | WriteReg("closeCmd", 0) |
− | WriteReg("valveMan_code", 0) | + | WriteReg("valveMan_code", 0) |
end | end | ||
− | WriteReg("inMotion_flag", 0) | + | WriteReg("inMotion_flag", 0) |
− | WriteReg("endPosTime", now | + | WriteReg("endPosTime", now) |
end | end | ||
− | -- | + | -- code of current state |
if (not open_sts) and (not close_sts) then | if (not open_sts) and (not close_sts) then | ||
− | WriteReg("valveStatus", 0); -- | + | WriteReg("valveStatus", 0); -- stopped |
elseif open_sts then | elseif open_sts then | ||
− | WriteReg("valveStatus", 1); -- | + | WriteReg("valveStatus", 1); -- opening |
else | else | ||
− | WriteReg("valveStatus", 2); -- | + | WriteReg("valveStatus", 2); -- closing |
end | end | ||
Строка 642: | Строка 635: | ||
------ v8 Drop Cur Cmd on Manual mode ---- | ------ v8 Drop Cur Cmd on Manual mode ---- | ||
function main (userId) | function main (userId) | ||
− | local auto_on = (GetReg("auto_mode")==1); -- | + | local auto_on = (GetReg("auto_mode")==1); -- |
if not auto_on then | if not auto_on then | ||
DEBUG_("Dropped cmds in manua mode ! "); | DEBUG_("Dropped cmds in manua mode ! "); | ||
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
− | WriteReg("closeCmd", 0) | + | WriteReg("closeCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end | end | ||
− | prev_mode = auto_on | + | prev_mode = auto_on |
end -- main | end -- main | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === | + | === Bit operation === |
==== Удобные битовые операции ==== | ==== Удобные битовые операции ==== |
Текущая версия на 12:25, 23 марта 2018
Содержание
- 1 Useful programs
- 1.1 Running script upon rising or falling edge of the signal
- 1.2 Timer with ON-delay (TON)
- 1.3 Alarming (sound, relya, sms, viber, telegram) about connection error
- 1.4 Moving average
- 1.5 PID - control
- 1.6 Running hour meter
- 1.7 Time circulation algorithm (together with redundancy function)
- 1.8 3-point control for a valve or servo
- 1.9 Bit operation
- 1.10 Обработка чисел с плавающей точкой двойной точности
- 1.11 Формирование общего регистра аварий по разным условиям
Useful programs
Running script upon rising or falling edge of the signal
The script should be defined as running upon register change. Inside the script, there have to be checking of the current state and espective actions on rising edge (current state = 1) or falling edge (=0). Example:
Timer with ON-delay (TON)
TON timer counts while input = 1, and after delay time sets output to "1".
-- globals
TIMER_DELAY = 20 -- timer delay 20 sec.
tmrStartTime = 0 -- timer start time
function main (userId)
-- local vars
local now = os.time(); -- current time
local startBit = (GetReg("tmrStartBit") == 1) -- just bit (D301@WebHMI)
-- CHECKING CONDITION ----------
if not startBit then
tmrStartTime = now -- here start time is stored
WriteReg("TON_out", 0) -- timer output bit register with alias"TON_out"
return 0 ;
else
if (now - tmrStartTime) > TIMER_DELAY then
-- end of countdown actions
WriteReg("TON_out", 1)
end -- if
end -- if
end -- main
Alarming (sound, relya, sms, viber, telegram) about connection error
It is possible to analyze the scan time by the script and if it exceeds the acceptable limit, signal it in different ways. Below is an example of processing a large scan time with signaling to the message buffer and Viber.
cntdownFlag = false -- countdown flag
timeStmp = 0 -- метка времени time stamp
msgSent = false; -- message sent flag
function main (userId)
-- reading inputs
local scan = GetReg(34); -- scan time
local c0 = GetReg(42); -- bad connection number
local SCANLIMIT = GetReg(886); -- scan time limit
local SCANDELAY = GetReg(887); -- error reaction delay
--
if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then
ERROR("scan / c0 was read as nil");
return 0;
end
-- reading time
local now = os.time()
-- шаблон сообщения
local msg1 = "Scan time long "..tostring(scan).." ".."ms, error in conn. "..tostring(c0);
--
if (scan > SCANLIMIT) then -- scan above limit
if not cntdownFlag then
cntdownFlag = true
timeStmp = now
else
if (now - timeStmp) > SCANDELAY then
if not msgSent then
AddAlertMessage(msg1)
SendViberMessage(398044391, msg1)
msgSent = true
end
end
end
else -- scan is normal
if (cntdownFlag == true) and msgSent then
AddInfoMessage("Scan is normal now");
SendViberMessage(398044391, "Scan is normal now");
msgSent = false
end
cntdownFlag = false
end
end -- main
In WebHMI there is a buzzer for the sound signal, and 2 output relays, which can be controlled for signaling (send a signal to the signal column or the PLC about the problem).
Moving average
The moving average is useful for smoothing the values of parameters that have noises, pulsations.
Algorithm of the moving average:
at the beginning of the filtering, first N values of sample 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.
-- globals
mav_len = 20; -- queue length
queue_fill = 0; -- queue filling index
av_sum = 0; -- accumulator for the moving average
function main (userId)
local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- reading parameter
if (queue_fill < mav_len) then -- queue is not filled
av_sum = av_sum + in_value; -- accumulating sum
queue_fill = queue_fill +1; -- and index
else -- queue is full
tmp_var = av_sum / mav_len; -- store one element
av_sum = av_sum - tmp_var + in_value; -- subtract and add new
end
--
if (queue_fill == mav_len ) then
out_value = av_sum / mav_len; -- calc. moving average
else
out_value = av_sum / queue_fill; -- mean average
end
WriteReg("Tout_mav", out_value); -- outside temperature
end
PID - control
An example of implementing a PID controller in WebHMI:
G_LIMIT = 100 -- ограничение выхода регулятора
--
function main (userId)
-- локальные переменные
local now = os.time()
local nexTime = GetReg("nextPidTime")
local CYCLE_TIME = GetReg("pidCycleTime")
-- параметры регулятора
local Kp = GetReg("Kp") -- Пропорц. коэф. (DS1400@WH Valve control)
local Ti = GetReg("Ki") -- Интегральный коэф. (DS1404@WH Valve control)
local Td = GetReg("Kd") -- Дифф. коэф. (DS1408@WH Valve control)
local Err, dErr, iSum_Limit = 0, 0, 0
local Int_sum = GetReg("pidIntegral") -- интегральный накопитель
local intPart = 0 -- инт. часть формулы
local G = GetReg("pidOut") -- выход регулятора
-- процесс
local PV = GetReg(1436) -- Мощность (PWR0@Scylar 8 INT)
local Sp = GetReg("targetPowerSp")
DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now))
-- условие работы
local auto = (GetReg("auto_mode") == 1) -- АВТ. РЕЖИМ ВКЛЮЧЕН (ds1176@WH Global)
local heatDemand = (GetReg("heatDemand") == 1)
if auto then
if heatDemand then
-- РЕГУЛЯТОР
if (now >= nexTime) then
DEBUG_("PID compute cycle")
WriteReg("nextPidTime", now + CYCLE_TIME)
Err = Sp - PV -- вычисляем ошибку
DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err)
dErr = Err - GetReg("pidPrevError") -- вычисляем производную ошибки
DEBUG_("dErr = "..dErr)
-- проверяем интегральное насыщение
iSum_Limit = (G_LIMIT * Ti / Kp) / 5
DEBUG_("iSum_Limit = "..iSum_Limit)
-- ПИД - регулятор
--проверка на 0 инт. составляющей
DEBUG_("prev Int_sum = "..Int_sum)
if (intPart <= iSum_Limit) and (intPart >= 0.0) then
Int_sum = Int_sum + Err -- накапливаем интеграл ошибки
DEBUG_("added error to Int_sum ")
elseif Int_sum < 0 then
Int_sum = 0
else
Int_sum = iSum_Limit -- ограничиваем интегральную составляющую
end
if (Ti == 0) then
intPart = 0
else
intPart = (1/Ti)*Int_sum
end
DEBUG_("new Int_part = "..intPart)
G = Kp * (Err + intPart + Td*dErr)
DEBUG_("Calculated G as "..G)
G = Round(G)
DEBUG_("Rounded G as "..G)
-- проверка выхода за диапазон
if G < 0 then
G = 0
end
if G > G_LIMIT then
G = G_LIMIT
end
WriteReg("pidPrevError", Err) -- запомнить предыдущую ошибку для след. скана
WriteReg("dErr", dErr) -- запомнить предыдущую ошибку для след. скана
WriteReg("pidIntegral", intPart) -- запомнить интегральную составляющую
WriteReg("pidOut", G)
WriteReg("posSPinput", G) -- дать уставку на клапан
end -- time stamp
else
DEBUG_("no heatDemand") -- вывести ПИД выход
G = 0
end -- heatDemand
-- DEBUG_("PID_out = "..G) -- вывести ПИД выход
WriteReg("pidOut", G)
WriteReg("posSPinput", G) -- дать уставку на клапан
end -- if auto
end -- main
-- rounding
function Round(var)
local integer, fraction = math.modf(var)
if fraction >= 0.5 then
integer = integer + 1
end
return math.floor(integer)
end
------ debug printing ------
thisScriptID = 45
function DEBUG_(str)
--ERROR("entered DEBUG_ in"..thisScriptID.." script");
local i = 0;
local tmp = "";
local id = tostring(thisScriptID);
local debug_id = GetReg("debug_IDs");
local capture_mask = "%s+(%d+)%s+"
while true do
i,_, tmp = string.find(debug_id,capture_mask,i+1)
if (i == nil) then
break -- not found
end
-- найдено
if (tmp ~= "0") then
if (tmp == tostring(thisScriptID)) then
DEBUG(str)
return 0
end
end
end
end -- DEBUG_
------------------------------
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)
Running hour meter
The hour meter is convenient for automatically generating a message about the need for maintenance for the equipment unit, changing the master pump in group to equalize the operating time, and so on.
An example of the implementation of the running hour meter on Lua in WebHMI (the program runs in each scan):
-- globals
run_state = false; -- to store current state
function main (userId)
-- locals
local check_mask = tonumber("0000100000000000",2); -- the mask to check running state bit in status register of the freq. drive FC 51 Danfoss
local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- checking result as a bool var
local now = os.time(); -- current system time
local time_diff = 0; -- time difference between calls
-- catching rising edge of the event
if (not run_state) and run_status then
WriteReg("P43StartTime", now); -- Drive start time №43
-- counting time
if run_state then
time_diff = (now - GetReg("P43StartTime")); -- get time difference
WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- increase running hour meter
WriteReg("P43StartTime", now); -- overwrite start time stamp
end
run_state = run_status
end
The timekeeping registers and time stamps should be made non-volatile.
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 running hours count. 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:
For simplicity and clarity, it is better to split the scripts into functional modules that can be quickly analyzed and placed in the desired order in the program list. The first script looks at the errors and if they do not exist, the units (air conditioners) alternate in time.
CIRCULATION_TIME = 30; -- for testing purposed circulation time = 30 sec.
function main (userId)
--[[
если нет ошибок, то чередуем работу по времени циркуляции
если есть ошибка на одном из кондиционеров, то он исключается из чередования
если ошибки на обоих, то стоим на месте
if there are no errors, then alternate the work on the time of circulation
If there is an error on one of the air conditioners, it is excluded from the circulation
if there are errors on both, then we stand in place
--]]
local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- error on a/c #1
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
-- working with 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
The second script looks at which a/c 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 selected a/c on dependting on pointer
--]]
local pointer = GetReg("activeAC"); -- active a/c (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 rise error flags upon 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 full valve travel time), initializes the coordinate, and then runs through the specified position.
To determine intermediate positions, a calculated value is used, determined from the characteristics of the actuator's 'full stroke time' , which can also be determined experimentally. As mentioned above, relatively complex and branched algorithms in WebHMI are better to divide into functionally complete and simple programs, the interaction and synchronization between which can be done using internal registers.
Below one of the examples for 3-point valve control with 2 limit switches is provided
The program split into 6 parts:
- v3 OpenClose Valve Manual - The script controls the drive in manual mode. Started by changing the number of the pressed button from the dashboard
- v4 Auto Valve Control - the main control script in the auto mode. It automatically performs the first initialization, and if the specified coordinate does not match the current one, it turns the drive in the desired direction, upon reaching the position, stops. Also, if the specified position is the same as the limit positions (0,100), the script continues to hold the command until the physical limit switch reached, thereby performing a periodic synchronization of the calculated position with the real position.
- v5 LatchStart_Time - script 'capturing' the initial position of the movement, is executed by changing the flag inMotionFlag, which is set by the previous programs v3 or v4.
- v6 CalcNewPosition - is executed 1 time per second. (on system time change), the script works as long as there is an inMotionFlag flag and re-counts the current time passed from the beginning of the movement to the current position (which in turn is used by the v4 program).
- v7 Limit sw reaction -When the limit switches are reached, it removes commands, and also removes the request from the manual control buttons.
- v8 Drop Cur Cmd on Manual mode - when switching to manual mode, turns off the current command.
At the beginning of the program names, the order of the required execution order (v3..v8) is indicated, since the necessary order of program execution can change undesirably, for example, when sorting programs and inaccurate 'dragging' programs in the list. Thus, the prefix reminds you of the desired order, it can also reflect the functionality of the script - 'v' Valves, 't' - temperature control, etc. It is more convenient to navigate in large lists and refer to it.
Text of the programs:
v3
------- MANUAL CONTROL FROM SCREEN BUTTONS OpenClose Valve Manual -------------------
function main (userId)
local button_value = GetReg("valveMan_code"); -- key code from buttons open / close
local manual_mode = (GetReg("auto_mode") == 0) ; -- auto mode is on (ds1176@WH Global)
if manual_mode then
if (button_value == 10) then
WriteReg("openCmd", 1); -- open command (DS1007@WH Valve control)
WriteReg("closeCmd", 0);
elseif (button_value == 5) then
WriteReg("closeCmd", 1); -- close command (DS1008@WH Valve control)
WriteReg("openCmd", 0)
else
-- invalid value - switch every command off
DEBUG("Read button value as "..tostring(button_value));
WriteReg("openCmd", 0)
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end --if
-- capture start position and start time
if (button_value == 10) or (button_value == 5) then
WriteReg("startPosition", GetReg("curPosition")); -- starting position (DS1020@WebHMI 3point control)
WriteReg("startPosTime", os.time()); -- start time stamp (DS1012@WebHMI 3point control)
WriteReg("inMotion_flag",1)
end
end -- manual_mode
end -- main -----------------------
v4
-- AUTO VALVE CONTROL ----
function main (userId)
local now = os.time(); -- current time
local timeStmp = GetReg("endPosTime"); -- end position time stamp (D1001@WebHMI Kitothemr)
local auto_mode = (GetReg("auto_mode")==1)
if auto_mode then
-- check if very first run = 0
if (timeStmp == 0) and (not home_mode) then
WriteReg("homingBit",1)
DEBUG("timeStmp = 0, homing needed! ");
end
local home_mode = (GetReg("homingBit") == 1); -- homing bit (D1006@WebHMI Kitothemr)
local full_close = GetReg("LLsw"); -- full close limit switch (D1010@WebHMI Kitothemr)
if home_mode then
-- go down till close switch to determine position
DEBUG("giving home_mode close cmd ! ");
WriteReg("closeCmd", 1)
if (full_close == 1) then
DEBUG("full close ! ")
WriteReg("closeCmd",0);
WriteReg("endPosTime", now); -- to avoid repeated homing
WriteReg("homingBit",0);
WriteReg("curPosition", 0); -- current position
end
return 0 -- exit while not the end
end -- home mode
-- MAIN PART ----
DEBUG("-------- auto valve control ");
local openingStatus = (GetReg("openCmd") == 1); --
local closingStatus = (GetReg("closeCmd") == 1); --
local inMotionFlag = openingStatus or closingStatus
local target_dir= 0; -- moving sign
local posSV = GetReg("posSetpoint"); -- valve setpoint
local posPV = GetReg("curPosition"); -- current position
-- determine start position and time
local startTime = GetReg("startPosTime");
local startPos = GetReg("startPosition");
local pathdone = 0; -- path done value
local OpenSw = GetReg("HLsw"); -- full open switch
local CloseSw = GetReg("LLsw"); -- full close switch
DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV));
-- already in motion
if inMotionFlag then
DEBUG("in motion now")
-- full close ?
if (closingStatus) and (posPV <= posSV) then
-- if 0 set then wait close limit switch, auto calibrate
if (posSV == 0) and (not CloseSw) then
WriteReg("closeCmd", 1);
return 0 -- cmd will be off in limit switch reaction script
end
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end
-- upper limit
if (openingStatus) and (posPV >= posSV) then
-- same as for close
if (posSV == 100) and (not OpenSw) then
WriteReg("openCmd", 1);
return 0; -- cmd will be off in limit switch reaction script
end
WriteReg("openCmd", 0)
WriteReg("inMotion_flag",0)
end
else -- was stopped
if (posSV ~= posPV) then
--
target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- get direction
DEBUG_("target_dir = "..tostring(target_dir));
if (target_dir > 0) then
WriteReg("openCmd" , 1);
DEBUG_("will open...");
else
WriteReg("closeCmd" , 1);
DEBUG_("will close...");
end
WriteReg("inMotion_flag", 1); -- set the start flag for other scripts
end
end -- auto mode motion control
end -- auto_mode
end -- main
v5
-- LatchStart_Time ---
function main (userId)
DEBUG("Entered latch start time and position");
local flag = (GetReg("inMotion_flag") == 1) ; -- in motion flag @WebHMI 3point control
DEBUG("motion flag = "..tostring(flag));
if flag then
DEBUG("now flag = 1");
WriteReg("startPosition", GetReg("curPosition")); -- start position stored (DS1020@WebHMI 3point control)
WriteReg("startPosTime", os.time()); -- start time remembered (DS1012@WebHMI 3point control)
end
end
v6
--- CalcNewPosition
-- constants
FULLPATH = 127 ; -- full path time
K = 100 / FULLPATH ; -- time to position k
function main (userId)
DEBUG("Entered #5 script, calc. cur position");
local now = os.time(); --
local open_sts = (GetReg("openCmd") == 1)
local close_sts = (GetReg("closeCmd") == 1)
local inMotionFlag = (open_sts or close_sts)
DEBUG("in motion flag = "..tostring(inMotionFlag));
local cur_dir = 0
local posPV = GetReg("curPosition");
local startTime = GetReg("startPosTime");
local startPos = GetReg("startPosition");
local pathdone = 0; -- пройденный путь
if inMotionFlag then
if open_sts then
cur_dir = 1;
else
cur_dir = -1;
end
pathdone = GetPathDone(startTime, now)
varPos = startPos + cur_dir*pathdone
if (varPos < 0) or (varPos > 100) then
DEBUG("new varPos calculaed outside limits, cur value "..tostring(varPos));
if (varPos < 0) then varPos = 0 end
if varPos > 100 then varPos = 100 end
end
DEBUG("startPos , pathdone = , new varPos "..tostring(startPos).." "..tostring(pathdone).." "..tostring(varPos));
WriteReg("curPosition", varPos); --
end -- auto mode motion control
end -- main
------------- calculates path done ---------------
function GetPathDone(startTime, curTime)
local curPos = (curTime - startTime) * K;
local remainder = curPos - math.floor(curPos);
if (remainder >= 0.5) then
-- округл. вниз
curPos = math.floor(curPos) + 1;
else
curPos = math.floor(curPos) ;
end
return curPos;
end
v7
-- Limit sw reaction------------
function main (userId)
local now = os.time();
local open_sts = (GetReg("openCmd") == 1)
local close_sts = (GetReg("closeCmd") == 1)
local full_close = (GetReg("LLsw") == 1)
local full_open = (GetReg("HLsw") == 1)
if (full_open or full_close) then
if full_open then
WriteReg("curPosition", 100);
WriteReg("openCmd", 0);
WriteReg("valveMan_code", 0)
end
if full_close then
WriteReg("curPosition", 0)
WriteReg("closeCmd", 0)
WriteReg("valveMan_code", 0)
end
WriteReg("inMotion_flag", 0)
WriteReg("endPosTime", now)
end
-- code of current state
if (not open_sts) and (not close_sts) then
WriteReg("valveStatus", 0); -- stopped
elseif open_sts then
WriteReg("valveStatus", 1); -- opening
else
WriteReg("valveStatus", 2); -- closing
end
end -- main --------------------------------
v8
------ v8 Drop Cur Cmd on Manual mode ----
function main (userId)
local auto_on = (GetReg("auto_mode")==1); --
if not auto_on then
DEBUG_("Dropped cmds in manua mode ! ");
WriteReg("openCmd", 0)
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end
prev_mode = auto_on
end -- main
Bit operation
Удобные битовые операции
Можно определить несколько удобных битовых операций, помня о том что в lua биты нумеруются с 1. Ссылка взята отсюда, там же есть и другие полезные функции.
function bit(n)
return 2 ^ (n - 1) -- возвращает число - вес разряда номер n
end
function hasbit(x, p)
return x % (p + p) >= p -- возвращает состояние бита p как true/false; if hasbit(value, bit(2)) then ...
end
function setbit(x, p)
return hasbit(x, p) and x or x + p -- установить бит № p в х Пример использования: х = setbit(х, bit(p))
end
function clearbit(x, p)
return hasbit(x, p) and x - p or x -- тоже только снять бит
end
Преобразование битовой таблички в число
Данная функция может быть полезна для нестандартных преобразований, когда число хранится в специфичном формате и его необходимо обработать по частям, используя заданную позицию 1-го бита и длину поля значения.
function getNumberFromTab(tab,start,length) -- получить число из таблички
local result_str = "";
for i=start,(start+length-1) do
result_str = result_str..tostring(tab[i]);
end
return tonumber(result_str,2);
end -- getNumberFromTab
Преобразование числа в битовую табличку
В lua (в версии, используемой в WebHMI) нет встроенной операции получения строкового представления двоичного числа. Однако эта функция очень полезна в пользовательсиких протоколах и др. задачах, где требуется перераспределить или каким то образом обработать отдельные биты числа. Текст варианта такой функции:
function getBits(input_num,length) -- работает с заданной длиной
local tab = {}; -- пустая табличка для ответа
local max_i = length - 1;
local remainder = input_num; -- остаток порязрядного взешивания
for i=max_i,0,-1 do
if remainder - 2^i >= 0 then
table.insert(tab, "1") ;
remainder = remainder - 2^i;
else
table.insert(tab, "0") ;
end
end
return tab;
end -- getBits
Далее можно переставить нужные биты местами и т.п. и получить нужное число. Ниже приведен пример перестановки битов 31 и 23 битов (при нумерации битов с 0) в ответе счетчика расходомера ВЛР 2301/2304 производства Асвега-У
function main (userId)
local input_num = GetReg(3); -- прочитать регистр с числом
local bitTable = getBits(input_num,32); -- таблица для обработки результата
local tmp_bit = ""; -- вспомогательный бит
tmp_bit = bitTable[1];
bitTable[1] = bitTable[9]; -- 31 бит это 1 элемент так как таблица развернута слева-направо, номера элементов табл.в lua c 1
bitTable[9] = tmp_bit;
-- конкатенация готовой таблицы в строку и преобразование в число из строки двоичного представления
WriteReg(1, tonumber(table.concat(bitTable),2));
end
Обработка чисел с плавающей точкой двойной точности
Некоторые устройства могут хранить данные в формате с повышенной точностью double float. В WebHMI текущей версии поддерживаются только 32 битные регистры, поэтому чтобы обработать 64 битное число необходимо будет использовать скрипт, которые "сцепит" два регистра в исходное число, а затем преобразует и запишет в обратно в float. Регистры должны быть типа double uint. Для "сцепки" чисел можно использовать описанную выше функцию getBits чтобы получить 2 таблицы, а затем дополнить первую таблицу битами из второй используя table.insert. В этом примере в качестве проверочного числа для простоты используется константа.
--------------------------------------
local test_var = 0xC1D312D000000000;
local NaN = tonumber("11111111111111111111111111111111",2);
--------------------------------------
function main (userId)
-- получить результирующую табличку "0" "1"
local result_tab = getBits(test_var,64);
WriteReg(11,table.concat(result_tab)); -- отладочная печать в регстр - строку
local result_num = 0.0; -- для хранения результата
local sign,exp,mantissa = 0,0,0;
local fraction_table = {}; -- табл. для дробной части
-- определить знак
if result_tab[1] == "1" then
sign = -1;
else
sign = 1;
end
-- экспоненту
exp = getNumberFromTab(result_tab,2,11);
DEBUG("exp = "..tostring(exp)); -- отл. печать
-- мантиссу
for i=13,64 do
table.insert(fraction_table,result_tab[i]);
end
-- посчитать мантиссу по-разрядно !!!
for j=1,52 do
if fraction_table[j]=="1" then
mantissa = mantissa +(2^(-1*j));
end
end
mantissa = mantissa +1;
DEBUG("m = "..tostring(mantissa)); -- отл. печать
result_num = sign*(2^(exp - 1023))*mantissa;
-- Обработка исключений
if exp == 0 then -- subnormals
result_num = sign*(2^(-1022))*(mantissa-1);
end
if exp == 0x7ff then -- nan
result_num = NaN;
end
-- Вывод результата в регистр типа float
WriteReg(10, result_num);
end -- main
Формирование общего регистра аварий по разным условиям
В WebHMI есть механизм аварий. Т.е. по состоянию бита в регистре можно автоматически генерировать записи в журнале о моменте возникновения, снятия аварии, квитировании ее оператором.
Однако в некоторых случаях использовать данный механизм непосредственно, как есть, может не совсем быть удобно. Например - аварийным считается определенный бит или их комбинация, разные коды ошибок (и их надо отличать друг от друга), значение больше или меньше порога и т.д. Если регистр имеет уже настроенные состояния для аварийной ситуации, используемое для отображения, то его нужно просто продублировать, выставив бит в общем аварийном регистре. Также может потребоваться введение задержки на срабатывание аварии, так как значение может иметь "дребезг" который надо фильтровать, прежде чем генерировать аварию в журнале.
Кроме этого удобнее, когда все аварии описаны в одном месте, а не разбросаны по сотням регистров. Т.е. рациональнее использовать один или несколько несколько 32-битных регистров для описания всех возможных аварий, и скрипт в котором эти аварии будут записываться в этот регистр по разным условиям. Ниже приведен пример такого скрипта:
Допустим, есть некая функциональная единица, например приточная вентиляционная установка, в которой есть некий общий признак аварии, есть несколько регистров с кодами текущих ошибок от частотных преобразователей и ситуация, когда один из регистров (влажность, СО2 и др.) вышел за допустимый диапазон, что является также аварийной ситуацией.
Тогда скрипт, который обработает все эти ситуации и сформирует нужные флаги на своих местах может выглядеть так:
allAlerts = { -- регистр бит - код ошибки тип операции
{srcAlias = "pv1Status", bits = {[1] = 1}, "bit9"},
{srcAlias = "vfdStatus_1", bits = {[2] = 10, [3] = 20, [4] = 30, [5] = 40}, "=="},
{srcAlias = "vfdStatus_2", bits = {[6] = 10, [7] = 20, [8] = 30, [9] = 40}, "=="},
{srcAlias = "pv1ZoneHumidity", bits = {[10] = 90}, ">="}
}
intAlertReg = "alertsReg" -- ОБЩИЙ РЕГИСТР АВАРИЙ
function main (userId)
local alertInput, alertOut = 0,0 -- временные переменные для чтения аварий и формирования результата
local digit = 0 -- номер бита в исходном регистре, указывающий на аварию..
--[[
Если назвать единообразно все параметры,
из которых надо формировать аварии и сложить их в одну структуру,
тогда можно использовать только один цикл for, например так:
--]]
for i,v in pairs(allAlerts) do
alertInput = GetReg(v.srcAlias) -- читаем регистр
for j,k in pairs(v.bits) do -- выполнится по числу элементов в под-табличке bits
-- проверяем тип операции - точное совпадение или другое условие
if (v[3] == "==") then
-- проверяем регистр на один из кодов ошибок
if (alertInput == k) then
alertOut = setbit(alertOut, j)
else
alertOut = clearbit(alertOut, j)
end
-- проверяем определенный бит
elseif (string.find(v[3], "bit%d+") ~= nil) then -- %d+ паттерн любая последовательность цифр
_,_,digit = tonumber(string.find(v[3], "bit(%d+)")) -- выделить из паттерна цифру и преобразовать в число
if hasbit(alertInput, bit(digit)) then -- если заданный бит установлен во входном регистре
alertOut = setbit(alertOut, j)
else
alertOut = clearbit(alertOut, j)
end
else
--- другие операции
end
end -- for bits inside
end -- for allAlerts
-- теперь просто записать то что получилось в общий регистр аварий
WriteReg(intAlertReg, alertOut)
end -- main
----------------- ДОПОЛНИТЕЛЬНЫЕ ФУНКЦИИ ----------------------------------
function bit(n)
return 2 ^ (n - 1) -- возвращает число - вес разряда номер n
end
function hasbit(x, p)
return x % (p + p) >= p -- возвращает состояние бита p как true/false; if hasbit(value, bit(2)) then ...
end
function setbit(x, p)
return hasbit(x, p) and x or x + p -- установить бит № p в х Пример использования: х = setbit(х, bit(p))
end
function clearbit(x, p)
return hasbit(x, p) and x - p or x -- тоже только снять бит
end
Если различных операций над значениями много: не только сравнение, но и например проверка конкретного бита и т.п. то можно детали реализации спрятать в функцию, которая получит на вход табличку с типом операции (установлен ли конкретный бит, код ошибки, параметр в границах и т.п.) и входным значением и поставит или сбросит нужный бит.
for i,v in pairs(allAlerts) do
alertInput = GetReg(v.srcAlias) -- читаем регистр
alertOut = MyGetAlerts(alertInput, alertOut, v) -- вызываем функцию, которая посмотрит что надо сделать по полю v[3], т.е. allAlerts[3]
end -- for