-- ======================
-- Description
-- ======================
--
-- Logic manages distributed variable. If changes any variable in input.counters list or in output.value variable,
-- then new value is copied to the all variables in output.settings list and to the output.value.
--
-- In KVSettings can be written transition table in the integer values form, for example: <KVSetting Key="5" Value="6"/>. In keys are stored
-- values of items from input and output lists. In values are stored values of output.value variable. When logic reads any item from input list,
-- then it compares given item value with each key in table. if matches, then it takes related value in table and then works with it. The same, if
-- logic wants to write any variable to the output list, then it compares given value with each value in transition table. if matches, then it takes
-- related key and write it to the output item from list
--
-- ======================
-- Parameters
-- ======================
--
-- Logic expects following variables:
--
-- setting.mode - work mode. 0 - auto, 1 - manual. When selected manual, then always setting.manual.value is copied
-- to the output.value. When selected auto, then logic traces changes in input.counters and output.value.
-- If detected changes, then all variables in output.settings and output.value are set to new value.
--
-- setting.manual.value - value, that is copied to the output.value when setting.mode is set to manual (1). This variable keeps always
-- values from output.value domain (read KVSettings).
--
-- output.value - main logic output value and bidirectional variable with highest priority (highest than variables in
-- input.counters list). When logic is in auto mode (setting.mode = 0) and this value changes, then all variables
-- in output.settings list are set to new value. In manual mode, this variable is always overriden by
-- value from setting.manual.value. This value can mapped by GUI application.
--
-- output.settings - control variables - Writing any value to this list causes setting new value in related external device
-- (H4F2 - setting.t.setpoint.value). In manual mode, to these variables are copied transformed by kvsetting value
-- from setting.manual.value. In auto mode, functionality was described at input.counters.
--
-- input.counters - when any variable in this list changes, then new value is copied to the all variables in output.settings and
-- to the output.value. It import list and values in this list should be mapped directly to the remote variables,
-- that are related with any devices (H4F2 - counter.t.setpoint). When detected changes at more than one variable
-- in this list, then priority is determined by postfix ASCII order.
--
-- ======================
-- mandatory variables
-- ======================
--
-- Logic expects following mandatory variables:
--
-- reload.trigger - causes reloading lua script
--
-- memcnt - current amount of memory used by lua in bytes
--
-- Logic expects following kv settings:
--
-- LuaScriptPath - path to the lua script - must be absolute
--
-- ======================
-- ChangeLog
-- ======================
--
-- 2017-06-09 ver 0.0.4
--
-- # obsługa wielu wartości do jednego key'a
--
-- 2017-06-07 ver 0.0.2.825
--
-- # obsługa automatycznego build'a
-- # opis logiki
--
-- 2017-06-07 ver 0.0.1
--
-- # zmiana nazw inputów
--
-- 2017-03-17 ver 0.0.0
--
-- # First release
--
-- user can use some functions provided by ibmanager.
-- ibmanager provides following functions to use:
--
-- function returns value of required ibmanager variable
-- @param fullName - string - variable name - name of variable of which value must be returned, for example "rs.0.id.255.input.t.0.value"
-- @return - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getValue(fullName)
-- function set value of given ibmanager variable
-- @param fullName - string - variable name - name of variable of which value we want to set, for example "rs.0.id.255.input.t.0.value"
-- @param value - string, int or boolean - value to set
-- @return - nothing
--
-- setValue(fullName, value)
-- function returns value of required ibmanager variable
-- @param key - placed in xml logic configuration file:
-- * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
-- * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
-- in the case of variables that are placed in lists
-- @return - string or integer - variable value or "nil" if variable not exist or is not readable
--
-- getLogicValue(key)
-- function set value of given ibmanager variable
-- @param key - placed in xml logic configuration file:
-- * as attribute: Name, in Var, RemoteVar and ImportVar elements in the case of stand alone variables
-- * as concatenation of two attributes: ListName.Postfix in VarListItem, RemoteVarListItem and ImportVarListItem elements
-- in the case of variables that are placed in lists
-- @param value - string, int or boolean - value to set
-- @return - nothing
--
-- setLogicValue(key, value)
-- function returns two sections of kvsettings from xml configuration file
-- returned value is two element table, each of these elements is table too.
-- indices of returned table are strings and equal "instance" and "global"
-- values of returned tables are tables and contain KVsettings for applicable section.
-- nested tables have form key = value, where key is index in nested table and value is value.
-- example: {"instance" = {"ikey1" = "ivalue1", "ikey2" = "gvalue2"}, "global" = {"gkey1" = "gvalue1", "gkey2" = "gvalue2", "gkey3" = "gvalue3"}}
-- @return - two dimensional array - kvsettings for global and instance sections
-- getKvSettings()
-- function schedules alert to send.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- scheduleAlert(id)
-- function cancels alert sending, if was previously scheduled. if not then only wakes up alerts handling thread, so if there is no need to call this function, then do not call it.
-- rules are defined in separated alert configuration files and are described in ibmanager instruction manual
-- @param id - alert identifier - must be defined in current logic configuration file in section: <Alert Id="any_identifier" ...
-- cancelAlert(name)
-- function returns table, containing Variables that belongs to required list.
-- @param listName - Name attribute of VarList, RemoteVarList or ImportVarList elemetnst in configurationfile
-- @return array of key-value pairs. Key - variable postfix, Value - Variable value
-- getVarList(listName)
-- function returns monotonic system clock value, that elapsed since specific epoch
-- returned time is expressed in milliseconds.
-- getClock()
-- function logs message to file, if defined in configuration file log level is less than passed to this function
-- @param logLevel - one of:
-- LogLevel.TraceLo
-- LogLevel.Trace
-- LogLevel.TraceHi
-- LogLevel.DebugLo
-- LogLevel.Debug
-- LogLevel.DebugHi
-- LogLevel.Info
-- LogLevel.Notice
-- LogLevel.Warning
-- LogLevel.Error
-- LogLevel.Critical
-- @param logMessage - string to log
-- log(logLevel, logMessage)
-- ibmanager provides following global variables:
-- logic type, (in this case it will always be "Lua") - the same as in logic configuration file in section: <Logic Type="Lua" ...
-- LOGIC_TYPE
-- logic version, the same as in logic configuration file in section: <Logic ... Version="x.y.z" ...
-- LOGIC_VERSION
-- logic sub-type, the same as in logic configuration file in section: <Logic ... SubType="Hysteresis" ...
-- LOGIC_SUBTYPE
-- logic sub-version, the same as in logic configuration file in section: <Logic ... SubVersion="x.y.z" ...
-- LOGIC_SUBVERSION
-- logic instance name - the same as in logic configuration file, in section: <Instance Name="0">
-- LOGIC_INSTANCE_NAME
-- add script directory to package path
package.path = package.path .. ";/ibsystem/ibmanager/logic/lua/utils/?.lua";
package.path = package.path .. ";/work/insbud/iblogics/Lua/Scripts/utils/?.lua";
-- ---------------------------------------------------------------------------------------------------------
-- global - enums
-- ---------------------------------------------------------------------------------------------------------
AUTO_MODE = 0;
MANUAL_MODE = 1;
-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------
-- logic class
Logic = {};
Logic.__index = Logic;
-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------
function Logic.create()
--our new object
local logic = {};
setmetatable(logic, Logic);
--our object fields initialization
logic.lastOutput = getLogicValue("output.value");
logic.items = {};
--keys - outputs, values - items
logic.outputsMapping = {};
--keys - items, values - outputs
logic.itemsMapping = {};
--transition table stored in KVSettings. Key - input, Value = output
local kvsections = getKvSettings();
local i = 0;
for section, kvsettings in pairs(kvsections) do
if section == "instance" then
for k, v in pairs(kvsettings) do
if k ~= "LuaScriptPath" then
local itemValues = logic:parseValues(k);
local outputValues = logic:parseValues(v);
if itemValues.size == 0 then
error("key parsing error in kvSettings");
end
if outputValues.size == 0 then
error("value parsing error in kvSettings");
end
if outputValues.size > 1 and itemValues.size > 1 then
error("mapping many to many in kvSettings: key = " .. k .. ", value = " .. v);
end
if outputValues.size == 1 and itemValues.size == 1 then
--mapping one item value to one output value
logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
elseif outputValues.size > 1 then
--mapping many output values to single item value
--we take only first output in items mappings, rest are abandoned
logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
--we map all outputs to single item
for _, ov in pairs(outputValues.arr) do
logic.outputsMapping[ov] = itemValues.arr[0];
end
logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
elseif itemValues.size > 1 then
--mapping many item values to one output value
--we take only first item in outputs mappings, rest are abandoned
logic.outputsMapping[outputValues.arr[0]] = itemValues.arr[0];
--we map all items to single output
for _, iv in pairs(itemValues.arr) do
logic.itemsMapping[iv] = outputValues.arr[0];
end
logic.itemsMapping[itemValues.arr[0]] = outputValues.arr[0];
end
end
end
end
end
--for item, output in pairs(logic.itemsMapping) do
-- print ("item: " .. item .. " -> output: " .. output);
--end
--for output, item in pairs(logic.outputsMapping) do
-- print ("output: " .. output .. " -> item: " .. item);
--end
-- adding items and filling counter and its value
local counterListName = "input.counters";
local lst = getVarList(counterListName);
if lst ~= nil then
for postfix, val in pairs(lst) do
-- building logic counter variable name
local cntVar = counterListName .. "." .. postfix;
local item = { counterVar = cntVar, value = val, settingVar = nil };
-- addint item
logic.items[postfix] = item;
end
end
-- filling settings in items
local settingListName = "output.settings";
lst = getVarList(settingListName);
if lst ~= nil then
for postfix, _ in pairs(lst) do
-- building logic setting variable name
local item = logic.items[postfix];
if item ~= nil then
item.settingVar = settingListName .. "." .. postfix;
end
end
end
--checking if correctly defined variables in list
for key, item in pairs(logic.items) do
if (item.counterVar == nil) then
error("Configuration error, defined " ..
key .. " postfix in list " .. settingListName .. " but not defined them in list " .. counterListName);
end
if (item.settingVar == nil) then
error("Configuration error, defined " ..
key .. " postfix in list " .. counterListName .. " but not defined them in list " .. settingListName);
end
-- print ("postfix: " .. key .. ", setting: " .. item.settingVar .. ", counter: " .. item.counterVar .. ", counter val: " .. item.value);
end
return logic;
end
-- ---------------------------------------------------------------------------------------------------------
-- function returns tables of numbers. these numbers are placed in string and separated by comma
-- ---------------------------------------------------------------------------------------------------------
function Logic:parseValues(str)
local array = {};
local i = 0;
local pos = 1;
local len = str:len();
if (len == 0) then
error("kvsettings item is empty");
end
while pos ~= nil do
local ret = str:match("%d+", pos);
if ret ~= nil then
array[i] = tonumber(ret);
--print("arr[" .. i .. "] = " .. array[i]);
i = i + 1;
end
pos = str:find(",", pos + 1)
end
local pair = { size = i, arr = array };
return pair;
end
-- ---------------------------------------------------------------------------------------------------------
-- function looks for given output value among values in kVSettings and if find, then returns related key (item)
-- if not find, then returns what it takes - outputValue
-- ---------------------------------------------------------------------------------------------------------
function Logic:getItemValue(outputValue, prefferedItemValue)
if prefferedItemValue ~= nil then
for item, output in pairs(self.itemsMapping) do
if item == prefferedItemValue and outputValue == output then
return prefferedItemValue;
end
end
end
local foundItem = nil;
for output, item in pairs(self.outputsMapping) do
if outputValue == output then
foundItem = item;
break;
end
end
if foundItem == nil then
return outputValue;
end
return foundItem;
end
-- ---------------------------------------------------------------------------------------------------------
-- function looks for given item among keys in kVSettings and if find, then returns related value (output)
-- if not find, then returns what it takes - inputValue
-- ---------------------------------------------------------------------------------------------------------
function Logic:getOutputValue(itemValue, prefferedOutputValue)
if prefferedOutputValue ~= nil then
for output, item in pairs(self.outputsMapping) do
if prefferedOutputValue == output and item == itemValue then
return prefferedOutputValue;
end
end
end
local foundOutput = nil;
for item, output in pairs(self.itemsMapping) do
if item == itemValue then
foundOutput = output;
break;
end
end
if foundOutput == nil then
return itemValue;
end
return foundOutput;
end
-- ---------------------------------------------------------------------------------------------------------
--
-- ---------------------------------------------------------------------------------------------------------
function Logic:call()
local mode = getLogicValue("setting.mode");
local manualValue = mode == MANUAL_MODE and getLogicValue("setting.manual.value") or nil;
local output = getLogicValue("output.value");
local intermediateValue = getLogicValue("counter.intermediate.output");
local changedItemValue = nil;
local changedOutputValue = output ~= self.lastOutput and output or nil;
local changedItemKey = nil;
local oldItemValue = nil;
--reading counters from device
for key, item in pairs(self.items) do
local logicItemValue = getLogicValue(item.counterVar);
if changedItemValue == nil and logicItemValue ~= item.value then
changedItemValue = logicItemValue;
changedItemKey = key;
oldItemValue = item.value;
end
item.value = logicItemValue;
end
local newOutputValue = nil;
local newIntermediateValue = nil;
if manualValue ~= nil then
newOutputValue = manualValue;
newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
--print("output: " .. output .. " -> " .. newOutputValue);
elseif changedOutputValue ~= nil then
newOutputValue = changedOutputValue;
newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
--print("output: " .. output .. " -> " .. newOutputValue);
elseif changedItemValue ~= nil then
newIntermediateValue = changedItemValue;
newOutputValue = self:getOutputValue(newIntermediateValue, output);
--print("item (" .. changedItemKey .. "): " .. oldItemValue .. " -> " .. newIntermediateValue .. ", new output: " .. newOutputValue .. ", new inter: " .. newIntermediateValue .. ", curr out: " .. output);
else
newOutputValue = output;
newIntermediateValue = self:getItemValue(newOutputValue, intermediateValue);
end
self.lastOutput = newOutputValue;
setLogicValue("output.value", newOutputValue);
setLogicValue("counter.intermediate.output", newIntermediateValue);
for _, item in pairs(self.items) do
setLogicValue(item.settingVar, newIntermediateValue);
end
end
-- ---------------------------------------------------------------------------------------------------------
-- main logic state object
g_logic = nil;
SUPPORTED_SUBLOGIC_TYPE = "DistVar";
SUPPORTED_SUBLOGIC_VERSION = "0.0.4";
g_versionChecked = false;
-- ---------------------------------------------------------------------------------------------------------
-- entry point to the logic
-- @param firstCall - tells if logic is called first time
-- @return - nothing
function onLogicCall(firstCall)
if not g_versionChecked then
-- checking sublogic type and sublogic version
if LOGIC_SUBTYPE ~= SUPPORTED_SUBLOGIC_TYPE then
error("Wrong logic sub-type. expected " .. SUPPORTED_SUBLOGIC_TYPE .. " but used " .. LOGIC_SUBTYPE);
end
local versionWithoutBuild = string.match(LOGIC_SUBVERSION, "[0-9]+%.[0-9]+%.[0-9]+");
if versionWithoutBuild ~= SUPPORTED_SUBLOGIC_VERSION then
error("Wrong logic sub-version. expected " .. SUPPORTED_SUBLOGIC_VERSION .. " but used " .. LOGIC_SUBVERSION);
end
g_versionChecked = true;
end
if g_logic == nil then
g_logic = Logic.create();
end
g_logic:call()
end
-- ---------------------------------------------------------------------------------------------------------