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) function apisessions.handle_packet(packet)
end end
function apisessions.check_all_watchdogs() -- attempt to identify which session's watchdog timer fired
end ---@param timer_event number
function apisessions.check_all_watchdogs(timer_event)
function apisessions.close_all()
end end
-- delete all closed sessions
function apisessions.free_all_closed() function apisessions.free_all_closed()
end end
-- close all open connections
function apisessions.close_all()
end
return apisessions return apisessions

View File

@ -14,17 +14,18 @@ local println = util.println
local print_ts = util.print_ts local print_ts = util.print_ts
local println_ts = util.println_ts local println_ts = util.println_ts
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPES = comms.DEVICE_TYPES local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMANDS = comms.UNIT_COMMANDS local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMANDS = comms.FAC_COMMANDS local FAC_COMMAND = comms.FAC_COMMAND
local coordinator = {} local coordinator = {}
-- request the user to select a monitor -- request the user to select a monitor
---@nodiscard
---@param names table available monitors ---@param names table available monitors
---@return boolean|string|nil ---@return boolean|string|nil
local function ask_monitor(names) local function ask_monitor(names)
@ -64,9 +65,11 @@ function coordinator.configure_monitors(num_units)
end end
-- we need a certain number of monitors (1 per unit + 1 primary display) -- we need a certain number of monitors (1 per unit + 1 primary display)
if #names < num_units + 1 then local num_displays_needed = num_units + 1
println("not enough monitors connected (need " .. num_units + 1 .. ")") if #names < num_displays_needed then
log.warning("insufficient monitors present (need " .. num_units + 1 .. ")") local message = "not enough monitors connected (need " .. num_displays_needed .. ")"
println(message)
log.warning(message)
return false return false
end end
@ -125,7 +128,6 @@ function coordinator.configure_monitors(num_units)
else else
-- make sure all displays are connected -- make sure all displays are connected
for i = 1, num_units do for i = 1, num_units do
---@diagnostic disable-next-line: need-check-nil
local display = unit_displays[i] local display = unit_displays[i]
if not util.table_contains(names, display) then 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_boot(message) log_dmesg(message, "BOOT") end
function coordinator.log_comms(message) log_dmesg(message, "COMMS") 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 ---@param message string
---@return function update, function done ---@return function update, function done
function coordinator.log_comms_connecting(message) function coordinator.log_comms_connecting(message)
---@diagnostic disable-next-line: return-type-mismatch local update, done = log_dmesg(message, "COMMS", true)
return log_dmesg(message, "COMMS", true) ---@cast update function
---@cast done function
return update, done
end end
-- coordinator communications -- coordinator communications
---@nodiscard
---@param version string coordinator version ---@param version string coordinator version
---@param modem table modem device ---@param modem table modem device
---@param sv_port integer port of configured supervisor ---@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_linked = false,
sv_seq_num = 0, sv_seq_num = 0,
sv_r_seq_num = nil, sv_r_seq_num = nil,
modem = modem,
connected = false, connected = false,
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
} }
---@class coord_comms
local public = {}
comms.set_trusted_range(range) comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
self.modem.closeAll() modem.closeAll()
self.modem.open(sv_listen) modem.open(sv_listen)
self.modem.open(api_listen) modem.open(api_listen)
end end
_conf_channels() _conf_channels()
-- send a packet to the supervisor -- 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 ---@param msg table
local function _send_sv(protocol, msg_type, msg) local function _send_sv(protocol, msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local pkt = nil ---@type mgmt_packet|crdn_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() pkt = comms.mgmt_packet()
elseif protocol == PROTOCOLS.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
pkt = comms.crdn_packet() pkt = comms.crdn_packet()
else else
return return
@ -242,28 +245,30 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
pkt.make(msg_type, msg) pkt.make(msg_type, msg)
s_pkt.make(self.sv_seq_num, protocol, pkt.raw_sendable()) 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 self.sv_seq_num = self.sv_seq_num + 1
end end
-- attempt connection establishment -- attempt connection establishment
local function _send_establish() 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 end
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) 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 end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class coord_comms
local public = {}
-- reconnect a newly connected modem -- reconnect a newly connected modem
---@param modem table ---@param new_modem table
---@diagnostic disable-next-line: redefined-local function public.reconnect_modem(new_modem)
function public.reconnect_modem(modem) modem = new_modem
self.modem = modem
_conf_channels() _conf_channels()
end end
@ -271,10 +276,11 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
function public.close() function public.close()
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_linked = false self.sv_linked = false
_send_sv(PROTOCOLS.SCADA_MGMT, SCADA_MGMT_TYPES.CLOSE, {}) _send_sv(PROTOCOL.SCADA_MGMT, SCADA_MGMT_TYPE.CLOSE, {})
end end
-- attempt to connect to the subervisor -- attempt to connect to the subervisor
---@nodiscard
---@param timeout_s number timeout in seconds ---@param timeout_s number timeout in seconds
---@param tick_dmesg_waiting function callback to tick dmesg waiting ---@param tick_dmesg_waiting function callback to tick dmesg waiting
---@param task_done function callback to show done on dmesg ---@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 elseif event == "modem_message" then
-- handle message -- handle message
local packet = public.parse_packet(p1, p2, p3, p4, p5) 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) public.handle_packet(packet)
end end
elseif event == "terminate" then elseif event == "terminate" then
@ -329,25 +335,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
end end
-- send a facility command -- send a facility command
---@param cmd FAC_COMMANDS command ---@param cmd FAC_COMMAND command
function public.send_fac_command(cmd) 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 end
-- send the auto process control configuration with a start command -- send the auto process control configuration with a start command
---@param config coord_auto_config configuration ---@param config coord_auto_config configuration
function public.send_auto_start(config) function public.send_auto_start(config)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { _send_sv(PROTOCOL.SCADA_CRDN, SCADA_CRDN_TYPE.FAC_CMD, {
FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits FAC_COMMAND.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
}) })
end end
-- send a unit command -- send a unit command
---@param cmd UNIT_COMMANDS command ---@param cmd UNIT_COMMAND command
---@param unit integer unit ID ---@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?) ---@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) 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 end
-- parse a packet -- 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 if s_pkt.is_valid() then
-- get as SCADA management packet -- 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() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() pkt = mgmt_pkt.get()
end end
-- get as coordinator packet -- 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() local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get() pkt = crdn_pkt.get()
end end
-- get as coordinator API packet -- 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() local capi_pkt = comms.capi_packet()
if capi_pkt.decode(s_pkt) then if capi_pkt.decode(s_pkt) then
pkt = capi_pkt.get() 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() local l_port = packet.scada_frame.local_port()
if l_port == api_listen then if l_port == api_listen then
if protocol == PROTOCOLS.COORD_API then if protocol == PROTOCOL.COORD_API then
---@diagnostic disable-next-line: param-type-mismatch ---@cast packet capi_frame
apisessions.handle_packet(packet) apisessions.handle_packet(packet)
else else
log.debug("illegal packet type " .. protocol .. " on api listening channel", true) 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() sv_watchdog.feed()
-- handle packet -- handle packet
if protocol == PROTOCOLS.SCADA_CRDN then if protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
if self.sv_linked then 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 if packet.length == 2 then
-- record builds -- record builds
local fac_builds = iocontrol.record_facility_builds(packet.data[1]) 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 if fac_builds and unit_builds then
-- acknowledge receipt of builds -- 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 else
log.error("received invalid INITIAL_BUILDS packet") log.debug("received invalid INITIAL_BUILDS packet")
end end
else else
log.debug("INITIAL_BUILDS packet length mismatch") log.debug("INITIAL_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPES.FAC_BUILDS then elseif packet.type == SCADA_CRDN_TYPE.FAC_BUILDS then
if packet.length == 1 then if packet.length == 1 then
-- record facility builds -- record facility builds
if iocontrol.record_facility_builds(packet.data[1]) then if iocontrol.record_facility_builds(packet.data[1]) then
-- acknowledge receipt of builds -- 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 else
log.error("received invalid FAC_BUILDS packet") log.debug("received invalid FAC_BUILDS packet")
end end
else else
log.debug("FAC_BUILDS packet length mismatch") log.debug("FAC_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPES.FAC_STATUS then elseif packet.type == SCADA_CRDN_TYPE.FAC_STATUS then
-- update facility status -- update facility status
if not iocontrol.update_facility_status(packet.data) then if not iocontrol.update_facility_status(packet.data) then
log.error("received invalid FAC_STATUS packet") log.debug("received invalid FAC_STATUS packet")
end end
elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then elseif packet.type == SCADA_CRDN_TYPE.FAC_CMD then
-- facility command acknowledgement -- facility command acknowledgement
if packet.length >= 2 then if packet.length >= 2 then
local cmd = packet.data[1] local cmd = packet.data[1]
local ack = packet.data[2] == true 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) 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) iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMANDS.START then elseif cmd == FAC_COMMAND.START then
if packet.length == 7 then if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) }) process.start_ack_handle({ table.unpack(packet.data, 2) })
else else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch") log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end 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) iocontrol.get_db().facility.ack_alarms_ack(ack)
else else
log.debug(util.c("received facility command ack with unknown command ", cmd)) 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 else
log.debug("SCADA_CRDN facility command ack packet length mismatch") log.debug("SCADA_CRDN facility command ack packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then elseif packet.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
-- record builds -- record builds
if packet.length == 1 then if packet.length == 1 then
if iocontrol.record_unit_builds(packet.data[1]) then if iocontrol.record_unit_builds(packet.data[1]) then
-- acknowledge receipt of builds -- 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 else
log.error("received invalid UNIT_BUILDS packet") log.debug("received invalid UNIT_BUILDS packet")
end end
else else
log.debug("UNIT_BUILDS packet length mismatch") log.debug("UNIT_BUILDS packet length mismatch")
end end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_STATUSES then elseif packet.type == SCADA_CRDN_TYPE.UNIT_STATUSES then
-- update statuses -- update statuses
if not iocontrol.update_unit_statuses(packet.data) then if not iocontrol.update_unit_statuses(packet.data) then
log.error("received invalid UNIT_STATUSES packet") log.error("received invalid UNIT_STATUSES packet")
end end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_CMD then elseif packet.type == SCADA_CRDN_TYPE.UNIT_CMD then
-- unit command acknowledgement -- unit command acknowledgement
if packet.length == 3 then if packet.length == 3 then
local cmd = packet.data[1] 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 local unit = iocontrol.get_db().units[unit_id] ---@type ioctl_unit
if unit ~= nil then if unit ~= nil then
if cmd == UNIT_COMMANDS.SCRAM then if cmd == UNIT_COMMAND.SCRAM then
unit.scram_ack(ack) unit.scram_ack(ack)
elseif cmd == UNIT_COMMANDS.START then elseif cmd == UNIT_COMMAND.START then
unit.start_ack(ack) unit.start_ack(ack)
elseif cmd == UNIT_COMMANDS.RESET_RPS then elseif cmd == UNIT_COMMAND.RESET_RPS then
unit.reset_rps_ack(ack) unit.reset_rps_ack(ack)
elseif cmd == UNIT_COMMANDS.SET_BURN then elseif cmd == UNIT_COMMAND.SET_BURN then
unit.set_burn_ack(ack) unit.set_burn_ack(ack)
elseif cmd == UNIT_COMMANDS.SET_WASTE then elseif cmd == UNIT_COMMAND.SET_WASTE then
unit.set_waste_ack(ack) 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) unit.ack_alarms_ack(ack)
elseif cmd == UNIT_COMMANDS.SET_GROUP then elseif cmd == UNIT_COMMAND.SET_GROUP then
---@todo how is this going to be handled? -- UI will be updated to display current group if changed successfully
else else
log.debug(util.c("received unit command ack with unknown command ", cmd)) log.debug(util.c("received unit command ack with unknown command ", cmd))
end end
@ -534,8 +541,9 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else else
log.debug("discarding SCADA_CRDN packet before linked") log.debug("discarding SCADA_CRDN packet before linked")
end end
elseif protocol == PROTOCOLS.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then ---@cast packet mgmt_frame
if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- connection with supervisor established -- connection with supervisor established
if packet.length == 2 then if packet.length == 2 then
local est_ack = packet.data[1] 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 self.sv_linked = true
else else
log.error("invalid supervisor configuration definitions received, establish failed") log.debug("invalid supervisor configuration definitions received, establish failed")
end end
else else
log.error("invalid supervisor configuration table received, establish failed") log.debug("invalid supervisor configuration table received, establish failed")
end end
else else
log.debug("SCADA_MGMT establish packet reply (len = 2) unsupported") 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 est_ack == ESTABLISH_ACK.DENY then
if self.last_est_ack ~= est_ack then if self.last_est_ack ~= est_ack then
log.debug("supervisor connection denied") log.info("supervisor connection denied")
end end
elseif est_ack == ESTABLISH_ACK.COLLISION then elseif est_ack == ESTABLISH_ACK.COLLISION then
if self.last_est_ack ~= est_ack 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 end
elseif est_ack == ESTABLISH_ACK.BAD_VERSION then elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
if self.last_est_ack ~= est_ack 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") log.debug("SCADA_MGMT establish packet length mismatch")
end end
elseif self.sv_linked then 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 -- keep alive request received, echo back
if packet.length == 1 then if packet.length == 1 then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -614,14 +622,14 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, range
else else
log.debug("SCADA keep alive packet length mismatch") log.debug("SCADA keep alive packet length mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
sv_watchdog.cancel() sv_watchdog.cancel()
self.sv_linked = false self.sv_linked = false
println_ts("server connection closed by remote host") 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 else
log.warning("received unknown SCADA_MGMT packet type " .. packet.type) log.debug("received unknown SCADA_MGMT packet type " .. packet.type)
end end
else else
log.debug("discarding non-link SCADA_MGMT packet before linked") 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 end
-- check if the coordinator is still linked to the supervisor -- check if the coordinator is still linked to the supervisor
---@nodiscard
function public.is_linked() return self.sv_linked end function public.is_linked() return self.sv_linked end
return public 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 log = require("scada-common.log")
local psil = require("scada-common.psil") local psil = require("scada-common.psil")
local types = require("scada-common.types") local types = require("scada-common.types")
@ -7,8 +10,6 @@ local util = require("scada-common.util")
local process = require("coordinator.process") local process = require("coordinator.process")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local ALARM_STATE = types.ALARM_STATE local ALARM_STATE = types.ALARM_STATE
local iocontrol = {} local iocontrol = {}
@ -19,7 +20,6 @@ local io = {}
-- initialize the coordinator IO controller -- initialize the coordinator IO controller
---@param conf facility_conf configuration ---@param conf facility_conf configuration
---@param comms coord_comms comms reference ---@param comms coord_comms comms reference
---@diagnostic disable-next-line: redefined-local
function iocontrol.init(conf, comms) function iocontrol.init(conf, comms)
---@class ioctl_facility ---@class ioctl_facility
io.facility = { io.facility = {
@ -59,7 +59,7 @@ function iocontrol.init(conf, comms)
env_d_data = {} 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 for _ = 1, conf.num_units do
local data = {} ---@type imatrix_session_db local data = {} ---@type imatrix_session_db
table.insert(io.facility.induction_ps_tbl, psil.create()) table.insert(io.facility.induction_ps_tbl, psil.create())
@ -173,6 +173,8 @@ end
---@param build table ---@param build table
---@return boolean valid ---@return boolean valid
function iocontrol.record_facility_builds(build) function iocontrol.record_facility_builds(build)
local valid = true
if type(build) == "table" then if type(build) == "table" then
local fac = io.facility local fac = io.facility
@ -190,35 +192,37 @@ function iocontrol.record_facility_builds(build)
end end
else else
log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id)) log.debug(util.c("iocontrol.record_facility_builds: invalid induction matrix id ", id))
valid = false
end end
end end
end end
else else
log.error("facility builds not a table") log.debug("facility builds not a table")
return false valid = false
end end
return true return valid
end end
-- populate unit structure builds -- populate unit structure builds
---@param builds table ---@param builds table
---@return boolean valid ---@return boolean valid
function iocontrol.record_unit_builds(builds) function iocontrol.record_unit_builds(builds)
local valid = true
-- note: if not all units and RTUs are connected, some will be nil -- note: if not all units and RTUs are connected, some will be nil
for id, build in pairs(builds) do for id, build in pairs(builds) do
local unit = io.units[id] ---@type ioctl_unit local unit = io.units[id] ---@type ioctl_unit
local log_header = util.c("iocontrol.record_unit_builds[UNIT ", id, "]: ")
if type(build) ~= "table" then if type(build) ~= "table" then
log.error(util.c("corrupted unit builds provided, unit ", id, " not a table")) log.debug(log_header .. "build not a table")
return false valid = false
elseif type(unit) ~= "table" then elseif type(unit) ~= "table" then
log.error(util.c("corrupted unit builds provided, invalid unit ", id)) log.debug(log_header .. "invalid unit id")
return false valid = false
end else
local log_header = util.c("iocontrol.record_unit_builds[unit ", id, "]: ")
-- reactor build -- reactor build
if type(build.reactor) == "table" then if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
@ -246,6 +250,7 @@ function iocontrol.record_unit_builds(builds)
end end
else else
log.debug(util.c(log_header, "invalid boiler id ", b_id)) log.debug(util.c(log_header, "invalid boiler id ", b_id))
valid = false
end end
end end
end end
@ -264,22 +269,26 @@ function iocontrol.record_unit_builds(builds)
end end
else else
log.debug(util.c(log_header, "invalid turbine id ", t_id)) log.debug(util.c(log_header, "invalid turbine id ", t_id))
valid = false
end
end end
end end
end end
end end
return true return valid
end end
-- update facility status -- update facility status
---@param status table ---@param status table
---@return boolean valid ---@return boolean valid
function iocontrol.update_facility_status(status) function iocontrol.update_facility_status(status)
local valid = true
local log_header = util.c("iocontrol.update_facility_status: ") local log_header = util.c("iocontrol.update_facility_status: ")
if type(status) ~= "table" then if type(status) ~= "table" then
log.debug(log_header .. "status not a table") log.debug(util.c(log_header, "status not a table"))
return false valid = false
else else
local fac = io.facility local fac = io.facility
@ -287,10 +296,17 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1] 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.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2] 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_ramping = ctl_status[4]
fac.auto_saturated = ctl_status[5] fac.auto_saturated = ctl_status[5]
@ -330,6 +346,7 @@ function iocontrol.update_facility_status(status)
end end
else else
log.debug(log_header .. "control status not a table or length mismatch") log.debug(log_header .. "control status not a table or length mismatch")
valid = false
end end
-- RTU statuses -- RTU statuses
@ -337,10 +354,10 @@ function iocontrol.update_facility_status(status)
local rtu_statuses = status[2] local rtu_statuses = status[2]
fac.rtu_count = 0 fac.rtu_count = 0
if type(rtu_statuses) == "table" then if type(rtu_statuses) == "table" then
-- connected RTU count -- connected RTU count
fac.rtu_count = rtu_statuses.count fac.rtu_count = rtu_statuses.count
fac.ps.publish("rtu_count", fac.rtu_count)
-- power statistics -- power statistics
if type(rtu_statuses.power) == "table" then 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]) fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
else else
log.debug(log_header .. "power statistics list not a table") log.debug(log_header .. "power statistics list not a table")
valid = false
end end
-- induction matricies statuses -- induction matricies statuses
@ -399,6 +417,7 @@ function iocontrol.update_facility_status(status)
end end
else else
log.debug(log_header .. "induction matrix list not a table") log.debug(log_header .. "induction matrix list not a table")
valid = false
end end
-- environment detector status -- environment detector status
@ -416,42 +435,46 @@ function iocontrol.update_facility_status(status)
end end
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "radiation monitor list not a table")
return false valid = false
end end
else else
log.debug(log_header .. "rtu statuses not a table") log.debug(log_header .. "rtu statuses not a table")
end valid = false
end end
return true fac.ps.publish("rtu_count", fac.rtu_count)
end
return valid
end end
-- update unit statuses -- update unit statuses
---@param statuses table ---@param statuses table
---@return boolean valid ---@return boolean valid
function iocontrol.update_unit_statuses(statuses) function iocontrol.update_unit_statuses(statuses)
local valid = true
if type(statuses) ~= "table" then if type(statuses) ~= "table" then
log.debug("iocontrol.update_unit_statuses: unit statuses not a table") log.debug("iocontrol.update_unit_statuses: unit statuses not a table")
return false valid = false
elseif #statuses ~= #io.units then elseif #statuses ~= #io.units then
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units") log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
return false valid = false
else else
local burn_rate_sum = 0.0 local burn_rate_sum = 0.0
-- get all unit statuses -- get all unit statuses
for i = 1, #statuses do for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ") local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
local unit = io.units[i] ---@type ioctl_unit local unit = io.units[i] ---@type ioctl_unit
local status = statuses[i] local status = statuses[i]
if type(status) ~= "table" or #status ~= 5 then if type(status) ~= "table" or #status ~= 5 then
log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)") log.debug(log_header .. "invalid status entry in unit statuses (not a table or invalid length)")
return false valid = false
end else
-- reactor PLC status -- reactor PLC status
local reactor_status = status[1] local reactor_status = status[1]
if type(reactor_status) ~= "table" then if type(reactor_status) ~= "table" then
@ -520,10 +543,10 @@ function iocontrol.update_unit_statuses(statuses)
end end
else else
log.debug(log_header .. "reactor status length mismatch") log.debug(log_header .. "reactor status length mismatch")
valid = false
end end
-- RTU statuses -- RTU statuses
local rtu_statuses = status[2] local rtu_statuses = status[2]
if type(rtu_statuses) == "table" then if type(rtu_statuses) == "table" then
@ -569,10 +592,12 @@ function iocontrol.update_unit_statuses(statuses)
end end
else else
log.debug(util.c(log_header, "invalid boiler id ", id)) log.debug(util.c(log_header, "invalid boiler id ", id))
valid = false
end end
end end
else else
log.debug(log_header .. "boiler list not a table") log.debug(log_header .. "boiler list not a table")
valid = false
end end
-- turbine statuses -- turbine statuses
@ -619,11 +644,12 @@ function iocontrol.update_unit_statuses(statuses)
end end
else else
log.debug(util.c(log_header, "invalid turbine id ", id)) log.debug(util.c(log_header, "invalid turbine id ", id))
valid = false
end end
end end
else else
log.debug(log_header .. "turbine list not a table") log.debug(log_header .. "turbine list not a table")
return false valid = false
end end
-- environment detector status -- environment detector status
@ -639,19 +665,20 @@ function iocontrol.update_unit_statuses(statuses)
end end
else else
log.debug(log_header .. "radiation monitor list not a table") log.debug(log_header .. "radiation monitor list not a table")
return false valid = false
end end
else else
log.debug(log_header .. "rtu list not a table") log.debug(log_header .. "rtu list not a table")
valid = false
end end
-- annunciator -- annunciator
unit.annunciator = status[3] unit.annunciator = status[3]
if type(unit.annunciator) ~= "table" then if type(unit.annunciator) ~= "table" then
unit.annunciator = {} unit.annunciator = {}
log.debug(log_header .. "annunciator state not a table") log.debug(log_header .. "annunciator state not a table")
valid = false
end end
for key, val in pairs(unit.annunciator) do for key, val in pairs(unit.annunciator) do
@ -678,7 +705,8 @@ function iocontrol.update_unit_statuses(statuses)
end end
elseif type(val) == "table" then elseif type(val) == "table" then
-- we missed one of the tables? -- we missed one of the tables?
log.error(log_header .. "unrecognized table found in annunciator list, this is a bug", true) log.debug(log_header .. "unrecognized table found in annunciator list, this is a bug")
valid = false
else else
-- non-table fields -- non-table fields
unit.unit_ps.publish(key, val) unit.unit_ps.publish(key, val)
@ -686,7 +714,6 @@ function iocontrol.update_unit_statuses(statuses)
end end
-- alarms -- alarms
local alarm_states = status[4] local alarm_states = status[4]
if type(alarm_states) == "table" then if type(alarm_states) == "table" then
@ -705,10 +732,10 @@ function iocontrol.update_unit_statuses(statuses)
end end
else else
log.debug(log_header .. "alarm states not a table") log.debug(log_header .. "alarm states not a table")
valid = false
end end
-- unit state fields -- unit state fields
local unit_state = status[5] local unit_state = status[5]
if type(unit_state) == "table" then if type(unit_state) == "table" then
@ -720,9 +747,12 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("U_AutoDegraded", unit_state[5]) unit.unit_ps.publish("U_AutoDegraded", unit_state[5])
else else
log.debug(log_header .. "unit state length mismatch") log.debug(log_header .. "unit state length mismatch")
valid = false
end end
else else
log.debug(log_header .. "unit state not a table") log.debug(log_header .. "unit state not a table")
valid = false
end
end end
end end
@ -732,7 +762,7 @@ function iocontrol.update_unit_statuses(statuses)
sounder.eval(io.units) sounder.eval(io.units)
end end
return true return valid
end end
-- get the IO controller database -- get the IO controller database

View File

@ -1,11 +1,14 @@
--
-- Process Control Management
--
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local FAC_COMMANDS = comms.FAC_COMMANDS local FAC_COMMAND = comms.FAC_COMMAND
local UNIT_COMMANDS = comms.UNIT_COMMANDS local UNIT_COMMAND = comms.UNIT_COMMAND
local PROCESS = types.PROCESS local PROCESS = types.PROCESS
@ -30,11 +33,11 @@ local self = {
-------------------------- --------------------------
-- initialize the process controller -- initialize the process controller
---@param iocontrol ioctl ---@param iocontrol ioctl iocontrl system
---@diagnostic disable-next-line: redefined-local ---@param coord_comms coord_comms coordinator communications
function process.init(iocontrol, comms) function process.init(iocontrol, coord_comms)
self.io = iocontrol self.io = iocontrol
self.comms = comms self.comms = coord_comms
for i = 1, self.io.facility.num_units do for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1 self.config.limits[i] = 0.1
@ -71,7 +74,7 @@ function process.init(iocontrol, comms)
if type(waste_mode) == "table" then if type(waste_mode) == "table" then
for id, mode in pairs(waste_mode) do 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 end
log.info("PROCESS: loaded waste mode settings from coord.settings") 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 if type(prio_groups) == "table" then
for id, group in pairs(prio_groups) do 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 end
log.info("PROCESS: loaded priority groups settings from coord.settings") log.info("PROCESS: loaded priority groups settings from coord.settings")
@ -90,45 +93,45 @@ end
-- facility SCRAM command -- facility SCRAM command
function process.fac_scram() function process.fac_scram()
self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL) self.comms.send_fac_command(FAC_COMMAND.SCRAM_ALL)
log.debug("FAC: SCRAM ALL") log.debug("PROCESS: FAC SCRAM ALL")
end end
-- facility alarm acknowledge command -- facility alarm acknowledge command
function process.fac_ack_alarms() function process.fac_ack_alarms()
self.comms.send_fac_command(FAC_COMMANDS.ACK_ALL_ALARMS) self.comms.send_fac_command(FAC_COMMAND.ACK_ALL_ALARMS)
log.debug("FAC: ACK ALL ALARMS") log.debug("PROCESS: FAC ACK ALL ALARMS")
end end
-- start reactor -- start reactor
---@param id integer unit ID ---@param id integer unit ID
function process.start(id) function process.start(id)
self.io.units[id].control_state = true self.io.units[id].control_state = true
self.comms.send_unit_command(UNIT_COMMANDS.START, id) self.comms.send_unit_command(UNIT_COMMAND.START, id)
log.debug(util.c("UNIT[", id, "]: START")) log.debug(util.c("PROCESS: UNIT[", id, "] START"))
end end
-- SCRAM reactor -- SCRAM reactor
---@param id integer unit ID ---@param id integer unit ID
function process.scram(id) function process.scram(id)
self.io.units[id].control_state = false self.io.units[id].control_state = false
self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id) self.comms.send_unit_command(UNIT_COMMAND.SCRAM, id)
log.debug(util.c("UNIT[", id, "]: SCRAM")) log.debug(util.c("PROCESS: UNIT[", id, "] SCRAM"))
end end
-- reset reactor protection system -- reset reactor protection system
---@param id integer unit ID ---@param id integer unit ID
function process.reset_rps(id) function process.reset_rps(id)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id) self.comms.send_unit_command(UNIT_COMMAND.RESET_RPS, id)
log.debug(util.c("UNIT[", id, "]: RESET RPS")) log.debug(util.c("PROCESS: UNIT[", id, "] RESET RPS"))
end end
-- set burn rate -- set burn rate
---@param id integer unit ID ---@param id integer unit ID
---@param rate number burn rate ---@param rate number burn rate
function process.set_rate(id, rate) function process.set_rate(id, rate)
self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate) self.comms.send_unit_command(UNIT_COMMAND.SET_BURN, id, rate)
log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate)) log.debug(util.c("PROCESS: UNIT[", id, "] SET BURN ", rate))
end end
-- set waste mode -- set waste mode
@ -138,14 +141,12 @@ function process.set_waste(id, mode)
-- publish so that if it fails then it gets reset -- publish so that if it fails then it gets reset
self.io.units[id].unit_ps.publish("U_WasteMode", mode) self.io.units[id].unit_ps.publish("U_WasteMode", mode)
self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode) self.comms.send_unit_command(UNIT_COMMAND.SET_WASTE, id, mode)
log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode)) log.debug(util.c("PROCESS: UNIT[", id, "] SET WASTE ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) ~= "table" then if type(waste_mode) ~= "table" then waste_mode = {} end
waste_mode = {}
end
waste_mode[id] = mode waste_mode[id] = mode
@ -159,38 +160,36 @@ end
-- acknowledge all alarms -- acknowledge all alarms
---@param id integer unit ID ---@param id integer unit ID
function process.ack_all_alarms(id) function process.ack_all_alarms(id)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id) self.comms.send_unit_command(UNIT_COMMAND.ACK_ALL_ALARMS, id)
log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS")) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALL ALARMS"))
end end
-- acknowledge an alarm -- acknowledge an alarm
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.ack_alarm(id, alarm) function process.ack_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm) self.comms.send_unit_command(UNIT_COMMAND.ACK_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm)) log.debug(util.c("PROCESS: UNIT[", id, "] ACK ALARM ", alarm))
end end
-- reset an alarm -- reset an alarm
---@param id integer unit ID ---@param id integer unit ID
---@param alarm integer alarm ID ---@param alarm integer alarm ID
function process.reset_alarm(id, alarm) function process.reset_alarm(id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm) self.comms.send_unit_command(UNIT_COMMAND.RESET_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm)) log.debug(util.c("PROCESS: UNIT[", id, "] RESET ALARM ", alarm))
end end
-- assign a unit to a group -- assign a unit to a group
---@param unit_id integer unit ID ---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent ---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id) function process.set_group(unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id) self.comms.send_unit_command(UNIT_COMMAND.SET_GROUP, unit_id, group_id)
log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id)) log.debug(util.c("PROCESS: UNIT[", unit_id, "] SET GROUP ", group_id))
local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil local prio_groups = settings.get("PRIORITY_GROUPS") ---@type table|nil
if type(prio_groups) ~= "table" then if type(prio_groups) ~= "table" then prio_groups = {} end
prio_groups = {}
end
prio_groups[unit_id] = group_id prio_groups[unit_id] = group_id
@ -207,14 +206,14 @@ end
-- stop automatic process control -- stop automatic process control
function process.stop_auto() function process.stop_auto()
self.comms.send_fac_command(FAC_COMMANDS.STOP) self.comms.send_fac_command(FAC_COMMAND.STOP)
log.debug("FAC: STOP AUTO") log.debug("PROCESS: STOP AUTO CTL")
end end
-- start automatic process control -- start automatic process control
function process.start_auto() function process.start_auto()
self.comms.send_auto_start(self.config) self.comms.send_auto_start(self.config)
log.debug("FAC: START AUTO") log.debug("PROCESS: START AUTO CTL")
end end
-- save process control settings -- 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") log.warning("process.save(): failed to save coordinator settings file")
end end
log.debug("saved = " .. util.strval(saved))
self.io.facility.save_cfg_ack(saved) self.io.facility.save_cfg_ack(saved)
end end
@ -273,18 +270,4 @@ function process.start_ack_handle(response)
self.io.facility.start_ack(ack) self.io.facility.start_ack(ack)
end 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 return process

View File

@ -1,3 +1,7 @@
--
-- Graphics Rendering Control
--
local log = require("scada-common.log") local log = require("scada-common.log")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -56,6 +60,7 @@ function renderer.set_displays(monitors)
end end
-- check if the renderer is configured to use a given monitor peripheral -- check if the renderer is configured to use a given monitor peripheral
---@nodiscard
---@param periph table peripheral ---@param periph table peripheral
---@return boolean is_used ---@return boolean is_used
function renderer.is_monitor_used(periph) function renderer.is_monitor_used(periph)
@ -87,6 +92,7 @@ function renderer.reset(recolor)
end end
-- check main display width -- check main display width
---@nodiscard
---@return boolean width_okay ---@return boolean width_okay
function renderer.validate_main_display_width() function renderer.validate_main_display_width()
local w, _ = engine.monitors.primary.getSize() local w, _ = engine.monitors.primary.getSize()
@ -94,6 +100,7 @@ function renderer.validate_main_display_width()
end end
-- check display sizes -- check display sizes
---@nodiscard
---@return boolean valid all unit display dimensions OK ---@return boolean valid all unit display dimensions OK
function renderer.validate_unit_display_sizes() function renderer.validate_unit_display_sizes()
local valid = true local valid = true
@ -101,7 +108,7 @@ function renderer.validate_unit_display_sizes()
for id, monitor in pairs(engine.monitors.unit_displays) do for id, monitor in pairs(engine.monitors.unit_displays) do
local w, h = monitor.getSize() local w, h = monitor.getSize()
if w ~= 79 or h ~= 52 then 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 valid = false
end end
end end
@ -171,6 +178,7 @@ function renderer.close_ui()
end end
-- is the UI ready? -- is the UI ready?
---@nodiscard
---@return boolean ready ---@return boolean ready
function renderer.ui_ready() return engine.ui_ready end function renderer.ui_ready() return engine.ui_ready end

View File

@ -26,7 +26,8 @@ local alarm_ctl = {
playing = false, playing = false,
num_active = 0, num_active = 0,
next_block = 1, 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 -- 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 -- calculate how many samples are in the given number of milliseconds
---@nodiscard
---@param ms integer milliseconds ---@param ms integer milliseconds
---@return integer samples ---@return integer samples
local function ms_to_samples(ms) return math.floor(ms * 48) end local function ms_to_samples(ms) return math.floor(ms * 48) end
@ -224,6 +226,7 @@ end
--#endregion --#endregion
-- hard audio limiter -- hard audio limiter
---@nodiscard
---@param output number output level ---@param output number output level
---@return number limited -128.0 to 127.0 ---@return number limited -128.0 to 127.0
local function limit(output) local function limit(output)
@ -454,7 +457,7 @@ function sounder.test_power_scale()
end end
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 end
--#endregion --#endregion

View File

@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol")
local renderer = require("coordinator.renderer") local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder") local sounder = require("coordinator.sounder")
local COORDINATOR_VERSION = "beta-v0.10.1" local COORDINATOR_VERSION = "v0.11.0"
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -81,7 +81,7 @@ local function main()
-- setup monitors -- setup monitors
local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS) local configured, monitors = coordinator.configure_monitors(config.NUM_UNITS)
if not configured or monitors == nil then if not configured or monitors == nil then
println("boot> monitor setup failed") println("startup> monitor setup failed")
log.fatal("monitor configuration failed") log.fatal("monitor configuration failed")
return return
end end
@ -91,11 +91,11 @@ local function main()
renderer.reset(config.RECOLOR) renderer.reset(config.RECOLOR)
if not renderer.validate_main_display_width() then 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") log.fatal("main display not wide enough")
return return
elseif not renderer.validate_unit_display_sizes() then 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") log.fatal("unit display dimensions incorrect")
return return
end end
@ -116,7 +116,7 @@ local function main()
local speaker = ppm.get_device("speaker") local speaker = ppm.get_device("speaker")
if speaker == nil then if speaker == nil then
log_boot("annunciator alarm speaker not found") log_boot("annunciator alarm speaker not found")
println("boot> speaker not found") println("startup> speaker not found")
log.fatal("no annunciator alarm speaker found") log.fatal("no annunciator alarm speaker found")
return return
else else
@ -135,7 +135,7 @@ local function main()
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
if modem == nil then if modem == nil then
log_comms("wireless modem not found") 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") log.fatal("no wireless modem on startup")
return return
else else
@ -145,12 +145,12 @@ local function main()
-- create connection watchdog -- create connection watchdog
local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) local conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
conn_watchdog.cancel() conn_watchdog.cancel()
log.debug("boot> conn watchdog created") log.debug("startup> conn watchdog created")
-- start comms, open all channels -- start comms, open all channels
local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.SCADA_SV_PORT, config.SCADA_SV_LISTEN, 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) config.SCADA_API_LISTEN, config.TRUSTED_RANGE, conn_watchdog)
log.debug("boot> comms init") log.debug("startup> comms init")
log_comms("comms initialized") log_comms("comms initialized")
-- base loop clock (2Hz, 10 ticks) -- base loop clock (2Hz, 10 ticks)
@ -176,7 +176,7 @@ local function main()
end end
if not init_connect_sv() then if not init_connect_sv() then
println("boot> failed to connect to supervisor") println("startup> failed to connect to supervisor")
log_sys("system shutdown") log_sys("system shutdown")
return return
else else
@ -199,7 +199,7 @@ local function main()
renderer.close_ui() renderer.close_ui()
log_graphics(util.c("UI crashed: ", message)) log_graphics(util.c("UI crashed: ", message))
println_ts("UI crashed") println_ts("UI crashed")
log.fatal(util.c("ui crashed with error ", message)) log.fatal(util.c("GUI crashed with error ", message))
else else
log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms") log_graphics("first UI draw took " .. (util.time_ms() - draw_start) .. "ms")
@ -223,7 +223,7 @@ local function main()
if ui_ok then if ui_ok then
-- start connection watchdog -- start connection watchdog
conn_watchdog.feed() conn_watchdog.feed()
log.debug("boot> conn watchdog started") log.debug("startup> conn watchdog started")
log_sys("system started successfully") log_sys("system started successfully")
end end
@ -243,7 +243,6 @@ local function main()
no_modem = true no_modem = true
log_sys("comms modem disconnected") log_sys("comms modem disconnected")
println_ts("wireless modem disconnected!") println_ts("wireless modem disconnected!")
log.error("comms modem disconnected!")
-- close out UI -- close out UI
renderer.close_ui() renderer.close_ui()
@ -252,20 +251,21 @@ local function main()
log_sys("awaiting comms modem reconnect...") log_sys("awaiting comms modem reconnect...")
else else
log_sys("non-comms modem disconnected") log_sys("non-comms modem disconnected")
log.warning("non-comms modem disconnected")
end end
elseif type == "monitor" then elseif type == "monitor" then
if renderer.is_monitor_used(device) then if renderer.is_monitor_used(device) then
-- "halt and catch fire" style handling -- "halt and catch fire" style handling
println_ts("lost a configured monitor, system will now exit") local msg = "lost a configured monitor, system will now exit"
log_sys("lost a configured monitor, system will now exit") println_ts(msg)
log_sys(msg)
break break
else else
log_sys("lost unused monitor, ignoring") log_sys("lost unused monitor, ignoring")
end end
elseif type == "speaker" then elseif type == "speaker" then
println_ts("lost alarm sounder speaker") local msg = "lost alarm sounder speaker"
log_sys("lost alarm sounder speaker") println_ts(msg)
log_sys(msg)
end end
end end
elseif event == "peripheral" then elseif event == "peripheral" then
@ -291,8 +291,9 @@ local function main()
elseif type == "monitor" then elseif type == "monitor" then
-- not supported, system will exit on loss of in-use monitors -- not supported, system will exit on loss of in-use monitors
elseif type == "speaker" then elseif type == "speaker" then
println_ts("alarm sounder speaker reconnected") local msg = "alarm sounder speaker reconnected"
log_sys("alarm sounder speaker reconnected") println_ts(msg)
log_sys(msg)
sounder.reconnect(device) sounder.reconnect(device)
end end
end end
@ -301,7 +302,7 @@ local function main()
-- main loop tick -- main loop tick
-- free any closed sessions -- free any closed sessions
--apisessions.free_all_closed() apisessions.free_all_closed()
-- update date and time string for main display -- update date and time string for main display
iocontrol.get_db().facility.ps.publish("date_time", os.date(date_format)) 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 -- a non-clock/main watchdog timer event
--check API watchdogs --check API watchdogs
--apisessions.check_all_watchdogs(param1) apisessions.check_all_watchdogs(param1)
-- notify timer callback dispatcher -- notify timer callback dispatcher
tcallbackdsp.handle(param1) tcallbackdsp.handle(param1)

View File

@ -16,11 +16,8 @@ local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light") local IndicatorLight = require("graphics.elements.indicators.light")
local RadIndicator = require("graphics.elements.indicators.rad") local RadIndicator = require("graphics.elements.indicators.rad")
local TriIndicatorLight = require("graphics.elements.indicators.trilight") local TriIndicatorLight = require("graphics.elements.indicators.trilight")
local VerticalBar = require("graphics.elements.indicators.vbar")
local HazardButton = require("graphics.elements.controls.hazard_button") 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 RadioButton = require("graphics.elements.controls.radio_button")
local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") 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 style = require("coordinator.ui.style")
local core = require("graphics.core") 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} 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) ps.subscribe("ccool_type", function (type)
if type == "mekanism:sodium" then if type == types.FLUID.SODIUM then
ccool.recolor(cpair(colors.lightBlue, colors.gray)) ccool.recolor(cpair(colors.lightBlue, colors.gray))
else else
ccool.recolor(cpair(colors.blue, colors.gray)) ccool.recolor(cpair(colors.blue, colors.gray))
@ -55,7 +57,7 @@ local function new_view(root, x, y, data, ps)
end) end)
ps.subscribe("hcool_type", function (type) 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)) hcool.recolor(cpair(colors.orange, colors.gray))
else else
hcool.recolor(cpair(colors.white, colors.gray)) hcool.recolor(cpair(colors.white, colors.gray))

View File

@ -287,7 +287,7 @@ local function init(parent, id)
end end
local t1_sdo = TriIndicatorLight{parent=rcs_annunc,label="Steam Relief Valve Open",c1=colors.gray,c2=colors.yellow,c3=colors.red} 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} 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)} 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 if unit.num_turbines > 1 then
TextBox{parent=rcs_tags,text="T2",width=2,height=1,fg_bg=bw_fg_bg} 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} 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} 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)} 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 if unit.num_turbines > 2 then
TextBox{parent=rcs_tags,text="T3",width=2,height=1,fg_bg=bw_fg_bg} 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} 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} 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)} local t3_tos = IndicatorLight{parent=rcs_annunc,label="Turbine Over Speed",colors=cpair(colors.red,colors.gray)}

View File

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

View File

@ -3,13 +3,11 @@ local completion = require("cc.completion")
local util = require("scada-common.util") local util = require("scada-common.util")
local print = util.print local print = util.print
local println = util.println
local print_ts = util.print_ts
local println_ts = util.println_ts
local dialog = {} local dialog = {}
-- ask the user yes or no -- ask the user yes or no
---@nodiscard
---@param question string ---@param question string
---@param default boolean ---@param default boolean
---@return boolean|nil ---@return boolean|nil
@ -36,6 +34,7 @@ function dialog.ask_y_n(question, default)
end end
-- ask the user for an input within a set of options -- ask the user for an input within a set of options
---@nodiscard
---@param options table ---@param options table
---@param cancel string ---@param cancel string
---@return boolean|string|nil ---@return boolean|string|nil

View File

@ -90,7 +90,7 @@ local function init(monitor)
cnc_bottom_align_start = cnc_bottom_align_start + 2 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 -- testing
---@fixme remove test code ---@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="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} 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 return main
end end

View File

@ -16,6 +16,7 @@ local events = {}
---@field y integer ---@field y integer
-- create a new touch event definition -- create a new touch event definition
---@nodiscard
---@param monitor string ---@param monitor string
---@param x integer ---@param x integer
---@param y integer ---@param y integer
@ -32,7 +33,7 @@ core.events = events
local graphics = {} local graphics = {}
---@alias TEXT_ALIGN integer ---@enum TEXT_ALIGN
graphics.TEXT_ALIGN = { graphics.TEXT_ALIGN = {
LEFT = 1, LEFT = 1,
CENTER = 2, CENTER = 2,
@ -47,6 +48,7 @@ graphics.TEXT_ALIGN = {
---@alias element_id string|integer ---@alias element_id string|integer
-- create a new border definition -- create a new border definition
---@nodiscard
---@param width integer border width ---@param width integer border width
---@param color color border color ---@param color color border color
---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false ---@param even? boolean whether to pad width extra to account for rectangular pixels, defaults to false
@ -66,6 +68,7 @@ end
---@field h integer ---@field h integer
-- create a new graphics frame definition -- create a new graphics frame definition
---@nodiscard
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@param w integer ---@param w integer
@ -91,6 +94,7 @@ end
---@field blit_bkg string ---@field blit_bkg string
-- create a new color pair definition -- create a new color pair definition
---@nodiscard
---@param a color ---@param a color
---@param b color ---@param b color
---@return cpair ---@return cpair
@ -120,9 +124,9 @@ end
---@field thin boolean true for 1 subpixel, false (default) for 2 ---@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 ---@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) -- note: pipe coordinate origin is (0, 0)
---@nodiscard
---@param x1 integer starting x, origin is 0 ---@param x1 integer starting x, origin is 0
---@param y1 integer starting y, origin is 0 ---@param y1 integer starting y, origin is 0
---@param x2 integer ending x, origin is 0 ---@param x2 integer ending x, origin is 0

View File

@ -47,6 +47,7 @@ local element = {}
---|tiling_args ---|tiling_args
-- a base graphics element, should not be created on its own -- a base graphics element, should not be created on its own
---@nodiscard
---@param args graphics_args arguments ---@param args graphics_args arguments
function element.new(args) function element.new(args)
local self = { local self = {
@ -172,6 +173,7 @@ function element.new(args)
end end
-- get value -- get value
---@nodiscard
function protected.get_value() function protected.get_value()
return protected.value return protected.value
end end
@ -218,6 +220,7 @@ function element.new(args)
end end
-- get public interface -- get public interface
---@nodiscard
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
function protected.get() return public, self.id end function protected.get() return public, self.id end
@ -246,11 +249,13 @@ function element.new(args)
---------------------- ----------------------
-- get the window object -- get the window object
---@nodiscard
function public.window() return protected.window end function public.window() return protected.window end
-- CHILD ELEMENTS -- -- CHILD ELEMENTS --
-- add a child element -- add a child element
---@nodiscard
---@param key string|nil id ---@param key string|nil id
---@param child graphics_template ---@param child graphics_template
---@return integer|string key ---@return integer|string key
@ -271,6 +276,7 @@ function element.new(args)
end end
-- get a child element -- get a child element
---@nodiscard
---@return graphics_element ---@return graphics_element
function public.get_child(key) return self.children[key] end 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 function public.remove(key) self.children[key] = nil end
-- attempt to get a child element by ID (does not include this element itself) -- attempt to get a child element by ID (does not include this element itself)
---@nodiscard
---@param id element_id ---@param id element_id
---@return graphics_element|nil element ---@return graphics_element|nil element
function public.get_element_by_id(id) function public.get_element_by_id(id)
@ -297,39 +304,49 @@ function element.new(args)
-- AUTO-PLACEMENT -- -- AUTO-PLACEMENT --
-- skip a line for automatically placed elements -- 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 -- -- PROPERTIES --
-- get the foreground/background colors -- get the foreground/background colors
---@nodiscard
---@return cpair fg_bg ---@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 -- get element x
---@nodiscard
---@return integer x ---@return integer x
function public.get_x() function public.get_x()
return protected.frame.x return protected.frame.x
end end
-- get element y -- get element y
---@nodiscard
---@return integer y ---@return integer y
function public.get_y() function public.get_y()
return protected.frame.y return protected.frame.y
end end
-- get element width -- get element width
---@nodiscard
---@return integer width ---@return integer width
function public.width() function public.width()
return protected.frame.w return protected.frame.w
end end
-- get element height -- get element height
---@nodiscard
---@return integer height ---@return integer height
function public.height() function public.height()
return protected.frame.h return protected.frame.h
end end
-- get the element value -- get the element value
---@nodiscard
---@return any value ---@return any value
function public.get_value() function public.get_value()
return protected.get_value() return protected.get_value()

View File

@ -12,6 +12,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new root display box -- new root display box
---@nodiscard
---@param args displaybox_args ---@param args displaybox_args
local function displaybox(args) local function displaybox(args)
-- create new graphics element base object -- create new graphics element base object

View File

@ -13,6 +13,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new div element -- new div element
---@nodiscard
---@param args div_args ---@param args div_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function div(args) local function div(args)

View File

@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new alarm indicator light -- new alarm indicator light
---@nodiscard
---@param args alarm_indicator_light ---@param args alarm_indicator_light
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function alarm_indicator_light(args) local function alarm_indicator_light(args)

View File

@ -14,6 +14,7 @@ local element = require("graphics.element")
---@field y? integer 1 if omitted ---@field y? integer 1 if omitted
-- new core map box -- new core map box
---@nodiscard
---@param args core_map_args ---@param args core_map_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function core_map(args) local function core_map(args)

View File

@ -19,6 +19,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new data indicator -- new data indicator
---@nodiscard
---@param args data_indicator_args ---@param args data_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function data(args) local function data(args)

View File

@ -17,6 +17,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new horizontal bar -- new horizontal bar
---@nodiscard
---@param args hbar_args ---@param args hbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function hbar(args) local function hbar(args)

View File

@ -20,6 +20,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new icon indicator -- new icon indicator
---@nodiscard
---@param args icon_indicator_args ---@param args icon_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function icon(args) local function icon(args)

View File

@ -18,6 +18,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new indicator light -- new indicator light
---@nodiscard
---@param args indicator_light_args ---@param args indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function indicator_light(args) local function indicator_light(args)

View File

@ -18,6 +18,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new power indicator -- new power indicator
---@nodiscard
---@param args power_indicator_args ---@param args power_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function power(args) local function power(args)

View File

@ -19,6 +19,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new radiation indicator -- new radiation indicator
---@nodiscard
---@param args rad_indicator_args ---@param args rad_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function rad(args) local function rad(args)

View File

@ -20,6 +20,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new state indicator -- new state indicator
---@nodiscard
---@param args state_indicator_args ---@param args state_indicator_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function state_indicator(args) local function state_indicator(args)

View File

@ -20,6 +20,7 @@ local flasher = require("graphics.flasher")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new tri-state indicator light -- new tri-state indicator light
---@nodiscard
---@param args tristate_indicator_light_args ---@param args tristate_indicator_light_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function tristate_indicator_light(args) local function tristate_indicator_light(args)

View File

@ -15,6 +15,7 @@ local element = require("graphics.element")
---@field fg_bg? cpair foreground/background colors ---@field fg_bg? cpair foreground/background colors
-- new vertical bar -- new vertical bar
---@nodiscard
---@param args vbar_args ---@param args vbar_args
---@return graphics_element element, element_id id ---@return graphics_element element, element_id id
local function vbar(args) local function vbar(args)

View File

@ -144,7 +144,7 @@ local function rectangle(args)
e.window.blit(spaces, blit_fg, blit_bg_top_bot) e.window.blit(spaces, blit_fg, blit_bg_top_bot)
end end
else else
if (args.thin == true) then if args.thin == true then
e.window.blit(p_s, blit_fg_sides, blit_bg_sides) e.window.blit(p_s, blit_fg_sides, blit_bg_sides)
else else
e.window.blit(p_s, blit_fg, blit_bg_sides) e.window.blit(p_s, blit_fg, blit_bg_sides)

View File

@ -60,7 +60,7 @@ local function tiling(args)
-- create pattern -- create pattern
for y = start_y, inner_height + (start_y - 1) do for y = start_y, inner_height + (start_y - 1) do
e.window.setCursorPos(start_x, y) e.window.setCursorPos(start_x, y)
for x = 1, inner_width do for _ = 1, inner_width do
if alternator then if alternator then
if even then if even then
e.window.blit(" ", "00", fill_a .. fill_a) 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 registry = { {}, {}, {} } -- one registry table per period
local callback_counter = 0 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 -- this assumes it is called every 250ms, it does no checking of time on its own
local function callback_250ms() local function callback_250ms()
if active then if active then
@ -55,8 +54,7 @@ function flasher.clear()
registry = { {}, {}, {} } registry = { {}, {}, {} }
end 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 -- 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 f function function to call each period
---@param period PERIOD time period option (1, 2, or 3) ---@param period PERIOD time period option (1, 2, or 3)

View File

@ -2,10 +2,10 @@
"versions": { "versions": {
"bootloader": "0.2", "bootloader": "0.2",
"comms": "1.4.0", "comms": "1.4.0",
"reactor-plc": "beta-v0.11.1", "reactor-plc": "v0.12.0",
"rtu": "beta-v0.11.2", "rtu": "v0.12.1",
"supervisor": "beta-v0.12.2", "supervisor": "v0.13.1",
"coordinator": "beta-v0.10.1", "coordinator": "v0.11.0",
"pocket": "alpha-v0.0.0" "pocket": "alpha-v0.0.0"
}, },
"files": { "files": {
@ -177,13 +177,13 @@
}, },
"sizes": { "sizes": {
"system": 1982, "system": 1982,
"common": 88163, "common": 88565,
"graphics": 99360, "graphics": 99858,
"lockbox": 100797, "lockbox": 100797,
"reactor-plc": 75902, "reactor-plc": 75621,
"rtu": 81679, "rtu": 85496,
"supervisor": 268416, "supervisor": 270182,
"coordinator": 181783, "coordinator": 183279,
"pocket": 335 "pocket": 335
} }
} }

View File

@ -1,4 +1,5 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local types = require("scada-common.types") local types = require("scada-common.types")
@ -6,15 +7,17 @@ local util = require("scada-common.util")
local plc = {} local plc = {}
local rps_status_t = types.rps_status_t local RPS_TRIP_CAUSE = types.RPS_TRIP_CAUSE
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPES = comms.DEVICE_TYPES local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local RPLC_TYPES = comms.RPLC_TYPES local RPLC_TYPE = comms.RPLC_TYPE
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local AUTO_ACK = comms.PLC_AUTO_ACK local AUTO_ACK = comms.PLC_AUTO_ACK
local RPS_LIMITS = const.RPS_LIMITS
local print = util.print local print = util.print
local println = util.println local println = util.println
local print_ts = util.print_ts 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_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active." local PCALL_START_MSG = "pcall: Reactor is already active."
-- RPS SAFETY CONSTANTS -- RPS: Reactor Protection System<br>
-- identifies dangerous states and SCRAMs reactor if warranted<br>
local MAX_DAMAGE_PERCENT = 90 -- autonomous from main SCADA supervisor/coordinator control
local MAX_DAMAGE_TEMPERATURE = 1200 ---@nodiscard
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
---@param reactor table ---@param reactor table
---@param is_formed boolean ---@param is_formed boolean
function plc.rps_init(reactor, is_formed) function plc.rps_init(reactor, is_formed)
@ -59,7 +51,6 @@ function plc.rps_init(reactor, is_formed)
} }
local self = { local self = {
reactor = reactor,
state = { false, false, false, false, false, false, false, false, false, false, false, false }, state = { false, false, false, false, false, false, false, false, false, false, false, false },
reactor_enabled = false, reactor_enabled = false,
enabled_at = 0, enabled_at = 0,
@ -69,14 +60,11 @@ function plc.rps_init(reactor, is_formed)
trip_cause = "ok" ---@type rps_trip_cause trip_cause = "ok" ---@type rps_trip_cause
} }
---@class rps
local public = {}
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- set reactor access fault flag -- set reactor access fault flag
local function _set_fault() 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 self.state[state_keys.fault] = true
end end
end end
@ -88,7 +76,7 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is formed -- check if the reactor is formed
local function _is_formed() local function _is_formed()
local formed = self.reactor.isFormed() local formed = reactor.isFormed()
if formed == ppm.ACCESS_FAULT then if formed == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
@ -103,7 +91,7 @@ function plc.rps_init(reactor, is_formed)
-- check if the reactor is force disabled -- check if the reactor is force disabled
local function _is_force_disabled() local function _is_force_disabled()
local disabled = self.reactor.isForceDisabled() local disabled = reactor.isForceDisabled()
if disabled == ppm.ACCESS_FAULT then if disabled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
@ -118,77 +106,80 @@ function plc.rps_init(reactor, is_formed)
-- check for critical damage -- check for critical damage
local function _damage_critical() local function _damage_critical()
local damage_percent = self.reactor.getDamagePercent() local damage_percent = reactor.getDamagePercent()
if damage_percent == ppm.ACCESS_FAULT then if damage_percent == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.dmg_crit] then 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
end end
-- check if the reactor is at a critically high temperature -- check if the reactor is at a critically high temperature
local function _high_temp() local function _high_temp()
-- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200
local temp = self.reactor.getTemperature() local temp = reactor.getTemperature()
if temp == ppm.ACCESS_FAULT then if temp == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.high_temp] then 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
end end
-- check if there is no coolant (<2% filled) -- check if there is no coolant (<2% filled)
local function _no_coolant() local function _no_coolant()
local coolant_filled = self.reactor.getCoolantFilledPercentage() local coolant_filled = reactor.getCoolantFilledPercentage()
if coolant_filled == ppm.ACCESS_FAULT then if coolant_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.no_coolant] then 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
end end
-- check for excess waste (>80% filled) -- check for excess waste (>80% filled)
local function _excess_waste() local function _excess_waste()
local w_filled = self.reactor.getWasteFilledPercentage() local w_filled = reactor.getWasteFilledPercentage()
if w_filled == ppm.ACCESS_FAULT then if w_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.ex_waste] then 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
end end
-- check for heated coolant backup (>95% filled) -- check for heated coolant backup (>95% filled)
local function _excess_heated_coolant() local function _excess_heated_coolant()
local hc_filled = self.reactor.getHeatedCoolantFilledPercentage() local hc_filled = reactor.getHeatedCoolantFilledPercentage()
if hc_filled == ppm.ACCESS_FAULT then if hc_filled == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.ex_hcoolant] then 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
end end
-- check if there is no fuel -- check if there is no fuel
local function _insufficient_fuel() local function _insufficient_fuel()
local fuel = self.reactor.getFuel() local fuel = reactor.getFuelFilledPercentage()
if fuel == ppm.ACCESS_FAULT then if fuel == ppm.ACCESS_FAULT then
-- lost the peripheral or terminated, handled later -- lost the peripheral or terminated, handled later
_set_fault() _set_fault()
elseif not self.state[state_keys.no_fuel] then 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
end end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class rps
local public = {}
-- re-link a reactor after a peripheral re-connect -- re-link a reactor after a peripheral re-connect
---@diagnostic disable-next-line: redefined-local ---@param new_reactor table reconnected reactor
function public.reconnect_reactor(reactor) function public.reconnect_reactor(new_reactor)
self.reactor = reactor reactor = new_reactor
end end
-- trip for lost peripheral -- trip for lost peripheral
@ -222,8 +213,8 @@ function plc.rps_init(reactor, is_formed)
function public.scram() function public.scram()
log.info("RPS: reactor SCRAM") log.info("RPS: reactor SCRAM")
self.reactor.scram() reactor.scram()
if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
log.error("RPS: failed reactor SCRAM") log.error("RPS: failed reactor SCRAM")
return false return false
else else
@ -239,8 +230,8 @@ function plc.rps_init(reactor, is_formed)
if not self.tripped then if not self.tripped then
log.info("RPS: reactor start") log.info("RPS: reactor start")
self.reactor.activate() reactor.activate()
if self.reactor.__p_is_faulted() and (self.reactor.__p_last_fault() ~= PCALL_START_MSG) then if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
log.error("RPS: failed reactor start") log.error("RPS: failed reactor start")
else else
self.reactor_enabled = true self.reactor_enabled = true
@ -260,7 +251,7 @@ function plc.rps_init(reactor, is_formed)
-- clear automatic SCRAM if it was the cause -- clear automatic SCRAM if it was the cause
if self.tripped and self.trip_cause == "automatic" then if self.tripped and self.trip_cause == "automatic" then
self.state[state_keys.automatic] = true self.state[state_keys.automatic] = true
self.trip_cause = rps_status_t.ok self.trip_cause = RPS_TRIP_CAUSE.OK
self.tripped = false self.tripped = false
log.debug("RPS: cleared automatic SCRAM for re-activation") log.debug("RPS: cleared automatic SCRAM for re-activation")
@ -270,9 +261,10 @@ function plc.rps_init(reactor, is_formed)
end end
-- check all safety conditions -- 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() function public.check()
local status = rps_status_t.ok local status = RPS_TRIP_CAUSE.OK
local was_tripped = self.tripped local was_tripped = self.tripped
local first_trip = false local first_trip = false
@ -298,47 +290,47 @@ function plc.rps_init(reactor, is_formed)
status = self.trip_cause status = self.trip_cause
elseif self.state[state_keys.sys_fail] then elseif self.state[state_keys.sys_fail] then
log.warning("RPS: system failure, reactor not formed") 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 elseif self.state[state_keys.force_disabled] then
log.warning("RPS: reactor was force disabled") 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 elseif self.state[state_keys.dmg_crit] then
log.warning("RPS: damage critical") 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 elseif self.state[state_keys.high_temp] then
log.warning("RPS: high temperature") 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 elseif self.state[state_keys.no_coolant] then
log.warning("RPS: no coolant") 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 elseif self.state[state_keys.ex_waste] then
log.warning("RPS: full waste") 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 elseif self.state[state_keys.ex_hcoolant] then
log.warning("RPS: heated coolant backup") 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 elseif self.state[state_keys.no_fuel] then
log.warning("RPS: no fuel") log.warning("RPS: no fuel")
status = rps_status_t.no_fuel status = RPS_TRIP_CAUSE.NO_FUEL
elseif self.state[state_keys.fault] then elseif self.state[state_keys.fault] then
log.warning("RPS: reactor access fault") log.warning("RPS: reactor access fault")
status = rps_status_t.fault status = RPS_TRIP_CAUSE.FAULT
elseif self.state[state_keys.timeout] then elseif self.state[state_keys.timeout] then
log.warning("RPS: supervisor connection timeout") log.warning("RPS: supervisor connection timeout")
status = rps_status_t.timeout status = RPS_TRIP_CAUSE.TIMEOUT
elseif self.state[state_keys.manual] then elseif self.state[state_keys.manual] then
log.warning("RPS: manual SCRAM requested") log.warning("RPS: manual SCRAM requested")
status = rps_status_t.manual status = RPS_TRIP_CAUSE.MANUAL
elseif self.state[state_keys.automatic] then elseif self.state[state_keys.automatic] then
log.warning("RPS: automatic SCRAM requested") log.warning("RPS: automatic SCRAM requested")
status = rps_status_t.automatic status = RPS_TRIP_CAUSE.AUTOMATIC
else else
self.tripped = false self.tripped = false
self.trip_cause = rps_status_t.ok self.trip_cause = RPS_TRIP_CAUSE.OK
end end
-- if a new trip occured... -- 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 first_trip = true
self.tripped = true self.tripped = true
self.trip_cause = status self.trip_cause = status
@ -359,16 +351,23 @@ function plc.rps_init(reactor, is_formed)
return self.tripped, status, first_trip return self.tripped, status, first_trip
end end
---@nodiscard
function public.status() return self.state end function public.status() return self.state end
---@nodiscard
function public.is_tripped() return self.tripped end function public.is_tripped() return self.tripped end
---@nodiscard
function public.get_trip_cause() return self.trip_cause end function public.get_trip_cause() return self.trip_cause end
---@nodiscard
function public.is_active() return self.reactor_enabled end function public.is_active() return self.reactor_enabled end
---@nodiscard
function public.is_formed() return self.formed end function public.is_formed() return self.formed end
---@nodiscard
function public.is_force_disabled() return self.force_disabled end function public.is_force_disabled() return self.force_disabled end
-- get the runtime of the reactor if active, or the last runtime if disabled -- get the runtime of the reactor if active, or the last runtime if disabled
---@nodiscard
---@return integer runtime time since last enable ---@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 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 ---@param quiet? boolean true to suppress the info log message
function public.reset(quiet) function public.reset(quiet)
self.tripped = false self.tripped = false
self.trip_cause = rps_status_t.ok self.trip_cause = RPS_TRIP_CAUSE.OK
for i = 1, #self.state do for i = 1, #self.state do
self.state[i] = false 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.automatic] = false
self.state[state_keys.timeout] = false self.state[state_keys.timeout] = false
if self.trip_cause == rps_status_t.automatic or self.trip_cause == rps_status_t.timeout then if self.trip_cause == RPS_TRIP_CAUSE.AUTOMATIC or self.trip_cause == RPS_TRIP_CAUSE.TIMEOUT then
self.trip_cause = rps_status_t.ok self.trip_cause = RPS_TRIP_CAUSE.OK
self.tripped = false self.tripped = false
log.info("RPS: auto reset") log.info("RPS: auto reset")
@ -402,6 +401,7 @@ function plc.rps_init(reactor, is_formed)
end end
-- Reactor PLC Communications -- Reactor PLC Communications
---@nodiscard
---@param id integer reactor ID ---@param id integer reactor ID
---@param version string PLC version ---@param version string PLC version
---@param modem table modem device ---@param modem table modem device
@ -415,10 +415,6 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
local self = { local self = {
seq_num = 0, seq_num = 0,
r_seq_num = nil, r_seq_num = nil,
modem = modem,
s_port = server_port,
l_port = local_port,
reactor = reactor,
scrammed = false, scrammed = false,
linked = false, linked = false,
last_est_ack = ESTABLISH_ACK.ALLOW, 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 max_burn_rate = nil
} }
---@class plc_comms
local public = {}
comms.set_trusted_range(range) comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
self.modem.closeAll() modem.closeAll()
self.modem.open(self.l_port) modem.open(local_port)
end end
_conf_channels() _conf_channels()
-- send an RPLC packet -- send an RPLC packet
---@param msg_type RPLC_TYPES ---@param msg_type RPLC_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet() local r_pkt = comms.rplc_packet()
r_pkt.make(id, msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES ---@param msg_type SCADA_MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -500,21 +493,21 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
} }
local tasks = { local tasks = {
function () data_table[1] = self.reactor.getStatus() end, function () data_table[1] = reactor.getStatus() end,
function () data_table[2] = self.reactor.getBurnRate() end, function () data_table[2] = reactor.getBurnRate() end,
function () data_table[3] = self.reactor.getActualBurnRate() end, function () data_table[3] = reactor.getActualBurnRate() end,
function () data_table[4] = self.reactor.getTemperature() end, function () data_table[4] = reactor.getTemperature() end,
function () data_table[5] = self.reactor.getDamagePercent() end, function () data_table[5] = reactor.getDamagePercent() end,
function () data_table[6] = self.reactor.getBoilEfficiency() end, function () data_table[6] = reactor.getBoilEfficiency() end,
function () data_table[7] = self.reactor.getEnvironmentalLoss() end, function () data_table[7] = reactor.getEnvironmentalLoss() end,
function () fuel = self.reactor.getFuel() end, function () fuel = reactor.getFuel() end,
function () data_table[9] = self.reactor.getFuelFilledPercentage() end, function () data_table[9] = reactor.getFuelFilledPercentage() end,
function () waste = self.reactor.getWaste() end, function () waste = reactor.getWaste() end,
function () data_table[11] = self.reactor.getWasteFilledPercentage() end, function () data_table[11] = reactor.getWasteFilledPercentage() end,
function () coolant = self.reactor.getCoolant() end, function () coolant = reactor.getCoolant() end,
function () data_table[14] = self.reactor.getCoolantFilledPercentage() end, function () data_table[14] = reactor.getCoolantFilledPercentage() end,
function () hcoolant = self.reactor.getHeatedCoolant() end, function () hcoolant = reactor.getHeatedCoolant() end,
function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end function () data_table[17] = reactor.getHeatedCoolantFilledPercentage() end
} }
parallel.waitForAll(table.unpack(tasks)) 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 data_table[16] = hcoolant.amount
end end
return data_table, self.reactor.__p_is_faulted() return data_table, reactor.__p_is_faulted()
end end
-- update the status cache if changed -- 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 -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) 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 end
-- general ack -- general ack
---@param msg_type RPLC_TYPES ---@param msg_type RPLC_TYPE
---@param status boolean|integer ---@param status boolean|integer
local function _send_ack(msg_type, status) local function _send_ack(msg_type, status)
_send(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 mek_data = { false, 0, 0, 0, min_pos, max_pos, 0, 0, 0, 0, 0, 0, 0, 0 }
local tasks = { local tasks = {
function () mek_data[1] = self.reactor.getLength() end, function () mek_data[1] = reactor.getLength() end,
function () mek_data[2] = self.reactor.getWidth() end, function () mek_data[2] = reactor.getWidth() end,
function () mek_data[3] = self.reactor.getHeight() end, function () mek_data[3] = reactor.getHeight() end,
function () mek_data[4] = self.reactor.getMinPos() end, function () mek_data[4] = reactor.getMinPos() end,
function () mek_data[5] = self.reactor.getMaxPos() end, function () mek_data[5] = reactor.getMaxPos() end,
function () mek_data[6] = self.reactor.getHeatCapacity() end, function () mek_data[6] = reactor.getHeatCapacity() end,
function () mek_data[7] = self.reactor.getFuelAssemblies() end, function () mek_data[7] = reactor.getFuelAssemblies() end,
function () mek_data[8] = self.reactor.getFuelSurfaceArea() end, function () mek_data[8] = reactor.getFuelSurfaceArea() end,
function () mek_data[9] = self.reactor.getFuelCapacity() end, function () mek_data[9] = reactor.getFuelCapacity() end,
function () mek_data[10] = self.reactor.getWasteCapacity() end, function () mek_data[10] = reactor.getWasteCapacity() end,
function () mek_data[11] = self.reactor.getCoolantCapacity() end, function () mek_data[11] = reactor.getCoolantCapacity() end,
function () mek_data[12] = self.reactor.getHeatedCoolantCapacity() end, function () mek_data[12] = reactor.getHeatedCoolantCapacity() end,
function () mek_data[13] = self.reactor.getMaxBurnRate() end function () mek_data[13] = reactor.getMaxBurnRate() end
} }
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
if not self.reactor.__p_is_faulted() then if not reactor.__p_is_faulted() then
_send(RPLC_TYPES.MEK_STRUCT, mek_data) _send(RPLC_TYPE.MEK_STRUCT, mek_data)
self.resend_build = false self.resend_build = false
else else
log.error("failed to send structure: PPM fault") 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 -- -- PUBLIC FUNCTIONS --
---@class plc_comms
local public = {}
-- reconnect a newly connected modem -- reconnect a newly connected modem
---@param modem table ---@param new_modem table
---@diagnostic disable-next-line: redefined-local function public.reconnect_modem(new_modem)
function public.reconnect_modem(modem) modem = new_modem
self.modem = modem
_conf_channels() _conf_channels()
end end
-- reconnect a newly connected reactor -- reconnect a newly connected reactor
---@param reactor table ---@param new_reactor table
---@diagnostic disable-next-line: redefined-local function public.reconnect_reactor(new_reactor)
function public.reconnect_reactor(reactor) reactor = new_reactor
self.reactor = reactor
self.status_cache = nil self.status_cache = nil
self.resend_build = true self.resend_build = true
self.max_burn_rate = nil self.max_burn_rate = nil
@ -643,12 +637,12 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
function public.close() function public.close()
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink() public.unlink()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
end end
-- attempt to establish link with supervisor -- attempt to establish link with supervisor
function public.send_link_req() 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 end
-- send live status information -- 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 mek_data = self.status_cache
end end
heating_rate = self.reactor.getHeatingRate() heating_rate = reactor.getHeatingRate()
end end
local sys_status = { local sys_status = {
@ -677,35 +671,29 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
mek_data -- mekanism status data mek_data -- mekanism status data
} }
_send(RPLC_TYPES.STATUS, sys_status) _send(RPLC_TYPE.STATUS, sys_status)
if self.resend_build then if self.resend_build then _send_struct() end
_send_struct()
end
end end
end end
-- send reactor protection system status -- send reactor protection system status
function public.send_rps_status() function public.send_rps_status()
if self.linked then 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
end end
-- send reactor protection system alarm -- 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) function public.send_rps_alarm(cause)
if self.linked then if self.linked then
local rps_alarm = { _send(RPLC_TYPE.RPS_ALARM, { cause, table.unpack(rps.status()) })
cause,
table.unpack(rps.status())
}
_send(RPLC_TYPES.RPS_ALARM, rps_alarm)
end end
end end
-- parse an RPLC packet -- parse an RPLC packet
---@nodiscard
---@param side string ---@param side string
---@param sender integer ---@param sender integer
---@param reply_to 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 if s_pkt.is_valid() then
-- get as RPLC packet -- get as RPLC packet
if s_pkt.protocol() == PROTOCOLS.RPLC then if s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get() pkt = rplc_pkt.get()
end end
-- get as SCADA management packet -- 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() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() 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 plc_state plc_state PLC state
---@param setpoints setpoints setpoint control table ---@param setpoints setpoints setpoint control table
function public.handle_packet(packet, plc_state, setpoints) 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 -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() 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() local protocol = packet.scada_frame.protocol()
-- handle packet -- handle packet
if protocol == PROTOCOLS.RPLC then if protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
if self.linked then 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 -- request of full status, clear cache first
self.status_cache = nil self.status_cache = nil
public.send_status(plc_state.no_reactor, plc_state.reactor_formed) public.send_status(plc_state.no_reactor, plc_state.reactor_formed)
log.debug("sent out status cache again, did supervisor miss it?") 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 -- request for physical structure
_send_struct() _send_struct()
log.debug("sent out structure again, did supervisor miss it?") 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 -- set the burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false 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 no known max burn rate, check again
if self.max_burn_rate == nil then if self.max_burn_rate == nil then
self.max_burn_rate = self.reactor.getMaxBurnRate() self.max_burn_rate = reactor.getMaxBurnRate()
end end
-- if we know our max burn rate, update current burn rate setpoint if in range -- 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 setpoints.burn_rate = burn_rate
success = true success = true
else else
self.reactor.setBurnRate(burn_rate) reactor.setBurnRate(burn_rate)
success = not self.reactor.__p_is_faulted() success = not reactor.__p_is_faulted()
end end
else else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate) 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 else
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate") log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end end
elseif packet.type == RPLC_TYPES.RPS_ENABLE then elseif packet.type == RPLC_TYPE.RPS_ENABLE then
-- enable the reactor -- enable the reactor
self.scrammed = false self.scrammed = false
_send_ack(packet.type, rps.activate()) _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 -- disable the reactor per manual request
self.scrammed = true self.scrammed = true
rps.trip_manual() rps.trip_manual()
_send_ack(packet.type, true) _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 -- disable the reactor per automatic request
self.scrammed = true self.scrammed = true
rps.trip_auto() rps.trip_auto()
_send_ack(packet.type, true) _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 -- reset the RPS status
rps.reset() rps.reset()
_send_ack(packet.type, true) _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 -- reset automatic SCRAM and timeout trips
rps.auto_reset() rps.auto_reset()
_send_ack(packet.type, true) _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 -- automatic control requested a new burn rate
if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then
local ack = AUTO_ACK.FAIL 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 no known max burn rate, check again
if self.max_burn_rate == nil then if self.max_burn_rate == nil then
self.max_burn_rate = self.reactor.getMaxBurnRate() self.max_burn_rate = reactor.getMaxBurnRate()
end end
-- if we know our max burn rate, update current burn rate setpoint if in range -- 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") log.debug("AUTO: stopping the reactor to meet 0.0 burn rate")
if rps.scram() then if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK ack = AUTO_ACK.ZERO_DIS_OK
self.auto_last_disable = util.time_ms()
else else
log.debug("AUTO: automatic reactor stop failed") log.warning("AUTO: automatic reactor stop failed")
end end
else else
ack = AUTO_ACK.ZERO_DIS_OK 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 -- activate the reactor
log.debug("AUTO: activating the reactor") log.debug("AUTO: activating the reactor")
self.reactor.setBurnRate(0.01) reactor.setBurnRate(0.01)
if self.reactor.__p_is_faulted() then if reactor.__p_is_faulted() then
log.debug("AUTO: failed to reset burn rate for auto activation") log.warning("AUTO: failed to reset burn rate for auto activation")
else else
if not rps.auto_activate() then if not rps.auto_activate() then
log.debug("AUTO: automatic reactor activation failed") log.warning("AUTO: automatic reactor activation failed")
end end
end 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 ack = AUTO_ACK.RAMP_SET_OK
else else
log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate)) log.debug(util.c("AUTO: setting burn rate directly to ", burn_rate))
self.reactor.setBurnRate(burn_rate) reactor.setBurnRate(burn_rate)
ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK) ack = util.trinary(reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end end
end end
else else
@ -898,9 +886,10 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else else
log.debug("discarding RPLC packet before linked") log.debug("discarding RPLC packet before linked")
end end
elseif protocol == PROTOCOLS.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
if self.linked then if self.linked then
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- link request confirmation -- link request confirmation
if packet.length == 1 then if packet.length == 1 then
log.debug("received unsolicited establish response") log.debug("received unsolicited establish response")
@ -933,7 +922,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else else
log.debug("SCADA_MGMT establish packet length mismatch") log.debug("SCADA_MGMT establish packet length mismatch")
end 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 -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -949,7 +938,7 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else else
log.debug("SCADA_MGMT keep alive packet length/type mismatch") log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- handle session close -- handle session close
conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink() public.unlink()
@ -958,14 +947,14 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
else else
log.warning("received unsupported SCADA_MGMT packet type " .. packet.type) log.warning("received unsupported SCADA_MGMT packet type " .. packet.type)
end end
elseif packet.type == SCADA_MGMT_TYPES.ESTABLISH then elseif packet.type == SCADA_MGMT_TYPE.ESTABLISH then
-- link request confirmation -- link request confirmation
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] local est_ack = packet.data[1]
if est_ack == ESTABLISH_ACK.ALLOW then if est_ack == ESTABLISH_ACK.ALLOW then
println_ts("linked!") println_ts("linked!")
log.debug("supervisor establish request approved") log.info("supervisor establish request approved, PLC is linked")
-- reset remote sequence number and cache -- reset remote sequence number and cache
self.r_seq_num = nil 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 elseif self.last_est_ack ~= est_ack then
if est_ack == ESTABLISH_ACK.DENY then if est_ack == ESTABLISH_ACK.DENY then
println_ts("link request denied, retrying...") 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 elseif est_ack == ESTABLISH_ACK.COLLISION then
println_ts("reactor PLC ID collision (check config), retrying...") 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 elseif est_ack == ESTABLISH_ACK.BAD_VERSION then
println_ts("supervisor version mismatch (try updating), retrying...") println_ts("supervisor version mismatch (try updating), retrying...")
log.warning("establish request version mismatch") log.warning("establish request version mismatch, retrying")
else else
println_ts("invalid link response, bad channel? retrying...") println_ts("invalid link response, bad channel? retrying...")
log.error("unknown establish request response") log.error("unknown establish request response, retrying")
end end
end end
@ -1006,7 +995,9 @@ function plc.comms(id, version, modem, local_port, server_port, range, reactor,
end end
end end
---@nodiscard
function public.is_scrammed() return self.scrammed end function public.is_scrammed() return self.scrammed end
---@nodiscard
function public.is_linked() return self.linked end function public.is_linked() return self.linked end
return public return public

View File

@ -14,7 +14,7 @@ local config = require("reactor-plc.config")
local plc = require("reactor-plc.plc") local plc = require("reactor-plc.plc")
local threads = require("reactor-plc.threads") 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 print = util.print
local println = util.println 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 -- we need a reactor, can at least do some things even if it isn't formed though
if smem_dev.reactor == nil then if smem_dev.reactor == nil then
println("boot> fission reactor not found"); println("init> fission reactor not found");
log.warning("no reactor on startup") log.warning("init> no reactor on startup")
plc_state.init_ok = false plc_state.init_ok = false
plc_state.degraded = true plc_state.degraded = true
plc_state.no_reactor = true plc_state.no_reactor = true
elseif not smem_dev.reactor.isFormed() then elseif not smem_dev.reactor.isFormed() then
println("boot> fission reactor not formed"); println("init> fission reactor not formed");
log.warning("reactor logic adapter present, but reactor is not formed") log.warning("init> reactor logic adapter present, but reactor is not formed")
plc_state.degraded = true plc_state.degraded = true
plc_state.reactor_formed = false plc_state.reactor_formed = false
@ -132,8 +132,8 @@ local function main()
-- modem is required if networked -- modem is required if networked
if __shared_memory.networked and smem_dev.modem == nil then if __shared_memory.networked and smem_dev.modem == nil then
println("boot> wireless modem not found") println("init> wireless modem not found")
log.warning("no wireless modem on startup") log.warning("init> no wireless modem on startup")
-- scram reactor if present and enabled -- scram reactor if present and enabled
if (smem_dev.reactor ~= nil) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then 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 plc_state.no_modem = true
end end
-- PLC init -- PLC init<br>
---
--- EVENT_CONSUMER: this function consumes events --- EVENT_CONSUMER: this function consumes events
local function init() local function init()
if plc_state.init_ok then 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) config.TRUSTED_RANGE, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog)
log.debug("init> comms init") log.debug("init> comms init")
else else
println("boot> starting in offline mode") println("init> starting in offline mode")
log.debug("init> running without networking") log.info("init> running without networking")
end end
---@diagnostic disable-next-line: param-type-mismatch
util.push_event("clock_start") util.push_event("clock_start")
println("boot> completed") println("init> completed")
log.debug("init> boot completed") log.info("init> startup completed")
else else
println("boot> system in degraded state, awaiting devices...") println("init> system in degraded state, awaiting devices...")
log.warning("init> booted in a degraded state, awaiting peripheral connections...") log.warning("init> started in a degraded state, awaiting peripheral connections...")
end end
end end

View File

@ -28,10 +28,12 @@ local MQ__COMM_CMD = {
} }
-- main thread -- main thread
---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
---@param init function ---@param init function
function threads.thread__main(smem, init) function threads.thread__main(smem, init)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -266,7 +268,6 @@ function threads.thread__main(smem, init)
-- this thread cannot be slept because it will miss events (namely "terminate" otherwise) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise)
if not plc_state.shutdown then if not plc_state.shutdown then
log.info("main thread restarting now...") log.info("main thread restarting now...")
---@diagnostic disable-next-line: param-type-mismatch
util.push_event("clock_start") util.push_event("clock_start")
end end
end end
@ -276,9 +277,11 @@ function threads.thread__main(smem, init)
end end
-- RPS operation thread -- RPS operation thread
---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__rps(smem) function threads.thread__rps(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -415,9 +418,11 @@ function threads.thread__rps(smem)
end end
-- communications sender thread -- communications sender thread
---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__comms_tx(smem) function threads.thread__comms_tx(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -489,9 +494,11 @@ function threads.thread__comms_tx(smem)
end end
-- communications handler thread -- communications handler thread
---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__comms_rx(smem) function threads.thread__comms_rx(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -562,10 +569,12 @@ function threads.thread__comms_rx(smem)
return public return public
end end
-- apply setpoints -- ramp control outputs to desired setpoints
---@nodiscard
---@param smem plc_shared_memory ---@param smem plc_shared_memory
function threads.thread__setpoint_control(smem) function threads.thread__setpoint_control(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()

View File

@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local boilerv_rtu = {} local boilerv_rtu = {}
-- create new boiler (mek 10.1+) device -- create new boiler (mek 10.1+) device
---@nodiscard
---@param boiler table ---@param boiler table
function boilerv_rtu.new(boiler) function boilerv_rtu.new(boiler)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local envd_rtu = {} local envd_rtu = {}
-- create new environment detector device -- create new environment detector device
---@nodiscard
---@param envd table ---@param envd table
function envd_rtu.new(envd) function envd_rtu.new(envd)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local imatrix_rtu = {} local imatrix_rtu = {}
-- create new induction matrix (mek 10.1+) device -- create new induction matrix (mek 10.1+) device
---@nodiscard
---@param imatrix table ---@param imatrix table
function imatrix_rtu.new(imatrix) function imatrix_rtu.new(imatrix)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -1,7 +1,7 @@
local rtu = require("rtu.rtu")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local rtu = require("rtu.rtu")
local redstone_rtu = {} local redstone_rtu = {}
local IO_LVL = rsio.IO_LVL local IO_LVL = rsio.IO_LVL
@ -10,14 +10,15 @@ local digital_read = rsio.digital_read
local digital_write = rsio.digital_write local digital_write = rsio.digital_write
-- create new redstone device -- create new redstone device
---@nodiscard
function redstone_rtu.new() function redstone_rtu.new()
local unit = rtu.init_unit() local unit = rtu.init_unit()
-- get RTU interface -- get RTU interface
local interface = unit.interface() local interface = unit.interface()
-- extends rtu_device; fields added manually to please Lua diagnostics
---@class rtu_rs_device ---@class rtu_rs_device
--- extends rtu_device; fields added manually to please Lua diagnostics
local public = { local public = {
io_count = interface.io_count, io_count = interface.io_count,
read_coil = interface.read_coil, read_coil = interface.read_coil,

View File

@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
local sna_rtu = {} local sna_rtu = {}
-- create new solar neutron activator (sna) device -- create new solar neutron activator (SNA) device
---@nodiscard
---@param sna table ---@param sna table
function sna_rtu.new(sna) function sna_rtu.new(sna)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -2,7 +2,8 @@ local rtu = require("rtu.rtu")
local sps_rtu = {} local sps_rtu = {}
-- create new super-critical phase shifter (sps) device -- create new super-critical phase shifter (SPS) device
---@nodiscard
---@param sps table ---@param sps table
function sps_rtu.new(sps) function sps_rtu.new(sps)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -3,6 +3,7 @@ local rtu = require("rtu.rtu")
local turbinev_rtu = {} local turbinev_rtu = {}
-- create new turbine (mek 10.1+) device -- create new turbine (mek 10.1+) device
---@nodiscard
---@param turbine table ---@param turbine table
function turbinev_rtu.new(turbine) function turbinev_rtu.new(turbine)
local unit = rtu.init_unit() local unit = rtu.init_unit()

View File

@ -7,22 +7,15 @@ local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE local MODBUS_EXCODE = types.MODBUS_EXCODE
-- new modbus comms handler object -- new modbus comms handler object
---@nodiscard
---@param rtu_dev rtu_device|rtu_rs_device RTU device ---@param rtu_dev rtu_device|rtu_rs_device RTU device
---@param use_parallel_read boolean whether or not to use parallel calls when reading ---@param use_parallel_read boolean whether or not to use parallel calls when reading
function modbus.new(rtu_dev, use_parallel_read) 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 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 -- returns a table of readings or a MODBUS_EXCODE error code
---@nodiscard
---@param c_addr_start integer ---@param c_addr_start integer
---@param count integer ---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings ---@return boolean ok, table|MODBUS_EXCODE readings
@ -30,20 +23,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {} local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false 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) local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = c_addr_start + i - 1 local addr = c_addr_start + i - 1
if self.use_parallel then if use_parallel_read then
insert(tasks, function () 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 if fault then access_fault = true else readings[i] = reading end
end) end)
else else
readings[i], access_fault = self.rtu.read_coil(addr) readings[i], access_fault = rtu_dev.read_coil(addr)
if access_fault then if access_fault then
return_ok = false return_ok = false
@ -54,7 +47,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- run parallel tasks if configured -- run parallel tasks if configured
if self.use_parallel then if use_parallel_read then
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
end end
@ -69,9 +62,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings return return_ok, readings
end 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 -- returns a table of readings or a MODBUS_EXCODE error code
---@nodiscard
---@param di_addr_start integer ---@param di_addr_start integer
---@param count integer ---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings ---@return boolean ok, table|MODBUS_EXCODE readings
@ -79,20 +72,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {} local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false 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) local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = di_addr_start + i - 1 local addr = di_addr_start + i - 1
if self.use_parallel then if use_parallel_read then
insert(tasks, function () 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 if fault then access_fault = true else readings[i] = reading end
end) end)
else else
readings[i], access_fault = self.rtu.read_di(addr) readings[i], access_fault = rtu_dev.read_di(addr)
if access_fault then if access_fault then
return_ok = false return_ok = false
@ -103,7 +96,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- run parallel tasks if configured -- run parallel tasks if configured
if self.use_parallel then if use_parallel_read then
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
end end
@ -118,9 +111,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings return return_ok, readings
end 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 -- returns a table of readings or a MODBUS_EXCODE error code
---@nodiscard
---@param hr_addr_start integer ---@param hr_addr_start integer
---@param count integer ---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings ---@return boolean ok, table|MODBUS_EXCODE readings
@ -128,20 +121,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {} local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false 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) local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = hr_addr_start + i - 1 local addr = hr_addr_start + i - 1
if self.use_parallel then if use_parallel_read then
insert(tasks, function () 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 if fault then access_fault = true else readings[i] = reading end
end) end)
else 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 if access_fault then
return_ok = false return_ok = false
@ -152,7 +145,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- run parallel tasks if configured -- run parallel tasks if configured
if self.use_parallel then if use_parallel_read then
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
end end
@ -167,9 +160,9 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, readings return return_ok, readings
end 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 -- returns a table of readings or a MODBUS_EXCODE error code
---@nodiscard
---@param ir_addr_start integer ---@param ir_addr_start integer
---@param count integer ---@param count integer
---@return boolean ok, table|MODBUS_EXCODE readings ---@return boolean ok, table|MODBUS_EXCODE readings
@ -177,20 +170,20 @@ function modbus.new(rtu_dev, use_parallel_read)
local tasks = {} local tasks = {}
local readings = {} ---@type table|MODBUS_EXCODE local readings = {} ---@type table|MODBUS_EXCODE
local access_fault = false 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) local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = ir_addr_start + i - 1 local addr = ir_addr_start + i - 1
if self.use_parallel then if use_parallel_read then
insert(tasks, function () 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 if fault then access_fault = true else readings[i] = reading end
end) end)
else 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 if access_fault then
return_ok = false return_ok = false
@ -201,7 +194,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- run parallel tasks if configured -- run parallel tasks if configured
if self.use_parallel then if use_parallel_read then
parallel.waitForAll(table.unpack(tasks)) parallel.waitForAll(table.unpack(tasks))
end end
@ -217,16 +210,17 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- write a single coil (digital output) -- write a single coil (digital output)
---@nodiscard
---@param c_addr integer ---@param c_addr integer
---@param value any ---@param value any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _5_write_single_coil(c_addr, value) local function _5_write_single_coil(c_addr, value)
local response = nil local response = nil
local _, coils, _, _ = self.rtu.io_count() local _, coils, _, _ = rtu_dev.io_count()
local return_ok = c_addr <= coils local return_ok = c_addr <= coils
if return_ok then 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 if access_fault then
return_ok = false return_ok = false
@ -240,16 +234,17 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- write a single holding register (analog output) -- write a single holding register (analog output)
---@nodiscard
---@param hr_addr integer ---@param hr_addr integer
---@param value any ---@param value any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _6_write_single_holding_register(hr_addr, value) local function _6_write_single_holding_register(hr_addr, value)
local response = nil local response = nil
local _, _, _, hold_regs = self.rtu.io_count() local _, _, _, hold_regs = rtu_dev.io_count()
local return_ok = hr_addr <= hold_regs local return_ok = hr_addr <= hold_regs
if return_ok then 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 if access_fault then
return_ok = false return_ok = false
@ -263,19 +258,20 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- write multiple coils (digital outputs) -- write multiple coils (digital outputs)
---@nodiscard
---@param c_addr_start integer ---@param c_addr_start integer
---@param values any ---@param values any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _15_write_multiple_coils(c_addr_start, values) local function _15_write_multiple_coils(c_addr_start, values)
local response = nil local response = nil
local _, coils, _, _ = self.rtu.io_count() local _, coils, _, _ = rtu_dev.io_count()
local count = #values local count = #values
local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = c_addr_start + i - 1 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 if access_fault then
return_ok = false return_ok = false
@ -291,19 +287,20 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- write multiple holding registers (analog outputs) -- write multiple holding registers (analog outputs)
---@nodiscard
---@param hr_addr_start integer ---@param hr_addr_start integer
---@param values any ---@param values any
---@return boolean ok, MODBUS_EXCODE ---@return boolean ok, MODBUS_EXCODE
local function _16_write_multiple_holding_registers(hr_addr_start, values) local function _16_write_multiple_holding_registers(hr_addr_start, values)
local response = nil local response = nil
local _, _, _, hold_regs = self.rtu.io_count() local _, _, _, hold_regs = rtu_dev.io_count()
local count = #values local count = #values
local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0)
if return_ok then if return_ok then
for i = 1, count do for i = 1, count do
local addr = hr_addr_start + i - 1 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 if access_fault then
return_ok = false return_ok = false
@ -318,7 +315,11 @@ function modbus.new(rtu_dev, use_parallel_read)
return return_ok, response return return_ok, response
end end
---@class modbus
local public = {}
-- validate a request without actually executing it -- validate a request without actually executing it
---@nodiscard
---@param packet modbus_frame ---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply ---@return boolean return_code, modbus_packet reply
function public.check_request(packet) function public.check_request(packet)
@ -360,6 +361,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- handle a MODBUS TCP packet and generate a reply -- handle a MODBUS TCP packet and generate a reply
---@nodiscard
---@param packet modbus_frame ---@param packet modbus_frame
---@return boolean return_code, modbus_packet reply ---@return boolean return_code, modbus_packet reply
function public.handle_packet(packet) function public.handle_packet(packet)
@ -420,6 +422,7 @@ function modbus.new(rtu_dev, use_parallel_read)
end end
-- return a SERVER_DEVICE_BUSY error reply -- return a SERVER_DEVICE_BUSY error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__srv_device_busy(packet) function modbus.reply__srv_device_busy(packet)
@ -432,6 +435,7 @@ function modbus.reply__srv_device_busy(packet)
end end
-- return a NEG_ACKNOWLEDGE error reply -- return a NEG_ACKNOWLEDGE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__neg_ack(packet) function modbus.reply__neg_ack(packet)
@ -444,6 +448,7 @@ function modbus.reply__neg_ack(packet)
end end
-- return a GATEWAY_PATH_UNAVAILABLE error reply -- return a GATEWAY_PATH_UNAVAILABLE error reply
---@nodiscard
---@param packet modbus_frame MODBUS packet frame ---@param packet modbus_frame MODBUS packet frame
---@return modbus_packet reply ---@return modbus_packet reply
function modbus.reply__gw_unavailable(packet) function modbus.reply__gw_unavailable(packet)

View File

@ -1,24 +1,26 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local ppm = require("scada-common.ppm") local ppm = require("scada-common.ppm")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local modbus = require("rtu.modbus") local modbus = require("rtu.modbus")
local rtu = {} local rtu = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPES = comms.DEVICE_TYPES local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print local print = util.print
local println = util.println local println = util.println
local print_ts = util.print_ts local print_ts = util.print_ts
local println_ts = util.println_ts local println_ts = util.println_ts
-- create a new RTU -- create a new RTU unit
---@nodiscard
function rtu.init_unit() function rtu.init_unit()
local self = { local self = {
discrete_inputs = {}, discrete_inputs = {},
@ -152,14 +154,13 @@ function rtu.init_unit()
-- public RTU device access -- public RTU device access
-- get the public interface to this RTU -- get the public interface to this RTU
function protected.interface() function protected.interface() return public end
return public
end
return protected return protected
end end
-- RTU Communications -- RTU Communications
---@nodiscard
---@param version string RTU version ---@param version string RTU version
---@param modem table modem device ---@param modem table modem device
---@param local_port integer local listening port ---@param local_port integer local listening port
@ -168,20 +169,12 @@ end
---@param conn_watchdog watchdog watchdog reference ---@param conn_watchdog watchdog watchdog reference
function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog) function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog)
local self = { local self = {
version = version,
seq_num = 0, seq_num = 0,
r_seq_num = nil, r_seq_num = nil,
txn_id = 0, txn_id = 0,
modem = modem,
s_port = server_port,
l_port = local_port,
conn_watchdog = conn_watchdog,
last_est_ack = ESTABLISH_ACK.ALLOW last_est_ack = ESTABLISH_ACK.ALLOW
} }
---@class rtu_comms
local public = {}
local insert = table.insert local insert = table.insert
comms.set_trusted_range(range) comms.set_trusted_range(range)
@ -190,50 +183,46 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
self.modem.closeAll() modem.closeAll()
self.modem.open(self.l_port) modem.open(local_port)
end end
_conf_channels() _conf_channels()
-- send a scada management packet -- send a scada management packet
---@param msg_type SCADA_MGMT_TYPES ---@param msg_type SCADA_MGMT_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
-- keep alive ack -- keep alive ack
---@param srv_time integer ---@param srv_time integer
local function _send_keep_alive_ack(srv_time) 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 end
-- generate device advertisement table -- generate device advertisement table
---@nodiscard
---@param units table ---@param units table
---@return table advertisement ---@return table advertisement
local function _generate_advertisement(units) local function _generate_advertisement(units)
local advertisement = {} local advertisement = {}
for i = 1, #units do for i = 1, #units do
local unit = units[i] --@type rtu_unit_registry_entry local unit = units[i] ---@type rtu_unit_registry_entry
local type = comms.rtu_t_to_unit_type(unit.type)
if type ~= nil then if unit.type ~= nil then
local advert = { local advert = { unit.type, unit.index, unit.reactor }
type,
unit.index,
unit.reactor
}
if type == RTU_UNIT_TYPES.REDSTONE then if unit.type == RTU_UNIT_TYPE.REDSTONE then
insert(advert, unit.device) insert(advert, unit.device)
end end
@ -246,20 +235,22 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class rtu_comms
local public = {}
-- send a MODBUS TCP packet -- send a MODBUS TCP packet
---@param m_pkt modbus_packet ---@param m_pkt modbus_packet
function public.send_modbus(m_pkt) function public.send_modbus(m_pkt)
local s_pkt = comms.scada_packet() 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.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 self.seq_num = self.seq_num + 1
end end
-- reconnect a newly connected modem -- reconnect a newly connected modem
---@param modem table ---@param new_modem table
---@diagnostic disable-next-line: redefined-local function public.reconnect_modem(new_modem)
function public.reconnect_modem(modem) modem = new_modem
self.modem = modem
_conf_channels() _conf_channels()
end end
@ -273,30 +264,31 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
-- close the connection to the server -- close the connection to the server
---@param rtu_state rtu_state ---@param rtu_state rtu_state
function public.close(rtu_state) function public.close(rtu_state)
self.conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink(rtu_state) public.unlink(rtu_state)
_send(SCADA_MGMT_TYPES.CLOSE, {}) _send(SCADA_MGMT_TYPE.CLOSE, {})
end end
-- send establish request (includes advertisement) -- send establish request (includes advertisement)
---@param units table ---@param units table
function public.send_establish(units) 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 end
-- send capability advertisement -- send capability advertisement
---@param units table ---@param units table
function public.send_advertisement(units) function public.send_advertisement(units)
_send(SCADA_MGMT_TYPES.RTU_ADVERT, _generate_advertisement(units)) _send(SCADA_MGMT_TYPE.RTU_ADVERT, _generate_advertisement(units))
end end
-- notify that a peripheral was remounted -- notify that a peripheral was remounted
---@param unit_index integer RTU unit ID ---@param unit_index integer RTU unit ID
function public.send_remounted(unit_index) 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 end
-- parse a MODBUS/SCADA packet -- parse a MODBUS/SCADA packet
---@nodiscard
---@param side string ---@param side string
---@param sender integer ---@param sender integer
---@param reply_to 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 if s_pkt.is_valid() then
-- get as MODBUS TCP packet -- 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() local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then if m_pkt.decode(s_pkt) then
pkt = m_pkt.get() pkt = m_pkt.get()
end end
-- get as SCADA management packet -- 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() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() 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 -- handle a MODBUS/SCADA packet
---@param packet modbus_frame|mgmt_frame ---@param packet modbus_frame|mgmt_frame
---@param units table ---@param units table RTU units
---@param rtu_state rtu_state ---@param rtu_state rtu_state
function public.handle_packet(packet, units, 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 -- check sequence number
if self.r_seq_num == nil then if self.r_seq_num == nil then
self.r_seq_num = packet.scada_frame.seq_num() 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 end
-- feed watchdog on valid sequence number -- feed watchdog on valid sequence number
self.conn_watchdog.feed() conn_watchdog.feed()
local protocol = packet.scada_frame.protocol() 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 if rtu_state.linked then
local return_code = false local return_code = false
---@diagnostic disable-next-line: param-type-mismatch
local reply = modbus.reply__neg_ack(packet) local reply = modbus.reply__neg_ack(packet)
-- handle MODBUS instruction -- 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 if unit.name == "redstone_io" then
-- immediately execute redstone RTU requests -- immediately execute redstone RTU requests
---@diagnostic disable-next-line: param-type-mismatch
return_code, reply = unit.modbus_io.handle_packet(packet) return_code, reply = unit.modbus_io.handle_packet(packet)
if not return_code then if not return_code then
log.warning("requested MODBUS operation failed" .. unit_dbg_tag) log.warning("requested MODBUS operation failed" .. unit_dbg_tag)
end end
else else
-- check validity then pass off to unit comms thread -- 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) return_code, reply = unit.modbus_io.check_request(packet)
if return_code then if return_code then
-- check if there are more than 3 active transactions -- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem -- still queue the packet, but this may indicate a problem
if unit.pkt_queue.length() > 3 then if unit.pkt_queue.length() > 3 then
---@diagnostic disable-next-line: param-type-mismatch
reply = modbus.reply__srv_device_busy(packet) reply = modbus.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() .. log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag) " 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 end
else else
-- unit ID out of range? -- unit ID out of range?
---@diagnostic disable-next-line: param-type-mismatch
reply = modbus.reply__gw_unavailable(packet) reply = modbus.reply__gw_unavailable(packet)
log.error("received MODBUS packet for non-existent unit") log.error("received MODBUS packet for non-existent unit")
end end
@ -401,9 +389,10 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
else else
log.debug("discarding MODBUS packet before linked") log.debug("discarding MODBUS packet before linked")
end end
elseif protocol == PROTOCOLS.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- SCADA management packet -- SCADA management packet
if packet.type == SCADA_MGMT_TYPES.ESTABLISH then if packet.type == SCADA_MGMT_TYPE.ESTABLISH then
if packet.length == 1 then if packet.length == 1 then
local est_ack = packet.data[1] 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 if est_ack == ESTABLISH_ACK.BAD_VERSION then
-- version mismatch -- version mismatch
println_ts("supervisor comms version mismatch (try updating), retrying...") 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 else
println_ts("supervisor connection denied, retrying...") println_ts("supervisor connection denied, retrying...")
log.warning("supervisor connection denied") log.warning("supervisor connection denied, retrying")
end end
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") log.debug("SCADA_MGMT establish packet length mismatch")
end end
elseif rtu_state.linked then 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 -- keep alive request received, echo back
if packet.length == 1 and type(packet.data[1]) == "number" then if packet.length == 1 and type(packet.data[1]) == "number" then
local timestamp = packet.data[1] local timestamp = packet.data[1]
@ -450,13 +439,13 @@ function rtu.comms(version, modem, local_port, server_port, range, conn_watchdog
else else
log.debug("SCADA_MGMT keep alive packet length/type mismatch") log.debug("SCADA_MGMT keep alive packet length/type mismatch")
end end
elseif packet.type == SCADA_MGMT_TYPES.CLOSE then elseif packet.type == SCADA_MGMT_TYPE.CLOSE then
-- close connection -- close connection
self.conn_watchdog.cancel() conn_watchdog.cancel()
public.unlink(rtu_state) public.unlink(rtu_state)
println_ts("server connection closed by remote host") println_ts("server connection closed by remote host")
log.warning("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 -- request for capabilities again
public.send_advertisement(units) public.send_advertisement(units)
else else

View File

@ -25,9 +25,9 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu") local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_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 print = util.print
local println = util.println local println = util.println
@ -132,13 +132,17 @@ local function main()
-- CHECK: reactor ID must be >= to 1 -- CHECK: reactor ID must be >= to 1
if (not util.is_int(io_reactor)) or (io_reactor < 0) then 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 return false
end end
-- CHECK: io table exists -- CHECK: io table exists
if type(io_table) ~= "table" then 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 return false
end end
@ -148,10 +152,10 @@ local function main()
local continue = true local continue = true
-- check for duplicate entries -- CHECK: no duplicate entries
for i = 1, #units do for i = 1, #units do
local unit = units[i] ---@type rtu_unit_registry_entry 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 -- duplicate entry
local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor, local message = util.c("configure> skipping definition block #", entry_idx, " for reactor ", io_reactor,
" with already defined redstone I/O") " 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, local message = util.c("configure> invalid redstone definition at index ", i, " in definition block #", entry_idx,
" (for reactor ", io_reactor, ")") " (for reactor ", io_reactor, ")")
println(message) println(message)
log.error(message) log.fatal(message)
return false return false
else else
-- link redstone in RTU -- link redstone in RTU
@ -224,23 +228,28 @@ local function main()
---@class rtu_unit_registry_entry ---@class rtu_unit_registry_entry
local unit = { local unit = {
uid = 0, uid = 0, ---@type integer
name = "redstone_io", name = "redstone_io", ---@type string
type = rtu_t.redstone, type = RTU_UNIT_TYPE.REDSTONE, ---@type RTU_UNIT_TYPE
index = entry_idx, index = entry_idx, ---@type integer
reactor = io_reactor, reactor = io_reactor, ---@type integer
device = capabilities, -- use device field for redstone ports device = capabilities, ---@type table use device field for redstone ports
is_multiblock = false, is_multiblock = false, ---@type boolean
formed = nil, ---@type boolean|nil formed = nil, ---@type boolean|nil
rtu = rs_rtu, ---@type rtu_device|rtu_rs_device rtu = rs_rtu, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rs_rtu, false), modbus_io = modbus.new(rs_rtu, false),
pkt_queue = nil, ---@type mqueue|nil pkt_queue = nil, ---@type mqueue|nil
thread = nil thread = nil ---@type parallel_thread|nil
} }
table.insert(units, unit) 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 unit.uid = #units
end end
@ -254,27 +263,33 @@ local function main()
-- CHECK: name is a string -- CHECK: name is a string
if type(name) ~= "string" then 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 return false
end end
-- CHECK: index is an integer >= 1 -- CHECK: index is an integer >= 1
if (not util.is_int(index)) or (index <= 0) then 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 return false
end end
-- CHECK: reactor is an integer >= 0 -- CHECK: reactor is an integer >= 0
if (not util.is_int(for_reactor)) or (for_reactor < 0) then 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 return false
end end
local device = ppm.get_periph(name) local device = ppm.get_periph(name)
local type = nil local type = nil ---@type string|nil
local rtu_iface = nil ---@type rtu_device local rtu_iface = nil ---@type rtu_device
local rtu_type = "" local rtu_type = nil ---@type RTU_UNIT_TYPE
local is_multiblock = false local is_multiblock = false
local formed = nil ---@type boolean|nil local formed = nil ---@type boolean|nil
@ -291,7 +306,7 @@ local function main()
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
rtu_type = rtu_t.boiler_valve rtu_type = RTU_UNIT_TYPE.BOILER_VALVE
rtu_iface = boilerv_rtu.new(device) rtu_iface = boilerv_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -303,7 +318,7 @@ local function main()
end end
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
rtu_type = rtu_t.turbine_valve rtu_type = RTU_UNIT_TYPE.TURBINE_VALVE
rtu_iface = turbinev_rtu.new(device) rtu_iface = turbinev_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -315,7 +330,7 @@ local function main()
end end
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
rtu_type = rtu_t.induction_matrix rtu_type = RTU_UNIT_TYPE.IMATRIX
rtu_iface = imatrix_rtu.new(device) rtu_iface = imatrix_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -327,7 +342,7 @@ local function main()
end end
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
rtu_type = rtu_t.sps rtu_type = RTU_UNIT_TYPE.SPS
rtu_iface = sps_rtu.new(device) rtu_iface = sps_rtu.new(device)
is_multiblock = true is_multiblock = true
formed = device.isFormed() formed = device.isFormed()
@ -339,15 +354,15 @@ local function main()
end end
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
rtu_type = rtu_t.sna rtu_type = RTU_UNIT_TYPE.SNA
rtu_iface = sna_rtu.new(device) rtu_iface = sna_rtu.new(device)
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
rtu_type = rtu_t.env_detector rtu_type = RTU_UNIT_TYPE.ENV_DETECTOR
rtu_iface = envd_rtu.new(device) rtu_iface = envd_rtu.new(device)
elseif type == ppm.VIRTUAL_DEVICE_TYPE then elseif type == ppm.VIRTUAL_DEVICE_TYPE then
-- placeholder device -- placeholder device
rtu_type = "virtual" rtu_type = RTU_UNIT_TYPE.VIRTUAL
rtu_iface = rtu.init_unit().interface() rtu_iface = rtu.init_unit().interface()
else else
local message = util.c("configure> device '", name, "' is not a known type (", type, ")") 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 ---@class rtu_unit_registry_entry
local rtu_unit = { local rtu_unit = {
uid = 0, uid = 0, ---@type integer
name = name, name = name, ---@type string
type = rtu_type, type = rtu_type, ---@type RTU_UNIT_TYPE
index = index, index = index, ---@type integer
reactor = for_reactor, reactor = for_reactor, ---@type integer
device = device, device = device, ---@type table
is_multiblock = is_multiblock, is_multiblock = is_multiblock, ---@type boolean
formed = formed, ---@type boolean|nil formed = formed, ---@type boolean|nil
rtu = rtu_iface, ---@type rtu_device|rtu_rs_device rtu = rtu_iface, ---@type rtu_device|rtu_rs_device
modbus_io = modbus.new(rtu_iface, true), modbus_io = modbus.new(rtu_iface, true),
pkt_queue = mqueue.new(), ---@type mqueue|nil 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) rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit)
@ -377,7 +392,7 @@ local function main()
table.insert(units, rtu_unit) table.insert(units, rtu_unit)
if is_multiblock and not formed then 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 end
local for_message = "facility" local for_message = "facility"
@ -385,7 +400,7 @@ local function main()
for_message = util.c("reactor ", for_reactor) for_message = util.c("reactor ", for_reactor)
end 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 rtu_unit.uid = #units
end end
@ -403,12 +418,12 @@ local function main()
if configure() then if configure() then
-- start connection watchdog -- start connection watchdog
smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT) smem_sys.conn_watchdog = util.new_watchdog(config.COMMS_TIMEOUT)
log.debug("boot> conn watchdog started") log.debug("startup> conn watchdog started")
-- setup comms -- setup comms
smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT,
config.TRUSTED_RANGE, smem_sys.conn_watchdog) config.TRUSTED_RANGE, smem_sys.conn_watchdog)
log.debug("boot> comms init") log.debug("startup> comms init")
-- init threads -- init threads
local main_thread = threads.thread__main(__shared_memory) local main_thread = threads.thread__main(__shared_memory)
@ -422,6 +437,8 @@ local function main()
end end
end end
log.info("startup> completed")
-- run threads -- run threads
parallel.waitForAll(table.unpack(_threads)) parallel.waitForAll(table.unpack(_threads))
else else

View File

@ -15,7 +15,7 @@ local modbus = require("rtu.modbus")
local threads = {} local threads = {}
local rtu_t = types.rtu_t local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -26,9 +26,11 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks)
local COMMS_SLEEP = 100 -- (100ms, 2 ticks) local COMMS_SLEEP = 100 -- (100ms, 2 ticks)
-- main thread -- main thread
---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
function threads.thread__main(smem) function threads.thread__main(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -93,8 +95,9 @@ function threads.thread__main(smem)
-- we are going to let the PPM prevent crashes -- we are going to let the PPM prevent crashes
-- return fault flags/codes to MODBUS queries -- return fault flags/codes to MODBUS queries
local unit = units[i] local unit = units[i]
println_ts(util.c("lost the ", unit.type, " on interface ", unit.name)) local type_name = types.rtu_type_to_string(unit.type)
log.warning(util.c("lost the ", unit.type, " unit peripheral on interface ", unit.name)) 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 break
end end
end end
@ -112,9 +115,9 @@ function threads.thread__main(smem)
rtu_comms.reconnect_modem(rtu_dev.modem) rtu_comms.reconnect_modem(rtu_dev.modem)
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected.") log.info("comms modem reconnected")
else else
log.info("wired modem reconnected.") log.info("wired modem reconnected")
end end
else else
-- relink lost peripheral to correct unit entry -- relink lost peripheral to correct unit entry
@ -129,51 +132,51 @@ function threads.thread__main(smem)
-- found, re-link -- found, re-link
unit.device = device unit.device = device
if unit.type == "virtual" then if unit.type == RTU_UNIT_TYPE.VIRTUAL then
resend_advert = true resend_advert = true
if type == "boilerValve" then if type == "boilerValve" then
-- boiler multiblock -- boiler multiblock
unit.type = rtu_t.boiler_valve unit.type = RTU_UNIT_TYPE.BOILER_VALVE
elseif type == "turbineValve" then elseif type == "turbineValve" then
-- turbine multiblock -- turbine multiblock
unit.type = rtu_t.turbine_valve unit.type = RTU_UNIT_TYPE.TURBINE_VALVE
elseif type == "inductionPort" then elseif type == "inductionPort" then
-- induction matrix multiblock -- induction matrix multiblock
unit.type = rtu_t.induction_matrix unit.type = RTU_UNIT_TYPE.IMATRIX
elseif type == "spsPort" then elseif type == "spsPort" then
-- SPS multiblock -- SPS multiblock
unit.type = rtu_t.sps unit.type = RTU_UNIT_TYPE.SPS
elseif type == "solarNeutronActivator" then elseif type == "solarNeutronActivator" then
-- SNA -- SNA
unit.type = rtu_t.sna unit.type = RTU_UNIT_TYPE.SNA
elseif type == "environmentDetector" then elseif type == "environmentDetector" then
-- advanced peripherals environment detector -- advanced peripherals environment detector
unit.type = rtu_t.env_detector unit.type = RTU_UNIT_TYPE.ENV_DETECTOR
else else
resend_advert = false resend_advert = false
log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")")) log.error(util.c("virtual device '", unit.name, "' cannot init to an unknown type (", type, ")"))
end end
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) unit.rtu = boilerv_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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) unit.rtu = turbinev_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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) unit.rtu = imatrix_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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) unit.rtu = sps_rtu.new(device)
-- if not formed, indexing the multiblock functions would have resulted in a PPM fault -- if not formed, indexing the multiblock functions would have resulted in a PPM fault
unit.formed = util.trinary(device.__p_is_faulted(), false, nil) 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) 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) unit.rtu = envd_rtu.new(device)
else else
log.error(util.c("failed to identify reconnected RTU unit type (", unit.name, ")"), true) 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) unit.modbus_io = modbus.new(unit.rtu, true)
println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) local type_name = types.rtu_type_to_string(unit.type)
log.info("reconnected the " .. unit.type .. " on interface " .. unit.name) local message = util.c("reconnected the ", type_name, " on interface ", unit.name)
println_ts(message)
log.info(message)
if resend_advert then if resend_advert then
rtu_comms.send_advertisement(units) rtu_comms.send_advertisement(units)
@ -229,9 +234,11 @@ function threads.thread__main(smem)
end end
-- communications handler thread -- communications handler thread
---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
function threads.thread__comms(smem) function threads.thread__comms(smem)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() function public.exec()
@ -301,14 +308,16 @@ function threads.thread__comms(smem)
end end
-- per-unit communications handler thread -- per-unit communications handler thread
---@nodiscard
---@param smem rtu_shared_memory ---@param smem rtu_shared_memory
---@param unit rtu_unit_registry_entry ---@param unit rtu_unit_registry_entry
function threads.thread__unit_comms(smem, unit) function threads.thread__unit_comms(smem, unit)
local public = {} ---@class thread ---@class parallel_thread
local public = {}
-- execute thread -- execute thread
function public.exec() 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 -- load in from shared memory
local rtu_state = smem.rtu_state local rtu_state = smem.rtu_state
@ -319,8 +328,8 @@ function threads.thread__unit_comms(smem, unit)
local last_f_check = 0 local last_f_check = 0
local detail_name = util.c(unit.type, " (", unit.name, ") [", unit.index, "] for reactor ", unit.reactor) 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(unit.type, " (", unit.name, ")") local short_name = util.c(types.rtu_type_to_string(unit.type), " (", unit.name, ")")
if packet_queue == nil then if packet_queue == nil then
log.error("rtu unit thread created without a message queue, exiting...", true) 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) local type, device = ppm.mount(iface)
if device ~= nil then 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 -- boiler multiblock
unit.device = device unit.device = device
unit.rtu = boilerv_rtu.new(device) unit.rtu = boilerv_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) 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 -- turbine multiblock
unit.device = device unit.device = device
unit.rtu = turbinev_rtu.new(device) unit.rtu = turbinev_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) 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 -- induction matrix multiblock
unit.device = device unit.device = device
unit.rtu = imatrix_rtu.new(device) unit.rtu = imatrix_rtu.new(device)
unit.formed = device.isFormed() unit.formed = device.isFormed()
unit.modbus_io = modbus.new(unit.rtu, true) 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 -- SPS multiblock
unit.device = device unit.device = device
unit.rtu = sps_rtu.new(device) unit.rtu = sps_rtu.new(device)
@ -433,7 +442,7 @@ function threads.thread__unit_comms(smem, unit)
end end
if not rtu_state.shutdown then 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) util.psleep(5)
end end
end end

View File

@ -3,21 +3,18 @@
-- --
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types")
---@class comms ---@class comms
local comms = {} local comms = {}
local rtu_t = types.rtu_t
local insert = table.insert local insert = table.insert
local max_distance = nil local max_distance = nil
comms.version = "1.4.0" comms.version = "1.4.0"
---@alias PROTOCOLS integer ---@enum PROTOCOL
local PROTOCOLS = { local PROTOCOL = {
MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol
RPLC = 1, -- reactor PLC protocol RPLC = 1, -- reactor PLC protocol
SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc 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 COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
} }
---@alias RPLC_TYPES integer ---@enum RPLC_TYPE
local RPLC_TYPES = { local RPLC_TYPE = {
STATUS = 0, -- reactor/system status STATUS = 0, -- reactor/system status
MEK_STRUCT = 1, -- mekanism build structure MEK_STRUCT = 1, -- mekanism build structure
MEK_BURN_RATE = 2, -- set burn rate 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 AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited
} }
---@alias SCADA_MGMT_TYPES integer ---@enum SCADA_MGMT_TYPE
local SCADA_MGMT_TYPES = { local SCADA_MGMT_TYPE = {
ESTABLISH = 0, -- establish new connection ESTABLISH = 0, -- establish new connection
KEEP_ALIVE = 1, -- keep alive packet w/ RTT KEEP_ALIVE = 1, -- keep alive packet w/ RTT
CLOSE = 2, -- close a connection 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 RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
} }
---@alias SCADA_CRDN_TYPES integer ---@enum SCADA_CRDN_TYPE
local SCADA_CRDN_TYPES = { local SCADA_CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds FAC_BUILDS = 1, -- facility RTU builds
FAC_STATUS = 2, -- state of facility and facility devices FAC_STATUS = 2, -- state of facility and facility devices
@ -60,12 +57,11 @@ local SCADA_CRDN_TYPES = {
UNIT_CMD = 6 -- command a reactor unit UNIT_CMD = 6 -- command a reactor unit
} }
---@alias CAPI_TYPES integer ---@enum CAPI_TYPE
local CAPI_TYPES = { local CAPI_TYPE = {
ESTABLISH = 0 -- initial greeting
} }
---@alias ESTABLISH_ACK integer ---@enum ESTABLISH_ACK
local ESTABLISH_ACK = { local ESTABLISH_ACK = {
ALLOW = 0, -- link approved ALLOW = 0, -- link approved
DENY = 1, -- link denied DENY = 1, -- link denied
@ -73,26 +69,15 @@ local ESTABLISH_ACK = {
BAD_VERSION = 3 -- link denied due to comms version mismatch BAD_VERSION = 3 -- link denied due to comms version mismatch
} }
---@alias DEVICE_TYPES integer ---@enum DEVICE_TYPE
local DEVICE_TYPES = { local DEVICE_TYPE = {
PLC = 0, -- PLC device type for establish PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish RTU = 1, -- RTU device type for establish
SV = 2, -- supervisor device type for establish SV = 2, -- supervisor device type for establish
CRDN = 3 -- coordinator device type for establish CRDN = 3 -- coordinator device type for establish
} }
---@alias RTU_UNIT_TYPES integer ---@enum PLC_AUTO_ACK
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
local PLC_AUTO_ACK = { local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid FAIL = 0, -- failed to set burn rate/burn rate invalid
DIRECT_SET_OK = 1, -- successfully set burn rate 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 ZERO_DIS_OK = 3 -- successfully disabled reactor with < 0.01 burn rate
} }
---@alias FAC_COMMANDS integer ---@enum FAC_COMMAND
local FAC_COMMANDS = { local FAC_COMMAND = {
SCRAM_ALL = 0, -- SCRAM all reactors SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control STOP = 1, -- stop automatic control
START = 2, -- start automatic control START = 2, -- start automatic control
ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units ACK_ALL_ALARMS = 3 -- acknowledge all alarms on all units
} }
---@alias UNIT_COMMANDS integer ---@enum UNIT_COMMAND
local UNIT_COMMANDS = { local UNIT_COMMAND = {
SCRAM = 0, -- SCRAM the reactor SCRAM = 0, -- SCRAM the reactor
START = 1, -- start the reactor START = 1, -- start the reactor
RESET_RPS = 2, -- reset the RPS RESET_RPS = 2, -- reset the RPS
@ -121,26 +106,25 @@ local UNIT_COMMANDS = {
SET_GROUP = 8 -- assign this unit to a group SET_GROUP = 8 -- assign this unit to a group
} }
comms.PROTOCOLS = PROTOCOLS comms.PROTOCOL = PROTOCOL
comms.RPLC_TYPES = RPLC_TYPES comms.RPLC_TYPE = RPLC_TYPE
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES comms.SCADA_MGMT_TYPE = SCADA_MGMT_TYPE
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES comms.SCADA_CRDN_TYPE = SCADA_CRDN_TYPE
comms.CAPI_TYPES = CAPI_TYPES comms.CAPI_TYPE = CAPI_TYPE
comms.ESTABLISH_ACK = ESTABLISH_ACK comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPES = DEVICE_TYPES comms.DEVICE_TYPE = DEVICE_TYPE
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
comms.PLC_AUTO_ACK = PLC_AUTO_ACK comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMANDS = UNIT_COMMANDS comms.UNIT_COMMAND = UNIT_COMMAND
comms.FAC_COMMANDS = FAC_COMMANDS comms.FAC_COMMAND = FAC_COMMAND
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet ---@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 ---@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 -- packets received with distances greater than this will be silently discarded
---@param distance integer max modem message distance (less than 1 disables the limit) ---@param distance integer max modem message distance (less than 1 disables the limit)
function comms.set_trusted_range(distance) function comms.set_trusted_range(distance)
@ -152,6 +136,7 @@ function comms.set_trusted_range(distance)
end end
-- generic SCADA packet object -- generic SCADA packet object
---@nodiscard
function comms.scada_packet() function comms.scada_packet()
local self = { local self = {
modem_msg_in = nil, modem_msg_in = nil,
@ -168,7 +153,7 @@ function comms.scada_packet()
-- make a SCADA packet -- make a SCADA packet
---@param seq_num integer ---@param seq_num integer
---@param protocol PROTOCOLS ---@param protocol PROTOCOL
---@param payload table ---@param payload table
function public.make(seq_num, protocol, payload) function public.make(seq_num, protocol, payload)
self.valid = true self.valid = true
@ -180,11 +165,12 @@ function comms.scada_packet()
end end
-- parse in a modem message as a SCADA packet -- parse in a modem message as a SCADA packet
---@param side string ---@param side string modem side
---@param sender integer ---@param sender integer sender port
---@param reply_to integer ---@param reply_to integer reply port
---@param message any ---@param message any message body
---@param distance integer ---@param distance integer transmission distance
---@return boolean valid valid message received
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
self.modem_msg_in = { self.modem_msg_in = {
iface = side, iface = side,
@ -223,24 +209,34 @@ function comms.scada_packet()
-- public accessors -- -- public accessors --
---@nodiscard
function public.modem_event() return self.modem_msg_in end function public.modem_event() return self.modem_msg_in end
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
---@nodiscard
function public.local_port() return self.modem_msg_in.s_port end 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 function public.remote_port() return self.modem_msg_in.r_port end
---@nodiscard
function public.is_valid() return self.valid end function public.is_valid() return self.valid end
---@nodiscard
function public.seq_num() return self.seq_num end function public.seq_num() return self.seq_num end
---@nodiscard
function public.protocol() return self.protocol end function public.protocol() return self.protocol end
---@nodiscard
function public.length() return self.length end function public.length() return self.length end
---@nodiscard
function public.data() return self.payload end function public.data() return self.payload end
return public return public
end end
-- MODBUS packet -- MODBUS packet<br>
-- modeled after MODBUS TCP packet -- modeled after MODBUS TCP packet
---@nodiscard
function comms.modbus_packet() function comms.modbus_packet()
local self = { local self = {
frame = nil, frame = nil,
@ -248,7 +244,7 @@ function comms.modbus_packet()
txn_id = -1, txn_id = -1,
length = 0, length = 0,
unit_id = -1, unit_id = -1,
func_code = 0, func_code = 0x80,
data = {} data = {}
} }
@ -285,7 +281,7 @@ function comms.modbus_packet()
if frame then if frame then
self.frame = frame self.frame = frame
if frame.protocol() == PROTOCOLS.MODBUS_TCP then if frame.protocol() == PROTOCOL.MODBUS_TCP then
local size_ok = frame.length() >= 3 local size_ok = frame.length() >= 3
if size_ok then if size_ok then
@ -309,9 +305,11 @@ function comms.modbus_packet()
end end
-- get raw to send -- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object -- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get() function public.get()
---@class modbus_frame ---@class modbus_frame
local frame = { local frame = {
@ -330,12 +328,13 @@ function comms.modbus_packet()
end end
-- reactor PLC packet -- reactor PLC packet
---@nodiscard
function comms.rplc_packet() function comms.rplc_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
id = 0, id = 0,
type = -1, type = 0, ---@type RPLC_TYPE
length = 0, length = 0,
data = {} data = {}
} }
@ -345,22 +344,22 @@ function comms.rplc_packet()
-- check that type is known -- check that type is known
local function _rplc_type_valid() local function _rplc_type_valid()
return self.type == RPLC_TYPES.STATUS or return self.type == RPLC_TYPE.STATUS or
self.type == RPLC_TYPES.MEK_STRUCT or self.type == RPLC_TYPE.MEK_STRUCT or
self.type == RPLC_TYPES.MEK_BURN_RATE or self.type == RPLC_TYPE.MEK_BURN_RATE or
self.type == RPLC_TYPES.RPS_ENABLE or self.type == RPLC_TYPE.RPS_ENABLE or
self.type == RPLC_TYPES.RPS_SCRAM or self.type == RPLC_TYPE.RPS_SCRAM or
self.type == RPLC_TYPES.RPS_ASCRAM or self.type == RPLC_TYPE.RPS_ASCRAM or
self.type == RPLC_TYPES.RPS_STATUS or self.type == RPLC_TYPE.RPS_STATUS or
self.type == RPLC_TYPES.RPS_ALARM or self.type == RPLC_TYPE.RPS_ALARM or
self.type == RPLC_TYPES.RPS_RESET or self.type == RPLC_TYPE.RPS_RESET or
self.type == RPLC_TYPES.RPS_AUTO_RESET or self.type == RPLC_TYPE.RPS_AUTO_RESET or
self.type == RPLC_TYPES.AUTO_BURN_RATE self.type == RPLC_TYPE.AUTO_BURN_RATE
end end
-- make an RPLC packet -- make an RPLC packet
---@param id integer ---@param id integer
---@param packet_type RPLC_TYPES ---@param packet_type RPLC_TYPE
---@param data table ---@param data table
function public.make(id, packet_type, data) function public.make(id, packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -387,7 +386,7 @@ function comms.rplc_packet()
if frame then if frame then
self.frame = frame self.frame = frame
if frame.protocol() == PROTOCOLS.RPLC then if frame.protocol() == PROTOCOL.RPLC then
local ok = frame.length() >= 2 local ok = frame.length() >= 2
if ok then if ok then
@ -410,9 +409,11 @@ function comms.rplc_packet()
end end
-- get raw to send -- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object -- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get() function public.get()
---@class rplc_frame ---@class rplc_frame
local frame = { local frame = {
@ -430,11 +431,12 @@ function comms.rplc_packet()
end end
-- SCADA management packet -- SCADA management packet
---@nodiscard
function comms.mgmt_packet() function comms.mgmt_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
type = -1, type = 0, ---@type SCADA_MGMT_TYPE
length = 0, length = 0,
data = {} data = {}
} }
@ -444,16 +446,16 @@ function comms.mgmt_packet()
-- check that type is known -- check that type is known
local function _scada_type_valid() local function _scada_type_valid()
return self.type == SCADA_MGMT_TYPES.ESTABLISH or return self.type == SCADA_MGMT_TYPE.ESTABLISH or
self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or self.type == SCADA_MGMT_TYPE.KEEP_ALIVE or
self.type == SCADA_MGMT_TYPES.CLOSE or self.type == SCADA_MGMT_TYPE.CLOSE or
self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or self.type == SCADA_MGMT_TYPE.REMOTE_LINKED or
self.type == SCADA_MGMT_TYPES.RTU_ADVERT or self.type == SCADA_MGMT_TYPE.RTU_ADVERT or
self.type == SCADA_MGMT_TYPES.RTU_DEV_REMOUNT self.type == SCADA_MGMT_TYPE.RTU_DEV_REMOUNT
end end
-- make a SCADA management packet -- make a SCADA management packet
---@param packet_type SCADA_MGMT_TYPES ---@param packet_type SCADA_MGMT_TYPE
---@param data table ---@param data table
function public.make(packet_type, data) function public.make(packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -479,7 +481,7 @@ function comms.mgmt_packet()
if frame then if frame then
self.frame = frame self.frame = frame
if frame.protocol() == PROTOCOLS.SCADA_MGMT then if frame.protocol() == PROTOCOL.SCADA_MGMT then
local ok = frame.length() >= 1 local ok = frame.length() >= 1
if ok then if ok then
@ -500,9 +502,11 @@ function comms.mgmt_packet()
end end
-- get raw to send -- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object -- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get() function public.get()
---@class mgmt_frame ---@class mgmt_frame
local frame = { local frame = {
@ -519,11 +523,12 @@ function comms.mgmt_packet()
end end
-- SCADA coordinator packet -- SCADA coordinator packet
---@nodiscard
function comms.crdn_packet() function comms.crdn_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
type = -1, type = 0, ---@type SCADA_CRDN_TYPE
length = 0, length = 0,
data = {} data = {}
} }
@ -532,18 +537,19 @@ function comms.crdn_packet()
local public = {} local public = {}
-- check that type is known -- check that type is known
---@nodiscard
local function _crdn_type_valid() local function _crdn_type_valid()
return self.type == SCADA_CRDN_TYPES.INITIAL_BUILDS or return self.type == SCADA_CRDN_TYPE.INITIAL_BUILDS or
self.type == SCADA_CRDN_TYPES.FAC_BUILDS or self.type == SCADA_CRDN_TYPE.FAC_BUILDS or
self.type == SCADA_CRDN_TYPES.FAC_STATUS or self.type == SCADA_CRDN_TYPE.FAC_STATUS or
self.type == SCADA_CRDN_TYPES.FAC_CMD or self.type == SCADA_CRDN_TYPE.FAC_CMD or
self.type == SCADA_CRDN_TYPES.UNIT_BUILDS or self.type == SCADA_CRDN_TYPE.UNIT_BUILDS or
self.type == SCADA_CRDN_TYPES.UNIT_STATUSES or self.type == SCADA_CRDN_TYPE.UNIT_STATUSES or
self.type == SCADA_CRDN_TYPES.UNIT_CMD self.type == SCADA_CRDN_TYPE.UNIT_CMD
end end
-- make a coordinator packet -- make a coordinator packet
---@param packet_type SCADA_CRDN_TYPES ---@param packet_type SCADA_CRDN_TYPE
---@param data table ---@param data table
function public.make(packet_type, data) function public.make(packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -569,7 +575,7 @@ function comms.crdn_packet()
if frame then if frame then
self.frame = frame self.frame = frame
if frame.protocol() == PROTOCOLS.SCADA_CRDN then if frame.protocol() == PROTOCOL.SCADA_CRDN then
local ok = frame.length() >= 1 local ok = frame.length() >= 1
if ok then if ok then
@ -590,9 +596,11 @@ function comms.crdn_packet()
end end
-- get raw to send -- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object -- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get() function public.get()
---@class crdn_frame ---@class crdn_frame
local frame = { local frame = {
@ -609,12 +617,13 @@ function comms.crdn_packet()
end end
-- coordinator API (CAPI) packet -- coordinator API (CAPI) packet
-- @todo ---@todo implement for pocket access, set enum type for self.type
---@nodiscard
function comms.capi_packet() function comms.capi_packet()
local self = { local self = {
frame = nil, frame = nil,
raw = {}, raw = {},
type = -1, type = 0,
length = 0, length = 0,
data = {} data = {}
} }
@ -623,12 +632,12 @@ function comms.capi_packet()
local public = {} local public = {}
local function _capi_type_valid() local function _capi_type_valid()
-- @todo ---@todo
return false return false
end end
-- make a coordinator API packet -- make a coordinator API packet
---@param packet_type CAPI_TYPES ---@param packet_type CAPI_TYPE
---@param data table ---@param data table
function public.make(packet_type, data) function public.make(packet_type, data)
if type(data) == "table" then if type(data) == "table" then
@ -654,7 +663,7 @@ function comms.capi_packet()
if frame then if frame then
self.frame = frame self.frame = frame
if frame.protocol() == PROTOCOLS.COORD_API then if frame.protocol() == PROTOCOL.COORD_API then
local ok = frame.length() >= 1 local ok = frame.length() >= 1
if ok then if ok then
@ -675,9 +684,11 @@ function comms.capi_packet()
end end
-- get raw to send -- get raw to send
---@nodiscard
function public.raw_sendable() return self.raw end function public.raw_sendable() return self.raw end
-- get this packet as a frame with an immutable relation to this object -- get this packet as a frame with an immutable relation to this object
---@nodiscard
function public.get() function public.get()
---@class capi_frame ---@class capi_frame
local frame = { local frame = {
@ -693,50 +704,4 @@ function comms.capi_packet()
return public return public
end 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 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 end
-- encrypt plaintext -- encrypt plaintext
---@nodiscard
---@param plaintext string ---@param plaintext string
---@return table initial_value, string ciphertext ---@return table initial_value, string ciphertext
function crypto.encrypt(plaintext) function crypto.encrypt(plaintext)
@ -113,6 +114,7 @@ function crypto.encrypt(plaintext)
end end
-- decrypt ciphertext -- decrypt ciphertext
---@nodiscard
---@param iv string CTR initial value ---@param iv string CTR initial value
---@param ciphertext string ciphertext hex ---@param ciphertext string ciphertext hex
---@return string plaintext ---@return string plaintext
@ -135,6 +137,7 @@ function crypto.decrypt(iv, ciphertext)
end end
-- generate HMAC of message -- generate HMAC of message
---@nodiscard
---@param message_hex string initial value concatenated with ciphertext ---@param message_hex string initial value concatenated with ciphertext
function crypto.hmac(message_hex) function crypto.hmac(message_hex)
local start = util.time() local start = util.time()
@ -201,11 +204,12 @@ function crypto.secure_modem(modem)
end end
-- parse in a modem message as a network packet -- parse in a modem message as a network packet
---@param side string ---@nodiscard
---@param sender integer ---@param side string modem side
---@param reply_to integer ---@param sender integer sender port
---@param reply_to integer reply port
---@param message any encrypted packet sent with secure_modem.transmit ---@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 ---@return string side, integer sender, integer reply_to, any plaintext_message, integer distance
function public.receive(side, sender, reply_to, message, distance) function public.receive(side, sender, reply_to, message, distance)
local body = "" local body = ""

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,12 +12,14 @@ local types = {}
---@field amount integer ---@field amount integer
-- create a new tank fluid -- create a new tank fluid
---@nodiscard
---@param n string name ---@param n string name
---@param a integer amount ---@param a integer amount
---@return radiation_reading ---@return radiation_reading
function types.new_tank_fluid(n, a) return { name = n, amount = a } end function types.new_tank_fluid(n, a) return { name = n, amount = a } end
-- create a new empty tank fluid -- create a new empty tank fluid
---@nodiscard
---@return tank_fluid ---@return tank_fluid
function types.new_empty_gas() return { type = "mekanism:empty_gas", amount = 0 } end 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 ---@field unit string
-- create a new radiation reading -- create a new radiation reading
---@nodiscard
---@param r number radiaiton level ---@param r number radiaiton level
---@param u string radiation unit ---@param u string radiation unit
---@return radiation_reading ---@return radiation_reading
function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end function types.new_radiation_reading(r, u) return { radiation = r, unit = u } end
-- create a new zeroed radiation reading -- create a new zeroed radiation reading
---@nodiscard
---@return radiation_reading ---@return radiation_reading
function types.new_zero_radiation_reading() return { radiation = 0, unit = "nSv" } end 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 ---@field z integer
-- create a new coordinate -- create a new coordinate
---@nodiscard
---@param x integer ---@param x integer
---@param y integer ---@param y integer
---@param z 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 function types.new_coordinate(x, y, z) return { x = x, y = y, z = z } end
-- create a new zero coordinate -- create a new zero coordinate
---@nodiscard
---@return coordinate ---@return coordinate
function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@class rtu_advertisement ---@class rtu_advertisement
---@field type integer ---@field type RTU_UNIT_TYPE
---@field index integer ---@field index integer
---@field reactor integer ---@field reactor integer
---@field rsio table|nil ---@field rsio table|nil
@ -62,15 +68,58 @@ function types.new_zero_coordinate() return { x = 0, y = 0, z = 0 } end
---@alias color integer ---@alias color integer
-- ENUMERATION TYPES -- -- ENUMERATION TYPES --
--#region
---@alias TRI_FAIL integer ---@enum RTU_UNIT_TYPE
types.TRI_FAIL = { types.RTU_UNIT_TYPE = {
OK = 0, VIRTUAL = 0, -- virtual device
PARTIAL = 1, REDSTONE = 1, -- redstone I/O
FULL = 2 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 = { types.PROCESS = {
INACTIVE = 0, INACTIVE = 0,
MAX_BURN = 1, MAX_BURN = 1,
@ -93,7 +142,7 @@ types.PROCESS_NAMES = {
"GEN_RATE_FAULT_IDLE" "GEN_RATE_FAULT_IDLE"
} }
---@alias WASTE_MODE integer ---@enum WASTE_MODE
types.WASTE_MODE = { types.WASTE_MODE = {
AUTO = 1, AUTO = 1,
PLUTONIUM = 2, PLUTONIUM = 2,
@ -101,7 +150,14 @@ types.WASTE_MODE = {
ANTI_MATTER = 4 ANTI_MATTER = 4
} }
---@alias ALARM integer types.WASTE_MODE_NAMES = {
"AUTO",
"PLUTONIUM",
"POLONIUM",
"ANTI_MATTER"
}
---@enum ALARM
types.ALARM = { types.ALARM = {
ContainmentBreach = 1, ContainmentBreach = 1,
ContainmentRadiation = 2, ContainmentRadiation = 2,
@ -117,7 +173,7 @@ types.ALARM = {
TurbineTrip = 12 TurbineTrip = 12
} }
types.alarm_string = { types.ALARM_NAMES = {
"ContainmentBreach", "ContainmentBreach",
"ContainmentRadiation", "ContainmentRadiation",
"ReactorLost", "ReactorLost",
@ -132,46 +188,40 @@ types.alarm_string = {
"TurbineTrip" "TurbineTrip"
} }
---@alias ALARM_PRIORITY integer ---@enum ALARM_PRIORITY
types.ALARM_PRIORITY = { types.ALARM_PRIORITY = {
CRITICAL = 0, CRITICAL = 1,
EMERGENCY = 1, EMERGENCY = 2,
URGENT = 2, URGENT = 3,
TIMELY = 3 TIMELY = 4
} }
types.alarm_prio_string = { types.ALARM_PRIORITY_NAMES = {
"CRITICAL", "CRITICAL",
"EMERGENCY", "EMERGENCY",
"URGENT", "URGENT",
"TIMELY" "TIMELY"
} }
-- map alarms to alarm priority ---@enum ALARM_STATE
types.ALARM_PRIO_MAP = { types.ALARM_STATE = {
types.ALARM_PRIORITY.CRITICAL, INACTIVE = 1,
types.ALARM_PRIORITY.CRITICAL, TRIPPED = 2,
types.ALARM_PRIORITY.URGENT, ACKED = 3,
types.ALARM_PRIORITY.CRITICAL, RING_BACK = 4
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.EMERGENCY,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.URGENT,
types.ALARM_PRIORITY.TIMELY,
types.ALARM_PRIORITY.URGENT
} }
---@alias ALARM_STATE integer types.ALARM_STATE_NAMES = {
types.ALARM_STATE = { "INACTIVE",
INACTIVE = 0, "TRIPPED",
TRIPPED = 1, "ACKED",
ACKED = 2, "RING_BACK"
RING_BACK = 3
} }
--#endregion
-- STRING TYPES -- -- STRING TYPES --
--#region
---@alias os_event ---@alias os_event
---| "alarm" ---| "alarm"
@ -206,14 +256,28 @@ types.ALARM_STATE = {
---| "websocket_failure" ---| "websocket_failure"
---| "websocket_message" ---| "websocket_message"
---| "websocket_success" ---| "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 ---@alias rps_trip_cause
---| "ok" ---| "ok"
---| "dmg_crit" ---| "dmg_crit"
---| "high_temp" ---| "high_temp"
---| "no_coolant" ---| "no_coolant"
---| "full_waste" ---| "ex_waste"
---| "heated_coolant_backup" ---| "ex_heated_coolant"
---| "no_fuel" ---| "no_fuel"
---| "fault" ---| "fault"
---| "timeout" ---| "timeout"
@ -222,59 +286,40 @@ types.ALARM_STATE = {
---| "sys_fail" ---| "sys_fail"
---| "force_disabled" ---| "force_disabled"
---@alias fluid types.RPS_TRIP_CAUSE = {
---| "mekanism:empty_gas" OK = "ok",
---| "minecraft:water" DMG_CRIT = "dmg_crit",
---| "mekanism:sodium" HIGH_TEMP = "high_temp",
---| "mekanism:superheated_sodium" NO_COOLANT = "no_coolant",
EX_WASTE = "ex_waste",
types.fluid = { EX_HCOOLANT = "ex_heated_coolant",
empty_gas = "mekanism:empty_gas", NO_FUEL = "no_fuel",
water = "minecraft:water", FAULT = "fault",
sodium = "mekanism:sodium", TIMEOUT = "timeout",
superheated_sodium = "mekanism:superheated_sodium" MANUAL = "manual",
AUTOMATIC = "automatic",
SYS_FAIL = "sys_fail",
FORCE_DISABLED = "force_disabled"
} }
---@alias rtu_t string ---@alias dumping_mode
types.rtu_t = { ---| "IDLE"
redstone = "redstone", ---| "DUMPING"
boiler_valve = "boiler_valve", ---| "DUMPING_EXCESS"
turbine_valve = "turbine_valve",
induction_matrix = "induction_matrix",
sps = "sps",
sna = "sna",
env_detector = "environment_detector"
}
---@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 = { types.DUMPING_MODE = {
IDLE = "IDLE", IDLE = "IDLE",
DUMPING = "DUMPING", DUMPING = "DUMPING",
DUMPING_EXCESS = "DUMPING_EXCESS" DUMPING_EXCESS = "DUMPING_EXCESS"
} }
-- MODBUS --#endregion
-- modbus function codes -- MODBUS --
---@alias MODBUS_FCODE integer --#region
-- MODBUS function codes
---@enum MODBUS_FCODE
types.MODBUS_FCODE = { types.MODBUS_FCODE = {
READ_COILS = 0x01, READ_COILS = 0x01,
READ_DISCRETE_INPUTS = 0x02, READ_DISCRETE_INPUTS = 0x02,
@ -287,8 +332,8 @@ types.MODBUS_FCODE = {
ERROR_FLAG = 0x80 ERROR_FLAG = 0x80
} }
-- modbus exception codes -- MODBUS exception codes
---@alias MODBUS_EXCODE integer ---@enum MODBUS_EXCODE
types.MODBUS_EXCODE = { types.MODBUS_EXCODE = {
ILLEGAL_FUNCTION = 0x01, ILLEGAL_FUNCTION = 0x01,
ILLEGAL_DATA_ADDR = 0x02, ILLEGAL_DATA_ADDR = 0x02,
@ -302,4 +347,6 @@ types.MODBUS_EXCODE = {
GATEWAY_TARGET_TIMEOUT = 0x0B GATEWAY_TARGET_TIMEOUT = 0x0B
} }
--#endregion
return types return types

View File

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

View File

@ -1,3 +1,4 @@
local const = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
@ -12,19 +13,13 @@ local PROCESS_NAMES = types.PROCESS_NAMES
local IO = rsio.IO 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) -- 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 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) local ALARM_LIMS = const.ALARM_LIMITS
-- "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 AUTO_SCRAM = { local AUTO_SCRAM = {
NONE = 0, NONE = 0,
@ -53,6 +48,7 @@ local rate_Kd = -1.0
local facility = {} local facility = {}
-- create a new facility management object -- create a new facility management object
---@nodiscard
---@param num_reactors integer number of reactor units ---@param num_reactors integer number of reactor units
---@param cooling_conf table cooling configurations of reactor units ---@param cooling_conf table cooling configurations of reactor units
function facility.new(num_reactors, cooling_conf) function facility.new(num_reactors, cooling_conf)
@ -124,6 +120,7 @@ function facility.new(num_reactors, cooling_conf)
end end
-- check if all auto-controlled units completed ramping -- check if all auto-controlled units completed ramping
---@nodiscard
local function _all_units_ramped() local function _all_units_ramped()
local all_ramped = true local all_ramped = true
@ -185,10 +182,7 @@ function facility.new(num_reactors, cooling_conf)
unallocated = math.max(0, unallocated - ctl.br100) unallocated = math.max(0, unallocated - ctl.br100)
if last ~= ctl.br100 then if last ~= ctl.br100 then u.a_commit_br100(ramp) end
log.debug("unit " .. u.get_id() .. ": set to " .. ctl.br100 .. " (was " .. last .. ")")
u.a_commit_br100(ramp)
end
end end
end end
end end
@ -426,7 +420,7 @@ function facility.new(num_reactors, cooling_conf)
self.accumulator = self.accumulator + (error * (now - self.last_time)) self.accumulator = self.accumulator + (error * (now - self.last_time))
end end
local runtime = now - self.time_start -- local runtime = now - self.time_start
local integral = self.accumulator local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time) 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 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] }", -- 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)) -- runtime, avg_charge, error, integral, output, out_c, P, I, D))
_allocate_burn_rate(out_c, true) _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)) self.accumulator = self.accumulator + (error * (now - self.last_time))
end end
local runtime = now - self.time_start -- local runtime = now - self.time_start
local integral = self.accumulator local integral = self.accumulator
local derivative = (error - self.last_error) / (now - self.last_time) 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 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] }", -- 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)) -- runtime, avg_inflow, error, integral, output, out_c, P, I, D))
_allocate_burn_rate(out_c, false) _allocate_burn_rate(out_c, false)
@ -564,10 +558,10 @@ function facility.new(num_reactors, cooling_conf)
-- check matrix fill too high -- check matrix fill too high
local was_fill = astatus.matrix_fill 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 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 end
-- check for critical unit alarms -- 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 envd = self.envd[1] ---@type unit_session
local e_db = envd.get_db() ---@type envd_session_db 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 else
-- don't clear, if it is true then we lost it with high radiation, so just keep alarming -- 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 -- 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 -- -- READ STATES/PROPERTIES --
-- get build properties of all machines -- get build properties of all machines
---@nodiscard
---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude ---@param inc_imatrix boolean? true/nil to include induction matrix build, false to exclude
function public.get_build(inc_imatrix) function public.get_build(inc_imatrix)
local build = {} local build = {}
@ -830,6 +825,7 @@ function facility.new(num_reactors, cooling_conf)
end end
-- get automatic process control status -- get automatic process control status
---@nodiscard
function public.get_control_status() function public.get_control_status()
local astat = self.ascram_status local astat = self.ascram_status
return { return {
@ -851,6 +847,7 @@ function facility.new(num_reactors, cooling_conf)
end end
-- get RTU statuses -- get RTU statuses
---@nodiscard
function public.get_rtu_statuses() function public.get_rtu_statuses()
local status = {} local status = {}
@ -889,9 +886,9 @@ function facility.new(num_reactors, cooling_conf)
return status return status
end end
function public.get_units() -- get the units in this facility
return self.units ---@nodiscard
end function public.get_units() return self.units end
return public return public
end end

View File

@ -1,18 +1,20 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
local coordinator = {} local coordinator = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES local SCADA_CRDN_TYPE = comms.SCADA_CRDN_TYPE
local UNIT_COMMANDS = comms.UNIT_COMMANDS local UNIT_COMMAND = comms.UNIT_COMMAND
local FAC_COMMANDS = comms.FAC_COMMANDS local FAC_COMMAND = comms.FAC_COMMAND
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local SV_Q_CMDS = svqtypes.SV_Q_CMDS local SV_Q_CMDS = svqtypes.SV_Q_CMDS
local SV_Q_DATA = svqtypes.SV_Q_DATA local SV_Q_DATA = svqtypes.SV_Q_DATA
@ -45,6 +47,7 @@ local PERIODICS = {
} }
-- coordinator supervisor session -- coordinator supervisor session
---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out 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 log_header = "crdn_session(" .. id .. "): "
local self = { local self = {
in_q = in_queue,
out_q = out_queue,
units = facility.get_units(), units = facility.get_units(),
-- connection properties -- connection properties
seq_num = 0, seq_num = 0,
@ -90,30 +91,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
end end
-- send a CRDN packet -- send a CRDN packet
---@param msg_type SCADA_CRDN_TYPES ---@param msg_type SCADA_CRDN_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local c_pkt = comms.crdn_packet() local c_pkt = comms.crdn_packet()
c_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES ---@param msg_type SCADA_MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -126,12 +127,12 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
unit_builds[unit.get_id()] = unit.get_build() unit_builds[unit.get_id()] = unit.get_build()
end end
_send(SCADA_CRDN_TYPES.INITIAL_BUILDS, { facility.get_build(), unit_builds }) _send(SCADA_CRDN_TYPE.INITIAL_BUILDS, { facility.get_build(), unit_builds })
end end
-- send facility builds -- send facility builds
local function _send_fac_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 end
-- send unit builds -- 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() builds[unit.get_id()] = unit.get_build()
end end
_send(SCADA_CRDN_TYPES.UNIT_BUILDS, { builds }) _send(SCADA_CRDN_TYPE.UNIT_BUILDS, { builds })
end end
-- send facility status -- send facility status
@ -153,7 +154,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
facility.get_rtu_statuses() facility.get_rtu_statuses()
} }
_send(SCADA_CRDN_TYPES.FAC_STATUS, status) _send(SCADA_CRDN_TYPE.FAC_STATUS, status)
end end
-- send unit statuses -- send unit statuses
@ -172,7 +173,7 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
} }
end end
_send(SCADA_CRDN_TYPES.UNIT_STATUSES, status) _send(SCADA_CRDN_TYPE.UNIT_STATUSES, status)
end end
-- handle a packet -- handle a packet
@ -192,8 +193,8 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
self.conn_watchdog.feed() self.conn_watchdog.feed()
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then if pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
@ -210,30 +211,30 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
else else
log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_CRDN then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_CRDN then
if pkt.type == SCADA_CRDN_TYPES.INITIAL_BUILDS then if pkt.type == SCADA_CRDN_TYPE.INITIAL_BUILDS then
-- acknowledgement to coordinator receiving builds -- acknowledgement to coordinator receiving builds
self.acks.builds = true 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 -- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true 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 if pkt.length >= 1 then
local cmd = pkt.data[1] local cmd = pkt.data[1]
if cmd == FAC_COMMANDS.SCRAM_ALL then if cmd == FAC_COMMAND.SCRAM_ALL then
facility.scram_all() facility.scram_all()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.STOP then elseif cmd == FAC_COMMAND.STOP then
facility.auto_stop() facility.auto_stop()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.START then elseif cmd == FAC_COMMAND.START then
if pkt.length == 6 then if pkt.length == 6 then
---@type coord_auto_config ---@type coord_auto_config
local config = { local config = {
@ -244,23 +245,23 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
limits = pkt.data[6] 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 else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch") log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
end end
elseif cmd == FAC_COMMANDS.ACK_ALL_ALARMS then elseif cmd == FAC_COMMAND.ACK_ALL_ALARMS then
facility.ack_all() facility.ack_all()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true }) _send(SCADA_CRDN_TYPE.FAC_CMD, { cmd, true })
else else
log.debug(log_header .. "CRDN facility command unknown") log.debug(log_header .. "CRDN facility command unknown")
end end
else else
log.debug(log_header .. "CRDN facility command packet length mismatch") log.debug(log_header .. "CRDN facility command packet length mismatch")
end end
elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then elseif pkt.type == SCADA_CRDN_TYPE.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds -- acknowledgement to coordinator receiving builds
self.acks.unit_builds = true 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 if pkt.length >= 2 then
-- get command and unit id -- get command and unit id
local cmd = pkt.data[1] 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 if util.is_int(uid) and uid > 0 and uid <= #self.units then
local unit = self.units[uid] ---@type reactor_unit local unit = self.units[uid] ---@type reactor_unit
if cmd == UNIT_COMMANDS.START then if cmd == UNIT_COMMAND.START then
self.out_q.push_data(SV_Q_DATA.START, data) out_queue.push_data(SV_Q_DATA.START, data)
elseif cmd == UNIT_COMMANDS.SCRAM then elseif cmd == UNIT_COMMAND.SCRAM then
self.out_q.push_data(SV_Q_DATA.SCRAM, data) out_queue.push_data(SV_Q_DATA.SCRAM, data)
elseif cmd == UNIT_COMMANDS.RESET_RPS then elseif cmd == UNIT_COMMAND.RESET_RPS then
self.out_q.push_data(SV_Q_DATA.RESET_RPS, data) out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMANDS.SET_BURN then elseif cmd == UNIT_COMMAND.SET_BURN then
if pkt.length == 3 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 else
log.debug(log_header .. "CRDN unit command burn rate missing option") log.debug(log_header .. "CRDN unit command burn rate missing option")
end 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 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]) unit.set_waste(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command set waste missing option") log.debug(log_header .. "CRDN unit command set waste missing option")
end end
elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then elseif cmd == UNIT_COMMAND.ACK_ALL_ALARMS then
unit.ack_all() unit.ack_all()
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, true }) _send(SCADA_CRDN_TYPE.UNIT_CMD, { cmd, uid, true })
elseif cmd == UNIT_COMMANDS.ACK_ALARM then elseif cmd == UNIT_COMMAND.ACK_ALARM then
if pkt.length == 3 then if pkt.length == 3 then
unit.ack_alarm(pkt.data[3]) unit.ack_alarm(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id") log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
end end
elseif cmd == UNIT_COMMANDS.RESET_ALARM then elseif cmd == UNIT_COMMAND.RESET_ALARM then
if pkt.length == 3 then if pkt.length == 3 then
unit.reset_alarm(pkt.data[3]) unit.reset_alarm(pkt.data[3])
else else
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id") log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
end 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 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]) 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 else
log.debug(log_header .. "CRDN unit command set group missing group id") log.debug(log_header .. "CRDN unit command set group missing group id")
end end
@ -332,9 +333,11 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local public = {} local public = {}
-- get the session ID -- get the session ID
---@nodiscard
function public.get_id() return id end function public.get_id() return id end
-- check if a timer matches this session's watchdog -- check if a timer matches this session's watchdog
---@nodiscard
function public.check_wd(timer) function public.check_wd(timer)
return self.conn_watchdog.is_timer(timer) and self.connected return self.conn_watchdog.is_timer(timer) and self.connected
end end
@ -342,12 +345,13 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
-- close the connection -- close the connection
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to coordinator " .. id .. " closed by server") println("connection to coordinator " .. id .. " closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
-- iterate the session -- iterate the session
---@nodiscard
---@return boolean connected ---@return boolean connected
function public.iterate() function public.iterate()
if self.connected then if self.connected then
@ -357,9 +361,9 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local handle_start = util.time() 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 -- get a new message to process
local message = self.in_q.pop() local message = in_queue.pop()
if message ~= nil then if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET 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 if cmd.key == CRD_S_DATA.CMD_ACK then
local ack = cmd.val ---@type coord_ack 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 elseif cmd.key == CRD_S_DATA.RESEND_PLC_BUILD then
-- re-send PLC build -- 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 -- 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 local unit = self.units[unit_id] ---@type reactor_unit
builds[unit_id] = unit.get_build(true, false, false) 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 elseif cmd.key == CRD_S_DATA.RESEND_RTU_BUILD then
local unit_id = cmd.val.unit local unit_id = cmd.val.unit
if unit_id > 0 then if unit_id > 0 then
@ -398,16 +402,16 @@ function coordinator.new_session(id, in_queue, out_queue, timeout, facility)
local builds = {} local builds = {}
local unit = self.units[unit_id] ---@type reactor_unit 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 else
-- re-send facility RTU builds -- 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 -- 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.retry_times.f_builds_packet = util.time() + PARTIAL_RETRY_PERIOD
self.acks.fac_builds = false 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 end
else else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)") 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 periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then 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 periodics.keep_alive = 0
end end

View File

@ -8,12 +8,11 @@ local svqtypes = require("supervisor.session.svqtypes")
local plc = {} local plc = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local RPLC_TYPES = comms.RPLC_TYPES local RPLC_TYPE = comms.RPLC_TYPE
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMAND = comms.UNIT_COMMAND
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -47,18 +46,16 @@ local PERIODICS = {
} }
-- PLC supervisor session -- PLC supervisor session
---@nodiscard
---@param id integer session ID ---@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 in_queue mqueue in message queue
---@param out_queue mqueue out message queue ---@param out_queue mqueue out message queue
---@param timeout number communications timeout ---@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 log_header = "plc_session(" .. id .. "): "
local self = { local self = {
for_reactor = for_reactor,
in_q = in_queue,
out_q = out_queue,
commanded_state = false, commanded_state = false,
commanded_burn_rate = 0.0, commanded_burn_rate = 0.0,
auto_cmd_token = 0, auto_cmd_token = 0,
@ -244,34 +241,35 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- send an RPLC packet -- send an RPLC packet
---@param msg_type RPLC_TYPES ---@param msg_type RPLC_TYPE
---@param msg table ---@param msg table
local function _send(msg_type, msg) local function _send(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local r_pkt = comms.rplc_packet() local r_pkt = comms.rplc_packet()
r_pkt.make(for_reactor, msg_type, msg) r_pkt.make(reactor_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.out_q.push_packet(s_pkt) out_queue.push_packet(s_pkt)
self.seq_num = self.seq_num + 1 self.seq_num = self.seq_num + 1
end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES ---@param msg_type SCADA_MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
-- get an ACK status -- get an ACK status
---@nodiscard
---@param pkt rplc_frame ---@param pkt rplc_frame
---@return boolean|nil ack ---@return boolean|nil ack
local function _get_ack(pkt) local function _get_ack(pkt)
@ -297,10 +295,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- process packet -- process packet
if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then if pkt.scada_frame.protocol() == PROTOCOL.RPLC then
-- check reactor ID -- check reactor ID
if pkt.id ~= for_reactor then if pkt.id ~= reactor_id then
log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id) log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. reactor_id .. " != " .. pkt.id)
return return
end end
@ -308,7 +306,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
self.plc_conn_watchdog.feed() self.plc_conn_watchdog.feed()
-- handle packet by type -- handle packet by type
if pkt.type == RPLC_TYPES.STATUS then if pkt.type == RPLC_TYPE.STATUS then
-- status packet received, update data -- status packet received, update data
if pkt.length >= 5 then if pkt.length >= 5 then
self.sDB.last_status_update = pkt.data[1] 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 else
log.debug(log_header .. "RPLC status packet length mismatch") log.debug(log_header .. "RPLC status packet length mismatch")
end end
elseif pkt.type == RPLC_TYPES.MEK_STRUCT then elseif pkt.type == RPLC_TYPE.MEK_STRUCT then
-- received reactor structure, record it -- received reactor structure, record it
if pkt.length == 14 then if pkt.length == 14 then
local status = pcall(_copy_struct, pkt.data) local status = pcall(_copy_struct, pkt.data)
if status then if status then
-- copied in structure data OK -- copied in structure data OK
self.received_struct = true 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 else
-- error copying structure data -- error copying structure data
log.error(log_header .. "failed to parse struct packet 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 else
log.debug(log_header .. "RPLC struct packet length mismatch") log.debug(log_header .. "RPLC struct packet length mismatch")
end end
elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then elseif pkt.type == RPLC_TYPE.MEK_BURN_RATE then
-- burn rate acknowledgement -- burn rate acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if ack then if ack then
@ -360,12 +358,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
unit = self.for_reactor, unit = reactor_id,
cmd = UNIT_COMMANDS.SET_BURN, cmd = UNIT_COMMAND.SET_BURN,
ack = ack ack = ack
}) })
elseif pkt.type == RPLC_TYPES.RPS_ENABLE then elseif pkt.type == RPLC_TYPE.RPS_ENABLE then
-- enable acknowledgement -- enable acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if ack then if ack then
@ -375,12 +373,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
unit = self.for_reactor, unit = reactor_id,
cmd = UNIT_COMMANDS.START, cmd = UNIT_COMMAND.START,
ack = ack ack = ack
}) })
elseif pkt.type == RPLC_TYPES.RPS_SCRAM then elseif pkt.type == RPLC_TYPE.RPS_SCRAM then
-- manual SCRAM acknowledgement -- manual SCRAM acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if ack then if ack then
@ -391,12 +389,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
unit = self.for_reactor, unit = reactor_id,
cmd = UNIT_COMMANDS.SCRAM, cmd = UNIT_COMMAND.SCRAM,
ack = ack ack = ack
}) })
elseif pkt.type == RPLC_TYPES.RPS_ASCRAM then elseif pkt.type == RPLC_TYPE.RPS_ASCRAM then
-- automatic SCRAM acknowledgement -- automatic SCRAM acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if ack then if ack then
@ -405,7 +403,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
elseif ack == false then elseif ack == false then
log.debug(log_header .. " automatic SCRAM failed!") log.debug(log_header .. " automatic SCRAM failed!")
end end
elseif pkt.type == RPLC_TYPES.RPS_STATUS then elseif pkt.type == RPLC_TYPE.RPS_STATUS then
-- RPS status packet received, copy data -- RPS status packet received, copy data
if pkt.length == 14 then if pkt.length == 14 then
local status = pcall(_copy_rps_status, pkt.data) 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 else
log.debug(log_header .. "RPLC RPS status packet length mismatch") log.debug(log_header .. "RPLC RPS status packet length mismatch")
end end
elseif pkt.type == RPLC_TYPES.RPS_ALARM then elseif pkt.type == RPLC_TYPE.RPS_ALARM then
-- RPS alarm -- RPS alarm
if pkt.length == 13 then if pkt.length == 13 then
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) }) 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 else
log.debug(log_header .. "RPLC RPS alarm packet length mismatch") log.debug(log_header .. "RPLC RPS alarm packet length mismatch")
end end
elseif pkt.type == RPLC_TYPES.RPS_RESET then elseif pkt.type == RPLC_TYPE.RPS_RESET then
-- RPS reset acknowledgement -- RPS reset acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if ack then if ack then
@ -443,18 +441,18 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- send acknowledgement to coordinator -- send acknowledgement to coordinator
self.out_q.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, { out_queue.push_data(svqtypes.SV_Q_DATA.CRDN_ACK, {
unit = self.for_reactor, unit = reactor_id,
cmd = UNIT_COMMANDS.RESET_RPS, cmd = UNIT_COMMAND.RESET_RPS,
ack = ack 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 -- RPS auto control reset acknowledgement
local ack = _get_ack(pkt) local ack = _get_ack(pkt)
if not ack then if not ack then
log.debug(log_header .. "RPS auto reset failed") log.debug(log_header .. "RPS auto reset failed")
end end
elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then elseif pkt.type == RPLC_TYPE.AUTO_BURN_RATE then
if pkt.length == 1 then if pkt.length == 1 then
local ack = pkt.data[1] local ack = pkt.data[1]
@ -473,8 +471,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
end end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
@ -491,7 +489,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
else else
@ -503,17 +501,22 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- get the session ID -- get the session ID
---@nodiscard
function public.get_id() return id end function public.get_id() return id end
-- get the session database -- get the session database
---@nodiscard
function public.get_db() return self.sDB end function public.get_db() return self.sDB end
-- check if ramping is completed by first verifying auto command token ack -- check if ramping is completed by first verifying auto command token ack
---@nodiscard
function public.is_ramp_complete() 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) return (self.sDB.auto_ack_token == self.auto_cmd_token) and (self.commanded_burn_rate == self.sDB.mek_status.act_burn_rate)
end end
-- get the reactor structure -- get the reactor structure
---@nodiscard
---@return mek_struct|table struct struct or empty table
function public.get_struct() function public.get_struct()
if self.received_struct then if self.received_struct then
return self.sDB.mek_struct return self.sDB.mek_struct
@ -523,6 +526,8 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- get the reactor status -- get the reactor status
---@nodiscard
---@return mek_status|table struct status or empty table
function public.get_status() function public.get_status()
if self.received_status_cache then if self.received_status_cache then
return self.sDB.mek_status return self.sDB.mek_status
@ -532,11 +537,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
end end
-- get the reactor RPS status -- get the reactor RPS status
---@nodiscard
function public.get_rps() function public.get_rps()
return self.sDB.rps_status return self.sDB.rps_status
end end
-- get the general status information -- get the general status information
---@nodiscard
function public.get_general_status() function public.get_general_status()
return { return {
self.sDB.last_status_update, 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 ---@param ramp boolean true to ramp, false to not
function public.auto_set_burn(rate, ramp) function public.auto_set_burn(rate, ramp)
self.ramping_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 end
-- check if a timer matches this session's watchdog -- check if a timer matches this session's watchdog
---@nodiscard
function public.check_wd(timer) function public.check_wd(timer)
return self.plc_conn_watchdog.is_timer(timer) and self.connected return self.plc_conn_watchdog.is_timer(timer) and self.connected
end end
@ -575,12 +583,13 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- close the connection -- close the connection
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println("connection to reactor " .. self.for_reactor .. " PLC closed by server") println("connection to reactor " .. reactor_id .. " PLC closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
-- iterate the session -- iterate the session
---@nodiscard
---@return boolean connected ---@return boolean connected
function public.iterate() function public.iterate()
if self.connected then 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() 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 -- get a new message to process
local message = self.in_q.pop() local message = in_queue.pop()
if message ~= nil then if message ~= nil then
if message.qtype == mqueue.TYPE.PACKET 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 if cmd == PLC_S_CMDS.ENABLE then
-- enable reactor -- enable reactor
if not self.auto_lock then if not self.auto_lock then
_send(RPLC_TYPES.RPS_ENABLE, {}) _send(RPLC_TYPE.RPS_ENABLE, {})
end end
elseif cmd == PLC_S_CMDS.SCRAM then elseif cmd == PLC_S_CMDS.SCRAM then
-- SCRAM reactor -- SCRAM reactor
self.acks.scram = false self.acks.scram = false
self.retry_times.scram_req = util.time() + INITIAL_WAIT 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 elseif cmd == PLC_S_CMDS.ASCRAM then
-- SCRAM reactor -- SCRAM reactor
self.acks.ascram = false self.acks.ascram = false
self.retry_times.ascram_req = util.time() + INITIAL_WAIT 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 elseif cmd == PLC_S_CMDS.RPS_RESET then
-- reset RPS -- reset RPS
self.acks.ascram = true self.acks.ascram = true
self.acks.rps_reset = false self.acks.rps_reset = false
self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT 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 elseif cmd == PLC_S_CMDS.RPS_AUTO_RESET then
if self.sDB.rps_status.automatic or self.sDB.rps_status.timeout 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 end
else else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)") 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.ramping_rate = false
self.acks.burn_rate = false self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT 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
end end
elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then 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.ramping_rate = true
self.acks.burn_rate = false self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT 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
end end
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then 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.acks.burn_rate = not self.ramping_rate
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT 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
end end
else else
@ -688,7 +697,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
-- exit if connection was closed -- exit if connection was closed
if not self.connected then 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") log.info(log_header .. "session closed by remote host")
return self.connected return self.connected
end end
@ -705,7 +714,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
periodics.keep_alive = periodics.keep_alive + elapsed periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then 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 periodics.keep_alive = 0
end end
@ -722,7 +731,7 @@ function plc.new_session(id, for_reactor, in_queue, out_queue, timeout)
if not self.received_struct then if not self.received_struct then
if rtimes.struct_req - util.time() <= 0 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 rtimes.struct_req = util.time() + RETRY_PERIOD
end end
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 not self.received_status_cache then
if rtimes.status_req - util.time() <= 0 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 rtimes.status_req = util.time() + RETRY_PERIOD
end end
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 rtimes.burn_rate_req - util.time() <= 0 then
if self.auto_cmd_token > 0 then if self.auto_cmd_token > 0 then
if self.auto_lock 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 else
-- would have been an auto command, but disengaged, so stop retrying -- would have been an auto command, but disengaged, so stop retrying
self.acks.burn_rate = true self.acks.burn_rate = true
end end
elseif not self.auto_lock then 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 else
-- shouldn't be in this state, just pretend it was acknowledged -- shouldn't be in this state, just pretend it was acknowledged
self.acks.burn_rate = true 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 not self.acks.scram then
if rtimes.scram_req - util.time() <= 0 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 rtimes.scram_req = util.time() + RETRY_PERIOD
end end
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 not self.acks.ascram then
if rtimes.ascram_req - util.time() <= 0 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 rtimes.ascram_req = util.time() + RETRY_PERIOD
end end
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 not self.acks.rps_reset then
if rtimes.rps_reset_req - util.time() <= 0 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 rtimes.rps_reset_req = util.time() + RETRY_PERIOD
end end
end end

View File

@ -5,6 +5,7 @@
local rsctl = {} local rsctl = {}
-- create a new redstone RTU I/O controller -- create a new redstone RTU I/O controller
---@nodiscard
---@param redstone_rtus table redstone RTU sessions ---@param redstone_rtus table redstone RTU sessions
function rsctl.new(redstone_rtus) function rsctl.new(redstone_rtus)
---@class rs_controller ---@class rs_controller

View File

@ -1,7 +1,7 @@
local comms = require("scada-common.comms") local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") 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 util = require("scada-common.util")
local svqtypes = require("supervisor.session.svqtypes") local svqtypes = require("supervisor.session.svqtypes")
@ -18,9 +18,9 @@ local svrs_turbinev = require("supervisor.session.rtu.turbinev")
local rtu = {} local rtu = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local SCADA_MGMT_TYPE = comms.SCADA_MGMT_TYPE
local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -32,6 +32,7 @@ local PERIODICS = {
} }
-- create a new RTU session -- create a new RTU session
---@nodiscard
---@param id integer session ID ---@param id integer session ID
---@param in_queue mqueue in message queue ---@param in_queue mqueue in message queue
---@param out_queue mqueue out 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 log_header = "rtu_session(" .. id .. "): "
local self = { local self = {
in_q = in_queue,
out_q = out_queue,
modbus_q = mqueue.new(), modbus_q = mqueue.new(),
advert = advertisement, advert = advertisement,
fac_units = facility.get_units(), 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.index)
advert_validator.assert_type_int(unit_advert.reactor) 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) advert_validator.assert_type_table(unit_advert.rsio)
end end
@ -113,7 +112,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
end end
local type_string = util.strval(u_type) 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 -- 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 if unit_advert.reactor > 0 then
local target_unit = self.fac_units[unit_advert.reactor] ---@type reactor_unit 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 -- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_redstone(unit) end 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+) -- boiler (Mekanism 10.1+)
unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q) unit = svrs_boilerv.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_boiler(unit) end 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+) -- turbine (Mekanism 10.1+)
unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q) unit = svrs_turbinev.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_turbine(unit) end 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 -- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then target_unit.add_envd(unit) end 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)) log.error(util.c(log_header, "bad advertisement: encountered unsupported reactor-specific RTU type ", type_string))
end end
else else
if u_type == RTU_UNIT_TYPES.REDSTONE then if u_type == RTU_UNIT_TYPE.REDSTONE then
-- redstone -- redstone
unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q) unit = svrs_redstone.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_redstone(unit) end 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 -- induction matrix
unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q) unit = svrs_imatrix.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_imatrix(unit) end 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 -- super-critical phase shifter
unit = svrs_sps.new(id, i, unit_advert, self.modbus_q) 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 -- solar neutron activator
unit = svrs_sna.new(id, i, unit_advert, self.modbus_q) 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 -- environment detector
unit = svrs_envd.new(id, i, unit_advert, self.modbus_q) unit = svrs_envd.new(id, i, unit_advert, self.modbus_q)
if type(unit) ~= "nil" then facility.add_envd(unit) end 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 function _send_modbus(m_pkt)
local s_pkt = comms.scada_packet() 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 self.seq_num = self.seq_num + 1
end end
-- send a SCADA management packet -- send a SCADA management packet
---@param msg_type SCADA_MGMT_TYPES ---@param msg_type SCADA_MGMT_TYPE
---@param msg table ---@param msg table
local function _send_mgmt(msg_type, msg) local function _send_mgmt(msg_type, msg)
local s_pkt = comms.scada_packet() local s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(msg_type, msg) 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 self.seq_num = self.seq_num + 1
end end
@ -231,15 +230,15 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
self.rtu_conn_watchdog.feed() self.rtu_conn_watchdog.feed()
-- process packet -- 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 if self.units[pkt.unit_id] ~= nil then
local unit = self.units[pkt.unit_id] ---@type unit_session local unit = self.units[pkt.unit_id] ---@type unit_session
---@diagnostic disable-next-line: param-type-mismatch ---@diagnostic disable-next-line: param-type-mismatch
unit.handle_packet(pkt) unit.handle_packet(pkt)
end end
elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then elseif pkt.scada_frame.protocol() == PROTOCOL.SCADA_MGMT then
-- handle management packet -- handle management packet
if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then if pkt.type == SCADA_MGMT_TYPE.KEEP_ALIVE then
-- keep alive reply -- keep alive reply
if pkt.length == 2 then if pkt.length == 2 then
local srv_start = pkt.data[1] local srv_start = pkt.data[1]
@ -256,20 +255,17 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
else else
log.debug(log_header .. "SCADA keep alive packet length mismatch") log.debug(log_header .. "SCADA keep alive packet length mismatch")
end end
elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then elseif pkt.type == SCADA_MGMT_TYPE.CLOSE then
-- close the session -- close the session
_close() _close()
elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then elseif pkt.type == SCADA_MGMT_TYPE.RTU_ADVERT then
-- RTU unit advertisement -- RTU unit advertisement
log.debug(log_header .. "received updated advertisement") log.debug(log_header .. "received updated advertisement")
-- copy advertisement and remove version tag
self.advert = pkt.data self.advert = pkt.data
table.remove(self.advert, 1)
-- handle advertisement; this will re-create all unit sub-sessions -- handle advertisement; this will re-create all unit sub-sessions
_handle_advertisement() _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 if pkt.length == 1 then
local unit_id = pkt.data[1] local unit_id = pkt.data[1]
if self.units[unit_id] ~= nil then 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 function public.get_id() return id end
-- check if a timer matches this session's watchdog -- check if a timer matches this session's watchdog
---@nodiscard
---@param timer number ---@param timer number
function public.check_wd(timer) function public.check_wd(timer)
return self.rtu_conn_watchdog.is_timer(timer) and self.connected 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 -- close the connection
function public.close() function public.close()
_close() _close()
_send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) _send_mgmt(SCADA_MGMT_TYPE.CLOSE, {})
println(log_header .. "connection to RTU closed by server") println(log_header .. "connection to RTU closed by server")
log.info(log_header .. "session closed by server") log.info(log_header .. "session closed by server")
end end
-- iterate the session -- iterate the session
---@nodiscard
---@return boolean connected ---@return boolean connected
function public.iterate() function public.iterate()
if self.connected then 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() 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 -- get a new message to process
local msg = self.in_q.pop() local msg = in_queue.pop()
if msg ~= nil then if msg ~= nil then
if msg.qtype == mqueue.TYPE.PACKET 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 periodics.keep_alive = periodics.keep_alive + elapsed
if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then 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 periodics.keep_alive = 0
end end
@ -389,7 +387,7 @@ function rtu.new_session(id, in_queue, out_queue, timeout, advertisement, facili
-- instruction with body -- instruction with body
local cmd = msg.message ---@type queue_data local cmd = msg.message ---@type queue_data
if cmd.key == unit_session.RTU_US_DATA.BUILD_CHANGED then 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 end
end end

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local boilerv = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = { local TXN_TYPES = {
@ -32,14 +31,15 @@ local PERIODICS = {
} }
-- create a new boilerv rtu session runner -- create a new boilerv rtu session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function boilerv.new(session_id, unit_id, advert, out_queue) function boilerv.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.BOILER_VALVE then if advert.type ~= RTU_UNIT_TYPE.BOILER_VALVE then
log.error("attempt to instantiate boilerv RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate boilerv RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -239,6 +239,7 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local envd = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = { local TXN_TYPES = {
@ -23,14 +22,15 @@ local PERIODICS = {
} }
-- create a new environment detector rtu session runner -- create a new environment detector rtu session runner
---@nodiscard
---@param session_id integer ---@param session_id integer
---@param unit_id integer ---@param unit_id integer
---@param advert rtu_advertisement ---@param advert rtu_advertisement
---@param out_queue mqueue ---@param out_queue mqueue
function envd.new(session_id, unit_id, advert, out_queue) function envd.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.ENV_DETECTOR then if advert.type ~= RTU_UNIT_TYPE.ENV_DETECTOR then
log.error("attempt to instantiate envd RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate envd RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -100,6 +100,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local imatrix = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = { local TXN_TYPES = {
@ -32,14 +31,15 @@ local PERIODICS = {
} }
-- create a new imatrix rtu session runner -- create a new imatrix rtu session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function imatrix.new(session_id, unit_id, advert, out_queue) function imatrix.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.IMATRIX then if advert.type ~= RTU_UNIT_TYPE.IMATRIX then
log.error("attempt to instantiate imatrix RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate imatrix RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -213,6 +213,7 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,6 +1,4 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -9,7 +7,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local redstone = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local IO_PORT = rsio.IO local IO_PORT = rsio.IO
@ -47,14 +45,15 @@ local PERIODICS = {
---@field req IO_LVL ---@field req IO_LVL
-- create a new redstone rtu session runner -- create a new redstone rtu session runner
---@nodiscard
---@param session_id integer ---@param session_id integer
---@param unit_id integer ---@param unit_id integer
---@param advert rtu_advertisement ---@param advert rtu_advertisement
---@param out_queue mqueue ---@param out_queue mqueue
function redstone.new(session_id, unit_id, advert, out_queue) function redstone.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.REDSTONE then if advert.type ~= RTU_UNIT_TYPE.REDSTONE then
log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate redstone RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -120,6 +119,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_in[port].phy) end,
---@param active boolean ---@param active boolean
write = function (active) end write = function (active) end
@ -134,6 +134,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_dig_io ---@class rs_db_dig_io
local io_f = { local io_f = {
---@nodiscard
read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end, read = function () return rsio.digital_is_active(port, self.phy_io.digital_out[port].phy) end,
---@param active boolean ---@param active boolean
write = function (active) write = function (active)
@ -151,6 +152,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_in[port].phy end, read = function () return self.phy_io.analog_in[port].phy end,
---@param value integer ---@param value integer
@ -166,6 +168,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
---@class rs_db_ana_io ---@class rs_db_ana_io
local io_f = { local io_f = {
---@nodiscard
---@return integer ---@return integer
read = function () return self.phy_io.analog_out[port].phy end, read = function () return self.phy_io.analog_out[port].phy end,
---@param value integer ---@param value integer
@ -380,6 +383,7 @@ function redstone.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local sna = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = { local TXN_TYPES = {
@ -29,14 +28,15 @@ local PERIODICS = {
} }
-- create a new sna rtu session runner -- create a new sna rtu session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function sna.new(session_id, unit_id, advert, out_queue) function sna.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.SNA then if advert.type ~= RTU_UNIT_TYPE.SNA then
log.error("attempt to instantiate sna RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate sna RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -176,6 +176,7 @@ function sna.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local types = require("scada-common.types") local types = require("scada-common.types")
local util = require("scada-common.util") local util = require("scada-common.util")
@ -7,7 +6,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local sps = {} 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 MODBUS_FCODE = types.MODBUS_FCODE
local TXN_TYPES = { local TXN_TYPES = {
@ -32,14 +31,15 @@ local PERIODICS = {
} }
-- create a new sps rtu session runner -- create a new sps rtu session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function sps.new(session_id, unit_id, advert, out_queue) function sps.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.SPS then if advert.type ~= RTU_UNIT_TYPE.SPS then
log.error("attempt to instantiate sps RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate sps RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -113,7 +113,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- query the tanks of the device -- query the tanks of the device
local function _request_tanks() local function _request_tanks()
-- read input registers 11 through 19 (start = 11, count = 9) -- 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 end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
@ -223,6 +223,7 @@ function sps.new(session_id, unit_id, advert, out_queue)
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -1,4 +1,3 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log") local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue") local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types") local types = require("scada-common.types")
@ -9,7 +8,7 @@ local unit_session = require("supervisor.session.rtu.unit_session")
local turbinev = {} 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 DUMPING_MODE = types.DUMPING_MODE
local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_FCODE = types.MODBUS_FCODE
@ -44,14 +43,15 @@ local PERIODICS = {
} }
-- create a new turbinev rtu session runner -- create a new turbinev rtu session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer RTU unit ID ---@param unit_id integer RTU unit ID
---@param advert rtu_advertisement RTU advertisement table ---@param advert rtu_advertisement RTU advertisement table
---@param out_queue mqueue RTU unit message out queue ---@param out_queue mqueue RTU unit message out queue
function turbinev.new(session_id, unit_id, advert, out_queue) function turbinev.new(session_id, unit_id, advert, out_queue)
-- type check -- type check
if advert.type ~= RTU_UNIT_TYPES.TURBINE_VALVE then if advert.type ~= RTU_UNIT_TYPE.TURBINE_VALVE then
log.error("attempt to instantiate turbinev RTU for type '" .. advert.type .. "'. this is a bug.") log.error("attempt to instantiate turbinev RTU for type '" .. types.rtu_type_to_string(advert.type) .. "'. this is a bug.")
return nil return nil
end end
@ -92,7 +92,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
flow_rate = 0, flow_rate = 0,
prod_rate = 0, prod_rate = 0,
steam_input_rate = 0, steam_input_rate = 0,
dumping_mode = DUMPING_MODE.IDLE ---@type DUMPING_MODE dumping_mode = DUMPING_MODE.IDLE ---@type dumping_mode
}, },
tanks = { tanks = {
last_update = 0, last_update = 0,
@ -123,7 +123,7 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
end end
-- set the dumping mode -- set the dumping mode
---@param mode DUMPING_MODE ---@param mode dumping_mode
local function _set_dump_mode(mode) local function _set_dump_mode(mode)
-- write holding register 1 -- write holding register 1
self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }) 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 end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() return self.db end function public.get_db() return self.db end
return public return public

View File

@ -9,6 +9,7 @@ local txnctrl = {}
local TIMEOUT = 2000 -- 2000ms max wait local TIMEOUT = 2000 -- 2000ms max wait
-- create a new transaction controller -- create a new transaction controller
---@nodiscard
function txnctrl.new() function txnctrl.new()
local self = { local self = {
list = {}, list = {},
@ -22,16 +23,19 @@ function txnctrl.new()
local remove = table.remove local remove = table.remove
-- get the length of the transaction list -- get the length of the transaction list
---@nodiscard
function public.length() function public.length()
return #self.list return #self.list
end end
-- check if there are no active transactions -- check if there are no active transactions
---@nodiscard
function public.empty() function public.empty()
return #self.list == 0 return #self.list == 0
end end
-- create a new transaction of the given type -- create a new transaction of the given type
---@nodiscard
---@param txn_type integer ---@param txn_type integer
---@return integer txn_id ---@return integer txn_id
function public.create(txn_type) function public.create(txn_type)
@ -49,6 +53,7 @@ function txnctrl.new()
end end
-- mark a transaction as resolved to get its transaction type -- mark a transaction as resolved to get its transaction type
---@nodiscard
---@param txn_id integer ---@param txn_id integer
---@return integer txn_type ---@return integer txn_type
function public.resolve(txn_id) function public.resolve(txn_id)

View File

@ -8,7 +8,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl")
local unit_session = {} local unit_session = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_FCODE = types.MODBUS_FCODE
local MODBUS_EXCODE = types.MODBUS_EXCODE 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 unit_session.RTU_US_DATA = RTU_US_DATA
-- create a new unit session runner -- create a new unit session runner
---@nodiscard
---@param session_id integer RTU session ID ---@param session_id integer RTU session ID
---@param unit_id integer MODBUS unit ID ---@param unit_id integer MODBUS unit ID
---@param advert rtu_advertisement RTU advertisement for this unit ---@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 ---@param txn_tags table transaction log tags
function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags) function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_tags)
local self = { local self = {
log_tag = log_tag,
txn_tags = txn_tags,
unit_id = unit_id,
device_index = advert.index, device_index = advert.index,
reactor = advert.reactor, reactor = advert.reactor,
out_q = out_queue,
transaction_controller = txnctrl.new(), transaction_controller = txnctrl.new(),
connected = true, connected = true,
device_fail = false 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 m_pkt = comms.modbus_packet()
local txn_id = self.transaction_controller.create(txn_type) 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 return txn_id
end end
-- try to resolve a MODBUS transaction -- try to resolve a MODBUS transaction
---@nodiscard
---@param m_pkt modbus_frame MODBUS packet ---@param m_pkt modbus_frame MODBUS packet
---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID ---@return integer|false txn_type, integer txn_id transaction type or false on error/busy, transaction ID
function protected.try_resolve(m_pkt) function protected.try_resolve(m_pkt)
if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then if m_pkt.scada_frame.protocol() == PROTOCOL.MODBUS_TCP then
if m_pkt.unit_id == self.unit_id then if m_pkt.unit_id == unit_id then
local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) 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 if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then
-- transaction incomplete or failed -- transaction incomplete or failed
@ -135,26 +133,35 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
end end
-- get the public interface -- get the public interface
---@nodiscard
function protected.get() return public end function protected.get() return public end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
-- get the unit ID -- get the unit ID
---@nodiscard
function public.get_session_id() return session_id end function public.get_session_id() return session_id end
-- get the unit ID -- 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 -- get the device index
---@nodiscard
function public.get_device_idx() return self.device_index end function public.get_device_idx() return self.device_index end
-- get the reactor ID -- get the reactor ID
---@nodiscard
function public.get_reactor() return self.reactor end function public.get_reactor() return self.reactor end
-- get the command queue -- get the command queue
---@nodiscard
function public.get_cmd_queue() return protected.in_q end function public.get_cmd_queue() return protected.in_q end
-- close this unit -- close this unit
---@nodiscard
function public.close() self.connected = false end function public.close() self.connected = false end
-- check if this unit is connected -- check if this unit is connected
---@nodiscard
function public.is_connected() return self.connected end function public.is_connected() return self.connected end
-- check if this unit is faulted -- check if this unit is faulted
---@nodiscard
function public.is_faulted() return self.device_fail end function public.is_faulted() return self.device_fail end
-- PUBLIC TEMPLATE FUNCTIONS -- -- PUBLIC TEMPLATE FUNCTIONS --
@ -179,6 +186,7 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
end end
-- get the unit session database -- get the unit session database
---@nodiscard
function public.get_db() function public.get_db()
log.debug("template unit_session.get_db() called", true) log.debug("template unit_session.get_db() called", true)
return {} return {}

View File

@ -183,9 +183,10 @@ local function _free_closed(sessions)
end end
-- find a session by remote port -- find a session by remote port
---@nodiscard
---@param list table ---@param list table
---@param port integer ---@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) local function _find_session(list, port)
for i = 1, #list do for i = 1, #list do
if list[i].r_port == port then return list[i] end if list[i].r_port == port then return list[i] end
@ -212,54 +213,63 @@ function svsessions.relink_modem(modem)
end end
-- find an RTU session by the remote port -- find an RTU session by the remote port
---@nodiscard
---@param remote_port integer ---@param remote_port integer
---@return rtu_session_struct|nil ---@return rtu_session_struct|nil
function svsessions.find_rtu_session(remote_port) function svsessions.find_rtu_session(remote_port)
-- check RTU sessions -- check RTU sessions
---@diagnostic disable-next-line: return-type-mismatch local session = _find_session(self.rtu_sessions, remote_port)
return _find_session(self.rtu_sessions, remote_port) ---@cast session rtu_session_struct
return session
end end
-- find a PLC session by the remote port -- find a PLC session by the remote port
---@nodiscard
---@param remote_port integer ---@param remote_port integer
---@return plc_session_struct|nil ---@return plc_session_struct|nil
function svsessions.find_plc_session(remote_port) function svsessions.find_plc_session(remote_port)
-- check PLC sessions -- check PLC sessions
---@diagnostic disable-next-line: return-type-mismatch local session = _find_session(self.plc_sessions, remote_port)
return _find_session(self.plc_sessions, remote_port) ---@cast session plc_session_struct
return session
end end
-- find a PLC/RTU session by the remote port -- find a PLC/RTU session by the remote port
---@nodiscard
---@param remote_port integer ---@param remote_port integer
---@return plc_session_struct|rtu_session_struct|nil ---@return plc_session_struct|rtu_session_struct|nil
function svsessions.find_device_session(remote_port) function svsessions.find_device_session(remote_port)
-- check RTU sessions -- check RTU sessions
local s = _find_session(self.rtu_sessions, remote_port) local session = _find_session(self.rtu_sessions, remote_port)
-- check PLC sessions -- 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 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 -- only one coordinator is allowed, but this is kept to be consistent with all other session tables
---@nodiscard
---@param remote_port integer ---@param remote_port integer
---@return nil ---@return coord_session_struct|nil
function svsessions.find_coord_session(remote_port) function svsessions.find_coord_session(remote_port)
-- check coordinator sessions -- check coordinator sessions
---@diagnostic disable-next-line: return-type-mismatch local session = _find_session(self.coord_sessions, remote_port)
return _find_session(self.coord_sessions, remote_port) ---@cast session coord_session_struct
return session
end end
-- get the a coordinator session if exists -- get the a coordinator session if exists
---@nodiscard
---@return coord_session_struct|nil ---@return coord_session_struct|nil
function svsessions.get_coord_session() function svsessions.get_coord_session()
return self.coord_sessions[1] return self.coord_sessions[1]
end end
-- get a session by reactor ID -- get a session by reactor ID
---@nodiscard
---@param reactor integer ---@param reactor integer
---@return plc_session_struct|nil session ---@return plc_session_struct|nil session
function svsessions.get_reactor_session(reactor) function svsessions.get_reactor_session(reactor)
@ -275,6 +285,7 @@ function svsessions.get_reactor_session(reactor)
end end
-- establish a new PLC session -- establish a new PLC session
---@nodiscard
---@param local_port integer ---@param local_port integer
---@param remote_port integer ---@param remote_port integer
---@param for_reactor integer ---@param for_reactor integer
@ -314,6 +325,7 @@ function svsessions.establish_plc_session(local_port, remote_port, for_reactor,
end end
-- establish a new RTU session -- establish a new RTU session
---@nodiscard
---@param local_port integer ---@param local_port integer
---@param remote_port integer ---@param remote_port integer
---@param advertisement table ---@param advertisement table
@ -344,6 +356,7 @@ function svsessions.establish_rtu_session(local_port, remote_port, advertisement
end end
-- establish a new coordinator session -- establish a new coordinator session
---@nodiscard
---@param local_port integer ---@param local_port integer
---@param remote_port integer ---@param remote_port integer
---@param version string ---@param version string

View File

@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions")
local config = require("supervisor.config") local config = require("supervisor.config")
local supervisor = require("supervisor.supervisor") local supervisor = require("supervisor.supervisor")
local SUPERVISOR_VERSION = "beta-v0.12.2" local SUPERVISOR_VERSION = "v0.13.1"
local print = util.print local print = util.print
local println = util.println local println = util.println
@ -81,7 +81,7 @@ local function main()
local modem = ppm.get_wireless_modem() local modem = ppm.get_wireless_modem()
if modem == nil then if modem == nil then
println("boot> wireless modem not found") println("startup> wireless modem not found")
log.fatal("no wireless modem on startup") log.fatal("no wireless modem on startup")
return return
end end
@ -110,7 +110,7 @@ local function main()
-- we only care if this is our wireless modem -- we only care if this is our wireless modem
if device == modem then if device == modem then
println_ts("wireless modem disconnected!") println_ts("wireless modem disconnected!")
log.error("comms modem disconnected!") log.warning("comms modem disconnected")
else else
log.warning("non-comms modem disconnected") log.warning("non-comms modem disconnected")
end end
@ -127,9 +127,9 @@ local function main()
superv_comms.reconnect_modem(modem) superv_comms.reconnect_modem(modem)
println_ts("wireless modem reconnected.") println_ts("wireless modem reconnected.")
log.info("comms modem reconnected.") log.info("comms modem reconnected")
else else
log.info("wired modem reconnected.") log.info("wired modem reconnected")
end end
end end
end end

View File

@ -6,10 +6,10 @@ local svsessions = require("supervisor.session.svsessions")
local supervisor = {} local supervisor = {}
local PROTOCOLS = comms.PROTOCOLS local PROTOCOL = comms.PROTOCOL
local DEVICE_TYPES = comms.DEVICE_TYPES local DEVICE_TYPE = comms.DEVICE_TYPE
local ESTABLISH_ACK = comms.ESTABLISH_ACK 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 print = util.print
local println = util.println local println = util.println
@ -17,6 +17,7 @@ local print_ts = util.print_ts
local println_ts = util.println_ts local println_ts = util.println_ts
-- supervisory controller communications -- supervisory controller communications
---@nodiscard
---@param version string supervisor version ---@param version string supervisor version
---@param num_reactors integer number of reactors ---@param num_reactors integer number of reactors
---@param cooling_conf table cooling configuration table ---@param cooling_conf table cooling configuration table
@ -26,32 +27,24 @@ local println_ts = util.println_ts
---@param range integer trusted device connection range ---@param range integer trusted device connection range
function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range) function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen, coord_listen, range)
local self = { local self = {
version = version, last_est_acks = {}
num_reactors = num_reactors,
modem = modem,
dev_listen = dev_listen,
coord_listen = coord_listen,
reactor_struct_cache = nil
} }
---@class superv_comms
local public = {}
comms.set_trusted_range(range) comms.set_trusted_range(range)
-- PRIVATE FUNCTIONS -- -- PRIVATE FUNCTIONS --
-- configure modem channels -- configure modem channels
local function _conf_channels() local function _conf_channels()
self.modem.closeAll() modem.closeAll()
self.modem.open(self.dev_listen) modem.open(dev_listen)
self.modem.open(self.coord_listen) modem.open(coord_listen)
end end
_conf_channels() _conf_channels()
-- link modem to svsessions -- 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 -- send an establish request response to a PLC/RTU
---@param dest integer ---@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 s_pkt = comms.scada_packet()
local m_pkt = comms.mgmt_packet() local m_pkt = comms.mgmt_packet()
m_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg) m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) 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 end
-- send coordinator connection establish response -- 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 s_pkt = comms.scada_packet()
local c_pkt = comms.mgmt_packet() local c_pkt = comms.mgmt_packet()
c_pkt.make(SCADA_MGMT_TYPES.ESTABLISH, msg) c_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, msg)
s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, c_pkt.raw_sendable()) 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 end
-- PUBLIC FUNCTIONS -- -- PUBLIC FUNCTIONS --
---@class superv_comms
local public = {}
-- reconnect a newly connected modem -- reconnect a newly connected modem
---@param modem table ---@param new_modem table
---@diagnostic disable-next-line: redefined-local function public.reconnect_modem(new_modem)
function public.reconnect_modem(modem) modem = new_modem
self.modem = modem svsessions.relink_modem(new_modem)
svsessions.relink_modem(self.modem)
_conf_channels() _conf_channels()
end end
-- parse a packet -- parse a packet
---@nodiscard
---@param side string ---@param side string
---@param sender integer ---@param sender integer
---@param reply_to 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 if s_pkt.is_valid() then
-- get as MODBUS TCP packet -- 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() local m_pkt = comms.modbus_packet()
if m_pkt.decode(s_pkt) then if m_pkt.decode(s_pkt) then
pkt = m_pkt.get() pkt = m_pkt.get()
end end
-- get as RPLC packet -- get as RPLC packet
elseif s_pkt.protocol() == PROTOCOLS.RPLC then elseif s_pkt.protocol() == PROTOCOL.RPLC then
local rplc_pkt = comms.rplc_packet() local rplc_pkt = comms.rplc_packet()
if rplc_pkt.decode(s_pkt) then if rplc_pkt.decode(s_pkt) then
pkt = rplc_pkt.get() pkt = rplc_pkt.get()
end end
-- get as SCADA management packet -- 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() local mgmt_pkt = comms.mgmt_packet()
if mgmt_pkt.decode(s_pkt) then if mgmt_pkt.decode(s_pkt) then
pkt = mgmt_pkt.get() pkt = mgmt_pkt.get()
end end
-- get as coordinator packet -- 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() local crdn_pkt = comms.crdn_packet()
if crdn_pkt.decode(s_pkt) then if crdn_pkt.decode(s_pkt) then
pkt = crdn_pkt.get() 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() local protocol = packet.scada_frame.protocol()
-- device (RTU/PLC) listening channel -- device (RTU/PLC) listening channel
if l_port == self.dev_listen then if l_port == dev_listen then
if protocol == PROTOCOLS.MODBUS_TCP then if protocol == PROTOCOL.MODBUS_TCP then
---@cast packet modbus_frame
-- look for an associated session -- look for an associated session
local session = svsessions.find_rtu_session(r_port) 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 -- any other packet should be session related, discard it
log.debug("discarding MODBUS_TCP packet without a known session") log.debug("discarding MODBUS_TCP packet without a known session")
end end
elseif protocol == PROTOCOLS.RPLC then elseif protocol == PROTOCOL.RPLC then
---@cast packet rplc_frame
-- look for an associated session -- look for an associated session
local session = svsessions.find_plc_session(r_port) 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") 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 }) _send_dev_establish(packet.scada_frame.seq_num() + 1, r_port, { ESTABLISH_ACK.DENY })
end end
elseif protocol == PROTOCOLS.SCADA_MGMT then elseif protocol == PROTOCOL.SCADA_MGMT then
---@cast packet mgmt_frame
-- look for an associated session -- look for an associated session
local session = svsessions.find_device_session(r_port) 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 if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) 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 -- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1 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] local dev_type = packet.data[3]
if comms_v ~= comms.version then if comms_v ~= comms.version then
log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v, if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
" (expected v", comms.version, ")")) log.info(util.c("dropping device 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 }) self.last_est_acks[r_port] = ESTABLISH_ACK.BAD_VERSION
return
end 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 -- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4] 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 if plc_id == false then
-- reactor already has a PLC assigned -- reactor already has a PLC assigned
if self.last_est_acks[r_port] ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id)) 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 }) _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
else else
-- got an ID; assigned to a reactor successfully -- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [:", r_port, "] \xbb reactor ", reactor_id, " connected")) 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)) 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 }) _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
end end
else else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type") log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")
_send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
end end
elseif dev_type == DEVICE_TYPES.RTU then elseif dev_type == DEVICE_TYPE.RTU then
if packet.length == 4 then if packet.length == 4 then
-- this is an RTU advertisement for a new session -- this is an RTU advertisement for a new session
local rtu_advert = packet.data[4] 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")) 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)) 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 }) _send_dev_establish(next_seq_id, r_port, { ESTABLISH_ACK.ALLOW })
else else
log.debug("RTU_ESTABLISH: packet length mismatch") 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") log.debug("illegal packet type " .. protocol .. " on device listening channel")
end end
-- coordinator listening channel -- coordinator listening channel
elseif l_port == self.coord_listen then elseif l_port == coord_listen then
-- look for an associated session -- look for an associated session
local session = svsessions.find_coord_session(r_port) 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 -- SCADA management packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler
session.in_queue.push_packet(packet) 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 -- establish a new session
local next_seq_id = packet.scada_frame.seq_num() + 1 local next_seq_id = packet.scada_frame.seq_num() + 1
@ -267,21 +274,21 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
local dev_type = packet.data[3] local dev_type = packet.data[3]
if comms_v ~= comms.version then if comms_v ~= comms.version then
log.debug(util.c("dropping establish packet with incorrect comms version v", comms_v, if self.last_est_acks[r_port] ~= ESTABLISH_ACK.BAD_VERSION then
" (expected v", comms.version, ")")) log.info(util.c("dropping coordinator 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 }) self.last_est_acks[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 end
_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
-- this is an attempt to establish a new session -- this is an attempt to establish a new session
local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v) local s_id = svsessions.establish_coord_session(l_port, r_port, firmware_v)
if s_id ~= false then if s_id ~= false then
local config = { self.num_reactors } local config = { num_reactors }
for i = 1, #cooling_conf do for i = 1, #cooling_conf do
table.insert(config, cooling_conf[i].BOILERS) table.insert(config, cooling_conf[i].BOILERS)
table.insert(config, cooling_conf[i].TURBINES) table.insert(config, cooling_conf[i].TURBINES)
@ -289,11 +296,18 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
println(util.c("CRD (",firmware_v, ") [:", r_port, "] \xbb connected")) 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)) 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.ALLOW, config })
self.last_est_acks[r_port] = ESTABLISH_ACK.ALLOW
else else
log.debug("CRDN_ESTABLISH: denied new coordinator due to already being connected to another coordinator") 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 }) _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.COLLISION })
end end
end
else else
log.debug("CRDN_ESTABLISH: establish packet length mismatch") log.debug("CRDN_ESTABLISH: establish packet length mismatch")
_send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY }) _send_crdn_establish(next_seq_id, r_port, { ESTABLISH_ACK.DENY })
@ -302,7 +316,8 @@ function supervisor.comms(version, num_reactors, cooling_conf, modem, dev_listen
-- any other packet should be session related, discard it -- any other packet should be session related, discard it
log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session") log.debug(r_port .. "->" .. l_port .. ": discarding SCADA_MGMT packet without a known session")
end end
elseif protocol == PROTOCOLS.SCADA_CRDN then elseif protocol == PROTOCOL.SCADA_CRDN then
---@cast packet crdn_frame
-- coordinator packet -- coordinator packet
if session ~= nil then if session ~= nil then
-- pass the packet onto the session handler -- pass the packet onto the session handler

View File

@ -12,20 +12,15 @@ local rsctl = require("supervisor.session.rsctl")
local unit = {} local unit = {}
local WASTE_MODE = types.WASTE_MODE local WASTE_MODE = types.WASTE_MODE
local ALARM = types.ALARM local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE 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 PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local IO = rsio.IO local IO = rsio.IO
local FLOW_STABILITY_DELAY_MS = 15000
local DT_KEYS = { local DT_KEYS = {
ReactorBurnR = "RBR", ReactorBurnR = "RBR",
ReactorTemp = "RTP", ReactorTemp = "RTP",
@ -41,18 +36,16 @@ local DT_KEYS = {
TurbinePower = "TPR" TurbinePower = "TPR"
} }
---@alias ALARM_INT_STATE integer ---@enum ALARM_INT_STATE
local AISTATE = { local AISTATE = {
INACTIVE = 0, INACTIVE = 1,
TRIPPING = 1, TRIPPING = 2,
TRIPPED = 2, TRIPPED = 3,
ACKED = 3, ACKED = 4,
RING_BACK = 4, RING_BACK = 5,
RING_BACK_TRIPPING = 5 RING_BACK_TRIPPING = 6
} }
unit.FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS
---@class alarm_def ---@class alarm_def
---@field state ALARM_INT_STATE internal alarm state ---@field state ALARM_INT_STATE internal alarm state
---@field trip_time integer time (ms) when first tripped ---@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) ---@field tier integer alarm urgency tier (0 = highest)
-- create a new reactor unit -- 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_boilers integer number of boilers expected
---@param num_turbines integer number of turbines 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 ---@class _unit_self
local self = { local self = {
r_id = for_reactor, r_id = reactor_id,
plc_s = nil, ---@class plc_session_struct plc_s = nil, ---@class plc_session_struct
plc_i = nil, ---@class plc_session plc_i = nil, ---@class plc_session
num_boilers = num_boilers, num_boilers = num_boilers,
num_turbines = num_turbines, num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE }, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
defs = { FLOW_STABILITY_DELAY_MS = FLOW_STABILITY_DELAY_MS },
-- rtus -- rtus
redstone = {}, redstone = {},
boilers = {}, boilers = {},
@ -278,6 +271,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
local function _reset_dt(key) self.deltas[key] = nil end local function _reset_dt(key) self.deltas[key] = nil end
-- get the delta t of a value -- get the delta t of a value
---@nodiscard
---@param key string value key ---@param key string value key
---@return number value value or 0 if not known ---@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 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 --#region redstone I/O
local __rs_w = self.io_ctl.digital_write local __rs_w = self.io_ctl.digital_write
local __rs_r = self.io_ctl.digital_read
-- valves -- valves
local waste_pu = { open = function () __rs_w(IO.WASTE_PU, true) end, close = function () __rs_w(IO.WASTE_PU, false) end } 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
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 -- if it is degraded or not ready, the limit will be 0
---@nodiscard
---@return integer lim_br100 ---@return integer lim_br100
function public.a_get_effective_limit() function public.a_get_effective_limit()
if not self.db.control.ready or self.db.control.degraded or self.plc_cache.rps_trip then 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 end
-- check if ramping is complete (burn rate is same as target) -- check if ramping is complete (burn rate is same as target)
---@nodiscard
---@return boolean complete ---@return boolean complete
function public.a_ramp_complete() function public.a_ramp_complete()
if self.plc_i ~= nil then if self.plc_i ~= nil then
@ -610,7 +604,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- acknowledge an alarm (if possible) -- acknowledge an alarm (if possible)
---@param id ALARM alarm ID ---@param id ALARM alarm ID
function public.ack_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 self.db.alarm_states[id] = ALARM_STATE.ACKED
end end
end end
@ -618,7 +612,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- reset an alarm (if possible) -- reset an alarm (if possible)
---@param id ALARM alarm ID ---@param id ALARM alarm ID
function public.reset_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 self.db.alarm_states[id] = ALARM_STATE.INACTIVE
end end
end end
@ -675,6 +669,8 @@ function unit.new(for_reactor, num_boilers, num_turbines)
--#region --#region
-- check if a critical alarm is tripped -- check if a critical alarm is tripped
---@nodiscard
---@return boolean tripped
function public.has_critical_alarm() function public.has_critical_alarm()
for _, alarm in pairs(self.alarms) do for _, alarm in pairs(self.alarms) do
if alarm.tier == PRIO.CRITICAL and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then 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 end
-- get build properties of all machines -- get build properties of all machines
---@nodiscard
---@param inc_plc boolean? true/nil to include PLC build, false to exclude ---@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_boilers boolean? true/nil to include boiler builds, false to exclude
---@param inc_turbines boolean? true/nil to include turbine 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 end
-- get reactor status -- get reactor status
---@nodiscard
function public.get_reactor_status() function public.get_reactor_status()
local status = {} local status = {}
if self.plc_i ~= nil then if self.plc_i ~= nil then
@ -728,6 +726,7 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end end
-- get RTU statuses -- get RTU statuses
---@nodiscard
function public.get_rtu_statuses() function public.get_rtu_statuses()
local status = {} local status = {}
@ -769,20 +768,25 @@ function unit.new(for_reactor, num_boilers, num_turbines)
end end
-- get the annunciator status -- get the annunciator status
---@nodiscard
function public.get_annunciator() return self.db.annunciator end function public.get_annunciator() return self.db.annunciator end
-- get the alarm states -- get the alarm states
---@nodiscard
function public.get_alarms() return self.db.alarm_states end function public.get_alarms() return self.db.alarm_states end
-- get information required for automatic reactor control -- get information required for automatic reactor control
---@nodiscard
function public.get_control_inf() return self.db.control end function public.get_control_inf() return self.db.control end
-- get unit state -- get unit state
---@nodiscard
function public.get_state() function public.get_state()
return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded } return { self.status_text[1], self.status_text[2], self.waste_mode, self.db.control.ready, self.db.control.degraded }
end end
-- get the reactor ID -- get the reactor ID
---@nodiscard
function public.get_id() return self.r_id end function public.get_id() return self.r_id end
--#endregion --#endregion

View File

@ -1,3 +1,4 @@
local const = require("scada-common.constants")
local log = require("scada-common.log") local log = require("scada-common.log")
local rsio = require("scada-common.rsio") local rsio = require("scada-common.rsio")
local types = require("scada-common.types") local types = require("scada-common.types")
@ -5,17 +6,16 @@ local util = require("scada-common.util")
local plc = require("supervisor.session.plc") 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 DUMPING_MODE = types.DUMPING_MODE
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local IO = rsio.IO local IO = rsio.IO
local PLC_S_CMDS = plc.PLC_S_CMDS local PLC_S_CMDS = plc.PLC_S_CMDS
local aistate_string = { local AISTATE_NAMES = {
"INACTIVE", "INACTIVE",
"TRIPPING", "TRIPPING",
"TRIPPED", "TRIPPED",
@ -24,11 +24,10 @@ local aistate_string = {
"RING_BACK_TRIPPING" "RING_BACK_TRIPPING"
} }
-- background radiation 0.0000001 Sv/h (99.99 nSv/h) local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
-- "green tint" radiation 0.00001 Sv/h (10 uSv/h)
-- damaging radiation 0.00006 Sv/h (60 uSv/h) local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
local RADIATION_ALERT_LEVEL = 0.00001 -- 10 uSv/h local ALARM_LIMS = const.ALARM_LIMITS
local RADIATION_ALARM_LEVEL = 0.00005 -- 50 uSv/h, not yet damaging but this isn't good
---@class unit_logic_extension ---@class unit_logic_extension
local logic = {} local logic = {}
@ -108,15 +107,15 @@ function logic.update_annunciator(self)
-- update other annunciator fields -- update other annunciator fields
self.db.annunciator.ReactorSCRAM = plc_db.rps_tripped 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.ManualReactorSCRAM = plc_db.rps_trip_cause == types.RPS_TRIP_CAUSE.MANUAL
self.db.annunciator.AutoReactorSCRAM = plc_db.rps_trip_cause == types.rps_status_t.automatic 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.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.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < ANNUNC_LIMS.RCSFlowLow
self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < 0.4 self.db.annunciator.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100 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 <= 0.01 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 >= 0.85 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) -- 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: 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 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 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 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 for i = 1, #self.envd do
local envd = self.envd[i] ---@type unit_session local envd = self.envd[i] ---@type unit_session
self.db.annunciator.RadiationMonitor = util.trinary(envd.is_faulted(), 2, 3) 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 break
end 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) 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 -- 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 sfmismatch = sfmismatch or boiler_steam_dt_sum > 2.0 or boiler_water_dt_sum < -2.0
self.db.annunciator.SteamFeedMismatch = sfmismatch self.db.annunciator.SteamFeedMismatch = sfmismatch
self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate and total_flow_rate ~= 0 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 else
alarm.state = AISTATE.TRIPPED alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.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 ", log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]")) types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end end
else else
alarm.trip_time = util.time_ms() 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 if elapsed > (alarm.hold_time * 1000) then
alarm.state = AISTATE.TRIPPED alarm.state = AISTATE.TRIPPED
self.db.alarm_states[alarm.id] = ALARM_STATE.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 ", log.info(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): TRIPPED [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]")) types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end end
elseif int_state == AISTATE.RING_BACK_TRIPPING then elseif int_state == AISTATE.RING_BACK_TRIPPING then
alarm.trip_time = 0 alarm.trip_time = 0
@ -431,8 +430,8 @@ local function _update_alarm_state(self, tripped, alarm)
-- check for state change -- check for state change
if alarm.state ~= int_state then if alarm.state ~= int_state then
local change_str = util.c(aistate_string[int_state + 1], " -> ", aistate_string[alarm.state + 1]) 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_string[alarm.id], "): ", change_str)) log.debug(util.c("UNIT ", self.r_id, " ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], "): ", change_str))
end end
end end
@ -449,7 +448,7 @@ function logic.update_alarms(self)
-- Containment Radiation -- Containment Radiation
local rad_alarm = false local rad_alarm = false
for i = 1, #self.envd do 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 break
end end
_update_alarm_state(self, rad_alarm, self.alarms.ContainmentRadiation) _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) _update_alarm_state(self, (plc_cache.temp >= 1200) or rps_high_temp, self.alarms.ReactorOverTemp)
-- High Temperature -- 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 -- 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 -- High Waste
local rps_high_waste = plc_cache.rps_status.ex_waste and not self.last_rps_trips.ex_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) -- RPS Transient (excludes timeouts and manual trips)
local rps_alarm = false 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: -- annunciator indicators for these states may not indicate a real issue when:
-- > flow is ramping up right after reactor start -- > flow is ramping up right after reactor start
-- > flow is ramping down after reactor shutdown -- > 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 rcs_trans = rcs_trans or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch
end end
@ -530,8 +529,8 @@ function logic.update_auto_safety(public, self)
for _, alarm in pairs(self.alarms) do for _, alarm in pairs(self.alarms) do
if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then if alarm.tier <= PRIO.URGENT and (alarm.state == AISTATE.TRIPPED or alarm.state == AISTATE.ACKED) then
if not self.auto_was_alarmed 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 ", log.info(util.c("UNIT ", self.r_id, " AUTO SCRAM due to ALARM ", alarm.id, " (", types.ALARM_NAMES[alarm.id], ") [PRIORITY ",
types.alarm_prio_string[alarm.tier + 1],"]")) types.ALARM_PRIORITY_NAMES[alarm.tier],"]"))
end end
alarmed = true alarmed = true
@ -555,6 +554,7 @@ function logic.update_status_text(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
---@nodiscard
---@param alarm table alarm entry ---@param alarm table alarm entry
---@return boolean active ---@return boolean active
local function is_active(alarm) local function is_active(alarm)
@ -620,7 +620,7 @@ function logic.update_status_text(self)
self.status_text[2] = "insufficient fuel input rate" self.status_text[2] = "insufficient fuel input rate"
elseif self.db.annunciator.WasteLineOcclusion then elseif self.db.annunciator.WasteLineOcclusion then
self.status_text[2] = "insufficient waste output rate" 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" self.status_text[2] = "awaiting flow stability"
else else
self.status_text[2] = "system nominal" self.status_text[2] = "system nominal"
@ -636,9 +636,9 @@ function logic.update_status_text(self)
cause = "core temperature high" cause = "core temperature high"
elseif plc_db.rps_trip_cause == "no_coolant" then elseif plc_db.rps_trip_cause == "no_coolant" then
cause = "insufficient coolant" cause = "insufficient coolant"
elseif plc_db.rps_trip_cause == "full_waste" then elseif plc_db.rps_trip_cause == "ex_waste" then
cause = "excess waste" 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" cause = "excess heated coolant"
elseif plc_db.rps_trip_cause == "no_fuel" then elseif plc_db.rps_trip_cause == "no_fuel" then
cause = "insufficient fuel" cause = "insufficient fuel"
@ -670,7 +670,7 @@ function logic.update_status_text(self)
end end
end end
else else
self.status_text = { "Reactor Off-line", "awaiting connection..." } self.status_text = { "REACTOR OFF-LINE", "awaiting connection..." }
end end
end end
@ -680,6 +680,7 @@ function logic.handle_redstone(self)
local AISTATE = self.types.AISTATE local AISTATE = self.types.AISTATE
-- check if an alarm is active (tripped or ack'd) -- check if an alarm is active (tripped or ack'd)
---@nodiscard
---@param alarm table alarm entry ---@param alarm table alarm entry
---@return boolean active ---@return boolean active
local function is_active(alarm) local function is_active(alarm)