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

466 lines
13 KiB
Lua
Raw Normal View History

--
-- Protected Peripheral Manager
--
2022-06-05 15:16:25 +00:00
local log = require("scada-common.log")
local util = require("scada-common.util")
2022-05-31 20:09:06 +00:00
2022-05-10 21:06:27 +00:00
---@class ppm
local ppm = {}
local ACCESS_FAULT = nil ---@type nil
local UNDEFINED_FIELD = "__PPM_UNDEF_FIELD__"
local VIRTUAL_DEVICE_TYPE = "ppm_vdev"
ppm.ACCESS_FAULT = ACCESS_FAULT
ppm.UNDEFINED_FIELD = UNDEFINED_FIELD
ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
2022-04-07 15:44:17 +00:00
----------------------------
-- PRIVATE DATA/FUNCTIONS --
----------------------------
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
2023-02-21 15:31:05 +00:00
local ppm_sys = {
2022-04-02 15:45:43 +00:00
mounts = {},
next_vid = 0,
auto_cf = false,
faulted = false,
last_fault = "",
terminate = false,
2022-04-02 15:45:43 +00:00
mute = false
}
-- Wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program.
-- Additionally provides peripheral-specific fault checks (auto-clear fault defaults to true).<br>
-- Note: assumes iface is a valid peripheral.
2022-05-10 21:06:27 +00:00
---@param iface string CC peripheral interface
2022-05-31 20:09:06 +00:00
local function peri_init(iface)
local self = {
faulted = false,
last_fault = "",
fault_counts = {},
auto_cf = true,
type = VIRTUAL_DEVICE_TYPE,
device = {}
}
if iface ~= "__virtual__" then
self.type = peripheral.getType(iface)
self.device = peripheral.wrap(iface)
end
-- create a protected version of a peripheral function call
---@nodiscard
---@param key string function name
---@param func function function
---@return function method protected version of the function
local function protect_peri_function(key, func)
return function (...)
local return_table = table.pack(pcall(func, ...))
local status = return_table[1]
table.remove(return_table, 1)
if status then
-- auto fault clear
if self.auto_cf then self.faulted = false end
2023-02-21 15:31:05 +00:00
if ppm_sys.auto_cf then ppm_sys.faulted = false end
self.fault_counts[key] = 0
return table.unpack(return_table)
else
local result = return_table[1]
-- function failed
self.faulted = true
self.last_fault = result
2023-02-21 15:31:05 +00:00
ppm_sys.faulted = true
ppm_sys.last_fault = result
2023-02-21 15:31:05 +00:00
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = ""
if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total faults]"
end
log.error(util.c("PPM: [@", iface, "] protected ", key, "() -> ", result, count_str))
2022-04-02 15:45:43 +00:00
end
self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then ppm_sys.terminate = true end
return ACCESS_FAULT, result
end
end
end
-- initialization process (re-map)
for key, func in pairs(self.device) do
self.fault_counts[key] = 0
self.device[key] = protect_peri_function(key, func)
end
-- fault management & monitoring functions
2022-05-31 20:09:06 +00:00
local function clear_fault() self.faulted = false end
local function get_last_fault() return self.last_fault end
local function is_faulted() return self.faulted end
local function is_ok() return not self.faulted end
-- check if a peripheral has any faulted functions<br>
-- contrasted with is_faulted() and is_ok() as those only check if the last operation failed,
-- unless auto fault clearing is disabled, at which point faults become sticky faults
local function is_healthy()
for _, v in pairs(self.fault_counts) do if v > 0 then return false end end
return true
end
2022-05-31 20:09:06 +00:00
local function enable_afc() self.auto_cf = true end
local function disable_afc() self.auto_cf = false end
-- append PPM functions to device functions
self.device.__p_clear_fault = clear_fault
self.device.__p_last_fault = get_last_fault
self.device.__p_is_faulted = is_faulted
self.device.__p_is_ok = is_ok
self.device.__p_is_healthy = is_healthy
self.device.__p_enable_afc = enable_afc
self.device.__p_disable_afc = disable_afc
-- add default index function to catch undefined indicies
2022-10-26 03:40:36 +00:00
local mt = {
__index = function (_, key)
-- try to find the function in case it was added (multiblock formed)
local funcs = peripheral.wrap(iface)
if (type(funcs) == "table") and (type(funcs[key]) == "function") then
-- add this function then return it
self.fault_counts[key] = 0
self.device[key] = protect_peri_function(key, funcs[key])
log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()"))
return self.device[key]
end
-- function still missing, return an undefined function handler
-- note: code should avoid storing functions for multiblocks and instead try to index them again
return (function ()
-- this will continuously be counting calls here as faults
if self.fault_counts[key] == nil then self.fault_counts[key] = 0 end
-- function failed
self.faulted = true
self.last_fault = UNDEFINED_FIELD
ppm_sys.faulted = true
ppm_sys.last_fault = UNDEFINED_FIELD
if not ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then
local count_str = ""
if self.fault_counts[key] > 0 then
count_str = " [" .. self.fault_counts[key] .. " total calls]"
end
log.error(util.c("PPM: [@", iface, "] caught undefined function ", key, "()", count_str))
end
self.fault_counts[key] = self.fault_counts[key] + 1
return ACCESS_FAULT, UNDEFINED_FIELD
end)
2022-10-26 03:40:36 +00:00
end
}
setmetatable(self.device, mt)
---@class ppm_entry
local entry = { type = self.type, dev = self.device }
return entry
end
2022-04-07 15:44:17 +00:00
----------------------
-- PUBLIC FUNCTIONS --
----------------------
-- REPORTING --
2022-04-02 15:45:43 +00:00
-- silence error prints
2023-02-21 15:31:05 +00:00
function ppm.disable_reporting() ppm_sys.mute = true end
2022-04-02 15:45:43 +00:00
-- allow error prints
2023-02-21 15:31:05 +00:00
function ppm.enable_reporting() ppm_sys.mute = false end
2022-04-02 15:45:43 +00:00
-- FAULT MEMORY --
-- enable automatically clearing fault flag
2023-02-21 15:31:05 +00:00
function ppm.enable_afc() ppm_sys.auto_cf = true end
-- disable automatically clearing fault flag
2023-02-21 15:31:05 +00:00
function ppm.disable_afc() ppm_sys.auto_cf = false end
-- clear fault flag
2023-02-21 15:31:05 +00:00
function ppm.clear_fault() ppm_sys.faulted = false end
-- check fault flag
2023-02-21 15:31:05 +00:00
---@nodiscard
function ppm.is_faulted() return ppm_sys.faulted end
-- get the last fault message
2023-02-21 15:31:05 +00:00
---@nodiscard
function ppm.get_last_fault() return ppm_sys.last_fault end
-- TERMINATION --
-- if a caught error was a termination request
2023-02-21 15:31:05 +00:00
---@nodiscard
function ppm.should_terminate() return ppm_sys.terminate end
2022-04-07 15:44:17 +00:00
-- MOUNTING --
-- mount all available peripherals (clears mounts first)
2022-05-31 20:09:06 +00:00
function ppm.mount_all()
local ifaces = peripheral.getNames()
2023-02-21 15:31:05 +00:00
ppm_sys.mounts = {}
for i = 1, #ifaces do
2023-02-21 15:31:05 +00:00
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
2023-02-21 15:31:05 +00:00
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end
if #ifaces == 0 then
log.warning("PPM: mount_all() -> no devices found")
end
end
-- mount a particular device
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
2022-05-31 20:09:06 +00:00
function ppm.mount(iface)
local ifaces = peripheral.getNames()
local pm_dev = nil
local pm_type = nil
for i = 1, #ifaces do
if iface == ifaces[i] then
2023-02-21 15:31:05 +00:00
ppm_sys.mounts[iface] = peri_init(iface)
2023-02-21 15:31:05 +00:00
pm_type = ppm_sys.mounts[iface].type
pm_dev = ppm_sys.mounts[iface].dev
2022-06-05 15:16:25 +00:00
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
break
end
end
return pm_type, pm_dev
end
-- mount a virtual, placeholder device (specifically designed for RTU startup with missing devices)
2023-02-21 15:31:05 +00:00
---@nodiscard
---@return string type, table device
function ppm.mount_virtual()
2023-02-21 15:31:05 +00:00
local iface = "ppm_vdev_" .. ppm_sys.next_vid
2023-02-21 15:31:05 +00:00
ppm_sys.mounts[iface] = peri_init("__virtual__")
ppm_sys.next_vid = ppm_sys.next_vid + 1
log.info(util.c("PPM: mount_virtual() -> allocated new virtual device ", iface))
2023-02-21 15:31:05 +00:00
return ppm_sys.mounts[iface].type, ppm_sys.mounts[iface].dev
end
-- manually unmount a peripheral from the PPM
---@param device table device table
function ppm.unmount(device)
if device then
2023-02-21 15:31:05 +00:00
for side, data in pairs(ppm_sys.mounts) do
if data.dev == device then
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", side))
2023-02-21 15:31:05 +00:00
ppm_sys.mounts[side] = nil
break
end
end
end
end
-- handle peripheral_detach event
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
2022-05-31 20:09:06 +00:00
function ppm.handle_unmount(iface)
2022-05-10 21:06:27 +00:00
local pm_dev = nil
local pm_type = nil
-- what got disconnected?
2023-02-21 15:31:05 +00:00
local lost_dev = ppm_sys.mounts[iface]
2022-04-22 14:58:18 +00:00
if lost_dev then
2022-05-10 21:06:27 +00:00
pm_type = lost_dev.type
pm_dev = lost_dev.dev
2022-06-05 15:16:25 +00:00
log.warning(util.c("PPM: lost device ", pm_type, " mounted to ", iface))
2022-04-22 14:58:18 +00:00
else
2022-06-05 15:16:25 +00:00
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
2022-04-22 14:58:18 +00:00
end
2023-02-21 15:31:05 +00:00
ppm_sys.mounts[iface] = nil
2022-05-10 21:06:27 +00:00
return pm_type, pm_dev
end
-- log all mounts, to be used if `ppm.mount_all` is called before logging is ready
function ppm.log_mounts()
for iface, mount in pairs(ppm_sys.mounts) do
log.info(util.c("PPM: had found a ", mount.type, " (", iface, ")"))
end
2024-03-05 22:12:12 +00:00
if util.table_len(ppm_sys.mounts) == 0 then
log.warning("PPM: no devices had been found")
end
end
2022-04-07 15:44:17 +00:00
-- GENERAL ACCESSORS --
-- list all available peripherals
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return table names
function ppm.list_avail() return peripheral.getNames() end
-- list mounted peripherals
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return table mounts
function ppm.list_mounts()
local list = {}
for k, v in pairs(ppm_sys.mounts) do list[k] = v end
return list
end
-- get a mounted peripheral side/interface by device table
2023-02-21 15:31:05 +00:00
---@nodiscard
---@param device table device table
---@return string|nil iface CC peripheral interface
function ppm.get_iface(device)
if device then
2023-02-21 15:31:05 +00:00
for side, data in pairs(ppm_sys.mounts) do
if data.dev == device then return side end
end
end
return nil
end
-- get a mounted peripheral by side/interface
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param iface string CC peripheral interface
---@return table|nil device function table
2022-05-31 20:09:06 +00:00
function ppm.get_periph(iface)
2023-02-21 15:31:05 +00:00
if ppm_sys.mounts[iface] then
return ppm_sys.mounts[iface].dev
else return nil end
end
2022-03-15 15:58:22 +00:00
-- get a mounted peripheral type by side/interface
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param iface string CC peripheral interface
---@return string|nil type
2022-05-31 20:09:06 +00:00
function ppm.get_type(iface)
2023-02-21 15:31:05 +00:00
if ppm_sys.mounts[iface] then
return ppm_sys.mounts[iface].type
else return nil end
2022-03-15 15:58:22 +00:00
end
2022-04-07 15:44:17 +00:00
-- get all mounted peripherals by type
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param name string type name
---@return table devices device function tables
2022-05-31 20:09:06 +00:00
function ppm.get_all_devices(name)
2022-04-07 15:44:17 +00:00
local devices = {}
2023-02-21 15:31:05 +00:00
for _, data in pairs(ppm_sys.mounts) do
2022-04-07 15:44:17 +00:00
if data.type == name then
table.insert(devices, data.dev)
end
end
return devices
end
-- get a mounted peripheral by type (if multiple, returns the first)
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@param name string type name
---@return table|nil device function table
2022-05-31 20:09:06 +00:00
function ppm.get_device(name)
local device = nil
2023-02-21 15:31:05 +00:00
for _, data in pairs(ppm_sys.mounts) do
if data.type == name then
device = data.dev
break
end
end
2022-05-10 15:35:52 +00:00
return device
end
2022-04-07 15:44:17 +00:00
-- SPECIFIC DEVICE ACCESSORS --
2022-04-07 15:44:17 +00:00
-- get the fission reactor (if multiple, returns the first)
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return table|nil reactor function table
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
2022-04-07 15:44:17 +00:00
2023-02-21 15:31:05 +00:00
-- get the wireless modem (if multiple, returns the first)<br>
-- if this is in a CraftOS emulated environment, wired modems will be used instead
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return table|nil modem function table
2022-05-31 20:09:06 +00:00
function ppm.get_wireless_modem()
2022-04-07 15:44:17 +00:00
local w_modem = nil
local emulated_env = periphemu ~= nil
2022-04-07 15:44:17 +00:00
2023-02-21 15:31:05 +00:00
for _, device in pairs(ppm_sys.mounts) do
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
2022-04-07 15:44:17 +00:00
w_modem = device.dev
break
end
end
2022-04-07 15:44:17 +00:00
return w_modem
end
-- list all connected monitors
2023-02-21 15:31:05 +00:00
---@nodiscard
2022-05-10 21:06:27 +00:00
---@return table monitors
2022-05-31 20:09:06 +00:00
function ppm.get_monitor_list()
local list = {}
2023-02-21 15:31:05 +00:00
for iface, device in pairs(ppm_sys.mounts) do
if device.type == "monitor" then list[iface] = device end
end
return list
end
-- HELPER FUNCTIONS
-- get the block size of a monitor given its width and height <b>at a text scale of 0.5</b>
---@nodiscard
2024-02-19 01:21:07 +00:00
---@param width integer character width
---@param height integer character height
2024-02-18 20:30:18 +00:00
---@return integer block_width, integer block_height
2024-02-19 01:21:07 +00:00
function ppm.monitor_block_size(width, height)
return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1
end
return ppm