Hardcoded that the Application thinks its in an emulated env to thus allow the use of Wired modems instead of Wireless (because who wants to use wireless modems??!)
493 lines
14 KiB
Lua
493 lines
14 KiB
Lua
--
|
|
-- Protected Peripheral Manager
|
|
--
|
|
|
|
local log = require("scada-common.log")
|
|
local util = require("scada-common.util")
|
|
|
|
---@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
|
|
|
|
----------------------------
|
|
-- PRIVATE DATA/FUNCTIONS --
|
|
----------------------------
|
|
|
|
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
|
|
|
|
local ppm_sys = {
|
|
mounts = {}, ---@type { [string]: ppm_entry }
|
|
next_vid = 0,
|
|
auto_cf = false,
|
|
faulted = false,
|
|
last_fault = "",
|
|
terminate = false,
|
|
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.
|
|
---@param iface string CC peripheral interface
|
|
local function peri_init(iface)
|
|
local self = {
|
|
faulted = false,
|
|
last_fault = "",
|
|
fault_counts = {}, ---@type { [string]: integer }
|
|
auto_cf = true,
|
|
type = VIRTUAL_DEVICE_TYPE, ---@type string
|
|
device = {} ---@type { [string]: function }
|
|
}
|
|
|
|
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
|
|
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
|
|
|
|
ppm_sys.faulted = true
|
|
ppm_sys.last_fault = result
|
|
|
|
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))
|
|
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
|
|
|
|
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
|
|
|
|
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
|
|
|
|
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)
|
|
end
|
|
}
|
|
|
|
setmetatable(self.device, mt)
|
|
|
|
---@class ppm_entry
|
|
local entry = { type = self.type, dev = self.device }
|
|
|
|
return entry
|
|
end
|
|
|
|
----------------------
|
|
-- PUBLIC FUNCTIONS --
|
|
----------------------
|
|
|
|
-- REPORTING --
|
|
|
|
-- silence error prints
|
|
function ppm.disable_reporting() ppm_sys.mute = true end
|
|
|
|
-- allow error prints
|
|
function ppm.enable_reporting() ppm_sys.mute = false end
|
|
|
|
-- FAULT MEMORY --
|
|
|
|
-- enable automatically clearing fault flag
|
|
function ppm.enable_afc() ppm_sys.auto_cf = true end
|
|
|
|
-- disable automatically clearing fault flag
|
|
function ppm.disable_afc() ppm_sys.auto_cf = false end
|
|
|
|
-- clear fault flag
|
|
function ppm.clear_fault() ppm_sys.faulted = false end
|
|
|
|
-- check fault flag
|
|
---@nodiscard
|
|
function ppm.is_faulted() return ppm_sys.faulted end
|
|
|
|
-- get the last fault message
|
|
---@nodiscard
|
|
function ppm.get_last_fault() return ppm_sys.last_fault end
|
|
|
|
-- TERMINATION --
|
|
|
|
-- if a caught error was a termination request
|
|
---@nodiscard
|
|
function ppm.should_terminate() return ppm_sys.terminate end
|
|
|
|
-- MOUNTING --
|
|
|
|
-- mount all available peripherals (clears mounts first)
|
|
function ppm.mount_all()
|
|
local ifaces = peripheral.getNames()
|
|
|
|
ppm_sys.mounts = {}
|
|
|
|
for i = 1, #ifaces do
|
|
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
|
|
|
|
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 specified device
|
|
---@nodiscard
|
|
---@param iface string CC peripheral interface
|
|
---@return string|nil type, table|nil device
|
|
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
|
|
ppm_sys.mounts[iface] = peri_init(iface)
|
|
|
|
pm_type = ppm_sys.mounts[iface].type
|
|
pm_dev = ppm_sys.mounts[iface].dev
|
|
|
|
log.info(util.c("PPM: mount(", iface, ") -> found a ", pm_type))
|
|
break
|
|
end
|
|
end
|
|
|
|
return pm_type, pm_dev
|
|
end
|
|
|
|
-- unmount and remount a specified device
|
|
---@nodiscard
|
|
---@param iface string CC peripheral interface
|
|
---@return string|nil type, table|nil device
|
|
function ppm.remount(iface)
|
|
local ifaces = peripheral.getNames()
|
|
local pm_dev = nil
|
|
local pm_type = nil
|
|
|
|
for i = 1, #ifaces do
|
|
if iface == ifaces[i] then
|
|
log.info(util.c("PPM: remount(", iface, ") -> is a ", pm_type))
|
|
ppm.unmount(ppm_sys.mounts[iface].dev)
|
|
|
|
ppm_sys.mounts[iface] = peri_init(iface)
|
|
|
|
pm_type = ppm_sys.mounts[iface].type
|
|
pm_dev = ppm_sys.mounts[iface].dev
|
|
|
|
log.info(util.c("PPM: remount(", iface, ") -> remounted 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)
|
|
---@nodiscard
|
|
---@return string type, table device
|
|
function ppm.mount_virtual()
|
|
local iface = "ppm_vdev_" .. ppm_sys.next_vid
|
|
|
|
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))
|
|
|
|
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
|
|
for iface, data in pairs(ppm_sys.mounts) do
|
|
if data.dev == device then
|
|
log.warning(util.c("PPM: manually unmounted ", data.type, " mounted to ", iface))
|
|
ppm_sys.mounts[iface] = nil
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- handle peripheral_detach event
|
|
---@nodiscard
|
|
---@param iface string CC peripheral interface
|
|
---@return string|nil type, table|nil device
|
|
function ppm.handle_unmount(iface)
|
|
local pm_dev = nil
|
|
local pm_type = nil
|
|
|
|
-- what got disconnected?
|
|
local lost_dev = ppm_sys.mounts[iface]
|
|
|
|
if lost_dev then
|
|
pm_type = lost_dev.type
|
|
pm_dev = lost_dev.dev
|
|
|
|
log.warning(util.c("PPM: lost device ", pm_type, " mounted to ", iface))
|
|
else
|
|
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
|
|
end
|
|
|
|
ppm_sys.mounts[iface] = nil
|
|
|
|
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
|
|
|
|
if util.table_len(ppm_sys.mounts) == 0 then
|
|
log.warning("PPM: no devices had been found")
|
|
end
|
|
end
|
|
|
|
-- GENERAL ACCESSORS --
|
|
|
|
-- list all available peripherals
|
|
---@nodiscard
|
|
---@return string[] names
|
|
function ppm.list_avail() return peripheral.getNames() end
|
|
|
|
-- list mounted peripherals
|
|
---@nodiscard
|
|
---@return { [string]: ppm_entry } 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
|
|
---@nodiscard
|
|
---@param device table device table
|
|
---@return string|nil iface CC peripheral interface
|
|
function ppm.get_iface(device)
|
|
if device then
|
|
for iface, data in pairs(ppm_sys.mounts) do
|
|
if data.dev == device then return iface end
|
|
end
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
-- get a mounted peripheral by side/interface
|
|
---@nodiscard
|
|
---@param iface string CC peripheral interface
|
|
---@return { [string]: function }|nil device function table
|
|
function ppm.get_periph(iface)
|
|
if ppm_sys.mounts[iface] then
|
|
return ppm_sys.mounts[iface].dev
|
|
else return nil end
|
|
end
|
|
|
|
-- get a mounted peripheral type by side/interface
|
|
---@nodiscard
|
|
---@param iface string CC peripheral interface
|
|
---@return string|nil type
|
|
function ppm.get_type(iface)
|
|
if ppm_sys.mounts[iface] then
|
|
return ppm_sys.mounts[iface].type
|
|
else return nil end
|
|
end
|
|
|
|
-- get all mounted peripherals by type
|
|
---@nodiscard
|
|
---@param name string type name
|
|
---@return table devices device function tables
|
|
function ppm.get_all_devices(name)
|
|
local devices = {}
|
|
|
|
for _, data in pairs(ppm_sys.mounts) do
|
|
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)
|
|
---@nodiscard
|
|
---@param name string type name
|
|
---@return table|nil device function table
|
|
function ppm.get_device(name)
|
|
local device = nil
|
|
|
|
for _, data in pairs(ppm_sys.mounts) do
|
|
if data.type == name then
|
|
device = data.dev
|
|
break
|
|
end
|
|
end
|
|
|
|
return device
|
|
end
|
|
|
|
-- SPECIFIC DEVICE ACCESSORS --
|
|
|
|
-- get the fission reactor (if multiple, returns the first)
|
|
---@nodiscard
|
|
---@return table|nil reactor function table
|
|
function ppm.get_fission_reactor() return ppm.get_device("fissionReactorLogicAdapter") end
|
|
|
|
-- get the wireless modem (if multiple, returns the first)<br>
|
|
-- if this is in a CraftOS emulated environment, wired modems will be used instead
|
|
---@nodiscard
|
|
---@return Modem|nil modem function table
|
|
function ppm.get_wireless_modem()
|
|
local w_modem = nil
|
|
local emulated_env = true
|
|
|
|
for _, device in pairs(ppm_sys.mounts) do
|
|
if device.type == "modem" and (emulated_env or device.dev.isWireless()) then
|
|
w_modem = device.dev
|
|
break
|
|
end
|
|
end
|
|
|
|
return w_modem
|
|
end
|
|
|
|
-- list all connected monitors
|
|
---@nodiscard
|
|
---@return { [string]: ppm_entry } monitors
|
|
function ppm.get_monitor_list()
|
|
local list = {}
|
|
|
|
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
|
|
---@param width integer character width
|
|
---@param height integer character height
|
|
---@return integer block_width, integer block_height
|
|
function ppm.monitor_block_size(width, height)
|
|
return math.floor((width - 15) / 21) + 1, math.floor((height - 10) / 14) + 1
|
|
end
|
|
|
|
return ppm
|