#118 cleanup started of scada-common

This commit is contained in:
Mikayla Fischler 2023-02-21 10:31:05 -05:00
parent e2d2a0f1dc
commit 34cac6a8b8
13 changed files with 273 additions and 222 deletions

View File

@ -173,7 +173,6 @@ local function main()
log.debug("init> running without networking")
end
---@diagnostic disable-next-line: param-type-mismatch
util.push_event("clock_start")
println("boot> completed")

View File

@ -266,7 +266,6 @@ function threads.thread__main(smem, init)
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then
log.info("main thread restarting now...")
---@diagnostic disable-next-line: param-type-mismatch
util.push_event("clock_start")
end
end

View File

@ -16,7 +16,7 @@ local max_distance = nil
comms.version = "1.4.0"
---@alias PROTOCOLS integer
---@enum PROTOCOLS
local PROTOCOLS = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
@ -25,7 +25,7 @@ local PROTOCOLS = {
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
---@alias RPLC_TYPES integer
---@enum RPLC_TYPES
local RPLC_TYPES = {
STATUS = 0, -- reactor/system status
MEK_STRUCT = 1, -- mekanism build structure
@ -40,7 +40,7 @@ local RPLC_TYPES = {
AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
}
---@alias SCADA_MGMT_TYPES integer
---@enum SCADA_MGMT_TYPES
local SCADA_MGMT_TYPES = {
ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
@ -49,7 +49,7 @@ local SCADA_MGMT_TYPES = {
RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
}
---@alias SCADA_CRDN_TYPES integer
---@enum SCADA_CRDN_TYPES
local SCADA_CRDN_TYPES = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds
@ -60,12 +60,11 @@ local SCADA_CRDN_TYPES = {
UNIT_CMD = 6 -- command a reactor unit
}
---@alias CAPI_TYPES integer
---@enum CAPI_TYPES
local CAPI_TYPES = {
ESTABLISH = 0 -- initial greeting
}
---@alias ESTABLISH_ACK integer
---@enum ESTABLISH_ACK
local ESTABLISH_ACK = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
@ -73,7 +72,7 @@ local ESTABLISH_ACK = {
BAD_VERSION = 3 -- link denied due to comms version mismatch
}
---@alias DEVICE_TYPES integer
---@enum DEVICE_TYPES
local DEVICE_TYPES = {
PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish
@ -81,7 +80,7 @@ local DEVICE_TYPES = {
CRDN = 3 -- coordinator device type for establish
}
---@alias RTU_UNIT_TYPES integer
---@enum RTU_UNIT_TYPES
local RTU_UNIT_TYPES = {
REDSTONE = 0, -- redstone I/O
BOILER_VALVE = 1, -- boiler mekanism 10.1+
@ -92,7 +91,7 @@ local RTU_UNIT_TYPES = {
ENV_DETECTOR = 6 -- environment detector
}
---@alias PLC_AUTO_ACK integer
---@enum PLC_AUTO_ACK
local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid
DIRECT_SET_OK = 1, -- successfully set burn rate
@ -100,7 +99,7 @@ local PLC_AUTO_ACK = {
ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
}
---@alias FAC_COMMANDS integer
---@enum FAC_COMMANDS
local FAC_COMMANDS = {
SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control
@ -108,7 +107,7 @@ local FAC_COMMANDS = {
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units
}
---@alias UNIT_COMMANDS integer
---@enum UNIT_COMMANDS
local UNIT_COMMANDS = {
SCRAM = 0, -- SCRAM the reactor
START = 1, -- start the reactor
@ -152,6 +151,7 @@ function comms.set_trusted_range(distance)
end
-- generic SCADA packet object
---@nodiscard
function comms.scada_packet()
local self = {
modem_msg_in = nil,
@ -180,11 +180,12 @@ function comms.scada_packet()
end
-- parse in a modem message as a SCADA packet
---@param side string
---@param sender integer
---@param reply_to integer
---@param message any
---@param distance integer
---@param side string modem side
---@param sender integer sender port
---@param reply_to integer reply port
---@param message any message body
---@param distance integer transmission distance
---@return boolean valid valid message received
function public.receive(side, sender, reply_to, message, distance)
self.modem_msg_in = {
iface = side,
@ -223,24 +224,34 @@ function comms.scada_packet()
-- public accessors --
---@nodiscard
function public.modem_event() return self.modem_msg_in end
---@nodiscard
function public.raw_sendable() return self.raw end
---@nodiscard
function public.local_port() return self.modem_msg_in.s_port end
---@nodiscard
function public.remote_port() return self.modem_msg_in.r_port end
---@nodiscard
function public.is_valid() return self.valid end
---@nodiscard
function public.seq_num() return self.seq_num end
---@nodiscard
function public.protocol() return self.protocol end
---@nodiscard
function public.length() return self.length end
---@nodiscard
function public.data() return self.payload end
return public
end
-- MODBUS packet
-- MODBUS packet<br>
-- modeled after MODBUS TCP packet
---@nodiscard
function comms.modbus_packet()
local self = {
frame = nil,
@ -309,9 +320,11 @@ function comms.modbus_packet()
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class modbus_frame
local frame = {
@ -330,6 +343,7 @@ function comms.modbus_packet()
end
-- reactor PLC packet
---@nodiscard
function comms.rplc_packet()
local self = {
frame = nil,
@ -410,9 +424,11 @@ function comms.rplc_packet()
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class rplc_frame
local frame = {
@ -430,6 +446,7 @@ function comms.rplc_packet()
end
-- SCADA management packet
---@nodiscard
function comms.mgmt_packet()
local self = {
frame = nil,
@ -500,9 +517,11 @@ function comms.mgmt_packet()
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class mgmt_frame
local frame = {
@ -519,6 +538,7 @@ function comms.mgmt_packet()
end
-- SCADA coordinator packet
---@nodiscard
function comms.crdn_packet()
local self = {
frame = nil,
@ -532,6 +552,7 @@ function comms.crdn_packet()
local public = {}
-- check that type is known
---@nodiscard
local function _crdn_type_valid()
return self.type == SCADA_CRDN_TYPES.INITIAL_BUILDS or
self.type == SCADA_CRDN_TYPES.FAC_BUILDS or
@ -590,9 +611,11 @@ function comms.crdn_packet()
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class crdn_frame
local frame = {
@ -609,7 +632,8 @@ function comms.crdn_packet()
end
-- coordinator API (CAPI) packet
-- @todo
---@todo implement for pocket access
---@nodiscard
function comms.capi_packet()
local self = {
frame = nil,
@ -623,7 +647,7 @@ function comms.capi_packet()
local public = {}
local function _capi_type_valid()
-- @todo
---@todo
return false
end
@ -675,9 +699,11 @@ function comms.capi_packet()
end
-- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get()
---@class capi_frame
local frame = {
@ -694,6 +720,7 @@ function comms.capi_packet()
end
-- convert rtu_t to RTU unit type
---@nodiscard
---@param type rtu_t
---@return RTU_UNIT_TYPES|nil
function comms.rtu_t_to_unit_type(type)
@ -717,6 +744,7 @@ function comms.rtu_t_to_unit_type(type)
end
-- convert RTU unit type to rtu_t
---@nodiscard
---@param utype RTU_UNIT_TYPES
---@return rtu_t|nil
function comms.advert_type_to_rtu_t(utype)

View File

@ -70,6 +70,7 @@ function crypto.init(password, server_port)
end
-- encrypt plaintext
---@nodiscard
---@param plaintext string
---@return table initial_value, string ciphertext
function crypto.encrypt(plaintext)
@ -113,6 +114,7 @@ function crypto.encrypt(plaintext)
end
-- decrypt ciphertext
---@nodiscard
---@param iv string CTR initial value
---@param ciphertext string ciphertext hex
---@return string plaintext
@ -135,6 +137,7 @@ function crypto.decrypt(iv, ciphertext)
end
-- generate HMAC of message
---@nodiscard
---@param message_hex string initial value concatenated with ciphertext
function crypto.hmac(message_hex)
local start = util.time()
@ -201,11 +204,12 @@ function crypto.secure_modem(modem)
end
-- parse in a modem message as a network packet
---@param side string
---@param sender integer
---@param reply_to integer
---@nodiscard
---@param side string modem side
---@param sender integer sender port
---@param reply_to integer reply port
---@param message any encrypted packet sent with secure_modem.transmit
---@param distance integer
---@param distance integer transmission distance
---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
function public.receive(side, sender, reply_to, message, distance)
local body = ""

View File

@ -18,7 +18,7 @@ log.MODE = MODE
-- whether to log debug messages or not
local LOG_DEBUG = true
local _log_sys = {
local log_sys = {
path = "/log.txt",
mode = MODE.APPEND,
file = nil,
@ -33,27 +33,25 @@ local free_space = fs.getFreeSpace
---@param write_mode MODE
---@param dmesg_redirect? table terminal/window to direct dmesg to
function log.init(path, write_mode, dmesg_redirect)
_log_sys.path = path
_log_sys.mode = write_mode
log_sys.path = path
log_sys.mode = write_mode
if _log_sys.mode == MODE.APPEND then
_log_sys.file = fs.open(path, "a")
if log_sys.mode == MODE.APPEND then
log_sys.file = fs.open(path, "a")
else
_log_sys.file = fs.open(path, "w")
log_sys.file = fs.open(path, "w")
end
if dmesg_redirect then
_log_sys.dmesg_out = dmesg_redirect
log_sys.dmesg_out = dmesg_redirect
else
_log_sys.dmesg_out = term.current()
log_sys.dmesg_out = term.current()
end
end
-- direct dmesg output to a monitor/window
---@param window table window or terminal reference
function log.direct_dmesg(window)
_log_sys.dmesg_out = window
end
function log.direct_dmesg(window) log_sys.dmesg_out = window end
-- private log write function
---@param msg string
@ -64,8 +62,8 @@ local function _log(msg)
-- attempt to write log
local status, result = pcall(function ()
_log_sys.file.writeLine(stamped)
_log_sys.file.flush()
log_sys.file.writeLine(stamped)
log_sys.file.flush()
end)
-- if we don't have space, we need to create a new log file
@ -80,18 +78,18 @@ local function _log(msg)
end
end
if out_of_space or (free_space(_log_sys.path) < 100) then
if out_of_space or (free_space(log_sys.path) < 100) then
-- delete the old log file before opening a new one
_log_sys.file.close()
fs.delete(_log_sys.path)
log_sys.file.close()
fs.delete(log_sys.path)
-- re-init logger and pass dmesg_out so that it doesn't change
log.init(_log_sys.path, _log_sys.mode, _log_sys.dmesg_out)
log.init(log_sys.path, log_sys.mode, log_sys.dmesg_out)
-- leave a message
_log_sys.file.writeLine(time_stamp .. "recycled log file")
_log_sys.file.writeLine(stamped)
_log_sys.file.flush()
log_sys.file.writeLine(time_stamp .. "recycled log file")
log_sys.file.writeLine(stamped)
log_sys.file.flush()
end
end
@ -109,7 +107,7 @@ function log.dmesg(msg, tag, tag_color)
tag = util.strval(tag)
local t_stamp = string.format("%12.2f", os.clock())
local out = _log_sys.dmesg_out
local out = log_sys.dmesg_out
if out ~= nil then
local out_w, out_h = out.getSize()
@ -197,6 +195,7 @@ function log.dmesg(msg, tag, tag_color)
end
-- print a dmesg message, but then show remaining seconds instead of timestamp
---@nodiscard
---@param msg string message
---@param tag? string log tag
---@param tag_color? integer log tag color
@ -204,7 +203,7 @@ end
function log.dmesg_working(msg, tag, tag_color)
local ts_coord = log.dmesg(msg, tag, tag_color)
local out = _log_sys.dmesg_out
local out = log_sys.dmesg_out
local width = (ts_coord.x2 - ts_coord.x1) + 1
if out ~= nil then

View File

@ -4,7 +4,7 @@
local mqueue = {}
---@alias MQ_TYPE integer
---@enum MQ_TYPE
local TYPE = {
COMMAND = 0,
DATA = 1,
@ -14,6 +14,7 @@ local TYPE = {
mqueue.TYPE = TYPE
-- create a new message queue
---@nodiscard
function mqueue.new()
local queue = {}
@ -35,10 +36,13 @@ function mqueue.new()
function public.length() return #queue end
-- check if queue is empty
---@nodiscard
---@return boolean is_empty
function public.empty() return #queue == 0 end
-- check if queue has contents
---@nodiscard
---@return boolean has_contents
function public.ready() return #queue ~= 0 end
-- push a new item onto the queue
@ -68,6 +72,7 @@ function mqueue.new()
end
-- get an item off the queue
---@nodiscard
---@return queue_item|nil
function public.pop()
if #queue > 0 then

View File

@ -24,7 +24,7 @@ ppm.VIRTUAL_DEVICE_TYPE = VIRTUAL_DEVICE_TYPE
local REPORT_FREQUENCY = 20 -- log every 20 faults per function
local _ppm_sys = {
local ppm_sys = {
mounts = {},
next_vid = 0,
auto_cf = false,
@ -34,11 +34,9 @@ local _ppm_sys = {
mute = false
}
-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program
---
---also provides peripheral-specific fault checks (auto-clear fault defaults to true)
---
---assumes iface is a valid peripheral
-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program<br>
-- also provides peripheral-specific fault checks (auto-clear fault defaults to true)<br>
-- assumes iface is a valid peripheral
---@param iface string CC peripheral interface
local function peri_init(iface)
local self = {
@ -68,7 +66,7 @@ local function peri_init(iface)
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
if ppm_sys.auto_cf then ppm_sys.faulted = false end
self.fault_counts[key] = 0
@ -80,10 +78,10 @@ local function peri_init(iface)
self.faulted = true
self.last_fault = result
_ppm_sys.faulted = true
_ppm_sys.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
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]"
@ -95,7 +93,7 @@ local function peri_init(iface)
self.fault_counts[key] = self.fault_counts[key] + 1
if result == "Terminated" then
_ppm_sys.terminate = true
ppm_sys.terminate = true
end
return ACCESS_FAULT
@ -136,10 +134,10 @@ local function peri_init(iface)
self.faulted = true
self.last_fault = UNDEFINED_FIELD
_ppm_sys.faulted = true
_ppm_sys.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
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]"
@ -169,48 +167,35 @@ end
-- REPORTING --
-- silence error prints
function ppm.disable_reporting()
_ppm_sys.mute = true
end
function ppm.disable_reporting() ppm_sys.mute = true end
-- allow error prints
function ppm.enable_reporting()
_ppm_sys.mute = false
end
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
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
function ppm.disable_afc() ppm_sys.auto_cf = false end
-- clear fault flag
function ppm.clear_fault()
_ppm_sys.faulted = false
end
function ppm.clear_fault() ppm_sys.faulted = false end
-- check fault flag
function ppm.is_faulted()
return _ppm_sys.faulted
end
---@nodiscard
function ppm.is_faulted() return ppm_sys.faulted end
-- get the last fault message
function ppm.get_last_fault()
return _ppm_sys.last_fault
end
---@nodiscard
function ppm.get_last_fault() return ppm_sys.last_fault end
-- TERMINATION --
-- if a caught error was a termination request
function ppm.should_terminate()
return _ppm_sys.terminate
end
---@nodiscard
function ppm.should_terminate() return ppm_sys.terminate end
-- MOUNTING --
@ -218,12 +203,12 @@ end
function ppm.mount_all()
local ifaces = peripheral.getNames()
_ppm_sys.mounts = {}
ppm_sys.mounts = {}
for i = 1, #ifaces do
_ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i])
log.info(util.c("PPM: found a ", _ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
log.info(util.c("PPM: found a ", ppm_sys.mounts[ifaces[i]].type, " (", ifaces[i], ")"))
end
if #ifaces == 0 then
@ -232,6 +217,7 @@ function ppm.mount_all()
end
-- mount a particular device
---@nodiscard
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
function ppm.mount(iface)
@ -241,10 +227,10 @@ function ppm.mount(iface)
for i = 1, #ifaces do
if iface == ifaces[i] then
_ppm_sys.mounts[iface] = peri_init(iface)
ppm_sys.mounts[iface] = peri_init(iface)
pm_type = _ppm_sys.mounts[iface].type
pm_dev = _ppm_sys.mounts[iface].dev
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
@ -255,26 +241,27 @@ function ppm.mount(iface)
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
local iface = "ppm_vdev_" .. ppm_sys.next_vid
_ppm_sys.mounts[iface] = peri_init("__virtual__")
_ppm_sys.next_vid = _ppm_sys.next_vid + 1
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
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 side, data in pairs(_ppm_sys.mounts) do
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))
_ppm_sys.mounts[side] = nil
ppm_sys.mounts[side] = nil
break
end
end
@ -282,6 +269,7 @@ function ppm.unmount(device)
end
-- handle peripheral_detach event
---@nodiscard
---@param iface string CC peripheral interface
---@return string|nil type, table|nil device
function ppm.handle_unmount(iface)
@ -289,7 +277,7 @@ function ppm.handle_unmount(iface)
local pm_type = nil
-- what got disconnected?
local lost_dev = _ppm_sys.mounts[iface]
local lost_dev = ppm_sys.mounts[iface]
if lost_dev then
pm_type = lost_dev.type
@ -300,7 +288,7 @@ function ppm.handle_unmount(iface)
log.error(util.c("PPM: lost device unknown to the PPM mounted to ", iface))
end
_ppm_sys.mounts[iface] = nil
ppm_sys.mounts[iface] = nil
return pm_type, pm_dev
end
@ -308,23 +296,26 @@ end
-- GENERAL ACCESSORS --
-- list all available peripherals
---@nodiscard
---@return table names
function ppm.list_avail()
return peripheral.getNames()
end
-- list mounted peripherals
---@nodiscard
---@return table mounts
function ppm.list_mounts()
return _ppm_sys.mounts
return ppm_sys.mounts
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 side, data in pairs(_ppm_sys.mounts) do
for side, data in pairs(ppm_sys.mounts) do
if data.dev == device then return side end
end
end
@ -333,30 +324,33 @@ function ppm.get_iface(device)
end
-- get a mounted peripheral by side/interface
---@nodiscard
---@param iface string CC peripheral interface
---@return table|nil device function table
function ppm.get_periph(iface)
if _ppm_sys.mounts[iface] then
return _ppm_sys.mounts[iface].dev
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
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
for _, data in pairs(ppm_sys.mounts) do
if data.type == name then
table.insert(devices, data.dev)
end
@ -366,12 +360,13 @@ function ppm.get_all_devices(name)
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 side, data in pairs(_ppm_sys.mounts) do
for _, data in pairs(ppm_sys.mounts) do
if data.type == name then
device = data.dev
break
@ -384,20 +379,21 @@ 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)
--
-- 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 table|nil modem function table
function ppm.get_wireless_modem()
local w_modem = nil
local emulated_env = periphemu ~= nil
for _, device in pairs(_ppm_sys.mounts) do
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
@ -408,11 +404,12 @@ function ppm.get_wireless_modem()
end
-- list all connected monitors
---@nodiscard
---@return table monitors
function ppm.get_monitor_list()
local list = {}
for iface, device in pairs(_ppm_sys.mounts) do
for iface, device in pairs(ppm_sys.mounts) do
if device.type == "monitor" then
list[iface] = device
end

View File

@ -5,6 +5,7 @@
local psil = {}
-- instantiate a new PSI layer
---@nodiscard
function psil.create()
local self = {
ic = {}
@ -19,8 +20,7 @@ function psil.create()
---@class psil
local public = {}
-- subscribe to a data object in the interconnect
--
-- subscribe to a data object in the interconnect<br>
-- will call func() right away if a value is already avaliable
---@param key string data key
---@param func function function to call on change

View File

@ -89,6 +89,7 @@ rsio.IO = IO_PORT
-----------------------
-- port to string
---@nodiscard
---@param port IO_PORT
function rsio.to_string(port)
local names = {
@ -194,6 +195,7 @@ local RS_DIO_MAP = {
}
-- get the mode of a port
---@nodiscard
---@param port IO_PORT
---@return IO_MODE
function rsio.get_io_mode(port)
@ -239,6 +241,7 @@ end
local RS_SIDES = rs.getSides()
-- check if a port is valid
---@nodiscard
---@param port IO_PORT
---@return boolean valid
function rsio.is_valid_port(port)
@ -246,6 +249,7 @@ function rsio.is_valid_port(port)
end
-- check if a side is valid
---@nodiscard
---@param side string
---@return boolean valid
function rsio.is_valid_side(side)
@ -258,6 +262,7 @@ function rsio.is_valid_side(side)
end
-- check if a color is a valid single color
---@nodiscard
---@param color integer
---@return boolean valid
function rsio.is_color(color)
@ -269,22 +274,25 @@ end
-----------------
-- get digital I/O level reading from a redstone boolean input value
---@param rs_value boolean
---@nodiscard
---@param rs_value boolean raw value from redstone
---@return IO_LVL
function rsio.digital_read(rs_value)
if rs_value then return IO_LVL.HIGH else return IO_LVL.LOW end
end
-- get redstone boolean output value corresponding to a digital I/O level
---@param level IO_LVL
---@nodiscard
---@param level IO_LVL logic level
---@return boolean
function rsio.digital_write(level)
return level == IO_LVL.HIGH
end
-- returns the level corresponding to active
---@param port IO_PORT
---@param active boolean
---@nodiscard
---@param port IO_PORT port (to determine active high/low)
---@param active boolean state to convert to logic level
---@return IO_LVL|false
function rsio.digital_write_active(port, active)
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then
@ -295,9 +303,10 @@ function rsio.digital_write_active(port, active)
end
-- returns true if the level corresponds to active
---@param port IO_PORT
---@param level IO_LVL
---@return boolean|nil
---@nodiscard
---@param port IO_PORT port (to determine active low/high)
---@param level IO_LVL logic level
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
function rsio.digital_is_active(port, level)
if not util.is_int(port) then
return nil
@ -313,6 +322,7 @@ end
----------------
-- read an analog value scaled from min to max
---@nodiscard
---@param rs_value number redstone reading (0 to 15)
---@param min number minimum of range
---@param max number maximum of range
@ -323,6 +333,7 @@ function rsio.analog_read(rs_value, min, max)
end
-- write an analog value from the provided scale range
---@nodiscard
---@param value number value to write (from min to max range)
---@param min number minimum of range
---@param max number maximum of range

View File

@ -19,8 +19,6 @@ function tcallbackdsp.dispatch(time, f)
duration = time,
expiry = time + util.time_s()
}
-- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]"))
end
-- request a function to be called after the specified time, aborting any registered instances of that function reference
@ -45,8 +43,6 @@ function tcallbackdsp.dispatch_unique(time, f)
duration = time,
expiry = time + util.time_s()
}
-- log.debug(util.c("TCD: queued callback for ", f, " [timer: ", timer, "]"))
end
-- abort a requested callback
@ -72,8 +68,7 @@ function tcallbackdsp.handle(event)
end
end
-- identify any overdo callbacks
--
-- identify any overdo callbacks<br>
-- prints to log debug output
function tcallbackdsp.diagnostics()
for timer, entry in pairs(registry) do

View File

@ -12,12 +12,14 @@ local types = {}
---@field amount integer
-- create a new tank fluid
---@nodiscard
---@param n string name
---@param a integer amount
---@return radiation_reading
function types.new_tank_fluid(n, a) return { name = n, amount = a } end
-- create a new empty tank fluid
---@nodiscard
---@return tank_fluid
function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 } end
@ -26,12 +28,14 @@ function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0
---@field unit string
-- create a new radiation reading
---@nodiscard
---@param r number radiaiton level
---@param u string radiation unit
---@return radiation_reading
function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end
-- create a new zeroed radiation reading
---@nodiscard
---@return radiation_reading
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
@ -41,6 +45,7 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
---@field z integer
-- create a new coordinate
---@nodiscard
---@param x integer
---@param y integer
---@param z integer
@ -48,11 +53,12 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
function types.new_coordinate(x, y, z) return { x = x, y = y, z = z } end
-- create a new zero coordinate
---@nodiscard
---@return coordinate
function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@class rtu_advertisement
---@field type integer
---@field type RTU_UNIT_TYPES
---@field index integer
---@field reactor integer
---@field rsio table|nil
@ -62,15 +68,16 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@alias color integer
-- ENUMERATION TYPES --
--#region
---@alias TRI_FAIL integer
---@enum TRI_FAIL
types.TRI_FAIL = {
OK = 0,
PARTIAL = 1,
FULL = 2
}
---@alias PROCESS integer
---@enum PROCESS
types.PROCESS = {
INACTIVE = 0,
MAX_BURN = 1,
@ -93,7 +100,7 @@ types.PROCESS_NAMES = {
"GEN_RATE_FAULT_IDLE"
}
---@alias WASTE_MODE integer
---@enum WASTE_MODE
types.WASTE_MODE = {
AUTO = 1,
PLUTONIUM = 2,
@ -101,7 +108,7 @@ types.WASTE_MODE = {
ANTI_MATTER = 4
}
---@alias ALARM integer
---@enum ALARM
types.ALARM = {
ContainmentBreach = 1,
ContainmentRadiation = 2,
@ -117,7 +124,7 @@ types.ALARM = {
TurbineTrip = 12
}
types.alarm_string = {
types.ALARM_NAMES = {
"ContainmentBreach",
"ContainmentRadiation",
"ReactorLost",
@ -132,7 +139,7 @@ types.alarm_string = {
"TurbineTrip"
}
---@alias ALARM_PRIORITY integer
---@enum ALARM_PRIORITY
types.ALARM_PRIORITY = {
CRITICAL = 0,
EMERGENCY = 1,
@ -140,30 +147,14 @@ types.ALARM_PRIORITY = {
TIMELY = 3
}
types.alarm_prio_string = {
types.ALARM_PRIORITY_NAMES = {
"CRITICAL",
"EMERGENCY",
"URGENT",
"TIMELY"
}
-- map alarms to alarm priority
types.ALARM_PRIO_MAP = {
types.ALARM_PRIORITY.CRITICAL,
types.ALARM_PRIORITY.CRITICAL,
types.ALARM_PRIORITY.URGENT,
types.ALARM_PRIORITY.CRITICAL,
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.URGENT,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.URGENT
}
---@alias ALARM_STATE integer
---@enum ALARM_STATE
types.ALARM_STATE = {
INACTIVE = 0,
TRIPPED = 1,
@ -171,7 +162,10 @@ types.ALARM_STATE = {
RING_BACK = 3
}
--#endregion
-- STRING TYPES --
--#region
---@alias os_event
---| "alarm"
@ -206,21 +200,7 @@ types.ALARM_STATE = {
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
---@alias rps_trip_cause
---| "ok"
---| "dmg_crit"
---| "high_temp"
---| "no_coolant"
---| "full_waste"
---| "heated_coolant_backup"
---| "no_fuel"
---| "fault"
---| "timeout"
---| "manual"
---| "automatic"
---| "sys_fail"
---| "force_disabled"
---| "clock_start" custom, added for reactor PLC
---@alias fluid
---| "mekanism:empty_gas"
@ -246,6 +226,21 @@ types.rtu_t = {
env_detector = "environment_detector"
}
---@alias rps_trip_cause
---| "ok"
---| "dmg_crit"
---| "high_temp"
---| "no_coolant"
---| "full_waste"
---| "heated_coolant_backup"
---| "no_fuel"
---| "fault"
---| "timeout"
---| "manual"
---| "automatic"
---| "sys_fail"
---| "force_disabled"
---@alias rps_status_t rps_trip_cause
types.rps_status_t = {
ok = "ok",
@ -263,18 +258,24 @@ types.rps_status_t = {
force_disabled = "force_disabled"
}
-- turbine steam dumping modes
---@alias DUMPING_MODE string
---@alias DUMPING_MODE
---| "IDLE"
---| "DUMPING"
---| "DUMPING_EXCESS"
types.DUMPING_MODE = {
IDLE = "IDLE",
DUMPING = "DUMPING",
DUMPING_EXCESS = "DUMPING_EXCESS"
}
-- MODBUS
--#endregion
-- modbus function codes
---@alias MODBUS_FCODE integer
-- MODBUS --
--#region
-- MODBUS function codes
---@enum MODBUS_FCODE
types.MODBUS_FCODE = {
READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02,
@ -287,8 +288,8 @@ types.MODBUS_FCODE = {
ERROR_FLAG = 0x80
}
-- modbus exception codes
---@alias MODBUS_EXCODE integer
-- MODBUS exception codes
---@enum MODBUS_EXCODE
types.MODBUS_EXCODE = {
ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDR = 0x02,
@ -302,4 +303,6 @@ types.MODBUS_EXCODE = {
GATEWAY_TARGET_TIMEOUT = 0x0B
}
--#endregion
return types

View File

@ -14,6 +14,7 @@ util.TICK_TIME_MS = 50
--#region
-- trinary operator
---@nodiscard
---@param cond boolean|nil condition
---@param a any return if true
---@param b any return if false
@ -57,6 +58,7 @@ end
--#region
-- get a value as a string
---@nodiscard
---@param val any
---@return string
function util.strval(val)
@ -69,6 +71,7 @@ function util.strval(val)
end
-- repeat a string n times
---@nodiscard
---@param str string
---@param n integer
---@return string
@ -81,6 +84,7 @@ function util.strrep(str, n)
end
-- repeat a space n times
---@nodiscard
---@param n integer
---@return string
function util.spaces(n)
@ -88,6 +92,7 @@ function util.spaces(n)
end
-- pad text to a minimum width
---@nodiscard
---@param str string text
---@param n integer minimum width
---@return string
@ -100,6 +105,7 @@ function util.pad(str, n)
end
-- wrap a string into a table of lines, supporting single dash splits
---@nodiscard
---@param str string
---@param limit integer line limit
---@return table lines
@ -147,13 +153,12 @@ function util.strwrap(str, limit)
end
-- concatenation with built-in to string
---@nodiscard
---@vararg any
---@return string
function util.concat(...)
local str = ""
for _, v in ipairs(arg) do
str = str .. util.strval(v)
end
for _, v in ipairs(arg) do str = str .. util.strval(v) end
return str
end
@ -161,15 +166,16 @@ end
util.c = util.concat
-- sprintf implementation
---@nodiscard
---@param format string
---@vararg any
function util.sprintf(format, ...)
return string.format(format, table.unpack(arg))
end
-- format a number string with commas as the thousands separator
--
-- format a number string with commas as the thousands separator<br>
-- subtracts from spaces at the start if present for each comma used
---@nodiscard
---@param num string number string
---@return string
function util.comma_format(num)
@ -196,6 +202,7 @@ end
--#region
-- is a value an integer
---@nodiscard
---@param x any value
---@return boolean is_integer if the number is an integer
function util.is_int(x)
@ -203,6 +210,7 @@ function util.is_int(x)
end
-- get the sign of a number
---@nodiscard
---@param x number value
---@return integer sign (-1 for < 0, 1 otherwise)
function util.sign(x)
@ -210,12 +218,14 @@ function util.sign(x)
end
-- round a number to an integer
---@nodiscard
---@return integer rounded
function util.round(x)
return math.floor(x + 0.5)
end
-- get a new moving average object
---@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)
@ -249,6 +259,7 @@ function util.mov_avg(length, default)
end
-- compute the moving average
---@nodiscard
---@return number average
function public.compute()
local sum = 0
@ -264,6 +275,7 @@ end
-- TIME --
-- current time
---@nodiscard
---@return integer milliseconds
function util.time_ms()
---@diagnostic disable-next-line: undefined-field
@ -271,6 +283,7 @@ function util.time_ms()
end
-- current time
---@nodiscard
---@return number seconds
function util.time_s()
---@diagnostic disable-next-line: undefined-field
@ -278,10 +291,9 @@ function util.time_s()
end
-- current time
---@nodiscard
---@return integer milliseconds
function util.time()
return util.time_ms()
end
function util.time() return util.time_ms() end
--#endregion
@ -289,6 +301,7 @@ end
--#region
-- OS pull event raw wrapper with types
---@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)
@ -309,6 +322,7 @@ function util.push_event(event, param1, param2, param3, param4, param5)
end
-- start an OS timer
---@nodiscard
---@param t number timer duration in seconds
---@return integer timer ID
function util.start_timer(t)
@ -336,14 +350,12 @@ function util.psleep(t)
pcall(os.sleep, t)
end
-- no-op to provide a brief pause (1 tick) to yield
---
-- no-op to provide a brief pause (1 tick) to yield<br>
--- EVENT_CONSUMER: this function consumes events
function util.nop()
util.psleep(0.05)
end
function util.nop() util.psleep(0.05) end
-- attempt to maintain a minimum loop timing (duration of execution)
---@nodiscard
---@param target_timing integer minimum amount of milliseconds to wait for
---@param last_update integer millisecond time of last update
---@return integer time_now
@ -351,9 +363,7 @@ end
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
if sleep_for >= 50 then
util.psleep(sleep_for / 1000.0)
end
if sleep_for >= 50 then util.psleep(sleep_for / 1000.0) end
return util.time()
end
@ -362,8 +372,7 @@ end
-- TABLE UTILITIES --
--#region
-- delete elements from a table if the passed function returns false when passed a table element
--
-- delete elements from a table if the passed function returns false when passed a table element<br>
-- 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
@ -388,6 +397,7 @@ function util.filter_table(t, f, on_delete)
end
-- check if a table contains the provided element
---@nodiscard
---@param t table table to check
---@param element any element to check for
function util.table_contains(t, element)
@ -404,11 +414,13 @@ end
--#region
-- convert Joules to FE
---@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
---@nodiscard
---@param FE number Forge Energy
---@return number J Joules
function util.fe_to_joules(FE) return (FE * 2.5) end
@ -422,6 +434,7 @@ local function EFE(fe) return fe / 1000000000000000000.0 end -- if you ac
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)
---@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
@ -430,9 +443,7 @@ function util.power_format(fe, combine_label, format)
local unit
local value
if type(format) ~= "string" then
format = "%.2f"
end
if type(format) ~= "string" then format = "%.2f" end
if fe < 1000.0 then
unit = "FE"
@ -474,10 +485,10 @@ end
-- WATCHDOG --
-- ComputerCraft OS Timer based Watchdog
-- OS timer based watchdog<br>
-- triggers a timer event if not fed within 'timeout' seconds
---@nodiscard
---@param timeout number timeout duration
---
--- triggers a timer event if not fed within 'timeout' seconds
function util.new_watchdog(timeout)
local self = {
timeout = timeout,
@ -487,10 +498,10 @@ function util.new_watchdog(timeout)
---@class watchdog
local public = {}
-- check if a timer is this watchdog
---@nodiscard
---@param timer number timer event timer ID
function public.is_timer(timer)
return self.wd_timer == timer
end
function public.is_timer(timer) return self.wd_timer == timer end
-- satiate the beast
function public.feed()
@ -512,10 +523,10 @@ end
-- LOOP CLOCK --
-- ComputerCraft OS Timer based Loop Clock
-- OS timer based loop clock<br>
-- fires a timer event at the specified period, does not start at construct time
---@nodiscard
---@param period number clock period
---
--- fires a timer event at the specified period, does not start at construct time
function util.new_clock(period)
local self = {
period = period,
@ -525,24 +536,22 @@ function util.new_clock(period)
---@class clock
local public = {}
-- check if a timer is this clock
---@nodiscard
---@param timer number timer event timer ID
function public.is_clock(timer)
return self.timer == timer
end
function public.is_clock(timer) return self.timer == timer end
-- start the clock
function public.start()
self.timer = util.start_timer(self.period)
end
function public.start() self.timer = util.start_timer(self.period) end
return public
end
-- FIELD VALIDATOR --
-- create a new type validator
--
-- create a new type validator<br>
-- can execute sequential checks and check valid() to see if it is still valid
---@nodiscard
function util.new_validator()
local valid = true
@ -565,6 +574,8 @@ function util.new_validator()
function public.assert_port(port) valid = valid and type(port) == "number" and port >= 0 and port <= 65535 end
-- check if all assertions passed successfully
---@nodiscard
function public.valid() return valid end
return public

View File

@ -367,8 +367,8 @@ local function _update_alarm_state(self, tripped, alarm)
else
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]"))
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier + 1],"]"))
end
else
alarm.trip_time = util.time_ms()
@ -381,8 +381,8 @@ local function _update_alarm_state(self, tripped, alarm)
if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.TRIPPED
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): TRIPPED [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]"))
log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier + 1],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
@ -432,7 +432,7 @@ local function _update_alarm_state(self, tripped, alarm)
-- check for state change
if alarm.state ~= int_state then
local change_str = util.c(aistate_string[int_state + 1], " -> ", aistate_string[alarm.state + 1])
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.alarm_string[alarm.id], "): ", change_str))
log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
end
end
@ -530,8 +530,8 @@ function logic.update_auto_safety(public, self)
for _, alarm in pairs(self.alarms) do
if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then
if not self.auto_was_alarmed then
log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.alarm_string[alarm.id], ") [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]"))
log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], ") [PRIORITY ",
types.ALARM_PRIORITY_NAMES[alarm.tier + 1],"]"))
end
alarmed = true