Merge pull request #175 from MikaylaFischler/118-code-cleanup-pass

#118 Code Cleanup
This commit is contained in:
Mikayla 2023-02-25 12:08:06 -05:00 committed by GitHub
commit 6eee0d0c72
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
77 changed files with 1946 additions and 1683 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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")

View File

@ -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))

View File

@ -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)}

View File

@ -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

View File

@ -1,5 +1,5 @@
--
-- Reactor Unit SCADA Coordinator GUI
-- Reactor Unit Waiting Spinner
--
local style = require("coordinator.ui.style")

View File

@ -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

View File

@ -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

View File

@ -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<br>
-- 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

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -21,8 +21,7 @@ local active = false
local registry = { {}, {}, {} } -- one registry table per period
local callback_counter = 0
-- blink registered indicators
--
-- blink registered indicators<br>
-- 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<br>
-- 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)

View File

@ -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
}
}

View File

@ -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<br>
-- identifies dangerous states and SCRAMs reactor if warranted<br>
-- 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

View File

@ -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<br>
--- 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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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,

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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)<br>
-- 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)<br>
-- 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)<br>
-- 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)<br>
-- 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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 <br/>
-- configure the maximum allowable message receive distance<br>
-- 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<br>
-- 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

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,14 @@ local types = {}
---@field amount integer
-- create a new tank fluid
---@nodiscard
---@param n string name
---@param a integer amount
---@return radiation_reading
function types.new_tank_fluid(n, a) return { name = n, amount = a } end
-- create a new empty tank fluid
---@nodiscard
---@return tank_fluid
function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 } end
@ -26,12 +28,14 @@ function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0
---@field unit string
-- create a new radiation reading
---@nodiscard
---@param r number radiaiton level
---@param u string radiation unit
---@return radiation_reading
function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end
-- create a new zeroed radiation reading
---@nodiscard
---@return radiation_reading
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end
@ -41,6 +45,7 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
---@field z integer
-- create a new coordinate
---@nodiscard
---@param x integer
---@param y integer
---@param z integer
@ -48,11 +53,12 @@ function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv"
function types.new_coordinate(x, y, z) return { x = x, y = y, z = z } end
-- create a new zero coordinate
---@nodiscard
---@return coordinate
function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@class rtu_advertisement
---@field type integer
---@field type RTU_UNIT_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

View File

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

View File

@ -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<br/>
-- 7.14 kJ per blade for 1 mB of fissile fuel<br>
-- 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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 {}

View File

@ -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<br>
-- 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

View File

@ -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

View File

@ -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

View File

@ -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<br>
-- 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

View File

@ -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)