diff --git a/coordinator/apisessions.lua b/coordinator/apisessions.lua
index 3c14c08..268052e 100644
--- a/coordinator/apisessions.lua
+++ b/coordinator/apisessions.lua
@@ -4,13 +4,17 @@ local apisessions = {}
function apisessions.handle_packet(packet)
end
-function apisessions.check_all_watchdogs()
-end
-
-function apisessions.close_all()
+-- attempt to identify which session's watchdog timer fired
+---@param timer_event number
+function apisessions.check_all_watchdogs(timer_event)
end
+-- delete all closed sessions
function apisessions.free_all_closed()
end
+-- close all open connections
+function apisessions.close_all()
+end
+
return apisessions
diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua
index 0e23b4f..e4bb283 100644
--- a/coordinator/coordinator.lua
+++ b/coordinator/coordinator.lua
@@ -14,17 +14,18 @@ local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
-local PROTOCOLS = comms.PROTOCOLS
-local DEVICE_TYPES = comms.DEVICE_TYPES
+local PROTOCOL = comms.PROTOCOL
+local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
-local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
-local UNIT_COMMANDS = comms.UNIT_COMMANDS
-local FAC_COMMANDS = comms.FAC_COMMANDS
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
+local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
+local UNIT_COMMAND = comms.UNIT_COMMAND
+local FAC_COMMAND = comms.FAC_COMMAND
local coordinator = {}
-- request the user to select a monitor
+---@nodiscard
---@param names table available monitors
---@return boolean|string|nil
local function ask_monitor(names)
@@ -64,9 +65,11 @@ function coordinator.configure_monitors(num_units)
end
-- we need a certain number of monitors (1 per unit + 1 primary display)
- if #names < num_units + 1 then
- println("not enough monitors connected (need " .. num_units + 1 .. ")")
- log.warning("insufficient monitors present (need " .. num_units + 1 .. ")")
+ local num_displays_needed = num_units + 1
+ if #names < num_displays_needed then
+ local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
+ println(message)
+ log.warning(message)
return false
end
@@ -125,7 +128,6 @@ function coordinator.configure_monitors(num_units)
else
-- make sure all displays are connected
for i = 1, num_units do
----@diagnostic disable-next-line: need-check-nil
local display = unit_displays[i]
if not util.table_contains(names, display) then
@@ -183,14 +185,19 @@ function coordinator.log_sys(message) log_dmesg(message, "SYSTEM") end
function coordinator.log_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") end
+-- log a message for communications connecting, providing access to progress indication control functions
+---@nodiscard
---@param message string
---@return function update, function done
function coordinator.log_comms_connecting(message)
----@diagnostic disable-next-line: return-type-mismatch
- return log_dmesg(message, "COMMS", true)
+ local update, done = log_dmesg(message, "COMMS", true)
+ ---@cast update function
+ ---@cast done function
+ return update, done
end
-- coordinator communications
+---@nodiscard
---@param version string coordinator version
---@param modem table modem device
---@param sv_port integer port of configured supervisor
@@ -203,37 +210,33 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
sv_linked = false,
sv_seq_num = 0,
sv_r_seq_num = nil,
- modem = modem,
connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW
}
- ---@class coord_comms
- local public = {}
-
comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS --
-- configure modem channels
local function _conf_channels()
- self.modem.closeAll()
- self.modem.open(sv_listen)
- self.modem.open(api_listen)
+ modem.closeAll()
+ modem.open(sv_listen)
+ modem.open(api_listen)
end
_conf_channels()
-- send a packet to the supervisor
- ---@param msg_type SCADA_MGMT_TYPES|SCADA_CRDN_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE
---@param msg table
local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = nil ---@type mgmt_packet|crdn_packet
- if protocol == PROTOCOLS.SCADA_MGMT then
+ if protocol == PROTOCOL.SCADA_MGMT then
pkt = comms.mgmt_packet()
- elseif protocol == PROTOCOLS.SCADA_CRDN then
+ elseif protocol == PROTOCOL.SCADA_CRDN then
pkt = comms.crdn_packet()
else
return
@@ -242,28 +245,30 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable())
- self.modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
+ modem.transmit(sv_port, sv_listen, s_pkt.raw_sendable())
self.sv_seq_num = self.sv_seq_num + 1
end
-- attempt connection establishment
local function _send_establish()
- _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.CRDN })
+ _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.CRDN })
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
- _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
+ _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- PUBLIC FUNCTIONS --
+ ---@class coord_comms
+ local public = {}
+
-- reconnect a newly connected modem
- ---@param modem table
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_modem(modem)
- self.modem = modem
+ ---@param new_modem table
+ function public.reconnect_modem(new_modem)
+ modem = new_modem
_conf_channels()
end
@@ -271,10 +276,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
function public.close()
sv_watchdog.cancel()
self.sv_linked = false
- _send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {})
+ _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end
-- attempt to connect to the subervisor
+ ---@nodiscard
---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg
@@ -300,7 +306,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
elseif event == "modem_message" then
-- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5)
- if packet ~= nil and packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ if packet ~= nil and packet.type == SCADA_MGMT_TYPE.ESTABLISH then
public.handle_packet(packet)
end
elseif event == "terminate" then
@@ -329,25 +335,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end
-- send a facility command
- ---@param cmd FAC_COMMANDS command
+ ---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd)
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd })
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, { cmd })
end
-- send the auto process control configuration with a start command
---@param config coord_auto_config configuration
function public.send_auto_start(config)
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, {
- FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
+ FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
})
end
-- send a unit command
- ---@param cmd UNIT_COMMANDS command
+ ---@param cmd UNIT_COMMAND command
---@param unit integer unit ID
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
function public.send_unit_command(cmd, unit, option)
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option })
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_CMD, { cmd, unit, option })
end
-- parse a packet
@@ -366,19 +372,19 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if s_pkt.is_valid() then
-- get as SCADA management packet
- if s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
+ if s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
- elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
+ elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
end
-- get as coordinator API packet
- elseif s_pkt.protocol() == PROTOCOLS.COORD_API then
+ elseif s_pkt.protocol() == PROTOCOL.COORD_API then
local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get()
@@ -399,8 +405,8 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
local l_port = packet.scada_frame.local_port()
if l_port == api_listen then
- if protocol == PROTOCOLS.COORD_API then
----@diagnostic disable-next-line: param-type-mismatch
+ if protocol == PROTOCOL.COORD_API then
+ ---@cast packet capi_frame
apisessions.handle_packet(packet)
else
log.debug("illegal packet type " .. protocol .. " on api listening channel", true)
@@ -420,9 +426,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
sv_watchdog.feed()
-- handle packet
- if protocol == PROTOCOLS.SCADA_CRDN then
+ if protocol == PROTOCOL.SCADA_CRDN then
+ ---@cast packet crdn_frame
if self.sv_linked then
- if packet.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then
+ if packet.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
if packet.length == 2 then
-- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1])
@@ -430,47 +437,47 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if fac_builds and unit_builds then
-- acknowledge receipt of builds
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.INITIAL_BUILDS, {})
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.INITIAL_BUILDS, {})
else
- log.error("received invalid INITIAL_BUILDS packet")
+ log.debug("received invalid INITIAL_BUILDS packet")
end
else
log.debug("INITIAL_BUILDS packet length mismatch")
end
- elseif packet.type == SCADA_CRDN_TYPES.FAC_BUILDS then
+ elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
if packet.length == 1 then
-- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_BUILDS, {})
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_BUILDS, {})
else
- log.error("received invalid FAC_BUILDS packet")
+ log.debug("received invalid FAC_BUILDS packet")
end
else
log.debug("FAC_BUILDS packet length mismatch")
end
- elseif packet.type == SCADA_CRDN_TYPES.FAC_STATUS then
+ elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
-- update facility status
if not iocontrol.update_facility_status(packet.data) then
- log.error("received invalid FAC_STATUS packet")
+ log.debug("received invalid FAC_STATUS packet")
end
- elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then
+ elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
- if cmd == FAC_COMMANDS.SCRAM_ALL then
+ if cmd == FAC_COMMAND.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
- elseif cmd == FAC_COMMANDS.STOP then
+ elseif cmd == FAC_COMMAND.STOP then
iocontrol.get_db().facility.stop_ack(ack)
- elseif cmd == FAC_COMMANDS.START then
+ elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
- elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then
+ elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
iocontrol.get_db().facility.ack_alarms_ack(ack)
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
@@ -478,24 +485,24 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
- elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
+ elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
-- record builds
if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds
- _send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_BUILDS, {})
+ _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.UNIT_BUILDS, {})
else
- log.error("received invalid UNIT_BUILDS packet")
+ log.debug("received invalid UNIT_BUILDS packet")
end
else
log.debug("UNIT_BUILDS packet length mismatch")
end
- elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then
+ elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
-- update statuses
if not iocontrol.update_unit_statuses(packet.data) then
log.error("received invalid UNIT_STATUSES packet")
end
- elseif packet.type == SCADA_CRDN_TYPES.UNIT_CMD then
+ elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement
if packet.length == 3 then
local cmd = packet.data[1]
@@ -505,20 +512,20 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
if unit ~= nil then
- if cmd == UNIT_COMMANDS.SCRAM then
+ if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack)
- elseif cmd == UNIT_COMMANDS.START then
+ elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack)
- elseif cmd == UNIT_COMMANDS.RESET_RPS then
+ elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack)
- elseif cmd == UNIT_COMMANDS.SET_BURN then
+ elseif cmd == UNIT_COMMAND.SET_BURN then
unit.set_burn_ack(ack)
- elseif cmd == UNIT_COMMANDS.SET_WASTE then
+ elseif cmd == UNIT_COMMAND.SET_WASTE then
unit.set_waste_ack(ack)
- elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
+ elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_alarms_ack(ack)
- elseif cmd == UNIT_COMMANDS.SET_GROUP then
- ---@todo how is this going to be handled?
+ elseif cmd == UNIT_COMMAND.SET_GROUP then
+ -- UI will be updated to display current group if changed successfully
else
log.debug(util.c("received unit command ack with unknown command ", cmd))
end
@@ -534,8 +541,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else
log.debug("discarding SCADA_CRDN packet before linked")
end
- elseif protocol == PROTOCOLS.SCADA_MGMT then
- if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ elseif protocol == PROTOCOL.SCADA_MGMT then
+ ---@cast packet mgmt_frame
+ if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established
if packet.length == 2 then
local est_ack = packet.data[1]
@@ -562,10 +570,10 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
self.sv_linked = true
else
- log.error("invalid supervisor configuration definitions received, establish failed")
+ log.debug("invalid supervisor configuration definitions received, establish failed")
end
else
- log.error("invalid supervisor configuration table received, establish failed")
+ log.debug("invalid supervisor configuration table received, establish failed")
end
else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported")
@@ -577,11 +585,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
if est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then
- log.debug("supervisor connection denied")
+ log.info("supervisor connection denied")
end
elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack then
- log.debug("supervisor connection denied due to collision")
+ log.info("supervisor connection denied due to collision")
end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack then
@@ -596,7 +604,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
log.debug("SCADA_MGMT establish packet length mismatch")
end
elseif self.sv_linked then
- if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 then
local timestamp = packet.data[1]
@@ -614,14 +622,14 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else
log.debug("SCADA keep alive packet length mismatch")
end
- elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
sv_watchdog.cancel()
self.sv_linked = false
println_ts("server connection closed by remote host")
- log.warning("server connection closed by remote host")
+ log.info("server connection closed by remote host")
else
- log.warning("received unknown SCADA_MGMT packet type " .. packet.type)
+ log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end
else
log.debug("discarding non-link SCADA_MGMT packet before linked")
@@ -636,6 +644,7 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end
-- check if the coordinator is still linked to the supervisor
+ ---@nodiscard
function public.is_linked() return self.sv_linked end
return public
diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua
index 30bb6e5..ea243cd 100644
--- a/coordinator/iocontrol.lua
+++ b/coordinator/iocontrol.lua
@@ -1,4 +1,7 @@
-local comms = require("scada-common.comms")
+--
+-- I/O Control for Supervisor/Coordinator Integration
+--
+
local log = require("scada-common.log")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
@@ -7,8 +10,6 @@ local util = require("scada-common.util")
local process = require("coordinator.process")
local sounder = require("coordinator.sounder")
-local UNIT_COMMANDS = comms.UNIT_COMMANDS
-
local ALARM_STATE = types.ALARM_STATE
local iocontrol = {}
@@ -19,7 +20,6 @@ local io = {}
-- initialize the coordinator IO controller
---@param conf facility_conf configuration
---@param comms coord_comms comms reference
----@diagnostic disable-next-line: redefined-local
function iocontrol.init(conf, comms)
---@class ioctl_facility
io.facility = {
@@ -44,11 +44,11 @@ function iocontrol.init(conf, comms)
radiation = types.new_zero_radiation_reading(),
- save_cfg_ack = function (success) end, ---@param success boolean
- start_ack = function (success) end, ---@param success boolean
- stop_ack = function (success) end, ---@param success boolean
- scram_ack = function (success) end, ---@param success boolean
- ack_alarms_ack = function (success) end, ---@param success boolean
+ save_cfg_ack = function (success) end, ---@param success boolean
+ start_ack = function (success) end, ---@param success boolean
+ stop_ack = function (success) end, ---@param success boolean
+ scram_ack = function (success) end, ---@param success boolean
+ ack_alarms_ack = function (success) end, ---@param success boolean
ps = psil.create(),
@@ -59,7 +59,7 @@ function iocontrol.init(conf, comms)
env_d_data = {}
}
- -- create induction tables (max 1 per unit, preferably 1 total)
+ -- create induction tables (currently only 1 is supported)
for _ = 1, conf.num_units do
local data = {} ---@type imatrix_session_db
table.insert(io.facility.induction_ps_tbl, psil.create())
@@ -173,6 +173,8 @@ end
---@param build table
---@return boolean valid
function iocontrol.record_facility_builds(build)
+ local valid = true
+
if type(build) == "table" then
local fac = io.facility
@@ -190,96 +192,103 @@ function iocontrol.record_facility_builds(build)
end
else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
+ valid = false
end
end
end
else
- log.error("facility builds not a table")
- return false
+ log.debug("facility builds not a table")
+ valid = false
end
- return true
+ return valid
end
-- populate unit structure builds
---@param builds table
---@return boolean valid
function iocontrol.record_unit_builds(builds)
+ local valid = true
+
-- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit
+ local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
+
if type(build) ~= "table" then
- log.error(util.c("corrupted unit builds provided, unit ", id, " not a table"))
- return false
+ log.debug(log_header .. "build not a table")
+ valid = false
elseif type(unit) ~= "table" then
- log.error(util.c("corrupted unit builds provided, invalid unit ", id))
- return false
- end
+ log.debug(log_header .. "invalid unit id")
+ valid = false
+ else
+ -- reactor build
+ if type(build.reactor) == "table" then
+ unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
+ for key, val in pairs(unit.reactor_data.mek_struct) do
+ unit.unit_ps.publish(key, val)
+ end
- local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ")
-
- -- reactor build
- if type(build.reactor) == "table" then
- unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
- for key, val in pairs(unit.reactor_data.mek_struct) do
- unit.unit_ps.publish(key, val)
- end
-
- if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and
- (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then
- unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width })
- end
- end
-
- -- boiler builds
- if type(build.boilers) == "table" then
- for b_id, boiler in pairs(build.boilers) do
- if type(unit.boiler_data_tbl[b_id]) == "table" then
- unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
- unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
-
- unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
-
- for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
- unit.boiler_ps_tbl[b_id].publish(key, val)
- end
- else
- log.debug(util.c(log_header, "invalid boiler id ", b_id))
+ if (type(unit.reactor_data.mek_struct.length) == "number") and (unit.reactor_data.mek_struct.length ~= 0) and
+ (type(unit.reactor_data.mek_struct.width) == "number") and (unit.reactor_data.mek_struct.width ~= 0) then
+ unit.unit_ps.publish("size", { unit.reactor_data.mek_struct.length, unit.reactor_data.mek_struct.width })
end
end
- end
- -- turbine builds
- if type(build.turbines) == "table" then
- for t_id, turbine in pairs(build.turbines) do
- if type(unit.turbine_data_tbl[t_id]) == "table" then
- unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
- unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
+ -- boiler builds
+ if type(build.boilers) == "table" then
+ for b_id, boiler in pairs(build.boilers) do
+ if type(unit.boiler_data_tbl[b_id]) == "table" then
+ unit.boiler_data_tbl[b_id].formed = boiler[1] ---@type boolean
+ unit.boiler_data_tbl[b_id].build = boiler[2] ---@type table
- unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
+ unit.boiler_ps_tbl[b_id].publish("formed", boiler[1])
- for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
- unit.turbine_ps_tbl[t_id].publish(key, val)
+ for key, val in pairs(unit.boiler_data_tbl[b_id].build) do
+ unit.boiler_ps_tbl[b_id].publish(key, val)
+ end
+ else
+ log.debug(util.c(log_header, "invalid boiler id ", b_id))
+ valid = false
+ end
+ end
+ end
+
+ -- turbine builds
+ if type(build.turbines) == "table" then
+ for t_id, turbine in pairs(build.turbines) do
+ if type(unit.turbine_data_tbl[t_id]) == "table" then
+ unit.turbine_data_tbl[t_id].formed = turbine[1] ---@type boolean
+ unit.turbine_data_tbl[t_id].build = turbine[2] ---@type table
+
+ unit.turbine_ps_tbl[t_id].publish("formed", turbine[1])
+
+ for key, val in pairs(unit.turbine_data_tbl[t_id].build) do
+ unit.turbine_ps_tbl[t_id].publish(key, val)
+ end
+ else
+ log.debug(util.c(log_header, "invalid turbine id ", t_id))
+ valid = false
end
- else
- log.debug(util.c(log_header, "invalid turbine id ", t_id))
end
end
end
end
- return true
+ return valid
end
-- update facility status
---@param status table
---@return boolean valid
function iocontrol.update_facility_status(status)
+ local valid = true
local log_header = util.c("iocontrol.update_facility_status: ")
+
if type(status) ~= "table" then
- log.debug(log_header .. "status not a table")
- return false
+ log.debug(util.c(log_header, "status not a table"))
+ valid = false
else
local fac = io.facility
@@ -287,10 +296,17 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1]
- if type(ctl_status) == "table" and (#ctl_status == 14) then
+ if type(ctl_status) == "table" and #ctl_status == 14 then
fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2]
- fac.auto_active = ctl_status[3] > 0
+
+ if type(ctl_status[3]) == "number" then
+ fac.auto_active = ctl_status[3] > 1
+ else
+ fac.auto_active = false
+ valid = false
+ end
+
fac.auto_ramping = ctl_status[4]
fac.auto_saturated = ctl_status[5]
@@ -330,6 +346,7 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "control status not a table or length mismatch")
+ valid = false
end
-- RTU statuses
@@ -337,10 +354,10 @@ function iocontrol.update_facility_status(status)
local rtu_statuses = status[2]
fac.rtu_count = 0
+
if type(rtu_statuses) == "table" then
-- connected RTU count
fac.rtu_count = rtu_statuses.count
- fac.ps.publish("rtu_count", fac.rtu_count)
-- power statistics
if type(rtu_statuses.power) == "table" then
@@ -349,6 +366,7 @@ function iocontrol.update_facility_status(status)
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
else
log.debug(log_header .. "power statistics list not a table")
+ valid = false
end
-- induction matricies statuses
@@ -374,16 +392,16 @@ function iocontrol.update_facility_status(status)
if data.formed then
if rtu_faulted then
- fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
+ fac.induction_ps_tbl[id].publish("computed_status", 3) -- faulted
elseif data.tanks.energy_fill >= 0.99 then
- fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
+ fac.induction_ps_tbl[id].publish("computed_status", 6) -- full
elseif data.tanks.energy_fill <= 0.01 then
- fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
+ fac.induction_ps_tbl[id].publish("computed_status", 5) -- empty
else
- fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
+ fac.induction_ps_tbl[id].publish("computed_status", 4) -- on-line
end
else
- fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
+ fac.induction_ps_tbl[id].publish("computed_status", 2) -- not formed
end
for key, val in pairs(fac.induction_data_tbl[id].state) do
@@ -399,6 +417,7 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "induction matrix list not a table")
+ valid = false
end
-- environment detector status
@@ -416,313 +435,324 @@ function iocontrol.update_facility_status(status)
end
else
log.debug(log_header .. "radiation monitor list not a table")
- return false
+ valid = false
end
else
log.debug(log_header .. "rtu statuses not a table")
+ valid = false
end
+
+ fac.ps.publish("rtu_count", fac.rtu_count)
end
- return true
+ return valid
end
-- update unit statuses
---@param statuses table
---@return boolean valid
function iocontrol.update_unit_statuses(statuses)
+ local valid = true
+
if type(statuses) ~= "table" then
log.debug("iocontrol.update_unit_statuses: unit statuses not a table")
- return false
+ valid = false
elseif #statuses ~= #io.units then
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
- return false
+ valid = false
else
local burn_rate_sum = 0.0
-- get all unit statuses
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
+
local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i]
if type(status) ~= "table" or #status ~= 5 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
- return false
- end
-
- -- reactor PLC status
-
- local reactor_status = status[1]
-
- if type(reactor_status) ~= "table" then
- reactor_status = {}
- log.debug(log_header .. "reactor status not a table")
- end
-
- if #reactor_status == 0 then
- unit.unit_ps.publish("computed_status", 1) -- disconnected
- elseif #reactor_status == 3 then
- local mek_status = reactor_status[1]
- local rps_status = reactor_status[2]
- local gen_status = reactor_status[3]
-
- if #gen_status == 6 then
- unit.reactor_data.last_status_update = gen_status[1]
- unit.reactor_data.control_state = gen_status[2]
- unit.reactor_data.rps_tripped = gen_status[3]
- unit.reactor_data.rps_trip_cause = gen_status[4]
- unit.reactor_data.no_reactor = gen_status[5]
- unit.reactor_data.formed = gen_status[6]
- else
- log.debug(log_header .. "reactor general status length mismatch")
- end
-
- unit.reactor_data.rps_status = rps_status ---@type rps_status
- unit.reactor_data.mek_status = mek_status ---@type mek_status
-
- -- if status hasn't been received, mek_status = {}
- if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
- burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
- end
-
- if unit.reactor_data.mek_status.status then
- unit.unit_ps.publish("computed_status", 5) -- running
- else
- if unit.reactor_data.no_reactor then
- unit.unit_ps.publish("computed_status", 3) -- faulted
- elseif not unit.reactor_data.formed then
- unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
- elseif unit.reactor_data.rps_status.force_dis then
- unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
- elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
- unit.unit_ps.publish("computed_status", 6) -- SCRAM
- else
- unit.unit_ps.publish("computed_status", 4) -- disabled
- end
- end
-
- for key, val in pairs(unit.reactor_data) do
- if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
- unit.unit_ps.publish(key, val)
- end
- end
-
- if type(unit.reactor_data.rps_status) == "table" then
- for key, val in pairs(unit.reactor_data.rps_status) do
- unit.unit_ps.publish(key, val)
- end
- end
-
- if type(unit.reactor_data.mek_status) == "table" then
- for key, val in pairs(unit.reactor_data.mek_status) do
- unit.unit_ps.publish(key, val)
- end
- end
+ valid = false
else
- log.debug(log_header .. "reactor status length mismatch")
- end
+ -- reactor PLC status
+ local reactor_status = status[1]
- -- RTU statuses
+ if type(reactor_status) ~= "table" then
+ reactor_status = {}
+ log.debug(log_header .. "reactor status not a table")
+ end
- local rtu_statuses = status[2]
+ if #reactor_status == 0 then
+ unit.unit_ps.publish("computed_status", 1) -- disconnected
+ elseif #reactor_status == 3 then
+ local mek_status = reactor_status[1]
+ local rps_status = reactor_status[2]
+ local gen_status = reactor_status[3]
- if type(rtu_statuses) == "table" then
- -- boiler statuses
- if type(rtu_statuses.boilers) == "table" then
- for id = 1, #unit.boiler_ps_tbl do
- if rtu_statuses.boilers[i] == nil then
- -- disconnected
- unit.boiler_ps_tbl[id].publish("computed_status", 1)
+ if #gen_status == 6 then
+ unit.reactor_data.last_status_update = gen_status[1]
+ unit.reactor_data.control_state = gen_status[2]
+ unit.reactor_data.rps_tripped = gen_status[3]
+ unit.reactor_data.rps_trip_cause = gen_status[4]
+ unit.reactor_data.no_reactor = gen_status[5]
+ unit.reactor_data.formed = gen_status[6]
+ else
+ log.debug(log_header .. "reactor general status length mismatch")
+ end
+
+ unit.reactor_data.rps_status = rps_status ---@type rps_status
+ unit.reactor_data.mek_status = mek_status ---@type mek_status
+
+ -- if status hasn't been received, mek_status = {}
+ if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
+ burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
+ end
+
+ if unit.reactor_data.mek_status.status then
+ unit.unit_ps.publish("computed_status", 5) -- running
+ else
+ if unit.reactor_data.no_reactor then
+ unit.unit_ps.publish("computed_status", 3) -- faulted
+ elseif not unit.reactor_data.formed then
+ unit.unit_ps.publish("computed_status", 2) -- multiblock not formed
+ elseif unit.reactor_data.rps_status.force_dis then
+ unit.unit_ps.publish("computed_status", 7) -- reactor force disabled
+ elseif unit.reactor_data.rps_tripped and unit.reactor_data.rps_trip_cause ~= "manual" then
+ unit.unit_ps.publish("computed_status", 6) -- SCRAM
+ else
+ unit.unit_ps.publish("computed_status", 4) -- disabled
end
end
- for id, boiler in pairs(rtu_statuses.boilers) do
- if type(unit.boiler_data_tbl[id]) == "table" then
- local rtu_faulted = boiler[1] ---@type boolean
- unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
- unit.boiler_data_tbl[id].state = boiler[3] ---@type table
- unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
+ for key, val in pairs(unit.reactor_data) do
+ if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
+ unit.unit_ps.publish(key, val)
+ end
+ end
- local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
+ if type(unit.reactor_data.rps_status) == "table" then
+ for key, val in pairs(unit.reactor_data.rps_status) do
+ unit.unit_ps.publish(key, val)
+ end
+ end
- unit.boiler_ps_tbl[id].publish("formed", data.formed)
- unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
+ if type(unit.reactor_data.mek_status) == "table" then
+ for key, val in pairs(unit.reactor_data.mek_status) do
+ unit.unit_ps.publish(key, val)
+ end
+ end
+ else
+ log.debug(log_header .. "reactor status length mismatch")
+ valid = false
+ end
- if rtu_faulted then
- unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
- elseif data.formed then
- if data.state.boil_rate > 0 then
- unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
+ -- RTU statuses
+ local rtu_statuses = status[2]
+
+ if type(rtu_statuses) == "table" then
+ -- boiler statuses
+ if type(rtu_statuses.boilers) == "table" then
+ for id = 1, #unit.boiler_ps_tbl do
+ if rtu_statuses.boilers[i] == nil then
+ -- disconnected
+ unit.boiler_ps_tbl[id].publish("computed_status", 1)
+ end
+ end
+
+ for id, boiler in pairs(rtu_statuses.boilers) do
+ if type(unit.boiler_data_tbl[id]) == "table" then
+ local rtu_faulted = boiler[1] ---@type boolean
+ unit.boiler_data_tbl[id].formed = boiler[2] ---@type boolean
+ unit.boiler_data_tbl[id].state = boiler[3] ---@type table
+ unit.boiler_data_tbl[id].tanks = boiler[4] ---@type table
+
+ local data = unit.boiler_data_tbl[id] ---@type boilerv_session_db
+
+ unit.boiler_ps_tbl[id].publish("formed", data.formed)
+ unit.boiler_ps_tbl[id].publish("faulted", rtu_faulted)
+
+ if rtu_faulted then
+ unit.boiler_ps_tbl[id].publish("computed_status", 3) -- faulted
+ elseif data.formed then
+ if data.state.boil_rate > 0 then
+ unit.boiler_ps_tbl[id].publish("computed_status", 5) -- active
+ else
+ unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
+ end
else
- unit.boiler_ps_tbl[id].publish("computed_status", 4) -- idle
+ unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
+ end
+
+ for key, val in pairs(unit.boiler_data_tbl[id].state) do
+ unit.boiler_ps_tbl[id].publish(key, val)
+ end
+
+ for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
+ unit.boiler_ps_tbl[id].publish(key, val)
end
else
- unit.boiler_ps_tbl[id].publish("computed_status", 2) -- not formed
+ log.debug(util.c(log_header, "invalid boiler id ", id))
+ valid = false
end
-
- for key, val in pairs(unit.boiler_data_tbl[id].state) do
- unit.boiler_ps_tbl[id].publish(key, val)
- end
-
- for key, val in pairs(unit.boiler_data_tbl[id].tanks) do
- unit.boiler_ps_tbl[id].publish(key, val)
- end
- else
- log.debug(util.c(log_header, "invalid boiler id ", id))
- end
- end
- else
- log.debug(log_header .. "boiler list not a table")
- end
-
- -- turbine statuses
- if type(rtu_statuses.turbines) == "table" then
- for id = 1, #unit.turbine_ps_tbl do
- if rtu_statuses.turbines[i] == nil then
- -- disconnected
- unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
+ else
+ log.debug(log_header .. "boiler list not a table")
+ valid = false
end
- for id, turbine in pairs(rtu_statuses.turbines) do
- if type(unit.turbine_data_tbl[id]) == "table" then
- local rtu_faulted = turbine[1] ---@type boolean
- unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
- unit.turbine_data_tbl[id].state = turbine[3] ---@type table
- unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
+ -- turbine statuses
+ if type(rtu_statuses.turbines) == "table" then
+ for id = 1, #unit.turbine_ps_tbl do
+ if rtu_statuses.turbines[i] == nil then
+ -- disconnected
+ unit.turbine_ps_tbl[id].publish("computed_status", 1)
+ end
+ end
- local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
+ for id, turbine in pairs(rtu_statuses.turbines) do
+ if type(unit.turbine_data_tbl[id]) == "table" then
+ local rtu_faulted = turbine[1] ---@type boolean
+ unit.turbine_data_tbl[id].formed = turbine[2] ---@type boolean
+ unit.turbine_data_tbl[id].state = turbine[3] ---@type table
+ unit.turbine_data_tbl[id].tanks = turbine[4] ---@type table
- unit.turbine_ps_tbl[id].publish("formed", data.formed)
- unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
+ local data = unit.turbine_data_tbl[id] ---@type turbinev_session_db
- if rtu_faulted then
- unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
- elseif data.formed then
- if data.tanks.energy_fill >= 0.99 then
- unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
- elseif data.state.flow_rate < 100 then
- unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
+ unit.turbine_ps_tbl[id].publish("formed", data.formed)
+ unit.turbine_ps_tbl[id].publish("faulted", rtu_faulted)
+
+ if rtu_faulted then
+ unit.turbine_ps_tbl[id].publish("computed_status", 3) -- faulted
+ elseif data.formed then
+ if data.tanks.energy_fill >= 0.99 then
+ unit.turbine_ps_tbl[id].publish("computed_status", 6) -- trip
+ elseif data.state.flow_rate < 100 then
+ unit.turbine_ps_tbl[id].publish("computed_status", 4) -- idle
+ else
+ unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
+ end
else
- unit.turbine_ps_tbl[id].publish("computed_status", 5) -- active
+ unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
+ end
+
+ for key, val in pairs(unit.turbine_data_tbl[id].state) do
+ unit.turbine_ps_tbl[id].publish(key, val)
+ end
+
+ for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
+ unit.turbine_ps_tbl[id].publish(key, val)
end
else
- unit.turbine_ps_tbl[id].publish("computed_status", 2) -- not formed
+ log.debug(util.c(log_header, "invalid turbine id ", id))
+ valid = false
end
+ end
+ else
+ log.debug(log_header .. "turbine list not a table")
+ valid = false
+ end
- for key, val in pairs(unit.turbine_data_tbl[id].state) do
- unit.turbine_ps_tbl[id].publish(key, val)
- end
+ -- environment detector status
+ if type(rtu_statuses.rad_mon) == "table" then
+ if #rtu_statuses.rad_mon > 0 then
+ local rad_mon = rtu_statuses.rad_mon[1]
+ local rtu_faulted = rad_mon[1] ---@type boolean
+ unit.radiation = rad_mon[2] ---@type number
- for key, val in pairs(unit.turbine_data_tbl[id].tanks) do
- unit.turbine_ps_tbl[id].publish(key, val)
- end
+ unit.unit_ps.publish("radiation", unit.radiation)
else
- log.debug(util.c(log_header, "invalid turbine id ", id))
+ unit.radiation = types.new_zero_radiation_reading()
+ end
+ else
+ log.debug(log_header .. "radiation monitor list not a table")
+ valid = false
+ end
+ else
+ log.debug(log_header .. "rtu list not a table")
+ valid = false
+ end
+
+ -- annunciator
+ unit.annunciator = status[3]
+
+ if type(unit.annunciator) ~= "table" then
+ unit.annunciator = {}
+ log.debug(log_header .. "annunciator state not a table")
+ valid = false
+ end
+
+ for key, val in pairs(unit.annunciator) do
+ if key == "TurbineTrip" then
+ -- split up turbine trip table for all turbines and a general OR combination
+ local trips = val
+ local any = false
+
+ for id = 1, #trips do
+ any = any or trips[id]
+ unit.turbine_ps_tbl[id].publish(key, trips[id])
+ end
+
+ unit.unit_ps.publish("TurbineTrip", any)
+ elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
+ -- split up array for all boilers
+ for id = 1, #val do
+ unit.boiler_ps_tbl[id].publish(key, val[id])
+ end
+ elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
+ -- split up array for all turbines
+ for id = 1, #val do
+ unit.turbine_ps_tbl[id].publish(key, val[id])
+ end
+ elseif type(val) == "table" then
+ -- we missed one of the tables?
+ log.debug(log_header .. "unrecognized table found in annunciator list, this is a bug")
+ valid = false
+ else
+ -- non-table fields
+ unit.unit_ps.publish(key, val)
+ end
+ end
+
+ -- alarms
+ local alarm_states = status[4]
+
+ if type(alarm_states) == "table" then
+ for id = 1, #alarm_states do
+ local state = alarm_states[id]
+
+ unit.alarms[id] = state
+
+ if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then
+ unit.unit_ps.publish("Alarm_" .. id, 2)
+ elseif state == types.ALARM_STATE.RING_BACK then
+ unit.unit_ps.publish("Alarm_" .. id, 3)
+ else
+ unit.unit_ps.publish("Alarm_" .. id, 1)
end
end
else
- log.debug(log_header .. "turbine list not a table")
- return false
+ log.debug(log_header .. "alarm states not a table")
+ valid = false
end
- -- environment detector status
- if type(rtu_statuses.rad_mon) == "table" then
- if #rtu_statuses.rad_mon > 0 then
- local rad_mon = rtu_statuses.rad_mon[1]
- local rtu_faulted = rad_mon[1] ---@type boolean
- unit.radiation = rad_mon[2] ---@type number
+ -- unit state fields
+ local unit_state = status[5]
- unit.unit_ps.publish("radiation", unit.radiation)
+ if type(unit_state) == "table" then
+ if #unit_state == 5 then
+ unit.unit_ps.publish("U_StatusLine1", unit_state[1])
+ unit.unit_ps.publish("U_StatusLine2", unit_state[2])
+ unit.unit_ps.publish("U_WasteMode", unit_state[3])
+ unit.unit_ps.publish("U_AutoReady", unit_state[4])
+ unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
else
- unit.radiation = types.new_zero_radiation_reading()
+ log.debug(log_header .. "unit state length mismatch")
+ valid = false
end
else
- log.debug(log_header .. "radiation monitor list not a table")
- return false
+ log.debug(log_header .. "unit state not a table")
+ valid = false
end
- else
- log.debug(log_header .. "rtu list not a table")
- end
-
- -- annunciator
-
- unit.annunciator = status[3]
-
- if type(unit.annunciator) ~= "table" then
- unit.annunciator = {}
- log.debug(log_header .. "annunciator state not a table")
- end
-
- for key, val in pairs(unit.annunciator) do
- if key == "TurbineTrip" then
- -- split up turbine trip table for all turbines and a general OR combination
- local trips = val
- local any = false
-
- for id = 1, #trips do
- any = any or trips[id]
- unit.turbine_ps_tbl[id].publish(key, trips[id])
- end
-
- unit.unit_ps.publish("TurbineTrip", any)
- elseif key == "BoilerOnline" or key == "HeatingRateLow" or key == "WaterLevelLow" then
- -- split up array for all boilers
- for id = 1, #val do
- unit.boiler_ps_tbl[id].publish(key, val[id])
- end
- elseif key == "TurbineOnline" or key == "SteamDumpOpen" or key == "TurbineOverSpeed" then
- -- split up array for all turbines
- for id = 1, #val do
- unit.turbine_ps_tbl[id].publish(key, val[id])
- end
- elseif type(val) == "table" then
- -- we missed one of the tables?
- log.error(log_header .. "unrecognized table found in annunciator list, this is a bug", true)
- else
- -- non-table fields
- unit.unit_ps.publish(key, val)
- end
- end
-
- -- alarms
-
- local alarm_states = status[4]
-
- if type(alarm_states) == "table" then
- for id = 1, #alarm_states do
- local state = alarm_states[id]
-
- unit.alarms[id] = state
-
- if state == types.ALARM_STATE.TRIPPED or state == types.ALARM_STATE.ACKED then
- unit.unit_ps.publish("Alarm_" .. id, 2)
- elseif state == types.ALARM_STATE.RING_BACK then
- unit.unit_ps.publish("Alarm_" .. id, 3)
- else
- unit.unit_ps.publish("Alarm_" .. id, 1)
- end
- end
- else
- log.debug(log_header .. "alarm states not a table")
- end
-
- -- unit state fields
-
- local unit_state = status[5]
-
- if type(unit_state) == "table" then
- if #unit_state == 5 then
- unit.unit_ps.publish("U_StatusLine1", unit_state[1])
- unit.unit_ps.publish("U_StatusLine2", unit_state[2])
- unit.unit_ps.publish("U_WasteMode", unit_state[3])
- unit.unit_ps.publish("U_AutoReady", unit_state[4])
- unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
- else
- log.debug(log_header .. "unit state length mismatch")
- end
- else
- log.debug(log_header .. "unit state not a table")
end
end
@@ -732,7 +762,7 @@ function iocontrol.update_unit_statuses(statuses)
sounder.eval(io.units)
end
- return true
+ return valid
end
-- get the IO controller database
diff --git a/coordinator/process.lua b/coordinator/process.lua
index b3a6bb3..1e318ed 100644
--- a/coordinator/process.lua
+++ b/coordinator/process.lua
@@ -1,11 +1,14 @@
+--
+-- Process Control Management
+--
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
-local FAC_COMMANDS = comms.FAC_COMMANDS
-local UNIT_COMMANDS = comms.UNIT_COMMANDS
+local FAC_COMMAND = comms.FAC_COMMAND
+local UNIT_COMMAND = comms.UNIT_COMMAND
local PROCESS = types.PROCESS
@@ -30,11 +33,11 @@ local self = {
--------------------------
-- initialize the process controller
----@param iocontrol ioctl
----@diagnostic disable-next-line: redefined-local
-function process.init(iocontrol, comms)
+---@param iocontrol ioctl iocontrl system
+---@param coord_comms coord_comms coordinator communications
+function process.init(iocontrol, coord_comms)
self.io = iocontrol
- self.comms = comms
+ self.comms = coord_comms
for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1
@@ -71,7 +74,7 @@ function process.init(iocontrol, comms)
if type(waste_mode) == "table" then
for id, mode in pairs(waste_mode) do
- self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
+ self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
end
log.info("PROCESS: loaded waste mode settings from coord.settings")
@@ -81,7 +84,7 @@ function process.init(iocontrol, comms)
if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do
- self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, id, group)
+ self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, id, group)
end
log.info("PROCESS: loaded priority groups settings from coord.settings")
@@ -90,45 +93,45 @@ end
-- facility SCRAM command
function process.fac_scram()
- self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL)
- log.debug("FAC: SCRAM ALL")
+ self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
+ log.debug("PROCESS: FAC SCRAM ALL")
end
-- facility alarm acknowledge command
function process.fac_ack_alarms()
- self.comms.send_fac_command(FAC_COMMANDS.ACK_ALL_ALARMS)
- log.debug("FAC: ACK ALL ALARMS")
+ self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
+ log.debug("PROCESS: FAC ACK ALL ALARMS")
end
-- start reactor
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
- self.comms.send_unit_command(UNIT_COMMANDS.START, id)
- log.debug(util.c("UNIT[", id, "]: START"))
+ self.comms.send_unit_command(UNIT_COMMAND.START, id)
+ log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end
-- SCRAM reactor
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
- self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id)
- log.debug(util.c("UNIT[", id, "]: SCRAM"))
+ self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
+ log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
- self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id)
- log.debug(util.c("UNIT[", id, "]: RESET RPS"))
+ self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
+ log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end
-- set burn rate
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
- self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate)
- log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate))
+ self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
+ log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end
-- set waste mode
@@ -138,14 +141,12 @@ function process.set_waste(id, mode)
-- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode)
- self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
- log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode))
+ self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
+ log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
- if type(waste_mode) ~= "table" then
- waste_mode = {}
- end
+ if type(waste_mode) ~= "table" then waste_mode = {} end
waste_mode[id] = mode
@@ -159,38 +160,36 @@ end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
- self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
- log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS"))
+ self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
+ log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end
-- acknowledge an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
- self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
- log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm))
+ self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
+ log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end
-- reset an alarm
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
- self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
- log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm))
+ self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
+ log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end
-- assign a unit to a group
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
- self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id)
- log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id))
+ self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
+ log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
- if type(prio_groups) ~= "table" then
- prio_groups = {}
- end
+ if type(prio_groups) ~= "table" then prio_groups = {} end
prio_groups[unit_id] = group_id
@@ -207,14 +206,14 @@ end
-- stop automatic process control
function process.stop_auto()
- self.comms.send_fac_command(FAC_COMMANDS.STOP)
- log.debug("FAC: STOP AUTO")
+ self.comms.send_fac_command(FAC_COMMAND.STOP)
+ log.debug("PROCESS: STOP AUTO CTL")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.config)
- log.debug("FAC: START AUTO")
+ log.debug("PROCESS: START AUTO CTL")
end
-- save process control settings
@@ -246,8 +245,6 @@ function process.save(mode, burn_target, charge_target, gen_target, limits)
log.warning("process.save(): failed to save coordinator settings file")
end
- log.debug("saved = " .. util.strval(saved))
-
self.io.facility.save_cfg_ack(saved)
end
@@ -273,18 +270,4 @@ function process.start_ack_handle(response)
self.io.facility.start_ack(ack)
end
---------------------------
--- SUPERVISOR RESPONSES --
---------------------------
-
--- acknowledgement from the supervisor to assign a unit to a group
-function process.sv_assign(unit_id, group_id)
- self.io.units[unit_id].group = group_id
-end
-
--- acknowledgement from the supervisor to assign a unit a burn rate limit
-function process.sv_limit(unit_id, limit)
- self.io.units[unit_id].limit = limit
-end
-
return process
diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua
index 51816f6..4003d71 100644
--- a/coordinator/renderer.lua
+++ b/coordinator/renderer.lua
@@ -1,3 +1,7 @@
+--
+-- Graphics Rendering Control
+--
+
local log = require("scada-common.log")
local util = require("scada-common.util")
@@ -56,6 +60,7 @@ function renderer.set_displays(monitors)
end
-- check if the renderer is configured to use a given monitor peripheral
+---@nodiscard
---@param periph table peripheral
---@return boolean is_used
function renderer.is_monitor_used(periph)
@@ -87,6 +92,7 @@ function renderer.reset(recolor)
end
-- check main display width
+---@nodiscard
---@return boolean width_okay
function renderer.validate_main_display_width()
local w, _ = engine.monitors.primary.getSize()
@@ -94,6 +100,7 @@ function renderer.validate_main_display_width()
end
-- check display sizes
+---@nodiscard
---@return boolean valid all unit display dimensions OK
function renderer.validate_unit_display_sizes()
local valid = true
@@ -101,7 +108,7 @@ function renderer.validate_unit_display_sizes()
for id, monitor in pairs(engine.monitors.unit_displays) do
local w, h = monitor.getSize()
if w ~= 79 or h ~= 52 then
- log.warning(util.c("unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
+ log.warning(util.c("RENDERER: unit ", id, " display resolution not 79 wide by 52 tall: ", w, ", ", h))
valid = false
end
end
@@ -171,6 +178,7 @@ function renderer.close_ui()
end
-- is the UI ready?
+---@nodiscard
---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end
diff --git a/coordinator/sounder.lua b/coordinator/sounder.lua
index ff0ec17..373b8f1 100644
--- a/coordinator/sounder.lua
+++ b/coordinator/sounder.lua
@@ -14,7 +14,7 @@ local sounder = {}
local _2_PI = 2 * math.pi -- 2 whole pies, hope you're hungry
local _DRATE = 48000 -- 48kHz audio
-local _MAX_VAL = 127/2 -- max signed integer in this 8-bit audio
+local _MAX_VAL = 127 / 2 -- max signed integer in this 8-bit audio
local _MAX_SAMPLES = 0x20000 -- 128 * 1024 samples
local _05s_SAMPLES = 24000 -- half a second worth of samples
@@ -26,7 +26,8 @@ local alarm_ctl = {
playing = false,
num_active = 0,
next_block = 1,
- quad_buffer = { {}, {}, {}, {} } -- split audio up into 0.5s samples so specific components can be ended quicker
+ -- split audio up into 0.5s samples so specific components can be ended quicker
+ quad_buffer = { {}, {}, {}, {} }
}
-- sounds modeled after https://www.e2s.com/references-and-guidelines/listen-and-download-alarm-tones
@@ -52,6 +53,7 @@ local TONES = {
}
-- calculate how many samples are in the given number of milliseconds
+---@nodiscard
---@param ms integer milliseconds
---@return integer samples
local function ms_to_samples(ms) return math.floor(ms * 48) end
@@ -224,6 +226,7 @@ end
--#endregion
-- hard audio limiter
+---@nodiscard
---@param output number output level
---@return number limited -128.0 to 127.0
local function limit(output)
@@ -454,7 +457,7 @@ function sounder.test_power_scale()
end
end
- log.debug("power rescale test took " .. (util.time_ms() - start) .. "ms")
+ log.debug("SOUNDER: power rescale test took " .. (util.time_ms() - start) .. "ms")
end
--#endregion
diff --git a/coordinator/startup.lua b/coordinator/startup.lua
index aebb5dd..8af8567 100644
--- a/coordinator/startup.lua
+++ b/coordinator/startup.lua
@@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
-local COORDINATOR_VERSION = "beta-v0.10.1"
+local COORDINATOR_VERSION = "v0.11.0"
local print = util.print
local println = util.println
@@ -81,7 +81,7 @@ local function main()
-- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured or monitors == nil then
- println("boot> monitor setup failed")
+ println("startup> monitor setup failed")
log.fatal("monitor configuration failed")
return
end
@@ -91,11 +91,11 @@ local function main()
renderer.reset(config.RECOLOR)
if not renderer.validate_main_display_width() then
- println("boot> main display must be 8 blocks wide")
+ println("startup> main display must be 8 blocks wide")
log.fatal("main display not wide enough")
return
elseif not renderer.validate_unit_display_sizes() then
- println("boot> one or more unit display dimensions incorrect; they must be 4x4 blocks")
+ println("startup> one or more unit display dimensions incorrect; they must be 4x4 blocks")
log.fatal("unit display dimensions incorrect")
return
end
@@ -116,7 +116,7 @@ local function main()
local speaker = ppm.get_device("speaker")
if speaker == nil then
log_boot("annunciator alarm speaker not found")
- println("boot> speaker not found")
+ println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found")
return
else
@@ -135,7 +135,7 @@ local function main()
local modem = ppm.get_wireless_modem()
if modem == nil then
log_comms("wireless modem not found")
- println("boot> wireless modem not found")
+ println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
else
@@ -145,12 +145,12 @@ local function main()
-- create connection watchdog
local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
conn_watchdog.cancel()
- log.debug("boot> conn watchdog created")
+ log.debug("startup> conn watchdog created")
-- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN,
config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
- log.debug("boot> comms init")
+ log.debug("startup> comms init")
log_comms("comms initialized")
-- base loop clock (2Hz, 10 ticks)
@@ -176,7 +176,7 @@ local function main()
end
if not init_connect_sv() then
- println("boot> failed to connect to supervisor")
+ println("startup> failed to connect to supervisor")
log_sys("system shutdown")
return
else
@@ -199,7 +199,7 @@ local function main()
renderer.close_ui()
log_graphics(util.c("UI crashed: ", message))
println_ts("UI crashed")
- log.fatal(util.c("ui crashed with error ", message))
+ log.fatal(util.c("GUI crashed with error ", message))
else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
@@ -223,7 +223,7 @@ local function main()
if ui_ok then
-- start connection watchdog
conn_watchdog.feed()
- log.debug("boot> conn watchdog started")
+ log.debug("startup> conn watchdog started")
log_sys("system started successfully")
end
@@ -243,7 +243,6 @@ local function main()
no_modem = true
log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!")
- log.error("comms modem disconnected!")
-- close out UI
renderer.close_ui()
@@ -252,20 +251,21 @@ local function main()
log_sys("awaiting comms modem reconnect...")
else
log_sys("non-comms modem disconnected")
- log.warning("non-comms modem disconnected")
end
elseif type == "monitor" then
if renderer.is_monitor_used(device) then
-- "halt and catch fire" style handling
- println_ts("lost a configured monitor, system will now exit")
- log_sys("lost a configured monitor, system will now exit")
+ local msg = "lost a configured monitor, system will now exit"
+ println_ts(msg)
+ log_sys(msg)
break
else
log_sys("lost unused monitor, ignoring")
end
elseif type == "speaker" then
- println_ts("lost alarm sounder speaker")
- log_sys("lost alarm sounder speaker")
+ local msg = "lost alarm sounder speaker"
+ println_ts(msg)
+ log_sys(msg)
end
end
elseif event == "peripheral" then
@@ -291,8 +291,9 @@ local function main()
elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors
elseif type == "speaker" then
- println_ts("alarm sounder speaker reconnected")
- log_sys("alarm sounder speaker reconnected")
+ local msg = "alarm sounder speaker reconnected"
+ println_ts(msg)
+ log_sys(msg)
sounder.reconnect(device)
end
end
@@ -301,7 +302,7 @@ local function main()
-- main loop tick
-- free any closed sessions
- --apisessions.free_all_closed()
+ apisessions.free_all_closed()
-- update date and time string for main display
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format))
@@ -326,7 +327,7 @@ local function main()
-- a non-clock/main watchdog timer event
--check API watchdogs
- --apisessions.check_all_watchdogs(param1)
+ apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher
tcallbackdsp.handle(param1)
diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua
index 17b3e66..26944e0 100644
--- a/coordinator/ui/components/processctl.lua
+++ b/coordinator/ui/components/processctl.lua
@@ -16,11 +16,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad")
local TriIndicatorLight = require("graphics.elements.indicators.trilight")
-local VerticalBar = require("graphics.elements.indicators.vbar")
local HazardButton = require("graphics.elements.controls.hazard_button")
-local MultiButton = require("graphics.elements.controls.multi_button")
-local PushButton = require("graphics.elements.controls.push_button")
local RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric")
diff --git a/coordinator/ui/components/reactor.lua b/coordinator/ui/components/reactor.lua
index 5dc9f23..a17fc75 100644
--- a/coordinator/ui/components/reactor.lua
+++ b/coordinator/ui/components/reactor.lua
@@ -1,3 +1,5 @@
+local types = require("scada-common.types")
+
local style = require("coordinator.ui.style")
local core = require("graphics.core")
@@ -47,7 +49,7 @@ local function new_view(root, x, y, data, ps)
local waste = HorizontalBar{parent=reactor_fills,x=8,y=5,show_percent=true,bar_fg_bg=cpair(colors.brown,colors.gray),height=1,width=14}
ps.subscribe("ccool_type", function (type)
- if type == "mekanism:sodium" then
+ if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray))
else
ccool.recolor(cpair(colors.blue, colors.gray))
@@ -55,7 +57,7 @@ local function new_view(root, x, y, data, ps)
end)
ps.subscribe("hcool_type", function (type)
- if type == "mekanism:superheated_sodium" then
+ if type == types.FLUID.SUPERHEATED_SODIUM then
hcool.recolor(cpair(colors.orange, colors.gray))
else
hcool.recolor(cpair(colors.white, colors.gray))
diff --git a/coordinator/ui/components/unit_detail.lua b/coordinator/ui/components/unit_detail.lua
index f5716b5..f507682 100644
--- a/coordinator/ui/components/unit_detail.lua
+++ b/coordinator/ui/components/unit_detail.lua
@@ -237,13 +237,13 @@ local function init(parent, id)
local rcs_annunc = Div{parent=rcs,width=27,height=22,x=2,y=1}
local rcs_tags = Div{parent=rcs,width=2,height=13,x=29,y=9}
- local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
- local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow}
- local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
- local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
- local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
- local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
- local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
+ local c_flt = IndicatorLight{parent=rcs_annunc,label="RCS Hardware Fault",colors=cpair(colors.yellow,colors.gray)}
+ local c_emg = TriIndicatorLight{parent=rcs_annunc,label="Emergency Coolant",c1=colors.gray,c2=colors.white,c3=colors.yellow}
+ local c_cfm = IndicatorLight{parent=rcs_annunc,label="Coolant Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
+ local c_brm = IndicatorLight{parent=rcs_annunc,label="Boil Rate Mismatch",colors=cpair(colors.yellow,colors.gray)}
+ local c_sfm = IndicatorLight{parent=rcs_annunc,label="Steam Feed Mismatch",colors=cpair(colors.yellow,colors.gray)}
+ local c_mwrf = IndicatorLight{parent=rcs_annunc,label="Max Water Return Feed",colors=cpair(colors.yellow,colors.gray)}
+ local c_tbnt = IndicatorLight{parent=rcs_annunc,label="Turbine Trip",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS}
u_ps.subscribe("RCSFault", c_flt.update)
u_ps.subscribe("EmergencyCoolant", c_emg.update)
@@ -287,7 +287,7 @@ local function init(parent, id)
end
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[1].subscribe("SteamDumpOpen", function (val) t1_sdo.update(val + 1) end)
+ t_ps[1].subscribe("SteamDumpOpen", t1_sdo.update)
TextBox{parent=rcs_tags,text="T1",width=2,height=1,fg_bg=bw_fg_bg}
local t1_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
@@ -300,7 +300,7 @@ local function init(parent, id)
if unit.num_turbines > 1 then
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[2].subscribe("SteamDumpOpen", function (val) t2_sdo.update(val + 1) end)
+ t_ps[2].subscribe("SteamDumpOpen", t2_sdo.update)
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg}
local t2_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
@@ -314,7 +314,7 @@ local function init(parent, id)
if unit.num_turbines > 2 then
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red}
- t_ps[3].subscribe("SteamDumpOpen", function (val) t3_sdo.update(val + 1) end)
+ t_ps[3].subscribe("SteamDumpOpen", t3_sdo.update)
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg}
local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}
diff --git a/coordinator/ui/components/unit_overview.lua b/coordinator/ui/components/unit_overview.lua
index 672c6ea..e5a07f9 100644
--- a/coordinator/ui/components/unit_overview.lua
+++ b/coordinator/ui/components/unit_overview.lua
@@ -101,16 +101,16 @@ local function make(parent, x, y, unit)
local steam_pipes_b = {}
if no_boilers then
- table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
- table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
+ table.insert(steam_pipes_b, pipe(0, 1, 3, 1, colors.white)) -- steam to turbine 1
+ table.insert(steam_pipes_b, pipe(0, 2, 3, 2, colors.blue)) -- water to turbine 1
if num_turbines >= 2 then
- table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
- table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
+ table.insert(steam_pipes_b, pipe(1, 2, 3, 9, colors.white)) -- steam to turbine 2
+ table.insert(steam_pipes_b, pipe(2, 3, 3, 10, colors.blue)) -- water to turbine 2
end
if num_turbines >= 3 then
- table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
+ table.insert(steam_pipes_b, pipe(1, 9, 3, 17, colors.white)) -- steam boiler 1 to turbine 1 junction end
table.insert(steam_pipes_b, pipe(2, 10, 3, 18, colors.blue)) -- water boiler 1 to turbine 1 junction start
end
else
diff --git a/coordinator/ui/components/unit_waiting.lua b/coordinator/ui/components/unit_waiting.lua
index f7fd7ec..3b1a846 100644
--- a/coordinator/ui/components/unit_waiting.lua
+++ b/coordinator/ui/components/unit_waiting.lua
@@ -1,5 +1,5 @@
--
--- Reactor Unit SCADA Coordinator GUI
+-- Reactor Unit Waiting Spinner
--
local style = require("coordinator.ui.style")
diff --git a/coordinator/ui/dialog.lua b/coordinator/ui/dialog.lua
index 4c2a522..676ae2b 100644
--- a/coordinator/ui/dialog.lua
+++ b/coordinator/ui/dialog.lua
@@ -3,13 +3,11 @@ local completion = require("cc.completion")
local util = require("scada-common.util")
local print = util.print
-local println = util.println
-local print_ts = util.print_ts
-local println_ts = util.println_ts
local dialog = {}
-- ask the user yes or no
+---@nodiscard
---@param question string
---@param default boolean
---@return boolean|nil
@@ -36,6 +34,7 @@ function dialog.ask_y_n(question, default)
end
-- ask the user for an input within a set of options
+---@nodiscard
---@param options table
---@param cancel string
---@return boolean|string|nil
diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua
index b200f44..94d25f8 100644
--- a/coordinator/ui/layout/main_view.lua
+++ b/coordinator/ui/layout/main_view.lua
@@ -77,7 +77,7 @@ local function init(monitor)
end
end
- -- command & control
+ -- command & control
cnc_y_start = cnc_y_start
@@ -90,7 +90,7 @@ local function init(monitor)
cnc_bottom_align_start = cnc_bottom_align_start + 2
- local process = process_ctl(main, 2, cnc_bottom_align_start)
+ process_ctl(main, 2, cnc_bottom_align_start)
-- testing
---@fixme remove test code
@@ -123,7 +123,7 @@ local function init(monitor)
SwitchButton{parent=audio,x=1,text="RCS TRANSIENT",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_rcs}
SwitchButton{parent=audio,x=1,text="TURBINE TRIP",min_width=23,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=sounder.test_turbinet}
- local imatrix_1 = imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
+ imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
return main
end
diff --git a/graphics/core.lua b/graphics/core.lua
index 510e31d..98c8ed5 100644
--- a/graphics/core.lua
+++ b/graphics/core.lua
@@ -16,6 +16,7 @@ local events = {}
---@field y integer
-- create a new touch event definition
+---@nodiscard
---@param monitor string
---@param x integer
---@param y integer
@@ -32,7 +33,7 @@ core.events = events
local graphics = {}
----@alias TEXT_ALIGN integer
+---@enum TEXT_ALIGN
graphics.TEXT_ALIGN = {
LEFT = 1,
CENTER = 2,
@@ -47,6 +48,7 @@ graphics.TEXT_ALIGN = {
---@alias element_id string|integer
-- create a new border definition
+---@nodiscard
---@param width integer border width
---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
@@ -66,6 +68,7 @@ end
---@field h integer
-- create a new graphics frame definition
+---@nodiscard
---@param x integer
---@param y integer
---@param w integer
@@ -91,6 +94,7 @@ end
---@field blit_bkg string
-- create a new color pair definition
+---@nodiscard
---@param a color
---@param b color
---@return cpair
@@ -120,9 +124,9 @@ end
---@field thin boolean true for 1 subpixel, false (default) for 2
---@field align_tr boolean false to align bottom left (default), true to align top right
--- create a new pipe
---
+-- create a new pipe
-- note: pipe coordinate origin is (0, 0)
+---@nodiscard
---@param x1 integer starting x, origin is 0
---@param y1 integer starting y, origin is 0
---@param x2 integer ending x, origin is 0
diff --git a/graphics/element.lua b/graphics/element.lua
index 5f32060..8aa3ce9 100644
--- a/graphics/element.lua
+++ b/graphics/element.lua
@@ -47,6 +47,7 @@ local element = {}
---|tiling_args
-- a base graphics element, should not be created on its own
+---@nodiscard
---@param args graphics_args arguments
function element.new(args)
local self = {
@@ -172,6 +173,7 @@ function element.new(args)
end
-- get value
+ ---@nodiscard
function protected.get_value()
return protected.value
end
@@ -218,6 +220,7 @@ function element.new(args)
end
-- get public interface
+ ---@nodiscard
---@return graphics_element element, element_id id
function protected.get() return public, self.id end
@@ -246,11 +249,13 @@ function element.new(args)
----------------------
-- get the window object
+ ---@nodiscard
function public.window() return protected.window end
-- CHILD ELEMENTS --
-- add a child element
+ ---@nodiscard
---@param key string|nil id
---@param child graphics_template
---@return integer|string key
@@ -271,6 +276,7 @@ function element.new(args)
end
-- get a child element
+ ---@nodiscard
---@return graphics_element
function public.get_child(key) return self.children[key] end
@@ -279,6 +285,7 @@ function element.new(args)
function public.remove(key) self.children[key] = nil end
-- attempt to get a child element by ID (does not include this element itself)
+ ---@nodiscard
---@param id element_id
---@return graphics_element|nil element
function public.get_element_by_id(id)
@@ -297,39 +304,49 @@ function element.new(args)
-- AUTO-PLACEMENT --
-- skip a line for automatically placed elements
- function public.line_break() self.next_y = self.next_y + 1 end
+ function public.line_break()
+ self.next_y = self.next_y + 1
+ end
-- PROPERTIES --
-- get the foreground/background colors
+ ---@nodiscard
---@return cpair fg_bg
- function public.get_fg_bg() return protected.fg_bg end
+ function public.get_fg_bg()
+ return protected.fg_bg
+ end
-- get element x
+ ---@nodiscard
---@return integer x
function public.get_x()
return protected.frame.x
end
-- get element y
+ ---@nodiscard
---@return integer y
function public.get_y()
return protected.frame.y
end
-- get element width
+ ---@nodiscard
---@return integer width
function public.width()
return protected.frame.w
end
-- get element height
+ ---@nodiscard
---@return integer height
function public.height()
return protected.frame.h
end
-- get the element value
+ ---@nodiscard
---@return any value
function public.get_value()
return protected.get_value()
diff --git a/graphics/elements/displaybox.lua b/graphics/elements/displaybox.lua
index 0d7fd47..c7e5c9f 100644
--- a/graphics/elements/displaybox.lua
+++ b/graphics/elements/displaybox.lua
@@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new root display box
+---@nodiscard
---@param args displaybox_args
local function displaybox(args)
-- create new graphics element base object
diff --git a/graphics/elements/div.lua b/graphics/elements/div.lua
index 59e3a1e..5eeef71 100644
--- a/graphics/elements/div.lua
+++ b/graphics/elements/div.lua
@@ -13,6 +13,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new div element
+---@nodiscard
---@param args div_args
---@return graphics_element element, element_id id
local function div(args)
diff --git a/graphics/elements/indicators/alight.lua b/graphics/elements/indicators/alight.lua
index eea103a..8bb8fa6 100644
--- a/graphics/elements/indicators/alight.lua
+++ b/graphics/elements/indicators/alight.lua
@@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
-- new alarm indicator light
+---@nodiscard
---@param args alarm_indicator_light
---@return graphics_element element, element_id id
local function alarm_indicator_light(args)
diff --git a/graphics/elements/indicators/coremap.lua b/graphics/elements/indicators/coremap.lua
index 0ca72e1..c50348b 100644
--- a/graphics/elements/indicators/coremap.lua
+++ b/graphics/elements/indicators/coremap.lua
@@ -14,6 +14,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted
-- new core map box
+---@nodiscard
---@param args core_map_args
---@return graphics_element element, element_id id
local function core_map(args)
diff --git a/graphics/elements/indicators/data.lua b/graphics/elements/indicators/data.lua
index d19fab0..66d45dc 100644
--- a/graphics/elements/indicators/data.lua
+++ b/graphics/elements/indicators/data.lua
@@ -19,6 +19,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new data indicator
+---@nodiscard
---@param args data_indicator_args
---@return graphics_element element, element_id id
local function data(args)
diff --git a/graphics/elements/indicators/hbar.lua b/graphics/elements/indicators/hbar.lua
index a05cdb6..2d9b110 100644
--- a/graphics/elements/indicators/hbar.lua
+++ b/graphics/elements/indicators/hbar.lua
@@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new horizontal bar
+---@nodiscard
---@param args hbar_args
---@return graphics_element element, element_id id
local function hbar(args)
diff --git a/graphics/elements/indicators/icon.lua b/graphics/elements/indicators/icon.lua
index 0c71d29..f31479d 100644
--- a/graphics/elements/indicators/icon.lua
+++ b/graphics/elements/indicators/icon.lua
@@ -20,6 +20,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new icon indicator
+---@nodiscard
---@param args icon_indicator_args
---@return graphics_element element, element_id id
local function icon(args)
diff --git a/graphics/elements/indicators/light.lua b/graphics/elements/indicators/light.lua
index 3695553..e764ad9 100644
--- a/graphics/elements/indicators/light.lua
+++ b/graphics/elements/indicators/light.lua
@@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
-- new indicator light
+---@nodiscard
---@param args indicator_light_args
---@return graphics_element element, element_id id
local function indicator_light(args)
diff --git a/graphics/elements/indicators/power.lua b/graphics/elements/indicators/power.lua
index e76f690..1d727ae 100644
--- a/graphics/elements/indicators/power.lua
+++ b/graphics/elements/indicators/power.lua
@@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new power indicator
+---@nodiscard
---@param args power_indicator_args
---@return graphics_element element, element_id id
local function power(args)
diff --git a/graphics/elements/indicators/rad.lua b/graphics/elements/indicators/rad.lua
index 86ec856..2e4ad56 100644
--- a/graphics/elements/indicators/rad.lua
+++ b/graphics/elements/indicators/rad.lua
@@ -19,6 +19,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new radiation indicator
+---@nodiscard
---@param args rad_indicator_args
---@return graphics_element element, element_id id
local function rad(args)
diff --git a/graphics/elements/indicators/state.lua b/graphics/elements/indicators/state.lua
index 386910c..10d081b 100644
--- a/graphics/elements/indicators/state.lua
+++ b/graphics/elements/indicators/state.lua
@@ -20,6 +20,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new state indicator
+---@nodiscard
---@param args state_indicator_args
---@return graphics_element element, element_id id
local function state_indicator(args)
diff --git a/graphics/elements/indicators/trilight.lua b/graphics/elements/indicators/trilight.lua
index 2c61fb7..543ebf5 100644
--- a/graphics/elements/indicators/trilight.lua
+++ b/graphics/elements/indicators/trilight.lua
@@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors
-- new tri-state indicator light
+---@nodiscard
---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id
local function tristate_indicator_light(args)
diff --git a/graphics/elements/indicators/vbar.lua b/graphics/elements/indicators/vbar.lua
index be7d9e4..fe7f9bc 100644
--- a/graphics/elements/indicators/vbar.lua
+++ b/graphics/elements/indicators/vbar.lua
@@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors
-- new vertical bar
+---@nodiscard
---@param args vbar_args
---@return graphics_element element, element_id id
local function vbar(args)
diff --git a/graphics/elements/rectangle.lua b/graphics/elements/rectangle.lua
index 09c20da..6422cbc 100644
--- a/graphics/elements/rectangle.lua
+++ b/graphics/elements/rectangle.lua
@@ -144,7 +144,7 @@ local function rectangle(args)
e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end
else
- if (args.thin == true) then
+ if args.thin == true then
e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
else
e.window.blit(p_s, blit_fg, blit_bg_sides)
diff --git a/graphics/elements/tiling.lua b/graphics/elements/tiling.lua
index 86af96d..a97438a 100644
--- a/graphics/elements/tiling.lua
+++ b/graphics/elements/tiling.lua
@@ -60,7 +60,7 @@ local function tiling(args)
-- create pattern
for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y)
- for x = 1, inner_width do
+ for _ = 1, inner_width do
if alternator then
if even then
e.window.blit(" ", "00", fill_a .. fill_a)
diff --git a/graphics/flasher.lua b/graphics/flasher.lua
index b5eed69..0a3d9ea 100644
--- a/graphics/flasher.lua
+++ b/graphics/flasher.lua
@@ -21,8 +21,7 @@ local active = false
local registry = { {}, {}, {} } -- one registry table per period
local callback_counter = 0
--- blink registered indicators
---
+-- blink registered indicators
-- this assumes it is called every 250ms, it does no checking of time on its own
local function callback_250ms()
if active then
@@ -55,8 +54,7 @@ function flasher.clear()
registry = { {}, {}, {} }
end
--- register a function to be called on the selected blink period
---
+-- register a function to be called on the selected blink period
-- times are not strictly enforced, but all with a given period will be set at the same time
---@param f function function to call each period
---@param period PERIOD time period option (1, 2, or 3)
diff --git a/install_manifest.json b/install_manifest.json
index f68183c..1f0e40f 100644
--- a/install_manifest.json
+++ b/install_manifest.json
@@ -2,10 +2,10 @@
"versions": {
"bootloader": "0.2",
"comms": "1.4.0",
- "reactor-plc": "beta-v0.11.1",
- "rtu": "beta-v0.11.2",
- "supervisor": "beta-v0.12.2",
- "coordinator": "beta-v0.10.1",
+ "reactor-plc": "v0.12.0",
+ "rtu": "v0.12.1",
+ "supervisor": "v0.13.1",
+ "coordinator": "v0.11.0",
"pocket": "alpha-v0.0.0"
},
"files": {
@@ -177,13 +177,13 @@
},
"sizes": {
"system": 1982,
- "common": 88163,
- "graphics": 99360,
+ "common": 88565,
+ "graphics": 99858,
"lockbox": 100797,
- "reactor-plc": 75902,
- "rtu": 81679,
- "supervisor": 268416,
- "coordinator": 181783,
+ "reactor-plc": 75621,
+ "rtu": 85496,
+ "supervisor": 270182,
+ "coordinator": 183279,
"pocket": 335
}
}
\ No newline at end of file
diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua
index f6bbe5d..973ed66 100644
--- a/reactor-plc/plc.lua
+++ b/reactor-plc/plc.lua
@@ -1,4 +1,5 @@
local comms = require("scada-common.comms")
+local const = require("scada-common.constants")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local types = require("scada-common.types")
@@ -6,15 +7,17 @@ local util = require("scada-common.util")
local plc = {}
-local rps_status_t = types.rps_status_t
+local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
-local PROTOCOLS = comms.PROTOCOLS
-local DEVICE_TYPES = comms.DEVICE_TYPES
+local PROTOCOL = comms.PROTOCOL
+local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
-local RPLC_TYPES = comms.RPLC_TYPES
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
+local RPLC_TYPE = comms.RPLC_TYPE
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local AUTO_ACK = comms.PLC_AUTO_ACK
+local RPS_LIMITS = const.RPS_LIMITS
+
local print = util.print
local println = util.println
local print_ts = util.print_ts
@@ -25,21 +28,10 @@ local println_ts = util.println_ts
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active."
--- RPS SAFETY CONSTANTS
-
-local MAX_DAMAGE_PERCENT = 90
-local MAX_DAMAGE_TEMPERATURE = 1200
-local MIN_COOLANT_FILL = 0.10
-local MAX_WASTE_FILL = 0.8
-local MAX_HEATED_COLLANT_FILL = 0.95
-
--- END RPS SAFETY CONSTANTS
-
---- RPS: Reactor Protection System
----
---- identifies dangerous states and SCRAMs reactor if warranted
----
---- autonomous from main SCADA supervisor/coordinator control
+-- RPS: Reactor Protection System
+-- identifies dangerous states and SCRAMs reactor if warranted
+-- autonomous from main SCADA supervisor/coordinator control
+---@nodiscard
---@param reactor table
---@param is_formed boolean
function plc.rps_init(reactor, is_formed)
@@ -59,24 +51,20 @@ function plc.rps_init(reactor, is_formed)
}
local self = {
- reactor = reactor,
state = { false, false, false, false, false, false, false, false, false, false, false, false },
reactor_enabled = false,
enabled_at = 0,
formed = is_formed,
force_disabled = false,
tripped = false,
- trip_cause = "ok" ---@type rps_trip_cause
+ trip_cause = "ok" ---@type rps_trip_cause
}
- ---@class rps
- local public = {}
-
-- PRIVATE FUNCTIONS --
-- set reactor access fault flag
local function _set_fault()
- if self.reactor.__p_last_fault() ~= "Terminated" then
+ if reactor.__p_last_fault() ~= "Terminated" then
self.state[state_keys.fault] = true
end
end
@@ -88,7 +76,7 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is formed
local function _is_formed()
- local formed = self.reactor.isFormed()
+ local formed = reactor.isFormed()
if formed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
@@ -103,7 +91,7 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is force disabled
local function _is_force_disabled()
- local disabled = self.reactor.isForceDisabled()
+ local disabled = reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
@@ -118,77 +106,80 @@ function plc.rps_init(reactor, is_formed)
-- check for critical damage
local function _damage_critical()
- local damage_percent = self.reactor.getDamagePercent()
+ local damage_percent = reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.dmg_crit] then
- self.state[state_keys.dmg_crit] = damage_percent >= MAX_DAMAGE_PERCENT
+ self.state[state_keys.dmg_crit] = damage_percent >= RPS_LIMITS.MAX_DAMAGE_PERCENT
end
end
-- check if the reactor is at a critically high temperature
local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
- local temp = self.reactor.getTemperature()
+ local temp = reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.high_temp] then
- self.state[state_keys.high_temp] = temp >= MAX_DAMAGE_TEMPERATURE
+ self.state[state_keys.high_temp] = temp >= RPS_LIMITS.MAX_DAMAGE_TEMPERATURE
end
end
-- check if there is no coolant (<2% filled)
local function _no_coolant()
- local coolant_filled = self.reactor.getCoolantFilledPercentage()
+ local coolant_filled = reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.no_coolant] then
- self.state[state_keys.no_coolant] = coolant_filled < MIN_COOLANT_FILL
+ self.state[state_keys.no_coolant] = coolant_filled < RPS_LIMITS.MIN_COOLANT_FILL
end
end
-- check for excess waste (>80% filled)
local function _excess_waste()
- local w_filled = self.reactor.getWasteFilledPercentage()
+ local w_filled = reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_waste] then
- self.state[state_keys.ex_waste] = w_filled > MAX_WASTE_FILL
+ self.state[state_keys.ex_waste] = w_filled > RPS_LIMITS.MAX_WASTE_FILL
end
end
-- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant()
- local hc_filled = self.reactor.getHeatedCoolantFilledPercentage()
+ local hc_filled = reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.ex_hcoolant] then
- self.state[state_keys.ex_hcoolant] = hc_filled > MAX_HEATED_COLLANT_FILL
+ self.state[state_keys.ex_hcoolant] = hc_filled > RPS_LIMITS.MAX_HEATED_COLLANT_FILL
end
end
-- check if there is no fuel
local function _insufficient_fuel()
- local fuel = self.reactor.getFuel()
+ local fuel = reactor.getFuelFilledPercentage()
if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later
_set_fault()
elseif not self.state[state_keys.no_fuel] then
- self.state[state_keys.no_fuel] = fuel == 0
+ self.state[state_keys.no_fuel] = fuel <= RPS_LIMITS.NO_FUEL_FILL
end
end
-- PUBLIC FUNCTIONS --
+ ---@class rps
+ local public = {}
+
-- re-link a reactor after a peripheral re-connect
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_reactor(reactor)
- self.reactor = reactor
+ ---@param new_reactor table reconnected reactor
+ function public.reconnect_reactor(new_reactor)
+ reactor = new_reactor
end
-- trip for lost peripheral
@@ -222,8 +213,8 @@ function plc.rps_init(reactor, is_formed)
function public.scram()
log.info("RPS: reactor SCRAM")
- self.reactor.scram()
- if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
+ reactor.scram()
+ if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
log.error("RPS: failed reactor SCRAM")
return false
else
@@ -239,8 +230,8 @@ function plc.rps_init(reactor, is_formed)
if not self.tripped then
log.info("RPS: reactor start")
- self.reactor.activate()
- if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_START_MSG) then
+ reactor.activate()
+ if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true
@@ -260,7 +251,7 @@ function plc.rps_init(reactor, is_formed)
-- clear automatic SCRAM if it was the cause
if self.tripped and self.trip_cause == "automatic" then
self.state[state_keys.automatic] = true
- self.trip_cause = rps_status_t.ok
+ self.trip_cause = RPS_TRIP_CAUSE.OK
self.tripped = false
log.debug("RPS: cleared automatic SCRAM for re-activation")
@@ -270,9 +261,10 @@ function plc.rps_init(reactor, is_formed)
end
-- check all safety conditions
- ---@return boolean tripped, rps_status_t trip_status, boolean first_trip
+ ---@nodiscard
+ ---@return boolean tripped, rps_trip_cause trip_status, boolean first_trip
function public.check()
- local status = rps_status_t.ok
+ local status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped
local first_trip = false
@@ -298,47 +290,47 @@ function plc.rps_init(reactor, is_formed)
status = self.trip_cause
elseif self.state[state_keys.sys_fail] then
log.warning("RPS: system failure, reactor not formed")
- status = rps_status_t.sys_fail
+ status = RPS_TRIP_CAUSE.SYS_FAIL
elseif self.state[state_keys.force_disabled] then
log.warning("RPS: reactor was force disabled")
- status = rps_status_t.force_disabled
+ status = RPS_TRIP_CAUSE.FORCE_DISABLED
elseif self.state[state_keys.dmg_crit] then
log.warning("RPS: damage critical")
- status = rps_status_t.dmg_crit
+ status = RPS_TRIP_CAUSE.DMG_CRIT
elseif self.state[state_keys.high_temp] then
log.warning("RPS: high temperature")
- status = rps_status_t.high_temp
+ status = RPS_TRIP_CAUSE.HIGH_TEMP
elseif self.state[state_keys.no_coolant] then
log.warning("RPS: no coolant")
- status = rps_status_t.no_coolant
+ status = RPS_TRIP_CAUSE.NO_COOLANT
elseif self.state[state_keys.ex_waste] then
log.warning("RPS: full waste")
- status = rps_status_t.ex_waste
+ status = RPS_TRIP_CAUSE.EX_WASTE
elseif self.state[state_keys.ex_hcoolant] then
log.warning("RPS: heated coolant backup")
- status = rps_status_t.ex_hcoolant
+ status = RPS_TRIP_CAUSE.EX_HCOOLANT
elseif self.state[state_keys.no_fuel] then
log.warning("RPS: no fuel")
- status = rps_status_t.no_fuel
+ status = RPS_TRIP_CAUSE.NO_FUEL
elseif self.state[state_keys.fault] then
log.warning("RPS: reactor access fault")
- status = rps_status_t.fault
+ status = RPS_TRIP_CAUSE.FAULT
elseif self.state[state_keys.timeout] then
log.warning("RPS: supervisor connection timeout")
- status = rps_status_t.timeout
+ status = RPS_TRIP_CAUSE.TIMEOUT
elseif self.state[state_keys.manual] then
log.warning("RPS: manual SCRAM requested")
- status = rps_status_t.manual
+ status = RPS_TRIP_CAUSE.MANUAL
elseif self.state[state_keys.automatic] then
log.warning("RPS: automatic SCRAM requested")
- status = rps_status_t.automatic
+ status = RPS_TRIP_CAUSE.AUTOMATIC
else
self.tripped = false
- self.trip_cause = rps_status_t.ok
+ self.trip_cause = RPS_TRIP_CAUSE.OK
end
-- if a new trip occured...
- if (not was_tripped) and (status ~= rps_status_t.ok) then
+ if (not was_tripped) and (status ~= RPS_TRIP_CAUSE.OK) then
first_trip = true
self.tripped = true
self.trip_cause = status
@@ -359,16 +351,23 @@ function plc.rps_init(reactor, is_formed)
return self.tripped, status, first_trip
end
+ ---@nodiscard
function public.status() return self.state end
+ ---@nodiscard
function public.is_tripped() return self.tripped end
+ ---@nodiscard
function public.get_trip_cause() return self.trip_cause end
+ ---@nodiscard
function public.is_active() return self.reactor_enabled end
+ ---@nodiscard
function public.is_formed() return self.formed end
+ ---@nodiscard
function public.is_force_disabled() return self.force_disabled end
-- get the runtime of the reactor if active, or the last runtime if disabled
+ ---@nodiscard
---@return integer runtime time since last enable
function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end
@@ -376,7 +375,7 @@ function plc.rps_init(reactor, is_formed)
---@param quiet? boolean true to suppress the info log message
function public.reset(quiet)
self.tripped = false
- self.trip_cause = rps_status_t.ok
+ self.trip_cause = RPS_TRIP_CAUSE.OK
for i = 1, #self.state do
self.state[i] = false
@@ -390,8 +389,8 @@ function plc.rps_init(reactor, is_formed)
self.state[state_keys.automatic] = false
self.state[state_keys.timeout] = false
- if self.trip_cause == rps_status_t.automatic or self.trip_cause == rps_status_t.timeout then
- self.trip_cause = rps_status_t.ok
+ if self.trip_cause == RPS_TRIP_CAUSE.AUTOMATIC or self.trip_cause == RPS_TRIP_CAUSE.TIMEOUT then
+ self.trip_cause = RPS_TRIP_CAUSE.OK
self.tripped = false
log.info("RPS: auto reset")
@@ -402,6 +401,7 @@ function plc.rps_init(reactor, is_formed)
end
-- Reactor PLC Communications
+---@nodiscard
---@param id integer reactor ID
---@param version string PLC version
---@param modem table modem device
@@ -415,10 +415,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local self = {
seq_num = 0,
r_seq_num = nil,
- modem = modem,
- s_port = server_port,
- l_port = local_port,
- reactor = reactor,
scrammed = false,
linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW,
@@ -428,46 +424,43 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
max_burn_rate = nil
}
- ---@class plc_comms
- local public = {}
-
comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS --
-- configure modem channels
local function _conf_channels()
- self.modem.closeAll()
- self.modem.open(self.l_port)
+ modem.closeAll()
+ modem.open(local_port)
end
_conf_channels()
-- send an RPLC packet
- ---@param msg_type RPLC_TYPES
+ ---@param msg_type RPLC_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
r_pkt.make(id, msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
- self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
+ modem.transmit(server_port, local_port, s_pkt.raw_sendable())
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
- ---@param msg_type SCADA_MGMT_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
+ modem.transmit(server_port, local_port, s_pkt.raw_sendable())
self.seq_num = self.seq_num + 1
end
@@ -500,21 +493,21 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
}
local tasks = {
- function () data_table[1] = self.reactor.getStatus() end,
- function () data_table[2] = self.reactor.getBurnRate() end,
- function () data_table[3] = self.reactor.getActualBurnRate() end,
- function () data_table[4] = self.reactor.getTemperature() end,
- function () data_table[5] = self.reactor.getDamagePercent() end,
- function () data_table[6] = self.reactor.getBoilEfficiency() end,
- function () data_table[7] = self.reactor.getEnvironmentalLoss() end,
- function () fuel = self.reactor.getFuel() end,
- function () data_table[9] = self.reactor.getFuelFilledPercentage() end,
- function () waste = self.reactor.getWaste() end,
- function () data_table[11] = self.reactor.getWasteFilledPercentage() end,
- function () coolant = self.reactor.getCoolant() end,
- function () data_table[14] = self.reactor.getCoolantFilledPercentage() end,
- function () hcoolant = self.reactor.getHeatedCoolant() end,
- function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end
+ function () data_table[1] = reactor.getStatus() end,
+ function () data_table[2] = reactor.getBurnRate() end,
+ function () data_table[3] = reactor.getActualBurnRate() end,
+ function () data_table[4] = reactor.getTemperature() end,
+ function () data_table[5] = reactor.getDamagePercent() end,
+ function () data_table[6] = reactor.getBoilEfficiency() end,
+ function () data_table[7] = reactor.getEnvironmentalLoss() end,
+ function () fuel = reactor.getFuel() end,
+ function () data_table[9] = reactor.getFuelFilledPercentage() end,
+ function () waste = reactor.getWaste() end,
+ function () data_table[11] = reactor.getWasteFilledPercentage() end,
+ function () coolant = reactor.getCoolant() end,
+ function () data_table[14] = reactor.getCoolantFilledPercentage() end,
+ function () hcoolant = reactor.getHeatedCoolant() end,
+ function () data_table[17] = reactor.getHeatedCoolantFilledPercentage() end
}
parallel.waitForAll(table.unpack(tasks))
@@ -537,7 +530,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
data_table[16] = hcoolant.amount
end
- return data_table, self.reactor.__p_is_faulted()
+ return data_table, reactor.__p_is_faulted()
end
-- update the status cache if changed
@@ -569,11 +562,11 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
- _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
+ _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- general ack
- ---@param msg_type RPLC_TYPES
+ ---@param msg_type RPLC_TYPE
---@param status boolean|integer
local function _send_ack(msg_type, status)
_send(msg_type, { status })
@@ -587,25 +580,25 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 }
local tasks = {
- function () mek_data[1] = self.reactor.getLength() end,
- function () mek_data[2] = self.reactor.getWidth() end,
- function () mek_data[3] = self.reactor.getHeight() end,
- function () mek_data[4] = self.reactor.getMinPos() end,
- function () mek_data[5] = self.reactor.getMaxPos() end,
- function () mek_data[6] = self.reactor.getHeatCapacity() end,
- function () mek_data[7] = self.reactor.getFuelAssemblies() end,
- function () mek_data[8] = self.reactor.getFuelSurfaceArea() end,
- function () mek_data[9] = self.reactor.getFuelCapacity() end,
- function () mek_data[10] = self.reactor.getWasteCapacity() end,
- function () mek_data[11] = self.reactor.getCoolantCapacity() end,
- function () mek_data[12] = self.reactor.getHeatedCoolantCapacity() end,
- function () mek_data[13] = self.reactor.getMaxBurnRate() end
+ function () mek_data[1] = reactor.getLength() end,
+ function () mek_data[2] = reactor.getWidth() end,
+ function () mek_data[3] = reactor.getHeight() end,
+ function () mek_data[4] = reactor.getMinPos() end,
+ function () mek_data[5] = reactor.getMaxPos() end,
+ function () mek_data[6] = reactor.getHeatCapacity() end,
+ function () mek_data[7] = reactor.getFuelAssemblies() end,
+ function () mek_data[8] = reactor.getFuelSurfaceArea() end,
+ function () mek_data[9] = reactor.getFuelCapacity() end,
+ function () mek_data[10] = reactor.getWasteCapacity() end,
+ function () mek_data[11] = reactor.getCoolantCapacity() end,
+ function () mek_data[12] = reactor.getHeatedCoolantCapacity() end,
+ function () mek_data[13] = reactor.getMaxBurnRate() end
}
parallel.waitForAll(table.unpack(tasks))
- if not self.reactor.__p_is_faulted() then
- _send(RPLC_TYPES.MEK_STRUCT, mek_data)
+ if not reactor.__p_is_faulted() then
+ _send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false
else
log.error("failed to send structure: PPM fault")
@@ -614,19 +607,20 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- PUBLIC FUNCTIONS --
+ ---@class plc_comms
+ local public = {}
+
-- reconnect a newly connected modem
- ---@param modem table
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_modem(modem)
- self.modem = modem
+ ---@param new_modem table
+ function public.reconnect_modem(new_modem)
+ modem = new_modem
_conf_channels()
end
-- reconnect a newly connected reactor
- ---@param reactor table
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_reactor(reactor)
- self.reactor = reactor
+ ---@param new_reactor table
+ function public.reconnect_reactor(new_reactor)
+ reactor = new_reactor
self.status_cache = nil
self.resend_build = true
self.max_burn_rate = nil
@@ -643,12 +637,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
function public.close()
conn_watchdog.cancel()
public.unlink()
- _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
+ _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
end
-- attempt to establish link with supervisor
function public.send_link_req()
- _send_mgmt(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, version, DEVICE_TYPES.PLC, id })
+ _send_mgmt(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PLC, id })
end
-- send live status information
@@ -664,7 +658,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
mek_data = self.status_cache
end
- heating_rate = self.reactor.getHeatingRate()
+ heating_rate = reactor.getHeatingRate()
end
local sys_status = {
@@ -677,35 +671,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
mek_data -- mekanism status data
}
- _send(RPLC_TYPES.STATUS, sys_status)
+ _send(RPLC_TYPE.STATUS, sys_status)
- if self.resend_build then
- _send_struct()
- end
+ if self.resend_build then _send_struct() end
end
end
-- send reactor protection system status
function public.send_rps_status()
if self.linked then
- _send(RPLC_TYPES.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) })
+ _send(RPLC_TYPE.RPS_STATUS, { rps.is_tripped(), rps.get_trip_cause(), table.unpack(rps.status()) })
end
end
-- send reactor protection system alarm
- ---@param cause rps_status_t reactor protection system status
+ ---@param cause rps_trip_cause reactor protection system status
function public.send_rps_alarm(cause)
if self.linked then
- local rps_alarm = {
- cause,
- table.unpack(rps.status())
- }
-
- _send(RPLC_TYPES.RPS_ALARM, rps_alarm)
+ _send(RPLC_TYPE.RPS_ALARM, { cause, table.unpack(rps.status()) })
end
end
-- parse an RPLC packet
+ ---@nodiscard
---@param side string
---@param sender integer
---@param reply_to integer
@@ -721,13 +709,13 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
if s_pkt.is_valid() then
-- get as RPLC packet
- if s_pkt.protocol() == PROTOCOLS.RPLC then
+ if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get()
end
-- get as SCADA management packet
- elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
+ elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
@@ -745,7 +733,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
---@param plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints)
- if packet ~= nil and packet.scada_frame.local_port() == self.l_port then
+ if packet.scada_frame.local_port() == local_port then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
@@ -762,18 +750,19 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local protocol = packet.scada_frame.protocol()
-- handle packet
- if protocol == PROTOCOLS.RPLC then
+ if protocol == PROTOCOL.RPLC then
+ ---@cast packet rplc_frame
if self.linked then
- if packet.type == RPLC_TYPES.STATUS then
+ if packet.type == RPLC_TYPE.STATUS then
-- request of full status, clear cache first
self.status_cache = nil
public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("sent out status cache again, did supervisor miss it?")
- elseif packet.type == RPLC_TYPES.MEK_STRUCT then
+ elseif packet.type == RPLC_TYPE.MEK_STRUCT then
-- request for physical structure
_send_struct()
log.debug("sent out structure again, did supervisor miss it?")
- elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then
+ elseif packet.type == RPLC_TYPE.MEK_BURN_RATE then
-- set the burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
@@ -782,7 +771,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
- self.max_burn_rate = self.reactor.getMaxBurnRate()
+ self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
@@ -793,8 +782,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
setpoints.burn_rate = burn_rate
success = true
else
- self.reactor.setBurnRate(burn_rate)
- success = not self.reactor.__p_is_faulted()
+ reactor.setBurnRate(burn_rate)
+ success = not reactor.__p_is_faulted()
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
@@ -805,29 +794,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
- elseif packet.type == RPLC_TYPES.RPS_ENABLE then
+ elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor
self.scrammed = false
_send_ack(packet.type, rps.activate())
- elseif packet.type == RPLC_TYPES.RPS_SCRAM then
+ elseif packet.type == RPLC_TYPE.RPS_SCRAM then
-- disable the reactor per manual request
self.scrammed = true
rps.trip_manual()
_send_ack(packet.type, true)
- elseif packet.type == RPLC_TYPES.RPS_ASCRAM then
+ elseif packet.type == RPLC_TYPE.RPS_ASCRAM then
-- disable the reactor per automatic request
self.scrammed = true
rps.trip_auto()
_send_ack(packet.type, true)
- elseif packet.type == RPLC_TYPES.RPS_RESET then
+ elseif packet.type == RPLC_TYPE.RPS_RESET then
-- reset the RPS status
rps.reset()
_send_ack(packet.type, true)
- elseif packet.type == RPLC_TYPES.RPS_AUTO_RESET then
+ elseif packet.type == RPLC_TYPE.RPS_AUTO_RESET then
-- reset automatic SCRAM and timeout trips
rps.auto_reset()
_send_ack(packet.type, true)
- elseif packet.type == RPLC_TYPES.AUTO_BURN_RATE then
+ elseif packet.type == RPLC_TYPE.AUTO_BURN_RATE then
-- automatic control requested a new burn rate
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL
@@ -837,7 +826,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
- self.max_burn_rate = self.reactor.getMaxBurnRate()
+ self.max_burn_rate = reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
@@ -848,9 +837,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
- self.auto_last_disable = util.time_ms()
else
- log.debug("AUTO: automatic reactor stop failed")
+ log.warning("AUTO: automatic reactor stop failed")
end
else
ack = AUTO_ACK.ZERO_DIS_OK
@@ -860,12 +848,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
-- activate the reactor
log.debug("AUTO: activating the reactor")
- self.reactor.setBurnRate(0.01)
- if self.reactor.__p_is_faulted() then
- log.debug("AUTO: failed to reset burn rate for auto activation")
+ reactor.setBurnRate(0.01)
+ if reactor.__p_is_faulted() then
+ log.warning("AUTO: failed to reset burn rate for auto activation")
else
if not rps.auto_activate() then
- log.debug("AUTO: automatic reactor activation failed")
+ log.warning("AUTO: automatic reactor activation failed")
end
end
end
@@ -879,8 +867,8 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
ack = AUTO_ACK.RAMP_SET_OK
else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
- self.reactor.setBurnRate(burn_rate)
- ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
+ reactor.setBurnRate(burn_rate)
+ ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
@@ -898,9 +886,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else
log.debug("discarding RPLC packet before linked")
end
- elseif protocol == PROTOCOLS.SCADA_MGMT then
+ elseif protocol == PROTOCOL.SCADA_MGMT then
+ ---@cast packet mgmt_frame
if self.linked then
- if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- link request confirmation
if packet.length == 1 then
log.debug("received unsolicited establish response")
@@ -933,7 +922,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else
log.debug("SCADA_MGMT establish packet length mismatch")
end
- elseif packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ elseif packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1]
@@ -949,7 +938,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end
- elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close
conn_watchdog.cancel()
public.unlink()
@@ -958,14 +947,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
end
- elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- link request confirmation
if packet.length == 1 then
local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then
println_ts("linked!")
- log.debug("supervisor establish request approved")
+ log.info("supervisor establish request approved, PLC is linked")
-- reset remote sequence number and cache
self.r_seq_num = nil
@@ -978,16 +967,16 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
elseif self.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then
println_ts("link request denied, retrying...")
- log.debug("establish request denied")
+ log.info("supervisor establish request denied, retrying")
elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("reactor PLC ID collision (check config), retrying...")
- log.warning("establish request collision")
+ log.warning("establish request collision, retrying")
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
println_ts("supervisor version mismatch (try updating), retrying...")
- log.warning("establish request version mismatch")
+ log.warning("establish request version mismatch, retrying")
else
println_ts("invalid link response, bad channel? retrying...")
- log.error("unknown establish request response")
+ log.error("unknown establish request response, retrying")
end
end
@@ -1006,7 +995,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
end
end
+ ---@nodiscard
function public.is_scrammed() return self.scrammed end
+ ---@nodiscard
function public.is_linked() return self.linked end
return public
diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua
index 9fefc1d..6eecad0 100644
--- a/reactor-plc/startup.lua
+++ b/reactor-plc/startup.lua
@@ -14,7 +14,7 @@ local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads")
-local R_PLC_VERSION = "beta-v0.11.1"
+local R_PLC_VERSION = "v0.12.1"
local print = util.print
local println = util.println
@@ -116,15 +116,15 @@ local function main()
-- we need a reactor, can at least do some things even if it isn't formed though
if smem_dev.reactor == nil then
- println("boot> fission reactor not found");
- log.warning("no reactor on startup")
+ println("init> fission reactor not found");
+ log.warning("init> no reactor on startup")
plc_state.init_ok = false
plc_state.degraded = true
plc_state.no_reactor = true
elseif not smem_dev.reactor.isFormed() then
- println("boot> fission reactor not formed");
- log.warning("reactor logic adapter present, but reactor is not formed")
+ println("init> fission reactor not formed");
+ log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true
plc_state.reactor_formed = false
@@ -132,8 +132,8 @@ local function main()
-- modem is required if networked
if __shared_memory.networked and smem_dev.modem == nil then
- println("boot> wireless modem not found")
- log.warning("no wireless modem on startup")
+ println("init> wireless modem not found")
+ log.warning("init> no wireless modem on startup")
-- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
@@ -145,8 +145,7 @@ local function main()
plc_state.no_modem = true
end
- -- PLC init
- ---
+ -- PLC init
--- EVENT_CONSUMER: this function consumes events
local function init()
if plc_state.init_ok then
@@ -169,18 +168,17 @@ local function main()
config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init")
else
- println("boot> starting in offline mode")
- log.debug("init> running without networking")
+ println("init> starting in offline mode")
+ log.info("init> running without networking")
end
----@diagnostic disable-next-line: param-type-mismatch
util.push_event("clock_start")
- println("boot> completed")
- log.debug("init> boot completed")
+ println("init> completed")
+ log.info("init> startup completed")
else
- println("boot> system in degraded state, awaiting devices...")
- log.warning("init> booted in a degraded state, awaiting peripheral connections...")
+ println("init> system in degraded state, awaiting devices...")
+ log.warning("init> started in a degraded state, awaiting peripheral connections...")
end
end
diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua
index 1c3c29f..d2708fd 100644
--- a/reactor-plc/threads.lua
+++ b/reactor-plc/threads.lua
@@ -28,10 +28,12 @@ local MQ__COMM_CMD = {
}
-- main thread
+---@nodiscard
---@param smem plc_shared_memory
---@param init function
function threads.thread__main(smem, init)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
@@ -44,9 +46,9 @@ function threads.thread__main(smem, init)
local loop_clock = util.new_clock(MAIN_CLOCK)
-- load in from shared memory
- local networked = smem.networked
- local plc_state = smem.plc_state
- local plc_dev = smem.plc_dev
+ local networked = smem.networked
+ local plc_state = smem.plc_state
+ local plc_dev = smem.plc_dev
-- event loop
while true do
@@ -266,7 +268,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
@@ -276,9 +277,11 @@ function threads.thread__main(smem, init)
end
-- RPS operation thread
+---@nodiscard
---@param smem plc_shared_memory
function threads.thread__rps(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
@@ -297,10 +300,10 @@ function threads.thread__rps(smem)
-- thread loop
while true do
-- get plc_sys fields (may have been set late due to degraded boot)
- local rps = smem.plc_sys.rps
- local plc_comms = smem.plc_sys.plc_comms
+ local rps = smem.plc_sys.rps
+ local plc_comms = smem.plc_sys.plc_comms
-- get reactor, may have changed do to disconnect/reconnect
- local reactor = plc_dev.reactor
+ local reactor = plc_dev.reactor
-- RPS checks
if plc_state.init_ok then
@@ -415,9 +418,11 @@ function threads.thread__rps(smem)
end
-- communications sender thread
+---@nodiscard
---@param smem plc_shared_memory
function threads.thread__comms_tx(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
@@ -489,9 +494,11 @@ function threads.thread__comms_tx(smem)
end
-- communications handler thread
+---@nodiscard
---@param smem plc_shared_memory
function threads.thread__comms_rx(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
@@ -562,10 +569,12 @@ function threads.thread__comms_rx(smem)
return public
end
--- apply setpoints
+-- ramp control outputs to desired setpoints
+---@nodiscard
---@param smem plc_shared_memory
function threads.thread__setpoint_control(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua
index ed7cdb5..b93d412 100644
--- a/rtu/dev/boilerv_rtu.lua
+++ b/rtu/dev/boilerv_rtu.lua
@@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device
+---@nodiscard
---@param boiler table
function boilerv_rtu.new(boiler)
local unit = rtu.init_unit()
diff --git a/rtu/dev/envd_rtu.lua b/rtu/dev/envd_rtu.lua
index c09ee0c..ba4758a 100644
--- a/rtu/dev/envd_rtu.lua
+++ b/rtu/dev/envd_rtu.lua
@@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local envd_rtu = {}
-- create new environment detector device
+---@nodiscard
---@param envd table
function envd_rtu.new(envd)
local unit = rtu.init_unit()
diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua
index 6e99453..29405b8 100644
--- a/rtu/dev/imatrix_rtu.lua
+++ b/rtu/dev/imatrix_rtu.lua
@@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device
+---@nodiscard
---@param imatrix table
function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit()
diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua
index 13ca83b..da7db6b 100644
--- a/rtu/dev/redstone_rtu.lua
+++ b/rtu/dev/redstone_rtu.lua
@@ -1,7 +1,7 @@
-local rtu = require("rtu.rtu")
-
local rsio = require("scada-common.rsio")
+local rtu = require("rtu.rtu")
+
local redstone_rtu = {}
local IO_LVL = rsio.IO_LVL
@@ -10,14 +10,15 @@ local digital_read = rsio.digital_read
local digital_write = rsio.digital_write
-- create new redstone device
+---@nodiscard
function redstone_rtu.new()
local unit = rtu.init_unit()
-- get RTU interface
local interface = unit.interface()
+ -- extends rtu_device; fields added manually to please Lua diagnostics
---@class rtu_rs_device
- --- extends rtu_device; fields added manually to please Lua diagnostics
local public = {
io_count = interface.io_count,
read_coil = interface.read_coil,
diff --git a/rtu/dev/sna_rtu.lua b/rtu/dev/sna_rtu.lua
index a4c250f..0339794 100644
--- a/rtu/dev/sna_rtu.lua
+++ b/rtu/dev/sna_rtu.lua
@@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
local sna_rtu = {}
--- create new solar neutron activator (sna) device
+-- create new solar neutron activator (SNA) device
+---@nodiscard
---@param sna table
function sna_rtu.new(sna)
local unit = rtu.init_unit()
diff --git a/rtu/dev/sps_rtu.lua b/rtu/dev/sps_rtu.lua
index 3b7fdf1..ba0a18c 100644
--- a/rtu/dev/sps_rtu.lua
+++ b/rtu/dev/sps_rtu.lua
@@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
local sps_rtu = {}
--- create new super-critical phase shifter (sps) device
+-- create new super-critical phase shifter (SPS) device
+---@nodiscard
---@param sps table
function sps_rtu.new(sps)
local unit = rtu.init_unit()
diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua
index 191427d..eba310c 100644
--- a/rtu/dev/turbinev_rtu.lua
+++ b/rtu/dev/turbinev_rtu.lua
@@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device
+---@nodiscard
---@param turbine table
function turbinev_rtu.new(turbine)
local unit = rtu.init_unit()
diff --git a/rtu/modbus.lua b/rtu/modbus.lua
index 5411f37..20c5939 100644
--- a/rtu/modbus.lua
+++ b/rtu/modbus.lua
@@ -7,22 +7,15 @@ local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE
-- new modbus comms handler object
+---@nodiscard
---@param rtu_dev rtu_device|rtu_rs_device RTU device
---@param use_parallel_read boolean whether or not to use parallel calls when reading
function modbus.new(rtu_dev, use_parallel_read)
- local self = {
- rtu = rtu_dev,
- use_parallel = use_parallel_read
- }
-
- ---@class modbus
- local public = {}
-
local insert = table.insert
- -- read a span of coils (digital outputs)
- --
+ -- read a span of coils (digital outputs)
-- returns a table of readings or a MODBUS_EXCODE error code
+ ---@nodiscard
---@param c_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
@@ -30,20 +23,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
- local _, coils, _, _ = self.rtu.io_count()
+ local _, coils, _, _ = rtu_dev.io_count()
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = c_addr_start + i - 1
- if self.use_parallel then
+ if use_parallel_read then
insert(tasks, function ()
- local reading, fault = self.rtu.read_coil(addr)
+ local reading, fault = rtu_dev.read_coil(addr)
if fault then access_fault = true else readings[i] = reading end
end)
else
- readings[i], access_fault = self.rtu.read_coil(addr)
+ readings[i], access_fault = rtu_dev.read_coil(addr)
if access_fault then
return_ok = false
@@ -54,7 +47,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- run parallel tasks if configured
- if self.use_parallel then
+ if use_parallel_read then
parallel.waitForAll(table.unpack(tasks))
end
@@ -69,9 +62,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings
end
- -- read a span of discrete inputs (digital inputs)
- --
+ -- read a span of discrete inputs (digital inputs)
-- returns a table of readings or a MODBUS_EXCODE error code
+ ---@nodiscard
---@param di_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
@@ -79,20 +72,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
- local discrete_inputs, _, _, _ = self.rtu.io_count()
+ local discrete_inputs, _, _, _ = rtu_dev.io_count()
local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = di_addr_start + i - 1
- if self.use_parallel then
+ if use_parallel_read then
insert(tasks, function ()
- local reading, fault = self.rtu.read_di(addr)
+ local reading, fault = rtu_dev.read_di(addr)
if fault then access_fault = true else readings[i] = reading end
end)
else
- readings[i], access_fault = self.rtu.read_di(addr)
+ readings[i], access_fault = rtu_dev.read_di(addr)
if access_fault then
return_ok = false
@@ -103,7 +96,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- run parallel tasks if configured
- if self.use_parallel then
+ if use_parallel_read then
parallel.waitForAll(table.unpack(tasks))
end
@@ -118,9 +111,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings
end
- -- read a span of holding registers (analog outputs)
- --
+ -- read a span of holding registers (analog outputs)
-- returns a table of readings or a MODBUS_EXCODE error code
+ ---@nodiscard
---@param hr_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
@@ -128,20 +121,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
- local _, _, _, hold_regs = self.rtu.io_count()
+ local _, _, _, hold_regs = rtu_dev.io_count()
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = hr_addr_start + i - 1
- if self.use_parallel then
+ if use_parallel_read then
insert(tasks, function ()
- local reading, fault = self.rtu.read_holding_reg(addr)
+ local reading, fault = rtu_dev.read_holding_reg(addr)
if fault then access_fault = true else readings[i] = reading end
end)
else
- readings[i], access_fault = self.rtu.read_holding_reg(addr)
+ readings[i], access_fault = rtu_dev.read_holding_reg(addr)
if access_fault then
return_ok = false
@@ -152,7 +145,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- run parallel tasks if configured
- if self.use_parallel then
+ if use_parallel_read then
parallel.waitForAll(table.unpack(tasks))
end
@@ -167,9 +160,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings
end
- -- read a span of input registers (analog inputs)
- --
+ -- read a span of input registers (analog inputs)
-- returns a table of readings or a MODBUS_EXCODE error code
+ ---@nodiscard
---@param ir_addr_start integer
---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings
@@ -177,20 +170,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false
- local _, _, input_regs, _ = self.rtu.io_count()
+ local _, _, input_regs, _ = rtu_dev.io_count()
local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = ir_addr_start + i - 1
- if self.use_parallel then
+ if use_parallel_read then
insert(tasks, function ()
- local reading, fault = self.rtu.read_input_reg(addr)
+ local reading, fault = rtu_dev.read_input_reg(addr)
if fault then access_fault = true else readings[i] = reading end
end)
else
- readings[i], access_fault = self.rtu.read_input_reg(addr)
+ readings[i], access_fault = rtu_dev.read_input_reg(addr)
if access_fault then
return_ok = false
@@ -201,7 +194,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- run parallel tasks if configured
- if self.use_parallel then
+ if use_parallel_read then
parallel.waitForAll(table.unpack(tasks))
end
@@ -217,16 +210,17 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- write a single coil (digital output)
+ ---@nodiscard
---@param c_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE
local function _5_write_single_coil(c_addr, value)
local response = nil
- local _, coils, _, _ = self.rtu.io_count()
+ local _, coils, _, _ = rtu_dev.io_count()
local return_ok = c_addr <= coils
if return_ok then
- local access_fault = self.rtu.write_coil(c_addr, value)
+ local access_fault = rtu_dev.write_coil(c_addr, value)
if access_fault then
return_ok = false
@@ -240,16 +234,17 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- write a single holding register (analog output)
+ ---@nodiscard
---@param hr_addr integer
---@param value any
---@return boolean ok, MODBUS_EXCODE
local function _6_write_single_holding_register(hr_addr, value)
local response = nil
- local _, _, _, hold_regs = self.rtu.io_count()
+ local _, _, _, hold_regs = rtu_dev.io_count()
local return_ok = hr_addr <= hold_regs
if return_ok then
- local access_fault = self.rtu.write_holding_reg(hr_addr, value)
+ local access_fault = rtu_dev.write_holding_reg(hr_addr, value)
if access_fault then
return_ok = false
@@ -263,19 +258,20 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- write multiple coils (digital outputs)
+ ---@nodiscard
---@param c_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE
local function _15_write_multiple_coils(c_addr_start, values)
local response = nil
- local _, coils, _, _ = self.rtu.io_count()
+ local _, coils, _, _ = rtu_dev.io_count()
local count = #values
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = c_addr_start + i - 1
- local access_fault = self.rtu.write_coil(addr, values[i])
+ local access_fault = rtu_dev.write_coil(addr, values[i])
if access_fault then
return_ok = false
@@ -291,19 +287,20 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- write multiple holding registers (analog outputs)
+ ---@nodiscard
---@param hr_addr_start integer
---@param values any
---@return boolean ok, MODBUS_EXCODE
local function _16_write_multiple_holding_registers(hr_addr_start, values)
local response = nil
- local _, _, _, hold_regs = self.rtu.io_count()
+ local _, _, _, hold_regs = rtu_dev.io_count()
local count = #values
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
if return_ok then
for i = 1, count do
local addr = hr_addr_start + i - 1
- local access_fault = self.rtu.write_holding_reg(addr, values[i])
+ local access_fault = rtu_dev.write_holding_reg(addr, values[i])
if access_fault then
return_ok = false
@@ -318,7 +315,11 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, response
end
+ ---@class modbus
+ local public = {}
+
-- validate a request without actually executing it
+ ---@nodiscard
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.check_request(packet)
@@ -360,6 +361,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- handle a MODBUS TCP packet and generate a reply
+ ---@nodiscard
---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply
function public.handle_packet(packet)
@@ -420,6 +422,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end
-- return a SERVER_DEVICE_BUSY error reply
+---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet)
@@ -432,6 +435,7 @@ function modbus.reply__srv_device_busy(packet)
end
-- return a NEG_ACKNOWLEDGE error reply
+---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__neg_ack(packet)
@@ -444,6 +448,7 @@ function modbus.reply__neg_ack(packet)
end
-- return a GATEWAY_PATH_UNAVAILABLE error reply
+---@nodiscard
---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet)
diff --git a/rtu/rtu.lua b/rtu/rtu.lua
index ff81659..1344a3e 100644
--- a/rtu/rtu.lua
+++ b/rtu/rtu.lua
@@ -1,24 +1,26 @@
local comms = require("scada-common.comms")
local ppm = require("scada-common.ppm")
local log = require("scada-common.log")
+local types = require("scada-common.types")
local util = require("scada-common.util")
local modbus = require("rtu.modbus")
local rtu = {}
-local PROTOCOLS = comms.PROTOCOLS
-local DEVICE_TYPES = comms.DEVICE_TYPES
+local PROTOCOL = comms.PROTOCOL
+local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
--- create a new RTU
+-- create a new RTU unit
+---@nodiscard
function rtu.init_unit()
local self = {
discrete_inputs = {},
@@ -152,14 +154,13 @@ function rtu.init_unit()
-- public RTU device access
-- get the public interface to this RTU
- function protected.interface()
- return public
- end
+ function protected.interface() return public end
return protected
end
-- RTU Communications
+---@nodiscard
---@param version string RTU version
---@param modem table modem device
---@param local_port integer local listening port
@@ -168,20 +169,12 @@ end
---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog)
local self = {
- version = version,
seq_num = 0,
r_seq_num = nil,
txn_id = 0,
- modem = modem,
- s_port = server_port,
- l_port = local_port,
- conn_watchdog = conn_watchdog,
last_est_ack = ESTABLISH_ACK.ALLOW
}
- ---@class rtu_comms
- local public = {}
-
local insert = table.insert
comms.set_trusted_range(range)
@@ -190,50 +183,46 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- configure modem channels
local function _conf_channels()
- self.modem.closeAll()
- self.modem.open(self.l_port)
+ modem.closeAll()
+ modem.open(local_port)
end
_conf_channels()
-- send a scada management packet
- ---@param msg_type SCADA_MGMT_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
+ modem.transmit(server_port, local_port, s_pkt.raw_sendable())
self.seq_num = self.seq_num + 1
end
-- keep alive ack
---@param srv_time integer
local function _send_keep_alive_ack(srv_time)
- _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() })
+ _send(SCADA_MGMT_TYPE.KEEP_ALIVE, { srv_time, util.time() })
end
-- generate device advertisement table
+ ---@nodiscard
---@param units table
---@return table advertisement
local function _generate_advertisement(units)
local advertisement = {}
for i = 1, #units do
- local unit = units[i] --@type rtu_unit_registry_entry
- local type = comms.rtu_t_to_unit_type(unit.type)
+ local unit = units[i] ---@type rtu_unit_registry_entry
- if type ~= nil then
- local advert = {
- type,
- unit.index,
- unit.reactor
- }
+ if unit.type ~= nil then
+ local advert = { unit.type, unit.index, unit.reactor }
- if type == RTU_UNIT_TYPES.REDSTONE then
+ if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device)
end
@@ -246,20 +235,22 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- PUBLIC FUNCTIONS --
+ ---@class rtu_comms
+ local public = {}
+
-- send a MODBUS TCP packet
---@param m_pkt modbus_packet
function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
- s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
- self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
+ modem.transmit(server_port, local_port, s_pkt.raw_sendable())
self.seq_num = self.seq_num + 1
end
-- reconnect a newly connected modem
- ---@param modem table
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_modem(modem)
- self.modem = modem
+ ---@param new_modem table
+ function public.reconnect_modem(new_modem)
+ modem = new_modem
_conf_channels()
end
@@ -273,30 +264,31 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- close the connection to the server
---@param rtu_state rtu_state
function public.close(rtu_state)
- self.conn_watchdog.cancel()
+ conn_watchdog.cancel()
public.unlink(rtu_state)
- _send(SCADA_MGMT_TYPES.CLOSE, {})
+ _send(SCADA_MGMT_TYPE.CLOSE, {})
end
-- send establish request (includes advertisement)
---@param units table
function public.send_establish(units)
- _send(SCADA_MGMT_TYPES.ESTABLISH, { comms.version, self.version, DEVICE_TYPES.RTU, _generate_advertisement(units) })
+ _send(SCADA_MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.RTU, _generate_advertisement(units) })
end
-- send capability advertisement
---@param units table
function public.send_advertisement(units)
- _send(SCADA_MGMT_TYPES.RTU_ADVERT, _generate_advertisement(units))
+ _send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units))
end
-- notify that a peripheral was remounted
---@param unit_index integer RTU unit ID
function public.send_remounted(unit_index)
- _send(SCADA_MGMT_TYPES.RTU_DEV_REMOUNT, { unit_index })
+ _send(SCADA_MGMT_TYPE.RTU_DEV_REMOUNT, { unit_index })
end
-- parse a MODBUS/SCADA packet
+ ---@nodiscard
---@param side string
---@param sender integer
---@param reply_to integer
@@ -312,13 +304,13 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
if s_pkt.is_valid() then
-- get as MODBUS TCP packet
- if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then
+ if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then
pkt = m_pkt.get()
end
-- get as SCADA management packet
- elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
+ elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
@@ -333,10 +325,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- handle a MODBUS/SCADA packet
---@param packet modbus_frame|mgmt_frame
- ---@param units table
+ ---@param units table RTU units
---@param rtu_state rtu_state
function public.handle_packet(packet, units, rtu_state)
- if packet ~= nil and packet.scada_frame.local_port() == self.l_port then
+ if packet.scada_frame.local_port() == local_port then
-- check sequence number
if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num()
@@ -348,14 +340,14 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
end
-- feed watchdog on valid sequence number
- self.conn_watchdog.feed()
+ conn_watchdog.feed()
local protocol = packet.scada_frame.protocol()
- if protocol == PROTOCOLS.MODBUS_TCP then
+ if protocol == PROTOCOL.MODBUS_TCP then
+ ---@cast packet modbus_frame
if rtu_state.linked then
local return_code = false
----@diagnostic disable-next-line: param-type-mismatch
local reply = modbus.reply__neg_ack(packet)
-- handle MODBUS instruction
@@ -365,20 +357,17 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
if unit.name == "redstone_io" then
-- immediately execute redstone RTU requests
----@diagnostic disable-next-line: param-type-mismatch
return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end
else
-- check validity then pass off to unit comms thread
----@diagnostic disable-next-line: param-type-mismatch
return_code, reply = unit.modbus_io.check_request(packet)
if return_code then
-- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem
if unit.pkt_queue.length() > 3 then
----@diagnostic disable-next-line: param-type-mismatch
reply = modbus.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag)
@@ -392,7 +381,6 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
end
else
-- unit ID out of range?
----@diagnostic disable-next-line: param-type-mismatch
reply = modbus.reply__gw_unavailable(packet)
log.error("received MODBUS packet for non-existent unit")
end
@@ -401,9 +389,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
else
log.debug("discarding MODBUS packet before linked")
end
- elseif protocol == PROTOCOLS.SCADA_MGMT then
+ elseif protocol == PROTOCOL.SCADA_MGMT then
+ ---@cast packet mgmt_frame
-- SCADA management packet
- if packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
if packet.length == 1 then
local est_ack = packet.data[1]
@@ -419,10 +408,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
if est_ack == ESTABLISH_ACK.BAD_VERSION then
-- version mismatch
println_ts("supervisor comms version mismatch (try updating), retrying...")
- log.warning("supervisor connection denied due to comms version mismatch")
+ log.warning("supervisor connection denied due to comms version mismatch, retrying")
else
println_ts("supervisor connection denied, retrying...")
- log.warning("supervisor connection denied")
+ log.warning("supervisor connection denied, retrying")
end
end
@@ -434,7 +423,7 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
log.debug("SCADA_MGMT establish packet length mismatch")
end
elseif rtu_state.linked then
- if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ if packet.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1]
@@ -450,15 +439,15 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
else
log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end
- elseif packet.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- close connection
- self.conn_watchdog.cancel()
+ conn_watchdog.cancel()
public.unlink(rtu_state)
println_ts("server connection closed by remote host")
log.warning("server connection closed by remote host")
- elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then
+ elseif packet.type == SCADA_MGMT_TYPE.RTU_ADVERT then
-- request for capabilities again
- public.send_advertisement(units)
+ public.send_advertisement(units)
else
-- not supported
log.warning("received unsupported SCADA_MGMT message type " .. packet.type)
diff --git a/rtu/startup.lua b/rtu/startup.lua
index 418921c..aff3a22 100644
--- a/rtu/startup.lua
+++ b/rtu/startup.lua
@@ -25,9 +25,9 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local RTU_VERSION = "beta-v0.11.2"
+local RTU_VERSION = "v0.12.1"
-local rtu_t = types.rtu_t
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print
local println = util.println
@@ -132,13 +132,17 @@ local function main()
-- CHECK: reactor ID must be >= to 1
if (not util.is_int(io_reactor)) or (io_reactor < 0) then
- println(util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0"))
+ local message = util.c("configure> redstone entry #", entry_idx, " : ", io_reactor, " isn't an integer >= 0")
+ println(message)
+ log.fatal(message)
return false
end
-- CHECK: io table exists
if type(io_table) ~= "table" then
- println(util.c("configure> redstone entry #", entry_idx, " no IO table found"))
+ local message = util.c("configure> redstone entry #", entry_idx, " no IO table found")
+ println(message)
+ log.fatal(message)
return false
end
@@ -148,10 +152,10 @@ local function main()
local continue = true
- -- check for duplicate entries
+ -- CHECK: no duplicate entries
for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry
- if unit.reactor == io_reactor and unit.type == rtu_t.redstone then
+ if unit.reactor == io_reactor and unit.type == RTU_UNIT_TYPE.REDSTONE then
-- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O")
@@ -181,7 +185,7 @@ local function main()
local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
" (for reactor ", io_reactor, ")")
println(message)
- log.error(message)
+ log.fatal(message)
return false
else
-- link redstone in RTU
@@ -224,23 +228,28 @@ local function main()
---@class rtu_unit_registry_entry
local unit = {
- uid = 0,
- name = "redstone_io",
- type = rtu_t.redstone,
- index = entry_idx,
- reactor = io_reactor,
- device = capabilities, -- use device field for redstone ports
- is_multiblock = false,
- formed = nil, ---@type boolean|nil
- rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
+ uid = 0, ---@type integer
+ name = "redstone_io", ---@type string
+ type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
+ index = entry_idx, ---@type integer
+ reactor = io_reactor, ---@type integer
+ device = capabilities, ---@type table use device field for redstone ports
+ is_multiblock = false, ---@type boolean
+ formed = nil, ---@type boolean|nil
+ rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false),
- pkt_queue = nil, ---@type mqueue|nil
- thread = nil
+ pkt_queue = nil, ---@type mqueue|nil
+ thread = nil ---@type parallel_thread|nil
}
table.insert(units, unit)
- log.debug(util.c("init> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for reactor ", io_reactor))
+ local for_message = "facility"
+ if io_reactor > 0 then
+ for_message = util.c("reactor ", io_reactor)
+ end
+
+ log.info(util.c("configure> initialized RTU unit #", #units, ": redstone_io (redstone) [1] for ", for_message))
unit.uid = #units
end
@@ -254,27 +263,33 @@ local function main()
-- CHECK: name is a string
if type(name) ~= "string" then
- println(util.c("configure> device entry #", i, ": device ", name, " isn't a string"))
+ local message = util.c("configure> device entry #", i, ": device ", name, " isn't a string")
+ println(message)
+ log.fatal(message)
return false
end
-- CHECK: index is an integer >= 1
if (not util.is_int(index)) or (index <= 0) then
- println(util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1"))
+ local message = util.c("configure> device entry #", i, ": index ", index, " isn't an integer >= 1")
+ println(message)
+ log.fatal(message)
return false
end
-- CHECK: reactor is an integer >= 0
if (not util.is_int(for_reactor)) or (for_reactor < 0) then
- println(util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0"))
+ local message = util.c("configure> device entry #", i, ": reactor ", for_reactor, " isn't an integer >= 0")
+ println(message)
+ log.fatal(message)
return false
end
local device = ppm.get_periph(name)
- local type = nil
+ local type = nil ---@type string|nil
local rtu_iface = nil ---@type rtu_device
- local rtu_type = ""
+ local rtu_type = nil ---@type RTU_UNIT_TYPE
local is_multiblock = false
local formed = nil ---@type boolean|nil
@@ -291,7 +306,7 @@ local function main()
if type == "boilerValve" then
-- boiler multiblock
- rtu_type = rtu_t.boiler_valve
+ rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface = boilerv_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -303,7 +318,7 @@ local function main()
end
elseif type == "turbineValve" then
-- turbine multiblock
- rtu_type = rtu_t.turbine_valve
+ rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface = turbinev_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -315,7 +330,7 @@ local function main()
end
elseif type == "inductionPort" then
-- induction matrix multiblock
- rtu_type = rtu_t.induction_matrix
+ rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface = imatrix_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -327,7 +342,7 @@ local function main()
end
elseif type == "spsPort" then
-- SPS multiblock
- rtu_type = rtu_t.sps
+ rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface = sps_rtu.new(device)
is_multiblock = true
formed = device.isFormed()
@@ -339,15 +354,15 @@ local function main()
end
elseif type == "solarNeutronActivator" then
-- SNA
- rtu_type = rtu_t.sna
+ rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface = sna_rtu.new(device)
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
- rtu_type = rtu_t.env_detector
+ rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device
- rtu_type = "virtual"
+ rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface()
else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")")
@@ -358,18 +373,18 @@ local function main()
---@class rtu_unit_registry_entry
local rtu_unit = {
- uid = 0,
- name = name,
- type = rtu_type,
- index = index,
- reactor = for_reactor,
- device = device,
- is_multiblock = is_multiblock,
+ uid = 0, ---@type integer
+ name = name, ---@type string
+ type = rtu_type, ---@type RTU_UNIT_TYPE
+ index = index, ---@type integer
+ reactor = for_reactor, ---@type integer
+ device = device, ---@type table
+ is_multiblock = is_multiblock, ---@type boolean
formed = formed, ---@type boolean|nil
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil
- thread = nil
+ thread = nil ---@type parallel_thread|nil
}
rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
@@ -377,7 +392,7 @@ local function main()
table.insert(units, rtu_unit)
if is_multiblock and not formed then
- log.debug(util.c("configure> device '", name, "' is not formed"))
+ log.info(util.c("configure> device '", name, "' is not formed"))
end
local for_message = "facility"
@@ -385,7 +400,7 @@ local function main()
for_message = util.c("reactor ", for_reactor)
end
- log.debug(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", rtu_type, ") [", index, "] for ", for_message))
+ log.info(util.c("configure> initialized RTU unit #", #units, ": ", name, " (", types.rtu_type_to_string(rtu_type), ") [", index, "] for ", for_message))
rtu_unit.uid = #units
end
@@ -403,12 +418,12 @@ local function main()
if configure() then
-- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
- log.debug("boot> conn watchdog started")
+ log.debug("startup> conn watchdog started")
-- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT,
config.TRUSTED_RANGE, smem_sys.conn_watchdog)
- log.debug("boot> comms init")
+ log.debug("startup> comms init")
-- init threads
local main_thread = threads.thread__main(__shared_memory)
@@ -422,6 +437,8 @@ local function main()
end
end
+ log.info("startup> completed")
+
-- run threads
parallel.waitForAll(table.unpack(_threads))
else
diff --git a/rtu/threads.lua b/rtu/threads.lua
index 7af184e..6b06eb0 100644
--- a/rtu/threads.lua
+++ b/rtu/threads.lua
@@ -1,21 +1,21 @@
-local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
-local ppm = require("scada-common.ppm")
-local types = require("scada-common.types")
-local util = require("scada-common.util")
+local log = require("scada-common.log")
+local mqueue = require("scada-common.mqueue")
+local ppm = require("scada-common.ppm")
+local types = require("scada-common.types")
+local util = require("scada-common.util")
-local boilerv_rtu = require("rtu.dev.boilerv_rtu")
-local envd_rtu = require("rtu.dev.envd_rtu")
-local imatrix_rtu = require("rtu.dev.imatrix_rtu")
-local sna_rtu = require("rtu.dev.sna_rtu")
-local sps_rtu = require("rtu.dev.sps_rtu")
-local turbinev_rtu = require("rtu.dev.turbinev_rtu")
+local boilerv_rtu = require("rtu.dev.boilerv_rtu")
+local envd_rtu = require("rtu.dev.envd_rtu")
+local imatrix_rtu = require("rtu.dev.imatrix_rtu")
+local sna_rtu = require("rtu.dev.sna_rtu")
+local sps_rtu = require("rtu.dev.sps_rtu")
+local turbinev_rtu = require("rtu.dev.turbinev_rtu")
-local modbus = require("rtu.modbus")
+local modbus = require("rtu.modbus")
local threads = {}
-local rtu_t = types.rtu_t
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print
local println = util.println
@@ -26,9 +26,11 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
-- main thread
+---@nodiscard
---@param smem rtu_shared_memory
function threads.thread__main(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
@@ -93,8 +95,9 @@ function threads.thread__main(smem)
-- we are going to let the PPM prevent crashes
-- return fault flags/codes to MODBUS queries
local unit = units[i]
- println_ts(util.c("lost the ", unit.type, " on interface ", unit.name))
- log.warning(util.c("lost the ", unit.type, " unit peripheral on interface ", unit.name))
+ local type_name = types.rtu_type_to_string(unit.type)
+ println_ts(util.c("lost the ", type_name, " on interface ", unit.name))
+ log.warning(util.c("lost the ", type_name, " unit peripheral on interface ", unit.name))
break
end
end
@@ -112,9 +115,9 @@ function threads.thread__main(smem)
rtu_comms.reconnect_modem(rtu_dev.modem)
println_ts("wireless modem reconnected.")
- log.info("comms modem reconnected.")
+ log.info("comms modem reconnected")
else
- log.info("wired modem reconnected.")
+ log.info("wired modem reconnected")
end
else
-- relink lost peripheral to correct unit entry
@@ -129,51 +132,51 @@ function threads.thread__main(smem)
-- found, re-link
unit.device = device
- if unit.type == "virtual" then
+ if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true
if type == "boilerValve" then
-- boiler multiblock
- unit.type = rtu_t.boiler_valve
+ unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then
-- turbine multiblock
- unit.type = rtu_t.turbine_valve
+ unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then
-- induction matrix multiblock
- unit.type = rtu_t.induction_matrix
+ unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then
-- SPS multiblock
- unit.type = rtu_t.sps
+ unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then
-- SNA
- unit.type = rtu_t.sna
+ unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then
-- advanced peripherals environment detector
- unit.type = rtu_t.env_detector
+ unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else
resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end
end
- if unit.type == rtu_t.boiler_valve then
+ if unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
unit.rtu = boilerv_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
- elseif unit.type == rtu_t.turbine_valve then
+ elseif unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
unit.rtu = turbinev_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
- elseif unit.type == rtu_t.induction_matrix then
+ elseif unit.type == RTU_UNIT_TYPE.IMATRIX then
unit.rtu = imatrix_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
- elseif unit.type == rtu_t.sps then
+ elseif unit.type == RTU_UNIT_TYPE.SPS then
unit.rtu = sps_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil)
- elseif unit.type == rtu_t.sna then
+ elseif unit.type == RTU_UNIT_TYPE.SNA then
unit.rtu = sna_rtu.new(device)
- elseif unit.type == rtu_t.env_detector then
+ elseif unit.type == RTU_UNIT_TYPE.ENV_DETECTOR then
unit.rtu = envd_rtu.new(device)
else
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true)
@@ -185,8 +188,10 @@ function threads.thread__main(smem)
unit.modbus_io = modbus.new(unit.rtu, true)
- println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name)
- log.info("reconnected the " .. unit.type .. " on interface " .. unit.name)
+ local type_name = types.rtu_type_to_string(unit.type)
+ local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
+ println_ts(message)
+ log.info(message)
if resend_advert then
rtu_comms.send_advertisement(units)
@@ -229,22 +234,24 @@ function threads.thread__main(smem)
end
-- communications handler thread
+---@nodiscard
---@param smem rtu_shared_memory
function threads.thread__comms(smem)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
log.debug("comms thread start")
-- load in from shared memory
- local rtu_state = smem.rtu_state
- local rtu_comms = smem.rtu_sys.rtu_comms
- local units = smem.rtu_sys.units
+ local rtu_state = smem.rtu_state
+ local rtu_comms = smem.rtu_sys.rtu_comms
+ local units = smem.rtu_sys.units
- local comms_queue = smem.q.mq_comms
+ local comms_queue = smem.q.mq_comms
- local last_update = util.time()
+ local last_update = util.time()
-- thread loop
while true do
@@ -301,14 +308,16 @@ function threads.thread__comms(smem)
end
-- per-unit communications handler thread
+---@nodiscard
---@param smem rtu_shared_memory
---@param unit rtu_unit_registry_entry
function threads.thread__unit_comms(smem, unit)
- local public = {} ---@class thread
+ ---@class parallel_thread
+ local public = {}
-- execute thread
function public.exec()
- log.debug("rtu unit thread start -> " .. unit.type .. "(" .. unit.name .. ")")
+ log.debug(util.c("rtu unit thread start -> ", types.rtu_type_to_string(unit.type), "(", unit.name, ")"))
-- load in from shared memory
local rtu_state = smem.rtu_state
@@ -319,8 +328,8 @@ function threads.thread__unit_comms(smem, unit)
local last_f_check = 0
- local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
- local short_name = util.c(unit.type, " (", unit.name, ")")
+ local detail_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor)
+ local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
if packet_queue == nil then
log.error("rtu unit thread created without a message queue, exiting...", true)
@@ -368,25 +377,25 @@ function threads.thread__unit_comms(smem, unit)
local type, device = ppm.mount(iface)
if device ~= nil then
- if type == "boilerValve" and unit.type == rtu_t.boiler_valve then
+ if type == "boilerValve" and unit.type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler multiblock
unit.device = device
unit.rtu = boilerv_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
- elseif type == "turbineValve" and unit.type == rtu_t.turbine_valve then
+ elseif type == "turbineValve" and unit.type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine multiblock
unit.device = device
unit.rtu = turbinev_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
- elseif type == "inductionPort" and unit.type == rtu_t.induction_matrix then
+ elseif type == "inductionPort" and unit.type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix multiblock
unit.device = device
unit.rtu = imatrix_rtu.new(device)
unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true)
- elseif type == "spsPort" and unit.type == rtu_t.sps then
+ elseif type == "spsPort" and unit.type == RTU_UNIT_TYPE.SPS then
-- SPS multiblock
unit.device = device
unit.rtu = sps_rtu.new(device)
@@ -433,7 +442,7 @@ function threads.thread__unit_comms(smem, unit)
end
if not rtu_state.shutdown then
- log.info(util.c("rtu unit thread ", unit.type, "(", unit.name, " restarting in 5 seconds..."))
+ log.info(util.c("rtu unit thread ", types.rtu_type_to_string(unit.type), "(", unit.name, " restarting in 5 seconds..."))
util.psleep(5)
end
end
diff --git a/scada-common/comms.lua b/scada-common/comms.lua
index b275622..ad7c0aa 100644
--- a/scada-common/comms.lua
+++ b/scada-common/comms.lua
@@ -3,21 +3,18 @@
--
local log = require("scada-common.log")
-local types = require("scada-common.types")
---@class comms
local comms = {}
-local rtu_t = types.rtu_t
-
local insert = table.insert
local max_distance = nil
comms.version = "1.4.0"
----@alias PROTOCOLS integer
-local PROTOCOLS = {
+---@enum PROTOCOL
+local PROTOCOL = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc
@@ -25,8 +22,8 @@ local PROTOCOLS = {
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
----@alias RPLC_TYPES integer
-local RPLC_TYPES = {
+---@enum RPLC_TYPE
+local RPLC_TYPE = {
STATUS = 0, -- reactor/system status
MEK_STRUCT = 1, -- mekanism build structure
MEK_BURN_RATE = 2, -- set burn rate
@@ -40,8 +37,8 @@ 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
-local SCADA_MGMT_TYPES = {
+---@enum SCADA_MGMT_TYPE
+local SCADA_MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection
@@ -49,8 +46,8 @@ local SCADA_MGMT_TYPES = {
RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
}
----@alias SCADA_CRDN_TYPES integer
-local SCADA_CRDN_TYPES = {
+---@enum SCADA_CRDN_TYPE
+local SCADA_CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds
FAC_STATUS = 2, -- state of facility and facility devices
@@ -60,12 +57,11 @@ local SCADA_CRDN_TYPES = {
UNIT_CMD = 6 -- command a reactor unit
}
----@alias CAPI_TYPES integer
-local CAPI_TYPES = {
- ESTABLISH = 0 -- initial greeting
+---@enum CAPI_TYPE
+local CAPI_TYPE = {
}
----@alias ESTABLISH_ACK integer
+---@enum ESTABLISH_ACK
local ESTABLISH_ACK = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
@@ -73,26 +69,15 @@ local ESTABLISH_ACK = {
BAD_VERSION = 3 -- link denied due to comms version mismatch
}
----@alias DEVICE_TYPES integer
-local DEVICE_TYPES = {
+---@enum DEVICE_TYPE
+local DEVICE_TYPE = {
PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish
SV = 2, -- supervisor device type for establish
CRDN = 3 -- coordinator device type for establish
}
----@alias RTU_UNIT_TYPES integer
-local RTU_UNIT_TYPES = {
- REDSTONE = 0, -- redstone I/O
- BOILER_VALVE = 1, -- boiler mekanism 10.1+
- TURBINE_VALVE = 2, -- turbine, mekanism 10.1+
- IMATRIX = 3, -- induction matrix
- SPS = 4, -- SPS
- SNA = 5, -- SNA
- 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,16 +85,16 @@ local PLC_AUTO_ACK = {
ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
}
----@alias FAC_COMMANDS integer
-local FAC_COMMANDS = {
+---@enum FAC_COMMAND
+local FAC_COMMAND = {
SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control
START = 2, -- start automatic control
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units
}
----@alias UNIT_COMMANDS integer
-local UNIT_COMMANDS = {
+---@enum UNIT_COMMAND
+local UNIT_COMMAND = {
SCRAM = 0, -- SCRAM the reactor
START = 1, -- start the reactor
RESET_RPS = 2, -- reset the RPS
@@ -121,26 +106,25 @@ local UNIT_COMMANDS = {
SET_GROUP = 8 -- assign this unit to a group
}
-comms.PROTOCOLS = PROTOCOLS
+comms.PROTOCOL = PROTOCOL
-comms.RPLC_TYPES = RPLC_TYPES
-comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
-comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
-comms.CAPI_TYPES = CAPI_TYPES
+comms.RPLC_TYPE = RPLC_TYPE
+comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE
+comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE
+comms.CAPI_TYPE = CAPI_TYPE
comms.ESTABLISH_ACK = ESTABLISH_ACK
-comms.DEVICE_TYPES = DEVICE_TYPES
-comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
+comms.DEVICE_TYPE = DEVICE_TYPE
comms.PLC_AUTO_ACK = PLC_AUTO_ACK
-comms.UNIT_COMMANDS = UNIT_COMMANDS
-comms.FAC_COMMANDS = FAC_COMMANDS
+comms.UNIT_COMMAND = UNIT_COMMAND
+comms.FAC_COMMAND = FAC_COMMAND
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame
--- configure the maximum allowable message receive distance
+-- configure the maximum allowable message receive distance
-- packets received with distances greater than this will be silently discarded
---@param distance integer max modem message distance (less than 1 disables the limit)
function comms.set_trusted_range(distance)
@@ -152,6 +136,7 @@ function comms.set_trusted_range(distance)
end
-- generic SCADA packet object
+---@nodiscard
function comms.scada_packet()
local self = {
modem_msg_in = nil,
@@ -168,7 +153,7 @@ function comms.scada_packet()
-- make a SCADA packet
---@param seq_num integer
- ---@param protocol PROTOCOLS
+ ---@param protocol PROTOCOL
---@param payload table
function public.make(seq_num, protocol, payload)
self.valid = true
@@ -180,11 +165,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 +209,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
-- modeled after MODBUS TCP packet
+---@nodiscard
function comms.modbus_packet()
local self = {
frame = nil,
@@ -248,7 +244,7 @@ function comms.modbus_packet()
txn_id = -1,
length = 0,
unit_id = -1,
- func_code = 0,
+ func_code = 0x80,
data = {}
}
@@ -285,7 +281,7 @@ function comms.modbus_packet()
if frame then
self.frame = frame
- if frame.protocol() == PROTOCOLS.MODBUS_TCP then
+ if frame.protocol() == PROTOCOL.MODBUS_TCP then
local size_ok = frame.length() >= 3
if size_ok then
@@ -309,9 +305,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,12 +328,13 @@ function comms.modbus_packet()
end
-- reactor PLC packet
+---@nodiscard
function comms.rplc_packet()
local self = {
frame = nil,
raw = {},
id = 0,
- type = -1,
+ type = 0, ---@type RPLC_TYPE
length = 0,
data = {}
}
@@ -345,22 +344,22 @@ function comms.rplc_packet()
-- check that type is known
local function _rplc_type_valid()
- return self.type == RPLC_TYPES.STATUS or
- self.type == RPLC_TYPES.MEK_STRUCT or
- self.type == RPLC_TYPES.MEK_BURN_RATE or
- self.type == RPLC_TYPES.RPS_ENABLE or
- self.type == RPLC_TYPES.RPS_SCRAM or
- self.type == RPLC_TYPES.RPS_ASCRAM or
- self.type == RPLC_TYPES.RPS_STATUS or
- self.type == RPLC_TYPES.RPS_ALARM or
- self.type == RPLC_TYPES.RPS_RESET or
- self.type == RPLC_TYPES.RPS_AUTO_RESET or
- self.type == RPLC_TYPES.AUTO_BURN_RATE
+ return self.type == RPLC_TYPE.STATUS or
+ self.type == RPLC_TYPE.MEK_STRUCT or
+ self.type == RPLC_TYPE.MEK_BURN_RATE or
+ self.type == RPLC_TYPE.RPS_ENABLE or
+ self.type == RPLC_TYPE.RPS_SCRAM or
+ self.type == RPLC_TYPE.RPS_ASCRAM or
+ self.type == RPLC_TYPE.RPS_STATUS or
+ self.type == RPLC_TYPE.RPS_ALARM or
+ self.type == RPLC_TYPE.RPS_RESET or
+ self.type == RPLC_TYPE.RPS_AUTO_RESET or
+ self.type == RPLC_TYPE.AUTO_BURN_RATE
end
-- make an RPLC packet
---@param id integer
- ---@param packet_type RPLC_TYPES
+ ---@param packet_type RPLC_TYPE
---@param data table
function public.make(id, packet_type, data)
if type(data) == "table" then
@@ -387,7 +386,7 @@ function comms.rplc_packet()
if frame then
self.frame = frame
- if frame.protocol() == PROTOCOLS.RPLC then
+ if frame.protocol() == PROTOCOL.RPLC then
local ok = frame.length() >= 2
if ok then
@@ -410,9 +409,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,11 +431,12 @@ function comms.rplc_packet()
end
-- SCADA management packet
+---@nodiscard
function comms.mgmt_packet()
local self = {
frame = nil,
raw = {},
- type = -1,
+ type = 0, ---@type SCADA_MGMT_TYPE
length = 0,
data = {}
}
@@ -444,16 +446,16 @@ function comms.mgmt_packet()
-- check that type is known
local function _scada_type_valid()
- return self.type == SCADA_MGMT_TYPES.ESTABLISH or
- self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or
- self.type == SCADA_MGMT_TYPES.CLOSE or
- self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or
- self.type == SCADA_MGMT_TYPES.RTU_ADVERT or
- self.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT
+ return self.type == SCADA_MGMT_TYPE.ESTABLISH or
+ self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or
+ self.type == SCADA_MGMT_TYPE.CLOSE or
+ self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
+ self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
+ self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT
end
-- make a SCADA management packet
- ---@param packet_type SCADA_MGMT_TYPES
+ ---@param packet_type SCADA_MGMT_TYPE
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
@@ -479,7 +481,7 @@ function comms.mgmt_packet()
if frame then
self.frame = frame
- if frame.protocol() == PROTOCOLS.SCADA_MGMT then
+ if frame.protocol() == PROTOCOL.SCADA_MGMT then
local ok = frame.length() >= 1
if ok then
@@ -500,9 +502,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,11 +523,12 @@ function comms.mgmt_packet()
end
-- SCADA coordinator packet
+---@nodiscard
function comms.crdn_packet()
local self = {
frame = nil,
raw = {},
- type = -1,
+ type = 0, ---@type SCADA_CRDN_TYPE
length = 0,
data = {}
}
@@ -532,18 +537,19 @@ 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
- self.type == SCADA_CRDN_TYPES.FAC_STATUS or
- self.type == SCADA_CRDN_TYPES.FAC_CMD or
- self.type == SCADA_CRDN_TYPES.UNIT_BUILDS or
- self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or
- self.type == SCADA_CRDN_TYPES.UNIT_CMD
+ return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or
+ self.type == SCADA_CRDN_TYPE.FAC_BUILDS or
+ self.type == SCADA_CRDN_TYPE.FAC_STATUS or
+ self.type == SCADA_CRDN_TYPE.FAC_CMD or
+ self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or
+ self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or
+ self.type == SCADA_CRDN_TYPE.UNIT_CMD
end
-- make a coordinator packet
- ---@param packet_type SCADA_CRDN_TYPES
+ ---@param packet_type SCADA_CRDN_TYPE
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
@@ -569,7 +575,7 @@ function comms.crdn_packet()
if frame then
self.frame = frame
- if frame.protocol() == PROTOCOLS.SCADA_CRDN then
+ if frame.protocol() == PROTOCOL.SCADA_CRDN then
local ok = frame.length() >= 1
if ok then
@@ -590,9 +596,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,12 +617,13 @@ function comms.crdn_packet()
end
-- coordinator API (CAPI) packet
--- @todo
+---@todo implement for pocket access, set enum type for self.type
+---@nodiscard
function comms.capi_packet()
local self = {
frame = nil,
raw = {},
- type = -1,
+ type = 0,
length = 0,
data = {}
}
@@ -623,12 +632,12 @@ function comms.capi_packet()
local public = {}
local function _capi_type_valid()
- -- @todo
+ ---@todo
return false
end
-- make a coordinator API packet
- ---@param packet_type CAPI_TYPES
+ ---@param packet_type CAPI_TYPE
---@param data table
function public.make(packet_type, data)
if type(data) == "table" then
@@ -654,7 +663,7 @@ function comms.capi_packet()
if frame then
self.frame = frame
- if frame.protocol() == PROTOCOLS.COORD_API then
+ if frame.protocol() == PROTOCOL.COORD_API then
local ok = frame.length() >= 1
if ok then
@@ -675,9 +684,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 = {
@@ -693,50 +704,4 @@ function comms.capi_packet()
return public
end
--- convert rtu_t to RTU unit type
----@param type rtu_t
----@return RTU_UNIT_TYPES|nil
-function comms.rtu_t_to_unit_type(type)
- if type == rtu_t.redstone then
- return RTU_UNIT_TYPES.REDSTONE
- elseif type == rtu_t.boiler_valve then
- return RTU_UNIT_TYPES.BOILER_VALVE
- elseif type == rtu_t.turbine_valve then
- return RTU_UNIT_TYPES.TURBINE_VALVE
- elseif type == rtu_t.induction_matrix then
- return RTU_UNIT_TYPES.IMATRIX
- elseif type == rtu_t.sps then
- return RTU_UNIT_TYPES.SPS
- elseif type == rtu_t.sna then
- return RTU_UNIT_TYPES.SNA
- elseif type == rtu_t.env_detector then
- return RTU_UNIT_TYPES.ENV_DETECTOR
- end
-
- return nil
-end
-
--- convert RTU unit type to rtu_t
----@param utype RTU_UNIT_TYPES
----@return rtu_t|nil
-function comms.advert_type_to_rtu_t(utype)
- if utype == RTU_UNIT_TYPES.REDSTONE then
- return rtu_t.redstone
- elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then
- return rtu_t.boiler_valve
- elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then
- return rtu_t.turbine_valve
- elseif utype == RTU_UNIT_TYPES.IMATRIX then
- return rtu_t.induction_matrix
- elseif utype == RTU_UNIT_TYPES.SPS then
- return rtu_t.sps
- elseif utype == RTU_UNIT_TYPES.SNA then
- return rtu_t.sna
- elseif utype == RTU_UNIT_TYPES.ENV_DETECTOR then
- return rtu_t.env_detector
- end
-
- return nil
-end
-
return comms
diff --git a/scada-common/constants.lua b/scada-common/constants.lua
new file mode 100644
index 0000000..c10722d
--- /dev/null
+++ b/scada-common/constants.lua
@@ -0,0 +1,71 @@
+--
+-- System and Safety Constants
+--
+
+-- Notes on Radiation
+-- - background radiation 0.0000001 Sv/h (99.99 nSv/h)
+-- - "green tint" radiation 0.00001 Sv/h (10 uSv/h)
+-- - damaging radiation 0.00006 Sv/h (60 uSv/h)
+
+local constants = {}
+
+--#region Reactor Protection System (on the PLC) Limits
+
+local rps = {}
+
+rps.MAX_DAMAGE_PERCENT = 90 -- damage >= 90%
+rps.MAX_DAMAGE_TEMPERATURE = 1200 -- temp >= 1200K
+rps.MIN_COOLANT_FILL = 0.10 -- fill < 10%
+rps.MAX_WASTE_FILL = 0.8 -- fill > 80%
+rps.MAX_HEATED_COLLANT_FILL = 0.95 -- fill > 95%
+rps.NO_FUEL_FILL = 0.0 -- fill <= 0%
+
+constants.RPS_LIMITS = rps
+
+--#endregion
+
+--#region Annunciator Limits
+
+local annunc = {}
+
+annunc.RCSFlowLow = -2.0 -- flow < -2.0 mB/s
+annunc.CoolantLevelLow = 0.4 -- fill < 40%
+annunc.ReactorTempHigh = 1000 -- temp > 1000K
+annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
+annunc.FuelLevelLow = 0.05 -- fill <= 5%
+annunc.WasteLevelHigh = 0.85 -- fill >= 85%
+annunc.SteamFeedMismatch = 10 -- ±10mB difference between total coolant flow and total steam input rate
+annunc.RadiationWarning = 0.00001 -- 10 uSv/h
+
+constants.ANNUNCIATOR_LIMITS = annunc
+
+--#endregion
+
+--#region Supervisor Alarm Limits
+
+local alarms = {}
+
+-- unit alarms
+
+alarms.HIGH_TEMP = 1150 -- temp >= 1150K
+alarms.HIGH_WASTE = 0.5 -- fill > 50%
+alarms.HIGH_RADIATION = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
+
+-- facility alarms
+
+alarms.CHARGE_HIGH = 1.0 -- once at or above 100% charge
+alarms.CHARGE_RE_ENABLE = 0.95 -- once below 95% charge
+alarms.FAC_HIGH_RAD = 0.00001 -- 10 uSv/h
+
+constants.ALARM_LIMITS = alarms
+
+--#endregion
+
+--#region Supervisor Constants
+
+-- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks
+constants.FLOW_STABILITY_DELAY_MS = 15000
+
+--#endregion
+
+return constants
diff --git a/scada-common/crypto.lua b/scada-common/crypto.lua
index 6424bcb..a1053bf 100644
--- a/scada-common/crypto.lua
+++ b/scada-common/crypto.lua
@@ -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 = ""
diff --git a/scada-common/log.lua b/scada-common/log.lua
index cc6dd0a..30f785d 100644
--- a/scada-common/log.lua
+++ b/scada-common/log.lua
@@ -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
diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua
index 22bae5d..b48e4ad 100644
--- a/scada-common/mqueue.lua
+++ b/scada-common/mqueue.lua
@@ -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
diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua
index f69de1e..fe9e026 100644
--- a/scada-common/ppm.lua
+++ b/scada-common/ppm.lua
@@ -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
+-- also provides peripheral-specific fault checks (auto-clear fault defaults to true)
+-- 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)
-- 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
diff --git a/scada-common/psil.lua b/scada-common/psil.lua
index ddadf36..c21b2cf 100644
--- a/scada-common/psil.lua
+++ b/scada-common/psil.lua
@@ -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
-- will call func() right away if a value is already avaliable
---@param key string data key
---@param func function function to call on change
diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua
index 79ced10..6b33a24 100644
--- a/scada-common/rsio.lua
+++ b/scada-common/rsio.lua
@@ -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
diff --git a/scada-common/tcallbackdsp.lua b/scada-common/tcallbackdsp.lua
index 52f55da..3f8f07a 100644
--- a/scada-common/tcallbackdsp.lua
+++ b/scada-common/tcallbackdsp.lua
@@ -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
-- prints to log debug output
function tcallbackdsp.diagnostics()
for timer, entry in pairs(registry) do
diff --git a/scada-common/types.lua b/scada-common/types.lua
index 23a4006..7ad0eb8 100644
--- a/scada-common/types.lua
+++ b/scada-common/types.lua
@@ -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_TYPE
---@field index integer
---@field reactor integer
---@field rsio table|nil
@@ -62,15 +68,58 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@alias color integer
-- ENUMERATION TYPES --
+--#region
----@alias TRI_FAIL integer
-types.TRI_FAIL = {
- OK = 0,
- PARTIAL = 1,
- FULL = 2
+---@enum RTU_UNIT_TYPE
+types.RTU_UNIT_TYPE = {
+ VIRTUAL = 0, -- virtual device
+ REDSTONE = 1, -- redstone I/O
+ BOILER_VALVE = 2, -- boiler mekanism 10.1+
+ TURBINE_VALVE = 3, -- turbine, mekanism 10.1+
+ IMATRIX = 4, -- induction matrix
+ SPS = 5, -- SPS
+ SNA = 6, -- SNA
+ ENV_DETECTOR = 7 -- environment detector
}
----@alias PROCESS integer
+types.RTU_UNIT_NAMES = {
+ "redstone",
+ "boiler_valve",
+ "turbine_valve",
+ "induction_matrix",
+ "sps",
+ "sna",
+ "environment_detector"
+}
+
+-- safe conversion of RTU UNIT TYPE to string
+---@nodiscard
+---@param utype RTU_UNIT_TYPE
+---@return string
+function types.rtu_type_to_string(utype)
+ if utype == types.RTU_UNIT_TYPE.VIRTUAL then
+ return "virtual"
+ elseif utype == types.RTU_UNIT_TYPE.REDSTONE or
+ utype == types.RTU_UNIT_TYPE.BOILER_VALVE or
+ utype == types.RTU_UNIT_TYPE.TURBINE_VALVE or
+ utype == types.RTU_UNIT_TYPE.IMATRIX or
+ utype == types.RTU_UNIT_TYPE.SPS or
+ utype == types.RTU_UNIT_TYPE.SNA or
+ utype == types.RTU_UNIT_TYPE.ENV_DETECTOR then
+ return types.RTU_UNIT_NAMES[utype]
+ else
+ return ""
+ end
+end
+
+---@enum TRI_FAIL
+types.TRI_FAIL = {
+ OK = 1,
+ PARTIAL = 2,
+ FULL = 3
+}
+
+---@enum PROCESS
types.PROCESS = {
INACTIVE = 0,
MAX_BURN = 1,
@@ -93,7 +142,7 @@ types.PROCESS_NAMES = {
"GEN_RATE_FAULT_IDLE"
}
----@alias WASTE_MODE integer
+---@enum WASTE_MODE
types.WASTE_MODE = {
AUTO = 1,
PLUTONIUM = 2,
@@ -101,7 +150,14 @@ types.WASTE_MODE = {
ANTI_MATTER = 4
}
----@alias ALARM integer
+types.WASTE_MODE_NAMES = {
+ "AUTO",
+ "PLUTONIUM",
+ "POLONIUM",
+ "ANTI_MATTER"
+}
+
+---@enum ALARM
types.ALARM = {
ContainmentBreach = 1,
ContainmentRadiation = 2,
@@ -117,7 +173,7 @@ types.ALARM = {
TurbineTrip = 12
}
-types.alarm_string = {
+types.ALARM_NAMES = {
"ContainmentBreach",
"ContainmentRadiation",
"ReactorLost",
@@ -132,46 +188,40 @@ types.alarm_string = {
"TurbineTrip"
}
----@alias ALARM_PRIORITY integer
+---@enum ALARM_PRIORITY
types.ALARM_PRIORITY = {
- CRITICAL = 0,
- EMERGENCY = 1,
- URGENT = 2,
- TIMELY = 3
+ CRITICAL = 1,
+ EMERGENCY = 2,
+ URGENT = 3,
+ TIMELY = 4
}
-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
+---@enum ALARM_STATE
+types.ALARM_STATE = {
+ INACTIVE = 1,
+ TRIPPED = 2,
+ ACKED = 3,
+ RING_BACK = 4
}
----@alias ALARM_STATE integer
-types.ALARM_STATE = {
- INACTIVE = 0,
- TRIPPED = 1,
- ACKED = 2,
- RING_BACK = 3
+types.ALARM_STATE_NAMES = {
+ "INACTIVE",
+ "TRIPPED",
+ "ACKED",
+ "RING_BACK"
}
+--#endregion
+
-- STRING TYPES --
+--#region
---@alias os_event
---| "alarm"
@@ -206,14 +256,28 @@ types.ALARM_STATE = {
---| "websocket_failure"
---| "websocket_message"
---| "websocket_success"
+---| "clock_start" custom, added for reactor PLC
+
+---@alias fluid
+---| "mekanism:empty_gas"
+---| "minecraft:water"
+---| "mekanism:sodium"
+---| "mekanism:superheated_sodium"
+
+types.FLUID = {
+ EMPTY_GAS = "mekanism:empty_gas",
+ WATER = "minecraft:water",
+ SODIUM = "mekanism:sodium",
+ SUPERHEATED_SODIUM = "mekanism:superheated_sodium"
+}
---@alias rps_trip_cause
---| "ok"
---| "dmg_crit"
---| "high_temp"
---| "no_coolant"
----| "full_waste"
----| "heated_coolant_backup"
+---| "ex_waste"
+---| "ex_heated_coolant"
---| "no_fuel"
---| "fault"
---| "timeout"
@@ -222,59 +286,40 @@ types.ALARM_STATE = {
---| "sys_fail"
---| "force_disabled"
----@alias fluid
----| "mekanism:empty_gas"
----| "minecraft:water"
----| "mekanism:sodium"
----| "mekanism:superheated_sodium"
-
-types.fluid = {
- empty_gas = "mekanism:empty_gas",
- water = "minecraft:water",
- sodium = "mekanism:sodium",
- superheated_sodium = "mekanism:superheated_sodium"
+types.RPS_TRIP_CAUSE = {
+ OK = "ok",
+ DMG_CRIT = "dmg_crit",
+ HIGH_TEMP = "high_temp",
+ NO_COOLANT = "no_coolant",
+ EX_WASTE = "ex_waste",
+ EX_HCOOLANT = "ex_heated_coolant",
+ NO_FUEL = "no_fuel",
+ FAULT = "fault",
+ TIMEOUT = "timeout",
+ MANUAL = "manual",
+ AUTOMATIC = "automatic",
+ SYS_FAIL = "sys_fail",
+ FORCE_DISABLED = "force_disabled"
}
----@alias rtu_t string
-types.rtu_t = {
- redstone = "redstone",
- boiler_valve = "boiler_valve",
- turbine_valve = "turbine_valve",
- induction_matrix = "induction_matrix",
- sps = "sps",
- sna = "sna",
- env_detector = "environment_detector"
-}
+---@alias dumping_mode
+---| "IDLE"
+---| "DUMPING"
+---| "DUMPING_EXCESS"
----@alias rps_status_t rps_trip_cause
-types.rps_status_t = {
- ok = "ok",
- dmg_crit = "dmg_crit",
- high_temp = "high_temp",
- no_coolant = "no_coolant",
- ex_waste = "full_waste",
- ex_hcoolant = "heated_coolant_backup",
- no_fuel = "no_fuel",
- fault = "fault",
- timeout = "timeout",
- manual = "manual",
- automatic = "automatic",
- sys_fail = "sys_fail",
- force_disabled = "force_disabled"
-}
-
--- turbine steam dumping modes
----@alias DUMPING_MODE string
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 +332,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 +347,6 @@ types.MODBUS_EXCODE = {
GATEWAY_TARGET_TIMEOUT = 0x0B
}
+--#endregion
+
return types
diff --git a/scada-common/util.lua b/scada-common/util.lua
index afe3c59..2913e9f 100644
--- a/scada-common/util.lua
+++ b/scada-common/util.lua
@@ -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
-- 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
--- 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
-- 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
@@ -418,10 +430,11 @@ 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
-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
+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)
+---@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
+-- 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
+-- 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
-- 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
diff --git a/supervisor/facility.lua b/supervisor/facility.lua
index fe9db20..106c4d7 100644
--- a/supervisor/facility.lua
+++ b/supervisor/facility.lua
@@ -1,3 +1,4 @@
+local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
@@ -12,19 +13,13 @@ local PROCESS_NAMES = types.PROCESS_NAMES
local IO = rsio.IO
--- 7.14 kJ per blade for 1 mB of fissile fuel
+-- 7.14 kJ per blade for 1 mB of fissile fuel
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
local POWER_PER_BLADE = util.joules_to_fe(7140)
-local FLOW_STABILITY_DELAY_S = unit.FLOW_STABILITY_DELAY_MS / 1000
+local FLOW_STABILITY_DELAY_S = const.FLOW_STABILITY_DELAY_MS / 1000
--- background radiation 0.0000001 Sv/h (99.99 nSv/h)
--- "green tint" radiation 0.00001 Sv/h (10 uSv/h)
--- damaging radiation 0.00006 Sv/h (60 uSv/h)
-local RADIATION_ALARM_LEVEL = 0.00001
-
-local HIGH_CHARGE = 1.0
-local RE_ENABLE_CHARGE = 0.95
+local ALARM_LIMS = const.ALARM_LIMITS
local AUTO_SCRAM = {
NONE = 0,
@@ -53,6 +48,7 @@ local rate_Kd = -1.0
local facility = {}
-- create a new facility management object
+---@nodiscard
---@param num_reactors integer number of reactor units
---@param cooling_conf table cooling configurations of reactor units
function facility.new(num_reactors, cooling_conf)
@@ -124,6 +120,7 @@ function facility.new(num_reactors, cooling_conf)
end
-- check if all auto-controlled units completed ramping
+ ---@nodiscard
local function _all_units_ramped()
local all_ramped = true
@@ -185,10 +182,7 @@ function facility.new(num_reactors, cooling_conf)
unallocated = math.max(0, unallocated - ctl.br100)
- if last ~= ctl.br100 then
- log.debug("unit " .. u.get_id() .. ": set to " .. ctl.br100 .. " (was " .. last .. ")")
- u.a_commit_br100(ramp)
- end
+ if last ~= ctl.br100 then u.a_commit_br100(ramp) end
end
end
end
@@ -426,7 +420,7 @@ function facility.new(num_reactors, cooling_conf)
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
- local runtime = now - self.time_start
+ -- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
@@ -441,8 +435,8 @@ function facility.new(num_reactors, cooling_conf)
self.saturated = output ~= out_c
- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }",
- runtime, avg_charge, error, integral, output, out_c, P, I, D))
+ -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }",
+ -- runtime, avg_charge, error, integral, output, out_c, P, I, D))
_allocate_burn_rate(out_c, true)
@@ -495,7 +489,7 @@ function facility.new(num_reactors, cooling_conf)
self.accumulator = self.accumulator + (error * (now - self.last_time))
end
- local runtime = now - self.time_start
+ -- local runtime = now - self.time_start
local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time)
@@ -513,8 +507,8 @@ function facility.new(num_reactors, cooling_conf)
self.saturated = output ~= out_c
- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
+ -- log.debug(util.sprintf("GEN_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }",
+ -- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
_allocate_burn_rate(out_c, false)
@@ -564,10 +558,10 @@ function facility.new(num_reactors, cooling_conf)
-- check matrix fill too high
local was_fill = astatus.matrix_fill
- astatus.matrix_fill = (db.tanks.energy_fill >= HIGH_CHARGE) or (astatus.matrix_fill and db.tanks.energy_fill > RE_ENABLE_CHARGE)
+ astatus.matrix_fill = (db.tanks.energy_fill >= ALARM_LIMS.CHARGE_HIGH) or (astatus.matrix_fill and db.tanks.energy_fill > ALARM_LIMS.CHARGE_RE_ENABLE)
if was_fill and not astatus.matrix_fill then
- log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (RE_ENABLE_CHARGE * 100) .. "%")
+ log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (ALARM_LIMS.CHARGE_RE_ENABLE * 100) .. "%")
end
-- check for critical unit alarms
@@ -586,7 +580,7 @@ function facility.new(num_reactors, cooling_conf)
local envd = self.envd[1] ---@type unit_session
local e_db = envd.get_db() ---@type envd_session_db
- astatus.radiation = e_db.radiation_raw > RADIATION_ALARM_LEVEL
+ astatus.radiation = e_db.radiation_raw > ALARM_LIMS.FAC_HIGH_RAD
else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming
-- operator can restart the system or hit the stop/reset button
@@ -814,6 +808,7 @@ function facility.new(num_reactors, cooling_conf)
-- READ STATES/PROPERTIES --
-- get build properties of all machines
+ ---@nodiscard
---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude
function public.get_build(inc_imatrix)
local build = {}
@@ -830,6 +825,7 @@ function facility.new(num_reactors, cooling_conf)
end
-- get automatic process control status
+ ---@nodiscard
function public.get_control_status()
local astat = self.ascram_status
return {
@@ -851,6 +847,7 @@ function facility.new(num_reactors, cooling_conf)
end
-- get RTU statuses
+ ---@nodiscard
function public.get_rtu_statuses()
local status = {}
@@ -889,9 +886,9 @@ function facility.new(num_reactors, cooling_conf)
return status
end
- function public.get_units()
- return self.units
- end
+ -- get the units in this facility
+ ---@nodiscard
+ function public.get_units() return self.units end
return public
end
diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua
index 55bbcc1..1402993 100644
--- a/supervisor/session/coordinator.lua
+++ b/supervisor/session/coordinator.lua
@@ -1,18 +1,20 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
+local types = require("scada-common.types")
local util = require("scada-common.util")
local svqtypes = require("supervisor.session.svqtypes")
local coordinator = {}
-local PROTOCOLS = comms.PROTOCOLS
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
-local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
-local UNIT_COMMANDS = comms.UNIT_COMMANDS
-local FAC_COMMANDS = comms.FAC_COMMANDS
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local PROTOCOL = comms.PROTOCOL
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
+local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
+local UNIT_COMMAND = comms.UNIT_COMMAND
+local FAC_COMMAND = comms.FAC_COMMAND
+
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
local SV_Q_DATA = svqtypes.SV_Q_DATA
@@ -45,6 +47,7 @@ local PERIODICS = {
}
-- coordinator supervisor session
+---@nodiscard
---@param id integer session ID
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
@@ -54,8 +57,6 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local log_header = "crdn_session(" .. id .. "): "
local self = {
- in_q = in_queue,
- out_q = out_queue,
units = facility.get_units(),
-- connection properties
seq_num = 0,
@@ -90,30 +91,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
end
-- send a CRDN packet
- ---@param msg_type SCADA_CRDN_TYPES
+ ---@param msg_type SCADA_CRDN_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_CRDN, c_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_CRDN, c_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
- ---@param msg_type SCADA_MGMT_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
@@ -126,12 +127,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
unit_builds[unit.get_id()] = unit.get_build()
end
- _send(SCADA_CRDN_TYPES.INITIAL_BUILDS, { facility.get_build(), unit_builds })
+ _send(SCADA_CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds })
end
-- send facility builds
local function _send_fac_builds()
- _send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build() })
+ _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build() })
end
-- send unit builds
@@ -143,7 +144,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
builds[unit.get_id()] = unit.get_build()
end
- _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
+ _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
end
-- send facility status
@@ -153,7 +154,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
facility.get_rtu_statuses()
}
- _send(SCADA_CRDN_TYPES.FAC_STATUS, status)
+ _send(SCADA_CRDN_TYPE.FAC_STATUS, status)
end
-- send unit statuses
@@ -172,7 +173,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
}
end
- _send(SCADA_CRDN_TYPES.UNIT_STATUSES, status)
+ _send(SCADA_CRDN_TYPE.UNIT_STATUSES, status)
end
-- handle a packet
@@ -192,8 +193,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
self.conn_watchdog.feed()
-- process packet
- if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
- if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
+ if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
@@ -210,30 +211,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
- elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session
_close()
else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end
- elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then
- if pkt.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then
+ elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
+ if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.builds = true
- elseif pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then
+ elseif pkt.type == SCADA_CRDN_TYPE.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true
- elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then
+ elseif pkt.type == SCADA_CRDN_TYPE.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
- if cmd == FAC_COMMANDS.SCRAM_ALL then
+ if cmd == FAC_COMMAND.SCRAM_ALL then
facility.scram_all()
- _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
- elseif cmd == FAC_COMMANDS.STOP then
+ _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
+ elseif cmd == FAC_COMMAND.STOP then
facility.auto_stop()
- _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
- elseif cmd == FAC_COMMANDS.START then
+ _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
+ elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then
---@type coord_auto_config
local config = {
@@ -244,23 +245,23 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
limits = pkt.data[6]
}
- _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
+ _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
end
- elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then
+ elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
facility.ack_all()
- _send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
+ _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
else
log.debug(log_header .. "CRDN facility command unknown")
end
else
log.debug(log_header .. "CRDN facility command packet length mismatch")
end
- elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
+ elseif pkt.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.unit_builds = true
- elseif pkt.type == SCADA_CRDN_TYPES.UNIT_CMD then
+ elseif pkt.type == SCADA_CRDN_TYPE.UNIT_CMD then
if pkt.length >= 2 then
-- get command and unit id
local cmd = pkt.data[1]
@@ -273,43 +274,43 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
if util.is_int(uid) and uid > 0 and uid <= #self.units then
local unit = self.units[uid] ---@type reactor_unit
- if cmd == UNIT_COMMANDS.START then
- self.out_q.push_data(SV_Q_DATA.START, data)
- elseif cmd == UNIT_COMMANDS.SCRAM then
- self.out_q.push_data(SV_Q_DATA.SCRAM, data)
- elseif cmd == UNIT_COMMANDS.RESET_RPS then
- self.out_q.push_data(SV_Q_DATA.RESET_RPS, data)
- elseif cmd == UNIT_COMMANDS.SET_BURN then
+ if cmd == UNIT_COMMAND.START then
+ out_queue.push_data(SV_Q_DATA.START, data)
+ elseif cmd == UNIT_COMMAND.SCRAM then
+ out_queue.push_data(SV_Q_DATA.SCRAM, data)
+ elseif cmd == UNIT_COMMAND.RESET_RPS then
+ out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
+ elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 then
- self.out_q.push_data(SV_Q_DATA.SET_BURN, data)
+ out_queue.push_data(SV_Q_DATA.SET_BURN, data)
else
log.debug(log_header .. "CRDN unit command burn rate missing option")
end
- elseif cmd == UNIT_COMMANDS.SET_WASTE then
+ elseif cmd == UNIT_COMMAND.SET_WASTE then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] > 0) and (pkt.data[3] <= 4) then
unit.set_waste(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command set waste missing option")
end
- elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
+ elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_all()
- _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, true })
- elseif cmd == UNIT_COMMANDS.ACK_ALARM then
+ _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, true })
+ elseif cmd == UNIT_COMMAND.ACK_ALARM then
if pkt.length == 3 then
unit.ack_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
end
- elseif cmd == UNIT_COMMANDS.RESET_ALARM then
+ elseif cmd == UNIT_COMMAND.RESET_ALARM then
if pkt.length == 3 then
unit.reset_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
end
- elseif cmd == UNIT_COMMANDS.SET_GROUP then
+ elseif cmd == UNIT_COMMAND.SET_GROUP then
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and (pkt.data[3] >= 0) and (pkt.data[3] <= 4) then
facility.set_group(unit.get_id(), pkt.data[3])
- _send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
+ _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN unit command set group missing group id")
end
@@ -332,9 +333,11 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local public = {}
-- get the session ID
+ ---@nodiscard
function public.get_id() return id end
-- check if a timer matches this session's watchdog
+ ---@nodiscard
function public.check_wd(timer)
return self.conn_watchdog.is_timer(timer) and self.connected
end
@@ -342,12 +345,13 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
-- close the connection
function public.close()
_close()
- _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
+ _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to coordinator " .. id .. " closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
+ ---@nodiscard
---@return boolean connected
function public.iterate()
if self.connected then
@@ -357,9 +361,9 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local handle_start = util.time()
- while self.in_q.ready() and self.connected do
+ while in_queue.ready() and self.connected do
-- get a new message to process
- local message = self.in_q.pop()
+ local message = in_queue.pop()
if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET then
@@ -373,7 +377,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
if cmd.key == CRD_S_DATA.CMD_ACK then
local ack = cmd.val ---@type coord_ack
- _send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
+ _send(SCADA_CRDN_TYPE.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
elseif cmd.key == CRD_S_DATA.RESEND_PLC_BUILD then
-- re-send PLC build
-- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update
@@ -386,7 +390,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local unit = self.units[unit_id] ---@type reactor_unit
builds[unit_id] = unit.get_build(true, false, false)
- _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
+ _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then
local unit_id = cmd.val.unit
if unit_id > 0 then
@@ -398,16 +402,16 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local builds = {}
local unit = self.units[unit_id] ---@type reactor_unit
- builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPES.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPES.TURBINE_VALVE)
+ builds[unit_id] = unit.get_build(false, cmd.val.type == RTU_UNIT_TYPE.BOILER_VALVE, cmd.val.type == RTU_UNIT_TYPE.TURBINE_VALVE)
- _send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds })
+ _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
else
-- re-send facility RTU builds
-- retry logic will be kept as-is, so as long as no retry is needed, this will be a small update
self.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD
self.acks.fac_builds = false
- _send(SCADA_CRDN_TYPES.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPES.IMATRIX) })
+ _send(SCADA_CRDN_TYPE.FAC_BUILDS, { facility.get_build(cmd.val.type == RTU_UNIT_TYPE.IMATRIX) })
end
else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
@@ -441,7 +445,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
- _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
+ _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua
index 3d3270b..676263c 100644
--- a/supervisor/session/plc.lua
+++ b/supervisor/session/plc.lua
@@ -8,12 +8,11 @@ local svqtypes = require("supervisor.session.svqtypes")
local plc = {}
-local PROTOCOLS = comms.PROTOCOLS
-local RPLC_TYPES = comms.RPLC_TYPES
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
+local PROTOCOL = comms.PROTOCOL
+local RPLC_TYPE = comms.RPLC_TYPE
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
-
-local UNIT_COMMANDS = comms.UNIT_COMMANDS
+local UNIT_COMMAND = comms.UNIT_COMMAND
local print = util.print
local println = util.println
@@ -47,18 +46,16 @@ local PERIODICS = {
}
-- PLC supervisor session
+---@nodiscard
---@param id integer session ID
----@param for_reactor integer reactor ID
+---@param reactor_id integer reactor ID
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
-function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
+function plc.new_session(id, reactor_id, in_queue, out_queue, timeout)
local log_header = "plc_session(" .. id .. "): "
local self = {
- for_reactor = for_reactor,
- in_q = in_queue,
- out_q = out_queue,
commanded_state = false,
commanded_burn_rate = 0.0,
auto_cmd_token = 0,
@@ -244,34 +241,35 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- send an RPLC packet
- ---@param msg_type RPLC_TYPES
+ ---@param msg_type RPLC_TYPE
---@param msg table
local function _send(msg_type, msg)
local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet()
- r_pkt.make(for_reactor, msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable())
+ r_pkt.make(reactor_id, msg_type, msg)
+ s_pkt.make(self.seq_num, PROTOCOL.RPLC, r_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
- ---@param msg_type SCADA_MGMT_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- get an ACK status
+ ---@nodiscard
---@param pkt rplc_frame
---@return boolean|nil ack
local function _get_ack(pkt)
@@ -297,10 +295,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- process packet
- if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then
+ if pkt.scada_frame.protocol() == PROTOCOL.RPLC then
-- check reactor ID
- if pkt.id ~= for_reactor then
- log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id)
+ if pkt.id ~= reactor_id then
+ log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
return
end
@@ -308,7 +306,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
self.plc_conn_watchdog.feed()
-- handle packet by type
- if pkt.type == RPLC_TYPES.STATUS then
+ if pkt.type == RPLC_TYPE.STATUS then
-- status packet received, update data
if pkt.length >= 5 then
self.sDB.last_status_update = pkt.data[1]
@@ -335,14 +333,14 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "RPLC status packet length mismatch")
end
- elseif pkt.type == RPLC_TYPES.MEK_STRUCT then
+ elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
-- received reactor structure, record it
if pkt.length == 14 then
local status = pcall(_copy_struct, pkt.data)
if status then
-- copied in structure data OK
self.received_struct = true
- self.out_q.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, for_reactor)
+ out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
-- error copying structure data
log.error(log_header .. "failed to parse struct packet data")
@@ -350,7 +348,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "RPLC struct packet length mismatch")
end
- elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then
+ elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
-- burn rate acknowledgement
local ack = _get_ack(pkt)
if ack then
@@ -360,12 +358,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- send acknowledgement to coordinator
- self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
- unit = self.for_reactor,
- cmd = UNIT_COMMANDS.SET_BURN,
+ out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
+ unit = reactor_id,
+ cmd = UNIT_COMMAND.SET_BURN,
ack = ack
})
- elseif pkt.type == RPLC_TYPES.RPS_ENABLE then
+ elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
-- enable acknowledgement
local ack = _get_ack(pkt)
if ack then
@@ -375,12 +373,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- send acknowledgement to coordinator
- self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
- unit = self.for_reactor,
- cmd = UNIT_COMMANDS.START,
+ out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
+ unit = reactor_id,
+ cmd = UNIT_COMMAND.START,
ack = ack
})
- elseif pkt.type == RPLC_TYPES.RPS_SCRAM then
+ elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
-- manual SCRAM acknowledgement
local ack = _get_ack(pkt)
if ack then
@@ -391,12 +389,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- send acknowledgement to coordinator
- self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
- unit = self.for_reactor,
- cmd = UNIT_COMMANDS.SCRAM,
+ out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
+ unit = reactor_id,
+ cmd = UNIT_COMMAND.SCRAM,
ack = ack
})
- elseif pkt.type == RPLC_TYPES.RPS_ASCRAM then
+ elseif pkt.type == RPLC_TYPE.RPS_ASCRAM then
-- automatic SCRAM acknowledgement
local ack = _get_ack(pkt)
if ack then
@@ -405,7 +403,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
elseif ack == false then
log.debug(log_header .. " automatic SCRAM failed!")
end
- elseif pkt.type == RPLC_TYPES.RPS_STATUS then
+ elseif pkt.type == RPLC_TYPE.RPS_STATUS then
-- RPS status packet received, copy data
if pkt.length == 14 then
local status = pcall(_copy_rps_status, pkt.data)
@@ -418,7 +416,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "RPLC RPS status packet length mismatch")
end
- elseif pkt.type == RPLC_TYPES.RPS_ALARM then
+ elseif pkt.type == RPLC_TYPE.RPS_ALARM then
-- RPS alarm
if pkt.length == 13 then
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) })
@@ -431,7 +429,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
end
- elseif pkt.type == RPLC_TYPES.RPS_RESET then
+ elseif pkt.type == RPLC_TYPE.RPS_RESET then
-- RPS reset acknowledgement
local ack = _get_ack(pkt)
if ack then
@@ -443,18 +441,18 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- send acknowledgement to coordinator
- self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
- unit = self.for_reactor,
- cmd = UNIT_COMMANDS.RESET_RPS,
+ out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
+ unit = reactor_id,
+ cmd = UNIT_COMMAND.RESET_RPS,
ack = ack
})
- elseif pkt.type == RPLC_TYPES.RPS_AUTO_RESET then
+ elseif pkt.type == RPLC_TYPE.RPS_AUTO_RESET then
-- RPS auto control reset acknowledgement
local ack = _get_ack(pkt)
if not ack then
log.debug(log_header .. "RPS auto reset failed")
end
- elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then
+ elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
if pkt.length == 1 then
local ack = pkt.data[1]
@@ -473,8 +471,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
end
- elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
- if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
+ if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
@@ -491,7 +489,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
- elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session
_close()
else
@@ -503,17 +501,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- PUBLIC FUNCTIONS --
-- get the session ID
+ ---@nodiscard
function public.get_id() return id end
-- get the session database
+ ---@nodiscard
function public.get_db() return self.sDB end
-- check if ramping is completed by first verifying auto command token ack
+ ---@nodiscard
function public.is_ramp_complete()
return (self.sDB.auto_ack_token == self.auto_cmd_token) and (self.commanded_burn_rate == self.sDB.mek_status.act_burn_rate)
end
-- get the reactor structure
+ ---@nodiscard
+ ---@return mek_struct|table struct struct or empty table
function public.get_struct()
if self.received_struct then
return self.sDB.mek_struct
@@ -523,6 +526,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- get the reactor status
+ ---@nodiscard
+ ---@return mek_status|table struct status or empty table
function public.get_status()
if self.received_status_cache then
return self.sDB.mek_status
@@ -532,11 +537,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end
-- get the reactor RPS status
+ ---@nodiscard
function public.get_rps()
return self.sDB.rps_status
end
-- get the general status information
+ ---@nodiscard
function public.get_general_status()
return {
self.sDB.last_status_update,
@@ -564,10 +571,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
---@param ramp boolean true to ramp, false to not
function public.auto_set_burn(rate, ramp)
self.ramping_rate = ramp
- self.in_q.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate)
+ in_queue.push_data(PLC_S_DATA.AUTO_BURN_RATE, rate)
end
-- check if a timer matches this session's watchdog
+ ---@nodiscard
function public.check_wd(timer)
return self.plc_conn_watchdog.is_timer(timer) and self.connected
end
@@ -575,12 +583,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- close the connection
function public.close()
_close()
- _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
- println("connection to reactor " .. self.for_reactor .. " PLC closed by server")
+ _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
+ println("connection to reactor " .. reactor_id .. " PLC closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
+ ---@nodiscard
---@return boolean connected
function public.iterate()
if self.connected then
@@ -590,9 +599,9 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
local handle_start = util.time()
- while self.in_q.ready() and self.connected do
+ while in_queue.ready() and self.connected do
-- get a new message to process
- local message = self.in_q.pop()
+ local message = in_queue.pop()
if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET then
@@ -604,27 +613,27 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if cmd == PLC_S_CMDS.ENABLE then
-- enable reactor
if not self.auto_lock then
- _send(RPLC_TYPES.RPS_ENABLE, {})
+ _send(RPLC_TYPE.RPS_ENABLE, {})
end
elseif cmd == PLC_S_CMDS.SCRAM then
-- SCRAM reactor
self.acks.scram = false
self.retry_times.scram_req = util.time() + INITIAL_WAIT
- _send(RPLC_TYPES.RPS_SCRAM, {})
+ _send(RPLC_TYPE.RPS_SCRAM, {})
elseif cmd == PLC_S_CMDS.ASCRAM then
-- SCRAM reactor
self.acks.ascram = false
self.retry_times.ascram_req = util.time() + INITIAL_WAIT
- _send(RPLC_TYPES.RPS_ASCRAM, {})
+ _send(RPLC_TYPE.RPS_ASCRAM, {})
elseif cmd == PLC_S_CMDS.RPS_RESET then
-- reset RPS
self.acks.ascram = true
self.acks.rps_reset = false
self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT
- _send(RPLC_TYPES.RPS_RESET, {})
+ _send(RPLC_TYPE.RPS_RESET, {})
elseif cmd == PLC_S_CMDS.RPS_AUTO_RESET then
if self.sDB.rps_status.automatic or self.sDB.rps_status.timeout then
- _send(RPLC_TYPES.RPS_AUTO_RESET, {})
+ _send(RPLC_TYPE.RPS_AUTO_RESET, {})
end
else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
@@ -642,7 +651,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
self.ramping_rate = false
self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
- _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
+ _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
end
elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then
@@ -655,7 +664,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
self.ramping_rate = true
self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
- _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
+ _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
end
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
@@ -670,7 +679,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
self.acks.burn_rate = not self.ramping_rate
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
- _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
+ _send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
end
end
else
@@ -688,7 +697,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- exit if connection was closed
if not self.connected then
- println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host")
+ println("connection to reactor " .. reactor_id .. " PLC closed by remote host")
log.info(log_header .. "session closed by remote host")
return self.connected
end
@@ -705,7 +714,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
- _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
+ _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
@@ -722,7 +731,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.received_struct then
if rtimes.struct_req - util.time() <= 0 then
- _send(RPLC_TYPES.MEK_STRUCT, {})
+ _send(RPLC_TYPE.MEK_STRUCT, {})
rtimes.struct_req = util.time() + RETRY_PERIOD
end
end
@@ -731,7 +740,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.received_status_cache then
if rtimes.status_req - util.time() <= 0 then
- _send(RPLC_TYPES.MEK_STATUS, {})
+ _send(RPLC_TYPE.MEK_STATUS, {})
rtimes.status_req = util.time() + RETRY_PERIOD
end
end
@@ -742,13 +751,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if rtimes.burn_rate_req - util.time() <= 0 then
if self.auto_cmd_token > 0 then
if self.auto_lock then
- _send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
+ _send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })
else
-- would have been an auto command, but disengaged, so stop retrying
self.acks.burn_rate = true
end
elseif not self.auto_lock then
- _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
+ _send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
else
-- shouldn't be in this state, just pretend it was acknowledged
self.acks.burn_rate = true
@@ -763,7 +772,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.acks.scram then
if rtimes.scram_req - util.time() <= 0 then
- _send(RPLC_TYPES.RPS_SCRAM, {})
+ _send(RPLC_TYPE.RPS_SCRAM, {})
rtimes.scram_req = util.time() + RETRY_PERIOD
end
end
@@ -772,7 +781,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.acks.ascram then
if rtimes.ascram_req - util.time() <= 0 then
- _send(RPLC_TYPES.RPS_ASCRAM, {})
+ _send(RPLC_TYPE.RPS_ASCRAM, {})
rtimes.ascram_req = util.time() + RETRY_PERIOD
end
end
@@ -781,7 +790,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.acks.rps_reset then
if rtimes.rps_reset_req - util.time() <= 0 then
- _send(RPLC_TYPES.RPS_RESET, {})
+ _send(RPLC_TYPE.RPS_RESET, {})
rtimes.rps_reset_req = util.time() + RETRY_PERIOD
end
end
diff --git a/supervisor/session/rsctl.lua b/supervisor/session/rsctl.lua
index 1544cc3..fb17efe 100644
--- a/supervisor/session/rsctl.lua
+++ b/supervisor/session/rsctl.lua
@@ -5,6 +5,7 @@
local rsctl = {}
-- create a new redstone RTU I/O controller
+---@nodiscard
---@param redstone_rtus table redstone RTU sessions
function rsctl.new(redstone_rtus)
---@class rs_controller
diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua
index 1db89cb..9d178fe 100644
--- a/supervisor/session/rtu.lua
+++ b/supervisor/session/rtu.lua
@@ -1,7 +1,7 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
-local rsio = require("scada-common.rsio")
+local types = require("scada-common.types")
local util = require("scada-common.util")
local svqtypes = require("supervisor.session.svqtypes")
@@ -18,9 +18,9 @@ local svrs_turbinev = require("supervisor.session.rtu.turbinev")
local rtu = {}
-local PROTOCOLS = comms.PROTOCOLS
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local PROTOCOL = comms.PROTOCOL
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print
local println = util.println
@@ -32,6 +32,7 @@ local PERIODICS = {
}
-- create a new RTU session
+---@nodiscard
---@param id integer session ID
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
@@ -42,8 +43,6 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
local log_header = "rtu_session(" .. id .. "): "
local self = {
- in_q = in_queue,
- out_q = out_queue,
modbus_q = mqueue.new(),
advert = advertisement,
fac_units = facility.get_units(),
@@ -99,7 +98,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
advert_validator.assert_type_int(unit_advert.index)
advert_validator.assert_type_int(unit_advert.reactor)
- if u_type == RTU_UNIT_TYPES.REDSTONE then
+ if u_type == RTU_UNIT_TYPE.REDSTONE then
advert_validator.assert_type_table(unit_advert.rsio)
end
@@ -113,7 +112,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
end
local type_string = util.strval(u_type)
- if type(u_type) == "number" then type_string = util.strval(comms.advert_type_to_rtu_t(u_type)) end
+ if type(u_type) == "number" then type_string = types.rtu_type_to_string(u_type) end
-- create unit by type
@@ -124,19 +123,19 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit
- if u_type == RTU_UNIT_TYPES.REDSTONE then
+ if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end
- elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then
+ elseif u_type == RTU_UNIT_TYPE.BOILER_VALVE then
-- boiler (Mekanism 10.1+)
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end
- elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then
+ elseif u_type == RTU_UNIT_TYPE.TURBINE_VALVE then
-- turbine (Mekanism 10.1+)
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end
- elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
+ elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
-- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_envd(unit) end
@@ -144,21 +143,21 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string))
end
else
- if u_type == RTU_UNIT_TYPES.REDSTONE then
+ if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_redstone(unit) end
- elseif u_type == RTU_UNIT_TYPES.IMATRIX then
+ elseif u_type == RTU_UNIT_TYPE.IMATRIX then
-- induction matrix
unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_imatrix(unit) end
- elseif u_type == RTU_UNIT_TYPES.SPS then
+ elseif u_type == RTU_UNIT_TYPE.SPS then
-- super-critical phase shifter
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q)
- elseif u_type == RTU_UNIT_TYPES.SNA then
+ elseif u_type == RTU_UNIT_TYPE.SNA then
-- solar neutron activator
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q)
- elseif u_type == RTU_UNIT_TYPES.ENV_DETECTOR then
+ elseif u_type == RTU_UNIT_TYPE.ENV_DETECTOR then
-- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_envd(unit) end
@@ -194,23 +193,23 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
local function _send_modbus(m_pkt)
local s_pkt = comms.scada_packet()
- s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.MODBUS_TCP, m_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
-- send a SCADA management packet
- ---@param msg_type SCADA_MGMT_TYPES
+ ---@param msg_type SCADA_MGMT_TYPE
---@param msg table
local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg)
- s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ s_pkt.make(self.seq_num, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.out_q.push_packet(s_pkt)
+ out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1
end
@@ -231,15 +230,15 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
self.rtu_conn_watchdog.feed()
-- process packet
- if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then
+ if pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
if self.units[pkt.unit_id] ~= nil then
local unit = self.units[pkt.unit_id] ---@type unit_session
---@diagnostic disable-next-line: param-type-mismatch
unit.handle_packet(pkt)
end
- elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then
+ elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
-- handle management packet
- if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then
+ if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply
if pkt.length == 2 then
local srv_start = pkt.data[1]
@@ -256,20 +255,17 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
else
log.debug(log_header .. "SCADA keep alive packet length mismatch")
end
- elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then
+ elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session
_close()
- elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then
+ elseif pkt.type == SCADA_MGMT_TYPE.RTU_ADVERT then
-- RTU unit advertisement
log.debug(log_header .. "received updated advertisement")
-
- -- copy advertisement and remove version tag
self.advert = pkt.data
- table.remove(self.advert, 1)
-- handle advertisement; this will re-create all unit sub-sessions
_handle_advertisement()
- elseif pkt.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT then
+ elseif pkt.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT then
if pkt.length == 1 then
local unit_id = pkt.data[1]
if self.units[unit_id] ~= nil then
@@ -291,6 +287,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
function public.get_id() return id end
-- check if a timer matches this session's watchdog
+ ---@nodiscard
---@param timer number
function public.check_wd(timer)
return self.rtu_conn_watchdog.is_timer(timer) and self.connected
@@ -299,12 +296,13 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- close the connection
function public.close()
_close()
- _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {})
+ _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println(log_header .. "connection to RTU closed by server")
log.info(log_header .. "session closed by server")
end
-- iterate the session
+ ---@nodiscard
---@return boolean connected
function public.iterate()
if self.connected then
@@ -314,9 +312,9 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
local handle_start = util.time()
- while self.in_q.ready() and self.connected do
+ while in_queue.ready() and self.connected do
-- get a new message to process
- local msg = self.in_q.pop()
+ local msg = in_queue.pop()
if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET then
@@ -365,7 +363,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then
- _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() })
+ _send_mgmt(SCADA_MGMT_TYPE.KEEP_ALIVE, { util.time() })
periodics.keep_alive = 0
end
@@ -389,7 +387,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- instruction with body
local cmd = msg.message ---@type queue_data
if cmd.key == unit_session.RTU_US_DATA.BUILD_CHANGED then
- self.out_q.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val)
+ out_queue.push_data(svqtypes.SV_Q_DATA.RTU_BUILD_CHANGED, cmd.val)
end
end
end
diff --git a/supervisor/session/rtu/boilerv.lua b/supervisor/session/rtu/boilerv.lua
index 0cc4945..2f3c231 100644
--- a/supervisor/session/rtu/boilerv.lua
+++ b/supervisor/session/rtu/boilerv.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local boilerv = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
@@ -32,14 +31,15 @@ local PERIODICS = {
}
-- create a new boilerv rtu session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then
- log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
+ log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -239,6 +239,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/envd.lua b/supervisor/session/rtu/envd.lua
index bc65476..3b4b666 100644
--- a/supervisor/session/rtu/envd.lua
+++ b/supervisor/session/rtu/envd.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local envd = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
@@ -23,14 +22,15 @@ local PERIODICS = {
}
-- create a new environment detector rtu session runner
+---@nodiscard
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
function envd.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.ENV_DETECTOR then
- log.error("attempt to instantiate envd RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
+ log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -100,6 +100,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/imatrix.lua b/supervisor/session/rtu/imatrix.lua
index 607c22d..0b120b4 100644
--- a/supervisor/session/rtu/imatrix.lua
+++ b/supervisor/session/rtu/imatrix.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local imatrix = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
@@ -32,14 +31,15 @@ local PERIODICS = {
}
-- create a new imatrix rtu session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function imatrix.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.IMATRIX then
- log.error("attempt to instantiate imatrix RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
+ log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -213,6 +213,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua
index a286f9c..7c813a2 100644
--- a/supervisor/session/rtu/redstone.lua
+++ b/supervisor/session/rtu/redstone.lua
@@ -1,6 +1,4 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
-local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -9,7 +7,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local redstone = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local IO_PORT = rsio.IO
@@ -47,14 +45,15 @@ local PERIODICS = {
---@field req IO_LVL
-- create a new redstone rtu session runner
+---@nodiscard
---@param session_id integer
---@param unit_id integer
---@param advert rtu_advertisement
---@param out_queue mqueue
function redstone.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.REDSTONE then
- log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
+ log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -120,6 +119,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_dig_io
local io_f = {
+ ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
---@param active boolean
write = function (active) end
@@ -134,6 +134,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_dig_io
local io_f = {
+ ---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end,
---@param active boolean
write = function (active)
@@ -151,6 +152,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_ana_io
local io_f = {
+ ---@nodiscard
---@return integer
read = function () return self.phy_io.analog_in[port].phy end,
---@param value integer
@@ -166,6 +168,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_ana_io
local io_f = {
+ ---@nodiscard
---@return integer
read = function () return self.phy_io.analog_out[port].phy end,
---@param value integer
@@ -380,6 +383,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/sna.lua b/supervisor/session/rtu/sna.lua
index e2a667e..006222b 100644
--- a/supervisor/session/rtu/sna.lua
+++ b/supervisor/session/rtu/sna.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local sna = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
@@ -29,14 +28,15 @@ local PERIODICS = {
}
-- create a new sna rtu session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function sna.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.SNA then
- log.error("attempt to instantiate sna RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.SNA then
+ log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -176,6 +176,7 @@ function sna.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/sps.lua b/supervisor/session/rtu/sps.lua
index 9b07f3e..da036cd 100644
--- a/supervisor/session/rtu/sps.lua
+++ b/supervisor/session/rtu/sps.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
@@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local sps = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = {
@@ -32,14 +31,15 @@ local PERIODICS = {
}
-- create a new sps rtu session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function sps.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.SPS then
- log.error("attempt to instantiate sps RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.SPS then
+ log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -113,7 +113,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- query the tanks of the device
local function _request_tanks()
-- read input registers 11 through 19 (start = 11, count = 9)
- self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 })
+ self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 })
end
-- PUBLIC FUNCTIONS --
@@ -223,6 +223,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua
index 3f8357f..4cf32c4 100644
--- a/supervisor/session/rtu/turbinev.lua
+++ b/supervisor/session/rtu/turbinev.lua
@@ -1,4 +1,3 @@
-local comms = require("scada-common.comms")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
@@ -9,7 +8,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local turbinev = {}
-local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
+local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local DUMPING_MODE = types.DUMPING_MODE
local MODBUS_FCODE = types.MODBUS_FCODE
@@ -44,14 +43,15 @@ local PERIODICS = {
}
-- create a new turbinev rtu session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue
function turbinev.new(session_id, unit_id, advert, out_queue)
-- type check
- if advert.type ~= RTU_UNIT_TYPES.TURBINE_VALVE then
- log.error("attempt to instantiate turbinev RTU for type '" .. advert.type .. "'. this is a bug.")
+ if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
+ log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil
end
@@ -92,7 +92,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
flow_rate = 0,
prod_rate = 0,
steam_input_rate = 0,
- dumping_mode = DUMPING_MODE.IDLE ---@type DUMPING_MODE
+ dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode
},
tanks = {
last_update = 0,
@@ -123,7 +123,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
end
-- set the dumping mode
- ---@param mode DUMPING_MODE
+ ---@param mode dumping_mode
local function _set_dump_mode(mode)
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
@@ -310,6 +310,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
end
-- get the unit session database
+ ---@nodiscard
function public.get_db() return self.db end
return public
diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua
index a1f3b4b..9766bab 100644
--- a/supervisor/session/rtu/txnctrl.lua
+++ b/supervisor/session/rtu/txnctrl.lua
@@ -6,9 +6,10 @@ local util = require("scada-common.util")
local txnctrl = {}
-local TIMEOUT = 2000 -- 2000ms max wait
+local TIMEOUT = 2000 -- 2000ms max wait
-- create a new transaction controller
+---@nodiscard
function txnctrl.new()
local self = {
list = {},
@@ -22,16 +23,19 @@ function txnctrl.new()
local remove = table.remove
-- get the length of the transaction list
+ ---@nodiscard
function public.length()
return #self.list
end
-- check if there are no active transactions
+ ---@nodiscard
function public.empty()
return #self.list == 0
end
-- create a new transaction of the given type
+ ---@nodiscard
---@param txn_type integer
---@return integer txn_id
function public.create(txn_type)
@@ -49,6 +53,7 @@ function txnctrl.new()
end
-- mark a transaction as resolved to get its transaction type
+ ---@nodiscard
---@param txn_id integer
---@return integer txn_type
function public.resolve(txn_id)
diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua
index 27b21c0..700f9b1 100644
--- a/supervisor/session/rtu/unit_session.lua
+++ b/supervisor/session/rtu/unit_session.lua
@@ -8,7 +8,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl")
local unit_session = {}
-local PROTOCOLS = comms.PROTOCOLS
+local PROTOCOL = comms.PROTOCOL
local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE
@@ -23,6 +23,7 @@ unit_session.RTU_US_CMDS = RTU_US_CMDS
unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner
+---@nodiscard
---@param session_id integer RTU session ID
---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit
@@ -31,12 +32,8 @@ unit_session.RTU_US_DATA = RTU_US_DATA
---@param txn_tags table transaction log tags
function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags)
local self = {
- log_tag = log_tag,
- txn_tags = txn_tags,
- unit_id = unit_id,
device_index = advert.index,
reactor = advert.reactor,
- out_q = out_queue,
transaction_controller = txnctrl.new(),
connected = true,
device_fail = false
@@ -61,21 +58,22 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
local m_pkt = comms.modbus_packet()
local txn_id = self.transaction_controller.create(txn_type)
- m_pkt.make(txn_id, self.unit_id, f_code, register_param)
+ m_pkt.make(txn_id, unit_id, f_code, register_param)
- self.out_q.push_packet(m_pkt)
+ out_queue.push_packet(m_pkt)
return txn_id
end
-- try to resolve a MODBUS transaction
+ ---@nodiscard
---@param m_pkt modbus_frame MODBUS packet
---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID
function protected.try_resolve(m_pkt)
- if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then
- if m_pkt.unit_id == self.unit_id then
+ if m_pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
+ if m_pkt.unit_id == unit_id then
local txn_type = self.transaction_controller.resolve(m_pkt.txn_id)
- local txn_tag = " (" .. util.strval(self.txn_tags[txn_type]) .. ")"
+ local txn_tag = " (" .. util.strval(txn_tags[txn_type]) .. ")"
if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then
-- transaction incomplete or failed
@@ -135,26 +133,35 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
end
-- get the public interface
+ ---@nodiscard
function protected.get() return public end
-- PUBLIC FUNCTIONS --
-- get the unit ID
+ ---@nodiscard
function public.get_session_id() return session_id end
-- get the unit ID
- function public.get_unit_id() return self.unit_id end
+ ---@nodiscard
+ function public.get_unit_id() return unit_id end
-- get the device index
+ ---@nodiscard
function public.get_device_idx() return self.device_index end
-- get the reactor ID
+ ---@nodiscard
function public.get_reactor() return self.reactor end
-- get the command queue
+ ---@nodiscard
function public.get_cmd_queue() return protected.in_q end
-- close this unit
+ ---@nodiscard
function public.close() self.connected = false end
-- check if this unit is connected
+ ---@nodiscard
function public.is_connected() return self.connected end
-- check if this unit is faulted
+ ---@nodiscard
function public.is_faulted() return self.device_fail end
-- PUBLIC TEMPLATE FUNCTIONS --
@@ -179,6 +186,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
end
-- get the unit session database
+ ---@nodiscard
function public.get_db()
log.debug("template unit_session.get_db() called", true)
return {}
diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua
index b7c8ef5..76fb6d1 100644
--- a/supervisor/session/svsessions.lua
+++ b/supervisor/session/svsessions.lua
@@ -183,9 +183,10 @@ local function _free_closed(sessions)
end
-- find a session by remote port
+---@nodiscard
---@param list table
---@param port integer
----@return plc_session_struct|rtu_session_struct|nil
+---@return plc_session_struct|rtu_session_struct|coord_session_struct|nil
local function _find_session(list, port)
for i = 1, #list do
if list[i].r_port == port then return list[i] end
@@ -212,54 +213,63 @@ function svsessions.relink_modem(modem)
end
-- find an RTU session by the remote port
+---@nodiscard
---@param remote_port integer
---@return rtu_session_struct|nil
function svsessions.find_rtu_session(remote_port)
-- check RTU sessions
----@diagnostic disable-next-line: return-type-mismatch
- return _find_session(self.rtu_sessions, remote_port)
+ local session = _find_session(self.rtu_sessions, remote_port)
+ ---@cast session rtu_session_struct
+ return session
end
-- find a PLC session by the remote port
+---@nodiscard
---@param remote_port integer
---@return plc_session_struct|nil
function svsessions.find_plc_session(remote_port)
-- check PLC sessions
----@diagnostic disable-next-line: return-type-mismatch
- return _find_session(self.plc_sessions, remote_port)
+ local session = _find_session(self.plc_sessions, remote_port)
+ ---@cast session plc_session_struct
+ return session
end
-- find a PLC/RTU session by the remote port
+---@nodiscard
---@param remote_port integer
---@return plc_session_struct|rtu_session_struct|nil
function svsessions.find_device_session(remote_port)
-- check RTU sessions
- local s = _find_session(self.rtu_sessions, remote_port)
+ local session = _find_session(self.rtu_sessions, remote_port)
-- check PLC sessions
- if s == nil then s = _find_session(self.plc_sessions, remote_port) end
+ if session == nil then session = _find_session(self.plc_sessions, remote_port) end
+ ---@cast session plc_session_struct|rtu_session_struct|nil
- return s
+ return session
end
--- find a coordinator session by the remote port
---
+-- find a coordinator session by the remote port
-- only one coordinator is allowed, but this is kept to be consistent with all other session tables
+---@nodiscard
---@param remote_port integer
----@return nil
+---@return coord_session_struct|nil
function svsessions.find_coord_session(remote_port)
-- check coordinator sessions
----@diagnostic disable-next-line: return-type-mismatch
- return _find_session(self.coord_sessions, remote_port)
+ local session = _find_session(self.coord_sessions, remote_port)
+ ---@cast session coord_session_struct
+ return session
end
-- get the a coordinator session if exists
+---@nodiscard
---@return coord_session_struct|nil
function svsessions.get_coord_session()
return self.coord_sessions[1]
end
-- get a session by reactor ID
+---@nodiscard
---@param reactor integer
---@return plc_session_struct|nil session
function svsessions.get_reactor_session(reactor)
@@ -275,6 +285,7 @@ function svsessions.get_reactor_session(reactor)
end
-- establish a new PLC session
+---@nodiscard
---@param local_port integer
---@param remote_port integer
---@param for_reactor integer
@@ -314,6 +325,7 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
end
-- establish a new RTU session
+---@nodiscard
---@param local_port integer
---@param remote_port integer
---@param advertisement table
@@ -344,6 +356,7 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
end
-- establish a new coordinator session
+---@nodiscard
---@param local_port integer
---@param remote_port integer
---@param version string
diff --git a/supervisor/startup.lua b/supervisor/startup.lua
index 5cba7f7..0682994 100644
--- a/supervisor/startup.lua
+++ b/supervisor/startup.lua
@@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor")
-local SUPERVISOR_VERSION = "beta-v0.12.2"
+local SUPERVISOR_VERSION = "v0.13.1"
local print = util.print
local println = util.println
@@ -81,7 +81,7 @@ local function main()
local modem = ppm.get_wireless_modem()
if modem == nil then
- println("boot> wireless modem not found")
+ println("startup> wireless modem not found")
log.fatal("no wireless modem on startup")
return
end
@@ -110,7 +110,7 @@ local function main()
-- we only care if this is our wireless modem
if device == modem then
println_ts("wireless modem disconnected!")
- log.error("comms modem disconnected!")
+ log.warning("comms modem disconnected")
else
log.warning("non-comms modem disconnected")
end
@@ -127,9 +127,9 @@ local function main()
superv_comms.reconnect_modem(modem)
println_ts("wireless modem reconnected.")
- log.info("comms modem reconnected.")
+ log.info("comms modem reconnected")
else
- log.info("wired modem reconnected.")
+ log.info("wired modem reconnected")
end
end
end
diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua
index 5cadad4..848dfc3 100644
--- a/supervisor/supervisor.lua
+++ b/supervisor/supervisor.lua
@@ -6,10 +6,10 @@ local svsessions = require("supervisor.session.svsessions")
local supervisor = {}
-local PROTOCOLS = comms.PROTOCOLS
-local DEVICE_TYPES = comms.DEVICE_TYPES
+local PROTOCOL = comms.PROTOCOL
+local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK
-local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
+local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local print = util.print
local println = util.println
@@ -17,6 +17,7 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- supervisory controller communications
+---@nodiscard
---@param version string supervisor version
---@param num_reactors integer number of reactors
---@param cooling_conf table cooling configuration table
@@ -26,32 +27,24 @@ local println_ts = util.println_ts
---@param range integer trusted device connection range
function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range)
local self = {
- version = version,
- num_reactors = num_reactors,
- modem = modem,
- dev_listen = dev_listen,
- coord_listen = coord_listen,
- reactor_struct_cache = nil
+ last_est_acks = {}
}
- ---@class superv_comms
- local public = {}
-
comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS --
-- configure modem channels
local function _conf_channels()
- self.modem.closeAll()
- self.modem.open(self.dev_listen)
- self.modem.open(self.coord_listen)
+ modem.closeAll()
+ modem.open(dev_listen)
+ modem.open(coord_listen)
end
_conf_channels()
-- link modem to svsessions
- svsessions.init(self.modem, num_reactors, cooling_conf)
+ svsessions.init(modem, num_reactors, cooling_conf)
-- send an establish request response to a PLC/RTU
---@param dest integer
@@ -60,10 +53,10 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet()
- m_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg)
- s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable())
+ m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
+ s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable())
- self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable())
+ modem.transmit(dest, dev_listen, s_pkt.raw_sendable())
end
-- send coordinator connection establish response
@@ -74,24 +67,27 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local s_pkt = comms.scada_packet()
local c_pkt = comms.mgmt_packet()
- c_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg)
- s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, c_pkt.raw_sendable())
+ c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
+ s_pkt.make(seq_id, PROTOCOL.SCADA_MGMT, c_pkt.raw_sendable())
- self.modem.transmit(dest, self.coord_listen, s_pkt.raw_sendable())
+ modem.transmit(dest, coord_listen, s_pkt.raw_sendable())
end
-- PUBLIC FUNCTIONS --
+ ---@class superv_comms
+ local public = {}
+
-- reconnect a newly connected modem
- ---@param modem table
----@diagnostic disable-next-line: redefined-local
- function public.reconnect_modem(modem)
- self.modem = modem
- svsessions.relink_modem(self.modem)
+ ---@param new_modem table
+ function public.reconnect_modem(new_modem)
+ modem = new_modem
+ svsessions.relink_modem(new_modem)
_conf_channels()
end
-- parse a packet
+ ---@nodiscard
---@param side string
---@param sender integer
---@param reply_to integer
@@ -107,25 +103,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
if s_pkt.is_valid() then
-- get as MODBUS TCP packet
- if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then
+ if s_pkt.protocol() == PROTOCOL.MODBUS_TCP then
local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then
pkt = m_pkt.get()
end
-- get as RPLC packet
- elseif s_pkt.protocol() == PROTOCOLS.RPLC then
+ elseif s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get()
end
-- get as SCADA management packet
- elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then
+ elseif s_pkt.protocol() == PROTOCOL.SCADA_MGMT then
local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get()
end
-- get as coordinator packet
- elseif s_pkt.protocol() == PROTOCOLS.SCADA_CRDN then
+ elseif s_pkt.protocol() == PROTOCOL.SCADA_CRDN then
local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get()
@@ -147,8 +143,9 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local protocol = packet.scada_frame.protocol()
-- device (RTU/PLC) listening channel
- if l_port == self.dev_listen then
- if protocol == PROTOCOLS.MODBUS_TCP then
+ if l_port == dev_listen then
+ if protocol == PROTOCOL.MODBUS_TCP then
+ ---@cast packet modbus_frame
-- look for an associated session
local session = svsessions.find_rtu_session(r_port)
@@ -160,7 +157,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
-- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session")
end
- elseif protocol == PROTOCOLS.RPLC then
+ elseif protocol == PROTOCOL.RPLC then
+ ---@cast packet rplc_frame
-- look for an associated session
local session = svsessions.find_plc_session(r_port)
@@ -173,7 +171,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
log.debug("PLC_ESTABLISH: no session but not an establish, forcing relink")
_send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY })
end
- elseif protocol == PROTOCOLS.SCADA_MGMT then
+ elseif protocol == PROTOCOL.SCADA_MGMT then
+ ---@cast packet mgmt_frame
-- look for an associated session
local session = svsessions.find_device_session(r_port)
@@ -181,7 +180,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
- elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1
@@ -192,13 +191,13 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local dev_type = packet.data[3]
if comms_v ~= comms.version then
- log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v,
- " (expected v", comms.version, ")"))
- _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
- return
- end
+ if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
+ log.info(util.c("dropping device establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
+ self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
+ end
- if dev_type == DEVICE_TYPES.PLC then
+ _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
+ elseif dev_type == DEVICE_TYPE.PLC then
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
@@ -206,19 +205,25 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
if plc_id == false then
-- reactor already has a PLC assigned
- log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
+ if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
+ log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
+ self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
+ end
+
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [:", r_port, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
+
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
+ self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
end
- elseif dev_type == DEVICE_TYPES.RTU then
+ elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then
-- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4]
@@ -226,6 +231,7 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
println(util.c("RTU (", firmware_v, ") [:", r_port, "] \xbb connected"))
log.info(util.c("RTU_ESTABLISH: RTU (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
+
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
else
log.debug("RTU_ESTABLISH: packet length mismatch")
@@ -247,16 +253,17 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
log.debug("illegal packet type " .. protocol .. " on device listening channel")
end
-- coordinator listening channel
- elseif l_port == self.coord_listen then
+ elseif l_port == coord_listen then
-- look for an associated session
local session = svsessions.find_coord_session(r_port)
- if protocol == PROTOCOLS.SCADA_MGMT then
+ if protocol == PROTOCOL.SCADA_MGMT then
+ ---@cast packet mgmt_frame
-- SCADA management packet
if session ~= nil then
-- pass the packet onto the session handler
session.in_queue.push_packet(packet)
- elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then
+ elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1
@@ -267,32 +274,39 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local dev_type = packet.data[3]
if comms_v ~= comms.version then
- log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v,
- " (expected v", comms.version, ")"))
- _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
- return
- elseif dev_type ~= DEVICE_TYPES.CRDN then
- log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel"))
- _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
- return
- end
-
- -- this is an attempt to establish a new session
- local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
-
- if s_id ~= false then
- local config = { self.num_reactors }
- for i = 1, #cooling_conf do
- table.insert(config, cooling_conf[i].BOILERS)
- table.insert(config, cooling_conf[i].TURBINES)
+ if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
+ log.info(util.c("dropping coordinator establish packet with incorrect comms version v", comms_v, " (expected v", comms.version, ")"))
+ self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
end
- println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected"))
- log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
- _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
+ _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.BAD_VERSION })
+ elseif dev_type ~= DEVICE_TYPE.CRDN then
+ log.debug(util.c("illegal establish packet for device ", dev_type, " on CRDN listening channel"))
+ _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
else
- log.debug("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
- _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
+ -- this is an attempt to establish a new session
+ local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
+
+ if s_id ~= false then
+ local config = { num_reactors }
+ for i = 1, #cooling_conf do
+ table.insert(config, cooling_conf[i].BOILERS)
+ table.insert(config, cooling_conf[i].TURBINES)
+ end
+
+ println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected"))
+ log.info(util.c("CRDN_ESTABLISH: coordinator (",firmware_v, ") [:", r_port, "] connected with session ID ", s_id))
+
+ _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW, config })
+ self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
+ else
+ if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
+ log.info("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator")
+ self.last_est_acks[r_port] = ESTABLISH_ACK.COLLISION
+ end
+
+ _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
+ end
end
else
log.debug("CRDN_ESTABLISH: establish packet length mismatch")
@@ -302,7 +316,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
-- any other packet should be session related, discard it
log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session")
end
- elseif protocol == PROTOCOLS.SCADA_CRDN then
+ elseif protocol == PROTOCOL.SCADA_CRDN then
+ ---@cast packet crdn_frame
-- coordinator packet
if session ~= nil then
-- pass the packet onto the session handler
diff --git a/supervisor/unit.lua b/supervisor/unit.lua
index 9b0849c..c3f9482 100644
--- a/supervisor/unit.lua
+++ b/supervisor/unit.lua
@@ -11,21 +11,16 @@ local rsctl = require("supervisor.session.rsctl")
---@class reactor_control_unit
local unit = {}
-local WASTE_MODE = types.WASTE_MODE
-
-local ALARM = types.ALARM
-local PRIO = types.ALARM_PRIORITY
-local ALARM_STATE = types.ALARM_STATE
-
-local TRI_FAIL = types.TRI_FAIL
-local DUMPING_MODE = types.DUMPING_MODE
+local WASTE_MODE = types.WASTE_MODE
+local ALARM = types.ALARM
+local PRIO = types.ALARM_PRIORITY
+local ALARM_STATE = types.ALARM_STATE
+local TRI_FAIL = types.TRI_FAIL
local PLC_S_CMDS = plc.PLC_S_CMDS
local IO = rsio.IO
-local FLOW_STABILITY_DELAY_MS = 15000
-
local DT_KEYS = {
ReactorBurnR = "RBR",
ReactorTemp = "RTP",
@@ -41,18 +36,16 @@ local DT_KEYS = {
TurbinePower = "TPR"
}
----@alias ALARM_INT_STATE integer
+---@enum ALARM_INT_STATE
local AISTATE = {
- INACTIVE = 0,
- TRIPPING = 1,
- TRIPPED = 2,
- ACKED = 3,
- RING_BACK = 4,
- RING_BACK_TRIPPING = 5
+ INACTIVE = 1,
+ TRIPPING = 2,
+ TRIPPED = 3,
+ ACKED = 4,
+ RING_BACK = 5,
+ RING_BACK_TRIPPING = 6
}
-unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS
-
---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped
@@ -61,19 +54,19 @@ unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS
---@field tier integer alarm urgency tier (0 = highest)
-- create a new reactor unit
----@param for_reactor integer reactor unit number
+---@nodiscard
+---@param reactor_id integer reactor unit number
---@param num_boilers integer number of boilers expected
---@param num_turbines integer number of turbines expected
-function unit.new(for_reactor, num_boilers, num_turbines)
+function unit.new(reactor_id, num_boilers, num_turbines)
---@class _unit_self
local self = {
- r_id = for_reactor,
+ r_id = reactor_id,
plc_s = nil, ---@class plc_session_struct
plc_i = nil, ---@class plc_session
num_boilers = num_boilers,
num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
- defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS },
-- rtus
redstone = {},
boilers = {},
@@ -278,6 +271,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
local function _reset_dt(key) self.deltas[key] = nil end
-- get the delta t of a value
+ ---@nodiscard
---@param key string value key
---@return number value value or 0 if not known
function self._get_dt(key) if self.deltas[key] then return self.deltas[key].dt else return 0.0 end end
@@ -326,7 +320,6 @@ function unit.new(for_reactor, num_boilers, num_turbines)
--#region redstone I/O
local __rs_w = self.io_ctl.digital_write
- local __rs_r = self.io_ctl.digital_read
-- valves
local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end }
@@ -525,9 +518,9 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
end
- -- get the actual limit of this unit
- --
+ -- get the actual limit of this unit
-- if it is degraded or not ready, the limit will be 0
+ ---@nodiscard
---@return integer lim_br100
function public.a_get_effective_limit()
if not self.db.control.ready or self.db.control.degraded or self.plc_cache.rps_trip then
@@ -551,6 +544,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
-- check if ramping is complete (burn rate is same as target)
+ ---@nodiscard
---@return boolean complete
function public.a_ramp_complete()
if self.plc_i ~= nil then
@@ -610,7 +604,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- acknowledge an alarm (if possible)
---@param id ALARM alarm ID
function public.ack_alarm(id)
- if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.TRIPPED) then
+ if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.TRIPPED then
self.db.alarm_states[id] = ALARM_STATE.ACKED
end
end
@@ -618,7 +612,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- reset an alarm (if possible)
---@param id ALARM alarm ID
function public.reset_alarm(id)
- if (type(id) == "number") and (self.db.alarm_states[id] == ALARM_STATE.RING_BACK) then
+ if type(id) == "number" and self.db.alarm_states[id] == ALARM_STATE.RING_BACK then
self.db.alarm_states[id] = ALARM_STATE.INACTIVE
end
end
@@ -675,6 +669,8 @@ function unit.new(for_reactor, num_boilers, num_turbines)
--#region
-- check if a critical alarm is tripped
+ ---@nodiscard
+ ---@return boolean tripped
function public.has_critical_alarm()
for _, alarm in pairs(self.alarms) do
if alarm.tier == PRIO.CRITICAL and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then
@@ -686,6 +682,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
-- get build properties of all machines
+ ---@nodiscard
---@param inc_plc boolean? true/nil to include PLC build, false to exclude
---@param inc_boilers boolean? true/nil to include boiler builds, false to exclude
---@param inc_turbines boolean? true/nil to include turbine builds, false to exclude
@@ -718,6 +715,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
-- get reactor status
+ ---@nodiscard
function public.get_reactor_status()
local status = {}
if self.plc_i ~= nil then
@@ -728,6 +726,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
-- get RTU statuses
+ ---@nodiscard
function public.get_rtu_statuses()
local status = {}
@@ -769,20 +768,25 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end
-- get the annunciator status
+ ---@nodiscard
function public.get_annunciator() return self.db.annunciator end
-- get the alarm states
+ ---@nodiscard
function public.get_alarms() return self.db.alarm_states end
-- get information required for automatic reactor control
+ ---@nodiscard
function public.get_control_inf() return self.db.control end
-- get unit state
+ ---@nodiscard
function public.get_state()
return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded }
end
-- get the reactor ID
+ ---@nodiscard
function public.get_id() return self.r_id end
--#endregion
diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua
index 836dad2..3a55c82 100644
--- a/supervisor/unitlogic.lua
+++ b/supervisor/unitlogic.lua
@@ -1,3 +1,4 @@
+local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
@@ -5,17 +6,16 @@ local util = require("scada-common.util")
local plc = require("supervisor.session.plc")
-local PRIO = types.ALARM_PRIORITY
-local ALARM_STATE = types.ALARM_STATE
-
-local TRI_FAIL = types.TRI_FAIL
+local TRI_FAIL = types.TRI_FAIL
local DUMPING_MODE = types.DUMPING_MODE
+local PRIO = types.ALARM_PRIORITY
+local ALARM_STATE = types.ALARM_STATE
local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS
-local aistate_string = {
+local AISTATE_NAMES = {
"INACTIVE",
"TRIPPING",
"TRIPPED",
@@ -24,11 +24,10 @@ local aistate_string = {
"RING_BACK_TRIPPING"
}
--- background radiation 0.0000001 Sv/h (99.99 nSv/h)
--- "green tint" radiation 0.00001 Sv/h (10 uSv/h)
--- damaging radiation 0.00006 Sv/h (60 uSv/h)
-local RADIATION_ALERT_LEVEL = 0.00001 -- 10 uSv/h
-local RADIATION_ALARM_LEVEL = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
+local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
+
+local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
+local ALARM_LIMS = const.ALARM_LIMITS
---@class unit_logic_extension
local logic = {}
@@ -108,15 +107,15 @@ function logic.update_annunciator(self)
-- update other annunciator fields
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped
- self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.manual
- self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.automatic
+ self.db.annunciator.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
+ self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.AUTOMATIC
self.db.annunciator.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.no_cool)
- self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < -2.0
- self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4
- self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000
- self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100
- self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= 0.01
- self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= 0.85
+ self.db.annunciator.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < ANNUNC_LIMS.RCSFlowLow
+ self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
+ self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
+ self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
+ self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
+ self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
-- this warning applies when no coolant is buffered (which we can't easily determine without running)
--[[
@@ -129,7 +128,7 @@ function logic.update_annunciator(self)
such as when a burn rate consumes half the coolant in the tank, meaning that:
50% at some point will be in the boiler, and 50% in a tube, so that leaves 0% in the reactor
]]--
- local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.fluid.sodium, 200000, 20000)
+ local heating_rate_conv = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, 200000, 20000)
local high_rate = (plc_db.mek_status.ccool_amnt / (plc_db.mek_status.burn_rate * heating_rate_conv)) < 4
self.db.annunciator.HighStartupRate = not plc_db.mek_status.status and high_rate
@@ -150,7 +149,7 @@ function logic.update_annunciator(self)
for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3)
- self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > RADIATION_ALERT_LEVEL
+ self.db.annunciator.RadiationWarning = envd.get_db().radiation_raw > ANNUNC_LIMS.RadiationWarning
break
end
@@ -299,7 +298,7 @@ function logic.update_annunciator(self)
self.db.annunciator.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate)
-- check for steam feed mismatch and max return rate
- local sfmismatch = math.abs(total_flow_rate - total_input_rate) > 10
+ local sfmismatch = math.abs(total_flow_rate - total_input_rate) > ANNUNC_LIMS.SteamFeedMismatch
sfmismatch = sfmismatch or boiler_steam_dt_sum > 2.0 or boiler_water_dt_sum < -2.0
self.db.annunciator.SteamFeedMismatch = sfmismatch
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0
@@ -367,8 +366,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],"]"))
end
else
alarm.trip_time = util.time_ms()
@@ -381,8 +380,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],"]"))
end
elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0
@@ -431,8 +430,8 @@ 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))
+ local change_str = util.c(AISTATE_NAMES[int_state], " -> ", AISTATE_NAMES[alarm.state])
+ log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
end
end
@@ -449,7 +448,7 @@ function logic.update_alarms(self)
-- Containment Radiation
local rad_alarm = false
for i = 1, #self.envd do
- rad_alarm = self.envd[i].get_db().radiation_raw > RADIATION_ALARM_LEVEL
+ rad_alarm = self.envd[i].get_db().radiation_raw > ALARM_LIMS.HIGH_RADIATION
break
end
_update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation)
@@ -469,14 +468,14 @@ function logic.update_alarms(self)
_update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp)
-- High Temperature
- _update_alarm_state(self, plc_cache.temp > 1150, self.alarms.ReactorHighTemp)
+ _update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp)
-- Waste Leak
- _update_alarm_state(self, plc_cache.waste >= 0.99, self.alarms.ReactorWasteLeak)
+ _update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak)
-- High Waste
local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_waste
- _update_alarm_state(self, (plc_cache.waste > 0.50) or rps_high_waste, self.alarms.ReactorHighWaste)
+ _update_alarm_state(self, (plc_cache.waste > ALARM_LIMS.HIGH_WASTE) or rps_high_waste, self.alarms.ReactorHighWaste)
-- RPS Transient (excludes timeouts and manual trips)
local rps_alarm = false
@@ -501,7 +500,7 @@ function logic.update_alarms(self)
-- annunciator indicators for these states may not indicate a real issue when:
-- > flow is ramping up right after reactor start
-- > flow is ramping down after reactor shutdown
- if ((util.time_ms() - self.last_rate_change_ms) > self.defs.FLOW_STABILITY_DELAY_MS) and plc_cache.active then
+ if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then
rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch
end
@@ -530,8 +529,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],"]"))
end
alarmed = true
@@ -555,6 +554,7 @@ function logic.update_status_text(self)
local AISTATE = self.types.AISTATE
-- check if an alarm is active (tripped or ack'd)
+ ---@nodiscard
---@param alarm table alarm entry
---@return boolean active
local function is_active(alarm)
@@ -620,7 +620,7 @@ function logic.update_status_text(self)
self.status_text[2] = "insufficient fuel input rate"
elseif self.db.annunciator.WasteLineOcclusion then
self.status_text[2] = "insufficient waste output rate"
- elseif (util.time_ms() - self.last_rate_change_ms) <= self.defs.FLOW_STABILITY_DELAY_MS then
+ elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then
self.status_text[2] = "awaiting flow stability"
else
self.status_text[2] = "system nominal"
@@ -636,9 +636,9 @@ function logic.update_status_text(self)
cause = "core temperature high"
elseif plc_db.rps_trip_cause == "no_coolant" then
cause = "insufficient coolant"
- elseif plc_db.rps_trip_cause == "full_waste" then
+ elseif plc_db.rps_trip_cause == "ex_waste" then
cause = "excess waste"
- elseif plc_db.rps_trip_cause == "heated_coolant_backup" then
+ elseif plc_db.rps_trip_cause == "ex_heated_coolant" then
cause = "excess heated coolant"
elseif plc_db.rps_trip_cause == "no_fuel" then
cause = "insufficient fuel"
@@ -670,7 +670,7 @@ function logic.update_status_text(self)
end
end
else
- self.status_text = { "Reactor Off-line", "awaiting connection..." }
+ self.status_text = { "REACTOR OFF-LINE", "awaiting connection..." }
end
end
@@ -680,6 +680,7 @@ function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE
-- check if an alarm is active (tripped or ack'd)
+ ---@nodiscard
---@param alarm table alarm entry
---@return boolean active
local function is_active(alarm)