Полезные программы/en — различия между версиями
(Новая страница: «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 lar…») |
|||
(не показано 46 промежуточных версий этого же участника) | |||
Строка 1: | Строка 1: | ||
+ | <languages/> | ||
== Useful programs == | == Useful programs == | ||
=== Run script on rising or falling edge of the discrete signal === | === Run script on rising or falling edge of the discrete signal === | ||
Строка 5: | Строка 6: | ||
[[Файл:Rising edge.png | 1000 px | left]] <br clear = all > | [[Файл:Rising edge.png | 1000 px | left]] <br clear = all > | ||
− | === | + | === Реализация универсального таймера (TON TOFF) === |
− | TON | + | Таймер сравнивает свое текущее состояние с состоянием на входе: |
+ | * текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0. | ||
+ | * текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0. | ||
+ | Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера. | ||
<syntaxhighlight lang="Lua"> | <syntaxhighlight lang="Lua"> | ||
− | |||
− | |||
− | |||
− | function | + | function TimerOnOff(bool_input, onDelay, offDelay, tmrAlias) |
− | + | -- bool seconds seconds string | |
− | local | + | local outAlias = tmrAlias.."_out" |
− | + | local curTmrState = (GetReg(outAlias) == 1) ; DEBUG("curTmr State "..tostring(curTmrState)) | |
+ | DEBUG("bool_input = "..tostring(bool_input)) | ||
+ | |||
+ | if (bool_input == curTmrState) then | ||
+ | WriteReg(tmrAlias, now) ; DEBUG("timer input match state ") | ||
+ | return curTmrState -- as bool | ||
+ | elseif bool_input then | ||
+ | DEBUG("countdown for onDelay = "..(onDelay - (now - GetReg(tmrAlias)))) | ||
+ | if now - GetReg(tmrAlias) > onDelay then | ||
+ | WriteReg(outAlias, 1) | ||
+ | DEBUG("detected ON state after delay ") | ||
+ | return true | ||
+ | end | ||
+ | else | ||
+ | DEBUG("countdown for offDelay = "..(offDelay - (now - GetReg(tmrAlias)))) | ||
+ | if (now - GetReg(tmrAlias) > offDelay) then | ||
+ | WriteReg(outAlias, 0) | ||
+ | DEBUG("detected OFF state after delay ") | ||
+ | return false | ||
+ | end | ||
+ | end -- | ||
+ | return curTmrState -- output rerurned as bool for simple usage in IFs | ||
+ | end -- function | ||
+ | </syntaxhighlight> | ||
+ | |||
+ | Пример использования: | ||
+ | |||
+ | Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if. | ||
+ | |||
+ | |||
+ | <syntaxhighlight lang="Lua"> | ||
+ | DEBUG(" hotWater timer call ") | ||
+ | local DI2 = (GetReg(39) == 1) -- hotwater low pressure | ||
+ | local DI3 = (GetReg(40) == 1) -- hotwater high pressure | ||
+ | local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off . | ||
+ | |||
+ | -- or like this | ||
+ | if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then | ||
+ | -- do something | ||
+ | end -- simple timer in one row ! | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
</syntaxhighlight> | </syntaxhighlight> | ||
Строка 40: | Строка 68: | ||
<syntaxhighlight lang= "lua"> | <syntaxhighlight lang= "lua"> | ||
− | cntdownFlag = false; -- | + | cntdownFlag = false; -- timer countdown flag |
− | timeStmp = 0; -- | + | timeStmp = 0; -- time stamp |
− | msgSent = false; -- | + | msgSent = false; -- message sent flag |
function main (userId) | function main (userId) | ||
− | -- | + | -- read inputs |
− | local scan = GetReg(34); -- | + | local scan = GetReg(34); -- scan time |
− | local c0 = GetReg(42); -- | + | local c0 = GetReg(42); -- failed connection number |
− | local SCANLIMIT = GetReg(886); -- | + | local SCANLIMIT = GetReg(886); -- scan time limit |
− | local SCANDELAY = GetReg(887); -- | + | local SCANDELAY = GetReg(887); -- error reaction time |
-- | -- | ||
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 | ||
Строка 56: | Строка 84: | ||
return 0; | return 0; | ||
end | end | ||
− | -- | + | -- read time |
− | local now = os.time() | + | local now = os.time() |
− | -- | + | -- message pattern |
− | local msg1 = " | + | local msg1 = "Scan is long "..tostring(scan).." ".."ms, error in connection "..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) -- Женя | |
− | + | msgSent = true | |
− | + | ||
− | msgSent = true | + | |
end | end | ||
end | end | ||
end | end | ||
− | else -- | + | else -- scan in normal |
if (cntdownFlag == true) and msgSent then | if (cntdownFlag == true) and msgSent then | ||
− | + | AddInfoMessage("Скан вернулся к норме ") | |
− | SendViberMessage(398044391, "Скан вернулся к норме ") | + | SendViberMessage(398044391, "Скан вернулся к норме ") |
− | + | msgSent = false | |
− | msgSent = false | + | |
end | end | ||
− | cntdownFlag = false | + | cntdownFlag = false |
− | + | ||
end | end | ||
Строка 91: | Строка 116: | ||
− | + | 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 devices 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 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. | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | -- | + | -- globals |
− | mav_len = 20; -- | + | mav_len = 20; -- queue length |
− | queue_fill = 0; -- | + | queue_fill = 0; -- queue fill index |
− | av_sum = 0; -- | + | av_sum = 0; -- accumulator moving average |
Строка 107: | Строка 132: | ||
− | local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- | + | local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- read input |
− | if (queue_fill < mav_len) then -- | + | if (queue_fill < mav_len) then -- queue 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 -- now filled queue |
− | 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 |
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; -- get moving average |
else | else | ||
− | out_value = av_sum / queue_fill; -- | + | out_value = av_sum / queue_fill; -- arithmetic mean |
end | end | ||
− | WriteReg("Tout_mav", out_value) | + | WriteReg("Tout_mav", out_value) -- |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
− | === | + | === PID - control === |
− | + | An example of implementing a PID controller in WebHMI: | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | G_LIMIT = 100 -- | + | G_LIMIT = 100 -- output limit |
-- | -- | ||
function main (userId) | function main (userId) | ||
− | -- | + | -- local vars |
local now = os.time() | local now = os.time() | ||
local nexTime = GetReg("nextPidTime") | local nexTime = GetReg("nextPidTime") | ||
local CYCLE_TIME = GetReg("pidCycleTime") | local CYCLE_TIME = GetReg("pidCycleTime") | ||
− | -- | + | -- pid settings |
− | local Kp = GetReg("Kp") -- | + | local Kp = GetReg("Kp") -- Proportional part (DS1400@WH Valve control) |
− | local Ti = GetReg("Ki") -- | + | local Ti = GetReg("Ki") -- Integral time constant (DS1404@WH Valve control) |
− | local Td = GetReg("Kd") -- | + | local Td = GetReg("Kd") -- Diff. constant (DS1408@WH Valve control) |
local Err, dErr, iSum_Limit = 0, 0, 0 | local Err, dErr, iSum_Limit = 0, 0, 0 | ||
− | local Int_sum = GetReg("pidIntegral") -- | + | local Int_sum = GetReg("pidIntegral") -- integral accumulator |
− | local intPart = 0 -- | + | local intPart = 0 -- integral part |
− | local G = GetReg("pidOut") -- | + | local G = GetReg("pidOut") -- pid output |
− | -- | + | -- process |
− | local PV = GetReg(1436) -- | + | local PV = GetReg(1436) -- power (PWR0@Scylar 8 INT) |
local Sp = GetReg("targetPowerSp") | local Sp = GetReg("targetPowerSp") | ||
DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now)) | DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now)) | ||
− | -- | + | -- condition of work |
− | local auto = (GetReg("auto_mode") == 1) -- | + | local auto = (GetReg("auto_mode") == 1) -- auto mode is on (ds1176@WH Global) |
local heatDemand = (GetReg("heatDemand") == 1) | local heatDemand = (GetReg("heatDemand") == 1) | ||
Строка 161: | Строка 186: | ||
if heatDemand then | if heatDemand then | ||
− | -- | + | -- PID - loop |
if (now >= nexTime) then | if (now >= nexTime) then | ||
DEBUG_("PID compute cycle") | DEBUG_("PID compute cycle") | ||
WriteReg("nextPidTime", now + CYCLE_TIME) | WriteReg("nextPidTime", now + CYCLE_TIME) | ||
− | Err = Sp - PV -- | + | Err = Sp - PV -- get error |
DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err) | DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err) | ||
− | dErr = Err - GetReg("pidPrevError") -- | + | dErr = Err - GetReg("pidPrevError") -- get error tendency |
DEBUG_("dErr = "..dErr) | DEBUG_("dErr = "..dErr) | ||
− | -- | + | -- calc. integral limit |
iSum_Limit = (G_LIMIT * Ti / Kp) / 5 | iSum_Limit = (G_LIMIT * Ti / Kp) / 5 | ||
DEBUG_("iSum_Limit = "..iSum_Limit) | DEBUG_("iSum_Limit = "..iSum_Limit) | ||
− | -- | + | -- PID loop |
− | -- | + | --check integral part |
DEBUG_("prev Int_sum = "..Int_sum) | DEBUG_("prev Int_sum = "..Int_sum) | ||
if (intPart <= iSum_Limit) and (intPart >= 0.0) then | if (intPart <= iSum_Limit) and (intPart >= 0.0) then | ||
− | + | Int_sum = Int_sum + Err -- accumulating integral of error | |
DEBUG_("added error to Int_sum ") | DEBUG_("added error to Int_sum ") | ||
Строка 183: | Строка 208: | ||
Int_sum = 0 | Int_sum = 0 | ||
else | else | ||
− | Int_sum = iSum_Limit -- | + | Int_sum = iSum_Limit -- strict integral part |
end | end | ||
if (Ti == 0) then | if (Ti == 0) then | ||
Строка 197: | Строка 222: | ||
G = Round(G) | G = Round(G) | ||
DEBUG_("Rounded G as "..G) | DEBUG_("Rounded G as "..G) | ||
− | -- | + | -- check output for limits |
if G < 0 then | if G < 0 then | ||
G = 0 | G = 0 | ||
Строка 205: | Строка 230: | ||
end | end | ||
− | WriteReg("pidPrevError", Err) -- | + | WriteReg("pidPrevError", Err) -- remember previous error |
− | WriteReg("dErr", dErr) -- | + | WriteReg("dErr", dErr) -- |
− | WriteReg("pidIntegral", intPart) -- | + | WriteReg("pidIntegral", intPart) -- remember integral |
WriteReg("pidOut", G) | WriteReg("pidOut", G) | ||
− | WriteReg("posSPinput", G) -- | + | WriteReg("posSPinput", G) -- output for valve position |
end -- time stamp | end -- time stamp | ||
else | else | ||
− | DEBUG_("no heatDemand") -- | + | DEBUG_("no heatDemand") -- |
G = 0 | G = 0 | ||
end -- heatDemand | end -- heatDemand | ||
− | -- DEBUG_("PID_out = "..G) | + | -- DEBUG_("PID_out = "..G) |
WriteReg("pidOut", G) | WriteReg("pidOut", G) | ||
− | WriteReg("posSPinput", G) | + | WriteReg("posSPinput", G) |
end -- if auto | end -- if auto | ||
end -- main | end -- main | ||
− | -- | + | -- rounding |
function Round(var) | function Round(var) | ||
Строка 250: | Строка 275: | ||
if (i == nil) then | if (i == nil) then | ||
− | break -- | + | break -- not found |
end | end | ||
-- найдено | -- найдено | ||
if (tmp ~= "0") then | if (tmp ~= "0") then | ||
− | -- | + | -- found , check equality |
if (tmp == tostring(thisScriptID)) then | if (tmp == tostring(thisScriptID)) then | ||
DEBUG(str) | DEBUG(str) | ||
Строка 266: | Строка 291: | ||
</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 hours meter === |
− | + | The running hour meter is convenient for automatically generating a message about the need for routine work for the equipment unit, changing the lead pump in the pumping group to equalize the operating time, and so on. | |
− | + | An example of the implementation of the run hour meter in WebHMI (the program runs in each scan): | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | -- | + | -- globals |
− | run_state = false; -- | + | run_state = false; -- to remember current state |
function main (userId) | function main (userId) | ||
− | -- | + | -- locals |
− | local check_mask = tonumber("0000100000000000",2); -- | + | local check_mask = tonumber("0000100000000000",2); -- bit mask to check rotation bit in frequency inverter FC 51 Danfoss |
− | local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- | + | local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- check 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 current time and last call time |
− | -- | + | -- catching edge of the unit state |
if (not run_state) and run_status then | if (not run_state) and run_status then | ||
− | WriteReg("P43StartTime", now); -- | + | WriteReg("P43StartTime", now); -- unit start time |
+ | end | ||
− | -- | + | -- count time |
if run_state then | if run_state then | ||
− | time_diff = (now - GetReg("P43StartTime")); -- | + | time_diff = (now - GetReg("P43StartTime")); -- calc. time diff. |
− | WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- | + | WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- increase meter |
− | WriteReg("P43StartTime", now); -- | + | WriteReg("P43StartTime", now); -- overwrite start point |
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 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: | |
[[Файл: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 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. | |
<syntaxhighlight lang = "lua"> | <syntaxhighlight lang = "lua"> | ||
− | CIRCULATION_TIME = 30; -- | + | CIRCULATION_TIME = 30; -- for tests circulation time is short |
function main (userId) | 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) ; -- | + | local acError1, acError2 = (GetReg("acError1") == 1), (GetReg("acError2") ==1) ; -- errro on a/c #1 (DS101@webmi) |
− | 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 | ||
− | -- | + | -- work on circulation |
if (now >= switchTime) then | if (now >= switchTime) then | ||
if (curActiveAC == 1) then | if (curActiveAC == 1) then | ||
Строка 351: | Строка 377: | ||
− | + | The second script looks at what kind of 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. | |
Строка 357: | Строка 383: | ||
function main (userId) | function main (userId) | ||
--[[ | --[[ | ||
− | + | turn on selected a/c depending on pointer | |
--]] | --]] | ||
− | local pointer = GetReg("activeAC"); -- | + | local pointer = GetReg("activeAC"); -- active conditioner (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 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- | + | === 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 valve's full travel time), initializes the coordinate, and then go to the specified position. |
− | <p> | + | <p> To determine intermediate positions, a calculated value is used, determined from the characteristics of the 'full path 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 them can be done using internal registers. </p> | |
− | <br> | + | The <br> |
− | + | Below is a variant of 3-point control for a valve with 2 end sensors. | |
− | + | The program is divided 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''' - manual control program, |
− | ::'''v4 Auto Valve Control''' - | + | ::'''v4 Auto Valve Control''' - main program. do homing on very first call, and if set in current position are no equal move into the desired direction when set positions are limits, then keeps on current command till the limit switch is reached |
− | ::'''v5 LatchStart_Time''' - | + | ::'''v5 LatchStart_Time''' - catches start time |
− | ::'''v6 CalcNewPosition''' - | + | ::'''v6 CalcNewPosition''' - calculate path done like time since start time while valve in motion (this path is checked in v4). |
− | ::'''v7 Limit sw reaction''' | + | ::'''v7 Limit sw reaction''' |
− | ::'''v8 Drop Cur Cmd on Manual mode''' - | + | ::'''v8 Drop Cur Cmd on Manual mode''' - when manual mode selected, clears current cmds |
− | + | At the beginning of the programs, the order of the required execution order (v3..v8) is indicated, since the necessary order of program execution can change undesirable, 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>Script are given below: </ p> |
+ | |||
'''v3''' | '''v3''' | ||
<syntaxhighlight lang = lua> | <syntaxhighlight lang = lua> | ||
− | ------- | + | ------- Manual control from dashboard buttons ------------------- |
function main (userId) | function main (userId) | ||
− | local button_value = GetReg("valveMan_code") | + | local button_value = GetReg("valveMan_code")-- pressed button code |
− | local manual_mode = (GetReg("auto_mode") == 0) | + | local manual_mode = (GetReg("auto_mode") == 0) -- auto mode ON (ds1176@WH Global) |
Строка 411: | Строка 438: | ||
if (button_value == 10) then | if (button_value == 10) then | ||
− | WriteReg("openCmd", 1); -- | + | WriteReg("openCmd", 1); -- opening 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); -- closing command (DS1008@WH Valve control) |
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
else | else | ||
− | -- | + | -- invalid code - all 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 | ||
− | -- | + | -- catching start position, and 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")); -- start position and time (DS1020@WebHMI 3point control) |
− | WriteReg("startPosTime", os.time()) | + | WriteReg("startPosTime", os.time()) -- start time time stamp (DS1012@WebHMI 3point control) |
− | WriteReg("inMotion_flag",1) | + | WriteReg("inMotion_flag",1) |
end | end | ||
end -- manual_mode | end -- manual_mode | ||
Строка 439: | Строка 466: | ||
'''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(); -- cur time |
− | local timeStmp = GetReg("endPosTime" | + | local timeStmp = GetReg("endPosTime") |
− | 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 |
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) |
− | local full_close = GetReg("LLsw" | + | local full_close = GetReg("LLsw") |
if home_mode then | if home_mode then | ||
− | -- | + | -- go down till low limit sw |
− | 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 homing again |
− | WriteReg("homingBit",0) | + | WriteReg("homingBit",0) |
− | WriteReg("curPosition", 0) | + | WriteReg("curPosition", 0) -- current valve position |
end | end | ||
− | return 0 | + | return 0 -- |
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 |
− | local posSV = GetReg("posSetpoint"); -- | + | local posSV = GetReg("posSetpoint"); -- position setpoint |
− | local posPV = GetReg("curPosition"); -- | + | local posPV = GetReg("curPosition"); -- current position |
− | |||
local startTime = GetReg("startPosTime"); | local startTime = GetReg("startPosTime"); | ||
local startPos = GetReg("startPosition"); | local startPos = GetReg("startPosition"); | ||
− | local pathdone = 0; | + | local pathdone = 0; |
− | local OpenSw = GetReg("HLsw"); -- | + | local OpenSw = GetReg("HLsw"); -- full open limit sw |
− | local CloseSw = GetReg("LLsw"); -- | + | local CloseSw = GetReg("LLsw"); -- full close limit sw |
− | DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV)) | + | DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV)) |
− | -- | + | -- Already moving |
if inMotionFlag then | if inMotionFlag then | ||
− | + | -- full closed | |
− | -- | + | |
if (closingStatus) and (posPV <= posSV) then | if (closingStatus) and (posPV <= posSV) then | ||
− | -- | + | -- if 0 is setpoint then move till the physical limit sw |
− | if ( | + | if (posSV == 0) and (not CloseSw) then |
− | WriteReg("closeCmd", 1) | + | WriteReg("closeCmd", 1) |
− | return 0 | + | return 0 -- exiting, cmd will be off in v7 script |
end | end | ||
− | WriteReg("closeCmd", 0) | + | WriteReg("closeCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end | end | ||
− | -- | + | -- full open ? |
if (openingStatus) and (posPV >= posSV) then | if (openingStatus) and (posPV >= posSV) then | ||
− | + | --- if 100 is setpoint then move till the physical limit sw | |
if (posPV == 100) and (not OpenSw) then | if (posPV == 100) and (not OpenSw) then | ||
− | WriteReg("openCmd", 1) | + | WriteReg("openCmd", 1) |
− | return 0 | + | return 0 --exiting the command will be off in v7 script |
end | end | ||
− | WriteReg("openCmd", 0) | + | WriteReg("openCmd", 0) |
− | WriteReg("inMotion_flag",0) | + | WriteReg("inMotion_flag",0) |
end | end | ||
− | -- | + | -- stopped but not in position ? |
else | else | ||
if (posSV ~= posPV) then | if (posSV ~= posPV) then | ||
− | + | target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- определить знак | |
− | + | ||
DEBUG_("target_dir = "..tostring(target_dir)); | DEBUG_("target_dir = "..tostring(target_dir)); | ||
if (target_dir > 0) then | if (target_dir > 0) then | ||
WriteReg("openCmd" , 1); | WriteReg("openCmd" , 1); | ||
− | DEBUG_("will open...") | + | DEBUG_("will open...") |
else | else | ||
WriteReg("closeCmd" , 1); | WriteReg("closeCmd" , 1); | ||
− | DEBUG_("will close...") | + | DEBUG_("will close...") |
end | end | ||
− | WriteReg("inMotion_flag", 1) | + | WriteReg("inMotion_flag", 1) -- sets in motion flag |
end | end | ||
end -- auto mode motion control | end -- auto mode motion control | ||
Строка 543: | Строка 567: | ||
'''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"); | ||
-- Add your code here | -- Add your code here | ||
− | local flag = (GetReg("inMotion_flag") == 1) ; -- | + | local flag = (GetReg("inMotion_flag") == 1) ; -- in motion (DS1016@WebHMI 3point control) |
− | 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 (DS1020@WebHMI 3point control) |
− | WriteReg("startPosTime", os.time()) | + | WriteReg("startPosTime", os.time()) -- start time stamp (DS1012@WebHMI 3point control) |
− | + | end | |
− | + | ||
Строка 564: | Строка 585: | ||
'''v6''' | '''v6''' | ||
<syntaxhighlight lang=lua> | <syntaxhighlight lang=lua> | ||
− | --- | + | --- CalcNewPosition |
− | -- | + | -- constants |
− | FULLPATH = 127 | + | FULLPATH = 127 -- full close -> full open time |
− | K = 100 / FULLPATH ; -- | + | K = 100 / FULLPATH ; -- travel time to position coef. |
Строка 573: | Строка 594: | ||
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 -- current direction |
− | local posPV = GetReg("curPosition") | + | local posPV = GetReg("curPosition") -- current position |
− | -- | + | -- |
− | local startTime = GetReg("startPosTime") | + | local startTime = GetReg("startPosTime") |
− | local startPos = GetReg("startPosition") | + | local startPos = GetReg("startPosition") |
− | local pathdone = 0 | + | local pathdone = 0 |
− | -- | + | -- moving now |
if inMotionFlag then | if inMotionFlag then | ||
− | -- | + | -- detect direction |
if open_sts then | if open_sts then | ||
− | cur_dir = 1 | + | cur_dir = 1 |
else | else | ||
− | 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 |
− | -- | + | -- check for valid range |
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)) |
if (varPos < 0) then varPos = 0 end | if (varPos < 0) then varPos = 0 end | ||
if varPos > 100 then varPos = 100 end | if varPos > 100 then varPos = 100 end | ||
end | end | ||
− | + | DEBUG("startPos , pathdone = , new varPos "..tostring(startPos).." "..tostring(pathdone).." "..tostring(varPos)) | |
− | + | WriteReg("curPosition", varPos); -- writing new position | |
− | WriteReg("curPosition", varPos); -- | + | |
end -- auto mode motion control | end -- auto mode motion control | ||
end -- main | end -- main | ||
− | + | ||
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) |
if (remainder >= 0.5) then | if (remainder >= 0.5) then | ||
− | -- | + | -- rounding |
− | curPos = math.floor(curPos) + 1 | + | curPos = math.floor(curPos) + 1 |
else | else | ||
− | curPos = math.floor(curPos) | + | curPos = math.floor(curPos) |
end | end | ||
− | return curPos | + | return curPos |
end | end | ||
</syntaxhighlight> | </syntaxhighlight> | ||
Строка 632: | Строка 652: | ||
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); -- for manual mode |
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 | ||
− | -- | + | -- show current valve status |
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 | ||
Строка 675: | Строка 695: | ||
------ 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) -- work on register change |
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 | ||
− | |||
end -- main | end -- main | ||
</syntaxhighlight> | </syntaxhighlight> |
Текущая версия на 10:35, 5 октября 2018
Содержание
- 1 Useful programs
- 1.1 Run script on rising or falling edge of the discrete signal
- 1.2 Реализация универсального таймера (TON TOFF)
- 1.3 Signalling (sound, relay, sms, viber, telegram) about connection errors
- 1.4 Moving average
- 1.5 PID - control
- 1.6 Running hours meter
- 1.7 Time Circulation algorithm (together with redundancy function)
- 1.8 3-point control for a valve or servo
Useful programs
Run script on rising or falling edge of the discrete signal
The script needs to be called on register's change. Inside the script, you need to check the current state of this register and perform corresponding actions either on the front (current state = 1) or on the falling edge (= 0). Example of the script:
Реализация универсального таймера (TON TOFF)
Таймер сравнивает свое текущее состояние с состоянием на входе:
- текущее состояние = 0, вход = 1 - таймер включит выход через время задержки onDelay. Таймер TON получится если указать время offDelay = 0.
- текущее состояние = 1, вход = 0 - таймер выключит выход через время задержки offDelay. Таймер TOFF получится если указать время onDelay = 0.
Для работы таймера необходимо завести 2 DS регистра типа Unix time и Bit и присвоить им имена (алиасы) вида "<Имя1>", "<Имя1>_out". В первом будет хранится время след. переключения, во втором - текущее состояние таймера.
function TimerOnOff(bool_input, onDelay, offDelay, tmrAlias)
-- bool seconds seconds string
local outAlias = tmrAlias.."_out"
local curTmrState = (GetReg(outAlias) == 1) ; DEBUG("curTmr State "..tostring(curTmrState))
DEBUG("bool_input = "..tostring(bool_input))
if (bool_input == curTmrState) then
WriteReg(tmrAlias, now) ; DEBUG("timer input match state ")
return curTmrState -- as bool
elseif bool_input then
DEBUG("countdown for onDelay = "..(onDelay - (now - GetReg(tmrAlias))))
if now - GetReg(tmrAlias) > onDelay then
WriteReg(outAlias, 1)
DEBUG("detected ON state after delay ")
return true
end
else
DEBUG("countdown for offDelay = "..(offDelay - (now - GetReg(tmrAlias))))
if (now - GetReg(tmrAlias) > offDelay) then
WriteReg(outAlias, 0)
DEBUG("detected OFF state after delay ")
return false
end
end --
return curTmrState -- output rerurned as bool for simple usage in IFs
end -- function
Пример использования:
Например есть два дискретных входа с датчиками реле низкого и высокого давления. Их состояние читается и переводится в переменную типа bool для упрощения последующих операторов сравнения. Функция также возвращает bool, чтобы ее можно было сразу использовать в операторах сравнения без дополнительных операторов if.
DEBUG(" hotWater timer call ")
local DI2 = (GetReg(39) == 1) -- hotwater low pressure
local DI3 = (GetReg(40) == 1) -- hotwater high pressure
local hotWater = TimerOnOff((DI2 or DI3), 15, 15, "Tmr2") -- 15 seconds delay for both on and off .
-- or like this
if TimerOnOff(((GetReg("hotWaterLowPressure") == 1) or (GetReg(hotWaterHighPressure) == 1)), 15, 2, "Tmr2") then
-- do something
end -- simple timer in one row !
Signalling (sound, relay, sms, viber, telegram) about connection errors
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; -- timer countdown flag
timeStmp = 0; -- time stamp
msgSent = false; -- message sent flag
function main (userId)
-- read inputs
local scan = GetReg(34); -- scan time
local c0 = GetReg(42); -- failed connection number
local SCANLIMIT = GetReg(886); -- scan time limit
local SCANDELAY = GetReg(887); -- error reaction time
--
if (scan == nil) or (c0 == nil) or (SCANLIMIT == nil) or (SCANDELAY == nil) then
ERROR("scan / c0 was read as nil");
return 0;
end
-- read time
local now = os.time()
-- message pattern
local msg1 = "Scan is long "..tostring(scan).." ".."ms, error in connection "..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 in normal
if (cntdownFlag == true) and msgSent then
AddInfoMessage("Скан вернулся к норме ")
SendViberMessage(398044391, "Скан вернулся к норме ")
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 devices 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 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.
-- globals
mav_len = 20; -- queue length
queue_fill = 0; -- queue fill index
av_sum = 0; -- accumulator moving average
function main (userId)
local in_value, tmp_var, out_value = GetReg(26), 0, 0; -- read input
if (queue_fill < mav_len) then -- queue not filled
av_sum = av_sum + in_value; -- accumulating sum
queue_fill = queue_fill +1; -- and index
else -- now filled queue
tmp_var = av_sum / mav_len; -- store one element
av_sum = av_sum - tmp_var + in_value; -- subtract and add
end
--
if (queue_fill == mav_len ) then
out_value = av_sum / mav_len; -- get moving average
else
out_value = av_sum / queue_fill; -- arithmetic mean
end
WriteReg("Tout_mav", out_value) --
end
PID - control
An example of implementing a PID controller in WebHMI:
G_LIMIT = 100 -- output limit
--
function main (userId)
-- local vars
local now = os.time()
local nexTime = GetReg("nextPidTime")
local CYCLE_TIME = GetReg("pidCycleTime")
-- pid settings
local Kp = GetReg("Kp") -- Proportional part (DS1400@WH Valve control)
local Ti = GetReg("Ki") -- Integral time constant (DS1404@WH Valve control)
local Td = GetReg("Kd") -- Diff. constant (DS1408@WH Valve control)
local Err, dErr, iSum_Limit = 0, 0, 0
local Int_sum = GetReg("pidIntegral") -- integral accumulator
local intPart = 0 -- integral part
local G = GetReg("pidOut") -- pid output
-- process
local PV = GetReg(1436) -- power (PWR0@Scylar 8 INT)
local Sp = GetReg("targetPowerSp")
DEBUG_("seconds left for PID cycle = "..tostring(nexTime - now))
-- condition of work
local auto = (GetReg("auto_mode") == 1) -- auto mode is on (ds1176@WH Global)
local heatDemand = (GetReg("heatDemand") == 1)
if auto then
if heatDemand then
-- PID - loop
if (now >= nexTime) then
DEBUG_("PID compute cycle")
WriteReg("nextPidTime", now + CYCLE_TIME)
Err = Sp - PV -- get error
DEBUG_("sp pv Err = "..Sp.." "..PV.." "..Err)
dErr = Err - GetReg("pidPrevError") -- get error tendency
DEBUG_("dErr = "..dErr)
-- calc. integral limit
iSum_Limit = (G_LIMIT * Ti / Kp) / 5
DEBUG_("iSum_Limit = "..iSum_Limit)
-- PID loop
--check integral part
DEBUG_("prev Int_sum = "..Int_sum)
if (intPart <= iSum_Limit) and (intPart >= 0.0) then
Int_sum = Int_sum + Err -- accumulating integral of error
DEBUG_("added error to Int_sum ")
elseif Int_sum < 0 then
Int_sum = 0
else
Int_sum = iSum_Limit -- strict integral part
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)
-- check output for limits
if G < 0 then
G = 0
end
if G > G_LIMIT then
G = G_LIMIT
end
WriteReg("pidPrevError", Err) -- remember previous error
WriteReg("dErr", dErr) --
WriteReg("pidIntegral", intPart) -- remember integral
WriteReg("pidOut", G)
WriteReg("posSPinput", G) -- output for valve position
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
-- found , check equality
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 hours meter
The running hour meter is convenient for automatically generating a message about the need for routine work for the equipment unit, changing the lead pump in the pumping group to equalize the operating time, and so on.
An example of the implementation of the run hour meter in WebHMI (the program runs in each scan):
-- globals
run_state = false; -- to remember current state
function main (userId)
-- locals
local check_mask = tonumber("0000100000000000",2); -- bit mask to check rotation bit in frequency inverter FC 51 Danfoss
local run_status = (bit.band(GetReg(109),check_mask) ~= 0); -- check result as a bool var
local now = os.time(); -- current system time
local time_diff = 0; -- time difference between current time and last call time
-- catching edge of the unit state
if (not run_state) and run_status then
WriteReg("P43StartTime", now); -- unit start time
end
-- count time
if run_state then
time_diff = (now - GetReg("P43StartTime")); -- calc. time diff.
WriteReg("P43RunTime", GetReg("P43RunTime")+time_diff); -- increase meter
WriteReg("P43StartTime", now); -- overwrite start point
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 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:
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.
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
The second script looks at what kind of 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. 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 them can be done using internal registers.
The
Below is a variant of 3-point control for a valve with 2 end sensors.
The program is divided into 6 parts:
- v3 OpenClose Valve Manual - manual control program,
- v4 Auto Valve Control - main program. do homing on very first call, and if set in current position are no equal move into the desired direction when set positions are limits, then keeps on current command till the limit switch is reached
- v5 LatchStart_Time - catches start time
- v6 CalcNewPosition - calculate path done like time since start time while valve in motion (this path is checked in v4).
- v7 Limit sw reaction
- v8 Drop Cur Cmd on Manual mode - when manual mode selected, clears current cmds
At the beginning of the programs, the order of the required execution order (v3..v8) is indicated, since the necessary order of program execution can change undesirable, 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.
Script are given below: </ p> v3
------- Manual control from dashboard buttons -------------------
function main (userId)
local button_value = GetReg("valveMan_code")-- pressed button code
local manual_mode = (GetReg("auto_mode") == 0) -- auto mode ON (ds1176@WH Global)
if manual_mode then
if (button_value == 10) then
WriteReg("openCmd", 1); -- opening command (DS1007@WH Valve control)
WriteReg("closeCmd", 0)
elseif (button_value == 5) then
WriteReg("closeCmd", 1); -- closing command (DS1008@WH Valve control)
WriteReg("openCmd", 0)
else
-- invalid code - all off
DEBUG("Read button value as "..tostring(button_value));
WriteReg("openCmd", 0)
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end --if
-- catching start position, and time
if (button_value == 10) or (button_value == 5) then
WriteReg("startPosition", GetReg("curPosition")); -- start position and time (DS1020@WebHMI 3point control)
WriteReg("startPosTime", os.time()) -- start time 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(); -- cur time
local timeStmp = GetReg("endPosTime")
local auto_mode = (GetReg("auto_mode")==1)
if auto_mode then
-- check if very first run
if (timeStmp == 0) and (not home_mode) then
WriteReg("homingBit",1)
DEBUG("timeStmp = 0, homing needed! ")
end
local home_mode = (GetReg("homingBit") == 1)
local full_close = GetReg("LLsw")
if home_mode then
-- go down till low limit sw
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 homing again
WriteReg("homingBit",0)
WriteReg("curPosition", 0) -- current valve position
end
return 0 --
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
local posSV = GetReg("posSetpoint"); -- position setpoint
local posPV = GetReg("curPosition"); -- current position
local startTime = GetReg("startPosTime");
local startPos = GetReg("startPosition");
local pathdone = 0;
local OpenSw = GetReg("HLsw"); -- full open limit sw
local CloseSw = GetReg("LLsw"); -- full close limit sw
DEBUG("posPV "..tostring(posPV).." pos SV"..tostring(posSV))
-- Already moving
if inMotionFlag then
-- full closed
if (closingStatus) and (posPV <= posSV) then
-- if 0 is setpoint then move till the physical limit sw
if (posSV == 0) and (not CloseSw) then
WriteReg("closeCmd", 1)
return 0 -- exiting, cmd will be off in v7 script
end
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end
-- full open ?
if (openingStatus) and (posPV >= posSV) then
--- if 100 is setpoint then move till the physical limit sw
if (posPV == 100) and (not OpenSw) then
WriteReg("openCmd", 1)
return 0 --exiting the command will be off in v7 script
end
WriteReg("openCmd", 0)
WriteReg("inMotion_flag",0)
end
-- stopped but not in position ?
else
if (posSV ~= posPV) then
target_dir = (posSV - posPV)/math.abs(posSV - posPV); -- определить знак
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) -- sets in motion flag
end
end -- auto mode motion control
end -- auto_mode
end -- main
v5
-- LatchStart_Time ---
function main (userId)
DEBUG("Entered latch start time and position");
-- Add your code here
local flag = (GetReg("inMotion_flag") == 1) ; -- in motion (DS1016@WebHMI 3point control)
DEBUG("motion flag = "..tostring(flag))
if flag then
DEBUG("now flag = 1")
WriteReg("startPosition", GetReg("curPosition")) -- start position (DS1020@WebHMI 3point control)
WriteReg("startPosTime", os.time()) -- start time stamp (DS1012@WebHMI 3point control)
end
end
v6
--- CalcNewPosition
-- constants
FULLPATH = 127 -- full close -> full open time
K = 100 / FULLPATH ; -- travel time to position coef.
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 -- current direction
local posPV = GetReg("curPosition") -- current position
--
local startTime = GetReg("startPosTime")
local startPos = GetReg("startPosition")
local pathdone = 0
-- moving now
if inMotionFlag then
-- detect direction
if open_sts then
cur_dir = 1
else
cur_dir = -1
end
pathdone = GetPathDone(startTime, now)
varPos = startPos + cur_dir*pathdone
-- check for valid range
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); -- writing new position
end -- auto mode motion control
end -- main
function GetPathDone(startTime, curTime)
local curPos = (curTime - startTime) * K;
local remainder = curPos - math.floor(curPos)
if (remainder >= 0.5) then
-- rounding
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); -- for manual mode
end
if full_close then
WriteReg("curPosition", 0)
WriteReg("closeCmd", 0)
WriteReg("valveMan_code", 0)
end
WriteReg("inMotion_flag", 0)
WriteReg("endPosTime", now)
end
-- show current valve status
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) -- work on register change
if not auto_on then
DEBUG_("Dropped cmds in manua mode ! ")
WriteReg("openCmd", 0)
WriteReg("closeCmd", 0)
WriteReg("inMotion_flag",0)
end
end -- main