TheHomecraft 4b8aa3e858
Some checks failed
Lua Checks / check (push) Successful in 9s
Deploy Installation Data / deploy (push) Failing after 1m25s
allowed for non-wireless modem setup
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??!)
2025-10-29 13:45:08 +01:00

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