SaferLua Controller revised (events, loop control, data structures,...)

This commit is contained in:
Joachim Stolberg 2018-06-30 00:23:18 +02:00
parent 96cb7b3ea1
commit b06e59d31f
7 changed files with 297 additions and 159 deletions

@ -8,41 +8,67 @@
LGPLv2.1+
See LICENSE.txt for more information
store.lua:
data_struct.lua:
see https://github.com/joe7575/techpack/wiki/Data-Structures
]]--
safer_lua.StoreHelp = [[
Store - a secure shell over the LUA table type.
safer_lua.DataStructHelp = [[
Data structures as a secure shell over the LUA table type.
see https://github.com/joe7575/techpack/wiki/Data-Structures
For records:
tbl = Store() --> {}
tbl.set(key,value) --> {key=value}
tbl.get(key) --> value
'key' can be a number or string
'value' can be number, string, boolean, or Store
Example: tbl.set("a","test")
'Arrays' are lists of elements, which can be addressed
by means of an index:
For lists:
tbl = Store(1,2,3,4) --> {1,2,3,4}
tbl.insert(pos, value)
tbl.remove(pos)
'pos' must be a number
a = Array(1,2,3,4) --> {1,2,3,4}
a.add(6) --> {1,2,3,4,6}
a.set(2, 8) --> {1,8,3,4,6}
a.insert(5,7) --> {1,8,3,4,7,6}
a.remove(3) --> {1,8,4,7,6}
a.insert(1, "hello") --> {"hello",1,8,4,7,6}
a.size() --> function returns 10
Unlike arrays, which are indexed by a range of numbers,
'stores' are indexed by keys:
Methods:
tbl.set(key, value) --> add/set a value
tbl.get(key) --> read a value
tbl.size() --> return the table size
tbl.capa() --> return the max. capacity
tbl.insert(pos, value) --> insert into list
tbl.remove(pos) --> return and remove from list
tbl.sort() -- sort list
tbl.dump() --> format as string (debugging)
s = Store() --> {}
s.set("val", 12) --> {val = 12}
s.get("val") --> returns 12
s.set(0, "hello") --> {val = 12, [0] = "hello"}
s.del("val") --> {[0] = "hello"}
s.size() --> function returns 6
A 'set' is an unordered collection with no duplicate
elements.
s = Set("Tom", "Lucy")
--> {Tom = true, Lucy = true}
s.del("Tom") --> {Lucy = true}
s.add("Susi") --> {Lucy = true, Susi = true}
s.has("Susi") --> function returns `true`
s.has("Mike") --> function returns `false`
s.size() --> function returns 11
]]
function safer_lua.Store(...)
local function var_count(v)
if type(v) == "number" then
return 1
elseif type(v) == "boolean" then
return 1
elseif v == nil then
return 0
elseif type(v) == "string" then
return #v
elseif type(v) == "table" then
return v.size()
else
return nil
end
end
function safer_lua.Store()
local new_t = {__data__ = {}}
local mt = {}
@ -52,56 +78,21 @@ function safer_lua.Store(...)
mt.__newindex = function(t, k, v) return end
mt.count = function(v)
if type(v) == "number" then
return 1
elseif type(v) == "boolean" then
return 1
elseif v == nil then
return 0
elseif type(v) == "string" then
return #v
elseif type(v) == "table" then
return v.size()
else
return nil
end
end
for idx = 1,select('#',...) do
local v = select(idx,...)
local cnt = mt.count(v)
if cnt then
Count = Count + cnt
if Count < safer_lua.MaxTableSize then
rawset(new_t.__data__,idx, v)
end
end
end
mt.count = var_count
new_t.set = function(k,v)
if type(k) == "number" then
local cnt = mt.count(v)
cnt = cnt - mt.count(rawget(new_t.__data__, k))
if Count + cnt < safer_lua.MaxTableSize then
rawset(new_t.__data__,k,v)
Count = Count + cnt
end
Count = Count - mt.count(rawget(new_t.__data__, k))
Count = Count + mt.count(v)
rawset(new_t.__data__,k,v)
elseif type(k) == "string" then
local cnt = mt.count(rawget(new_t.__data__, k))
if cnt == 0 then -- new entry?
cnt = mt.count(v)
cnt = cnt + mt.count(k)
elseif v == nil then -- delete entry?
cnt = - cnt - mt.count(k)
else -- overwrite
cnt = mt.count(v) - cnt
end
if Count + cnt < safer_lua.MaxTableSize then
rawset(new_t.__data__,k,v)
Count = Count + cnt
if rawget(new_t.__data__, k) then -- has entry?
Count = Count - mt.count(rawget(new_t.__data__, k))
else
Count = Count + mt.count(k)
end
Count = Count + mt.count(v)
rawset(new_t.__data__,k,v)
end
end
@ -109,34 +100,127 @@ function safer_lua.Store(...)
return rawget(new_t.__data__, k)
end
new_t.del = function(k)
Count = Count - mt.count(k)
Count = Count - mt.count(rawget(new_t.__data__, k))
rawset(new_t.__data__,k,nil)
end
new_t.size = function(t)
return Count
end
new_t.capa = function(t)
return safer_lua.MaxTableSize
new_t.dump = function(size)
size = size or 200
local s = dump(new_t.__data__)
if #s > size then s = s:sub(1, size).."..." end
return s
end
return setmetatable(new_t, mt)
end
function safer_lua.Array(...)
local new_t = {__data__ = {}}
local mt = {}
-- `all` will represent the number of both
local Count = 0
mt.__newindex = function(t, k, v) return end
mt.count = var_count
for idx = 1,select('#',...) do
local v = select(idx,...)
local cnt = mt.count(v)
if cnt then
Count = Count + cnt
rawset(new_t.__data__,idx, v)
end
end
new_t.add = function(v)
Count = Count + mt.count(v)
local i = #new_t.__data__ + 1
table.insert(new_t.__data__,i,v)
end
new_t.set = function(i,v)
i = math.min(#new_t.__data__, i)
Count = Count - mt.count(rawget(new_t.__data__, i))
Count = Count + mt.count(v)
rawset(new_t.__data__,i,v)
end
new_t.insert = function(v, i)
local cnt = mt.count(v)
if i == nil then i = #new_t.__data__ + 1 end
if Count + cnt < safer_lua.MaxTableSize then
table.insert(new_t.__data__,i,v)
Count = Count + cnt
end
new_t.insert = function(i, v)
Count = Count + mt.count(v)
i = math.min(#new_t.__data__, i)
table.insert(new_t.__data__,i,v)
end
new_t.remove = function(i)
local v = table.remove(new_t.__data__,i)
local cnt = mt.count(v)
Count = Count - cnt
Count = Count - mt.count(v)
return v
end
new_t.sort = function()
table.sort(new_t.__data__)
new_t.size = function(t)
return Count
end
new_t.dump = function(size)
size = size or 200
local s = dump(new_t.__data__)
if #s > size then s = s:sub(1, size).."..." end
return s
end
return setmetatable(new_t, mt)
end
function safer_lua.Set(...)
local new_t = {__data__ = {}}
local mt = {}
-- `all` will represent the number of both
local Count = 0
mt.__newindex = function(t, k, v) return end
mt.count = var_count
for idx = 1,select('#',...) do
local v = select(idx,...)
local cnt = mt.count(v)
if cnt then
Count = Count + cnt
rawset(new_t.__data__,v, true)
end
end
new_t.add = function(k)
Count = Count + mt.count(k)
rawset(new_t.__data__,k, true)
end
new_t.del = function(k)
Count = Count - mt.count(k)
rawset(new_t.__data__,k, nil)
end
new_t.has = function(k)
return rawget(new_t.__data__, k) == true
end
new_t.size = function(t)
return Count
end
new_t.dump = function(size)
size = size or 200
local s = dump(new_t.__data__)
@ -144,5 +228,5 @@ function safer_lua.Store(...)
return s
end
return setmetatable(new_t, mt)
return setmetatable(new_t, mt)
end

@ -13,18 +13,24 @@
]]--
safer_lua.MaxCodeSize = 2000 -- size in length of byte code
safer_lua.MaxTableSize = 1000 -- number of table entries considering string lenghts
safer_lua.MaxTableSize = 1000 -- sum over all table sizes
local function memsize()
return safer_lua.MaxTableSize
end
local BASE_ENV = {
Array = safer_lua.Array,
Store = safer_lua.Store,
Set = safer_lua.Set,
memsize = memsize,
math = {
floor = math.floor,
abs = math.abs,
max = math.max,
min = math.min,
random = math.random,
},
tonumber = tonumber,
tostring = tostring,
@ -39,6 +45,16 @@ local function map(dest, source)
return dest
end
local function calc_used_mem_size(env)
local size = 0
for key,val in pairs(env) do
if type(val) == "table" and val.size ~= nil then
size = size + val.size() or 0
end
end
return size
end
function safer_lua.config(max_code_size, max_table_size)
safer_lua.MaxCodeSize = max_code_size
safer_lua.MaxTableSize = max_table_size
@ -49,7 +65,7 @@ local function compile(pos, text, label, err_clbk)
text = text:gsub("%$", "S:")
local code, err = loadstring(text)
if not code then
err = err:gsub("%[string .+%]:", label)
err = err:gsub("%(load%):", label)
err_clbk(pos, err)
else
return code
@ -89,10 +105,18 @@ end
function safer_lua.run_loop(pos, elapsed, code, err_clbk)
local env = getfenv(code)
env.event = false
env.ticks = env.ticks + 1
env.elapsed = elapsed
if elapsed < 0 then -- event?
env.event = true
else
env.event = false
env.ticks = env.ticks + 1
end
local res, err = pcall(code)
if calc_used_mem_size(env) > safer_lua.MaxTableSize then
err_clbk("Memory limit exceeded")
return false
end
if not res then
err = err:gsub("%[string .+%]:", "loop() ")
err_clbk(pos, err)
@ -100,18 +124,3 @@ function safer_lua.run_loop(pos, elapsed, code, err_clbk)
end
return true
end
function safer_lua.run_event(pos, code, err_clbk)
local env = getfenv(code)
env.event = true
env.elapsed = 0
local res, err = pcall(code)
if not res then
err = err:gsub("%[string .+%]:", "loop() ")
err_clbk(pos, err)
return false
end
return true
end

@ -14,6 +14,6 @@
safer_lua = {}
dofile(minetest.get_modpath("safer_lua") .. "/store.lua")
dofile(minetest.get_modpath("safer_lua") .. "/data_struct.lua")
dofile(minetest.get_modpath("safer_lua") .. "/scanner.lua")
dofile(minetest.get_modpath("safer_lua") .. "/environ.lua")

@ -33,7 +33,7 @@ end
local function register_battery(ext, percent)
minetest.register_node("sl_controller:battery"..ext, {
description = "Battery",
description = "Battery "..ext,
inventory_image = 'sl_controller_battery_inventory.png',
wield_image = 'sl_controller_battery_inventory.png',
tiles = {

@ -19,8 +19,8 @@ sl_controller.register_function("get_input", {
num = tostring(num or "")
return sl_controller.get_input(self.meta.number, num)
end,
help = " $get_input(num) --> 'on', 'off', or nil\n"..
" Read local input value from device with number 'num'.\n"..
help = ' $get_input(num) --> "on", "off", or nil\n'..
' Read local input value from device with number "num".\n'..
' example: inp = $get_input("1234")\n'..
" The device has to be connected with the controller."
})
@ -30,12 +30,23 @@ sl_controller.register_function("get_status", {
num = tostring(num or "")
return tubelib.send_request(num, "state", nil)
end,
help = " $get_status(num) --> 'stopped', 'running',\n"..
" 'standby', 'blocked' or 'fault'\n"..
" Read status from a remote device.\n"..
help = " $get_status(num) ,\n"..
" Read status from a remote device. See\n"..
" https://github.com/joe7575/techpack/wiki/nodes\n"..
' example: sts = $get_status("1234")'
})
sl_controller.register_function("get_fuel_status", {
cmnd = function(self, num)
num = tostring(num or "")
return tubelib.send_request(num, "fuel", nil)
end,
help = " $get_fuel_status(num)\n"..
" Read fuel status from Harverster and Quarry.\n"..
' Fuel status is one of: "full","empty"\n'..
' example: sts = $get_fuel_status("1234")'
})
sl_controller.register_function("time_as_str", {
cmnd = function(self)
local t = minetest.get_timeofday()
@ -65,8 +76,8 @@ sl_controller.register_function("playerdetector", {
num = tostring(num or "")
return tubelib.send_request(num, "name", nil)
end,
help = " $playerdetector(num) --> e.g. 'Joe'\n"..
" '' is returned if no layer is nearby.\n"..
help = ' $playerdetector(num) --> e.g. "Joe"\n'..
' "" is returned if no player is nearby.\n'..
' example: name = $playerdetector("1234")'
})
@ -77,9 +88,9 @@ sl_controller.register_action("send_cmnd", {
tubelib.send_message(num, self.meta.owner, nil, text, nil)
end,
help = " $send_cmnd(num, text)\n"..
" Send a command to the device with number 'num'.\n"..
" For most devices: 'on', 'off'\n"..
" Signal Tower: 'green', 'amber', 'red'\n"..
' Send a command to the device with number "num".\n'..
" For more help, see:\n"..
" https://github.com/joe7575/techpack/wiki/nodes\n"..
' example: $send_cmnd("1234", "on")'
})
@ -91,7 +102,7 @@ sl_controller.register_action("display", {
tubelib.send_message(num, self.meta.owner, nil, "row", {row = row, str = text1..text2..text3})
end,
help = " $display(num, row, text,...)\n"..
" Send a text line to the display with number 'num'.\n"..
' Send a text line to the display with number "num".\n'..
" 'row' is a value from 1..9\n"..
" The function accepts up to 3 text parameters\n"..
' example: $display("0123", 1, "Hello ", name, " !")'
@ -103,8 +114,8 @@ sl_controller.register_action("clear_screen", {
tubelib.send_message(num, self.meta.owner, nil, "clear", nil)
end,
help = " $clear_screen(num)\n"..
" Clear the screen of the display\n"..
" with number 'num'.\n"..
' Clear the screen of the display\n'..
' with number "num".\n'..
' example: $clear_screen("1234")'
})
@ -140,8 +151,8 @@ sl_controller.register_action("door", {
end
end
end,
help = " %door(pos, text)\n"..
" Open/Close a door at position 'pos'\n"..
' example: door("123,7,-1200", "close")\n'..
help = " $door(pos, text)\n"..
' Open/Close a door at position "pos"\n'..
' example: $door("123,7,-1200", "close")\n'..
" Hint: Use the Tubelib Programmer to\ndetermine the door position."
})

@ -14,28 +14,22 @@
local sHELP = [[SaferLua Controller
SaferLua is a subset of Lua with the following restrictions:
- No loop keywords like: for, while, repeat,...
- No table construction {..}
- Limited set of available functions
- Store() as alternative to Lua tables
The controller needs a battery nearby.
This controller is used to control and monitor
Tubelib/TechPack machines.
This controller can be programmed in Lua.
The controller will be restarted with every
server start. That means, init() will be
called again and all variables are reset.
To store the data non-volatile, use a Server.
commands: $server_read(), $server_write().
See on GitHub for more help: goo.gl/Et8D6n
See: goo.gl/WRWZgt
The controller only runs, if a battery is
placed nearby.
]]
local Cache = {}
local tCommands = {}
local tFunctions = {" Overview", " Store"}
local tHelpTexts = {[" Overview"] = sHELP, [" Store"] = safer_lua.StoreHelp}
local tFunctions = {" Overview", " Data structures"}
local tHelpTexts = {[" Overview"] = sHELP, [" Data structures"] = safer_lua.DataStructHelp}
local sFunctionList = ""
local tFunctionIndex = {}
@ -83,10 +77,33 @@ sl_controller.register_action("print", {
text3 = tostring(text3 or "")
output(pos, text1..text2..text3)
end,
help = "$print(text,...)\n"..
"Send a text line to the output window.\n"..
"The function accepts up to 3 text strings\n"..
'e.g. $print("Hello ", name, " !")'
help = " $print(text,...)\n"..
" Send a text line to the output window.\n"..
" The function accepts up to 3 text strings\n"..
' e.g. $print("Hello ", name, " !")'
})
sl_controller.register_action("loopcycle", {
cmnd = function(self, cycletime)
cycletime = math.floor(tonumber(cycletime) or 0)
local meta = minetest.get_meta(self.meta.pos)
meta:set_int("cycletime", cycletime)
meta:set_int("cyclecount", 0)
end,
help = "$loopcycle(seconds)\n"..
" This function allows to change the\n"..
" call frequency of the loop() function.\n"..
" value is in seconds, 0 = disable\n"..
' e.g. $loopcycle(10)'
})
sl_controller.register_action("events", {
cmnd = function(self, event)
self.events = event or false
end,
help = "$events(true/false)\n"..
" Enable/disable event handling.\n"..
' e.g. $events(true) -- enable events'
})
@ -224,6 +241,9 @@ local function start_controller(pos)
meta:set_string("output", "<press update>")
meta:set_string("formspec", formspec3(meta))
meta:set_int("cycletime", 1)
meta:set_int("cyclecount", 0)
meta:set_int("cpu", 0)
if compile(pos, meta, number) then
meta:set_int("state", tubelib.RUNNING)
@ -266,12 +286,8 @@ local function update_battery(meta, cpu)
end
end
local function on_timer(pos, elapsed)
local function call_loop(pos, meta, elapsed)
local t = minetest.get_us_time()
local meta = minetest.get_meta(pos)
if meta:get_int("state") ~= tubelib.RUNNING then
return false
end
local number = meta:get_string("number")
if Cache[number] or compile(pos, meta, number) then
@ -293,6 +309,20 @@ local function on_timer(pos, elapsed)
return false
end
local function on_timer(pos, elapsed)
local meta = minetest.get_meta(pos)
-- considering cycle frequency
local cycletime = meta:get_int("cycletime") or 1
local cyclecount = (meta:get_int("cyclecount") or 0) + 1
if cyclecount < cycletime then
meta:set_int("cyclecount", cyclecount)
return true
end
meta:set_int("cyclecount", 0)
return call_loop(pos, meta, elapsed)
end
local function on_receive_fields(pos, formname, fields, player)
if minetest.is_protected(pos, player:get_player_name()) then
return
@ -369,7 +399,6 @@ minetest.register_node("sl_controller:controller", {
local meta = minetest.get_meta(pos)
local number = tubelib.add_node(pos, "sl_controller:controller")
meta:set_string("owner", placer:get_player_name())
--print("after_place_node", number)
meta:set_string("number", number)
meta:set_int("state", tubelib.STOPPED)
meta:set_string("init", "-- called only once")
@ -407,18 +436,24 @@ minetest.register_craft({
recipe = {"smartline:controller"}
})
local function set_input(number, input, val)
-- write inputs from remote nodes
local function set_input(pos, number, input, val)
if input then
if Cache[number] and Cache[number].inputs then
--print("set_input", input, val)
Cache[number].inputs[input] = val
-- only one event per second
local t = minetest.get_us_time()
if not Cache[number].last_event or Cache[number].last_event < t then
Cache[number].inputs[input] = val
local meta = minetest.get_meta(pos)
minetest.after(0.1, call_loop, pos, meta, -1)
Cache[number].last_event = t + 1000000 -- add one second
end
end
end
end
-- used by the command "input"
function sl_controller.get_input(number, input)
--print("get_input", number, input, dump(Cache[number].inputs))
if input then
if Cache[number] and Cache[number].inputs then
return Cache[number].inputs[input] or "off"
@ -429,14 +464,13 @@ end
tubelib.register_node("sl_controller:controller", {}, {
on_recv_message = function(pos, topic, payload)
-----------------------------------------------------
local meta = minetest.get_meta(pos)
local number = meta:get_string("number")
---------------------------------------------
if topic == "on" then
set_input(number, payload, topic)
set_input(pos, number, payload, topic)
elseif topic == "off" then
set_input(number, payload, topic)
set_input(pos, number, payload, topic)
elseif topic == "state" then
local state = meta:get_int("state")
return tubelib.statestring(state)

@ -279,5 +279,5 @@ This file has further helper functions and is recommended for deeper study.
## 7. History
2017-10-02 First draft
2017-10-29 Commands start/stop replaced by on/off
2017-10-29 Commands start/stop replaced by on/off
2018-03-31 Corrections for 'send_request' and 'add_node'