cc-mek-scada/scada-common/util.lua

587 lines
15 KiB
Lua
Raw Normal View History

2022-05-10 21:06:27 +00:00
--
-- Utility Functions
--
---@class util
local util = {}
-- ENVIRONMENT CONSTANTS --
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50
2022-05-31 19:55:40 +00:00
-- OPERATORS --
--#region
2022-05-31 19:55:40 +00:00
-- trinary operator
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param cond boolean|nil condition
2022-05-31 19:55:40 +00:00
---@param a any return if true
---@param b any return if false
---@return any value
2022-05-31 20:09:06 +00:00
function util.trinary(cond, a, b)
2022-05-31 19:55:40 +00:00
if cond then return a else return b end
end
--#endregion
-- PRINT --
--#region
2022-04-05 21:28:19 +00:00
-- print
---@param message any
2022-05-31 20:09:06 +00:00
function util.print(message)
term.write(tostring(message))
2022-04-05 21:28:19 +00:00
end
-- print line
---@param message any
2022-05-31 20:09:06 +00:00
function util.println(message)
print(tostring(message))
2022-04-05 21:28:19 +00:00
end
2022-01-13 15:11:42 +00:00
-- timestamped print
---@param message any
2022-05-31 20:09:06 +00:00
function util.print_ts(message)
term.write(os.date("[%H:%M:%S] ") .. tostring(message))
2022-01-13 15:11:42 +00:00
end
2022-04-05 21:28:19 +00:00
-- timestamped print line
---@param message any
2022-05-31 20:09:06 +00:00
function util.println_ts(message)
print(os.date("[%H:%M:%S] ") .. tostring(message))
end
--#endregion
-- STRING TOOLS --
--#region
-- get a value as a string
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param val any
---@return string
2022-05-31 20:09:06 +00:00
function util.strval(val)
local t = type(val)
if t == "table" or t == "function" then
return "[" .. tostring(val) .. "]"
else
return tostring(val)
end
end
-- repeat a string n times
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param str string
---@param n integer
---@return string
function util.strrep(str, n)
local repeated = ""
for _ = 1, n do
repeated = repeated .. str
end
return repeated
end
2022-06-11 16:20:49 +00:00
-- repeat a space n times
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-06-11 16:20:49 +00:00
---@param n integer
---@return string
function util.spaces(n)
return util.strrep(" ", n)
end
2022-08-16 15:22:06 +00:00
-- pad text to a minimum width
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-08-16 15:22:06 +00:00
---@param str string text
---@param n integer minimum width
---@return string
function util.pad(str, n)
local len = string.len(str)
local lpad = math.floor((n - len) / 2)
local rpad = (n - len) - lpad
return util.spaces(lpad) .. str .. util.spaces(rpad)
end
2022-06-08 22:48:20 +00:00
-- wrap a string into a table of lines, supporting single dash splits
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-06-08 22:48:20 +00:00
---@param str string
---@param limit integer line limit
---@return table lines
function util.strwrap(str, limit)
local lines = {}
local ln_start = 1
local first_break = str:find("([%-%s]+)")
if first_break ~= nil then
lines[1] = string.sub(str, 1, first_break - 1)
else
lines[1] = str
end
2022-06-08 22:48:20 +00:00
---@diagnostic disable-next-line: discard-returns
str:gsub("(%s+)()(%S+)()",
function(space, start, word, stop)
-- support splitting SINGLE DASH words
word:gsub("(%S+)(%-)()(%S+)()",
function (pre, dash, d_start, post, d_stop)
if (stop + d_stop) - ln_start <= limit then
-- do nothing, it will entirely fit
elseif ((start + d_start) + 1) - ln_start <= limit then
-- we can fit including the dash
lines[#lines] = lines[#lines] .. space .. pre .. dash
-- drop the space and replace the word with the post
space = ""
word = post
-- force a wrap
stop = limit + 1 + ln_start
-- change start position for new line start
start = start + d_start - 1
end
end)
-- can we append this or do we have to start a new line?
if stop - ln_start > limit then
-- starting new line
ln_start = start
lines[#lines + 1] = word
else lines[#lines] = lines[#lines] .. space .. word end
end)
return lines
end
-- concatenation with built-in to string
2023-02-21 15:31:05 +00:00
---@nodiscard
---@vararg any
---@return string
2022-05-31 20:09:06 +00:00
function util.concat(...)
local str = ""
2023-02-21 15:31:05 +00:00
for _, v in ipairs(arg) do str = str .. util.strval(v) end
return str
end
2022-05-31 19:55:40 +00:00
-- alias
util.c = util.concat
-- sprintf implementation
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param format string
---@vararg any
2022-05-31 20:09:06 +00:00
function util.sprintf(format, ...)
return string.format(format, table.unpack(arg))
2022-04-05 21:28:19 +00:00
end
2023-02-21 15:31:05 +00:00
-- format a number string with commas as the thousands separator<br>
-- subtracts from spaces at the start if present for each comma used
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param num string number string
---@return string
function util.comma_format(num)
local formatted = num
local commas = 0
local i = 1
while i > 0 do
formatted, i = formatted:gsub("^(%s-%d+)(%d%d%d)", '%1,%2')
if i > 0 then commas = commas + 1 end
end
local _, num_spaces = formatted:gsub(" %s-", "")
local remove = math.min(num_spaces, commas)
formatted = string.sub(formatted, remove + 1)
return formatted
end
--#endregion
2022-05-31 19:55:40 +00:00
-- MATH --
--#region
2022-05-31 19:55:40 +00:00
2022-06-05 20:51:38 +00:00
-- is a value an integer
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-06-05 20:51:38 +00:00
---@param x any value
2022-06-06 19:40:08 +00:00
---@return boolean is_integer if the number is an integer
2022-06-05 20:51:38 +00:00
function util.is_int(x)
return type(x) == "number" and x == math.floor(x)
end
-- get the sign of a number
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param x number value
---@return integer sign (-1 for < 0, 1 otherwise)
function util.sign(x)
return util.trinary(x < 0, -1, 1)
end
2022-05-31 19:55:40 +00:00
-- round a number to an integer
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-31 19:55:40 +00:00
---@return integer rounded
2022-05-31 20:09:06 +00:00
function util.round(x)
2022-05-31 19:55:40 +00:00
return math.floor(x + 0.5)
end
-- get a new moving average object
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param length integer history length
---@param default number value to fill history with for first call to compute()
function util.mov_avg(length, default)
local data = {}
local index = 1
local last_t = 0 ---@type number|nil
---@class moving_average
local public = {}
-- reset all to a given value
---@param x number value
function public.reset(x)
data = {}
for _ = 1, length do table.insert(data, x) end
end
-- record a new value
---@param x number new value
---@param t number? optional last update time to prevent duplicated entries
function public.record(x, t)
if type(t) == "number" and last_t == t then
return
end
data[index] = x
last_t = t
index = index + 1
if index > length then index = 1 end
end
-- compute the moving average
2023-02-21 15:31:05 +00:00
---@nodiscard
---@return number average
function public.compute()
local sum = 0
for i = 1, length do sum = sum + data[i] end
2023-02-04 02:05:21 +00:00
return sum / length
end
public.reset(default)
return public
end
-- TIME --
2022-05-10 21:06:27 +00:00
-- current time
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return integer milliseconds
2022-05-31 20:09:06 +00:00
function util.time_ms()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local')
end
2022-05-10 21:06:27 +00:00
-- current time
2023-02-21 15:31:05 +00:00
---@nodiscard
---@return number seconds
2022-05-31 20:09:06 +00:00
function util.time_s()
---@diagnostic disable-next-line: undefined-field
return os.epoch('local') / 1000.0
end
2022-05-10 21:06:27 +00:00
-- current time
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return integer milliseconds
2023-02-21 15:31:05 +00:00
function util.time() return util.time_ms() end
--#endregion
-- OS --
--#region
-- OS pull event raw wrapper with types
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param target_event? string event to wait for
---@return os_event event, any param1, any param2, any param3, any param4, any param5
function util.pull_event(target_event)
---@diagnostic disable-next-line: undefined-field
return os.pullEventRaw(target_event)
end
-- OS queue event raw wrapper with types
---@param event os_event
---@param param1 any
---@param param2 any
---@param param3 any
---@param param4 any
---@param param5 any
function util.push_event(event, param1, param2, param3, param4, param5)
---@diagnostic disable-next-line: undefined-field
return os.queueEvent(event, param1, param2, param3, param4, param5)
end
-- start an OS timer
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param t number timer duration in seconds
---@return integer timer ID
function util.start_timer(t)
---@diagnostic disable-next-line: undefined-field
return os.startTimer(t)
end
-- cancel an OS timer
---@param timer integer timer ID
function util.cancel_timer(timer)
---@diagnostic disable-next-line: undefined-field
os.cancelTimer(timer)
end
--#endregion
-- PARALLELIZATION --
--#region
-- protected sleep call so we still are in charge of catching termination
2022-05-10 21:06:27 +00:00
---@param t integer seconds
--- EVENT_CONSUMER: this function consumes events
2022-05-31 20:09:06 +00:00
function util.psleep(t)
---@diagnostic disable-next-line: undefined-field
pcall(os.sleep, t)
end
2023-02-21 15:31:05 +00:00
-- no-op to provide a brief pause (1 tick) to yield<br>
2022-05-10 21:06:27 +00:00
--- EVENT_CONSUMER: this function consumes events
2023-02-21 15:31:05 +00:00
function util.nop() util.psleep(0.05) end
-- attempt to maintain a minimum loop timing (duration of execution)
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param target_timing integer minimum amount of milliseconds to wait for
---@param last_update integer millisecond time of last update
---@return integer time_now
2022-07-06 03:48:01 +00:00
--- EVENT_CONSUMER: this function consumes events
2022-05-31 20:09:06 +00:00
function util.adaptive_delay(target_timing, last_update)
local sleep_for = target_timing - (util.time() - last_update)
-- only if >50ms since worker loops already yield 0.05s
2023-02-21 15:31:05 +00:00
if sleep_for >= 50 then util.psleep(sleep_for / 1000.0) end
return util.time()
end
--#endregion
2022-05-16 21:11:46 +00:00
-- TABLE UTILITIES --
--#region
2022-05-16 21:11:46 +00:00
2023-02-21 15:31:05 +00:00
-- delete elements from a table if the passed function returns false when passed a table element<br>
2022-05-16 21:11:46 +00:00
-- put briefly: deletes elements that return false, keeps elements that return true
---@param t table table to remove elements from
---@param f function should return false to delete an element when passed the element: f(elem) = true|false
---@param on_delete? function optional function to execute on deletion, passed the table element to be deleted as the parameter
2022-05-31 20:09:06 +00:00
function util.filter_table(t, f, on_delete)
2022-05-16 21:11:46 +00:00
local move_to = 1
for i = 1, #t do
local element = t[i]
if element ~= nil then
if f(element) then
if t[move_to] == nil then
t[move_to] = element
t[i] = nil
end
move_to = move_to + 1
else
if on_delete then on_delete(element) end
t[i] = nil
end
end
end
end
2022-05-18 18:30:48 +00:00
-- check if a table contains the provided element
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-18 18:30:48 +00:00
---@param t table table to check
---@param element any element to check for
2022-05-31 20:09:06 +00:00
function util.table_contains(t, element)
2022-05-18 18:30:48 +00:00
for i = 1, #t do
if t[i] == element then return true end
end
return false
end
--#endregion
2022-05-10 21:06:27 +00:00
-- MEKANISM POWER --
--#region
2022-05-10 21:06:27 +00:00
-- convert Joules to FE
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param J number Joules
---@return number FE Forge Energy
function util.joules_to_fe(J) return (J * 0.4) end
-- convert FE to Joules
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param FE number Forge Energy
---@return number J Joules
function util.fe_to_joules(FE) return (FE * 2.5) end
local function kFE(fe) return fe / 1000.0 end
local function MFE(fe) return fe / 1000000.0 end
local function GFE(fe) return fe / 1000000000.0 end
local function TFE(fe) return fe / 1000000000000.0 end
local function PFE(fe) return fe / 1000000000000000.0 end
2023-02-21 15:31:05 +00:00
local function EFE(fe) return fe / 1000000000000000000.0 end -- if you accomplish this please touch grass
local function ZFE(fe) return fe / 1000000000000000000000.0 end -- please stop
-- format a power value into XXX.XX UNIT format (FE, kFE, MFE, GFE, TFE, PFE, EFE, ZFE)
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param fe number forge energy value
---@param combine_label? boolean if a label should be included in the string itself
---@param format? string format override
---@return string str, string? unit
function util.power_format(fe, combine_label, format)
local unit
local value
2023-02-21 15:31:05 +00:00
if type(format) ~= "string" then format = "%.2f" end
if fe < 1000.0 then
unit = "FE"
value = fe
elseif fe < 1000000.0 then
unit = "kFE"
value = kFE(fe)
elseif fe < 1000000000.0 then
unit = "MFE"
value = MFE(fe)
elseif fe < 1000000000000.0 then
unit = "GFE"
value = GFE(fe)
elseif fe < 1000000000000000.0 then
unit = "TFE"
value = TFE(fe)
elseif fe < 1000000000000000000.0 then
unit = "PFE"
value = PFE(fe)
elseif fe < 1000000000000000000000.0 then
unit = "EFE"
value = EFE(fe)
else
unit = "ZFE"
value = ZFE(fe)
end
if combine_label then
return util.sprintf(util.c(format, " %s"), value, unit)
else
return util.sprintf(format, value), unit
end
end
2022-05-10 21:06:27 +00:00
--#endregion
-- UTILITY CLASSES --
--#region
-- WATCHDOG --
2022-04-05 21:28:19 +00:00
2023-02-21 15:31:05 +00:00
-- OS timer based watchdog<br>
-- triggers a timer event if not fed within 'timeout' seconds
---@nodiscard
2022-05-10 17:06:13 +00:00
---@param timeout number timeout duration
2022-05-31 20:09:06 +00:00
function util.new_watchdog(timeout)
2022-05-10 15:35:52 +00:00
local self = {
2022-05-10 17:06:13 +00:00
timeout = timeout,
wd_timer = util.start_timer(timeout)
2022-01-13 15:11:42 +00:00
}
2022-05-10 17:06:13 +00:00
---@class watchdog
local public = {}
2023-02-21 15:31:05 +00:00
-- check if a timer is this watchdog
---@nodiscard
2022-05-10 17:06:13 +00:00
---@param timer number timer event timer ID
2023-02-21 15:31:05 +00:00
function public.is_timer(timer) return self.wd_timer == timer end
2022-05-10 15:35:52 +00:00
2022-05-10 17:06:13 +00:00
-- satiate the beast
2022-05-31 20:09:06 +00:00
function public.feed()
2022-05-10 17:06:13 +00:00
if self.wd_timer ~= nil then
util.cancel_timer(self.wd_timer)
2022-01-13 15:11:42 +00:00
end
self.wd_timer = util.start_timer(self.timeout)
2022-01-13 15:11:42 +00:00
end
2022-05-10 17:06:13 +00:00
-- cancel the watchdog
2022-05-31 20:09:06 +00:00
function public.cancel()
2022-05-10 17:06:13 +00:00
if self.wd_timer ~= nil then
util.cancel_timer(self.wd_timer)
2022-05-02 15:44:10 +00:00
end
end
2022-05-10 17:06:13 +00:00
return public
end
-- LOOP CLOCK --
2023-02-21 15:31:05 +00:00
-- OS timer based loop clock<br>
-- fires a timer event at the specified period, does not start at construct time
---@nodiscard
2022-05-10 17:06:13 +00:00
---@param period number clock period
2022-05-31 20:09:06 +00:00
function util.new_clock(period)
2022-05-10 17:06:13 +00:00
local self = {
period = period,
timer = nil
2022-01-13 15:11:42 +00:00
}
2022-05-10 17:06:13 +00:00
---@class clock
local public = {}
2023-02-21 15:31:05 +00:00
-- check if a timer is this clock
---@nodiscard
2022-05-10 17:06:13 +00:00
---@param timer number timer event timer ID
2023-02-21 15:31:05 +00:00
function public.is_clock(timer) return self.timer == timer end
2022-05-10 17:06:13 +00:00
-- start the clock
2023-02-21 15:31:05 +00:00
function public.start() self.timer = util.start_timer(self.period) end
2022-05-10 17:06:13 +00:00
return public
2022-01-13 15:11:42 +00:00
end
-- FIELD VALIDATOR --
2023-02-21 15:31:05 +00:00
-- create a new type validator<br>
2022-06-05 19:09:02 +00:00
-- can execute sequential checks and check valid() to see if it is still valid
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-06-05 19:09:02 +00:00
function util.new_validator()
local valid = true
---@class validator
local public = {}
function public.assert_type_bool(value) valid = valid and type(value) == "boolean" end
function public.assert_type_num(value) valid = valid and type(value) == "number" end
2022-06-05 20:54:34 +00:00
function public.assert_type_int(value) valid = valid and util.is_int(value) end
2022-06-05 19:09:02 +00:00
function public.assert_type_str(value) valid = valid and type(value) == "string" end
function public.assert_type_table(value) valid = valid and type(value) == "table" end
function public.assert_eq(check, expect) valid = valid and check == expect end
function public.assert_min(check, min) valid = valid and check >= min end
function public.assert_min_ex(check, min) valid = valid and check > min end
function public.assert_max(check, max) valid = valid and check <= max end
function public.assert_max_ex(check, max) valid = valid and check < max end
function public.assert_range(check, min, max) valid = valid and check >= min and check <= max end
function public.assert_range_ex(check, min, max) valid = valid and check > min and check < max end
function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end
2023-02-21 15:31:05 +00:00
-- check if all assertions passed successfully
---@nodiscard
2022-06-05 19:09:02 +00:00
function public.valid() return valid end
return public
end
--#endregion
return util