From 3da7b74cfba0c4a61633e0906196907440949e09 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 11:07:16 -0400 Subject: [PATCH 001/168] initial base supervisor code --- supervisor/startup.lua | 78 +++++++++++++++++++++++++++------------ supervisor/supervisor.lua | 27 ++++++++++++++ 2 files changed, 81 insertions(+), 24 deletions(-) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 4ff56a9..cec58c0 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -7,22 +7,29 @@ os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") -os.loadAPI("supervisor/config.lua") -os.loadAPI("supervisor/supervisor.lua") +os.loadAPI("config.lua") +os.loadAPI("supervisor.lua") local SUPERVISOR_VERSION = "alpha-v0.1.0" +local print = util.print +local println = util.println local print_ts = util.print_ts +local println_ts = util.println_ts +log._info("========================================") +log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) +log._info("========================================") + +println(">> SCADA Supervisor " .. SUPERVISOR_VERSION .. " <<") + +-- mount connected devices ppm.mount_all() -local modem = ppm.get_device("modem") - -print("| SCADA Supervisor - " .. SUPERVISOR_VERSION .. " |") - --- we need a modem +local modem = ppm.get_wireless_modem() if modem == nil then - print("Please connect a modem.") + println("boot> wireless modem not found") + log._warning("no wireless modem on startup") return end @@ -33,34 +40,57 @@ if config.SYSTEM_TYPE == "active" then end -- start comms, open all channels -if not modem.isOpen(config.SCADA_DEV_LISTEN) then - modem.open(config.SCADA_DEV_LISTEN) -end -if not modem.isOpen(config.SCADA_FO_CHANNEL) then - modem.open(config.SCADA_FO_CHANNEL) -end -if not modem.isOpen(config.SCADA_SV_CHANNEL) then - modem.open(config.SCADA_SV_CHANNEL) -end - local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_FO_CHANNEL, config.SCADA_SV_CHANNEL) -- base loop clock (4Hz, 5 ticks) -local loop_tick = os.startTimer(0.25) +local loop_clock = os.startTimer(0.25) -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() -- handle event - if event == "timer" and param1 == loop_tick then + if event == "peripheral_detach" then + local device = ppm.handle_unmount(param1) + + if device.type == "modem" then + -- we only care if this is our wireless modem + if device.dev == modem then + println_ts("wireless modem disconnected!") + log._error("comms modem disconnected!") + else + log._warning("non-comms modem disconnected") + end + end + elseif event == "peripheral" then + local type, device = ppm.mount(param1) + + if type == "modem" then + if device.isWireless() then + -- reconnected modem + modem = device + superv_comms.reconnect_modem(modem) + + println_ts("wireless modem reconnected.") + log._info("comms modem reconnected.") + else + log._info("wired modem reconnected.") + end + end + elseif event == "timer" and param1 == loop_clock then -- basic event tick, send keep-alives + loop_clock = os.startTimer(0.25) elseif event == "modem_message" then -- got a packet - elseif event == "terminate" then - -- safe exit - print_ts("[alert] terminated\n") + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + log._warning("terminate requested, exiting...") -- todo: attempt failover, alert hot backup - return + break end end + +println_ts("exited") +log._info("exited") diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 5f988a8..d9b125c 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -12,4 +12,31 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_chan sv_channel = sv_channel, reactor_struct_cache = nil } + + -- PRIVATE FUNCTIONS -- + + -- open all channels + local _open_channels = function () + if not self.modem.isOpen(self.dev_listen) then + self.modem.open(self.dev_listen) + end + if not self.modem.isOpen(self.fo_channel) then + self.modem.open(self.fo_channel) + end + if not self.modem.isOpen(self.sv_channel) then + self.modem.open(self.sv_channel) + end + end + + -- PUBLIC FUNCTIONS -- + + -- reconnect a newly connected modem + local reconnect_modem = function (modem) + self.modem = modem + _open_channels() + end + + return { + reconnect_modem = reconnect_modem + } end From 04f8dc7d75b6333f84e3e6f33c510d7a0095af64 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 21 Apr 2022 10:17:14 -0400 Subject: [PATCH 002/168] readme update about coordinator --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 37b9124..ac2d6c4 100644 --- a/README.md +++ b/README.md @@ -10,16 +10,16 @@ This project implements concepts of a SCADA system in ComputerCraft (because why ![Architecture](https://upload.wikimedia.org/wikipedia/commons/thumb/1/10/Functional_levels_of_a_Distributed_Control_System.svg/1000px-Functional_levels_of_a_Distributed_Control_System.svg.png) SCADA and industrial automation terminology is used throughout the project, such as: -- Supervisory Computer: Gathers data and control the process +- Supervisory Computer: Gathers data and controls the process - Coordinating Computer: Used as the HMI component, user requests high-level processing operations - RTU: Remote Terminal Unit - PLC: Programmable Logic Controller ## ComputerCraft Architecture -### Coordinating Computers +### Coordinator Server -There can be one or more of these. They can be either an Advanced Computer or a Pocket Computer. +There can only be one of these. This server acts as a hybrid of levels 3 & 4 in the SCADA diagram above. In addition to viewing status and controlling processes with advanced monitors, it can host access for one or more Pocket computers. ### Supervisory Computers From 4842f9cb0dfc3ddd105315cd522766d96874078d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 21 Apr 2022 10:26:02 -0400 Subject: [PATCH 003/168] moved packet constructors and fixes to comms namespace references in plc comms code --- reactor-plc/plc.lua | 84 +++--------------------- reactor-plc/startup.lua | 2 +- rtu/rtu.lua | 2 +- rtu/startup.lua | 2 +- scada-common/comms.lua | 140 ++++++++++++++++++++++++++++++++++++++++ scada-common/modbus.lua | 62 +----------------- 6 files changed, 153 insertions(+), 139 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 6843303..3389ae7 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,6 +1,10 @@ -- #REQUIRES comms.lua -- #REQUIRES ppm.lua +local PROTOCOLS = comms.PROTOCOLS +local RPLC_TYPES = comms.RPLC_TYPES +local RPLC_LINKING = comms.RPLC_LINKING + -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main SCADA supervisor/coordinator control @@ -193,78 +197,6 @@ function iss_init(reactor) } end -function rplc_packet() - local self = { - frame = nil, - id = nil, - type = nil, - length = nil, - body = nil - } - - local _rplc_type_valid = function () - return self.type == RPLC_TYPES.KEEP_ALIVE or - self.type == RPLC_TYPES.LINK_REQ or - self.type == RPLC_TYPES.STATUS or - self.type == RPLC_TYPES.MEK_STRUCT or - self.type == RPLC_TYPES.MEK_SCRAM or - self.type == RPLC_TYPES.MEK_ENABLE or - self.type == RPLC_TYPES.MEK_BURN_RATE or - self.type == RPLC_TYPES.ISS_ALARM or - self.type == RPLC_TYPES.ISS_GET or - self.type == RPLC_TYPES.ISS_CLEAR - end - - -- make an RPLC packet - local make = function (id, packet_type, length, data) - self.id = id - self.type = packet_type - self.length = length - self.data = data - end - - -- decode an RPLC packet from a SCADA frame - local decode = function (frame) - if frame then - self.frame = frame - - if frame.protocol() == comms.PROTOCOLS.RPLC then - local data = frame.data() - local ok = #data > 2 - - if ok then - make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) - ok = _rplc_type_valid() - end - - return ok - else - log._debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) - return false - end - else - log._debug("nil frame encountered", true) - return false - end - end - - local get = function () - return { - scada_frame = self.frame, - id = self.id, - type = self.type, - length = self.length, - data = self.data - } - end - - return { - make = make, - decode = decode, - get = get - } -end - -- reactor PLC communications function comms_init(id, modem, local_port, server_port, reactor, iss) local self = { @@ -432,13 +364,13 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if s_pkt.is_valid() then -- get as RPLC packet if s_pkt.protocol() == PROTOCOLS.RPLC then - local rplc_pkt = rplc_packet() + local rplc_pkt = comms.rplc_packet() if rplc_pkt.decode(s_pkt) then pkt = rplc_pkt.get() end -- get as SCADA management packet elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then - local mgmt_pkt = mgmt_packet() + local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_packet.get() end @@ -478,11 +410,11 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) send_status() log._debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then - -- @todo: make sure this doesn't become an MITM security risk + -- @todo: make sure this doesn't become a MITM security risk print_ts("received unsolicited link denial, unlinking\n") log._debug("unsolicited rplc link request denied") elseif link_ack == RPLC_LINKING.COLLISION then - -- @todo: make sure this doesn't become an MITM security risk + -- @todo: make sure this doesn't become a MITM security risk print_ts("received unsolicited link collision, unlinking\n") log._warning("unsolicited rplc link request collision") else diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 4d13e74..80cb626 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.0" +local R_PLC_VERSION = "alpha-v0.2.1" local print = util.print local println = util.println diff --git a/rtu/rtu.lua b/rtu/rtu.lua index efa31f2..23ace96 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -153,7 +153,7 @@ function rtu_comms(modem, local_port, server_port) if s_pkt.is_valid() then -- get as MODBUS TCP packet if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then - local m_pkt = modbus.packet() + local m_pkt = comms.modbus_packet() if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end diff --git a/rtu/startup.lua b/rtu/startup.lua index 576dd47..bb7e3e1 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.2.0" +local RTU_VERSION = "alpha-v0.2.1" local print = util.print local println = util.println diff --git a/scada-common/comms.lua b/scada-common/comms.lua index ac0c3c5..4b77fb1 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -88,6 +88,9 @@ function scada_packet() local modem_event = function () return self.modem_msg_in end local raw = function () return self.raw end + local sender = function () return self.s_port end + local receiver = function () return self.r_port end + local is_valid = function () return self.valid end local seq_num = function () return self.seq_num end @@ -107,6 +110,8 @@ function scada_packet() receive = receive, modem_event = modem_event, raw = raw, + sender = sender, + receiver = receiver, is_valid = is_valid, seq_num = seq_num, protocol = protocol, @@ -115,6 +120,141 @@ function scada_packet() } end +-- MODBUS packet +function modbus_packet() + local self = { + frame = nil, + txn_id = txn_id, + protocol = protocol, + length = length, + unit_id = unit_id, + func_code = func_code, + data = data + } + + -- make a MODBUS packet + local make = function (txn_id, protocol, length, unit_id, func_code, data) + self.txn_id = txn_id + self.protocol = protocol + self.length = length + self.unit_id = unit_id + self.func_code = func_code + self.data = data + end + + -- decode a MODBUS packet from a SCADA frame + local decode = function (frame) + if frame then + self.frame = frame + + local data = frame.data() + local size_ok = #data ~= 6 + + if size_ok then + make(data[1], data[2], data[3], data[4], data[5], data[6]) + end + + return size_ok and self.protocol == comms.PROTOCOLS.MODBUS_TCP + else + log._debug("nil frame encountered", true) + return false + end + end + + -- get this packet + local get = function () + return { + scada_frame = self.frame, + txn_id = self.txn_id, + protocol = self.protocol, + length = self.length, + unit_id = self.unit_id, + func_code = self.func_code, + data = self.data + } + end + + return { + make = make, + decode = decode, + get = get + } +end + +-- reactor PLC packet +function rplc_packet() + local self = { + frame = nil, + id = nil, + type = nil, + length = nil, + body = nil + } + + local _rplc_type_valid = function () + return self.type == RPLC_TYPES.KEEP_ALIVE or + self.type == RPLC_TYPES.LINK_REQ or + self.type == RPLC_TYPES.STATUS or + self.type == RPLC_TYPES.MEK_STRUCT or + self.type == RPLC_TYPES.MEK_SCRAM or + self.type == RPLC_TYPES.MEK_ENABLE or + self.type == RPLC_TYPES.MEK_BURN_RATE or + self.type == RPLC_TYPES.ISS_ALARM or + self.type == RPLC_TYPES.ISS_GET or + self.type == RPLC_TYPES.ISS_CLEAR + end + + -- make an RPLC packet + local make = function (id, packet_type, length, data) + self.id = id + self.type = packet_type + self.length = length + self.data = data + end + + -- decode an RPLC packet from a SCADA frame + local decode = function (frame) + if frame then + self.frame = frame + + if frame.protocol() == comms.PROTOCOLS.RPLC then + local data = frame.data() + local ok = #data > 2 + + if ok then + make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) + ok = _rplc_type_valid() + end + + return ok + else + log._debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) + return false + end + else + log._debug("nil frame encountered", true) + return false + end + end + + local get = function () + return { + scada_frame = self.frame, + id = self.id, + type = self.type, + length = self.length, + data = self.data + } + end + + return { + make = make, + decode = decode, + get = get + } +end + +-- SCADA management packet function mgmt_packet() local self = { frame = nil, diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index 8a4137f..a8148b7 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -1,3 +1,5 @@ +-- #REQUIRES comms.lua + -- modbus function codes local MODBUS_FCODE = { READ_COILS = 0x01, @@ -263,63 +265,3 @@ function new(rtu_dev) handle_packet = handle_packet } end - -function packet() - local self = { - frame = nil, - txn_id = txn_id, - protocol = protocol, - length = length, - unit_id = unit_id, - func_code = func_code, - data = data - } - - -- make a MODBUS packet - local make = function (txn_id, protocol, length, unit_id, func_code, data) - self.txn_id = txn_id - self.protocol = protocol - self.length = length - self.unit_id = unit_id - self.func_code = func_code - self.data = data - end - - -- decode a MODBUS packet from a SCADA frame - local decode = function (frame) - if frame then - self.frame = frame - - local data = frame.data() - local size_ok = #data ~= 6 - - if size_ok then - make(data[1], data[2], data[3], data[4], data[5], data[6]) - end - - return size_ok and self.protocol == comms.PROTOCOLS.MODBUS_TCP - else - log._debug("nil frame encountered", true) - return false - end - end - - -- get this packet - local get = function () - return { - scada_frame = self.frame, - txn_id = self.txn_id, - protocol = self.protocol, - length = self.length, - unit_id = self.unit_id, - func_code = self.func_code, - data = self.data - } - end - - return { - make = make, - decode = decode, - get = get - } -end From 0c132f6e43d78d83bb4427c6f69e0348381d2b45 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 21 Apr 2022 10:44:43 -0400 Subject: [PATCH 004/168] todo comment format --- reactor-plc/plc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 3389ae7..7b750e4 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -487,7 +487,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) log._debug("discarding non-link packet before linked") end elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - -- todo + -- @todo end end end From b10a8d94799657b5031b613c7e1abecd974a7a07 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 21 Apr 2022 12:40:21 -0400 Subject: [PATCH 005/168] send ISS status automatically along with PLC status --- reactor-plc/config.lua | 2 +- reactor-plc/plc.lua | 32 ++++++++++++++++++++++++-------- reactor-plc/startup.lua | 14 +++++++------- scada-common/comms.lua | 6 +++--- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index 25f750c..dceeb21 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -1,4 +1,4 @@ --- set to false to run in standalone mode (safety regulation only) +-- set to false to run in offline mode (safety regulation only) NETWORKED = true -- unique reactor ID REACTOR_ID = 1 diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 7b750e4..5c50065 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -207,9 +207,10 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) l_port = local_port, reactor = reactor, iss = iss, - status_cache = nil, scrammed = false, - linked = false + linked = false, + status_cache = nil, + max_burn_rate = nil } -- open modem @@ -328,7 +329,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local _send_iss_status = function () local iss_status = { id = self.id, - type = RPLC_TYPES.ISS_GET, + type = RPLC_TYPES.ISS_STATUS, status = iss.status() } @@ -438,10 +439,17 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_ack(packet.type, self.reactor.activate() == ppm.ACCESS_OK) elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate - local burn_rate = packet.data[1] - local max_burn_rate = self.reactor.getMaxBurnRate() local success = false + local burn_rate = packet.data[1] + local max_burn_rate = self.max_burn_rate + -- if no known max burn rate, check again + if max_burn_rate == nil then + max_burn_rate = self.reactor.getMaxBurnRate() + self.max_burn_rate = max_burn_rate + end + + -- if we know our max burn rate, update current burn rate if in range if max_burn_rate ~= ppm.ACCESS_FAULT then if burn_rate > 0 and burn_rate <= max_burn_rate then success = self.reactor.setBurnRate(burn_rate) @@ -449,9 +457,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end _send_ack(packet.type, success == ppm.ACCESS_OK) - elseif packet.type == RPLC_TYPES.ISS_GET then - -- get the ISS status - _send_iss_status(iss.status()) elseif packet.type == RPLC_TYPES.ISS_CLEAR then -- clear the ISS status iss.reset() @@ -526,6 +531,16 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send(sys_status) end + local send_iss_status = function () + local iss_status = { + id = self.id, + type = RPLC_TYPES.ISS_STATUS, + status = iss.status() + } + + _send(iss_status) + end + local send_iss_alarm = function (cause) local iss_alarm = { id = self.id, @@ -548,6 +563,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) handle_packet = handle_packet, send_link_req = send_link_req, send_status = send_status, + send_iss_status = send_iss_status, send_iss_alarm = send_iss_alarm, is_scrammed = is_scrammed, is_linked = is_linked, diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 80cb626..1737f3f 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.1" +local R_PLC_VERSION = "alpha-v0.2.2" local print = util.print local println = util.println @@ -90,6 +90,7 @@ function init() conn_watchdog = util.new_watchdog(3) log._debug("conn watchdog started") else + println("boot> starting in offline mode"); log._debug("running without networking") end @@ -220,13 +221,13 @@ while true do -- check safety (SCRAM occurs if tripped) if not plc_state.degraded then - local iss_tripped, iss_status, iss_first = iss.check() + local iss_tripped, iss_status_string, iss_first = iss.check() plc_state.scram = plc_state.scram or iss_tripped if iss_first then - println_ts("[ISS] reactor shutdown, safety tripped: " .. iss_status) + println_ts("[ISS] reactor shutdown, safety tripped: " .. iss_status_string) if networked then - plc_comms.send_iss_alarm(iss_status) + plc_comms.send_iss_alarm(iss_status_string) end end else @@ -244,6 +245,7 @@ while true do if plc_comms.is_linked() then if ticks_to_update <= 0 then plc_comms.send_status(iss_tripped, plc_state.degraded) + plc_comms.send_iss_status() ticks_to_update = UPDATE_TICKS end else @@ -275,9 +277,8 @@ while true do -- check for termination request if event == "terminate" or ppm.should_terminate() then - log._warning("terminate requested, exiting...") - -- safe exit + log._warning("terminate requested, exiting...") if plc_state.init_ok then plc_state.scram = true if reactor.scram() ~= ppm.ACCESS_FAULT then @@ -287,7 +288,6 @@ while true do println_ts("exiting, reactor failed to disable") end end - break end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 4b77fb1..9181ce3 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -18,8 +18,8 @@ RPLC_TYPES = { MEK_SCRAM = 4, -- SCRAM reactor MEK_ENABLE = 5, -- enable reactor MEK_BURN_RATE = 6, -- set burn rate - ISS_ALARM = 7, -- ISS alarm broadcast - ISS_GET = 8, -- get ISS status + ISS_STATUS = 7, -- ISS status + ISS_ALARM = 8, -- ISS alarm broadcast ISS_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately) } @@ -200,7 +200,7 @@ function rplc_packet() self.type == RPLC_TYPES.MEK_ENABLE or self.type == RPLC_TYPES.MEK_BURN_RATE or self.type == RPLC_TYPES.ISS_ALARM or - self.type == RPLC_TYPES.ISS_GET or + self.type == RPLC_TYPES.ISS_STATUS or self.type == RPLC_TYPES.ISS_CLEAR end From 991c855c11837822530db3cbd17642d532a625e9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 21 Apr 2022 12:44:46 -0400 Subject: [PATCH 006/168] message queue --- supervisor/mqueue.lua | 48 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 supervisor/mqueue.lua diff --git a/supervisor/mqueue.lua b/supervisor/mqueue.lua new file mode 100644 index 0000000..1aa5869 --- /dev/null +++ b/supervisor/mqueue.lua @@ -0,0 +1,48 @@ +-- +-- Message Queue +-- + +TYPE = { + COMMAND = 0, + PACKET = 1 +} + +function new() + local queue = {} + + local length = function () + return #queue + end + + local empty = function () + return #queue == 0 + end + + local _push = function (qtype, message) + table.insert(queue, { qtype = qtype, message = message }) + end + + local push_packet = function (message) + push(TYPE.PACKET, message) + end + + local push_command = function (message) + push(TYPE.COMMAND, message) + end + + local pop = function () + if #queue > 0 then + return table.remove(queue) + else + return nil + end + end + + return { + length = length, + empty = empty, + push_packet = push_packet, + push_command = push_command, + pop = pop + } +end From fe8326a65d39fbec672183584dc2e579c14b1928 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 22 Apr 2022 09:37:40 -0400 Subject: [PATCH 007/168] Delete old LICENSE --- LICENSE | 674 -------------------------------------------------------- 1 file changed, 674 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. From 805de7463475aaf2f2343c0a4807f996bb5f22d2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 22 Apr 2022 10:05:36 -0400 Subject: [PATCH 008/168] Create LICENSE --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cd46cf --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Mikayla Fischler + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From f7c11febe526659d4322b2b8c6b72671b33b86b2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 10:21:15 -0400 Subject: [PATCH 009/168] check if interface exists before trying to get its device or type --- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 2 +- scada-common/ppm.lua | 8 ++++++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1737f3f..b57845c 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.2" +local R_PLC_VERSION = "alpha-v0.2.3" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index bb7e3e1..0074625 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.2.1" +local RTU_VERSION = "alpha-v0.2.2" local print = util.print local println = util.println diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index fd92b23..f751342 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -174,12 +174,16 @@ end -- get a mounted peripheral by side/interface function get_periph(iface) - return self.mounts[iface].dev + if self.mounts[iface] then + return self.mounts[iface].dev + else return nil end end -- get a mounted peripheral type by side/interface function get_type(iface) - return self.mounts[iface].type + if self.mounts[iface] then + return self.mounts[iface].type + else return nil end end -- get all mounted peripherals by type From 17d0213d58bbb1cadf1fce67ca4f81df1c82d595 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 10:58:18 -0400 Subject: [PATCH 010/168] RTU/PPM bugfixes --- reactor-plc/plc.lua | 10 ---------- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 4 ++-- scada-common/ppm.lua | 14 +++++++++----- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5c50065..c7a6346 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -326,16 +326,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end end - local _send_iss_status = function () - local iss_status = { - id = self.id, - type = RPLC_TYPES.ISS_STATUS, - status = iss.status() - } - - _send(iss_status) - end - -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index b57845c..d413684 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.3" +local R_PLC_VERSION = "alpha-v0.2.4" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index 0074625..42779c6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.2.2" +local RTU_VERSION = "alpha-v0.2.3" local print = util.print local println = util.println @@ -185,7 +185,7 @@ while true do for i = 1, #units do -- find disconnected device - if units[i].device == device then + if units[i].device == device.dev then -- we are going to let the PPM prevent crashes -- return fault flags/codes to MODBUS queries local unit = units[i] diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index f751342..66f26a7 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -116,7 +116,7 @@ function mount_all() dev = pm_dev } - log._debug("PPM: found a " .. self.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") + log._info("PPM: found a " .. self.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") end if #ifaces == 0 then @@ -132,7 +132,7 @@ function mount(iface) for i = 1, #ifaces do if iface == ifaces[i] then - log._debug("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) + log._info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) type = peripheral.getType(iface) pm_dev = peripheral.wrap(iface) @@ -153,9 +153,13 @@ end function handle_unmount(iface) -- what got disconnected? local lost_dev = self.mounts[iface] - local type = lost_dev.type - - log._warning("PPM: lost device " .. type .. " mounted to " .. iface) + + if lost_dev then + local type = lost_dev.type + log._warning("PPM: lost device " .. type .. " mounted to " .. iface) + else + log._error("PPM: lost device unknown to the PPM mounted to " .. iface) + end return lost_dev end From 1bf0d352a12fc190ed22624f4a9108be1feeb8f6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 11:07:59 -0400 Subject: [PATCH 011/168] supervisor sessions work in progress --- scada-common/comms.lua | 63 ++++++++ supervisor/config.lua | 7 +- supervisor/session/coordinator.lua | 0 supervisor/session/plc.lua | 248 +++++++++++++++++++++++++++++ supervisor/session/rtu.lua | 0 supervisor/session/svsessions.lua | 141 ++++++++++++++++ supervisor/startup.lua | 17 +- supervisor/supervisor.lua | 152 ++++++++++++++++-- 8 files changed, 611 insertions(+), 17 deletions(-) create mode 100644 supervisor/session/coordinator.lua create mode 100644 supervisor/session/plc.lua create mode 100644 supervisor/session/rtu.lua create mode 100644 supervisor/session/svsessions.lua diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 9181ce3..cb203bf 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -318,3 +318,66 @@ function mgmt_packet() get = get } end + +-- SCADA coordinator packet +-- @todo +function coord_packet() + local self = { + frame = nil, + type = nil, + length = nil, + data = nil + } + + local _coord_type_valid = function () + -- @todo + return false + end + + -- make a coordinator packet + local make = function (packet_type, length, data) + self.type = packet_type + self.length = length + self.data = data + end + + -- decode a coordinator packet from a SCADA frame + local decode = function (frame) + if frame then + self.frame = frame + + if frame.protocol() == comms.PROTOCOLS.COORD_DATA then + local data = frame.data() + local ok = #data > 1 + + if ok then + make(data[1], data[2], { table.unpack(data, 3, #data) }) + ok = _coord_type_valid() + end + + return ok + else + log._debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true) + return false + end + else + log._debug("nil frame encountered", true) + return false + end + end + + local get = function () + return { + scada_frame = self.frame, + type = self.type, + length = self.length, + data = self.data + } + end + + return { + make = make, + decode = decode, + get = get + } +end diff --git a/supervisor/config.lua b/supervisor/config.lua index bc02177..de10492 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -4,13 +4,12 @@ -- from all PLCs and coordinator(s) while in backup to allow -- instant failover if active goes offline without re-sync SYSTEM_TYPE = 'active' - -- scada network listen for PLC's and RTU's SCADA_DEV_LISTEN = 16000 -- failover synchronization -SCADA_FO_CHANNEL = 16001 +SCADA_FO_LOCAL = 16101 +SCADA_FO_PEER = 16102 -- listen port for SCADA supervisor access by coordinators -SCADA_SV_CHANNEL = 16002 - +SCADA_SV_LISTEN = 16201 -- expected number of reactors NUM_REACTORS = 4 diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua new file mode 100644 index 0000000..e69de29 diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua new file mode 100644 index 0000000..4008a33 --- /dev/null +++ b/supervisor/session/plc.lua @@ -0,0 +1,248 @@ +-- #REQUIRES mqueue.lua +-- #REQUIRES comms.lua +-- #REQUIRES log.lua +-- #REQUIRES util.lua + +local RPLC_TYPES = comms.RPLC_TYPES + +PLC_S_COMMANDS = { + SCRAM = 0, + ENABLE = 1, + ISS_CLEAR = 2 +} + +-- PLC supervisor session +function new_session(id, for_reactor, in_queue, out_queue) + local log_header = "plc_session(" .. id .. "): " + + local self = { + id = id, + for_reactor = for_reactor, + in_q = in_queue, + out_q = out_queue, + commanded_state = false, + -- connection properties + seq_num = 0, + connected = true, + received_struct = false, + plc_conn_watchdog = util.new_watchdog(3) + -- when to next retry one of these requests + retry_times = { + struct_req = 0, + scram_req = 0, + enable_req = 0 + }, + -- session PLC status database + sDB = { + control_state = false, + overridden = false, + degraded = false, + iss_status = { + dmg_crit = false, + ex_hcool = false, + ex_waste = false, + high_temp = false, + no_fuel = false, + no_cool = false, + timed_out = false + }, + mek_status = { + heating_rate = 0, + + status = false, + burn_rate = 0, + act_burn_rate = 0, + temp = 0, + damage = 0, + boil_eff = 0, + env_loss = 0, + + fuel = 0, + fuel_need = 0, + fuel_fill = 0, + waste = 0, + waste_need = 0, + waste_fill = 0, + cool_type = "?", + cool_amnt = 0, + cool_need = 0, + cool_fill = 0, + hcool_type = "?", + hcool_amnt = 0, + hcool_need = 0, + hcool_fill = 0 + }, + mek_struct = { + heat_cap = 0, + fuel_asm = 0, + fuel_sa = 0, + fuel_cap = 0, + waste_cap = 0, + cool_cap = 0, + hcool_cap = 0, + max_burn = 0 + } + } + } + + local _copy_iss_status = function (iss_status) + self.sDB.iss_status.dmg_crit = iss_status[1] + self.sDB.iss_status.ex_hcool = iss_status[2] + self.sDB.iss_status.ex_waste = iss_status[3] + self.sDB.iss_status.high_temp = iss_status[4] + self.sDB.iss_status.no_fuel = iss_status[5] + self.sDB.iss_status.no_cool = iss_status[6] + self.sDB.iss_status.timed_out = iss_status[7] + end + + local _copy_status = function (heating_rate, mek_data) + self.sDB.mek_status.heating_rate = heating_rate + for key, value in pairs(mek_data) do + self.sDB.mek_status[key] = value + end + end + + local _copy_struct = function (mek_data) + for key, value in pairs(mek_data) do + self.sDB.mek_struct[key] = value + end + end + + local _get_ack = function (pkt) + if rplc_packet.length == 1 then + return rplc_packet.data[1] + else + log._warning(log_header .. "RPLC ACK length mismatch") + return nil + end + end + + local get_id = function () return self.id end + + local close = function () self.connected = false end + + local check_wd = function (timer) + return timer == plc_conn_watchdog + end + + local get_struct = function () + if self.received_struct then + return self.sDB.mek_struct + else + -- @todo: need a system in place to re-request this periodically + return nil + end + end + + local iterate = function () + if self.connected and ~self.in_q.empty() then + -- get a new message to process + local message = self.in_q.pop() + + if message.qtype == mqueue.TYPE.PACKET then + -- handle an incoming packet from the PLC + rplc_pkt = message.message.get() + + if rplc_pkt.id == for_reactor then + if rplc_pkt.type == RPLC_TYPES.KEEP_ALIVE then + -- periodic keep alive + elseif rplc_pkt.type == RPLC_TYPES.STATUS then + -- status packet received, update data + if rplc_packet.length == 6 then + -- @todo [1] is timestamp, determine how this will be used (if at all) + self.sDB.control_state = rplc_packet.data[2] + self.sDB.overridden = rplc_packet.data[3] + self.sDB.degraded = rplc_packet.data[4] + + -- attempt to read mek_data table + if rplc_packet.data[6] ~= nil then + local status = pcall(_copy_status, rplc_packet.data[5], rplc_packet.data[6]) + if status then + -- copied in status data OK + else + -- error copying status data + log._error(log_header .. "failed to parse status packet data") + end + else + self.sDB.mek_status.heating_rate = rplc_packet.data[5] + end + else + log._warning(log_header .. "RPLC status packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_STRUCT then + -- received reactor structure, record it + if rplc_packet.length == 1 then + local status = pcall(_copy_struct, rplc_packet.data[1]) + if status then + -- copied in structure data OK + else + -- error copying structure data + log._error(log_header .. "failed to parse struct packet data") + end + else + log._warning(log_header .. "RPLC struct packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_SCRAM then + -- SCRAM acknowledgement + local ack = _get_ack(rplc_pkt) + if ack then + self.sDB.control_state = false + elseif ack == false then + log._warning(log_header .. "SCRAM failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_ENABLE then + -- enable acknowledgement + local ack = _get_ack(rplc_pkt) + if ack then + self.sDB.control_state = true + elseif ack == false then + log._warning(log_header .. "enable failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_BURN_RATE then + -- burn rate acknowledgement + if _get_ack(rplc_pkt) == false then + log._warning(log_header .. "burn rate update failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then + -- ISS status packet received, copy data + if rplc_packet.length == 1 then + local status = pcall(_copy_iss_status, rplc_packet.data[1]) + if status then + -- copied in ISS status data OK + else + -- error copying ISS status data + log._error(log_header .. "failed to parse ISS status packet data") + end + else + log._warning(log_header .. "RPLC ISS status packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.ISS_ALARM then + -- ISS alarm + self.sDB.overridden = true + -- @todo + elseif rplc_pkt.type == RPLC_TYPES.ISS_CLEAR then + -- ISS clear acknowledgement + -- @todo + else + log._warning(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) + end + else + log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. rplc_pkt.id) + end + elseif message.qtype == mqueue.TYPE.COMMAND then + -- handle instruction + + end + end + + return self.connected + end + + return { + get_id = get_id, + check_wd = check_wd, + get_struct = get_struct, + close = close, + iterate = iterate + } +end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua new file mode 100644 index 0000000..e69de29 diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua new file mode 100644 index 0000000..fcc6950 --- /dev/null +++ b/supervisor/session/svsessions.lua @@ -0,0 +1,141 @@ +-- #REQUIRES mqueue.lua +-- #REQUIRES log.lua + +-- Supervisor Sessions Handler + +SESSION_TYPE = { + RTU_SESSION = 0, + PLC_SESSION = 1, + COORD_SESSION = 2 +} + +local self = { + num_reactors = 0, + rtu_sessions = {}, + plc_sessions = {}, + coord_sessions = {}, + next_rtu_id = 0, + next_plc_id = 0, + next_coord_id = 0 +} + +function alloc_reactor_plcs(num_reactors) + self.num_reactors = num_reactors + for i = 1, num_reactors do + table.insert(self.plc_sessions, false) + end +end + +function find_session(stype, remote_port) + if stype == SESSION_TYPE.RTU_SESSION then + for i = 1, #self.rtu_sessions do + if self.rtu_sessions[i].r_host == remote_port then + return self.rtu_sessions[i] + end + end + elseif stype == SESSION_TYPE.PLC_SESSION then + for i = 1, #self.plc_sessions do + if self.plc_sessions[i].r_host == remote_port then + return self.plc_sessions[i] + end + end + elseif stype == SESSION_TYPE.COORD_SESSION then + for i = 1, #self.coord_sessions do + if self.coord_sessions[i].r_host == remote_port then + return self.coord_sessions[i] + end + end + else + log._error("cannot search for unknown session type " .. stype, true) + end + + return nil +end + +function get_reactor_session(reactor) + local session = nil + + for i = 1, #self.plc_sessions do + if self.plc_sessions[i].reactor == reactor then + session = self.plc_sessions[i] + end + end + + return session +end + +function establish_plc_session(remote_port, for_reactor) + if get_reactor_session(for_reactor) == nil then + local plc_s = { + open = true, + reactor = for_reactor, + r_host = remote_port, + in_queue = mqueue.new(), + out_queue = mqueue.new(), + instance = nil + } + + plc_s.instance = plc.new_session(next_plc_id, plc_s.in_queue, plc_s.out_queue) + table.insert(self.plc_sessions, plc_s) + next_plc_id = next_plc_id + 1 + + -- success + return plc_s.instance.get_id() + else + -- reactor already assigned to a PLC + return false + end +end + +local function _iterate(sessions) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + local ok = session.instance.iterate() + if not ok then + session.open = false + session.instance.close() + end + end + end +end + +function iterate_all() + -- iterate RTU sessions + _iterate(self.rtu_sessions) + + -- iterate PLC sessions + _iterate(self.plc_sessions) + + -- iterate coordinator sessions + _iterate(self.coord_sessions) +end + +local function _free_closed(sessions) + local move_to = 1 + for i = 1, #sessions do + local session = sessions[i] + if session ~= nil then + if sessions[i].open then + if sessions[move_to] == nil then + sessions[move_to] = session + sessions[i] = nil + end + move_to = move_to + 1 + else + sessions[i] = nil + end + end + end +end + +function free_all_closed() + -- free closed RTU sessions + _free_closed(self.rtu_sessions) + + -- free closed PLC sessions + _free_closed(self.plc_sessions) + + -- free closed coordinator sessions + _free_closed(self.coord_sessions) +end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index cec58c0..f58efd2 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -6,11 +6,18 @@ os.loadAPI("scada-common/log.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") +os.loadAPI("scada-common/modbus.lua") os.loadAPI("config.lua") +os.loadAPI("mqueue.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.0" +os.loadAPI("session/rtu.lua") +os.loadAPI("session/plc.lua") +os.loadAPI("session/coordinator.lua") +os.loadAPI("session/svsessions.lua") + +local SUPERVISOR_VERSION = "alpha-v0.1.1" local print = util.print local println = util.println @@ -20,7 +27,6 @@ local println_ts = util.println_ts log._info("========================================") log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) log._info("========================================") - println(">> SCADA Supervisor " .. SUPERVISOR_VERSION .. " <<") -- mount connected devices @@ -40,7 +46,8 @@ if config.SYSTEM_TYPE == "active" then end -- start comms, open all channels -local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_FO_CHANNEL, config.SCADA_SV_CHANNEL) +local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_FO_LOCAL, config.SCADA_FO_PEER, + config.SCADA_SV_CHANNEL) -- base loop clock (4Hz, 5 ticks) local loop_clock = os.startTimer(0.25) @@ -82,12 +89,14 @@ while true do loop_clock = os.startTimer(0.25) elseif event == "modem_message" then -- got a packet + local packet = superv_comms.parse_packet(p1, p2, p3, p4, p5) + superv_comms.handle_packet(packet) end -- check for termination request if event == "terminate" or ppm.should_terminate() then log._warning("terminate requested, exiting...") - -- todo: attempt failover, alert hot backup + -- @todo: attempt failover, alert hot backup break end end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index d9b125c..d6bfd54 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -1,15 +1,27 @@ -- #REQUIRES comms.lua +-- #REQUIRES modbus.lua +-- #REQUIRES mqueue.lua +-- #REQUIRES svsessions.lua + +local PROTOCOLS = comms.PROTOCOLS +local RPLC_TYPES = comms.RPLC_TYPES +local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES + +local SESSION_TYPE = svsessions.SESSION_TYPE -- supervisory controller communications -function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_channel) +function superv_comms(mode, num_reactors, modem, dev_listen, fo_local, fo_peer, coord_listen) local self = { mode = mode, - seq_num = 0, + fo_seq_num = 0, + ln_seq_num = 0, num_reactors = num_reactors, modem = modem, dev_listen = dev_listen, - fo_channel = fo_channel, - sv_channel = sv_channel, + fo_rx = fo_local, + fo_tx = fo_peer, + coord_listen = coord_listen, reactor_struct_cache = nil } @@ -20,14 +32,28 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_chan if not self.modem.isOpen(self.dev_listen) then self.modem.open(self.dev_listen) end - if not self.modem.isOpen(self.fo_channel) then - self.modem.open(self.fo_channel) + if not self.modem.isOpen(self.fo_rx) then + self.modem.open(self.fo_rx) end - if not self.modem.isOpen(self.sv_channel) then - self.modem.open(self.sv_channel) + if not self.modem.isOpen(self.coord_listen) then + self.modem.open(self.coord_listen) end end + local _send_fo = function (msg) + local packet = comms.scada_packet() + packet.make(self.fo_seq_num, PROTOCOLS.SCADA_MGMT, msg) + self.modem.transmit(self.fo_tx, self.fo_rx, packet.raw()) + self.fo_seq_num = self.fo_seq_num + 1 + end + + local _send_plc_linking = function (dest, msg) + local packet = comms.scada_packet() + packet.make(self.ln_seq_num, PROTOCOLS.RPLC, msg) + self.modem.transmit(dest, self.dev_listen, packet.raw()) + self.ln_seq_num = self.ln_seq_num + 1 + end + -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem @@ -36,7 +62,115 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_chan _open_channels() end + -- parse a packet + local parse_packet = function(side, sender, reply_to, message, distance) + local pkt = nil + local s_pkt = scada_packet() + + -- parse packet as generic SCADA packet + s_pkt.recieve(side, sender, reply_to, message, distance) + + if s_pkt.is_valid() then + -- get as MODBUS TCP packet + if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then + local m_pkt = comms.modbus_packet() + if m_pkt.decode(s_pkt) then + pkt = m_pkt.get() + end + -- get as RPLC packet + elseif s_pkt.protocol() == PROTOCOLS.RPLC then + local rplc_pkt = comms.rplc_packet() + if rplc_pkt.decode(s_pkt) then + pkt = rplc_pkt.get() + end + -- get as SCADA management packet + elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + local mgmt_pkt = comms.mgmt_packet() + if mgmt_pkt.decode(s_pkt) then + pkt = mgmt_packet.get() + end + -- get as coordinator packet + elseif s_pkt.protocol() == PROTOCOLS.COORD_DATA then + local coord_pkt = comms.coord_packet() + if coord_pkt.decode(s_pkt) then + pkt = coord_pkt.get() + end + else + log._debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) + end + end + + return pkt + end + + local handle_packet = function(packet) + if packet ~= nil then + local sender = packet.scada_frame.sender() + local receiver = packet.scada_frame.receiver() + local protocol = packet.scada_frame.protocol() + + -- device (RTU/PLC) listening channel + if receiver == self.dev_listen then + if protocol == PROTOCOLS.MODBUS_TCP then + -- MODBUS response + elseif protocol == PROTOCOLS.RPLC then + -- reactor PLC packet + local session = svsessions.find_session(SESSION_TYPE.PLC_SESSION, sender) + if session then + if packet.type == RPLC_TYPES.LINK_REQ then + -- new device on this port? that's a collision + _send_plc_linking(sender, { RPLC_LINKING.COLLISION }) + else + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + end + else + -- unknown session, is this a linking request? + if packet.type == RPLC_TYPES.LINK_REQ then + -- this is a linking request + local plc_id = svsessions.establish_plc_session(sender) + if plc_id == false then + -- reactor already has a PLC assigned + _send_plc_linking(sender, { RPLC_LINKING.COLLISION }) + else + -- got an ID; assigned to a reactor successfully + _send_plc_linking(sender, { RPLC_LINKING.ALLOW }) + end + else + -- force a re-link + _send_plc_linking(sender, { RPLC_LINKING.DENY }) + end + end + elseif protocol == PROTOCOLS.SCADA_MGMT then + -- SCADA management packet + else + log._debug("illegal packet type " .. protocol .. " on device listening channel") + end + -- failover listening channel + elseif receiver == self.fo_rx then + if protocol == PROTOCOLS.SCADA_MGMT then + -- SCADA management packet + else + log._debug("illegal packet type " .. protocol .. " on failover listening channel") + end + -- coordinator listening channel + elseif reciever == self.coord_listen then + if protocol == PROTOCOLS.SCADA_MGMT then + -- SCADA management packet + elseif protocol == PROTOCOLS.COORD_DATA then + -- coordinator packet + else + log._debug("illegal packet type " .. protocol .. " on coordinator listening channel") + end + else + log._error("received packet on unused channel " .. receiver, true) + end + end + end + return { - reconnect_modem = reconnect_modem + reconnect_modem = reconnect_modem, + parse_packet = parse_packet, + handle_packet = handle_packet } end From 6daf6df2d04ec34c09faa7d868ee71b63759face Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 11:15:16 -0400 Subject: [PATCH 012/168] active-backup supervisor setups are no longer planned --- README.md | 4 +++- scada-common/comms.lua | 7 +------ supervisor/config.lua | 11 +---------- supervisor/startup.lua | 10 +--------- supervisor/supervisor.lua | 22 +--------------------- 5 files changed, 7 insertions(+), 47 deletions(-) diff --git a/README.md b/README.md index ac2d6c4..1aa8880 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ # cc-mek-scada Configurable ComputerCraft SCADA system for multi-reactor control of Mekanism fission reactors with a GUI, automatic safety features, waste processing control, and more! +This requires CC: Tweaked and Mekanism v10.0+ (10.1 recommended for full feature set). + ## [SCADA](https://en.wikipedia.org/wiki/SCADA) > Supervisory control and data acquisition (SCADA) is a control system architecture comprising computers, networked data communications and graphical user interfaces for high-level supervision of machines and processes. It also covers sensors and other devices, such as programmable logic controllers, which interface with process plant or machinery. @@ -23,7 +25,7 @@ There can only be one of these. This server acts as a hybrid of levels 3 & 4 in ### Supervisory Computers -There can be at most two of these in an active-backup configuration. If a backup is configured, it will act as a hot backup. This means it will be live, all data will be recieved by both it and the active computer, but it will not be commanding anything unless it hears that the active supervisor is shutting down or loses communication with the active supervisor. +There should be one of these per facility system. Currently, that means only one. In the future, multiple supervisors would provide the capability of coordinating between multiple facilities (like a fission facility, fusion facility, etc). ### RTUs diff --git a/scada-common/comms.lua b/scada-common/comms.lua index cb203bf..28529ac 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,15 +1,10 @@ PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol - SCADA_MGMT = 2, -- SCADA supervisor intercommunication, device advertisements, etc + SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } -SCADA_SV_MODES = { - ACTIVE = 0, -- supervisor running as primary - BACKUP = 1 -- supervisor running as hot backup -} - RPLC_TYPES = { KEEP_ALIVE = 0, -- keep alive packets LINK_REQ = 1, -- linking requests diff --git a/supervisor/config.lua b/supervisor/config.lua index de10492..fde20b3 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -1,15 +1,6 @@ --- type ('active','backup') --- 'active' system carries through instructions and control --- 'backup' system serves as a hot backup, still recieving data --- from all PLCs and coordinator(s) while in backup to allow --- instant failover if active goes offline without re-sync -SYSTEM_TYPE = 'active' -- scada network listen for PLC's and RTU's SCADA_DEV_LISTEN = 16000 --- failover synchronization -SCADA_FO_LOCAL = 16101 -SCADA_FO_PEER = 16102 -- listen port for SCADA supervisor access by coordinators -SCADA_SV_LISTEN = 16201 +SCADA_SV_LISTEN = 16100 -- expected number of reactors NUM_REACTORS = 4 diff --git a/supervisor/startup.lua b/supervisor/startup.lua index f58efd2..6a46f5d 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -39,15 +39,8 @@ if modem == nil then return end --- determine active/backup mode -local mode = comms.SCADA_SV_MODES.BACKUP -if config.SYSTEM_TYPE == "active" then - mode = comms.SCADA_SV_MODES.ACTIVE -end - -- start comms, open all channels -local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_FO_LOCAL, config.SCADA_FO_PEER, - config.SCADA_SV_CHANNEL) +local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) -- base loop clock (4Hz, 5 ticks) local loop_clock = os.startTimer(0.25) @@ -96,7 +89,6 @@ while true do -- check for termination request if event == "terminate" or ppm.should_terminate() then log._warning("terminate requested, exiting...") - -- @todo: attempt failover, alert hot backup break end end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index d6bfd54..86e35a2 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -11,16 +11,13 @@ local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES local SESSION_TYPE = svsessions.SESSION_TYPE -- supervisory controller communications -function superv_comms(mode, num_reactors, modem, dev_listen, fo_local, fo_peer, coord_listen) +function superv_comms(mode, num_reactors, modem, dev_listen, coord_listen) local self = { mode = mode, - fo_seq_num = 0, ln_seq_num = 0, num_reactors = num_reactors, modem = modem, dev_listen = dev_listen, - fo_rx = fo_local, - fo_tx = fo_peer, coord_listen = coord_listen, reactor_struct_cache = nil } @@ -32,21 +29,11 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_local, fo_peer, if not self.modem.isOpen(self.dev_listen) then self.modem.open(self.dev_listen) end - if not self.modem.isOpen(self.fo_rx) then - self.modem.open(self.fo_rx) - end if not self.modem.isOpen(self.coord_listen) then self.modem.open(self.coord_listen) end end - local _send_fo = function (msg) - local packet = comms.scada_packet() - packet.make(self.fo_seq_num, PROTOCOLS.SCADA_MGMT, msg) - self.modem.transmit(self.fo_tx, self.fo_rx, packet.raw()) - self.fo_seq_num = self.fo_seq_num + 1 - end - local _send_plc_linking = function (dest, msg) local packet = comms.scada_packet() packet.make(self.ln_seq_num, PROTOCOLS.RPLC, msg) @@ -146,13 +133,6 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_local, fo_peer, else log._debug("illegal packet type " .. protocol .. " on device listening channel") end - -- failover listening channel - elseif receiver == self.fo_rx then - if protocol == PROTOCOLS.SCADA_MGMT then - -- SCADA management packet - else - log._debug("illegal packet type " .. protocol .. " on failover listening channel") - end -- coordinator listening channel elseif reciever == self.coord_listen then if protocol == PROTOCOLS.SCADA_MGMT then From 78a1073e2ad0b5b5f2bf1d780bc7a136197bf375 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 15:43:25 -0400 Subject: [PATCH 013/168] #30 comms rework --- scada-common/comms.lua | 242 +++++++++++++++++++++++++++++++++-------- 1 file changed, 198 insertions(+), 44 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 28529ac..c4b8791 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -2,7 +2,8 @@ PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc - COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller + COORD_DATA = 3, -- data/control packets for coordinators to/from supervisory controllers + COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } RPLC_TYPES = { @@ -44,20 +45,24 @@ function scada_packet() local self = { modem_msg_in = nil, valid = false, + raw = nil, seq_num = nil, protocol = nil, length = nil, - raw = nil + payload = nil } + -- make a SCADA packet local make = function (seq_num, protocol, payload) self.valid = true self.seq_num = seq_num self.protocol = protocol self.length = #payload - self.raw = { self.seq_num, self.protocol, self.length, payload } + self.payload = payload + self.raw = { self.seq_num, self.protocol, self.payload } end + -- parse in a modem message as a SCADA packet local receive = function (side, sender, reply_to, message, distance) self.modem_msg_in = { iface = side, @@ -69,17 +74,19 @@ function scada_packet() self.raw = self.modem_msg_in.msg - if #self.raw < 3 then - -- malformed - return false - else + if #self.raw >= 3 then self.valid = true self.seq_num = self.raw[1] self.protocol = self.raw[2] - self.length = self.raw[3] + self.length = #self.raw[3] + self.payload = self.raw[3] end + + return self.valid end + -- public accessors -- + local modem_event = function () return self.modem_msg_in end local raw = function () return self.raw end @@ -91,23 +98,21 @@ function scada_packet() local seq_num = function () return self.seq_num end local protocol = function () return self.protocol end local length = function () return self.length end - - local data = function () - local subset = nil - if self.valid then - subset = { table.unpack(self.raw, 4, 3 + self.length) } - end - return subset - end + local data = function () return self.payload end return { + -- construct make = make, receive = receive, + -- raw access modem_event = modem_event, raw = raw, + -- sender/receiver sender = sender, receiver = receiver, + -- well-formed is_valid = is_valid, + -- packet properties seq_num = seq_num, protocol = protocol, length = length, @@ -115,10 +120,12 @@ function scada_packet() } end --- MODBUS packet +-- MODBUS packet +-- modeled after MODBUS TCP packet, but length is not transmitted since these are tables (#data = length, easy) function modbus_packet() local self = { frame = nil, + raw = nil, txn_id = txn_id, protocol = protocol, length = length, @@ -128,13 +135,19 @@ function modbus_packet() } -- make a MODBUS packet - local make = function (txn_id, protocol, length, unit_id, func_code, data) + local make = function (txn_id, protocol, unit_id, func_code, data) self.txn_id = txn_id self.protocol = protocol - self.length = length + self.length = #data self.unit_id = unit_id self.func_code = func_code self.data = data + + -- populate raw array + self.raw = { self.txn_id, self.protocol, self.unit_id, self.func_code } + for i = 1, self.length do + table.insert(self.raw, data[i]) + end end -- decode a MODBUS packet from a SCADA frame @@ -142,20 +155,28 @@ function modbus_packet() if frame then self.frame = frame - local data = frame.data() - local size_ok = #data ~= 6 - - if size_ok then - make(data[1], data[2], data[3], data[4], data[5], data[6]) + if frame.protocol() == PROTOCOLS.MODBUS_TCP then + local size_ok = frame.length() >= 4 + + if size_ok then + local data = frame.data() + make(data[1], data[2], data[3], data[4], { table.unpack(data, 5, #data) }) + end + + return size_ok + else + log._debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) + return false end - - return size_ok and self.protocol == comms.PROTOCOLS.MODBUS_TCP else log._debug("nil frame encountered", true) return false end end + -- get raw to send + local raw_sendable = function () return self.raw end + -- get this packet local get = function () return { @@ -170,8 +191,12 @@ function modbus_packet() end return { + -- construct make = make, decode = decode, + -- raw access + raw_sendable = raw_sendable, + -- formatted access get = get } end @@ -180,12 +205,14 @@ end function rplc_packet() local self = { frame = nil, + raw = nil, id = nil, type = nil, length = nil, body = nil } + -- check that type is known local _rplc_type_valid = function () return self.type == RPLC_TYPES.KEEP_ALIVE or self.type == RPLC_TYPES.LINK_REQ or @@ -200,11 +227,18 @@ function rplc_packet() end -- make an RPLC packet - local make = function (id, packet_type, length, data) + local make = function (id, packet_type, data) + -- packet accessor properties self.id = id self.type = packet_type - self.length = length + self.length = #data self.data = data + + -- populate raw array + self.raw = { self.id, self.type } + for i = 1, #data do + table.insert(self.raw, data[i]) + end end -- decode an RPLC packet from a SCADA frame @@ -212,12 +246,12 @@ function rplc_packet() if frame then self.frame = frame - if frame.protocol() == comms.PROTOCOLS.RPLC then - local data = frame.data() - local ok = #data > 2 + if frame.protocol() == PROTOCOLS.RPLC then + local ok = frame.length() >= 2 if ok then - make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) + local data = frame.data() + make(data[1], data[2], { table.unpack(data, 3, #data) }) ok = _rplc_type_valid() end @@ -232,6 +266,10 @@ function rplc_packet() end end + -- get raw to send + local raw_sendable = function () return self.raw end + + -- get this packet local get = function () return { scada_frame = self.frame, @@ -243,8 +281,12 @@ function rplc_packet() end return { + -- construct make = make, decode = decode, + -- raw access + raw_sendable = raw_sendable, + -- formatted access get = get } end @@ -253,11 +295,13 @@ end function mgmt_packet() local self = { frame = nil, + raw = nil, type = nil, length = nil, data = nil } + -- check that type is known local _scada_type_valid = function () return self.type == SCADA_MGMT_TYPES.PING or self.type == SCADA_MGMT_TYPES.SV_HEARTBEAT or @@ -267,10 +311,17 @@ function mgmt_packet() end -- make a SCADA management packet - local make = function (packet_type, length, data) + local make = function (packet_type, data) + -- packet accessor properties self.type = packet_type - self.length = length + self.length = #data self.data = data + + -- populate raw array + self.raw = { self.type } + for i = 1, #data do + table.insert(self.raw, data[i]) + end end -- decode a SCADA management packet from a SCADA frame @@ -278,12 +329,12 @@ function mgmt_packet() if frame then self.frame = frame - if frame.protocol() == comms.PROTOCOLS.SCADA_MGMT then - local data = frame.data() - local ok = #data > 1 + if frame.protocol() == PROTOCOLS.SCADA_MGMT then + local ok = #data >= 1 if ok then - make(data[1], data[2], { table.unpack(data, 3, #data) }) + local data = frame.data() + make(data[1], { table.unpack(data, 2, #data) }) ok = _scada_type_valid() end @@ -298,6 +349,10 @@ function mgmt_packet() end end + -- get raw to send + local raw_sendable = function () return self.raw end + + -- get this packet local get = function () return { scada_frame = self.frame, @@ -308,8 +363,12 @@ function mgmt_packet() end return { + -- construct make = make, decode = decode, + -- raw access + raw_sendable = raw_sendable, + -- formatted access get = get } end @@ -319,6 +378,7 @@ end function coord_packet() local self = { frame = nil, + raw = nil, type = nil, length = nil, data = nil @@ -330,10 +390,17 @@ function coord_packet() end -- make a coordinator packet - local make = function (packet_type, length, data) + local make = function (packet_type, data) + -- packet accessor properties self.type = packet_type - self.length = length + self.length = #data self.data = data + + -- populate raw array + self.raw = { self.type } + for i = 1, #data do + table.insert(self.raw, data[i]) + end end -- decode a coordinator packet from a SCADA frame @@ -341,12 +408,12 @@ function coord_packet() if frame then self.frame = frame - if frame.protocol() == comms.PROTOCOLS.COORD_DATA then - local data = frame.data() - local ok = #data > 1 + if frame.protocol() == PROTOCOLS.COORD_DATA then + local ok = #data >= 1 if ok then - make(data[1], data[2], { table.unpack(data, 3, #data) }) + local data = frame.data() + make(data[1], { table.unpack(data, 2, #data) }) ok = _coord_type_valid() end @@ -361,6 +428,10 @@ function coord_packet() end end + -- get raw to send + local raw_sendable = function () return self.raw end + + -- get this packet local get = function () return { scada_frame = self.frame, @@ -371,8 +442,91 @@ function coord_packet() end return { + -- construct make = make, decode = decode, + -- raw access + raw_sendable = raw_sendable, + -- formatted access + get = get + } +end + +-- coordinator API (CAPI) packet +-- @todo +function capi_packet() + local self = { + frame = nil, + raw = nil, + type = nil, + length = nil, + data = nil + } + + local _coord_type_valid = function () + -- @todo + return false + end + + -- make a coordinator packet + local make = function (packet_type, data) + -- packet accessor properties + self.type = packet_type + self.length = #data + self.data = data + + -- populate raw array + self.raw = { self.type } + for i = 1, #data do + table.insert(self.raw, data[i]) + end + end + + -- decode a coordinator packet from a SCADA frame + local decode = function (frame) + if frame then + self.frame = frame + + if frame.protocol() == PROTOCOLS.COORD_API then + local ok = #data >= 1 + + if ok then + local data = frame.data() + make(data[1], { table.unpack(data, 2, #data) }) + ok = _coord_type_valid() + end + + return ok + else + log._debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true) + return false + end + else + log._debug("nil frame encountered", true) + return false + end + end + + -- get raw to send + local raw_sendable = function () return self.raw end + + -- get this packet + local get = function () + return { + scada_frame = self.frame, + type = self.type, + length = self.length, + data = self.data + } + end + + return { + -- construct + make = make, + decode = decode, + -- raw access + raw_sendable = raw_sendable, + -- formatted access get = get } end From 912011bfede5c239cbf170a203a1a9d575d715f8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 20:21:28 -0400 Subject: [PATCH 014/168] #30 modbus comms changes --- scada-common/comms.lua | 17 ++++++-------- scada-common/modbus.lua | 51 ++++++++++++++++++++++++++++------------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index c4b8791..972f768 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -88,7 +88,7 @@ function scada_packet() -- public accessors -- local modem_event = function () return self.modem_msg_in end - local raw = function () return self.raw end + local raw_sendable = function () return self.raw end local sender = function () return self.s_port end local receiver = function () return self.r_port end @@ -106,7 +106,7 @@ function scada_packet() receive = receive, -- raw access modem_event = modem_event, - raw = raw, + raw_sendable = raw_sendable, -- sender/receiver sender = sender, receiver = receiver, @@ -121,13 +121,12 @@ function scada_packet() end -- MODBUS packet --- modeled after MODBUS TCP packet, but length is not transmitted since these are tables (#data = length, easy) +-- modeled after MODBUS TCP packet function modbus_packet() local self = { frame = nil, raw = nil, txn_id = txn_id, - protocol = protocol, length = length, unit_id = unit_id, func_code = func_code, @@ -135,16 +134,15 @@ function modbus_packet() } -- make a MODBUS packet - local make = function (txn_id, protocol, unit_id, func_code, data) + local make = function (txn_id, unit_id, func_code, data) self.txn_id = txn_id - self.protocol = protocol self.length = #data self.unit_id = unit_id self.func_code = func_code self.data = data -- populate raw array - self.raw = { self.txn_id, self.protocol, self.unit_id, self.func_code } + self.raw = { self.txn_id, self.unit_id, self.func_code } for i = 1, self.length do table.insert(self.raw, data[i]) end @@ -156,11 +154,11 @@ function modbus_packet() self.frame = frame if frame.protocol() == PROTOCOLS.MODBUS_TCP then - local size_ok = frame.length() >= 4 + local size_ok = frame.length() >= 3 if size_ok then local data = frame.data() - make(data[1], data[2], data[3], data[4], { table.unpack(data, 5, #data) }) + make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) end return size_ok @@ -182,7 +180,6 @@ function modbus_packet() return { scada_frame = self.frame, txn_id = self.txn_id, - protocol = self.protocol, length = self.length, unit_id = self.unit_id, func_code = self.func_code, diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index a8148b7..494fde8 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -203,10 +203,10 @@ function new(rtu_dev) return return_ok, response end + -- handle a MODBUS TCP packet and generate a reply local handle_packet = function (packet) local return_code = true local response = nil - local reply = packet if #packet.data == 2 then -- handle by function code @@ -236,32 +236,51 @@ function new(rtu_dev) return_code = false end - if return_code then - -- default is to echo back - if type(response) == "table" then - reply.length = #response - reply.data = response - end - else + -- default is to echo back + local func_code = packet.func_code + if not return_code then -- echo back with error flag - reply.func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) if type(response) == "nil" then - reply.length = 0 - reply.data = {} + response = { } elseif type(response) == "number" then - reply.length = 1 - reply.data = { response } + response = { response } elseif type(response) == "table" then - reply.length = #response - reply.data = response + response = response end end + -- create reply + local reply = comms.modbus_packet() + reply.make(packet.txn_id, packet.unit_id, func_code, response) + return return_code, reply end + -- return a NEG_ACKNOWLEDGE error reply + local reply__neg_ack = function (packet) + -- reply back with error flag and exception code + local reply = comms.modbus_packet() + local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + local data = { MODBUS_EXCODE.NEG_ACKNOWLEDGE } + reply.make(packet.txn_id, packet.unit_id, fcode, data) + return reply + end + + -- return a GATEWAY_PATH_UNAVAILABLE error reply + local reply__gw_unavailable = function (packet) + -- reply back with error flag and exception code + local reply = comms.modbus_packet() + local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + local data = { MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE } + reply.make(packet.txn_id, packet.unit_id, fcode, data) + return reply + end + return { - handle_packet = handle_packet + handle_packet = handle_packet, + reply__neg_ack = reply__neg_ack, + reply__gw_unavailable = reply__gw_unavailable } end From 554f09c81749b9e080915bc8615e792b9fda7e8d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 20:23:40 -0400 Subject: [PATCH 015/168] #30 RTU comms code updated for new comms design --- rtu/rtu.lua | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 23ace96..dde73af 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -133,10 +133,21 @@ function rtu_comms(modem, local_port, server_port) -- PRIVATE FUNCTIONS -- - local _send = function (protocol, msg) - local packet = comms.scada_packet() - packet.make(self.seq_num, protocol, msg) - self.modem.transmit(self.s_port, self.l_port, packet.raw()) + local _send = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end + + local _send_modbus = function (m_pkt) + local s_pkt = comms.scada_packet() + s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -171,24 +182,29 @@ function rtu_comms(modem, local_port, server_port) return pkt end + -- handle a MODBUS/SCADA packet local handle_packet = function(packet, units, ref) if packet ~= nil then local protocol = packet.scada_frame.protocol() if protocol == PROTOCOLS.MODBUS_TCP then + local reply = modbus.reply__neg_ack(packet) + -- MODBUS instruction if packet.unit_id <= #units then local unit = units[packet.unit_id] - local return_code, response = unit.modbus_io.handle_packet(packet) - _send(PROTOCOLS.MODBUS_TCP, response) + local return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then log._warning("MODBUS operation failed") end else -- unit ID out of range? + reply = modbus.reply__gw_unavailable(packet) log._error("MODBUS packet requesting non-existent unit") end + + _send_modbus(reply) elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then @@ -210,10 +226,7 @@ function rtu_comms(modem, local_port, server_port) -- send capability advertisement local send_advertisement = function (units) - local advertisement = { - type = SCADA_MGMT_TYPES.RTU_ADVERT, - units = {} - } + local advertisement = {} for i = 1, #units do local type = nil @@ -230,7 +243,7 @@ function rtu_comms(modem, local_port, server_port) if type ~= nil then if type == RTU_ADVERT_TYPES.REDSTONE then - table.insert(advertisement.units, { + table.insert(advertisement, { unit = i, type = type, index = units[i].index, @@ -238,7 +251,7 @@ function rtu_comms(modem, local_port, server_port) rsio = units[i].device }) else - table.insert(advertisement.units, { + table.insert(advertisement, { unit = i, type = type, index = units[i].index, @@ -249,15 +262,11 @@ function rtu_comms(modem, local_port, server_port) end end - _send(PROTOCOLS.SCADA_MGMT, advertisement) + _send(SCADA_MGMT_TYPES.RTU_ADVERT, advertisement) end local send_heartbeat = function () - local heartbeat = { - type = SCADA_MGMT_TYPES.RTU_HEARTBEAT - } - - _send(PROTOCOLS.SCADA_MGMT, heartbeat) + _send(SCADA_MGMT_TYPES.RTU_HEARTBEAT, {}) end return { From b25d95eeb7db482765b29f528588f306ac109979 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 21:39:03 -0400 Subject: [PATCH 016/168] #30 PLC comms code updated for new comms design --- reactor-plc/plc.lua | 148 ++++++++++++++++++-------------------------- 1 file changed, 60 insertions(+), 88 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index c7a6346..6f9622d 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -220,10 +220,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- PRIVATE FUNCTIONS -- - local _send = function (msg) - local packet = scada_packet() - packet.make(self.seq_num, PROTOCOLS.RPLC, msg) - self.modem.transmit(self.s_port, self.l_port, packet.raw()) + local _send = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() + + r_pkt.make(self.id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) self.seq_num = self.seq_num + 1 end @@ -231,28 +235,28 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local _reactor_status = function () ppm.clear_fault() return { - status = self.reactor.getStatus(), - burn_rate = self.reactor.getBurnRate(), - act_burn_r = self.reactor.getActualBurnRate(), - temp = self.reactor.getTemperature(), - damage = self.reactor.getDamagePercent(), - boil_eff = self.reactor.getBoilEfficiency(), - env_loss = self.reactor.getEnvironmentalLoss(), + self.reactor.getStatus(), + self.reactor.getBurnRate(), + self.reactor.getActualBurnRate(), + self.reactor.getTemperature(), + self.reactor.getDamagePercent(), + self.reactor.getBoilEfficiency(), + self.reactor.getEnvironmentalLoss(), - fuel = self.reactor.getFuel(), - fuel_need = self.reactor.getFuelNeeded(), - fuel_fill = self.reactor.getFuelFilledPercentage(), - waste = self.reactor.getWaste(), - waste_need = self.reactor.getWasteNeeded(), - waste_fill = self.reactor.getWasteFilledPercentage(), - cool_type = self.reactor.getCoolant()['name'], - cool_amnt = self.reactor.getCoolant()['amount'], - cool_need = self.reactor.getCoolantNeeded(), - cool_fill = self.reactor.getCoolantFilledPercentage(), - hcool_type = self.reactor.getHeatedCoolant()['name'], - hcool_amnt = self.reactor.getHeatedCoolant()['amount'], - hcool_need = self.reactor.getHeatedCoolantNeeded(), - hcool_fill = self.reactor.getHeatedCoolantFilledPercentage() + self.reactor.getFuel(), + self.reactor.getFuelNeeded(), + self.reactor.getFuelFilledPercentage(), + self.reactor.getWaste(), + self.reactor.getWasteNeeded(), + self.reactor.getWasteFilledPercentage(), + self.reactor.getCoolant()['name'], + self.reactor.getCoolant()['amount'], + self.reactor.getCoolantNeeded(), + self.reactor.getCoolantFilledPercentage(), + self.reactor.getHeatedCoolant()['name'], + self.reactor.getHeatedCoolant()['amount'], + self.reactor.getHeatedCoolantNeeded(), + self.reactor.getHeatedCoolantFilledPercentage() }, ppm.faulted() end @@ -261,8 +265,8 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local changed = false if not faulted then - for key, value in pairs(status) do - if value ~= self.status_cache[key] then + for i = 1, #status do + if status[i] ~= self.status_cache[i] then changed = true break end @@ -277,50 +281,33 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end -- keep alive ack - local _send_keep_alive_ack = function () - local keep_alive_data = { - id = self.id, - timestamp = os.time(), - type = RPLC_TYPES.KEEP_ALIVE - } - - _send(keep_alive_data) + local _send_keep_alive_ack = function (srv_time) + _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, os.time() }) end -- general ack - local _send_ack = function (type, succeeded) - local ack_data = { - id = self.id, - type = type, - ack = succeeded - } - - _send(ack_data) + local _send_ack = function (msg_type, succeeded) + _send(msg_type, { succeeded }) end -- send structure properties (these should not change) -- (server will cache these) local _send_struct = function () ppm.clear_fault() + local mek_data = { - heat_cap = self.reactor.getHeatCapacity(), - fuel_asm = self.reactor.getFuelAssemblies(), - fuel_sa = self.reactor.getFuelSurfaceArea(), - fuel_cap = self.reactor.getFuelCapacity(), - waste_cap = self.reactor.getWasteCapacity(), - cool_cap = self.reactor.getCoolantCapacity(), - hcool_cap = self.reactor.getHeatedCoolantCapacity(), - max_burn = self.reactor.getMaxBurnRate() + self.reactor.getHeatCapacity(), + self.reactor.getFuelAssemblies(), + self.reactor.getFuelSurfaceArea(), + self.reactor.getFuelCapacity(), + self.reactor.getWasteCapacity(), + self.reactor.getCoolantCapacity(), + self.reactor.getHeatedCoolantCapacity(), + self.reactor.getMaxBurnRate() } - if not faulted then - local struct_packet = { - id = self.id, - type = RPLC_TYPES.MEK_STRUCT, - mek_data = mek_data - } - - _send(struct_packet) + if not ppm.is_faulted() then + _send(RPLC_TYPES.MEK_STRUCT, mek_data) else log._error("failed to send structure: PPM fault") end @@ -381,7 +368,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if packet.type == RPLC_TYPES.KEEP_ALIVE then -- keep alive request received, echo back local timestamp = packet.data[1] - local trip_time = os.time() - ts + local trip_time = os.time() - timestamp if trip_time < 0 then log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") @@ -389,7 +376,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) log._warning("PLC KEEP_ALIVE trip time > 1s (" .. trip_time .. ")") end - _send_keep_alive_ack() + _send_keep_alive_ack(timestamp) elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation log._debug("received unsolicited link request response") @@ -489,12 +476,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- attempt to establish link with supervisor local send_link_req = function () - local linking_data = { - id = self.id, - type = RPLC_TYPES.LINK_REQ - } - - _send(linking_data) + _send(RPLC_TYPES.LINK_REQ, {}) end -- send live status information @@ -508,38 +490,28 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end local sys_status = { - id = self.id, - type = RPLC_TYPES.STATUS, - timestamp = os.time(), - control_state = not self.scrammed, - overridden = overridden, - degraded = degraded, - heating_rate = self.reactor.getHeatingRate(), - mek_data = mek_data + os.time(), + (not self.scrammed), + overridden, + degraded, + self.reactor.getHeatingRate(), + mek_data } - _send(sys_status) + _send(RPLC_TYPES.STATUS, sys_status) end local send_iss_status = function () - local iss_status = { - id = self.id, - type = RPLC_TYPES.ISS_STATUS, - status = iss.status() - } - - _send(iss_status) + _send(RPLC_TYPES.ISS_STATUS, iss.status()) end local send_iss_alarm = function (cause) local iss_alarm = { - id = self.id, - type = RPLC_TYPES.ISS_ALARM, - cause = cause, - status = iss.status() + cause, + iss.status() } - _send(iss_alarm) + _send(RPLC_TYPES.ISS_ALARM, iss_alarm) end local is_scrammed = function () return self.scrammed end From 89ff502964b10e99e6c1857bcbab4306cb82efc9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 21:44:33 -0400 Subject: [PATCH 017/168] #30 supervisor comms code updated for new comms design --- supervisor/supervisor.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 86e35a2..0ce314a 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -29,15 +29,24 @@ function superv_comms(mode, num_reactors, modem, dev_listen, coord_listen) if not self.modem.isOpen(self.dev_listen) then self.modem.open(self.dev_listen) end + if not self.modem.isOpen(self.coord_listen) then self.modem.open(self.coord_listen) end end + -- open at construct time + _open_channels() + + -- send PLC link request responses local _send_plc_linking = function (dest, msg) - local packet = comms.scada_packet() - packet.make(self.ln_seq_num, PROTOCOLS.RPLC, msg) - self.modem.transmit(dest, self.dev_listen, packet.raw()) + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() + + r_pkt.make(0, RPLC_TYPES.LINK_REQ, msg) + s_pkt.make(self.ln_seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + + self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable()) self.ln_seq_num = self.ln_seq_num + 1 end From cd289ffb1e05f88f15076b53c0b0d4971b92c330 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 22 Apr 2022 21:55:26 -0400 Subject: [PATCH 018/168] #30 svsessions PLC comms code updated for new comms design --- supervisor/session/plc.lua | 68 +++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 4008a33..bd5679c 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -95,22 +95,45 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.iss_status.timed_out = iss_status[7] end - local _copy_status = function (heating_rate, mek_data) - self.sDB.mek_status.heating_rate = heating_rate - for key, value in pairs(mek_data) do - self.sDB.mek_status[key] = value - end + local _copy_status = function (mek_data) + self.sDB.mek_status.status = mek_data[1] + self.sDB.mek_status.burn_rate = mek_data[2] + self.sDB.mek_status.act_burn_rate = mek_data[3] + self.sDB.mek_status.temp = mek_data[4] + self.sDB.mek_status.damage = mek_data[5] + self.sDB.mek_status.boil_eff = mek_data[6] + self.sDB.mek_status.env_loss = mek_data[7] + + self.sDB.mek_status.fuel = mek_data[8] + self.sDB.mek_status.fuel_need = mek_data[9] + self.sDB.mek_status.fuel_fill = mek_data[10] + self.sDB.mek_status.waste = mek_data[11] + self.sDB.mek_status.waste_need = mek_data[12] + self.sDB.mek_status.waste_fill = mek_data[13] + self.sDB.mek_status.cool_type = mek_data[14] + self.sDB.mek_status.cool_amnt = mek_data[15] + self.sDB.mek_status.cool_need = mek_data[16] + self.sDB.mek_status.cool_fill = mek_data[17] + self.sDB.mek_status.hcool_type = mek_data[18] + self.sDB.mek_status.hcool_amnt = mek_data[19] + self.sDB.mek_status.hcool_need = mek_data[20] + self.sDB.mek_status.hcool_fill = mek_data[21] end local _copy_struct = function (mek_data) - for key, value in pairs(mek_data) do - self.sDB.mek_struct[key] = value - end + self.sDB.mek_struct.heat_cap = mek_data[1] + self.sDB.mek_struct.fuel_asm = mek_data[2] + self.sDB.mek_struct.fuel_sa = mek_data[3] + self.sDB.mek_struct.fuel_cap = mek_data[4] + self.sDB.mek_struct.waste_cap = mek_data[5] + self.sDB.mek_struct.cool_cap = mek_data[6] + self.sDB.mek_struct.hcool_cap = mek_data[7] + self.sDB.mek_struct.max_burn = mek_data[8] end local _get_ack = function (pkt) - if rplc_packet.length == 1 then - return rplc_packet.data[1] + if pkt.length == 1 then + return pkt.data[1] else log._warning(log_header .. "RPLC ACK length mismatch") return nil @@ -145,34 +168,33 @@ function new_session(id, for_reactor, in_queue, out_queue) if rplc_pkt.id == for_reactor then if rplc_pkt.type == RPLC_TYPES.KEEP_ALIVE then - -- periodic keep alive + -- keep alive reply elseif rplc_pkt.type == RPLC_TYPES.STATUS then -- status packet received, update data - if rplc_packet.length == 6 then + if rplc_pkt.length >= 5 then -- @todo [1] is timestamp, determine how this will be used (if at all) - self.sDB.control_state = rplc_packet.data[2] - self.sDB.overridden = rplc_packet.data[3] - self.sDB.degraded = rplc_packet.data[4] + self.sDB.control_state = rplc_pkt.data[2] + self.sDB.overridden = rplc_pkt.data[3] + self.sDB.degraded = rplc_pkt.data[4] + self.sDB.mek_status.heating_rate = rplc_pkt.data[5] -- attempt to read mek_data table - if rplc_packet.data[6] ~= nil then - local status = pcall(_copy_status, rplc_packet.data[5], rplc_packet.data[6]) + if rplc_pkt.data[6] ~= nil then + local status = pcall(_copy_status, rplc_pkt.data[6]) if status then -- copied in status data OK else -- error copying status data log._error(log_header .. "failed to parse status packet data") end - else - self.sDB.mek_status.heating_rate = rplc_packet.data[5] end else log._warning(log_header .. "RPLC status packet length mismatch") end elseif rplc_pkt.type == RPLC_TYPES.MEK_STRUCT then -- received reactor structure, record it - if rplc_packet.length == 1 then - local status = pcall(_copy_struct, rplc_packet.data[1]) + if rplc_pkt.length == 8 then + local status = pcall(_copy_struct, rplc_pkt.data) if status then -- copied in structure data OK else @@ -205,8 +227,8 @@ function new_session(id, for_reactor, in_queue, out_queue) end elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then -- ISS status packet received, copy data - if rplc_packet.length == 1 then - local status = pcall(_copy_iss_status, rplc_packet.data[1]) + if rplc_pkt.length == 7 then + local status = pcall(_copy_iss_status, rplc_pkt.data) if status then -- copied in ISS status data OK else From 812d10f374c40241a025c463685784fa9e68e7fd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Apr 2022 11:54:09 -0400 Subject: [PATCH 019/168] use epoch() instead of time() --- reactor-plc/plc.lua | 6 +++--- scada-common/alarm.lua | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 6f9622d..f0f00b1 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -282,7 +282,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- keep alive ack local _send_keep_alive_ack = function (srv_time) - _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, os.time() }) + _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, os.epoch() }) end -- general ack @@ -368,7 +368,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if packet.type == RPLC_TYPES.KEEP_ALIVE then -- keep alive request received, echo back local timestamp = packet.data[1] - local trip_time = os.time() - timestamp + local trip_time = os.epoch() - timestamp if trip_time < 0 then log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") @@ -490,7 +490,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end local sys_status = { - os.time(), + os.epoch(), (not self.scrammed), overridden, degraded, diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua index b93df73..d6670a4 100644 --- a/scada-common/alarm.lua +++ b/scada-common/alarm.lua @@ -9,7 +9,7 @@ SEVERITY = { function scada_alarm(severity, device, message) local self = { - time = os.time(), + time = os.epoch(), ts_string = os.date("[%H:%M:%S]"), severity = severity, device = device, From 3285f829f6e85a09c7f0ef8d1f083d10284ffd63 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Apr 2022 11:54:52 -0400 Subject: [PATCH 020/168] updated version for using epoch() --- reactor-plc/startup.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index d413684..07bb078 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.4" +local R_PLC_VERSION = "alpha-v0.2.5" local print = util.print local println = util.println From 852161317dcf943e314122226de1ed3da999cb75 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Apr 2022 12:12:33 -0400 Subject: [PATCH 021/168] #7 initial PLC session supervisor code --- supervisor/session/plc.lua | 295 ++++++++++++++++++++---------- supervisor/session/svsessions.lua | 47 ++++- supervisor/startup.lua | 14 +- supervisor/supervisor.lua | 4 + 4 files changed, 258 insertions(+), 102 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index bd5679c..7b17feb 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -3,6 +3,7 @@ -- #REQUIRES log.lua -- #REQUIRES util.lua +local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES PLC_S_COMMANDS = { @@ -11,6 +12,10 @@ PLC_S_COMMANDS = { ISS_CLEAR = 2 } +local PERIODICS = { + KEEP_ALIVE = 1.0 +} + -- PLC supervisor session function new_session(id, for_reactor, in_queue, out_queue) local log_header = "plc_session(" .. id .. "): " @@ -23,16 +28,22 @@ function new_session(id, for_reactor, in_queue, out_queue) commanded_state = false, -- connection properties seq_num = 0, + r_seq_num = nil, connected = true, received_struct = false, - plc_conn_watchdog = util.new_watchdog(3) + plc_conn_watchdog = util.new_watchdog(3), + last_rtt = 0, -- when to next retry one of these requests + periodics = { + last_update = 0 + keep_alive = 0 + }, retry_times = { struct_req = 0, scram_req = 0, enable_req = 0 }, - -- session PLC status database + -- session database sDB = { control_state = false, overridden = false, @@ -140,12 +151,167 @@ function new_session(id, for_reactor, in_queue, out_queue) end end + local _handle_packet = function (message) + local checks_ok = true + + -- handle an incoming packet from the PLC + rplc_pkt = message.get() + + -- check sequence number + if self.r_seq_num == nil then + self.r_seq_num = rplc_pkt.scada_frame.seq_num() + elseif self.r_seq_num >= rplc_pkt.scada_frame.seq_num() then + log._warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. rplc_pkt.scada_frame.seq_num()) + checks_ok = false + else + self.r_seq_num = rplc_pkt.scada_frame.seq_num() + end + + -- check reactor ID + if rplc_pkt.id ~= for_reactor then + log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. rplc_pkt.id) + checks_ok = false + end + + -- process packet + if checks_ok then + -- feed watchdog + self.plc_conn_watchdog.feed() + + -- handle packet by type + if rplc_pkt.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive reply + if rplc_pkt.length == 2 then + local srv_start = rplc_pkt.data[1] + local plc_send = rplc_pkt.data[2] + local srv_now = os.epoch() + self.last_rtt = srv_now - srv_start + + if self.last_rtt < 0 then + log._warning(log_header .. "PLC KEEP_ALIVE round trip time less than 0 (" .. trip_time .. ")") + elseif trip_time > 1 then + log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 1s (" .. trip_time .. ")") + end + + log._debug(log_header .. "RPLC RTT = ".. trip_time) + else + log._debug(log_header .. "RPLC keep alive packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.STATUS then + -- status packet received, update data + if rplc_pkt.length >= 5 then + -- @todo [1] is timestamp, determine how this will be used (if at all) + self.sDB.control_state = rplc_pkt.data[2] + self.sDB.overridden = rplc_pkt.data[3] + self.sDB.degraded = rplc_pkt.data[4] + self.sDB.mek_status.heating_rate = rplc_pkt.data[5] + + -- attempt to read mek_data table + if rplc_pkt.data[6] ~= nil then + local status = pcall(_copy_status, rplc_pkt.data[6]) + if status then + -- copied in status data OK + else + -- error copying status data + log._error(log_header .. "failed to parse status packet data") + end + end + else + log._debug(log_header .. "RPLC status packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_STRUCT then + -- received reactor structure, record it + if rplc_pkt.length == 8 then + local status = pcall(_copy_struct, rplc_pkt.data) + if status then + -- copied in structure data OK + else + -- error copying structure data + log._error(log_header .. "failed to parse struct packet data") + end + else + log._debug(log_header .. "RPLC struct packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_SCRAM then + -- SCRAM acknowledgement + local ack = _get_ack(rplc_pkt) + if ack then + self.sDB.control_state = false + elseif ack == false then + log._warning(log_header .. "SCRAM failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_ENABLE then + -- enable acknowledgement + local ack = _get_ack(rplc_pkt) + if ack then + self.sDB.control_state = true + elseif ack == false then + log._warning(log_header .. "enable failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.MEK_BURN_RATE then + -- burn rate acknowledgement + if _get_ack(rplc_pkt) == false then + log._warning(log_header .. "burn rate update failed!") + end + elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then + -- ISS status packet received, copy data + if rplc_pkt.length == 7 then + local status = pcall(_copy_iss_status, rplc_pkt.data) + if status then + -- copied in ISS status data OK + else + -- error copying ISS status data + log._error(log_header .. "failed to parse ISS status packet data") + end + else + log._debug(log_header .. "RPLC ISS status packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.ISS_ALARM then + -- ISS alarm + self.sDB.overridden = true + if rplc_pkt.length == 7 then + local status = pcall(_copy_iss_status, rplc_pkt.data) + if status then + -- copied in ISS status data OK + else + -- error copying ISS status data + log._error(log_header .. "failed to parse ISS status packet data") + end + else + log._debug(log_header .. "RPLC ISS alarm packet length mismatch") + end + elseif rplc_pkt.type == RPLC_TYPES.ISS_CLEAR then + -- ISS clear acknowledgement + if _get_ack(rplc_pkt) == false then + log._warning(log_header .. "ISS clear failed") + end + else + log._debug(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) + end + end + end + + local _send = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() + + r_pkt.make(self.id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + + self.out_q.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end + + -- PUBLIC FUNCTIONS -- + local get_id = function () return self.id end + local get_db = function () return self.sDB end + local close = function () self.connected = false end local check_wd = function (timer) - return timer == plc_conn_watchdog + return timer == self.plc_conn_watchdog.get_timer() end local get_struct = function () @@ -158,103 +324,37 @@ function new_session(id, for_reactor, in_queue, out_queue) end local iterate = function () - if self.connected and ~self.in_q.empty() then - -- get a new message to process - local message = self.in_q.pop() + if self.connected then + ------------------ + -- handle queue -- + ------------------ - if message.qtype == mqueue.TYPE.PACKET then - -- handle an incoming packet from the PLC - rplc_pkt = message.message.get() + if ~self.in_q.empty() then + -- get a new message to process + local message = self.in_q.pop() + + if message.qtype == mqueue.TYPE.PACKET then + _handle_packet(message.message) + elseif message.qtype == mqueue.TYPE.COMMAND then + -- handle instruction - if rplc_pkt.id == for_reactor then - if rplc_pkt.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive reply - elseif rplc_pkt.type == RPLC_TYPES.STATUS then - -- status packet received, update data - if rplc_pkt.length >= 5 then - -- @todo [1] is timestamp, determine how this will be used (if at all) - self.sDB.control_state = rplc_pkt.data[2] - self.sDB.overridden = rplc_pkt.data[3] - self.sDB.degraded = rplc_pkt.data[4] - self.sDB.mek_status.heating_rate = rplc_pkt.data[5] - - -- attempt to read mek_data table - if rplc_pkt.data[6] ~= nil then - local status = pcall(_copy_status, rplc_pkt.data[6]) - if status then - -- copied in status data OK - else - -- error copying status data - log._error(log_header .. "failed to parse status packet data") - end - end - else - log._warning(log_header .. "RPLC status packet length mismatch") - end - elseif rplc_pkt.type == RPLC_TYPES.MEK_STRUCT then - -- received reactor structure, record it - if rplc_pkt.length == 8 then - local status = pcall(_copy_struct, rplc_pkt.data) - if status then - -- copied in structure data OK - else - -- error copying structure data - log._error(log_header .. "failed to parse struct packet data") - end - else - log._warning(log_header .. "RPLC struct packet length mismatch") - end - elseif rplc_pkt.type == RPLC_TYPES.MEK_SCRAM then - -- SCRAM acknowledgement - local ack = _get_ack(rplc_pkt) - if ack then - self.sDB.control_state = false - elseif ack == false then - log._warning(log_header .. "SCRAM failed!") - end - elseif rplc_pkt.type == RPLC_TYPES.MEK_ENABLE then - -- enable acknowledgement - local ack = _get_ack(rplc_pkt) - if ack then - self.sDB.control_state = true - elseif ack == false then - log._warning(log_header .. "enable failed!") - end - elseif rplc_pkt.type == RPLC_TYPES.MEK_BURN_RATE then - -- burn rate acknowledgement - if _get_ack(rplc_pkt) == false then - log._warning(log_header .. "burn rate update failed!") - end - elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then - -- ISS status packet received, copy data - if rplc_pkt.length == 7 then - local status = pcall(_copy_iss_status, rplc_pkt.data) - if status then - -- copied in ISS status data OK - else - -- error copying ISS status data - log._error(log_header .. "failed to parse ISS status packet data") - end - else - log._warning(log_header .. "RPLC ISS status packet length mismatch") - end - elseif rplc_pkt.type == RPLC_TYPES.ISS_ALARM then - -- ISS alarm - self.sDB.overridden = true - -- @todo - elseif rplc_pkt.type == RPLC_TYPES.ISS_CLEAR then - -- ISS clear acknowledgement - -- @todo - else - log._warning(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) - end - else - log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. rplc_pkt.id) end - elseif message.qtype == mqueue.TYPE.COMMAND then - -- handle instruction - end + + ---------------------- + -- update periodics -- + ---------------------- + + local elapsed = os.clock() - self.periodics.last_update + + self.periodics.keep_alive += elapsed + + if self.periodics.keep_alive >= PERIODICS.KEEP_ALIVE then + _send(RPLC_TYPES.KEEP_ALIVE, { os.epoch() }) + self.periodics.keep_alive = 0 + end + + self.periodics.last_update = os.clock() end return self.connected @@ -262,9 +362,10 @@ function new_session(id, for_reactor, in_queue, out_queue) return { get_id = get_id, + get_db = get_db, + close = close, check_wd = check_wd, get_struct = get_struct, - close = close, iterate = iterate } end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index fcc6950..4e0c75e 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -10,6 +10,7 @@ SESSION_TYPE = { } local self = { + modem = nil num_reactors = 0, rtu_sessions = {}, plc_sessions = {}, @@ -19,6 +20,10 @@ local self = { next_coord_id = 0 } +function link_modem(modem) + self.modem = modem +end + function alloc_reactor_plcs(num_reactors) self.num_reactors = num_reactors for i = 1, num_reactors do @@ -64,12 +69,13 @@ function get_reactor_session(reactor) return session end -function establish_plc_session(remote_port, for_reactor) +function establish_plc_session(local_port, remote_port, for_reactor) if get_reactor_session(for_reactor) == nil then local plc_s = { open = true, reactor = for_reactor, - r_host = remote_port, + l_port = local_port, + r_port = remote_port, in_queue = mqueue.new(), out_queue = mqueue.new(), instance = nil @@ -87,12 +93,46 @@ function establish_plc_session(remote_port, for_reactor) end end +local function _check_watchdogs(sessions, timer_event) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + local triggered = session.instance.check_wd(timer_event) + if triggered then + log._debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port) + session.open = false + session.instance.close() + end + end + end +end + +function check_all_watchdogs(timer_event) + -- check RTU session watchdogs + _check_watchdogs(self.rtu_sessions, timer_event) + + -- check PLC session watchdogs + _check_watchdogs(self.plc_sessions, timer_event) + + -- check coordinator session watchdogs + _check_watchdogs(self.coord_sessions, timer_event) +end + local function _iterate(sessions) for i = 1, #sessions do local session = sessions[i] if session.open then local ok = session.instance.iterate() - if not ok then + if ok then + -- send packets in out queue + -- @todo handle commands if that's being used too + while not session.out_queue.empty() do + local msg = session.out_queue.pop() + if msg.qtype == mqueue.TYPE.PACKET then + self.modem.transmit(self.r_port, self.l_port, msg.message.raw_sendable()) + end + end + else session.open = false session.instance.close() end @@ -123,6 +163,7 @@ local function _free_closed(sessions) end move_to = move_to + 1 else + log._debug("free'ing closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port) sessions[i] = nil end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 6a46f5d..1ad31bb 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("session/plc.lua") os.loadAPI("session/coordinator.lua") os.loadAPI("session/svsessions.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.1" +local SUPERVISOR_VERSION = "alpha-v0.1.2" local print = util.print local println = util.println @@ -78,8 +78,18 @@ while true do end end elseif event == "timer" and param1 == loop_clock then - -- basic event tick, send keep-alives + -- main loop tick + + -- iterate sessions + svsessions.iterate_all() + + -- free any closed sessions + svsessions.free_all_closed() + loop_clock = os.startTimer(0.25) + elseif event == "timer" then + -- another timer event, check watchdogs + svsessions.check_all_watchdogs(param1) elseif event == "modem_message" then -- got a packet local packet = superv_comms.parse_packet(p1, p2, p3, p4, p5) diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 0ce314a..a3d2d2a 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -38,6 +38,9 @@ function superv_comms(mode, num_reactors, modem, dev_listen, coord_listen) -- open at construct time _open_channels() + -- link modem to svsessions + svsessions.link_modem(self.modem) + -- send PLC link request responses local _send_plc_linking = function (dest, msg) local s_pkt = comms.scada_packet() @@ -55,6 +58,7 @@ function superv_comms(mode, num_reactors, modem, dev_listen, coord_listen) -- reconnect a newly connected modem local reconnect_modem = function (modem) self.modem = modem + svsessions.link_modem(self.modem) _open_channels() end From fa19af308d4eda964c362aedd8ff9e7e1fd7d57e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Apr 2022 20:46:01 -0400 Subject: [PATCH 022/168] bugfix and use timestamp in packet --- supervisor/session/plc.lua | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 7b17feb..7694ef4 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -35,7 +35,7 @@ function new_session(id, for_reactor, in_queue, out_queue) last_rtt = 0, -- when to next retry one of these requests periodics = { - last_update = 0 + last_update = 0, keep_alive = 0 }, retry_times = { @@ -45,6 +45,7 @@ function new_session(id, for_reactor, in_queue, out_queue) }, -- session database sDB = { + last_status_update = 0, control_state = false, overridden = false, degraded = false, @@ -200,7 +201,7 @@ function new_session(id, for_reactor, in_queue, out_queue) elseif rplc_pkt.type == RPLC_TYPES.STATUS then -- status packet received, update data if rplc_pkt.length >= 5 then - -- @todo [1] is timestamp, determine how this will be used (if at all) + self.sDB.last_status_update = rplc_pkt.data[1] self.sDB.control_state = rplc_pkt.data[2] self.sDB.overridden = rplc_pkt.data[3] self.sDB.degraded = rplc_pkt.data[4] From 416255f41a8a48ad2cf2fa6e6004294a5f895c42 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 23 Apr 2022 21:10:25 -0400 Subject: [PATCH 023/168] PLC check sequence numbers, corrected trip time to ms --- reactor-plc/plc.lua | 14 +++++++++++++- supervisor/session/plc.lua | 4 ++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index f0f00b1..64adb88 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -202,6 +202,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local self = { id = id, seq_num = 0, + r_seq_num = nil, modem = modem, s_port = server_port, l_port = local_port, @@ -363,6 +364,17 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- handle an RPLC packet local handle_packet = function (packet, plc_state) if packet ~= nil then + -- check sequence number + if self.r_seq_num == nil then + self.r_seq_num = packet.scada_frame.seq_num() + elseif self.r_seq_num >= packet.scada_frame.seq_num() then + log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + return + else + self.r_seq_num = packet.scada_frame.seq_num() + end + + -- handle packet if packet.scada_frame.protocol() == PROTOCOLS.RPLC then if self.linked then if packet.type == RPLC_TYPES.KEEP_ALIVE then @@ -372,7 +384,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if trip_time < 0 then log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") - elseif trip_time > 1 then + elseif trip_time > 1000 then log._warning("PLC KEEP_ALIVE trip time > 1s (" .. trip_time .. ")") end diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 7694ef4..9fbed9b 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -190,11 +190,11 @@ function new_session(id, for_reactor, in_queue, out_queue) if self.last_rtt < 0 then log._warning(log_header .. "PLC KEEP_ALIVE round trip time less than 0 (" .. trip_time .. ")") - elseif trip_time > 1 then + elseif trip_time > 1000 then log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 1s (" .. trip_time .. ")") end - log._debug(log_header .. "RPLC RTT = ".. trip_time) + log._debug(log_header .. "RPLC RTT = ".. trip_time .. "ms") else log._debug(log_header .. "RPLC keep alive packet length mismatch") end From 86b0d155fa8d2d5e2202ca22968086464bf6f78d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 24 Apr 2022 12:04:31 -0400 Subject: [PATCH 024/168] #31 PPM cannot assume that we will get a fault on failure, apparently sometimes we will only get a nil return so the system can no longer check ACCESS_OK, now each device has its own fault tracking --- reactor-plc/plc.lua | 14 +++-- reactor-plc/startup.lua | 5 +- rtu/startup.lua | 2 +- scada-common/ppm.lua | 115 +++++++++++++++++++++---------------- supervisor/session/plc.lua | 2 +- supervisor/startup.lua | 2 +- 6 files changed, 81 insertions(+), 59 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 64adb88..f0f26c8 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -131,7 +131,8 @@ function iss_init(reactor) log._warning("ISS: reactor SCRAM") self.tripped = true self.trip_cause = status - if self.reactor.scram() == ppm.ACCESS_FAULT then + self.reactor.scram() + if self.reactor.__p_is_faulted() then log._error("ISS: failed reactor SCRAM") end end @@ -420,12 +421,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- disable the reactor self.scrammed = true plc_state.scram = true - _send_ack(packet.type, self.reactor.scram() == ppm.ACCESS_OK) + self.reactor.scram() + _send_ack(packet.type, self.reactor.__p_is_ok()) elseif packet.type == RPLC_TYPES.MEK_ENABLE then -- enable the reactor self.scrammed = false plc_state.scram = false - _send_ack(packet.type, self.reactor.activate() == ppm.ACCESS_OK) + self.reactor.activate() + _send_ack(packet.type, self.reactor.__p_is_ok()) elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate local success = false @@ -441,11 +444,12 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- if we know our max burn rate, update current burn rate if in range if max_burn_rate ~= ppm.ACCESS_FAULT then if burn_rate > 0 and burn_rate <= max_burn_rate then - success = self.reactor.setBurnRate(burn_rate) + self.reactor.setBurnRate(burn_rate) + success = self.reactor.__p_is_ok() end end - _send_ack(packet.type, success == ppm.ACCESS_OK) + _send_ack(packet.type, success) elseif packet.type == RPLC_TYPES.ISS_CLEAR then -- clear the ISS status iss.reset() diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 07bb078..7cef958 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.5" +local R_PLC_VERSION = "alpha-v0.2.6" local print = util.print local println = util.println @@ -281,7 +281,8 @@ while true do log._warning("terminate requested, exiting...") if plc_state.init_ok then plc_state.scram = true - if reactor.scram() ~= ppm.ACCESS_FAULT then + reactor.scram() + if reactor.__p_is_ok() then println_ts("reactor disabled") else -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? diff --git a/rtu/startup.lua b/rtu/startup.lua index 42779c6..4534f18 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.2.3" +local RTU_VERSION = "alpha-v0.2.4" local print = util.print local println = util.println diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 66f26a7..f383bae 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -4,14 +4,13 @@ -- Protected Peripheral Manager -- -ACCESS_OK = true ACCESS_FAULT = nil ---------------------------- -- PRIVATE DATA/FUNCTIONS -- ---------------------------- -local self = { +local _ppm_sys = { mounts = {}, auto_cf = false, faulted = false, @@ -20,38 +19,66 @@ local self = { } -- wrap peripheral calls with lua protected call --- ex. reason: we don't want a disconnect to crash the program before a SCRAM -local peri_init = function (device) - for key, func in pairs(device) do - device[key] = function (...) +-- we don't want a disconnect to crash a program +-- also provides peripheral-specific fault checks (auto-clear fault defaults to true) +local peri_init = function (iface) + local self = { + faulted = false, + auto_cf = true, + type = peripheral.getType(iface), + device = peripheral.wrap(iface) + } + + -- initialization process (re-map) + + for key, func in pairs(self.device) do + self.device[key] = function (...) local status, result = pcall(func, ...) if status then -- auto fault clear if self.auto_cf then self.faulted = false end - - -- assume nil is only for functions with no return, so return status - if result == nil then - return ACCESS_OK - else - return result - end + if _ppm_sys.auto_cf then _ppm_sys.faulted = false end + return result else -- function failed self.faulted = true + _ppm_sys.faulted = true - if not mute then + if not _ppm_sys.mute then log._error("PPM: protected " .. key .. "() -> " .. result) end if result == "Terminated" then - self.terminate = true + _ppm_sys.terminate = true end return ACCESS_FAULT end end end + + -- fault management functions + + local clear_fault = function () self.faulted = false end + local is_faulted = function () return self.faulted end + local is_ok = function () return not self.faulted end + + local enable_afc = function () self.auto_cf = true end + local disable_afc = function () self.auto_cf = false end + + -- append to device functions + + self.device.__p_clear_fault = clear_fault + self.device.__p_is_faulted = is_faulted + self.device.__p_is_ok = is_ok + self.device.__p_enable_afc = enable_afc + self.device.__p_disable_afc = disable_afc + + return { + type = self.type, + dev = self.device + } end ---------------------- @@ -62,41 +89,41 @@ end -- silence error prints function disable_reporting() - self.mute = true + _ppm_sys.mute = true end -- allow error prints function enable_reporting() - self.mute = false + _ppm_sys.mute = false end -- FAULT MEMORY -- -- enable automatically clearing fault flag function enable_afc() - self.auto_cf = true + _ppm_sys.auto_cf = true end -- disable automatically clearing fault flag function disable_afc() - self.auto_cf = false + _ppm_sys.auto_cf = false end -- check fault flag function is_faulted() - return self.faulted + return _ppm_sys.faulted end -- clear fault flag function clear_fault() - self.faulted = false + _ppm_sys.faulted = false end -- TERMINATION -- -- if a caught error was a termination request function should_terminate() - return self.terminate + return _ppm_sys.terminate end -- MOUNTING -- @@ -105,18 +132,12 @@ end function mount_all() local ifaces = peripheral.getNames() - self.mounts = {} + _ppm_sys.mounts = {} for i = 1, #ifaces do - local pm_dev = peripheral.wrap(ifaces[i]) - peri_init(pm_dev) + _ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) - self.mounts[ifaces[i]] = { - type = peripheral.getType(ifaces[i]), - dev = pm_dev - } - - log._info("PPM: found a " .. self.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") + log._info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") end if #ifaces == 0 then @@ -128,31 +149,27 @@ end function mount(iface) local ifaces = peripheral.getNames() local pm_dev = nil - local type = nil + local pm_type = nil for i = 1, #ifaces do if iface == ifaces[i] then log._info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) - type = peripheral.getType(iface) - pm_dev = peripheral.wrap(iface) - peri_init(pm_dev) + _ppm_sys.mounts[iface] = peri_init(iface) - self.mounts[iface] = { - type = peripheral.getType(iface), - dev = pm_dev - } + pm_type = _ppm_sys.mounts[iface].type + pm_dev = _ppm_sys.mounts[iface].dev break end end - return type, pm_dev + return pm_type, pm_dev end -- handle peripheral_detach event function handle_unmount(iface) -- what got disconnected? - local lost_dev = self.mounts[iface] + local lost_dev = _ppm_sys.mounts[iface] if lost_dev then local type = lost_dev.type @@ -173,20 +190,20 @@ end -- list mounted peripherals function list_mounts() - return self.mounts + return _ppm_sys.mounts end -- get a mounted peripheral by side/interface function get_periph(iface) - if self.mounts[iface] then - return self.mounts[iface].dev + if _ppm_sys.mounts[iface] then + return _ppm_sys.mounts[iface].dev else return nil end end -- get a mounted peripheral type by side/interface function get_type(iface) - if self.mounts[iface] then - return self.mounts[iface].type + if _ppm_sys.mounts[iface] then + return _ppm_sys.mounts[iface].type else return nil end end @@ -194,7 +211,7 @@ end function get_all_devices(name) local devices = {} - for side, data in pairs(self.mounts) do + for side, data in pairs(_ppm_sys.mounts) do if data.type == name then table.insert(devices, data.dev) end @@ -207,7 +224,7 @@ end function get_device(name) local device = nil - for side, data in pairs(self.mounts) do + for side, data in pairs(_ppm_sys.mounts) do if data.type == name then device = data.dev break @@ -228,7 +245,7 @@ end function get_wireless_modem() local w_modem = nil - for side, device in pairs(self.mounts) do + for side, device in pairs(_ppm_sys.mounts) do if device.type == "modem" and device.dev.isWireless() then w_modem = device.dev break diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 9fbed9b..91fbacc 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -330,7 +330,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- handle queue -- ------------------ - if ~self.in_q.empty() then + if not self.in_q.empty() then -- get a new message to process local message = self.in_q.pop() diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 1ad31bb..3395b81 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("session/plc.lua") os.loadAPI("session/coordinator.lua") os.loadAPI("session/svsessions.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.2" +local SUPERVISOR_VERSION = "alpha-v0.1.3" local print = util.print local println = util.println From 74168707c68c6bd8c203e48569a04391d3bdb3fa Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 24 Apr 2022 13:21:55 -0400 Subject: [PATCH 025/168] PLC clock timing fix --- reactor-plc/startup.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 7cef958..0c97472 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.6" +local R_PLC_VERSION = "alpha-v0.2.7" local print = util.print local println = util.println @@ -95,7 +95,7 @@ function init() end -- loop clock (10Hz, 2 ticks) - loop_clock = os.startTimer(0.05) + loop_clock = os.startTimer(0.1) log._debug("loop clock started") println("boot> completed"); @@ -257,7 +257,7 @@ while true do end -- start next clock timer - loop_clock = os.startTimer(0.05) + loop_clock = os.startTimer(0.1) elseif event == "modem_message" and networked and not plc_state.no_modem then -- got a packet -- feed the watchdog first so it doesn't uhh...eat our packets From 074f6448e1c739e6c5666e821af55fdb9a453d52 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 24 Apr 2022 13:22:45 -0400 Subject: [PATCH 026/168] some supervisor bugfixes --- supervisor/session/plc.lua | 2 +- supervisor/session/svsessions.lua | 2 +- supervisor/startup.lua | 7 ++++--- supervisor/supervisor.lua | 3 +-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 91fbacc..3adb85d 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -348,7 +348,7 @@ function new_session(id, for_reactor, in_queue, out_queue) local elapsed = os.clock() - self.periodics.last_update - self.periodics.keep_alive += elapsed + self.periodics.keep_alive = self.periodics.keep_alive + elapsed if self.periodics.keep_alive >= PERIODICS.KEEP_ALIVE then _send(RPLC_TYPES.KEEP_ALIVE, { os.epoch() }) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 4e0c75e..edc2ea2 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -10,7 +10,7 @@ SESSION_TYPE = { } local self = { - modem = nil + modem = nil, num_reactors = 0, rtu_sessions = {}, plc_sessions = {}, diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 3395b81..fa4de82 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -10,14 +10,15 @@ os.loadAPI("scada-common/modbus.lua") os.loadAPI("config.lua") os.loadAPI("mqueue.lua") -os.loadAPI("supervisor.lua") os.loadAPI("session/rtu.lua") os.loadAPI("session/plc.lua") os.loadAPI("session/coordinator.lua") os.loadAPI("session/svsessions.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.3" +os.loadAPI("supervisor.lua") + +local SUPERVISOR_VERSION = "alpha-v0.1.4" local print = util.print local println = util.println @@ -40,7 +41,7 @@ if modem == nil then end -- start comms, open all channels -local comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) +local superv_comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) -- base loop clock (4Hz, 5 ticks) local loop_clock = os.startTimer(0.25) diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index a3d2d2a..25154bb 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -11,9 +11,8 @@ local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES local SESSION_TYPE = svsessions.SESSION_TYPE -- supervisory controller communications -function superv_comms(mode, num_reactors, modem, dev_listen, coord_listen) +function superv_comms(num_reactors, modem, dev_listen, coord_listen) local self = { - mode = mode, ln_seq_num = 0, num_reactors = num_reactors, modem = modem, From 1744527a41e08766cb15c1307441556c842456e6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 10:34:41 -0400 Subject: [PATCH 027/168] ISS cleanup --- reactor-plc/plc.lua | 104 ++++++++++++++++++---------------------- reactor-plc/startup.lua | 5 +- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index f0f26c8..6e92cfd 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -11,18 +11,16 @@ local RPLC_LINKING = comms.RPLC_LINKING function iss_init(reactor) local self = { reactor = reactor, + cache = { false, false, false, false, false, false, false }, timed_out = false, tripped = false, trip_cause = "" } - -- re-link a reactor after a peripheral re-connect - local reconnect_reactor = function (reactor) - self.reactor = reactor - end + -- PRIVATE FUNCTIONS -- -- check for critical damage - local damage_critical = function () + local _damage_critical = function () local damage_percent = self.reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later @@ -34,7 +32,7 @@ function iss_init(reactor) end -- check for heated coolant backup - local excess_heated_coolant = function () + local _excess_heated_coolant = function () local hc_needed = self.reactor.getHeatedCoolantNeeded() if hc_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later @@ -46,7 +44,7 @@ function iss_init(reactor) end -- check for excess waste - local excess_waste = function () + local _excess_waste = function () local w_needed = self.reactor.getWasteNeeded() if w_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later @@ -58,7 +56,7 @@ function iss_init(reactor) end -- check if the reactor is at a critically high temperature - local high_temp = function () + local _high_temp = function () -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 local temp = self.reactor.getTemperature() if temp == ppm.ACCESS_FAULT then @@ -71,7 +69,7 @@ function iss_init(reactor) end -- check if there is no fuel - local insufficient_fuel = function () + local _insufficient_fuel = function () local fuel = self.reactor.getFuel() if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later @@ -83,7 +81,7 @@ function iss_init(reactor) end -- check if there is no coolant - local no_coolant = function () + local _no_coolant = function () local coolant_filled = self.reactor.getCoolantFilledPercentage() if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later @@ -94,39 +92,63 @@ function iss_init(reactor) end end - -- if PLC timed out - local timed_out = function () - return self.timed_out + -- PUBLIC FUNCTIONS -- + + -- re-link a reactor after a peripheral re-connect + local reconnect_reactor = function (reactor) + self.reactor = reactor + end + + -- report a PLC comms timeout + local trip_timeout = function () + self.timed_out = true end -- check all safety conditions local check = function () local status = "ok" local was_tripped = self.tripped + + -- update cache + self.cache = { + _damage_critical(), + _excess_heated_coolant(), + _excess_waste(), + _high_temp(), + _insufficient_fuel(), + _no_coolant(), + self.timed_out + } -- check system states in order of severity - if damage_critical() then + if self.cache[1] then log._warning("ISS: damage critical!") status = "dmg_crit" - elseif high_temp() then + elseif self.cache[4] then log._warning("ISS: high temperature!") status = "high_temp" - elseif excess_heated_coolant() then + elseif self.cache[2] then log._warning("ISS: heated coolant backup!") status = "heated_coolant_backup" - elseif excess_waste() then + elseif self.cache[6] then + log._warning("ISS: no coolant!") + status = "no_coolant" + elseif self.cache[3] then log._warning("ISS: full waste!") status = "full_waste" - elseif insufficient_fuel() then + elseif self.cache[5] then log._warning("ISS: no fuel!") status = "no_fuel" + elseif self.timed_out then + log._warning("ISS: supervisor connection timeout!") + status = "timeout" elseif self.tripped then status = self.trip_cause else self.tripped = false end - -- if a new trip occured... + -- if a trip occured... if status ~= "ok" then log._warning("ISS: reactor SCRAM") self.tripped = true @@ -137,19 +159,12 @@ function iss_init(reactor) end end + -- evaluate if this is a new trip local first_trip = not was_tripped and self.tripped return self.tripped, status, first_trip end - -- report a PLC comms timeout - local trip_timeout = function () - self.tripped = false - self.trip_cause = "timeout" - self.timed_out = true - self.reactor.scram() - end - -- reset the ISS local reset = function () self.timed_out = false @@ -158,43 +173,16 @@ function iss_init(reactor) end -- get the ISS status - local status = function (named) - if named then - return { - damage_critical = damage_critical(), - excess_heated_coolant = excess_heated_coolant(), - excess_waste = excess_waste(), - high_temp = high_temp(), - insufficient_fuel = insufficient_fuel(), - no_coolant = no_coolant(), - timed_out = timed_out() - } - else - return { - damage_critical(), - excess_heated_coolant(), - excess_waste(), - high_temp(), - insufficient_fuel(), - no_coolant(), - timed_out() - } - end + local status = function () + return self.cache end return { reconnect_reactor = reconnect_reactor, - check = check, trip_timeout = trip_timeout, + check = check, reset = reset, - status = status, - damage_critical = damage_critical, - excess_heated_coolant = excess_heated_coolant, - excess_waste = excess_waste, - high_temp = high_temp, - insufficient_fuel = insufficient_fuel, - no_coolant = no_coolant, - timed_out = timed_out + status = status } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 0c97472..acf19b2 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,7 +10,7 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.2.7" +local R_PLC_VERSION = "alpha-v0.2.8" local print = util.print local println = util.println @@ -230,7 +230,8 @@ while true do plc_comms.send_iss_alarm(iss_status_string) end end - else + elseif not plc_state.no_reactor then + -- degraded but we have a reactor reactor.scram() end end From c46a7b2486eef802ae235648764be3136dc55f99 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 10:36:47 -0400 Subject: [PATCH 028/168] added time functions to util, also task_wait --- reactor-plc/plc.lua | 7 ++++--- scada-common/alarm.lua | 4 +++- scada-common/util.lua | 24 ++++++++++++++++++++++++ supervisor/session/plc.lua | 4 ++-- 4 files changed, 33 insertions(+), 6 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 6e92cfd..67708d5 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,5 +1,6 @@ -- #REQUIRES comms.lua -- #REQUIRES ppm.lua +-- #REQUIRES util.lua local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES @@ -272,7 +273,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- keep alive ack local _send_keep_alive_ack = function (srv_time) - _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, os.epoch() }) + _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack @@ -369,7 +370,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if packet.type == RPLC_TYPES.KEEP_ALIVE then -- keep alive request received, echo back local timestamp = packet.data[1] - local trip_time = os.epoch() - timestamp + local trip_time = util.time() - timestamp if trip_time < 0 then log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") @@ -494,7 +495,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end local sys_status = { - os.epoch(), + util.time(), (not self.scrammed), overridden, degraded, diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua index d6670a4..e8464a5 100644 --- a/scada-common/alarm.lua +++ b/scada-common/alarm.lua @@ -1,3 +1,5 @@ +-- #REQUIRES util.lua + SEVERITY = { INFO = 0, -- basic info message WARNING = 1, -- warning about some abnormal state @@ -9,7 +11,7 @@ SEVERITY = { function scada_alarm(severity, device, message) local self = { - time = os.epoch(), + time = util.time(), ts_string = os.date("[%H:%M:%S]"), severity = severity, device = device, diff --git a/scada-common/util.lua b/scada-common/util.lua index 1d4e11c..050b18a 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -1,3 +1,5 @@ +-- PRINT -- + -- we are overwriting 'print' so save it first local _print = print @@ -21,6 +23,28 @@ function println_ts(message) _print(os.date("[%H:%M:%S] ") .. message) end +-- TIME -- + +function time_ms() + return os.epoch('local') +end + +function time_s() + return os.epoch('local') / 1000 +end + +function time() + return time_ms() +end + +-- PARALLELIZATION -- + +-- block waiting for parallel call +function task_wait(f) + parallel.waitForAll(f) +end + +-- WATCHDOG -- -- ComputerCraft OS Timer based Watchdog -- triggers a timer event if not fed within 'timeout' seconds diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 3adb85d..695b34b 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -185,7 +185,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if rplc_pkt.length == 2 then local srv_start = rplc_pkt.data[1] local plc_send = rplc_pkt.data[2] - local srv_now = os.epoch() + local srv_now = util.time() self.last_rtt = srv_now - srv_start if self.last_rtt < 0 then @@ -351,7 +351,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.periodics.keep_alive = self.periodics.keep_alive + elapsed if self.periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send(RPLC_TYPES.KEEP_ALIVE, { os.epoch() }) + _send(RPLC_TYPES.KEEP_ALIVE, { util.time() }) self.periodics.keep_alive = 0 end From 0fc49d312d21db6f0e31a8262df85287f1667d73 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 11:40:53 -0400 Subject: [PATCH 029/168] #32 parallel reactor PLC code --- reactor-plc/plc.lua | 19 +-- reactor-plc/startup.lua | 261 ++++++---------------------------- reactor-plc/threads.lua | 304 ++++++++++++++++++++++++++++++++++++++++ scada-common/util.lua | 2 +- 4 files changed, 358 insertions(+), 228 deletions(-) create mode 100644 reactor-plc/threads.lua diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 67708d5..50092a3 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -89,7 +89,7 @@ function iss_init(reactor) log._error("ISS: failed to check reactor coolant level") return false else - return coolant_filled < 2 + return coolant_filled < 0.02 end end @@ -122,7 +122,9 @@ function iss_init(reactor) } -- check system states in order of severity - if self.cache[1] then + if self.tripped then + status = self.trip_cause + elseif self.cache[1] then log._warning("ISS: damage critical!") status = "dmg_crit" elseif self.cache[4] then @@ -143,25 +145,24 @@ function iss_init(reactor) elseif self.timed_out then log._warning("ISS: supervisor connection timeout!") status = "timeout" - elseif self.tripped then - status = self.trip_cause else self.tripped = false end - -- if a trip occured... - if status ~= "ok" then + -- if a new trip occured... + local first_trip = false + if not was_tripped and status ~= "ok" then log._warning("ISS: reactor SCRAM") + + first_trip = true self.tripped = true self.trip_cause = status + self.reactor.scram() if self.reactor.__p_is_faulted() then log._error("ISS: failed reactor SCRAM") end end - - -- evaluate if this is a new trip - local first_trip = not was_tripped and self.tripped return self.tripped, status, first_trip end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index acf19b2..c79ec77 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -9,8 +9,9 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") +os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.2.8" +local R_PLC_VERSION = "alpha-v0.3.0" local print = util.print local println = util.println @@ -25,21 +26,37 @@ println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") -- mount connected devices ppm.mount_all() -local reactor = ppm.get_fission_reactor() -local modem = ppm.get_wireless_modem() +-- shared memory across threads +local __shared_memory = { + networked = config.NETWORKED, -local networked = config.NETWORKED + plc_state = { + init_ok = true, + scram = true, + degraded = false, + no_reactor = false, + no_modem = false + }, + + plc_devices = { + reactor = ppm.get_fission_reactor(), + modem = ppm.get_wireless_modem() + }, -local plc_state = { - init_ok = true, - scram = true, - degraded = false, - no_reactor = false, - no_modem = false + system = { + iss = nil, + plc_comms = nil, + conn_watchdog = nil + } } +local smem_dev = __shared_memory.plc_devices +local smem_sys = __shared_memory.system + +local plc_state = __shared_memory.plc_state + -- we need a reactor and a modem -if reactor == nil then +if smem_dev.reactor == nil then println("boot> fission reactor not found"); log._warning("no reactor on startup") @@ -47,12 +64,12 @@ if reactor == nil then plc_state.degraded = true plc_state.no_reactor = true end -if networked and modem == nil then +if networked and smem_dev.modem == nil then println("boot> wireless modem not found") log._warning("no wireless modem on startup") - if reactor ~= nil then - reactor.scram() + if smem_dev.reactor ~= nil then + smem_dev.reactor.scram() end plc_state.init_ok = false @@ -60,43 +77,29 @@ if networked and modem == nil then plc_state.no_modem = true end -local iss = nil -local plc_comms = nil -local conn_watchdog = nil - --- send status updates at ~3.33Hz (every 6 server ticks) (every 3 loop ticks) --- send link requests at 0.5Hz (every 40 server ticks) (every 20 loop ticks) -local UPDATE_TICKS = 3 -local LINK_TICKS = 20 - -local loop_clock = nil -local ticks_to_update = LINK_TICKS -- start by linking - function init() if plc_state.init_ok then -- just booting up, no fission allowed (neutrons stay put thanks) - reactor.scram() + smem_dev.reactor.scram() -- init internal safety system - iss = plc.iss_init(reactor) + smem_sys.iss = plc.iss_init(smem_dev.reactor) log._debug("iss init") - if networked then + if __shared_memory.networked then -- start comms - plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) + smem_sys.plc_comms = plc.comms_init(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss) log._debug("comms init") -- comms watchdog, 3 second timeout - conn_watchdog = util.new_watchdog(3) + smem_sys.conn_watchdog = util.new_watchdog(3) log._debug("conn watchdog started") else println("boot> starting in offline mode"); log._debug("running without networking") end - -- loop clock (10Hz, 2 ticks) - loop_clock = os.startTimer(0.1) - log._debug("loop clock started") + os.queueEvent("clock_start") println("boot> completed"); else @@ -108,191 +111,13 @@ end -- initialize PLC init() --- event loop -while true do - local event, param1, param2, param3, param4, param5 = os.pullEventRaw() +-- init threads +local main_thread = threads.thread__main(__shared_memory, init) +local iss_thread = threads.thread__iss(__shared_memory) +-- local comms_thread = plc.thread__comms(__shared_memory) - if plc_state.init_ok then - -- if we tried to SCRAM but failed, keep trying - -- if it disconnected, isPowered will return nil (and error logs will get spammed at 10Hz, so disable reporting) - -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) - ppm.disable_reporting() - if plc_state.scram and reactor.getStatus() then - reactor.scram() - end - ppm.enable_reporting() - end - - -- check for peripheral changes before ISS checks - if event == "peripheral_detach" then - local device = ppm.handle_unmount(param1) - - if device.type == "fissionReactor" then - println_ts("reactor disconnected!") - log._error("reactor disconnected!") - plc_state.no_reactor = true - plc_state.degraded = true - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? - elseif networked and device.type == "modem" then - -- we only care if this is our wireless modem - if device.dev == modem then - println_ts("wireless modem disconnected!") - log._error("comms modem disconnected!") - plc_state.no_modem = true - - if plc_state.init_ok then - -- try to scram reactor if it is still connected - plc_state.scram = true - if reactor.scram() then - println_ts("successful reactor SCRAM") - log._error("successful reactor SCRAM") - else - println_ts("failed reactor SCRAM") - log._error("failed reactor SCRAM") - end - end - - plc_state.degraded = true - else - log._warning("non-comms modem disconnected") - end - end - elseif event == "peripheral" then - local type, device = ppm.mount(param1) - - if type == "fissionReactor" then - -- reconnected reactor - reactor = device - - plc_state.scram = true - reactor.scram() - - println_ts("reactor reconnected.") - log._info("reactor reconnected.") - plc_state.no_reactor = false - - if plc_state.init_ok then - iss.reconnect_reactor(reactor) - if networked then - plc_comms.reconnect_reactor(reactor) - end - end - - -- determine if we are still in a degraded state - if not networked or ppm.get_device("modem") ~= nil then - plc_state.degraded = false - end - elseif networked and type == "modem" then - if device.isWireless() then - -- reconnected modem - modem = device - - if plc_state.init_ok then - plc_comms.reconnect_modem(modem) - end - - println_ts("wireless modem reconnected.") - log._info("comms modem reconnected.") - plc_state.no_modem = false - - -- determine if we are still in a degraded state - if ppm.get_device("fissionReactor") ~= nil then - plc_state.degraded = false - end - else - log._info("wired modem reconnected.") - end - end - - if not plc_state.init_ok and not plc_state.degraded then - plc_state.init_ok = true - init() - end - end - - -- ISS - if plc_state.init_ok then - -- if we are in standalone mode, continuously reset ISS - -- ISS will trip again if there are faults, but if it isn't cleared, the user can't re-enable - if not networked then - plc_state.scram = false - iss.reset() - end - - -- check safety (SCRAM occurs if tripped) - if not plc_state.degraded then - local iss_tripped, iss_status_string, iss_first = iss.check() - plc_state.scram = plc_state.scram or iss_tripped - - if iss_first then - println_ts("[ISS] reactor shutdown, safety tripped: " .. iss_status_string) - if networked then - plc_comms.send_iss_alarm(iss_status_string) - end - end - elseif not plc_state.no_reactor then - -- degraded but we have a reactor - reactor.scram() - end - end - - -- handle event - if event == "timer" and param1 == loop_clock then - -- basic event tick, send updated data if it is time (~3.33Hz) - -- iss was already checked (that's the main reason for this tick rate) - if networked and not plc_state.no_modem then - ticks_to_update = ticks_to_update - 1 - - if plc_comms.is_linked() then - if ticks_to_update <= 0 then - plc_comms.send_status(iss_tripped, plc_state.degraded) - plc_comms.send_iss_status() - ticks_to_update = UPDATE_TICKS - end - else - if ticks_to_update <= 0 then - plc_comms.send_link_req() - ticks_to_update = LINK_TICKS - end - end - end - - -- start next clock timer - loop_clock = os.startTimer(0.1) - elseif event == "modem_message" and networked and not plc_state.no_modem then - -- got a packet - -- feed the watchdog first so it doesn't uhh...eat our packets - conn_watchdog.feed() - - -- handle the packet (plc_state passed to allow clearing SCRAM flag) - local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) - plc_comms.handle_packet(packet, plc_state) - elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then - -- haven't heard from server recently? shutdown reactor - plc_state.scram = true - plc_comms.unlink() - iss.trip_timeout() - println_ts("server timeout, reactor disabled") - log._warning("server timeout, reactor disabled") - end - - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - -- safe exit - log._warning("terminate requested, exiting...") - if plc_state.init_ok then - plc_state.scram = true - reactor.scram() - if reactor.__p_is_ok() then - println_ts("reactor disabled") - else - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? - println_ts("exiting, reactor failed to disable") - end - end - break - end -end +-- run threads +parallel.waitForAll(main_thread.exec, iss_thread.exec) -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? println_ts("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua new file mode 100644 index 0000000..2cd282b --- /dev/null +++ b/reactor-plc/threads.lua @@ -0,0 +1,304 @@ +-- #REQUIRES comms.lua +-- #REQUIRES ppm.lua +-- #REQUIRES plc.lua +-- #REQUIRES util.lua + +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + +local async_wait = util.async_wait + +local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) +local ISS_CLOCK = 0.5 -- (2Hz, 10 ticks) + +local ISS_EVENT = { + SCRAM = 1, + DEGRADED_SCRAM = 2, + TRIP_TIMEOUT = 3 +} + +-- main thread +function thread__main(shared_memory, init) + -- execute thread + local exec = function () + -- send status updates at 2Hz (every 10 server ticks) (every loop tick) + -- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks) + local LINK_TICKS = 4 + + local loop_clock = nil + local ticks_to_update = LINK_TICKS -- start by linking + + -- load in from shared memory + local networked = shared_memory.networked + local plc_state = shared_memory.plc_state + local plc_devices = shared_memory.plc_devices + + local iss = shared_memory.system.iss + local plc_comms = shared_memory.system.plc_comms + local conn_watchdog = shared_memory.system.conn_watchdog + + -- debug + -- local last_update = util.time() + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + + -- handle event + if event == "timer" and param1 == loop_clock then + -- core clock tick + if networked then + -- start next clock timer + loop_clock = os.startTimer(MAIN_CLOCK) + + -- send updated data + if not plc_state.no_modem then + + if plc_comms.is_linked() then + async_wait(function () + plc_comms.send_status(iss_tripped, plc_state.degraded) + plc_comms.send_iss_status() + end) + else + ticks_to_update = ticks_to_update - 1 + + if ticks_to_update <= 0 then + plc_comms.send_link_req() + ticks_to_update = LINK_TICKS + end + end + end + + -- debug + -- print(util.time() - last_update) + -- println("ms") + -- last_update = util.time() + end + elseif event == "modem_message" and networked and not plc_state.no_modem then + -- got a packet + -- feed the watchdog first so it doesn't uhh...eat our packets + conn_watchdog.feed() + + -- handle the packet (plc_state passed to allow clearing SCRAM flag) + local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) + async_wait(function () plc_comms.handle_packet(packet, plc_state) end) + elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then + -- haven't heard from server recently? shutdown reactor + println("timed out, passing event") + plc_comms.unlink() + os.queueEvent("iss_command", ISS_EVENT.TRIP_TIMEOUT) + elseif event == "peripheral_detach" then + -- peripheral disconnect + local device = ppm.handle_unmount(param1) + + if device.type == "fissionReactor" then + println_ts("reactor disconnected!") + log._error("reactor disconnected!") + plc_state.no_reactor = true + plc_state.degraded = true + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? + elseif networked and device.type == "modem" then + -- we only care if this is our wireless modem + if device.dev == modem then + println_ts("wireless modem disconnected!") + log._error("comms modem disconnected!") + plc_state.no_modem = true + + if plc_state.init_ok then + -- try to scram reactor if it is still connected + os.queueEvent("iss_command", ISS_EVENT.DEGRADED_SCRAM) + end + + plc_state.degraded = true + else + log._warning("non-comms modem disconnected") + end + end + elseif event == "peripheral" then + -- peripheral connect + local type, device = ppm.mount(param1) + + if type == "fissionReactor" then + -- reconnected reactor + plc_devices.reactor = device + + os.queueEvent("iss_command", ISS_EVENT.SCRAM) + + println_ts("reactor reconnected.") + log._info("reactor reconnected.") + plc_state.no_reactor = false + + if plc_state.init_ok then + iss.reconnect_reactor(plc_devices.reactor) + if networked then + plc_comms.reconnect_reactor(plc_devices.reactor) + end + end + + -- determine if we are still in a degraded state + if not networked or ppm.get_device("modem") ~= nil then + plc_state.degraded = false + end + elseif networked and type == "modem" then + if device.isWireless() then + -- reconnected modem + plc_devices.modem = device + + if plc_state.init_ok then + plc_comms.reconnect_modem(plc_devices.modem) + end + + println_ts("wireless modem reconnected.") + log._info("comms modem reconnected.") + plc_state.no_modem = false + + -- determine if we are still in a degraded state + if ppm.get_device("fissionReactor") ~= nil then + plc_state.degraded = false + end + else + log._info("wired modem reconnected.") + end + end + + if not plc_state.init_ok and not plc_state.degraded then + plc_state.init_ok = true + init() + end + elseif event == "clock_start" then + -- start loop clock + loop_clock = os.startTimer(MAIN_CLOCK) + log._debug("loop clock started") + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + -- iss handles reactor shutdown + log._warning("terminate requested, main thread exiting") + break + end + end + end + + return { exec = exec } +end + +-- ISS monitor thread +function thread__iss(shared_memory) + -- execute thread + local exec = function () + local loop_clock = nil + + -- load in from shared memory + local networked = shared_memory.networked + local plc_state = shared_memory.plc_state + local plc_devices = shared_memory.plc_devices + + local iss = shared_memory.system.iss + local plc_comms = shared_memory.system.plc_comms + + -- debug + -- local last_update = util.time() + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + + local reactor = shared_memory.plc_devices.reactor + + if event == "timer" and param1 == loop_clock then + -- start next clock timer + loop_clock = os.startTimer(ISS_CLOCK) + + -- ISS checks + if plc_state.init_ok then + -- if we tried to SCRAM but failed, keep trying + -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) + async_wait(function () + if not plc_state.no_reactor and plc_state.scram and reactor.getStatus() then + reactor.scram() + end + end) + + -- if we are in standalone mode, continuously reset ISS + -- ISS will trip again if there are faults, but if it isn't cleared, the user can't re-enable + if not networked then + plc_state.scram = false + iss.reset() + end + + -- check safety (SCRAM occurs if tripped) + async_wait(function () + if not plc_state.degraded then + local iss_tripped, iss_status_string, iss_first = iss.check() + plc_state.scram = plc_state.scram or iss_tripped + + if iss_first then + println_ts("[ISS] SCRAM! safety trip: " .. iss_status_string) + if networked then + plc_comms.send_iss_alarm(iss_status_string) + end + end + end + end) + end + + -- debug + -- print(util.time() - last_update) + -- println("ms") + -- last_update = util.time() + elseif event == "iss_command" then + -- handle ISS commands + println("got iss command?") + if param1 == ISS_EVENT.SCRAM then + -- basic SCRAM + plc_state.scram = true + async_wait(reactor.scram) + elseif param1 == ISS_EVENT.DEGRADED_SCRAM then + -- SCRAM with print + plc_state.scram = true + async_wait(function () + if reactor.scram() then + println_ts("successful reactor SCRAM") + log._error("successful reactor SCRAM") + else + println_ts("failed reactor SCRAM") + log._error("failed reactor SCRAM") + end + end) + elseif param1 == ISS_EVENT.TRIP_TIMEOUT then + -- watchdog tripped + plc_state.scram = true + iss.trip_timeout() + println_ts("server timeout, reactor disabled") + log._warning("server timeout, reactor disabled") + end + elseif event == "clock_start" then + -- start loop clock + loop_clock = os.startTimer(ISS_CLOCK) + log._debug("loop clock started") + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + -- safe exit + log._warning("terminate requested, iss thread shutdown") + if plc_state.init_ok then + plc_state.scram = true + async_wait(reactor.scram) + if reactor.__p_is_ok() then + println_ts("reactor disabled") + else + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? + println_ts("exiting, reactor failed to disable") + end + end + break + end + end + end + + return { exec = exec } +end diff --git a/scada-common/util.lua b/scada-common/util.lua index 050b18a..97ce601 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -40,7 +40,7 @@ end -- PARALLELIZATION -- -- block waiting for parallel call -function task_wait(f) +function async_wait(f) parallel.waitForAll(f) end From b1998b61bcf8a4f090a8be5263966cad800ee9e1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 11:44:34 -0400 Subject: [PATCH 030/168] #32 parallel RTU execution of packet handler --- rtu/startup.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 4534f18..ecb3f3d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,13 +17,15 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.2.4" +local RTU_VERSION = "alpha-v0.3.0" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +local async_wait = util.async_wait + log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) log._info("========================================") @@ -218,6 +220,9 @@ while true do end end elseif event == "timer" and param1 == loop_clock then + -- start next clock timer + loop_clock = os.startTimer(2) + -- period tick, if we are linked send heartbeat, if not send advertisement if linked then rtu_comms.send_heartbeat() @@ -225,15 +230,12 @@ while true do -- advertise units rtu_comms.send_advertisement(units) end - - -- start next clock timer - loop_clock = os.startTimer(2) elseif event == "modem_message" then -- got a packet local link_ref = { linked = linked } local packet = rtu_comms.parse_packet(p1, p2, p3, p4, p5) - rtu_comms.handle_packet(packet, units, link_ref) + async_wait(function () rtu_comms.handle_packet(packet, units, link_ref) end) -- if linked, stop sending advertisements linked = link_ref.linked From e119c112044e908f90df14079221a4d9cf376bce Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 15:44:28 -0400 Subject: [PATCH 031/168] removed debug print --- reactor-plc/threads.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 2cd282b..8bd9626 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -251,7 +251,6 @@ function thread__iss(shared_memory) -- last_update = util.time() elseif event == "iss_command" then -- handle ISS commands - println("got iss command?") if param1 == ISS_EVENT.SCRAM then -- basic SCRAM plc_state.scram = true From b861d3f66841e9d24d4942fdf9b921a6ffb810a6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 15:46:32 -0400 Subject: [PATCH 032/168] removed debug print --- reactor-plc/threads.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 8bd9626..201abe1 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -86,7 +86,6 @@ function thread__main(shared_memory, init) async_wait(function () plc_comms.handle_packet(packet, plc_state) end) elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor - println("timed out, passing event") plc_comms.unlink() os.queueEvent("iss_command", ISS_EVENT.TRIP_TIMEOUT) elseif event == "peripheral_detach" then From 3ef2902829a7eb317624433a12fa7ab447205a22 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 15:49:04 -0400 Subject: [PATCH 033/168] apparently I forgot how to spell receive a few more times --- reactor-plc/plc.lua | 2 +- rtu/rtu.lua | 2 +- supervisor/supervisor.lua | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 50092a3..a6d260a 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -329,7 +329,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local s_pkt = scada_packet() -- parse packet as generic SCADA packet - s_pkt.recieve(side, sender, reply_to, message, distance) + s_pkt.receive(side, sender, reply_to, message, distance) if s_pkt.is_valid() then -- get as RPLC packet diff --git a/rtu/rtu.lua b/rtu/rtu.lua index dde73af..ccf6e44 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -159,7 +159,7 @@ function rtu_comms(modem, local_port, server_port) local s_pkt = comms.scada_packet() -- parse packet as generic SCADA packet - s_pkt.recieve(side, sender, reply_to, message, distance) + s_pkt.receive(side, sender, reply_to, message, distance) if s_pkt.is_valid() then -- get as MODBUS TCP packet diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 25154bb..31b032e 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -67,7 +67,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) local s_pkt = scada_packet() -- parse packet as generic SCADA packet - s_pkt.recieve(side, sender, reply_to, message, distance) + s_pkt.receive(side, sender, reply_to, message, distance) if s_pkt.is_valid() then -- get as MODBUS TCP packet @@ -146,7 +146,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) log._debug("illegal packet type " .. protocol .. " on device listening channel") end -- coordinator listening channel - elseif reciever == self.coord_listen then + elseif receiver == self.coord_listen then if protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet elseif protocol == PROTOCOLS.COORD_DATA then From 19a4b3c0ef5a21e84840cb69a8d505955ee5df26 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 15:50:24 -0400 Subject: [PATCH 034/168] ticked up versions --- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index c79ec77..f37280d 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.3.0" +local R_PLC_VERSION = "alpha-v0.3.1" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index ecb3f3d..4626df2 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.3.0" +local RTU_VERSION = "alpha-v0.3.1" local print = util.print local println = util.println From f7f723829c69adf70b2835984b81f7230f27cba0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 25 Apr 2022 21:00:50 -0400 Subject: [PATCH 035/168] #7 work on PLC session comms, bugfixes with comms, general supervisor bugfixes --- reactor-plc/plc.lua | 157 ++++++++++++++++-------------- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 42 ++++---- rtu/startup.lua | 4 +- scada-common/comms.lua | 26 ++--- supervisor/mqueue.lua | 4 +- supervisor/session/svsessions.lua | 12 ++- supervisor/startup.lua | 4 +- supervisor/supervisor.lua | 41 +++++--- 9 files changed, 158 insertions(+), 134 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index a6d260a..da71998 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -6,6 +6,11 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local RPLC_LINKING = comms.RPLC_LINKING +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main SCADA supervisor/coordinator control @@ -167,6 +172,10 @@ function iss_init(reactor) return self.tripped, status, first_trip end + -- get the ISS status + local status = function () return self.cache end + local is_tripped = function () return self.tripped end + -- reset the ISS local reset = function () self.timed_out = false @@ -174,17 +183,13 @@ function iss_init(reactor) self.trip_cause = "" end - -- get the ISS status - local status = function () - return self.cache - end - return { reconnect_reactor = reconnect_reactor, trip_timeout = trip_timeout, check = check, - reset = reset, - status = status + status = status, + is_tripped = is_tripped, + reset = reset } end @@ -225,7 +230,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- variable reactor status information, excluding heating rate local _reactor_status = function () - ppm.clear_fault() return { self.reactor.getStatus(), self.reactor.getBurnRate(), @@ -249,20 +253,24 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.reactor.getHeatedCoolant()['amount'], self.reactor.getHeatedCoolantNeeded(), self.reactor.getHeatedCoolantFilledPercentage() - }, ppm.faulted() + }, self.reactor.__p_is_faulted() end local _update_status_cache = function () local status, faulted = _reactor_status() local changed = false - if not faulted then - for i = 1, #status do - if status[i] ~= self.status_cache[i] then - changed = true - break + if self.status_cache ~= nil then + if not faulted then + for i = 1, #status do + if status[i] ~= self.status_cache[i] then + changed = true + break + end end end + else + changed = true end if changed then @@ -285,8 +293,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- send structure properties (these should not change) -- (server will cache these) local _send_struct = function () - ppm.clear_fault() - local mek_data = { self.reactor.getHeatCapacity(), self.reactor.getFuelAssemblies(), @@ -298,7 +304,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.reactor.getMaxBurnRate() } - if not ppm.is_faulted() then + if not self.reactor.__p_is_faulted() then _send(RPLC_TYPES.MEK_STRUCT, mek_data) else log._error("failed to send structure: PPM fault") @@ -323,10 +329,48 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _update_status_cache() end + -- attempt to establish link with supervisor + local send_link_req = function () + _send(RPLC_TYPES.LINK_REQ, { self.id }) + end + + -- send live status information + local send_status = function (degraded) + local mek_data = nil + + if _update_status_cache() then + mek_data = self.status_cache + end + + local sys_status = { + util.time(), + (not self.scrammed), + iss.is_tripped(), + degraded, + self.reactor.getHeatingRate(), + mek_data + } + + _send(RPLC_TYPES.STATUS, sys_status) + end + + local send_iss_status = function () + _send(RPLC_TYPES.ISS_STATUS, iss.status()) + end + + local send_iss_alarm = function (cause) + local iss_alarm = { + cause, + iss.status() + } + + _send(RPLC_TYPES.ISS_ALARM, iss_alarm) + end + -- parse an RPLC packet local parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil - local s_pkt = scada_packet() + local s_pkt = comms.scada_packet() -- parse packet as generic SCADA packet s_pkt.receive(side, sender, reply_to, message, distance) @@ -358,7 +402,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() - elseif self.r_seq_num >= packet.scada_frame.seq_num() then + elseif self.linked and self.r_seq_num >= packet.scada_frame.seq_num() then log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return else @@ -388,19 +432,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if link_ack == RPLC_LINKING.ALLOW then _send_struct() - send_status() + send_status(plc_state.degraded) log._debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then -- @todo: make sure this doesn't become a MITM security risk - print_ts("received unsolicited link denial, unlinking\n") - log._debug("unsolicited rplc link request denied") + println_ts("received unsolicited link denial, unlinking") + log._debug("unsolicited RPLC link request denied") elseif link_ack == RPLC_LINKING.COLLISION then -- @todo: make sure this doesn't become a MITM security risk - print_ts("received unsolicited link collision, unlinking\n") - log._warning("unsolicited rplc link request collision") + println_ts("received unsolicited link collision, unlinking") + log._warning("unsolicited RPLC link request collision") else - print_ts("invalid unsolicited link response\n") - log._error("unsolicited unknown rplc link request response") + println_ts("invalid unsolicited link response") + log._error("unsolicited unknown RPLC link request response") end self.linked = link_ack == RPLC_LINKING.ALLOW @@ -452,22 +496,25 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local link_ack = packet.data[1] if link_ack == RPLC_LINKING.ALLOW then - print_ts("...linked!\n") - log._debug("rplc link request approved") + println_ts("linked!") + log._debug("RPLC link request approved") + + -- reset remote sequence number + self.r_seq_num = nil _send_struct() - send_status() + send_status(plc_state.degraded) log._debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then - print_ts("...denied, retrying...\n") - log._debug("rplc link request denied") + println_ts("link request denied, retrying...") + log._debug("RPLC link request denied") elseif link_ack == RPLC_LINKING.COLLISION then - print_ts("reactor PLC ID collision (check config), retrying...\n") - log._warning("rplc link request collision") + println_ts("reactor PLC ID collision (check config), retrying...") + log._warning("RPLC link request collision") else - print_ts("invalid link response, bad channel? retrying...\n") - log._error("unknown rplc link request response") + println_ts("invalid link response, bad channel? retrying...") + log._error("unknown RPLC link request response") end self.linked = link_ack == RPLC_LINKING.ALLOW @@ -480,46 +527,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end end - -- attempt to establish link with supervisor - local send_link_req = function () - _send(RPLC_TYPES.LINK_REQ, {}) - end - - -- send live status information - -- overridden : if ISS force disabled reactor - -- degraded : if PLC status is degraded - local send_status = function (overridden, degraded) - local mek_data = nil - - if _update_status_cache() then - mek_data = self.status_cache - end - - local sys_status = { - util.time(), - (not self.scrammed), - overridden, - degraded, - self.reactor.getHeatingRate(), - mek_data - } - - _send(RPLC_TYPES.STATUS, sys_status) - end - - local send_iss_status = function () - _send(RPLC_TYPES.ISS_STATUS, iss.status()) - end - - local send_iss_alarm = function (cause) - local iss_alarm = { - cause, - iss.status() - } - - _send(RPLC_TYPES.ISS_ALARM, iss_alarm) - end - local is_scrammed = function () return self.scrammed end local is_linked = function () return self.linked end local unlink = function () self.linked = false end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index f37280d..a4ac6a8 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.3.1" +local R_PLC_VERSION = "alpha-v0.3.3" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 201abe1..e9f0d3d 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -10,8 +10,8 @@ local println_ts = util.println_ts local async_wait = util.async_wait -local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) -local ISS_CLOCK = 0.5 -- (2Hz, 10 ticks) +local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) +local ISS_CLOCK = 0.25 -- (4Hz, 5 ticks) however this is AFTER all the ISS checks, so it is a pause between calls, not start-to-start local ISS_EVENT = { SCRAM = 1, @@ -28,7 +28,7 @@ function thread__main(shared_memory, init) local LINK_TICKS = 4 local loop_clock = nil - local ticks_to_update = LINK_TICKS -- start by linking + local ticks_to_update = 0 -- load in from shared memory local networked = shared_memory.networked @@ -40,7 +40,7 @@ function thread__main(shared_memory, init) local conn_watchdog = shared_memory.system.conn_watchdog -- debug - -- local last_update = util.time() + local last_update = util.time() -- event loop while true do @@ -55,26 +55,25 @@ function thread__main(shared_memory, init) -- send updated data if not plc_state.no_modem then - if plc_comms.is_linked() then async_wait(function () plc_comms.send_status(iss_tripped, plc_state.degraded) plc_comms.send_iss_status() end) else - ticks_to_update = ticks_to_update - 1 - - if ticks_to_update <= 0 then + if ticks_to_update == 0 then plc_comms.send_link_req() ticks_to_update = LINK_TICKS + else + ticks_to_update = ticks_to_update - 1 end end end -- debug - -- print(util.time() - last_update) - -- println("ms") - -- last_update = util.time() + print(util.time() - last_update) + println("ms") + last_update = util.time() end elseif event == "modem_message" and networked and not plc_state.no_modem then -- got a packet @@ -82,8 +81,10 @@ function thread__main(shared_memory, init) conn_watchdog.feed() -- handle the packet (plc_state passed to allow clearing SCRAM flag) - local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) - async_wait(function () plc_comms.handle_packet(packet, plc_state) end) + async_wait(function () + local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) + plc_comms.handle_packet(packet, plc_state) + end) elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor plc_comms.unlink() @@ -169,7 +170,7 @@ function thread__main(shared_memory, init) elseif event == "clock_start" then -- start loop clock loop_clock = os.startTimer(MAIN_CLOCK) - log._debug("loop clock started") + log._debug("main thread started") end -- check for termination request @@ -208,9 +209,6 @@ function thread__iss(shared_memory) local reactor = shared_memory.plc_devices.reactor if event == "timer" and param1 == loop_clock then - -- start next clock timer - loop_clock = os.startTimer(ISS_CLOCK) - -- ISS checks if plc_state.init_ok then -- if we tried to SCRAM but failed, keep trying @@ -244,6 +242,10 @@ function thread__iss(shared_memory) end) end + -- start next clock timer after all the long operations + -- otherwise we will never get around to other events + loop_clock = os.startTimer(ISS_CLOCK) + -- debug -- print(util.time() - last_update) -- println("ms") @@ -270,13 +272,13 @@ function thread__iss(shared_memory) -- watchdog tripped plc_state.scram = true iss.trip_timeout() - println_ts("server timeout, reactor disabled") - log._warning("server timeout, reactor disabled") + println_ts("server timeout") + log._warning("server timeout") end elseif event == "clock_start" then -- start loop clock loop_clock = os.startTimer(ISS_CLOCK) - log._debug("loop clock started") + log._debug("iss thread started") end -- check for termination request diff --git a/rtu/startup.lua b/rtu/startup.lua index 4626df2..2d4c58f 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.3.1" +local RTU_VERSION = "alpha-v0.3.2" local print = util.print local println = util.println @@ -233,7 +233,7 @@ while true do elseif event == "modem_message" then -- got a packet local link_ref = { linked = linked } - local packet = rtu_comms.parse_packet(p1, p2, p3, p4, p5) + local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5) async_wait(function () rtu_comms.handle_packet(packet, units, link_ref) end) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 972f768..bca5682 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -74,12 +74,14 @@ function scada_packet() self.raw = self.modem_msg_in.msg - if #self.raw >= 3 then - self.valid = true - self.seq_num = self.raw[1] - self.protocol = self.raw[2] - self.length = #self.raw[3] - self.payload = self.raw[3] + if type(self.raw) == "table" then + if #self.raw >= 3 then + self.valid = true + self.seq_num = self.raw[1] + self.protocol = self.raw[2] + self.length = #self.raw[3] + self.payload = self.raw[3] + end end return self.valid @@ -90,12 +92,12 @@ function scada_packet() local modem_event = function () return self.modem_msg_in end local raw_sendable = function () return self.raw end - local sender = function () return self.s_port end - local receiver = function () return self.r_port end + local local_port = function () return self.modem_msg_in.s_port end + local remote_port = function () return self.modem_msg_in.r_port end local is_valid = function () return self.valid end - local seq_num = function () return self.seq_num end + local seq_num = function () return self.seq_num end local protocol = function () return self.protocol end local length = function () return self.length end local data = function () return self.payload end @@ -107,9 +109,9 @@ function scada_packet() -- raw access modem_event = modem_event, raw_sendable = raw_sendable, - -- sender/receiver - sender = sender, - receiver = receiver, + -- ports + local_port = local_port, + remote_port = remote_port, -- well-formed is_valid = is_valid, -- packet properties diff --git a/supervisor/mqueue.lua b/supervisor/mqueue.lua index 1aa5869..f79e686 100644 --- a/supervisor/mqueue.lua +++ b/supervisor/mqueue.lua @@ -23,11 +23,11 @@ function new() end local push_packet = function (message) - push(TYPE.PACKET, message) + _push(TYPE.PACKET, message) end local push_command = function (message) - push(TYPE.COMMAND, message) + _push(TYPE.COMMAND, message) end local pop = function () diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index edc2ea2..e02619c 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -70,6 +70,7 @@ function get_reactor_session(reactor) end function establish_plc_session(local_port, remote_port, for_reactor) + util.println(remote_port) if get_reactor_session(for_reactor) == nil then local plc_s = { open = true, @@ -81,9 +82,12 @@ function establish_plc_session(local_port, remote_port, for_reactor) instance = nil } - plc_s.instance = plc.new_session(next_plc_id, plc_s.in_queue, plc_s.out_queue) + plc_s.instance = plc.new_session(self.next_plc_id, for_reactor, plc_s.in_queue, plc_s.out_queue) table.insert(self.plc_sessions, plc_s) - next_plc_id = next_plc_id + 1 + + log._debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id) + + self.next_plc_id = self.next_plc_id + 1 -- success return plc_s.instance.get_id() @@ -129,7 +133,7 @@ local function _iterate(sessions) while not session.out_queue.empty() do local msg = session.out_queue.pop() if msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(self.r_port, self.l_port, msg.message.raw_sendable()) + self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) end end else @@ -163,7 +167,7 @@ local function _free_closed(sessions) end move_to = move_to + 1 else - log._debug("free'ing closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port) + log._debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) sessions[i] = nil end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index fa4de82..27a9db6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.4" +local SUPERVISOR_VERSION = "alpha-v0.1.5" local print = util.print local println = util.println @@ -93,7 +93,7 @@ while true do svsessions.check_all_watchdogs(param1) elseif event == "modem_message" then -- got a packet - local packet = superv_comms.parse_packet(p1, p2, p3, p4, p5) + local packet = superv_comms.parse_packet(param1, param2, param3, param4, param5) superv_comms.handle_packet(packet) end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 31b032e..571ab49 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -5,6 +5,7 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES +local RPLC_LINKING = comms.RPLC_LINKING local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES @@ -64,7 +65,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) -- parse a packet local parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil - local s_pkt = scada_packet() + local s_pkt = comms.scada_packet() -- parse packet as generic SCADA packet s_pkt.receive(side, sender, reply_to, message, distance) @@ -104,21 +105,22 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) local handle_packet = function(packet) if packet ~= nil then - local sender = packet.scada_frame.sender() - local receiver = packet.scada_frame.receiver() + local l_port = packet.scada_frame.local_port() + local r_port = packet.scada_frame.remote_port() local protocol = packet.scada_frame.protocol() -- device (RTU/PLC) listening channel - if receiver == self.dev_listen then + if l_port == self.dev_listen then if protocol == PROTOCOLS.MODBUS_TCP then -- MODBUS response elseif protocol == PROTOCOLS.RPLC then -- reactor PLC packet - local session = svsessions.find_session(SESSION_TYPE.PLC_SESSION, sender) + local session = svsessions.find_session(SESSION_TYPE.PLC_SESSION, r_port) if session then if packet.type == RPLC_TYPES.LINK_REQ then -- new device on this port? that's a collision - _send_plc_linking(sender, { RPLC_LINKING.COLLISION }) + log._debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision") + _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) else -- pass the packet onto the session handler session.in_queue.push_packet(packet) @@ -126,18 +128,25 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) else -- unknown session, is this a linking request? if packet.type == RPLC_TYPES.LINK_REQ then - -- this is a linking request - local plc_id = svsessions.establish_plc_session(sender) - if plc_id == false then - -- reactor already has a PLC assigned - _send_plc_linking(sender, { RPLC_LINKING.COLLISION }) + if packet.length == 1 then + -- this is a linking request + local plc_id = svsessions.establish_plc_session(l_port, r_port, packet.data[1]) + if plc_id == false then + -- reactor already has a PLC assigned + log._debug("PLC_LNK: assignment collision with reactor " .. packet.data[1]) + _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) + else + -- got an ID; assigned to a reactor successfully + log._debug("PLC_LNK: allowed for device at " .. r_port) + _send_plc_linking(r_port, { RPLC_LINKING.ALLOW }) + end else - -- got an ID; assigned to a reactor successfully - _send_plc_linking(sender, { RPLC_LINKING.ALLOW }) + log._debug("PLC_LNK: new linking packet length mismatch") end else -- force a re-link - _send_plc_linking(sender, { RPLC_LINKING.DENY }) + log._debug("PLC_LNK: no session but not a link, force relink") + _send_plc_linking(r_port, { RPLC_LINKING.DENY }) end end elseif protocol == PROTOCOLS.SCADA_MGMT then @@ -146,7 +155,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) log._debug("illegal packet type " .. protocol .. " on device listening channel") end -- coordinator listening channel - elseif receiver == self.coord_listen then + elseif l_port == self.coord_listen then if protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet elseif protocol == PROTOCOLS.COORD_DATA then @@ -155,7 +164,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) log._debug("illegal packet type " .. protocol .. " on coordinator listening channel") end else - log._error("received packet on unused channel " .. receiver, true) + log._error("received packet on unused channel " .. l_port, true) end end end From 68011d6734fa8526f6dc851d506333d8e77007d8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 12:21:10 -0400 Subject: [PATCH 036/168] #32 new threaded PLC code --- reactor-plc/startup.lua | 31 ++- reactor-plc/threads.lua | 273 ++++++++++++++---------- {supervisor => scada-common}/mqueue.lua | 23 +- scada-common/util.lua | 7 +- supervisor/session/plc.lua | 2 +- supervisor/session/svsessions.lua | 2 +- supervisor/startup.lua | 4 +- 7 files changed, 210 insertions(+), 132 deletions(-) rename {supervisor => scada-common}/mqueue.lua (78%) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index a4ac6a8..e448e55 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -6,12 +6,13 @@ os.loadAPI("scada-common/log.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") +os.loadAPI("scada-common/mqueue.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.3.3" +local R_PLC_VERSION = "alpha-v0.4.0" local print = util.print local println = util.println @@ -28,30 +29,42 @@ ppm.mount_all() -- shared memory across threads local __shared_memory = { + -- networked setting networked = config.NETWORKED, + -- PLC system state flags plc_state = { init_ok = true, + shutdown = false, scram = true, degraded = false, no_reactor = false, no_modem = false }, - plc_devices = { + -- core PLC devices + plc_dev = { reactor = ppm.get_fission_reactor(), modem = ppm.get_wireless_modem() }, - system = { + -- system control objects + plc_sys = { iss = nil, plc_comms = nil, conn_watchdog = nil + }, + + -- message queues + q = { + mq_main = mqueue.new(), + mq_iss = mqueue.new(), + mq_comms = mqeuue.new() } } -local smem_dev = __shared_memory.plc_devices -local smem_sys = __shared_memory.system +local smem_dev = __shared_memory.plc_dev +local smem_sys = __shared_memory.plc_sys local plc_state = __shared_memory.plc_state @@ -112,12 +125,12 @@ end init() -- init threads -local main_thread = threads.thread__main(__shared_memory, init) -local iss_thread = threads.thread__iss(__shared_memory) --- local comms_thread = plc.thread__comms(__shared_memory) +local main_thread = threads.thread__main(__shared_memory, init) +local iss_thread = threads.thread__iss(__shared_memory) +local comms_thread = threads.thread__comms(__shared_memory) -- run threads -parallel.waitForAll(main_thread.exec, iss_thread.exec) +parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread.exec) -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? println_ts("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index e9f0d3d..eb5468d 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -8,36 +8,37 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local async_wait = util.async_wait +local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) +local ISS_CLOCK = 0.5 -- (2Hz, 10 ticks) +local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) -local MAIN_CLOCK = 0.5 -- (2Hz, 10 ticks) -local ISS_CLOCK = 0.25 -- (4Hz, 5 ticks) however this is AFTER all the ISS checks, so it is a pause between calls, not start-to-start - -local ISS_EVENT = { +local MQ__ISS_CMD = { SCRAM = 1, DEGRADED_SCRAM = 2, TRIP_TIMEOUT = 3 } +local MQ__COMM_CMD = { + SEND_STATUS = 1 +} + -- main thread -function thread__main(shared_memory, init) +function thread__main(smem, init) -- execute thread local exec = function () -- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks) local LINK_TICKS = 4 - - local loop_clock = nil local ticks_to_update = 0 + local loop_clock = nil -- load in from shared memory - local networked = shared_memory.networked - local plc_state = shared_memory.plc_state - local plc_devices = shared_memory.plc_devices - - local iss = shared_memory.system.iss - local plc_comms = shared_memory.system.plc_comms - local conn_watchdog = shared_memory.system.conn_watchdog + local networked = smem.networked + local plc_state = smem.plc_state + local plc_dev = smem.plc_dev + local iss = smem.plc_sys.iss + local plc_comms = smem.plc_sys.plc_comms + local conn_watchdog = smem.plc_sys.conn_watchdog -- debug local last_update = util.time() @@ -56,10 +57,7 @@ function thread__main(shared_memory, init) -- send updated data if not plc_state.no_modem then if plc_comms.is_linked() then - async_wait(function () - plc_comms.send_status(iss_tripped, plc_state.degraded) - plc_comms.send_iss_status() - end) + smem.q.mq_comms.push_command(MQ__COMM_CMD.SEND_STATUS) else if ticks_to_update == 0 then plc_comms.send_link_req() @@ -80,15 +78,15 @@ function thread__main(shared_memory, init) -- feed the watchdog first so it doesn't uhh...eat our packets conn_watchdog.feed() - -- handle the packet (plc_state passed to allow clearing SCRAM flag) - async_wait(function () - local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) - plc_comms.handle_packet(packet, plc_state) - end) + -- handle the packet + local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) + if packet ~= nil then + smem.q.mq_comms.puch_packet(packet) + end elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor plc_comms.unlink() - os.queueEvent("iss_command", ISS_EVENT.TRIP_TIMEOUT) + smem.q.mq_iss.push_command(MQ__ISS_CMD.TRIP_TIMEOUT) elseif event == "peripheral_detach" then -- peripheral disconnect local device = ppm.handle_unmount(param1) @@ -108,7 +106,7 @@ function thread__main(shared_memory, init) if plc_state.init_ok then -- try to scram reactor if it is still connected - os.queueEvent("iss_command", ISS_EVENT.DEGRADED_SCRAM) + smem.q.mq_iss.push_command(MQ__ISS_CMD.DEGRADED_SCRAM) end plc_state.degraded = true @@ -122,18 +120,18 @@ function thread__main(shared_memory, init) if type == "fissionReactor" then -- reconnected reactor - plc_devices.reactor = device + plc_dev.reactor = device - os.queueEvent("iss_command", ISS_EVENT.SCRAM) + smem.q.mq_iss.push_command(MQ__ISS_CMD.SCRAM) println_ts("reactor reconnected.") log._info("reactor reconnected.") plc_state.no_reactor = false if plc_state.init_ok then - iss.reconnect_reactor(plc_devices.reactor) + iss.reconnect_reactor(plc_dev.reactor) if networked then - plc_comms.reconnect_reactor(plc_devices.reactor) + plc_comms.reconnect_reactor(plc_dev.reactor) end end @@ -144,10 +142,10 @@ function thread__main(shared_memory, init) elseif networked and type == "modem" then if device.isWireless() then -- reconnected modem - plc_devices.modem = device + plc_dev.modem = device if plc_state.init_ok then - plc_comms.reconnect_modem(plc_devices.modem) + plc_comms.reconnect_modem(plc_dev.modem) end println_ts("wireless modem reconnected.") @@ -176,6 +174,7 @@ function thread__main(shared_memory, init) -- check for termination request if event == "terminate" or ppm.should_terminate() then -- iss handles reactor shutdown + plc_state.shutdown = true log._warning("terminate requested, main thread exiting") break end @@ -186,80 +185,66 @@ function thread__main(shared_memory, init) end -- ISS monitor thread -function thread__iss(shared_memory) +function thread__iss(smem) -- execute thread local exec = function () - local loop_clock = nil - -- load in from shared memory - local networked = shared_memory.networked - local plc_state = shared_memory.plc_state - local plc_devices = shared_memory.plc_devices + local networked = smem.networked + local plc_state = smem.plc_state + local plc_dev = smem.plc_dev + local iss = smem.plc_sys.iss + local plc_comms = smem.plc_sys.plc_comms - local iss = shared_memory.system.iss - local plc_comms = shared_memory.system.plc_comms + local iss_queue = smem.q.mq_iss - -- debug - -- local last_update = util.time() + local last_update = util.time() - -- event loop + -- thread loop while true do - local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + local reactor = smem.plc_dev.reactor - local reactor = shared_memory.plc_devices.reactor - - if event == "timer" and param1 == loop_clock then - -- ISS checks - if plc_state.init_ok then - -- if we tried to SCRAM but failed, keep trying - -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) - async_wait(function () - if not plc_state.no_reactor and plc_state.scram and reactor.getStatus() then - reactor.scram() - end - end) - - -- if we are in standalone mode, continuously reset ISS - -- ISS will trip again if there are faults, but if it isn't cleared, the user can't re-enable - if not networked then - plc_state.scram = false - iss.reset() - end - - -- check safety (SCRAM occurs if tripped) - async_wait(function () - if not plc_state.degraded then - local iss_tripped, iss_status_string, iss_first = iss.check() - plc_state.scram = plc_state.scram or iss_tripped - - if iss_first then - println_ts("[ISS] SCRAM! safety trip: " .. iss_status_string) - if networked then - plc_comms.send_iss_alarm(iss_status_string) - end - end - end - end) + -- ISS checks + if plc_state.init_ok then + -- if we tried to SCRAM but failed, keep trying + -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) + if not plc_state.no_reactor and plc_state.scram and reactor.getStatus() then + reactor.scram() end - -- start next clock timer after all the long operations - -- otherwise we will never get around to other events - loop_clock = os.startTimer(ISS_CLOCK) + -- if we are in standalone mode, continuously reset ISS + -- ISS will trip again if there are faults, but if it isn't cleared, the user can't re-enable + if not networked then + plc_state.scram = false + iss.reset() + end - -- debug - -- print(util.time() - last_update) - -- println("ms") - -- last_update = util.time() - elseif event == "iss_command" then - -- handle ISS commands - if param1 == ISS_EVENT.SCRAM then - -- basic SCRAM - plc_state.scram = true - async_wait(reactor.scram) - elseif param1 == ISS_EVENT.DEGRADED_SCRAM then - -- SCRAM with print - plc_state.scram = true - async_wait(function () + -- check safety (SCRAM occurs if tripped) + if not plc_state.degraded then + local iss_tripped, iss_status_string, iss_first = iss.check() + plc_state.scram = plc_state.scram or iss_tripped + + if iss_first then + println_ts("[ISS] SCRAM! safety trip: " .. iss_status_string) + if networked then + plc_comms.send_iss_alarm(iss_status_string) + end + end + end + end + + -- check for messages in the message queue + while comms_queue.ready() do + local msg = comms_queue.pop() + + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__ISS_CMD.SCRAM then + -- basic SCRAM + plc_state.scram = true + reactor.scram() + elseif msg.message == MQ__ISS_CMD.DEGRADED_SCRAM then + -- SCRAM with print + plc_state.scram = true if reactor.scram() then println_ts("successful reactor SCRAM") log._error("successful reactor SCRAM") @@ -267,38 +252,106 @@ function thread__iss(shared_memory) println_ts("failed reactor SCRAM") log._error("failed reactor SCRAM") end - end) - elseif param1 == ISS_EVENT.TRIP_TIMEOUT then - -- watchdog tripped - plc_state.scram = true - iss.trip_timeout() - println_ts("server timeout") - log._warning("server timeout") + elseif msg.message == MQ__ISS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + plc_state.scram = true + iss.trip_timeout() + println_ts("server timeout") + log._warning("server timeout") + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet end - elseif event == "clock_start" then - -- start loop clock - loop_clock = os.startTimer(ISS_CLOCK) - log._debug("iss thread started") + + -- quick yield + if iss_queue.ready() then util.nop() end end -- check for termination request - if event == "terminate" or ppm.should_terminate() then + if plc_state.shutdown then -- safe exit - log._warning("terminate requested, iss thread shutdown") + log._warning("iss thread shutdown initiated") if plc_state.init_ok then plc_state.scram = true - async_wait(reactor.scram) + reactor.scram() if reactor.__p_is_ok() then println_ts("reactor disabled") + log._info("iss thread reactor SCRAM OK") else -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? println_ts("exiting, reactor failed to disable") + log._error("iss thread failed to SCRAM reactor on exit") end end - break + log._warning("iss thread exiting") + return + end + + -- debug + -- print(util.time() - last_update) + -- println("ms") + -- last_update = util.time() + + -- delay before next check + local sleep_for = ISS_CLOCK - (util.time() - last_update) + if sleep_for > 0.05 then + sleep(sleep_for) end end end return { exec = exec } end + +function thread__comms(smem) + -- execute thread + local exec = function () + -- load in from shared memory + local plc_state = smem.plc_state + local plc_comms = smem.plc_sys.plc_comms + + local comms_queue = smem.q.mq_comms + + -- thread loop + while true do + local last_update = util.time() + + -- check for messages in the message queue + while comms_queue.ready() do + local msg = comms_queue.pop() + + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__COMM_CMD.SEND_STATUS then + -- send PLC/ISS status + plc_comms.send_status(plc_state.degraded) + plc_comms.send_iss_status() + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (plc_state passed to allow clearing SCRAM flag) + plc_comms.handle_packet(msg.message, plc_state) + end + + -- quick yield + if comms_queue.ready() then util.nop() end + end + + -- check for termination request + if plc_state.shutdown then + log._warning("comms thread exiting") + return + end + + -- delay before next check + local sleep_for = COMMS_CLOCK - (util.time() - last_update) + if sleep_for > 0.05 then + sleep(sleep_for) + end + end + end +end diff --git a/supervisor/mqueue.lua b/scada-common/mqueue.lua similarity index 78% rename from supervisor/mqueue.lua rename to scada-common/mqueue.lua index f79e686..3881d02 100644 --- a/supervisor/mqueue.lua +++ b/scada-common/mqueue.lua @@ -4,7 +4,8 @@ TYPE = { COMMAND = 0, - PACKET = 1 + DATA = 1, + PACKET = 2 } function new() @@ -17,19 +18,27 @@ function new() local empty = function () return #queue == 0 end + + local ready = function () + return #queue > 0 + end local _push = function (qtype, message) table.insert(queue, { qtype = qtype, message = message }) end - local push_packet = function (message) - _push(TYPE.PACKET, message) - end - local push_command = function (message) _push(TYPE.COMMAND, message) end - + + local push_data = function (message) + _push(TYPE.DATA, message) + end + + local push_packet = function (message) + _push(TYPE.PACKET, message) + end + local pop = function () if #queue > 0 then return table.remove(queue) @@ -41,7 +50,9 @@ function new() return { length = length, empty = empty, + ready = ready, push_packet = push_packet, + push_data = push_data, push_command = push_command, pop = pop } diff --git a/scada-common/util.lua b/scada-common/util.lua index 97ce601..11b258e 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -39,9 +39,10 @@ end -- PARALLELIZATION -- --- block waiting for parallel call -function async_wait(f) - parallel.waitForAll(f) +-- no-op to provide a brief pause (and a yield) +-- EVENT_CONSUMER: this function consumes events +function nop() + sleep(0.05) end -- WATCHDOG -- diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 695b34b..3a51666 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -330,7 +330,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- handle queue -- ------------------ - if not self.in_q.empty() then + if self.in_q.ready() then -- get a new message to process local message = self.in_q.pop() diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index e02619c..9c02ce0 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -130,7 +130,7 @@ local function _iterate(sessions) if ok then -- send packets in out queue -- @todo handle commands if that's being used too - while not session.out_queue.empty() do + while session.out_queue.ready() do local msg = session.out_queue.pop() if msg.qtype == mqueue.TYPE.PACKET then self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 27a9db6..138cfd8 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -7,9 +7,9 @@ os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") os.loadAPI("scada-common/modbus.lua") +os.loadAPI("scada-common/mqueue.lua") os.loadAPI("config.lua") -os.loadAPI("mqueue.lua") os.loadAPI("session/rtu.lua") os.loadAPI("session/plc.lua") @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.5" +local SUPERVISOR_VERSION = "alpha-v0.1.6" local print = util.print local println = util.println From 1ba5c7f8283406613ab8645b54604a1d45835e21 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 12:27:15 -0400 Subject: [PATCH 037/168] fixed PLC mqueue typo and removed unused mq_main --- reactor-plc/startup.lua | 3 +-- reactor-plc/threads.lua | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e448e55..56bb072 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -57,9 +57,8 @@ local __shared_memory = { -- message queues q = { - mq_main = mqueue.new(), mq_iss = mqueue.new(), - mq_comms = mqeuue.new() + mq_comms = mqeueu.new() } } diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index eb5468d..060bbe5 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -1,6 +1,5 @@ -- #REQUIRES comms.lua -- #REQUIRES ppm.lua --- #REQUIRES plc.lua -- #REQUIRES util.lua local print = util.print From ccf06956f9c71b6a27e9a44b7277fae48ab8aaf7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 12:37:28 -0400 Subject: [PATCH 038/168] fixed another typo --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 56bb072..025c274 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -48,7 +48,7 @@ local __shared_memory = { modem = ppm.get_wireless_modem() }, - -- system control objects + -- system objects plc_sys = { iss = nil, plc_comms = nil, diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 060bbe5..ec0d267 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -1,4 +1,5 @@ -- #REQUIRES comms.lua +-- #REQUIRES log.lua -- #REQUIRES ppm.lua -- #REQUIRES util.lua @@ -80,7 +81,7 @@ function thread__main(smem, init) -- handle the packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) if packet ~= nil then - smem.q.mq_comms.puch_packet(packet) + smem.q.mq_comms.push_packet(packet) end elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor From 71be6aca1a09400688e218f5973a71bede67143b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 12:43:32 -0400 Subject: [PATCH 039/168] cleanup and last_update bugfix for comms thread --- reactor-plc/threads.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index ec0d267..f4375c9 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -286,7 +286,7 @@ function thread__iss(smem) end end log._warning("iss thread exiting") - return + break end -- debug @@ -305,6 +305,7 @@ function thread__iss(smem) return { exec = exec } end +-- communications handler thread function thread__comms(smem) -- execute thread local exec = function () @@ -314,10 +315,10 @@ function thread__comms(smem) local comms_queue = smem.q.mq_comms + local last_update = util.time() + -- thread loop while true do - local last_update = util.time() - -- check for messages in the message queue while comms_queue.ready() do local msg = comms_queue.pop() @@ -344,7 +345,7 @@ function thread__comms(smem) -- check for termination request if plc_state.shutdown then log._warning("comms thread exiting") - return + break end -- delay before next check From 8c4598e7a6d6565a8be8944ddb5758f3be94b763 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 12:46:04 -0400 Subject: [PATCH 040/168] #32 new threaded RTU code --- rtu/rtu.lua | 4 +- rtu/startup.lua | 143 ++++++++++++++++------------------------------ rtu/threads.lua | 147 ++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 199 insertions(+), 95 deletions(-) create mode 100644 rtu/threads.lua diff --git a/rtu/rtu.lua b/rtu/rtu.lua index ccf6e44..54bf780 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -183,7 +183,7 @@ function rtu_comms(modem, local_port, server_port) end -- handle a MODBUS/SCADA packet - local handle_packet = function(packet, units, ref) + local handle_packet = function(packet, units, rtu_state) if packet ~= nil then local protocol = packet.scada_frame.protocol() @@ -209,7 +209,7 @@ function rtu_comms(modem, local_port, server_port) -- SCADA management packet if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then -- acknowledgement - ref.linked = true + rtu_state.linked = true elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- request for capabilities again send_advertisement(units) diff --git a/rtu/startup.lua b/rtu/startup.lua index 2d4c58f..990819f 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -6,26 +6,26 @@ os.loadAPI("scada-common/log.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") +os.loadAPI("scada-common/mqueue.lua") os.loadAPI("scada-common/modbus.lua") os.loadAPI("scada-common/rsio.lua") os.loadAPI("config.lua") os.loadAPI("rtu.lua") +os.loadAPI("threads.lua") os.loadAPI("dev/redstone_rtu.lua") os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.3.2" +local RTU_VERSION = "alpha-v0.4.0" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local async_wait = util.async_wait - log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) log._info("========================================") @@ -35,15 +35,37 @@ println(">> RTU " .. RTU_VERSION .. " <<") -- startup ---------------------------------------- -local units = {} -local linked = false - -- mount connected devices ppm.mount_all() +local __shared_memory = { + -- RTU system state flags + rtu_state = { + linked = false, + shutdown = false + }, + + -- core RTU devices + rtu_dev = { + modem = ppm.get_wireless_modem() + }, + + -- system objects + rtu_sys = { + rtu_comms = nil, + units = {} + }, + + -- message queues + q = { + mq_comms = mqeueu.new() + } +} + +local smem_dev = __shared_memory.rtu_dev + -- get modem -local modem = ppm.get_wireless_modem() -if modem == nil then +if smem_dev.modem == nil then println("boot> wireless modem not found") log._warning("no wireless modem on startup") return @@ -52,9 +74,11 @@ end local rtu_comms = rtu.rtu_comms(modem, config.LISTEN_PORT, config.SERVER_PORT) ---------------------------------------- --- determine configuration +-- interpret config and init units ---------------------------------------- +local units = __shared_memory.rtu_sys.units + local rtu_redstone = config.RTU_REDSTONE local rtu_devices = config.RTU_DEVICES @@ -69,12 +93,12 @@ for reactor_idx = 1, #rtu_redstone do for i = 1, #io_table do local valid = false - local config = io_table[i] + local conf = io_table[i] -- verify configuration - if rsio.is_valid_channel(config.channel) and rsio.is_valid_side(config.side) then - if config.bundled_color then - valid = rsio.is_color(config.bundled_color) + if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then + if conf.bundled_color then + valid = rsio.is_color(conf.bundled_color) else valid = true end @@ -87,24 +111,24 @@ for reactor_idx = 1, #rtu_redstone do log._warning(message) else -- link redstone in RTU - local mode = rsio.get_io_mode(config.channel) + local mode = rsio.get_io_mode(conf.channel) if mode == rsio.IO_MODE.DIGITAL_IN then - rs_rtu.link_di(config.channel, config.side, config.bundled_color) + rs_rtu.link_di(conf.channel, conf.side, conf.bundled_color) elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(config.channel, config.side, config.bundled_color) + rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) elseif mode == rsio.IO_MODE.ANALOG_IN then - rs_rtu.link_ai(config.channel, config.side) + rs_rtu.link_ai(conf.channel, conf.side) elseif mode == rsio.IO_MODE.ANALOG_OUT then - rs_rtu.link_ao(config.channel, config.side) + rs_rtu.link_ao(conf.channel, conf.side) else -- should be unreachable code, we already validated channels log._error("init> fell through if chain attempting to identify IO mode", true) break end - table.insert(capabilities, config.channel) + table.insert(capabilities, conf.channel) - log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(config.channel) .. " (" .. config.side .. + log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. ") for reactor " .. rtu_redstone[reactor_idx].for_reactor) end end @@ -171,82 +195,15 @@ for i = 1, #rtu_devices do end ---------------------------------------- --- main loop +-- start system ---------------------------------------- --- advertisement/heartbeat clock (every 2 seconds) -local loop_clock = os.startTimer(2) +-- init threads +local main_thread = threads.thread__main(__shared_memory) +local comms_thread = threads.thread__comms(__shared_memory) --- event loop -while true do - local event, param1, param2, param3, param4, param5 = os.pullEventRaw() - - if event == "peripheral_detach" then - -- handle loss of a device - local device = ppm.handle_unmount(param1) - - for i = 1, #units do - -- find disconnected device - if units[i].device == device.dev then - -- we are going to let the PPM prevent crashes - -- return fault flags/codes to MODBUS queries - local unit = units[i] - println_ts("lost the " .. unit.type .. " on interface " .. unit.name) - end - end - elseif event == "peripheral" then - -- relink lost peripheral to correct unit entry - local type, device = ppm.mount(param1) - - for i = 1, #units do - local unit = units[i] - - -- find disconnected device to reconnect - if unit.name == param1 then - -- found, re-link - unit.device = device - - if unit.type == "boiler" then - unit.rtu = boiler_rtu.new(device) - elseif unit.type == "turbine" then - unit.rtu = turbine_rtu.new(device) - elseif unit.type == "imatrix" then - unit.rtu = imatrix_rtu.new(device) - end - - unit.modbus_io = modbus.new(unit.rtu) - - println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) - end - end - elseif event == "timer" and param1 == loop_clock then - -- start next clock timer - loop_clock = os.startTimer(2) - - -- period tick, if we are linked send heartbeat, if not send advertisement - if linked then - rtu_comms.send_heartbeat() - else - -- advertise units - rtu_comms.send_advertisement(units) - end - elseif event == "modem_message" then - -- got a packet - local link_ref = { linked = linked } - local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5) - - async_wait(function () rtu_comms.handle_packet(packet, units, link_ref) end) - - -- if linked, stop sending advertisements - linked = link_ref.linked - end - - -- check for termination request - if event == "terminate" or ppm.should_terminate() then - log._warning("terminate requested, exiting...") - break - end -end +-- run threads +parallel.waitForAll(main_thread.exec, comms_thread.exec) println_ts("exited") log._info("exited") diff --git a/rtu/threads.lua b/rtu/threads.lua new file mode 100644 index 0000000..bc96d3b --- /dev/null +++ b/rtu/threads.lua @@ -0,0 +1,147 @@ +-- #REQUIRES comms.lua +-- #REQUIRES log.lua +-- #REQUIRES ppm.lua +-- #REQUIRES util.lua + +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + +local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) +local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) + +-- main thread +function thread__main(smem) + -- execute thread + local exec = function () + -- advertisement/heartbeat clock + local loop_clock = os.startTimer(MAIN_CLOCK) + + -- load in from shared memory + local rtu_state = smem.rtu_state + local rtu_dev = smem.rtu_dev + local rtu_comms = smem.rtu_sys.rtu_comms + + -- event loop + while true do + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + + if event == "peripheral_detach" then + -- handle loss of a device + local device = ppm.handle_unmount(param1) + + for i = 1, #units do + -- find disconnected device + if units[i].device == device.dev then + -- we are going to let the PPM prevent crashes + -- return fault flags/codes to MODBUS queries + local unit = units[i] + println_ts("lost the " .. unit.type .. " on interface " .. unit.name) + end + end + elseif event == "peripheral" then + -- relink lost peripheral to correct unit entry + local type, device = ppm.mount(param1) + + for i = 1, #units do + local unit = units[i] + + -- find disconnected device to reconnect + if unit.name == param1 then + -- found, re-link + unit.device = device + + if unit.type == "boiler" then + unit.rtu = boiler_rtu.new(device) + elseif unit.type == "turbine" then + unit.rtu = turbine_rtu.new(device) + elseif unit.type == "imatrix" then + unit.rtu = imatrix_rtu.new(device) + end + + unit.modbus_io = modbus.new(unit.rtu) + + println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) + end + end + elseif event == "timer" and param1 == loop_clock then + -- start next clock timer + loop_clock = os.startTimer(MAIN_CLOCK) + + -- period tick, if we are linked send heartbeat, if not send advertisement + if rtu_state.linked then + rtu_comms.send_heartbeat() + else + -- advertise units + rtu_comms.send_advertisement(units) + end + elseif event == "modem_message" then + -- got a packet + local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5) + if packet ~= nil then + smem.q.mq_comms.push_packet(packet) + end + + rtu_comms.handle_packet(packet, units, link_ref) + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + rtu_state.shutdown = true + log._warning("terminate requested, main thread exiting") + break + end + end + end + + return { exec = exec } +end + +-- communications handler thread +function thread__comms(smem) + -- execute thread + local exec = function () + -- load in from shared memory + local rtu_state = smem.rtu_state + local rtu_comms = smem.rtu_sys.rtu_comms + local units = smem.rtu_sys.units + + local comms_queue = smem.q.mq_comms + + local last_update = util.time() + + -- thread loop + while true do + -- check for messages in the message queue + while comms_queue.ready() do + local msg = comms_queue.pop() + + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (rtu_state passed to allow setting link flag) + rtu_comms.handle_packet(msg.message, units, rtu_state) + end + + -- quick yield + if comms_queue.ready() then util.nop() end + end + + -- check for termination request + if rtu_state.shutdown then + log._warning("comms thread exiting") + break + end + + -- delay before next check + local sleep_for = COMMS_CLOCK - (util.time() - last_update) + if sleep_for > 0.05 then + sleep(sleep_for) + end + end + end +end From 14377e734853e5ab5a43186eea57a2ecd5373d1a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 15:01:10 -0400 Subject: [PATCH 041/168] don't run PLC comms thread if not networked --- reactor-plc/startup.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 025c274..23ca80a 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -129,7 +129,11 @@ local iss_thread = threads.thread__iss(__shared_memory) local comms_thread = threads.thread__comms(__shared_memory) -- run threads -parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread.exec) +if __shared_memory.networked then + parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread.exec) +else + parallel.waitForAll(main_thread.exec, iss_thread.exec) +end -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? println_ts("exited") From 67a93016c0f837fa12a4738c2c56a5e88d6689fe Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 15:52:34 -0400 Subject: [PATCH 042/168] threaded RTU/PLC bugfixes --- reactor-plc/startup.lua | 4 ++-- reactor-plc/threads.lua | 28 +++++++++++++++++++++------- rtu/startup.lua | 7 ++++--- rtu/threads.lua | 14 ++++++++++++-- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 23ca80a..df117f5 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.0" +local R_PLC_VERSION = "alpha-v0.4.1" local print = util.print local println = util.println @@ -58,7 +58,7 @@ local __shared_memory = { -- message queues q = { mq_iss = mqueue.new(), - mq_comms = mqeueu.new() + mq_comms = mqueue.new() } } diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index f4375c9..17ab22e 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -26,6 +26,8 @@ local MQ__COMM_CMD = { function thread__main(smem, init) -- execute thread local exec = function () + log._debug("main thread init, clock inactive") + -- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks) local LINK_TICKS = 4 @@ -168,7 +170,7 @@ function thread__main(smem, init) elseif event == "clock_start" then -- start loop clock loop_clock = os.startTimer(MAIN_CLOCK) - log._debug("main thread started") + log._debug("main thread clock started") end -- check for termination request @@ -188,6 +190,8 @@ end function thread__iss(smem) -- execute thread local exec = function () + log._debug("iss thread start") + -- load in from shared memory local networked = smem.networked local plc_state = smem.plc_state @@ -233,8 +237,8 @@ function thread__iss(smem) end -- check for messages in the message queue - while comms_queue.ready() do - local msg = comms_queue.pop() + while iss_queue.ready() do + local msg = iss_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then -- received a command @@ -265,7 +269,7 @@ function thread__iss(smem) -- received a packet end - -- quick yield + -- quick yield if we are looping right back if iss_queue.ready() then util.nop() end end @@ -296,8 +300,11 @@ function thread__iss(smem) -- delay before next check local sleep_for = ISS_CLOCK - (util.time() - last_update) - if sleep_for > 0.05 then + last_update = util.time() + if sleep_for > 0 then sleep(sleep_for) + else + sleep(0.05) end end end @@ -309,6 +316,8 @@ end function thread__comms(smem) -- execute thread local exec = function () + log._debug("comms thread start") + -- load in from shared memory local plc_state = smem.plc_state local plc_comms = smem.plc_sys.plc_comms @@ -338,7 +347,7 @@ function thread__comms(smem) plc_comms.handle_packet(msg.message, plc_state) end - -- quick yield + -- quick yield if we are looping right back if comms_queue.ready() then util.nop() end end @@ -350,9 +359,14 @@ function thread__comms(smem) -- delay before next check local sleep_for = COMMS_CLOCK - (util.time() - last_update) - if sleep_for > 0.05 then + last_update = util.time() + if sleep_for > 0 then sleep(sleep_for) + else + sleep(0.05) end end end + + return { exec = exec } end diff --git a/rtu/startup.lua b/rtu/startup.lua index 990819f..fed7dd0 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.0" +local RTU_VERSION = "alpha-v0.4.1" local print = util.print local println = util.println @@ -58,11 +58,12 @@ local __shared_memory = { -- message queues q = { - mq_comms = mqeueu.new() + mq_comms = mqueue.new() } } local smem_dev = __shared_memory.rtu_dev +local smem_sys = __shared_memory.rtu_sys -- get modem if smem_dev.modem == nil then @@ -71,7 +72,7 @@ if smem_dev.modem == nil then return end -local rtu_comms = rtu.rtu_comms(modem, config.LISTEN_PORT, config.SERVER_PORT) +smem_sys.rtu_comms = rtu.rtu_comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT) ---------------------------------------- -- interpret config and init units diff --git a/rtu/threads.lua b/rtu/threads.lua index bc96d3b..3f021a6 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -15,6 +15,8 @@ local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) function thread__main(smem) -- execute thread local exec = function () + log._debug("main thread start") + -- advertisement/heartbeat clock local loop_clock = os.startTimer(MAIN_CLOCK) @@ -22,6 +24,7 @@ function thread__main(smem) local rtu_state = smem.rtu_state local rtu_dev = smem.rtu_dev local rtu_comms = smem.rtu_sys.rtu_comms + local units = smem.rtu_sys.units -- event loop while true do @@ -102,6 +105,8 @@ end function thread__comms(smem) -- execute thread local exec = function () + log._debug("comms thread start") + -- load in from shared memory local rtu_state = smem.rtu_state local rtu_comms = smem.rtu_sys.rtu_comms @@ -127,7 +132,7 @@ function thread__comms(smem) rtu_comms.handle_packet(msg.message, units, rtu_state) end - -- quick yield + -- quick yield if we are looping right back if comms_queue.ready() then util.nop() end end @@ -139,9 +144,14 @@ function thread__comms(smem) -- delay before next check local sleep_for = COMMS_CLOCK - (util.time() - last_update) - if sleep_for > 0.05 then + last_update = util.time() + if sleep_for > 0 then sleep(sleep_for) + else + sleep(0.05) end end end + + return { exec = exec } end From 146e0bf5693e09dc7478047102025560bc56627e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 15:56:55 -0400 Subject: [PATCH 043/168] protected sleep call --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 10 ++++++---- rtu/startup.lua | 2 +- rtu/threads.lua | 6 ++++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index df117f5..bca0027 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.1" +local R_PLC_VERSION = "alpha-v0.4.2" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 17ab22e..ce17609 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -8,6 +8,8 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +local psleep = util.psleep + local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) local ISS_CLOCK = 0.5 -- (2Hz, 10 ticks) local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) @@ -302,9 +304,9 @@ function thread__iss(smem) local sleep_for = ISS_CLOCK - (util.time() - last_update) last_update = util.time() if sleep_for > 0 then - sleep(sleep_for) + psleep(sleep_for) else - sleep(0.05) + psleep(0.05) end end end @@ -361,9 +363,9 @@ function thread__comms(smem) local sleep_for = COMMS_CLOCK - (util.time() - last_update) last_update = util.time() if sleep_for > 0 then - sleep(sleep_for) + psleep(sleep_for) else - sleep(0.05) + psleep(0.05) end end end diff --git a/rtu/startup.lua b/rtu/startup.lua index fed7dd0..c04a902 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.1" +local RTU_VERSION = "alpha-v0.4.2" local print = util.print local println = util.println diff --git a/rtu/threads.lua b/rtu/threads.lua index 3f021a6..692697d 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -8,6 +8,8 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +local psleep = util.psleep + local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) @@ -146,9 +148,9 @@ function thread__comms(smem) local sleep_for = COMMS_CLOCK - (util.time() - last_update) last_update = util.time() if sleep_for > 0 then - sleep(sleep_for) + psleep(sleep_for) else - sleep(0.05) + psleep(0.05) end end end From d40937b46795285ac8385a1a2194ad71bb806497 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 16:06:30 -0400 Subject: [PATCH 044/168] this was supposed to be in that pr merge oops --- scada-common/util.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index 11b258e..69bc7f1 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -39,10 +39,15 @@ end -- PARALLELIZATION -- +-- protected sleep call so we still are in charge of catching termination +function psleep(t) + pcall(os.sleep, t) +end + -- no-op to provide a brief pause (and a yield) -- EVENT_CONSUMER: this function consumes events function nop() - sleep(0.05) + psleep(0.05) end -- WATCHDOG -- From 82726520b8b0ab1735364c2fcf4f538a95fca3a6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 16:24:28 -0400 Subject: [PATCH 045/168] that was a stack not a queue, nice --- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 2 +- scada-common/mqueue.lua | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index bca0027..659c8a6 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.2" +local R_PLC_VERSION = "alpha-v0.4.3" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index c04a902..be27048 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.2" +local RTU_VERSION = "alpha-v0.4.3" local print = util.print local println = util.println diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index 3881d02..f14951e 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -41,7 +41,7 @@ function new() local pop = function () if #queue > 0 then - return table.remove(queue) + return table.remove(queue, 1) else return nil end From 46a27a3f3a36365ee05d1d58c6d4f83f399274de Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 16:38:41 -0400 Subject: [PATCH 046/168] check shutdown flag in worker loops so they don't lock up the exit process --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 4 ++-- rtu/startup.lua | 2 +- rtu/threads.lua | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 659c8a6..ae38449 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.3" +local R_PLC_VERSION = "alpha-v0.4.4" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index ce17609..b774dbb 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -239,7 +239,7 @@ function thread__iss(smem) end -- check for messages in the message queue - while iss_queue.ready() do + while iss_queue.ready() and not plc_state.shutdown do local msg = iss_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then @@ -331,7 +331,7 @@ function thread__comms(smem) -- thread loop while true do -- check for messages in the message queue - while comms_queue.ready() do + while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then diff --git a/rtu/startup.lua b/rtu/startup.lua index be27048..8fea859 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.3" +local RTU_VERSION = "alpha-v0.4.4" local print = util.print local println = util.println diff --git a/rtu/threads.lua b/rtu/threads.lua index 692697d..c8abdd9 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -121,7 +121,7 @@ function thread__comms(smem) -- thread loop while true do -- check for messages in the message queue - while comms_queue.ready() do + while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then From fe3b8e6f88641e7c4b61eb9173c05dbef6743356 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 17:59:25 -0400 Subject: [PATCH 047/168] fixed up worker loop delay logic --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 43 +++++++++++++++-------------------------- rtu/startup.lua | 2 +- rtu/threads.lua | 19 +++++++++--------- 4 files changed, 27 insertions(+), 39 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index ae38449..2ceb289 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.4" +local R_PLC_VERSION = "alpha-v0.4.5" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index b774dbb..ed1d93b 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -10,9 +10,9 @@ local println_ts = util.println_ts local psleep = util.psleep -local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) -local ISS_CLOCK = 0.5 -- (2Hz, 10 ticks) -local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) +local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) +local ISS_SLEEP = 500 -- (500ms, 10 ticks) +local COMMS_SLEEP = 150 -- (150ms, 3 ticks) local MQ__ISS_CMD = { SCRAM = 1, @@ -44,9 +44,6 @@ function thread__main(smem, init) local plc_comms = smem.plc_sys.plc_comms local conn_watchdog = smem.plc_sys.conn_watchdog - -- debug - local last_update = util.time() - -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() @@ -71,11 +68,6 @@ function thread__main(smem, init) end end end - - -- debug - print(util.time() - last_update) - println("ms") - last_update = util.time() end elseif event == "modem_message" and networked and not plc_state.no_modem then -- got a packet @@ -271,8 +263,8 @@ function thread__iss(smem) -- received a packet end - -- quick yield if we are looping right back - if iss_queue.ready() then util.nop() end + -- quick yield + util.nop() end -- check for termination request @@ -300,13 +292,11 @@ function thread__iss(smem) -- println("ms") -- last_update = util.time() - -- delay before next check - local sleep_for = ISS_CLOCK - (util.time() - last_update) + -- delay before next check, only if >50ms since we did already yield + local sleep_for = ISS_SLEEP - (util.time() - last_update) last_update = util.time() - if sleep_for > 0 then - psleep(sleep_for) - else - psleep(0.05) + if sleep_for >= 50 then + psleep(sleep_for / 1000.0) end end end @@ -349,8 +339,8 @@ function thread__comms(smem) plc_comms.handle_packet(msg.message, plc_state) end - -- quick yield if we are looping right back - if comms_queue.ready() then util.nop() end + -- quick yield + util.nop() end -- check for termination request @@ -359,13 +349,12 @@ function thread__comms(smem) break end - -- delay before next check - local sleep_for = COMMS_CLOCK - (util.time() - last_update) + -- delay before next check, only if >50ms since we did already yield + local sleep_for = COMMS_SLEEP - (util.time() - last_update) last_update = util.time() - if sleep_for > 0 then - psleep(sleep_for) - else - psleep(0.05) + if sleep_for >= 50 then + println("sleep for " .. (sleep_for / 1000.0)) + psleep(sleep_for / 1000.0) end end end diff --git a/rtu/startup.lua b/rtu/startup.lua index 8fea859..f3e1a8d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.4" +local RTU_VERSION = "alpha-v0.4.5" local print = util.print local println = util.println diff --git a/rtu/threads.lua b/rtu/threads.lua index c8abdd9..c2c1cfe 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -10,8 +10,8 @@ local println_ts = util.println_ts local psleep = util.psleep -local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) -local COMMS_CLOCK = 0.25 -- (4Hz, 5 ticks) +local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) +local COMMS_SLEEP = 150 -- (150ms, 3 ticks) -- main thread function thread__main(smem) @@ -134,8 +134,8 @@ function thread__comms(smem) rtu_comms.handle_packet(msg.message, units, rtu_state) end - -- quick yield if we are looping right back - if comms_queue.ready() then util.nop() end + -- quick yield + util.nop() end -- check for termination request @@ -144,13 +144,12 @@ function thread__comms(smem) break end - -- delay before next check - local sleep_for = COMMS_CLOCK - (util.time() - last_update) + -- delay before next check, only if >50ms since we did already yield + local sleep_for = COMMS_SLEEP - (util.time() - last_update) last_update = util.time() - if sleep_for > 0 then - psleep(sleep_for) - else - psleep(0.05) + if sleep_for >= 50 then + println("sleep for " .. (sleep_for / 1000.0)) + psleep(sleep_for / 1000.0) end end end From f067da31b4c821c17a78635301877d53284d5056 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 18:43:07 -0400 Subject: [PATCH 048/168] #38 handle out of space when logging --- scada-common/log.lua | 35 ++++++++++++++++++++++++++++++++--- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index fbe2d66..cf493eb 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -6,13 +6,42 @@ -- underscores are used since some of these names are used elsewhere (e.g. 'debug' is a lua table) local LOG_DEBUG = true +local LOG_PATH = "/log.txt" -local file_handle = fs.open("/log.txt", "a") +local file_handle = fs.open(LOG_PATH, "a") local _log = function (msg) local stamped = os.date("[%c] ") .. msg - file_handle.writeLine(stamped) - file_handle.flush() + + -- attempt to write log + local status, result = pcall(function () + file_handle.writeLine(stamped) + file_handle.flush() + end) + + -- if we don't have much space, we need to create a new log file + local delete_log = fs.getFreeSpace(LOG_PATH) < 100 + + if not status then + if result == "Out of space" then + delete_log = true + elseif result ~= nil then + print("unknown error writing to logfile: " .. result) + end + end + + if delete_log then + -- delete the old log file and open a new one + file_handle.close() + fs.delete(LOG_PATH) + file_handle = fs.open(LOG_PATH, "a") + + -- leave a message + local notif = os.date("[%c] ") .. "recycled log file" + file_handle.writeLine(notif) + file_handle.writeLine(stamped) + file_handle.flush() + end end function _debug(msg, trace) From 7f0f4234504fb94f2b6cbccf45b41ee7afc05d2c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 18:49:54 -0400 Subject: [PATCH 049/168] PLC bugfixes/optimizations, removed some debug prints --- reactor-plc/plc.lua | 38 +++++++++++++++++++++++++++----------- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 6 ------ rtu/threads.lua | 1 - 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index da71998..361c174 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -230,6 +230,24 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- variable reactor status information, excluding heating rate local _reactor_status = function () + local coolant = self.reactor.getCoolant() + local coolant_name = "" + local coolant_amnt = 0 + + local hcoolant = self.reactor.getHeatedCoolant() + local hcoolant_name = "" + local hcoolant_amnt = 0 + + if coolant ~= nil then + coolant_name = coolant.name + coolant_amnt = coolant.amount + end + + if hcoolant ~= nil then + hcoolant_name = hcoolant.name + hcoolant_amnt = hcoolant.amount + end + return { self.reactor.getStatus(), self.reactor.getBurnRate(), @@ -240,18 +258,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.reactor.getEnvironmentalLoss(), self.reactor.getFuel(), - self.reactor.getFuelNeeded(), self.reactor.getFuelFilledPercentage(), self.reactor.getWaste(), - self.reactor.getWasteNeeded(), self.reactor.getWasteFilledPercentage(), - self.reactor.getCoolant()['name'], - self.reactor.getCoolant()['amount'], - self.reactor.getCoolantNeeded(), + coolant_name, + coolant_amnt, self.reactor.getCoolantFilledPercentage(), - self.reactor.getHeatedCoolant()['name'], - self.reactor.getHeatedCoolant()['amount'], - self.reactor.getHeatedCoolantNeeded(), + hcoolant_name, + hcoolant_amnt, self.reactor.getHeatedCoolantFilledPercentage() }, self.reactor.__p_is_faulted() end @@ -273,7 +287,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) changed = true end - if changed then + if changed and not faulted then self.status_cache = status end @@ -419,10 +433,12 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if trip_time < 0 then log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") - elseif trip_time > 1000 then - log._warning("PLC KEEP_ALIVE trip time > 1s (" .. trip_time .. ")") + elseif trip_time > 1200 then + log._warning("PLC KEEP_ALIVE trip time > 1.2s (" .. trip_time .. ")") end + -- log._debug("RPLC RTT = ".. trip_time .. "ms") + _send_keep_alive_ack(timestamp) elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 2ceb289..4709a9c 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.5" +local R_PLC_VERSION = "alpha-v0.4.6" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index ed1d93b..5cc80bc 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -287,11 +287,6 @@ function thread__iss(smem) break end - -- debug - -- print(util.time() - last_update) - -- println("ms") - -- last_update = util.time() - -- delay before next check, only if >50ms since we did already yield local sleep_for = ISS_SLEEP - (util.time() - last_update) last_update = util.time() @@ -353,7 +348,6 @@ function thread__comms(smem) local sleep_for = COMMS_SLEEP - (util.time() - last_update) last_update = util.time() if sleep_for >= 50 then - println("sleep for " .. (sleep_for / 1000.0)) psleep(sleep_for / 1000.0) end end diff --git a/rtu/threads.lua b/rtu/threads.lua index c2c1cfe..d736ee1 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -148,7 +148,6 @@ function thread__comms(smem) local sleep_for = COMMS_SLEEP - (util.time() - last_update) last_update = util.time() if sleep_for >= 50 then - println("sleep for " .. (sleep_for / 1000.0)) psleep(sleep_for / 1000.0) end end From f14d7150707b18cb65a24eedcd2471c2e938219c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 18:52:06 -0400 Subject: [PATCH 050/168] #7 PLC session comms link, accept statuses, functional keep-alives --- supervisor/session/plc.lua | 49 ++++++++++++++++--------------- supervisor/session/svsessions.lua | 6 ++-- supervisor/startup.lua | 9 +++--- 3 files changed, 34 insertions(+), 30 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 3a51666..48c4a53 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -13,7 +13,7 @@ PLC_S_COMMANDS = { } local PERIODICS = { - KEEP_ALIVE = 1.0 + KEEP_ALIVE = 2.0 } -- PLC supervisor session @@ -117,19 +117,15 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.mek_status.env_loss = mek_data[7] self.sDB.mek_status.fuel = mek_data[8] - self.sDB.mek_status.fuel_need = mek_data[9] - self.sDB.mek_status.fuel_fill = mek_data[10] - self.sDB.mek_status.waste = mek_data[11] - self.sDB.mek_status.waste_need = mek_data[12] - self.sDB.mek_status.waste_fill = mek_data[13] - self.sDB.mek_status.cool_type = mek_data[14] - self.sDB.mek_status.cool_amnt = mek_data[15] - self.sDB.mek_status.cool_need = mek_data[16] - self.sDB.mek_status.cool_fill = mek_data[17] - self.sDB.mek_status.hcool_type = mek_data[18] - self.sDB.mek_status.hcool_amnt = mek_data[19] - self.sDB.mek_status.hcool_need = mek_data[20] - self.sDB.mek_status.hcool_fill = mek_data[21] + self.sDB.mek_status.fuel_fill = mek_data[9] + self.sDB.mek_status.waste = mek_data[10] + self.sDB.mek_status.waste_fill = mek_data[11] + self.sDB.mek_status.cool_type = mek_data[12] + self.sDB.mek_status.cool_amnt = mek_data[13] + self.sDB.mek_status.cool_fill = mek_data[14] + self.sDB.mek_status.hcool_type = mek_data[15] + self.sDB.mek_status.hcool_amnt = mek_data[16] + self.sDB.mek_status.hcool_fill = mek_data[17] end local _copy_struct = function (mek_data) @@ -152,12 +148,9 @@ function new_session(id, for_reactor, in_queue, out_queue) end end - local _handle_packet = function (message) + local _handle_packet = function (rplc_pkt) local checks_ok = true - -- handle an incoming packet from the PLC - rplc_pkt = message.get() - -- check sequence number if self.r_seq_num == nil then self.r_seq_num = rplc_pkt.scada_frame.seq_num() @@ -189,12 +182,13 @@ function new_session(id, for_reactor, in_queue, out_queue) self.last_rtt = srv_now - srv_start if self.last_rtt < 0 then - log._warning(log_header .. "PLC KEEP_ALIVE round trip time less than 0 (" .. trip_time .. ")") - elseif trip_time > 1000 then - log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 1s (" .. trip_time .. ")") + log._warning(log_header .. "PLC KEEP_ALIVE round trip time less than 0 (" .. self.last_rtt .. ")") + elseif self.last_rtt > 1200 then + log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 1.2s (" .. self.last_rtt .. ")") end - log._debug(log_header .. "RPLC RTT = ".. trip_time .. "ms") + -- log._debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms") + -- log._debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms") else log._debug(log_header .. "RPLC keep alive packet length mismatch") end @@ -330,16 +324,25 @@ function new_session(id, for_reactor, in_queue, out_queue) -- handle queue -- ------------------ - if self.in_q.ready() then + local handle_start = util.time() + + while self.in_q.ready() do -- get a new message to process local message = self.in_q.pop() if message.qtype == mqueue.TYPE.PACKET then + -- handle a packet _handle_packet(message.message) elseif message.qtype == mqueue.TYPE.COMMAND then -- handle instruction end + + -- max 100ms spent processing queue + if util.time() - handle_start > 100 then + log._warning(log_header .. "exceeded 100ms queue process limit") + break + end end ---------------------- diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 9c02ce0..76fdea3 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -34,19 +34,19 @@ end function find_session(stype, remote_port) if stype == SESSION_TYPE.RTU_SESSION then for i = 1, #self.rtu_sessions do - if self.rtu_sessions[i].r_host == remote_port then + if self.rtu_sessions[i].r_port == remote_port then return self.rtu_sessions[i] end end elseif stype == SESSION_TYPE.PLC_SESSION then for i = 1, #self.plc_sessions do - if self.plc_sessions[i].r_host == remote_port then + if self.plc_sessions[i].r_port == remote_port then return self.plc_sessions[i] end end elseif stype == SESSION_TYPE.COORD_SESSION then for i = 1, #self.coord_sessions do - if self.coord_sessions[i].r_host == remote_port then + if self.coord_sessions[i].r_port == remote_port then return self.coord_sessions[i] end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 138cfd8..71f0eae 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.6" +local SUPERVISOR_VERSION = "alpha-v0.1.7" local print = util.print local println = util.println @@ -43,8 +43,9 @@ end -- start comms, open all channels local superv_comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) --- base loop clock (4Hz, 5 ticks) -local loop_clock = os.startTimer(0.25) +-- base loop clock (6.67Hz, 3 ticks) +local MAIN_CLOCK = 0.15 +local loop_clock = os.startTimer(MAIN_CLOCK) -- event loop while true do @@ -87,7 +88,7 @@ while true do -- free any closed sessions svsessions.free_all_closed() - loop_clock = os.startTimer(0.25) + loop_clock = os.startTimer(MAIN_CLOCK) elseif event == "timer" then -- another timer event, check watchdogs svsessions.check_all_watchdogs(param1) From aff166e27d22ab0d80e773fe4f4b46db1b11e813 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 27 Apr 2022 19:06:01 -0400 Subject: [PATCH 051/168] added util adaptive_delay to replace repeated code --- reactor-plc/threads.lua | 16 ++++------------ rtu/threads.lua | 8 ++------ scada-common/util.lua | 10 ++++++++++ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 5cc80bc..a9b95d7 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -287,12 +287,8 @@ function thread__iss(smem) break end - -- delay before next check, only if >50ms since we did already yield - local sleep_for = ISS_SLEEP - (util.time() - last_update) - last_update = util.time() - if sleep_for >= 50 then - psleep(sleep_for / 1000.0) - end + -- delay before next check + last_update = util.adaptive_delay(ISS_SLEEP, last_update) end end @@ -344,12 +340,8 @@ function thread__comms(smem) break end - -- delay before next check, only if >50ms since we did already yield - local sleep_for = COMMS_SLEEP - (util.time() - last_update) - last_update = util.time() - if sleep_for >= 50 then - psleep(sleep_for / 1000.0) - end + -- delay before next check + last_update = util.adaptive_delay(COMMS_SLEEP, last_update) end end diff --git a/rtu/threads.lua b/rtu/threads.lua index d736ee1..fece382 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -144,12 +144,8 @@ function thread__comms(smem) break end - -- delay before next check, only if >50ms since we did already yield - local sleep_for = COMMS_SLEEP - (util.time() - last_update) - last_update = util.time() - if sleep_for >= 50 then - psleep(sleep_for / 1000.0) - end + -- delay before next check + last_update = util.adaptive_delay(COMMS_SLEEP, last_update) end end diff --git a/scada-common/util.lua b/scada-common/util.lua index 69bc7f1..36761fd 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -50,6 +50,16 @@ function nop() psleep(0.05) end +-- attempt to maintain a minimum loop timing (duration of execution) +function adaptive_delay(target_timing, last_update) + local sleep_for = target_timing - (time() - last_update) + -- only if >50ms since worker loops already yield 0.05s + if sleep_for >= 50 then + psleep(sleep_for / 1000.0) + end + return time() +end + -- WATCHDOG -- -- ComputerCraft OS Timer based Watchdog From 67ec8fbd910bdacade32ffd7ceb625b4e1d03f09 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 28 Apr 2022 22:36:45 -0400 Subject: [PATCH 052/168] rx and tx threads for PLC comms to maintain quick comms and #36 only feed watchdog on valid sequence numbers --- reactor-plc/plc.lua | 11 ++++--- reactor-plc/startup.lua | 15 ++++++--- reactor-plc/threads.lua | 67 +++++++++++++++++++++++++++++++++-------- 3 files changed, 71 insertions(+), 22 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 361c174..127820f 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -411,7 +411,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end -- handle an RPLC packet - local handle_packet = function (packet, plc_state) + local handle_packet = function (packet, plc_state, conn_watchdog) if packet ~= nil then -- check sequence number if self.r_seq_num == nil then @@ -423,6 +423,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.r_seq_num = packet.scada_frame.seq_num() end + -- feed the watchdog first so it doesn't uhh...eat our packets + conn_watchdog.feed() + -- handle packet if packet.scada_frame.protocol() == PROTOCOLS.RPLC then if self.linked then @@ -431,10 +434,8 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local timestamp = packet.data[1] local trip_time = util.time() - timestamp - if trip_time < 0 then - log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") - elseif trip_time > 1200 then - log._warning("PLC KEEP_ALIVE trip time > 1.2s (" .. trip_time .. ")") + if trip_time > 500 then + log._warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") end -- log._debug("RPLC RTT = ".. trip_time .. "ms") diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 4709a9c..3bf458f 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.6" +local R_PLC_VERSION = "alpha-v0.4.7" local print = util.print local println = util.println @@ -58,7 +58,8 @@ local __shared_memory = { -- message queues q = { mq_iss = mqueue.new(), - mq_comms = mqueue.new() + mq_comms_tx = mqueue.new(), + mq_comms_rx = mqueue.new() } } @@ -126,12 +127,16 @@ init() -- init threads local main_thread = threads.thread__main(__shared_memory, init) local iss_thread = threads.thread__iss(__shared_memory) -local comms_thread = threads.thread__comms(__shared_memory) --- run threads if __shared_memory.networked then - parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread.exec) + -- init comms threads + local comms_thread_tx = threads.thread__comms_tx(__shared_memory) + local comms_thread_rx = threads.thread__comms_rx(__shared_memory) + + -- run threads + parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec) else + -- run threads, excluding comms parallel.waitForAll(main_thread.exec, iss_thread.exec) end diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index a9b95d7..c946ed5 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -58,7 +58,7 @@ function thread__main(smem, init) -- send updated data if not plc_state.no_modem then if plc_comms.is_linked() then - smem.q.mq_comms.push_command(MQ__COMM_CMD.SEND_STATUS) + smem.q.mq_comms_tx.push_command(MQ__COMM_CMD.SEND_STATUS) else if ticks_to_update == 0 then plc_comms.send_link_req() @@ -71,13 +71,10 @@ function thread__main(smem, init) end elseif event == "modem_message" and networked and not plc_state.no_modem then -- got a packet - -- feed the watchdog first so it doesn't uhh...eat our packets - conn_watchdog.feed() - - -- handle the packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) if packet ~= nil then - smem.q.mq_comms.push_packet(packet) + -- pass the packet onto the comms message queue + smem.q.mq_comms_rx.push_packet(packet) end elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor @@ -296,16 +293,16 @@ function thread__iss(smem) end -- communications handler thread -function thread__comms(smem) +function thread__comms_tx(smem) -- execute thread local exec = function () - log._debug("comms thread start") + log._debug("comms tx thread start") -- load in from shared memory local plc_state = smem.plc_state local plc_comms = smem.plc_sys.plc_comms - local comms_queue = smem.q.mq_comms + local comms_queue = smem.q.mq_comms_tx local last_update = util.time() @@ -326,8 +323,6 @@ function thread__comms(smem) -- received data elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet - -- handle the packet (plc_state passed to allow clearing SCRAM flag) - plc_comms.handle_packet(msg.message, plc_state) end -- quick yield @@ -336,7 +331,55 @@ function thread__comms(smem) -- check for termination request if plc_state.shutdown then - log._warning("comms thread exiting") + log._warning("comms tx thread exiting") + break + end + + -- delay before next check + last_update = util.adaptive_delay(COMMS_SLEEP, last_update) + end + end + + return { exec = exec } +end + +function thread__comms_rx(smem) + -- execute thread + local exec = function () + log._debug("comms rx thread start") + + -- load in from shared memory + local plc_state = smem.plc_state + local plc_comms = smem.plc_sys.plc_comms + local conn_watchdog = smem.plc_sys.conn_watchdog + + local comms_queue = smem.q.mq_comms_rx + + local last_update = util.time() + + -- thread loop + while true do + -- check for messages in the message queue + while comms_queue.ready() and not plc_state.shutdown do + local msg = comms_queue.pop() + + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (plc_state passed to allow clearing SCRAM flag) + plc_comms.handle_packet(msg.message, plc_state, conn_watchdog) + end + + -- quick yield + util.nop() + end + + -- check for termination request + if plc_state.shutdown then + log._warning("comms rx thread exiting") break end From d688f9a1c62b6464c9ca3e3eeb868d154175d1b6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 28 Apr 2022 22:41:08 -0400 Subject: [PATCH 053/168] supervisor code cleanup, adjusted prints --- supervisor/session/plc.lua | 6 ++---- supervisor/session/svsessions.lua | 1 - supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 7 +++++++ 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 48c4a53..8c08b86 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -181,10 +181,8 @@ function new_session(id, for_reactor, in_queue, out_queue) local srv_now = util.time() self.last_rtt = srv_now - srv_start - if self.last_rtt < 0 then - log._warning(log_header .. "PLC KEEP_ALIVE round trip time less than 0 (" .. self.last_rtt .. ")") - elseif self.last_rtt > 1200 then - log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 1.2s (" .. self.last_rtt .. ")") + if self.last_rtt > 500 then + log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")") end -- log._debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms") diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 76fdea3..72f12d4 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -70,7 +70,6 @@ function get_reactor_session(reactor) end function establish_plc_session(local_port, remote_port, for_reactor) - util.println(remote_port) if get_reactor_session(for_reactor) == nil then local plc_s = { open = true, diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 71f0eae..1e5396d 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.7" +local SUPERVISOR_VERSION = "alpha-v0.1.8" local print = util.print local println = util.println diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 571ab49..b8addc2 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -1,6 +1,7 @@ -- #REQUIRES comms.lua -- #REQUIRES modbus.lua -- #REQUIRES mqueue.lua +-- #REQUIRES util.lua -- #REQUIRES svsessions.lua local PROTOCOLS = comms.PROTOCOLS @@ -11,6 +12,11 @@ local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES local SESSION_TYPE = svsessions.SESSION_TYPE +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + -- supervisory controller communications function superv_comms(num_reactors, modem, dev_listen, coord_listen) local self = { @@ -137,6 +143,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully + println("connected to reactor " .. packet.data[1] .. " PLC (port " .. r_port .. ")") log._debug("PLC_LNK: allowed for device at " .. r_port) _send_plc_linking(r_port, { RPLC_LINKING.ALLOW }) end From 4d5cbcf475b1df4fb53258a7d7e6ac107098fb6c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 09:07:29 -0400 Subject: [PATCH 054/168] PLC comms packet length checks --- reactor-plc/plc.lua | 142 ++++++++++++++++++++++------------------ reactor-plc/startup.lua | 2 +- 2 files changed, 80 insertions(+), 64 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 127820f..f76bfb6 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -431,40 +431,48 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if self.linked then if packet.type == RPLC_TYPES.KEEP_ALIVE then -- keep alive request received, echo back - local timestamp = packet.data[1] - local trip_time = util.time() - timestamp + if packet.length == 1 then + local timestamp = packet.data[1] + local trip_time = util.time() - timestamp - if trip_time > 500 then - log._warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") + if trip_time > 500 then + log._warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") + end + + -- log._debug("RPLC RTT = ".. trip_time .. "ms") + + _send_keep_alive_ack(timestamp) + else + log._debug(log_header .. "RPLC keep alive packet length mismatch") end - - -- log._debug("RPLC RTT = ".. trip_time .. "ms") - - _send_keep_alive_ack(timestamp) elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation - log._debug("received unsolicited link request response") + if packet.length == 1 then + log._debug("received unsolicited link request response") - local link_ack = packet.data[1] - - if link_ack == RPLC_LINKING.ALLOW then - _send_struct() - send_status(plc_state.degraded) - log._debug("re-sent initial status data") - elseif link_ack == RPLC_LINKING.DENY then - -- @todo: make sure this doesn't become a MITM security risk - println_ts("received unsolicited link denial, unlinking") - log._debug("unsolicited RPLC link request denied") - elseif link_ack == RPLC_LINKING.COLLISION then - -- @todo: make sure this doesn't become a MITM security risk - println_ts("received unsolicited link collision, unlinking") - log._warning("unsolicited RPLC link request collision") + local link_ack = packet.data[1] + + if link_ack == RPLC_LINKING.ALLOW then + _send_struct() + send_status(plc_state.degraded) + log._debug("re-sent initial status data") + elseif link_ack == RPLC_LINKING.DENY then + -- @todo: make sure this doesn't become a MITM security risk + println_ts("received unsolicited link denial, unlinking") + log._debug("unsolicited RPLC link request denied") + elseif link_ack == RPLC_LINKING.COLLISION then + -- @todo: make sure this doesn't become a MITM security risk + println_ts("received unsolicited link collision, unlinking") + log._warning("unsolicited RPLC link request collision") + else + println_ts("invalid unsolicited link response") + log._error("unsolicited unknown RPLC link request response") + end + + self.linked = link_ack == RPLC_LINKING.ALLOW else - println_ts("invalid unsolicited link response") - log._error("unsolicited unknown RPLC link request response") + log._debug(log_header .. "RPLC link req packet length mismatch") end - - self.linked = link_ack == RPLC_LINKING.ALLOW elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure _send_struct() @@ -482,25 +490,29 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_ack(packet.type, self.reactor.__p_is_ok()) elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate - local success = false - local burn_rate = packet.data[1] - local max_burn_rate = self.max_burn_rate + if packet.length == 1 then + local success = false + local burn_rate = packet.data[1] + local max_burn_rate = self.max_burn_rate - -- if no known max burn rate, check again - if max_burn_rate == nil then - max_burn_rate = self.reactor.getMaxBurnRate() - self.max_burn_rate = max_burn_rate - end - - -- if we know our max burn rate, update current burn rate if in range - if max_burn_rate ~= ppm.ACCESS_FAULT then - if burn_rate > 0 and burn_rate <= max_burn_rate then - self.reactor.setBurnRate(burn_rate) - success = self.reactor.__p_is_ok() + -- if no known max burn rate, check again + if max_burn_rate == nil then + max_burn_rate = self.reactor.getMaxBurnRate() + self.max_burn_rate = max_burn_rate end - end - _send_ack(packet.type, success) + -- if we know our max burn rate, update current burn rate if in range + if max_burn_rate ~= ppm.ACCESS_FAULT then + if burn_rate > 0 and burn_rate <= max_burn_rate then + self.reactor.setBurnRate(burn_rate) + success = self.reactor.__p_is_ok() + end + end + + _send_ack(packet.type, success) + else + log._debug(log_header .. "RPLC set burn rate packet length mismatch") + end elseif packet.type == RPLC_TYPES.ISS_CLEAR then -- clear the ISS status iss.reset() @@ -510,31 +522,35 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation - local link_ack = packet.data[1] - - if link_ack == RPLC_LINKING.ALLOW then - println_ts("linked!") - log._debug("RPLC link request approved") + if packet.length == 1 then + local link_ack = packet.data[1] - -- reset remote sequence number - self.r_seq_num = nil + if link_ack == RPLC_LINKING.ALLOW then + println_ts("linked!") + log._debug("RPLC link request approved") - _send_struct() - send_status(plc_state.degraded) + -- reset remote sequence number + self.r_seq_num = nil - log._debug("sent initial status data") - elseif link_ack == RPLC_LINKING.DENY then - println_ts("link request denied, retrying...") - log._debug("RPLC link request denied") - elseif link_ack == RPLC_LINKING.COLLISION then - println_ts("reactor PLC ID collision (check config), retrying...") - log._warning("RPLC link request collision") + _send_struct() + send_status(plc_state.degraded) + + log._debug("sent initial status data") + elseif link_ack == RPLC_LINKING.DENY then + println_ts("link request denied, retrying...") + log._debug("RPLC link request denied") + elseif link_ack == RPLC_LINKING.COLLISION then + println_ts("reactor PLC ID collision (check config), retrying...") + log._warning("RPLC link request collision") + else + println_ts("invalid link response, bad channel? retrying...") + log._error("unknown RPLC link request response") + end + + self.linked = link_ack == RPLC_LINKING.ALLOW else - println_ts("invalid link response, bad channel? retrying...") - log._error("unknown RPLC link request response") + log._debug(log_header .. "RPLC link req packet length mismatch") end - - self.linked = link_ack == RPLC_LINKING.ALLOW else log._debug("discarding non-link packet before linked") end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 3bf458f..7c1de69 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.7" +local R_PLC_VERSION = "alpha-v0.4.8" local print = util.print local println = util.println From 07e9101ac7b6456e88fb7c7385d5027174a59cb8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 09:25:08 -0400 Subject: [PATCH 055/168] PLC modem disconnect bugfix --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 7c1de69..811f4f2 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.8" +local R_PLC_VERSION = "alpha-v0.4.9" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index c946ed5..eb3c026 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -92,7 +92,7 @@ function thread__main(smem, init) -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? elseif networked and device.type == "modem" then -- we only care if this is our wireless modem - if device.dev == modem then + if device.dev == plc_dev.modem then println_ts("wireless modem disconnected!") log._error("comms modem disconnected!") plc_state.no_modem = true From ef1fdc7f393a6393873de5988c89411c5ce6e562 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 09:27:05 -0400 Subject: [PATCH 056/168] #34 RTU modem disconnect/reconnect handling, bugfix in comms thread --- rtu/rtu.lua | 11 +++++++ rtu/startup.lua | 2 +- rtu/threads.lua | 76 +++++++++++++++++++++++++++++++------------------ 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 54bf780..2d7306d 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -153,6 +153,16 @@ function rtu_comms(modem, local_port, server_port) -- PUBLIC FUNCTIONS -- + -- reconnect a newly connected modem + local reconnect_modem = function (modem) + self.modem = modem + + -- open modem + if not self.modem.isOpen(self.l_port) then + self.modem.open(self.l_port) + end + end + -- parse a MODBUS/SCADA packet local parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil @@ -270,6 +280,7 @@ function rtu_comms(modem, local_port, server_port) end return { + reconnect_modem = reconnect_modem, parse_packet = parse_packet, handle_packet = handle_packet, send_advertisement = send_advertisement, diff --git a/rtu/startup.lua b/rtu/startup.lua index f3e1a8d..9219bff 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.5" +local RTU_VERSION = "alpha-v0.4.6" local print = util.print local println = util.println diff --git a/rtu/threads.lua b/rtu/threads.lua index fece382..049ca89 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -36,38 +36,62 @@ function thread__main(smem) -- handle loss of a device local device = ppm.handle_unmount(param1) - for i = 1, #units do - -- find disconnected device - if units[i].device == device.dev then - -- we are going to let the PPM prevent crashes - -- return fault flags/codes to MODBUS queries - local unit = units[i] - println_ts("lost the " .. unit.type .. " on interface " .. unit.name) + if device.type == "modem" then + -- we only care if this is our wireless modem + if device.dev == rtu_dev.modem then + println_ts("wireless modem disconnected!") + log._warning("comms modem disconnected!") + else + log._warning("non-comms modem disconnected") + end + else + for i = 1, #units do + -- find disconnected device + if units[i].device == device.dev then + -- we are going to let the PPM prevent crashes + -- return fault flags/codes to MODBUS queries + local unit = units[i] + println_ts("lost the " .. unit.type .. " on interface " .. unit.name) + end end end elseif event == "peripheral" then - -- relink lost peripheral to correct unit entry + -- peripheral connect local type, device = ppm.mount(param1) - for i = 1, #units do - local unit = units[i] + if type == "modem" then + if device.isWireless() then + -- reconnected modem + rtu_dev.modem = device + rtu_comms.reconnect_modem(rtu_dev.modem) - -- find disconnected device to reconnect - if unit.name == param1 then - -- found, re-link - unit.device = device + println_ts("wireless modem reconnected.") + log._info("comms modem reconnected.") + else + log._info("wired modem reconnected.") + end + else + -- relink lost peripheral to correct unit entry + for i = 1, #units do + local unit = units[i] - if unit.type == "boiler" then - unit.rtu = boiler_rtu.new(device) - elseif unit.type == "turbine" then - unit.rtu = turbine_rtu.new(device) - elseif unit.type == "imatrix" then - unit.rtu = imatrix_rtu.new(device) + -- find disconnected device to reconnect + if unit.name == param1 then + -- found, re-link + unit.device = device + + if unit.type == "boiler" then + unit.rtu = boiler_rtu.new(device) + elseif unit.type == "turbine" then + unit.rtu = turbine_rtu.new(device) + elseif unit.type == "imatrix" then + unit.rtu = imatrix_rtu.new(device) + end + + unit.modbus_io = modbus.new(unit.rtu) + + println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end - - unit.modbus_io = modbus.new(unit.rtu) - - println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end end elseif event == "timer" and param1 == loop_clock then @@ -87,8 +111,6 @@ function thread__main(smem) if packet ~= nil then smem.q.mq_comms.push_packet(packet) end - - rtu_comms.handle_packet(packet, units, link_ref) end -- check for termination request @@ -121,7 +143,7 @@ function thread__comms(smem) -- thread loop while true do -- check for messages in the message queue - while comms_queue.ready() and not plc_state.shutdown do + while comms_queue.ready() and not rtu_state.shutdown do local msg = comms_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then From e833176c65359a74d301450927802339d99393af Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 10:19:05 -0400 Subject: [PATCH 057/168] #40 RTU sequence number verification --- reactor-plc/plc.lua | 8 ++++-- reactor-plc/startup.lua | 2 +- rtu/rtu.lua | 27 ++++++++++++++++-- rtu/startup.lua | 7 ++++- rtu/threads.lua | 63 +++++++++++++++++++++++------------------ 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index f76bfb6..c1ffbf0 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -457,11 +457,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) send_status(plc_state.degraded) log._debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then - -- @todo: make sure this doesn't become a MITM security risk println_ts("received unsolicited link denial, unlinking") log._debug("unsolicited RPLC link request denied") elseif link_ack == RPLC_LINKING.COLLISION then - -- @todo: make sure this doesn't become a MITM security risk println_ts("received unsolicited link collision, unlinking") log._warning("unsolicited RPLC link request collision") else @@ -562,7 +560,11 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local is_scrammed = function () return self.scrammed end local is_linked = function () return self.linked end - local unlink = function () self.linked = false end + + local unlink = function () + self.linked = false + self.r_seq_num = nil + end return { reconnect_modem = reconnect_modem, diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 811f4f2..37ac6de 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.9" +local R_PLC_VERSION = "alpha-v0.4.10" local print = util.print local println = util.println diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 2d7306d..27f18c6 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -120,6 +120,7 @@ end function rtu_comms(modem, local_port, server_port) local self = { seq_num = 0, + r_seq_num = nil, txn_id = 0, modem = modem, s_port = server_port, @@ -193,8 +194,23 @@ function rtu_comms(modem, local_port, server_port) end -- handle a MODBUS/SCADA packet - local handle_packet = function(packet, units, rtu_state) + local handle_packet = function(packet, units, rtu_state, conn_watchdog) if packet ~= nil then + local seq_ok = true + + -- check sequence number + if self.r_seq_num == nil then + self.r_seq_num = packet.scada_frame.seq_num() + elseif rtu_state.linked and self.r_seq_num >= packet.scada_frame.seq_num() then + log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + return + else + self.r_seq_num = packet.scada_frame.seq_num() + end + + -- feed watchdog on valid sequence number + conn_watchdog.feed() + local protocol = packet.scada_frame.protocol() if protocol == PROTOCOLS.MODBUS_TCP then @@ -220,6 +236,7 @@ function rtu_comms(modem, local_port, server_port) if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then -- acknowledgement rtu_state.linked = true + self.r_seq_num = nil elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- request for capabilities again send_advertisement(units) @@ -279,11 +296,17 @@ function rtu_comms(modem, local_port, server_port) _send(SCADA_MGMT_TYPES.RTU_HEARTBEAT, {}) end + local unlink = function (rtu_state) + rtu_state.linked = false + self.r_seq_num = nil + end + return { reconnect_modem = reconnect_modem, parse_packet = parse_packet, handle_packet = handle_packet, send_advertisement = send_advertisement, - send_heartbeat = send_heartbeat + send_heartbeat = send_heartbeat, + unlink = unlink } end diff --git a/rtu/startup.lua b/rtu/startup.lua index 9219bff..d4360b5 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.6" +local RTU_VERSION = "alpha-v0.4.7" local print = util.print local println = util.println @@ -53,6 +53,7 @@ local __shared_memory = { -- system objects rtu_sys = { rtu_comms = nil, + conn_watchdog = nil, units = {} }, @@ -203,6 +204,10 @@ end local main_thread = threads.thread__main(__shared_memory) local comms_thread = threads.thread__comms(__shared_memory) +-- start connection watchdog +smem_sys.conn_watchdog = util.new_watchdog(5) +log._debug("init> conn watchdog started") + -- run threads parallel.waitForAll(main_thread.exec, comms_thread.exec) diff --git a/rtu/threads.lua b/rtu/threads.lua index 049ca89..b02aafb 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -23,16 +23,38 @@ function thread__main(smem) local loop_clock = os.startTimer(MAIN_CLOCK) -- load in from shared memory - local rtu_state = smem.rtu_state - local rtu_dev = smem.rtu_dev - local rtu_comms = smem.rtu_sys.rtu_comms - local units = smem.rtu_sys.units + local rtu_state = smem.rtu_state + local rtu_dev = smem.rtu_dev + local rtu_comms = smem.rtu_sys.rtu_comms + local conn_watchdog = smem.rtu_sys.conn_watchdog + local units = smem.rtu_sys.units -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() - if event == "peripheral_detach" then + if event == "timer" and param1 == loop_clock then + -- start next clock timer + loop_clock = os.startTimer(MAIN_CLOCK) + + -- period tick, if we are linked send heartbeat, if not send advertisement + if rtu_state.linked then + rtu_comms.send_heartbeat() + else + -- advertise units + rtu_comms.send_advertisement(units) + end + elseif event == "modem_message" then + -- got a packet + local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5) + if packet ~= nil then + -- pass the packet onto the comms message queue + smem.q.mq_comms.push_packet(packet) + end + elseif event == "timer" and param1 == conn_watchdog.get_timer() then + -- haven't heard from server recently? unlink + rtu_comms.unlink(rtu_state) + elseif event == "peripheral_detach" then -- handle loss of a device local device = ppm.handle_unmount(param1) @@ -94,23 +116,6 @@ function thread__main(smem) end end end - elseif event == "timer" and param1 == loop_clock then - -- start next clock timer - loop_clock = os.startTimer(MAIN_CLOCK) - - -- period tick, if we are linked send heartbeat, if not send advertisement - if rtu_state.linked then - rtu_comms.send_heartbeat() - else - -- advertise units - rtu_comms.send_advertisement(units) - end - elseif event == "modem_message" then - -- got a packet - local packet = rtu_comms.parse_packet(param1, param2, param3, param4, param5) - if packet ~= nil then - smem.q.mq_comms.push_packet(packet) - end end -- check for termination request @@ -132,13 +137,14 @@ function thread__comms(smem) log._debug("comms thread start") -- load in from shared memory - local rtu_state = smem.rtu_state - local rtu_comms = smem.rtu_sys.rtu_comms - local units = smem.rtu_sys.units + local rtu_state = smem.rtu_state + local rtu_comms = smem.rtu_sys.rtu_comms + local conn_watchdog = smem.rtu_sys.conn_watchdog + local units = smem.rtu_sys.units - local comms_queue = smem.q.mq_comms + local comms_queue = smem.q.mq_comms - local last_update = util.time() + local last_update = util.time() -- thread loop while true do @@ -153,7 +159,8 @@ function thread__comms(smem) elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet -- handle the packet (rtu_state passed to allow setting link flag) - rtu_comms.handle_packet(msg.message, units, rtu_state) + -- (conn_watchdog passed to allow feeding watchdog) + rtu_comms.handle_packet(msg.message, units, rtu_state, conn_watchdog) end -- quick yield From 84e7ad43bc81222d6062c09ac323e8b850e864be Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 13:19:01 -0400 Subject: [PATCH 058/168] #39 RTU unit threads --- rtu/rtu.lua | 43 +++++++++++++++++++++++----------- rtu/startup.lua | 30 +++++++++++++++++++----- rtu/threads.lua | 49 ++++++++++++++++++++++++++++++++++++++ scada-common/modbus.lua | 52 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 155 insertions(+), 19 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 27f18c6..a35770d 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -145,13 +145,6 @@ function rtu_comms(modem, local_port, server_port) self.seq_num = self.seq_num + 1 end - local _send_modbus = function (m_pkt) - local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 - end - -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem @@ -164,6 +157,14 @@ function rtu_comms(modem, local_port, server_port) end end + -- send a MODBUS TCP packet + local send_modbus = function (m_pkt) + local s_pkt = comms.scada_packet() + s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end + -- parse a MODBUS/SCADA packet local parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil @@ -216,13 +217,28 @@ function rtu_comms(modem, local_port, server_port) if protocol == PROTOCOLS.MODBUS_TCP then local reply = modbus.reply__neg_ack(packet) - -- MODBUS instruction + -- handle MODBUS instruction if packet.unit_id <= #units then local unit = units[packet.unit_id] - local return_code, reply = unit.modbus_io.handle_packet(packet) - - if not return_code then - log._warning("MODBUS operation failed") + if unit.name == "redstone_io" then + -- immediately execute redstone RTU requests + local return_code, reply = unit.modbus_io.handle_packet(packet) + if not return_code then + log._warning("requested MODBUS operation failed") + end + else + -- check validity then pass off to unit comms thread + local return_code, reply = unit.modbus_io.check_request(packet) + if return_code then + -- check if an operation is already in progress for this unit + if unit.modbus_busy then + reply = unit.modbus_io.reply__srv_device_busy(packet) + else + unit.pkt_queue.push(packet) + end + else + log._warning("cannot perform requested MODBUS operation") + end end else -- unit ID out of range? @@ -230,7 +246,7 @@ function rtu_comms(modem, local_port, server_port) log._error("MODBUS packet requesting non-existent unit") end - _send_modbus(reply) + send_modbus(reply) elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then @@ -302,6 +318,7 @@ function rtu_comms(modem, local_port, server_port) end return { + send_modbus = send_modbus, reconnect_modem = reconnect_modem, parse_packet = parse_packet, handle_packet = handle_packet, diff --git a/rtu/startup.lua b/rtu/startup.lua index d4360b5..aad3483 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.7" +local RTU_VERSION = "alpha-v0.4.8" local print = util.print local println = util.println @@ -142,7 +142,10 @@ for reactor_idx = 1, #rtu_redstone do reactor = rtu_redstone[reactor_idx].for_reactor, device = capabilities, -- use device field for redstone channels rtu = rs_rtu, - modbus_io = modbus.new(rs_rtu) + modbus_io = modbus.new(rs_rtu), + modbus_busy = false, + pkt_queue = nil, + thread = nil }) log._debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) @@ -180,15 +183,22 @@ for i = 1, #rtu_devices do end if rtu_iface ~= nil then - table.insert(units, { + local rtu_unit = { name = rtu_devices[i].name, type = rtu_type, index = rtu_devices[i].index, reactor = rtu_devices[i].for_reactor, device = device, rtu = rtu_iface, - modbus_io = modbus.new(rtu_iface) - }) + modbus_io = modbus.new(rtu_iface), + modbus_busy = false, + pkt_queue = mqueue.new(), + thread = nil + } + + rtu_unit.thread = threads.thread__unit_comms(__shared_memory, rtu_unit) + + table.insert(units, rtu_unit) log._debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" .. rtu_devices[i].index .. "] for reactor " .. rtu_devices[i].for_reactor) @@ -208,8 +218,16 @@ local comms_thread = threads.thread__comms(__shared_memory) smem_sys.conn_watchdog = util.new_watchdog(5) log._debug("init> conn watchdog started") +-- assemble thread list +local _threads = { main_thread.exec, comms_thread.exec } +for i = 1, #units do + if units[i].thread ~= nil then + table.insert(_threads, units[i].thread.exec) + end +end + -- run threads -parallel.waitForAll(main_thread.exec, comms_thread.exec) +parallel.waitForAll(table.unpack(_threads)) println_ts("exited") log._info("exited") diff --git a/rtu/threads.lua b/rtu/threads.lua index b02aafb..8aef13f 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -180,3 +180,52 @@ function thread__comms(smem) return { exec = exec } end + +-- per-unit communications handler thread +function thread__unit_comms(smem, unit) + -- execute thread + local exec = function () + log._debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")") + + -- load in from shared memory + local rtu_state = smem.rtu_state + + local packet_queue = unit.pkt_queue + + local last_update = util.time() + + -- thread loop + while true do + -- check for messages in the message queue + while packet_queue.ready() and not rtu_state.shutdown do + local msg = packet_queue.pop() + + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + unit.modbus_busy = true + local return_code, reply = unit.modbus_io.handle_packet(packet) + rtu.send_modbus(reply) + unit.modbus_busy = false + end + + -- quick yield + util.nop() + end + + -- check for termination request + if rtu_state.shutdown then + log._warning("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")") + break + end + + -- delay before next check + last_update = util.adaptive_delay(COMMS_SLEEP, last_update) + end + end + + return { exec = exec } +end diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index 494fde8..a2a1cc3 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -203,6 +203,46 @@ function new(rtu_dev) return return_ok, response end + -- validate a request without actually executing it + local check_request = function (packet) + local return_code = true + local response = { MODBUS_EXCODE.ACKNOWLEDGE } + + if #packet.data == 2 then + -- handle by function code + if packet.func_code == MODBUS_FCODE.READ_COILS then + elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then + elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then + elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGISTERS then + elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then + elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then + elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then + elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then + else + -- unknown function + return_code = false + response = { MODBUS_EXCODE.ILLEGAL_FUNCTION } + end + else + -- invalid length + return_code = false + response = { MODBUS_EXCODE.NEG_ACKNOWLEDGE } + end + + -- default is to echo back + local func_code = packet.func_code + if not return_code then + -- echo back with error flag + func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + end + + -- create reply + local reply = comms.modbus_packet() + reply.make(packet.txn_id, packet.unit_id, func_code, response) + + return return_code, reply + end + -- handle a MODBUS TCP packet and generate a reply local handle_packet = function (packet) local return_code = true @@ -258,6 +298,16 @@ function new(rtu_dev) return return_code, reply end + -- return a SERVER_DEVICE_BUSY error reply + local reply__srv_device_busy = function (packet) + -- reply back with error flag and exception code + local reply = comms.modbus_packet() + local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + local data = { MODBUS_EXCODE.SERVER_DEVICE_BUSY } + reply.make(packet.txn_id, packet.unit_id, fcode, data) + return reply + end + -- return a NEG_ACKNOWLEDGE error reply local reply__neg_ack = function (packet) -- reply back with error flag and exception code @@ -279,7 +329,9 @@ function new(rtu_dev) end return { + check_request = check_request, handle_packet = handle_packet, + reply__srv_device_busy = reply__srv_device_busy, reply__neg_ack = reply__neg_ack, reply__gw_unavailable = reply__gw_unavailable } From 358735221936935b9930f510f8f3ae942f9c750b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 13:20:56 -0400 Subject: [PATCH 059/168] log exit notices as info messages not warnings --- reactor-plc/threads.lua | 10 +++++----- rtu/threads.lua | 6 +++--- supervisor/startup.lua | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index eb3c026..93a243e 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -168,7 +168,7 @@ function thread__main(smem, init) if event == "terminate" or ppm.should_terminate() then -- iss handles reactor shutdown plc_state.shutdown = true - log._warning("terminate requested, main thread exiting") + log._info("terminate requested, main thread exiting") break end end @@ -267,7 +267,7 @@ function thread__iss(smem) -- check for termination request if plc_state.shutdown then -- safe exit - log._warning("iss thread shutdown initiated") + log._info("iss thread shutdown initiated") if plc_state.init_ok then plc_state.scram = true reactor.scram() @@ -280,7 +280,7 @@ function thread__iss(smem) log._error("iss thread failed to SCRAM reactor on exit") end end - log._warning("iss thread exiting") + log._info("iss thread exiting") break end @@ -331,7 +331,7 @@ function thread__comms_tx(smem) -- check for termination request if plc_state.shutdown then - log._warning("comms tx thread exiting") + log._info("comms tx thread exiting") break end @@ -379,7 +379,7 @@ function thread__comms_rx(smem) -- check for termination request if plc_state.shutdown then - log._warning("comms rx thread exiting") + log._info("comms rx thread exiting") break end diff --git a/rtu/threads.lua b/rtu/threads.lua index 8aef13f..c5af330 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -121,7 +121,7 @@ function thread__main(smem) -- check for termination request if event == "terminate" or ppm.should_terminate() then rtu_state.shutdown = true - log._warning("terminate requested, main thread exiting") + log._info("terminate requested, main thread exiting") break end end @@ -169,7 +169,7 @@ function thread__comms(smem) -- check for termination request if rtu_state.shutdown then - log._warning("comms thread exiting") + log._info("comms thread exiting") break end @@ -218,7 +218,7 @@ function thread__unit_comms(smem, unit) -- check for termination request if rtu_state.shutdown then - log._warning("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")") + log._info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")") break end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 1e5396d..917280d 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -100,7 +100,7 @@ while true do -- check for termination request if event == "terminate" or ppm.should_terminate() then - log._warning("terminate requested, exiting...") + log._info("terminate requested, exiting...") break end end From c805b6e0c54aa71fcdb59d487f0d442d0e7e13cc Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 13:32:37 -0400 Subject: [PATCH 060/168] log init function to set path and write mode --- coordinator/startup.lua | 19 ++++++++++++++----- reactor-plc/startup.lua | 4 +++- rtu/startup.lua | 4 +++- scada-common/log.lua | 29 +++++++++++++++++++++++------ supervisor/startup.lua | 4 +++- 5 files changed, 46 insertions(+), 14 deletions(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 20be7a3..a3f12d4 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -10,18 +10,27 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("coordinator/config.lua") os.loadAPI("coordinator/coordinator.lua") -local COORDINATOR_VERSION = "alpha-v0.1.0" +local COORDINATOR_VERSION = "alpha-v0.1.1" +local print = util.print +local println = util.println local print_ts = util.print_ts +local println_ts = util.println_ts +log.init("/log.txt", log.MODE.APPEND) + +log._info("========================================") +log._info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) +log._info("========================================") +println(">> RTU " .. COORDINATOR_VERSION .. " <<") + +-- mount connected devices ppm.mount_all() -local modem = ppm.get_device("modem") - -print("| SCADA Coordinator - " .. COORDINATOR_VERSION .. " |") +local modem = ppm.get_wireless_modem() -- we need a modem if modem == nil then - print("Please connect a modem.") + println("please connect a wireless modem") return end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 37ac6de..2952191 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,13 +12,15 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.10" +local R_PLC_VERSION = "alpha-v0.4.11" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +log.init("/log.txt", log.MODE.APPEND) + log._info("========================================") log._info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) log._info("========================================") diff --git a/rtu/startup.lua b/rtu/startup.lua index aad3483..6220ad1 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,13 +19,15 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.8" +local RTU_VERSION = "alpha-v0.4.9" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +log.init("/log.txt", log.MODE.APPEND) + log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) log._info("========================================") diff --git a/scada-common/log.lua b/scada-common/log.lua index cf493eb..1aafda3 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -5,10 +5,16 @@ -- we use extra short abbreviations since computer craft screens are very small -- underscores are used since some of these names are used elsewhere (e.g. 'debug' is a lua table) -local LOG_DEBUG = true -local LOG_PATH = "/log.txt" +MODE = { + APPEND = 0, + NEW = 1 +} -local file_handle = fs.open(LOG_PATH, "a") +local LOG_DEBUG = true + +local log_path = "/log.txt" +local mode = MODE.APPEND +local file_handle = nil local _log = function (msg) local stamped = os.date("[%c] ") .. msg @@ -20,7 +26,7 @@ local _log = function (msg) end) -- if we don't have much space, we need to create a new log file - local delete_log = fs.getFreeSpace(LOG_PATH) < 100 + local delete_log = fs.getFreeSpace(log_path) < 100 if not status then if result == "Out of space" then @@ -33,8 +39,8 @@ local _log = function (msg) if delete_log then -- delete the old log file and open a new one file_handle.close() - fs.delete(LOG_PATH) - file_handle = fs.open(LOG_PATH, "a") + fs.delete(log_path) + init(log_path, mode) -- leave a message local notif = os.date("[%c] ") .. "recycled log file" @@ -44,6 +50,17 @@ local _log = function (msg) end end +function init(path, write_mode) + log_path = path + mode = write_mode + + if mode == MODE.APPEND then + file_handle = fs.open(path, "a") + else + file_handle = fs.open(path, "w+") + end +end + function _debug(msg, trace) if LOG_DEBUG then local dbg_info = "" diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 917280d..9cf704a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,13 +18,15 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.8" +local SUPERVISOR_VERSION = "alpha-v0.1.9" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +log.init("/log.txt", log.MODE.APPEND) + log._info("========================================") log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) log._info("========================================") From e1135eac019bfb4a8aee33b19724da169e46fff4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 13:36:00 -0400 Subject: [PATCH 061/168] log init parameters in config files --- reactor-plc/config.lua | 6 ++++++ reactor-plc/startup.lua | 2 +- rtu/config.lua | 6 ++++++ rtu/startup.lua | 2 +- supervisor/config.lua | 6 ++++++ supervisor/startup.lua | 2 +- 6 files changed, 21 insertions(+), 3 deletions(-) diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index dceeb21..43086d5 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -6,3 +6,9 @@ REACTOR_ID = 1 SERVER_PORT = 16000 -- port to listen to incoming packets FROM server LISTEN_PORT = 14001 +-- log path +LOG_PATH = "/log.txt" +-- log mode +-- 0 = APPEND (adds to existing file on start) +-- 1 = NEW (replaces existing file on start) +LOG_MODE = 0 diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 2952191..69038d3 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -19,7 +19,7 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -log.init("/log.txt", log.MODE.APPEND) +log.init(config.LOG_PATH, config.LOG_MODE) log._info("========================================") log._info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) diff --git a/rtu/config.lua b/rtu/config.lua index 71804a4..6ba5653 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -4,6 +4,12 @@ SERVER_PORT = 16000 -- port to listen to incoming packets FROM server LISTEN_PORT = 15001 +-- log path +LOG_PATH = "/log.txt" +-- log mode +-- 0 = APPEND (adds to existing file on start) +-- 1 = NEW (replaces existing file on start) +LOG_MODE = 0 -- RTU peripheral devices (named: side/network device name) RTU_DEVICES = { { diff --git a/rtu/startup.lua b/rtu/startup.lua index 6220ad1..59c4354 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -26,7 +26,7 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -log.init("/log.txt", log.MODE.APPEND) +log.init(config.LOG_PATH, config.LOG_MODE) log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) diff --git a/supervisor/config.lua b/supervisor/config.lua index fde20b3..b8ba7fa 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -4,3 +4,9 @@ SCADA_DEV_LISTEN = 16000 SCADA_SV_LISTEN = 16100 -- expected number of reactors NUM_REACTORS = 4 +-- log path +LOG_PATH = "/log.txt" +-- log mode +-- 0 = APPEND (adds to existing file on start) +-- 1 = NEW (replaces existing file on start) +LOG_MODE = 0 diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 9cf704a..946ef0c 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -25,7 +25,7 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -log.init("/log.txt", log.MODE.APPEND) +log.init(config.LOG_PATH, config.LOG_MODE) log._info("========================================") log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) From 10aa34a8e801dc0949725ee0eae8f24e56c9cbc9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 29 Apr 2022 22:27:54 -0400 Subject: [PATCH 062/168] #17 PLC ramp burn rate to setpoint --- reactor-plc/plc.lua | 20 ++++----- reactor-plc/startup.lua | 11 ++++- reactor-plc/threads.lua | 99 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 111 insertions(+), 19 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index c1ffbf0..a8c7c5e 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -357,12 +357,12 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end local sys_status = { - util.time(), - (not self.scrammed), - iss.is_tripped(), - degraded, - self.reactor.getHeatingRate(), - mek_data + util.time(), -- timestamp + (not self.scrammed), -- enabled + iss.is_tripped(), -- overridden + degraded, -- degraded + self.reactor.getHeatingRate(), -- heating rate + mek_data -- mekanism status data } _send(RPLC_TYPES.STATUS, sys_status) @@ -411,7 +411,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end -- handle an RPLC packet - local handle_packet = function (packet, plc_state, conn_watchdog) + local handle_packet = function (packet, plc_state, setpoints, conn_watchdog) if packet ~= nil then -- check sequence number if self.r_seq_num == nil then @@ -499,11 +499,11 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.max_burn_rate = max_burn_rate end - -- if we know our max burn rate, update current burn rate if in range + -- if we know our max burn rate, update current burn rate setpoint if in range if max_burn_rate ~= ppm.ACCESS_FAULT then if burn_rate > 0 and burn_rate <= max_burn_rate then - self.reactor.setBurnRate(burn_rate) - success = self.reactor.__p_is_ok() + setpoints.burn_rate = burn_rate + success = true end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 69038d3..2328a00 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.11" +local R_PLC_VERSION = "alpha-v0.4.12" local print = util.print local println = util.println @@ -43,6 +43,10 @@ local __shared_memory = { no_reactor = false, no_modem = false }, + + setpoints = { + burn_rate = 0.0 + }, -- core PLC devices plc_dev = { @@ -135,8 +139,11 @@ if __shared_memory.networked then local comms_thread_tx = threads.thread__comms_tx(__shared_memory) local comms_thread_rx = threads.thread__comms_rx(__shared_memory) + -- setpoint control only needed when networked + local sp_ctrl_thread = threads.thread__setpoint_control(__shared_memory) + -- run threads - parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec) + parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec, sp_ctrl_thread.exec) else -- run threads, excluding comms parallel.waitForAll(main_thread.exec, iss_thread.exec) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 93a243e..6a55e7f 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -10,9 +10,12 @@ local println_ts = util.println_ts local psleep = util.psleep -local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) -local ISS_SLEEP = 500 -- (500ms, 10 ticks) -local COMMS_SLEEP = 150 -- (150ms, 3 ticks) +local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) +local ISS_SLEEP = 500 -- (500ms, 10 ticks) +local COMMS_SLEEP = 150 -- (150ms, 3 ticks) +local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks) + +local BURN_RATE_RAMP_mB_s = 5.0 local MQ__ISS_CMD = { SCRAM = 1, @@ -196,7 +199,7 @@ function thread__iss(smem) -- thread loop while true do - local reactor = smem.plc_dev.reactor + local reactor = plc_dev.reactor -- ISS checks if plc_state.init_ok then @@ -292,7 +295,7 @@ function thread__iss(smem) return { exec = exec } end --- communications handler thread +-- communications sender thread function thread__comms_tx(smem) -- execute thread local exec = function () @@ -343,6 +346,7 @@ function thread__comms_tx(smem) return { exec = exec } end +-- communications handler thread function thread__comms_rx(smem) -- execute thread local exec = function () @@ -350,6 +354,7 @@ function thread__comms_rx(smem) -- load in from shared memory local plc_state = smem.plc_state + local setpoints = smem.setpoints local plc_comms = smem.plc_sys.plc_comms local conn_watchdog = smem.plc_sys.conn_watchdog @@ -369,8 +374,10 @@ function thread__comms_rx(smem) -- received data elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet - -- handle the packet (plc_state passed to allow clearing SCRAM flag) - plc_comms.handle_packet(msg.message, plc_state, conn_watchdog) + -- handle the packet (setpoints passed to update burn rate setpoint) + -- (plc_state passed to allow clearing SCRAM flag and check if degraded) + -- (conn_watchdog passed to allow feeding the watchdog) + plc_comms.handle_packet(msg.message, setpoints, plc_state, conn_watchdog) end -- quick yield @@ -390,3 +397,81 @@ function thread__comms_rx(smem) return { exec = exec } end + +-- apply setpoints +function thread__setpoint_control(smem) + -- execute thread + local exec = function () + log._debug("comms rx thread start") + + -- load in from shared memory + local plc_state = smem.plc_state + local setpoints = smem.setpoints + local plc_dev = smem.plc_dev + + local last_update = util.time() + local running = false + + local last_sp_burn = 0 + + -- thread loop + while true do + local reactor = plc_dev.reactor + + -- check if we should start ramping + if setpoints.burn_rate ~= last_sp_burn then + last_sp_burn = setpoints.burn_rate + running = true + end + + -- only check I/O if active to save on processing time + if running then + -- do not use the actual elapsed time, it could spike + -- we do not want to have big jumps as that is what we are trying to avoid in the first place + local min_elapsed_s = SETPOINT_CTRL_SLEEP / 1000.0 + + -- clear so we can later evaluate if we should keep running + running = false + + -- adjust burn rate (setpoints.burn_rate) + if not plc_state.scram then + local current_burn_rate = reactor.getBurnRate() + if (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then + -- calculate new burn rate + local new_burn_rate = current_burn_rate + + if setpoints.burn_rate > current_burn_rate then + -- need to ramp up + local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate > setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + else + -- need to ramp down + local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate < setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + end + + -- set the burn rate + reactor.setBurnRate(new_burn_rate) + + running = running or (new_burn_rate ~= setpoints.burn_rate) + end + end + end + + -- check for termination request + if plc_state.shutdown then + log._info("setpoint control thread exiting") + break + end + + -- delay before next check + last_update = util.adaptive_delay(SETPOINT_CTRL_SLEEP, last_update) + end + end + + return { exec = exec } +end From aeda38fa0133f96817ec4d07e40126aca2850147 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 30 Apr 2022 03:03:34 -0400 Subject: [PATCH 063/168] #17 set burn rate right away if within range, reset last setpoint on SCRAM --- reactor-plc/threads.lua | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 6a55e7f..b1b331a 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -420,8 +420,20 @@ function thread__setpoint_control(smem) -- check if we should start ramping if setpoints.burn_rate ~= last_sp_burn then - last_sp_burn = setpoints.burn_rate - running = true + if not plc_state.scram then + if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then + -- update without ramp if <= 5 mB/t change + log._debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") + reactor.setBurnRate(setpoints.burn_rate) + else + log._debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") + running = true + end + + last_sp_burn = setpoints.burn_rate + else + last_sp_burn = 0 + end end -- only check I/O if active to save on processing time @@ -459,6 +471,8 @@ function thread__setpoint_control(smem) running = running or (new_burn_rate ~= setpoints.burn_rate) end + else + last_sp_burn = 0 end end From 3fe47f99a991d100f8340b0980fe85375c1db549 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 30 Apr 2022 13:44:28 -0400 Subject: [PATCH 064/168] PLC bugfix --- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 2328a00..3905f0e 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.12" +local R_PLC_VERSION = "alpha-v0.4.13" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index b1b331a..36d3179 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -483,7 +483,7 @@ function thread__setpoint_control(smem) end -- delay before next check - last_update = util.adaptive_delay(SETPOINT_CTRL_SLEEP, last_update) + last_update = util.adaptive_delay(SP_CTRL_SLEEP, last_update) end end From 479194b589c7ddf89c7b478b7ff75d233fdc26e3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 May 2022 13:26:02 -0400 Subject: [PATCH 065/168] ISS alarm status packet adjustments --- reactor-plc/plc.lua | 6 ++++-- reactor-plc/startup.lua | 4 ++-- supervisor/session/plc.lua | 13 ++++++++++--- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index a8c7c5e..52bd16c 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -147,7 +147,7 @@ function iss_init(reactor) elseif self.cache[5] then log._warning("ISS: no fuel!") status = "no_fuel" - elseif self.timed_out then + elseif self.cache[7] then log._warning("ISS: supervisor connection timeout!") status = "timeout" else @@ -368,14 +368,16 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send(RPLC_TYPES.STATUS, sys_status) end + -- send safety system status local send_iss_status = function () _send(RPLC_TYPES.ISS_STATUS, iss.status()) end + -- send safety system alarm local send_iss_alarm = function (cause) local iss_alarm = { cause, - iss.status() + table.unpack(iss.status()) } _send(RPLC_TYPES.ISS_ALARM, iss_alarm) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 3905f0e..17e4a2e 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -131,8 +131,8 @@ end init() -- init threads -local main_thread = threads.thread__main(__shared_memory, init) -local iss_thread = threads.thread__iss(__shared_memory) +local main_thread = threads.thread__main(__shared_memory, init) +local iss_thread = threads.thread__iss(__shared_memory) if __shared_memory.networked then -- init comms threads diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 8c08b86..bf95dbb 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -49,6 +49,8 @@ function new_session(id, for_reactor, in_queue, out_queue) control_state = false, overridden = false, degraded = false, + iss_tripped = false, + iss_trip_cause = "ok", iss_status = { dmg_crit = false, ex_hcool = false, @@ -262,13 +264,15 @@ function new_session(id, for_reactor, in_queue, out_queue) elseif rplc_pkt.type == RPLC_TYPES.ISS_ALARM then -- ISS alarm self.sDB.overridden = true - if rplc_pkt.length == 7 then - local status = pcall(_copy_iss_status, rplc_pkt.data) + if rplc_pkt.length == 8 then + self.sDB.iss_tripped = true + self.sDB.iss_trip_cause = rplc_pkt.data[1] + local status = pcall(_copy_iss_status, { table.unpack(rplc_pkt.data, 2, #rplc_pkt.length) }) if status then -- copied in ISS status data OK else -- error copying ISS status data - log._error(log_header .. "failed to parse ISS status packet data") + log._error(log_header .. "failed to parse ISS alarm status data") end else log._debug(log_header .. "RPLC ISS alarm packet length mismatch") @@ -277,6 +281,9 @@ function new_session(id, for_reactor, in_queue, out_queue) -- ISS clear acknowledgement if _get_ack(rplc_pkt) == false then log._warning(log_header .. "ISS clear failed") + else + self.sDB.iss_tripped = false + self.sDB.iss_trip_cause = "ok" end else log._debug(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) From b76871aa0702ce098654e899dbb451983529b134 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 May 2022 15:34:44 -0400 Subject: [PATCH 066/168] fixed incorrect program type in startup message --- coordinator/startup.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coordinator/startup.lua b/coordinator/startup.lua index a3f12d4..fe0bf48 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -22,7 +22,7 @@ log.init("/log.txt", log.MODE.APPEND) log._info("========================================") log._info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) log._info("========================================") -println(">> RTU " .. COORDINATOR_VERSION .. " <<") +println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") -- mount connected devices ppm.mount_all() From cd46c69a6639dc107e9f4628dbea52af9d6c028b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 May 2022 15:35:07 -0400 Subject: [PATCH 067/168] defined push_data to be implemented --- scada-common/mqueue.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index f14951e..dc3e47f 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -31,8 +31,8 @@ function new() _push(TYPE.COMMAND, message) end - local push_data = function (message) - _push(TYPE.DATA, message) + local push_data = function (key, value) + _push(TYPE.DATA, { key = key, val = value }) end local push_packet = function (message) From 7ff0e257113ce846a6b9870ea49705ce8aab5e63 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 1 May 2022 17:04:38 -0400 Subject: [PATCH 068/168] #7 sending commands to PLCs, retrying failed sends until confirmed --- supervisor/session/plc.lua | 148 ++++++++++++++++++++++++++---- supervisor/session/svsessions.lua | 1 - supervisor/startup.lua | 2 +- 3 files changed, 130 insertions(+), 21 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index bf95dbb..fa805c4 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -6,10 +6,15 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES -PLC_S_COMMANDS = { +-- retry time constants in ms +local INITIAL_WAIT = 1500 +local RETRY_PERIOD = 1000 + +PLC_S_CMDS = { SCRAM = 0, ENABLE = 1, - ISS_CLEAR = 2 + BURN_RATE = 2, + ISS_CLEAR = 3 } local PERIODICS = { @@ -26,6 +31,7 @@ function new_session(id, for_reactor, in_queue, out_queue) in_q = in_queue, out_q = out_queue, commanded_state = false, + commanded_burn_rate = 0.0, -- connection properties seq_num = 0, r_seq_num = nil, @@ -33,15 +39,25 @@ function new_session(id, for_reactor, in_queue, out_queue) received_struct = false, plc_conn_watchdog = util.new_watchdog(3), last_rtt = 0, - -- when to next retry one of these requests + -- periodic messages periodics = { last_update = 0, keep_alive = 0 }, + -- when to next retry one of these requests retry_times = { - struct_req = 0, + struct_req = 500, scram_req = 0, - enable_req = 0 + enable_req = 0, + burn_rate_req = 0, + iss_clear_req = 0 + }, + -- command acknowledgements + acks = { + scram = true, + enable = true, + burn_rate = true, + iss_clear = true }, -- session database sDB = { @@ -110,6 +126,7 @@ function new_session(id, for_reactor, in_queue, out_queue) end local _copy_status = function (mek_data) + -- copy status information self.sDB.mek_status.status = mek_data[1] self.sDB.mek_status.burn_rate = mek_data[2] self.sDB.mek_status.act_burn_rate = mek_data[3] @@ -118,6 +135,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.mek_status.boil_eff = mek_data[6] self.sDB.mek_status.env_loss = mek_data[7] + -- copy container information self.sDB.mek_status.fuel = mek_data[8] self.sDB.mek_status.fuel_fill = mek_data[9] self.sDB.mek_status.waste = mek_data[10] @@ -128,6 +146,14 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.mek_status.hcool_type = mek_data[15] self.sDB.mek_status.hcool_amnt = mek_data[16] self.sDB.mek_status.hcool_fill = mek_data[17] + + -- update computable fields if we have our structure + if self.received_struct then + self.sDB.mek_status.fuel_need = self.sDB.mek_struct.fuel_cap - self.sDB.mek_status.fuel_fill + self.sDB.mek_status.waste_need = self.sDB.mek_struct.waste_cap - self.sDB.mek_status.waste_fill + self.sDB.mek_status.cool_need = self.sDB.mek_struct.cool_cap - self.sDB.mek_status.cool_fill + self.sDB.mek_status.hcool_need = self.sDB.mek_struct.hcool_cap - self.sDB.mek_status.hcool_fill + end end local _copy_struct = function (mek_data) @@ -231,22 +257,26 @@ function new_session(id, for_reactor, in_queue, out_queue) -- SCRAM acknowledgement local ack = _get_ack(rplc_pkt) if ack then + self.acks.scram = true self.sDB.control_state = false elseif ack == false then - log._warning(log_header .. "SCRAM failed!") + log._debug(log_header .. "SCRAM failed!") end elseif rplc_pkt.type == RPLC_TYPES.MEK_ENABLE then -- enable acknowledgement local ack = _get_ack(rplc_pkt) if ack then + self.acks.enable = true self.sDB.control_state = true elseif ack == false then - log._warning(log_header .. "enable failed!") + log._debug(log_header .. "enable failed!") end elseif rplc_pkt.type == RPLC_TYPES.MEK_BURN_RATE then -- burn rate acknowledgement - if _get_ack(rplc_pkt) == false then - log._warning(log_header .. "burn rate update failed!") + if _get_ack(rplc_pkt) then + self.acks.burn_rate = true + else + log._debug(log_header .. "burn rate update failed!") end elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then -- ISS status packet received, copy data @@ -279,11 +309,12 @@ function new_session(id, for_reactor, in_queue, out_queue) end elseif rplc_pkt.type == RPLC_TYPES.ISS_CLEAR then -- ISS clear acknowledgement - if _get_ack(rplc_pkt) == false then - log._warning(log_header .. "ISS clear failed") - else + if _get_ack(rplc_pkt) then + self.acks.iss_tripped = true self.sDB.iss_tripped = false self.sDB.iss_trip_cause = "ok" + else + log._debug(log_header .. "ISS clear failed") end else log._debug(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) @@ -318,7 +349,6 @@ function new_session(id, for_reactor, in_queue, out_queue) if self.received_struct then return self.sDB.mek_struct else - -- @todo: need a system in place to re-request this periodically return nil end end @@ -340,7 +370,33 @@ function new_session(id, for_reactor, in_queue, out_queue) _handle_packet(message.message) elseif message.qtype == mqueue.TYPE.COMMAND then -- handle instruction - + local cmd = message.message + if cmd == PLC_S_CMDS.SCRAM then + -- SCRAM reactor + self.acks.scram = false + self.retry_times.scram_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_SCRAM, {}) + elseif cmd == PLC_S_CMDS.ENABLE then + -- enable reactor + self.acks.enable = false + self.retry_times.enable_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_ENABLE, {}) + elseif cmd == PLC_S_CMDS.ISS_CLEAR then + -- clear ISS + self.acks.iss_clear = false + self.retry_times.iss_clear_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.ISS_CLEAR, {}) + end + elseif message.qtype == mqueue.TYPE.DATA then + -- instruction with body + local cmd = message.message + if cmd.key == PLC_S_CMDS.BURN_RATE then + -- update burn rate + self.commanded_burn_rate = cmd.val + self.acks.burn_rate = false + self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate }) + end end -- max 100ms spent processing queue @@ -354,16 +410,70 @@ function new_session(id, for_reactor, in_queue, out_queue) -- update periodics -- ---------------------- - local elapsed = os.clock() - self.periodics.last_update + local elapsed = util.time() - self.periodics.last_update - self.periodics.keep_alive = self.periodics.keep_alive + elapsed + local periodics = self.periodics - if self.periodics.keep_alive >= PERIODICS.KEEP_ALIVE then + -- keep alive + + periodics.keep_alive = periodics.keep_alive + elapsed + if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then _send(RPLC_TYPES.KEEP_ALIVE, { util.time() }) - self.periodics.keep_alive = 0 + periodics.keep_alive = 0 end - self.periodics.last_update = os.clock() + self.periodics.last_update = util.time() + + --------------------- + -- attempt retries -- + --------------------- + + local rtimes = self.rtimes + + -- struct request retry + + if not self.received_struct then + if rtimes.struct_req - util.time() <= 0 then + _send(RPLC_TYPES.LINK_REQ, {}) + rtimes.struct_req = util.time() + RETRY_PERIOD + end + end + + -- SCRAM request retry + + if not self.acks.scram then + if rtimes.scram_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_SCRAM, {}) + rtimes.scram_req = util.time() + RETRY_PERIOD + end + end + + -- enable request retry + + if not self.acks.enable then + if rtimes.enable_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_ENABLE, {}) + rtimes.enable_req = util.time() + RETRY_PERIOD + end + end + + -- burn rate request retry + + if not self.acks.burn_rate then + if rtimes.burn_rate_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate }) + rtimes.burn_rate_req = util.time() + RETRY_PERIOD + end + end + + -- ISS clear request retry + + if not self.acks.iss_clear then + if rtimes.iss_clear_req - util.time() <= 0 then + _send(RPLC_TYPES.ISS_CLEAR, {}) + rtimes.iss_clear_req = util.time() + RETRY_PERIOD + end + end end return self.connected diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 72f12d4..6304384 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -128,7 +128,6 @@ local function _iterate(sessions) local ok = session.instance.iterate() if ok then -- send packets in out queue - -- @todo handle commands if that's being used too while session.out_queue.ready() do local msg = session.out_queue.pop() if msg.qtype == mqueue.TYPE.PACKET then diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 946ef0c..dceeeb9 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.9" +local SUPERVISOR_VERSION = "alpha-v0.1.10" local print = util.print local println = util.println From 76c81395b74aa147b92706fdcdd2b739bb422ae3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 11:42:24 -0400 Subject: [PATCH 069/168] #41 close session connections --- scada-common/comms.lua | 4 +- supervisor/session/plc.lua | 139 +++++++++++++++++++----------- supervisor/session/svsessions.lua | 71 +++++++++------ supervisor/startup.lua | 7 +- supervisor/supervisor.lua | 8 +- 5 files changed, 149 insertions(+), 80 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index bca5682..998e914 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -27,10 +27,10 @@ RPLC_LINKING = { SCADA_MGMT_TYPES = { PING = 0, -- generic ping - SV_HEARTBEAT = 1, -- supervisor heartbeat + CLOSE = 1, -- close a connection REMOTE_LINKED = 2, -- remote device linked RTU_ADVERT = 3, -- RTU capability advertisement - RTU_HEARTBEAT = 4, -- RTU heartbeat + RTU_HEARTBEAT = 4 -- RTU heartbeat } RTU_ADVERT_TYPES = { diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index fa805c4..810d8df 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -167,6 +167,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.mek_struct.max_burn = mek_data[8] end + -- get an ACK status local _get_ack = function (pkt) if pkt.length == 1 then return pkt.data[1] @@ -176,36 +177,35 @@ function new_session(id, for_reactor, in_queue, out_queue) end end - local _handle_packet = function (rplc_pkt) - local checks_ok = true - + -- handle a packet + local _handle_packet = function (pkt) -- check sequence number if self.r_seq_num == nil then - self.r_seq_num = rplc_pkt.scada_frame.seq_num() - elseif self.r_seq_num >= rplc_pkt.scada_frame.seq_num() then - log._warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. rplc_pkt.scada_frame.seq_num()) - checks_ok = false + self.r_seq_num = pkt.scada_frame.seq_num() + elseif self.r_seq_num >= pkt.scada_frame.seq_num() then + log._warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) + return else - self.r_seq_num = rplc_pkt.scada_frame.seq_num() - end - - -- check reactor ID - if rplc_pkt.id ~= for_reactor then - log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. rplc_pkt.id) - checks_ok = false + self.r_seq_num = pkt.scada_frame.seq_num() end -- process packet - if checks_ok then + if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then + -- check reactor ID + if pkt.id ~= for_reactor then + log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id) + return + end + -- feed watchdog self.plc_conn_watchdog.feed() -- handle packet by type - if rplc_pkt.type == RPLC_TYPES.KEEP_ALIVE then + if pkt.type == RPLC_TYPES.KEEP_ALIVE then -- keep alive reply - if rplc_pkt.length == 2 then - local srv_start = rplc_pkt.data[1] - local plc_send = rplc_pkt.data[2] + if pkt.length == 2 then + local srv_start = pkt.data[1] + local plc_send = pkt.data[2] local srv_now = util.time() self.last_rtt = srv_now - srv_start @@ -218,18 +218,18 @@ function new_session(id, for_reactor, in_queue, out_queue) else log._debug(log_header .. "RPLC keep alive packet length mismatch") end - elseif rplc_pkt.type == RPLC_TYPES.STATUS then + elseif pkt.type == RPLC_TYPES.STATUS then -- status packet received, update data - if rplc_pkt.length >= 5 then - self.sDB.last_status_update = rplc_pkt.data[1] - self.sDB.control_state = rplc_pkt.data[2] - self.sDB.overridden = rplc_pkt.data[3] - self.sDB.degraded = rplc_pkt.data[4] - self.sDB.mek_status.heating_rate = rplc_pkt.data[5] + if pkt.length >= 5 then + self.sDB.last_status_update = pkt.data[1] + self.sDB.control_state = pkt.data[2] + self.sDB.overridden = pkt.data[3] + self.sDB.degraded = pkt.data[4] + self.sDB.mek_status.heating_rate = pkt.data[5] -- attempt to read mek_data table - if rplc_pkt.data[6] ~= nil then - local status = pcall(_copy_status, rplc_pkt.data[6]) + if pkt.data[6] ~= nil then + local status = pcall(_copy_status, pkt.data[6]) if status then -- copied in status data OK else @@ -240,10 +240,10 @@ function new_session(id, for_reactor, in_queue, out_queue) else log._debug(log_header .. "RPLC status packet length mismatch") end - elseif rplc_pkt.type == RPLC_TYPES.MEK_STRUCT then + elseif pkt.type == RPLC_TYPES.MEK_STRUCT then -- received reactor structure, record it - if rplc_pkt.length == 8 then - local status = pcall(_copy_struct, rplc_pkt.data) + if pkt.length == 8 then + local status = pcall(_copy_struct, pkt.data) if status then -- copied in structure data OK else @@ -253,35 +253,36 @@ function new_session(id, for_reactor, in_queue, out_queue) else log._debug(log_header .. "RPLC struct packet length mismatch") end - elseif rplc_pkt.type == RPLC_TYPES.MEK_SCRAM then + elseif pkt.type == RPLC_TYPES.MEK_SCRAM then -- SCRAM acknowledgement - local ack = _get_ack(rplc_pkt) + local ack = _get_ack(pkt) if ack then self.acks.scram = true self.sDB.control_state = false elseif ack == false then log._debug(log_header .. "SCRAM failed!") end - elseif rplc_pkt.type == RPLC_TYPES.MEK_ENABLE then + elseif pkt.type == RPLC_TYPES.MEK_ENABLE then -- enable acknowledgement - local ack = _get_ack(rplc_pkt) + local ack = _get_ack(pkt) if ack then self.acks.enable = true self.sDB.control_state = true elseif ack == false then log._debug(log_header .. "enable failed!") end - elseif rplc_pkt.type == RPLC_TYPES.MEK_BURN_RATE then + elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then -- burn rate acknowledgement - if _get_ack(rplc_pkt) then + local ack = _get_ack(pkt) + if ack then self.acks.burn_rate = true - else + elseif ack == false then log._debug(log_header .. "burn rate update failed!") end - elseif rplc_pkt.type == RPLC_TYPES.ISS_STATUS then + elseif pkt.type == RPLC_TYPES.ISS_STATUS then -- ISS status packet received, copy data - if rplc_pkt.length == 7 then - local status = pcall(_copy_iss_status, rplc_pkt.data) + if pkt.length == 7 then + local status = pcall(_copy_iss_status, pkt.data) if status then -- copied in ISS status data OK else @@ -291,13 +292,13 @@ function new_session(id, for_reactor, in_queue, out_queue) else log._debug(log_header .. "RPLC ISS status packet length mismatch") end - elseif rplc_pkt.type == RPLC_TYPES.ISS_ALARM then + elseif pkt.type == RPLC_TYPES.ISS_ALARM then -- ISS alarm self.sDB.overridden = true - if rplc_pkt.length == 8 then + if pkt.length == 8 then self.sDB.iss_tripped = true - self.sDB.iss_trip_cause = rplc_pkt.data[1] - local status = pcall(_copy_iss_status, { table.unpack(rplc_pkt.data, 2, #rplc_pkt.length) }) + self.sDB.iss_trip_cause = pkt.data[1] + local status = pcall(_copy_iss_status, { table.unpack(pkt.data, 2, #pkt.length) }) if status then -- copied in ISS status data OK else @@ -307,21 +308,30 @@ function new_session(id, for_reactor, in_queue, out_queue) else log._debug(log_header .. "RPLC ISS alarm packet length mismatch") end - elseif rplc_pkt.type == RPLC_TYPES.ISS_CLEAR then + elseif pkt.type == RPLC_TYPES.ISS_CLEAR then -- ISS clear acknowledgement - if _get_ack(rplc_pkt) then + local ack = _get_ack(pkt) + if ack then self.acks.iss_tripped = true self.sDB.iss_tripped = false self.sDB.iss_trip_cause = "ok" - else + elseif ack == false then log._debug(log_header .. "ISS clear failed") end else - log._debug(log_header .. "handler received unsupported RPLC packet type " .. rplc_pkt.type) + log._debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) + end + elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + if pkt.type == SCADA_MGMT_TYPES.CLOSE then + -- close the session + self.connected = false + else + log._debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end end end + -- send an RPLC packet local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() @@ -333,18 +343,38 @@ function new_session(id, for_reactor, in_queue, out_queue) self.seq_num = self.seq_num + 1 end + -- send a SCADA management packet + local _send_mgmt = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + + self.out_q.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end + -- PUBLIC FUNCTIONS -- + -- get the session ID local get_id = function () return self.id end + -- get the session database local get_db = function () return self.sDB end - local close = function () self.connected = false end + -- close the connection + local close = function () + self.connected = false + _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + end + -- check if a timer matches this session's watchdog local check_wd = function (timer) return timer == self.plc_conn_watchdog.get_timer() end + -- get the reactor structure local get_struct = function () if self.received_struct then return self.sDB.mek_struct @@ -353,6 +383,7 @@ function new_session(id, for_reactor, in_queue, out_queue) end end + -- iterate the session local iterate = function () if self.connected then ------------------ @@ -361,7 +392,7 @@ function new_session(id, for_reactor, in_queue, out_queue) local handle_start = util.time() - while self.in_q.ready() do + while self.in_q.ready() and self.connected do -- get a new message to process local message = self.in_q.pop() @@ -406,6 +437,12 @@ function new_session(id, for_reactor, in_queue, out_queue) end end + -- exit if connection was closed + if not self.connected then + log._info(log_header .. "session closed by remote host") + return false + end + ---------------------- -- update periodics -- ---------------------- diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 6304384..f25427e 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -24,39 +24,33 @@ function link_modem(modem) self.modem = modem end -function alloc_reactor_plcs(num_reactors) - self.num_reactors = num_reactors - for i = 1, num_reactors do - table.insert(self.plc_sessions, false) +-- find a session by the remote port +function find_session(,remote_port) + -- check RTU sessions + for i = 1, #self.rtu_sessions do + if self.rtu_sessions[i].r_port == remote_port then + return self.rtu_sessions[i] + end end -end -function find_session(stype, remote_port) - if stype == SESSION_TYPE.RTU_SESSION then - for i = 1, #self.rtu_sessions do - if self.rtu_sessions[i].r_port == remote_port then - return self.rtu_sessions[i] - end + -- check PLC sessions + for i = 1, #self.plc_sessions do + if self.plc_sessions[i].r_port == remote_port then + return self.plc_sessions[i] end - elseif stype == SESSION_TYPE.PLC_SESSION then - for i = 1, #self.plc_sessions do - if self.plc_sessions[i].r_port == remote_port then - return self.plc_sessions[i] - end + end + + -- check coordinator sessions + for i = 1, #self.coord_sessions do + if self.coord_sessions[i].r_port == remote_port then + return self.coord_sessions[i] end - elseif stype == SESSION_TYPE.COORD_SESSION then - for i = 1, #self.coord_sessions do - if self.coord_sessions[i].r_port == remote_port then - return self.coord_sessions[i] - end - end - else - log._error("cannot search for unknown session type " .. stype, true) end return nil end +-- get a session by reactor ID function get_reactor_session(reactor) local session = nil @@ -69,6 +63,7 @@ function get_reactor_session(reactor) return session end +-- establish a new PLC session function establish_plc_session(local_port, remote_port, for_reactor) if get_reactor_session(for_reactor) == nil then local plc_s = { @@ -96,6 +91,7 @@ function establish_plc_session(local_port, remote_port, for_reactor) end end +-- check if a watchdog timer event matches that of one of the provided sessions local function _check_watchdogs(sessions, timer_event) for i = 1, #sessions do local session = sessions[i] @@ -110,6 +106,7 @@ local function _check_watchdogs(sessions, timer_event) end end +-- attempt to identify which session's watchdog timer fired function check_all_watchdogs(timer_event) -- check RTU session watchdogs _check_watchdogs(self.rtu_sessions, timer_event) @@ -121,6 +118,7 @@ function check_all_watchdogs(timer_event) _check_watchdogs(self.coord_sessions, timer_event) end +-- iterate all the given sessions local function _iterate(sessions) for i = 1, #sessions do local session = sessions[i] @@ -142,6 +140,7 @@ local function _iterate(sessions) end end +-- iterate all sessions function iterate_all() -- iterate RTU sessions _iterate(self.rtu_sessions) @@ -153,6 +152,7 @@ function iterate_all() _iterate(self.coord_sessions) end +-- delete any closed sessions local function _free_closed(sessions) local move_to = 1 for i = 1, #sessions do @@ -172,6 +172,7 @@ local function _free_closed(sessions) end end +-- delete all closed sessions function free_all_closed() -- free closed RTU sessions _free_closed(self.rtu_sessions) @@ -182,3 +183,25 @@ function free_all_closed() -- free closed coordinator sessions _free_closed(self.coord_sessions) end + +-- close connections +local function _close(sessions) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + session.open = false + session.instance.close() + end + end +end + +-- close all open connections +function close_all() + -- close sessions + _close(self.rtu_sessions) + _close(self.plc_sessions) + _close(self.coord_sessions) + + -- free sessions + free_all_closed() +end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index dceeeb9..43599c7 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.10" +local SUPERVISOR_VERSION = "alpha-v0.1.11" local print = util.print local println = util.println @@ -102,7 +102,10 @@ while true do -- check for termination request if event == "terminate" or ppm.should_terminate() then - log._info("terminate requested, exiting...") + println_ts("closing sessions...") + log._info("terminate requested, closing sessions...") + svsessions.close_all() + log._info("sessions closed") break end end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index b8addc2..9f8a3a6 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -117,11 +117,13 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) -- device (RTU/PLC) listening channel if l_port == self.dev_listen then + -- look for an associated session + local session = svsessions.find_session(r_port) + if protocol == PROTOCOLS.MODBUS_TCP then -- MODBUS response elseif protocol == PROTOCOLS.RPLC then -- reactor PLC packet - local session = svsessions.find_session(SESSION_TYPE.PLC_SESSION, r_port) if session then if packet.type == RPLC_TYPES.LINK_REQ then -- new device on this port? that's a collision @@ -158,6 +160,10 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) end elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet + if session then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + end else log._debug("illegal packet type " .. protocol .. " on device listening channel") end From b28020144650618577607ff0dbe6f3bc86f5d645 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 11:44:10 -0400 Subject: [PATCH 070/168] #41 cancel session watchdog timer --- scada-common/util.lua | 6 ++++++ supervisor/session/plc.lua | 1 + 2 files changed, 7 insertions(+) diff --git a/scada-common/util.lua b/scada-common/util.lua index 36761fd..bcab1b5 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -81,6 +81,12 @@ function new_watchdog(timeout) self._wd_timer = os.startTimer(self._timeout) end + local cancel = function () + if self._wd_timer ~= nil then + os.cancelTimer(self._wd_timer) + end + end + return { get_timer = get_timer, feed = feed diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 810d8df..6f8cb5f 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -365,6 +365,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- close the connection local close = function () + self.plc_conn_watchdod.cancel() self.connected = false _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) end From 5ce3f84dfa03fd0a50ff7fcec26cfc31ef415ef2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 12:06:04 -0400 Subject: [PATCH 071/168] #41 PLC connection closing --- reactor-plc/plc.lua | 52 ++++++++++++++++++++++++++++++++++------- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 22 ++++++++++++++--- 3 files changed, 64 insertions(+), 12 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 52bd16c..5aabaae 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -197,6 +197,7 @@ end function comms_init(id, modem, local_port, server_port, reactor, iss) local self = { id = id, + open = false, seq_num = 0, r_seq_num = nil, modem = modem, @@ -218,14 +219,29 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- PRIVATE FUNCTIONS -- local _send = function (msg_type, msg) - local s_pkt = comms.scada_packet() - local r_pkt = comms.rplc_packet() + if self.open then + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() - r_pkt.make(self.id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + r_pkt.make(self.id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end + end + + local _send_mgmt = function (msg_type, msg) + if self.open then + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, m_pkt.raw_sendable()) + + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end end -- variable reactor status information, excluding heating rate @@ -425,6 +441,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.r_seq_num = packet.scada_frame.seq_num() end + -- mark connection as open + self.open = true + -- feed the watchdog first so it doesn't uhh...eat our packets conn_watchdog.feed() @@ -555,19 +574,34 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) log._debug("discarding non-link packet before linked") end elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - -- @todo + -- handle session close + if packet.type == SCADA_MGMT_TYPES.CLOSE then + self.open = false + conn_watchdog.cancel() + unlink() + else + log._warning("received unknown SCADA_MGMT packet type " .. packet.type) + end end end end local is_scrammed = function () return self.scrammed end local is_linked = function () return self.linked end + local is_closed = function () return not self.open end local unlink = function () self.linked = false self.r_seq_num = nil end + local close = function () + self.open = false + conn_watchdog.cancel() + unlink() + _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + end + return { reconnect_modem = reconnect_modem, reconnect_reactor = reconnect_reactor, @@ -579,6 +613,8 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) send_iss_alarm = send_iss_alarm, is_scrammed = is_scrammed, is_linked = is_linked, - unlink = unlink + is_closed = is_closed, + unlink = unlink, + close = close } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 17e4a2e..8070ea1 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.13" +local R_PLC_VERSION = "alpha-v0.4.14" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 36d3179..3e9ee2e 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -169,9 +169,11 @@ function thread__main(smem, init) -- check for termination request if event == "terminate" or ppm.should_terminate() then + log._info("terminate requested, main thread exiting") -- iss handles reactor shutdown plc_state.shutdown = true - log._info("terminate requested, main thread exiting") + -- close connection + plc_comms.close() break end end @@ -195,6 +197,7 @@ function thread__iss(smem) local iss_queue = smem.q.mq_iss + local was_closed = true local last_update = util.time() -- thread loop @@ -203,6 +206,19 @@ function thread__iss(smem) -- ISS checks if plc_state.init_ok then + -- SCRAM if no open connection + if networked and plc_comms.is_closed() then + plc_state.scram = true + if not was_closed then + was_closed = true + iss.trip_timeout() + println_ts("server connection closed by remote host") + log._warning("server connection closed by remote host") + end + else + was_closed = false + end + -- if we tried to SCRAM but failed, keep trying -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) if not plc_state.no_reactor and plc_state.scram and reactor.getStatus() then @@ -217,13 +233,13 @@ function thread__iss(smem) end -- check safety (SCRAM occurs if tripped) - if not plc_state.degraded then + if not plc_state.no_reactor then local iss_tripped, iss_status_string, iss_first = iss.check() plc_state.scram = plc_state.scram or iss_tripped if iss_first then println_ts("[ISS] SCRAM! safety trip: " .. iss_status_string) - if networked then + if networked and not plc_state.no_modem then plc_comms.send_iss_alarm(iss_status_string) end end From 4bc50e4bada3cc7302c4ad159e0214e793d3c552 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 13:15:08 -0400 Subject: [PATCH 072/168] #41 RTU comms closing --- rtu/rtu.lua | 12 +++++++++++- rtu/startup.lua | 2 +- rtu/threads.lua | 1 + 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index a35770d..fc32fac 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -249,6 +249,10 @@ function rtu_comms(modem, local_port, server_port) send_modbus(reply) elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet + if packet.type == SCADA_MGMT_TYPES.CLOSE then + -- close connection + conn_watchdog.cancel() + unlink(rtu_state) if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then -- acknowledgement rtu_state.linked = true @@ -317,6 +321,11 @@ function rtu_comms(modem, local_port, server_port) self.r_seq_num = nil end + local close = function (rtu_state) + unlink(rtu_state) + _send(SCADA_MGMT_TYPES.CLOSE, {}) + end + return { send_modbus = send_modbus, reconnect_modem = reconnect_modem, @@ -324,6 +333,7 @@ function rtu_comms(modem, local_port, server_port) handle_packet = handle_packet, send_advertisement = send_advertisement, send_heartbeat = send_heartbeat, - unlink = unlink + unlink = unlink, + close = close } end diff --git a/rtu/startup.lua b/rtu/startup.lua index 59c4354..e73eab6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.9" +local RTU_VERSION = "alpha-v0.4.10" local print = util.print local println = util.println diff --git a/rtu/threads.lua b/rtu/threads.lua index c5af330..9cb2923 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -169,6 +169,7 @@ function thread__comms(smem) -- check for termination request if rtu_state.shutdown then + rtu_comms.close() log._info("comms thread exiting") break end From c19a58380c3cb4edd7e0f9b59d5fdf9f8de33f35 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 17:33:54 -0400 Subject: [PATCH 073/168] fixed cancel not being exposed for watchdog functions --- scada-common/util.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index bcab1b5..a226e9f 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -89,6 +89,7 @@ function new_watchdog(timeout) return { get_timer = get_timer, - feed = feed + feed = feed, + cancel = cancel } end From 1ac4de65a90b7581e7a4143b843ed9722bc1784a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 17:34:24 -0400 Subject: [PATCH 074/168] added close to valid scada types and fixed length checks for packet decoders --- scada-common/comms.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 998e914..581e493 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -303,7 +303,7 @@ function mgmt_packet() -- check that type is known local _scada_type_valid = function () return self.type == SCADA_MGMT_TYPES.PING or - self.type == SCADA_MGMT_TYPES.SV_HEARTBEAT or + self.type == SCADA_MGMT_TYPES.CLOSE or self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or self.type == SCADA_MGMT_TYPES.RTU_ADVERT or self.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT @@ -329,7 +329,7 @@ function mgmt_packet() self.frame = frame if frame.protocol() == PROTOCOLS.SCADA_MGMT then - local ok = #data >= 1 + local ok = frame.length() >= 1 if ok then local data = frame.data() @@ -408,7 +408,7 @@ function coord_packet() self.frame = frame if frame.protocol() == PROTOCOLS.COORD_DATA then - local ok = #data >= 1 + local ok = frame.length() >= 1 if ok then local data = frame.data() @@ -487,7 +487,7 @@ function capi_packet() self.frame = frame if frame.protocol() == PROTOCOLS.COORD_API then - local ok = #data >= 1 + local ok = frame.length() >= 1 if ok then local data = frame.data() From e3e370d3ab8de00488e0724694282dd6a31d3a9e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 17:34:57 -0400 Subject: [PATCH 075/168] fixed RTU mgmt_pkt reference --- rtu/rtu.lua | 2 +- rtu/startup.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index fc32fac..b9a3061 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -184,7 +184,7 @@ function rtu_comms(modem, local_port, server_port) elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then - pkt = mgmt_packet.get() + pkt = mgmt_pkt.get() end else log._error("illegal packet type " .. s_pkt.protocol(), true) diff --git a/rtu/startup.lua b/rtu/startup.lua index e73eab6..20177e6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,7 +19,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.10" +local RTU_VERSION = "alpha-v0.4.11" local print = util.print local println = util.println From 574b85e1772be0ec3dee47bbf3eaa22fd8e7c94a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 17:40:00 -0400 Subject: [PATCH 076/168] PLC bugfixes and #37 optimized status packets and structure packets --- reactor-plc/plc.lua | 235 ++++++++++++++++++++++------------------ reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 19 ++-- 3 files changed, 139 insertions(+), 117 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5aabaae..a1e5225 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -5,6 +5,7 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local RPLC_LINKING = comms.RPLC_LINKING +local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local print = util.print local println = util.println @@ -197,7 +198,6 @@ end function comms_init(id, modem, local_port, server_port, reactor, iss) local self = { id = id, - open = false, seq_num = 0, r_seq_num = nil, modem = modem, @@ -219,71 +219,83 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- PRIVATE FUNCTIONS -- local _send = function (msg_type, msg) - if self.open then - local s_pkt = comms.scada_packet() - local r_pkt = comms.rplc_packet() + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() - r_pkt.make(self.id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + r_pkt.make(self.id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 - end + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 end local _send_mgmt = function (msg_type, msg) - if self.open then - local s_pkt = comms.scada_packet() - local m_pkt = comms.mgmt_packet() + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() - m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, m_pkt.raw_sendable()) + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 - end + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 end -- variable reactor status information, excluding heating rate local _reactor_status = function () - local coolant = self.reactor.getCoolant() - local coolant_name = "" - local coolant_amnt = 0 + local coolant = nil + local hcoolant = nil - local hcoolant = self.reactor.getHeatedCoolant() - local hcoolant_name = "" - local hcoolant_amnt = 0 + local data_table = { + false, -- getStatus + 0, -- getBurnRate + 0, -- getActualBurnRate + 0, -- getTemperature + 0, -- getDamagePercent + 0, -- getBoilEfficiency + 0, -- getEnvironmentalLoss + 0, -- getFuel + 0, -- getFuelFilledPercentage + 0, -- getWaste + 0, -- getWasteFilledPercentage + "", -- coolant_name + 0, -- coolant_amnt + 0, -- getCoolantFilledPercentage + "", -- hcoolant_name + 0, -- hcoolant_amnt + 0 -- getHeatedCoolantFilledPercentage + } + + local tasks = { + function () data_table[1] = self.reactor.getStatus() end, + function () data_table[2] = self.reactor.getBurnRate() end, + function () data_table[3] = self.reactor.getActualBurnRate() end, + function () data_table[4] = self.reactor.getTemperature() end, + function () data_table[5] = self.reactor.getDamagePercent() end, + function () data_table[6] = self.reactor.getBoilEfficiency() end, + function () data_table[7] = self.reactor.getEnvironmentalLoss() end, + function () data_table[8] = self.reactor.getFuel() end, + function () data_table[9] = self.reactor.getFuelFilledPercentage() end, + function () data_table[10] = self.reactor.getWaste() end, + function () data_table[11] = self.reactor.getWasteFilledPercentage() end, + function () coolant = self.reactor.getCoolant() end, + function () data_table[14] = self.reactor.getCoolantFilledPercentage() end, + function () hcoolant = self.reactor.getHeatedCoolant() end, + function () data_table[17] = self.reactor.getHeatedCoolantFilledPercentage() end + } + + parallel.waitForAll(table.unpack(tasks)) if coolant ~= nil then - coolant_name = coolant.name - coolant_amnt = coolant.amount + data_table[12] = coolant.name + data_table[13] = coolant.amount end if hcoolant ~= nil then - hcoolant_name = hcoolant.name - hcoolant_amnt = hcoolant.amount + data_table[15] = hcoolant.name + data_table[16] = hcoolant.amount end - return { - self.reactor.getStatus(), - self.reactor.getBurnRate(), - self.reactor.getActualBurnRate(), - self.reactor.getTemperature(), - self.reactor.getDamagePercent(), - self.reactor.getBoilEfficiency(), - self.reactor.getEnvironmentalLoss(), - - self.reactor.getFuel(), - self.reactor.getFuelFilledPercentage(), - self.reactor.getWaste(), - self.reactor.getWasteFilledPercentage(), - coolant_name, - coolant_amnt, - self.reactor.getCoolantFilledPercentage(), - hcoolant_name, - hcoolant_amnt, - self.reactor.getHeatedCoolantFilledPercentage() - }, self.reactor.__p_is_faulted() + return data_table, self.reactor.__p_is_faulted() end local _update_status_cache = function () @@ -320,20 +332,23 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send(msg_type, { succeeded }) end - -- send structure properties (these should not change) - -- (server will cache these) + -- send structure properties (these should not change, server will cache these) local _send_struct = function () - local mek_data = { - self.reactor.getHeatCapacity(), - self.reactor.getFuelAssemblies(), - self.reactor.getFuelSurfaceArea(), - self.reactor.getFuelCapacity(), - self.reactor.getWasteCapacity(), - self.reactor.getCoolantCapacity(), - self.reactor.getHeatedCoolantCapacity(), - self.reactor.getMaxBurnRate() + local mek_data = { 0, 0, 0, 0, 0, 0, 0, 0 } + + local tasks = { + function () mek_data[1] = self.reactor.getHeatCapacity() end, + function () mek_data[2] = self.reactor.getFuelAssemblies() end, + function () mek_data[3] = self.reactor.getFuelSurfaceArea() end, + function () mek_data[4] = self.reactor.getFuelCapacity() end, + function () mek_data[5] = self.reactor.getWasteCapacity() end, + function () mek_data[6] = self.reactor.getCoolantCapacity() end, + function () mek_data[7] = self.reactor.getHeatedCoolantCapacity() end, + function () mek_data[8] = self.reactor.getMaxBurnRate() end } + parallel.waitForAll(table.unpack(tasks)) + if not self.reactor.__p_is_faulted() then _send(RPLC_TYPES.MEK_STRUCT, mek_data) else @@ -359,6 +374,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _update_status_cache() end + -- unlink from the server + local unlink = function () + self.linked = false + self.r_seq_num = nil + end + + -- close the connection to the server + local close = function (conn_watchdog) + conn_watchdog.cancel() + unlink() + _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + end + -- attempt to establish link with supervisor local send_link_req = function () _send(RPLC_TYPES.LINK_REQ, { self.id }) @@ -366,37 +394,47 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- send live status information local send_status = function (degraded) - local mek_data = nil + if self.linked then + local mek_data = nil - if _update_status_cache() then - mek_data = self.status_cache + if _update_status_cache() then + mek_data = self.status_cache + end + + local sys_status = { + util.time(), -- timestamp + (not self.scrammed), -- enabled + iss.is_tripped(), -- overridden + degraded, -- degraded + self.reactor.getHeatingRate(), -- heating rate + mek_data -- mekanism status data + } + + if not self.reactor.__p_is_faulted() then + _send(RPLC_TYPES.STATUS, sys_status) + else + log._error("failed to send status: PPM fault") + end end - - local sys_status = { - util.time(), -- timestamp - (not self.scrammed), -- enabled - iss.is_tripped(), -- overridden - degraded, -- degraded - self.reactor.getHeatingRate(), -- heating rate - mek_data -- mekanism status data - } - - _send(RPLC_TYPES.STATUS, sys_status) end -- send safety system status local send_iss_status = function () - _send(RPLC_TYPES.ISS_STATUS, iss.status()) + if self.linked then + _send(RPLC_TYPES.ISS_STATUS, iss.status()) + end end -- send safety system alarm local send_iss_alarm = function (cause) - local iss_alarm = { - cause, - table.unpack(iss.status()) - } + if self.linked then + local iss_alarm = { + cause, + table.unpack(iss.status()) + } - _send(RPLC_TYPES.ISS_ALARM, iss_alarm) + _send(RPLC_TYPES.ISS_ALARM, iss_alarm) + end end -- parse an RPLC packet @@ -418,7 +456,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then - pkt = mgmt_packet.get() + pkt = mgmt_pkt.get() end else log._error("illegal packet type " .. s_pkt.protocol(), true) @@ -441,9 +479,6 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.r_seq_num = packet.scada_frame.seq_num() end - -- mark connection as open - self.open = true - -- feed the watchdog first so it doesn't uhh...eat our packets conn_watchdog.feed() @@ -464,7 +499,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_keep_alive_ack(timestamp) else - log._debug(log_header .. "RPLC keep alive packet length mismatch") + log._debug("RPLC keep alive packet length mismatch") end elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation @@ -490,11 +525,12 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.linked = link_ack == RPLC_LINKING.ALLOW else - log._debug(log_header .. "RPLC link req packet length mismatch") + log._debug("RPLC link req packet length mismatch") end elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure _send_struct() + log._debug("sent out structure again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_SCRAM then -- disable the reactor self.scrammed = true @@ -530,7 +566,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_ack(packet.type, success) else - log._debug(log_header .. "RPLC set burn rate packet length mismatch") + log._debug("RPLC set burn rate packet length mismatch") end elseif packet.type == RPLC_TYPES.ISS_CLEAR then -- clear the ISS status @@ -568,7 +604,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.linked = link_ack == RPLC_LINKING.ALLOW else - log._debug(log_header .. "RPLC link req packet length mismatch") + log._debug("RPLC link req packet length mismatch") end else log._debug("discarding non-link packet before linked") @@ -576,9 +612,10 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then -- handle session close if packet.type == SCADA_MGMT_TYPES.CLOSE then - self.open = false conn_watchdog.cancel() unlink() + println_ts("server connection closed by remote host") + log._warning("server connection closed by remote host") else log._warning("received unknown SCADA_MGMT packet type " .. packet.type) end @@ -588,33 +625,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local is_scrammed = function () return self.scrammed end local is_linked = function () return self.linked end - local is_closed = function () return not self.open end - - local unlink = function () - self.linked = false - self.r_seq_num = nil - end - - local close = function () - self.open = false - conn_watchdog.cancel() - unlink() - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) - end return { reconnect_modem = reconnect_modem, reconnect_reactor = reconnect_reactor, - parse_packet = parse_packet, - handle_packet = handle_packet, + unlink = unlink, + close = close, send_link_req = send_link_req, send_status = send_status, send_iss_status = send_iss_status, send_iss_alarm = send_iss_alarm, + parse_packet = parse_packet, + handle_packet = handle_packet, is_scrammed = is_scrammed, - is_linked = is_linked, - is_closed = is_closed, - unlink = unlink, - close = close + is_linked = is_linked } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 8070ea1..cd68962 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -12,7 +12,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.4.14" +local R_PLC_VERSION = "alpha-v0.5.0" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 3e9ee2e..ee23710 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -170,10 +170,10 @@ function thread__main(smem, init) -- check for termination request if event == "terminate" or ppm.should_terminate() then log._info("terminate requested, main thread exiting") + -- close connection + plc_comms.close(conn_watchdog) -- iss handles reactor shutdown plc_state.shutdown = true - -- close connection - plc_comms.close() break end end @@ -197,7 +197,7 @@ function thread__iss(smem) local iss_queue = smem.q.mq_iss - local was_closed = true + local was_linked = false local last_update = util.time() -- thread loop @@ -207,16 +207,15 @@ function thread__iss(smem) -- ISS checks if plc_state.init_ok then -- SCRAM if no open connection - if networked and plc_comms.is_closed() then + if networked and not plc_comms.is_linked() then plc_state.scram = true - if not was_closed then - was_closed = true + if was_linked then + was_linked = false iss.trip_timeout() - println_ts("server connection closed by remote host") - log._warning("server connection closed by remote host") end else - was_closed = false + -- would do elseif not networked but there is no reason to do that extra operation + was_linked = true end -- if we tried to SCRAM but failed, keep trying @@ -418,7 +417,7 @@ end function thread__setpoint_control(smem) -- execute thread local exec = function () - log._debug("comms rx thread start") + log._debug("setpoint control thread start") -- load in from shared memory local plc_state = smem.plc_state From 62b4b63f4a0c62e53a45b9974ad5b730a4613e2a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 2 May 2022 17:43:23 -0400 Subject: [PATCH 077/168] supervisor PLC session bugfixes --- supervisor/session/plc.lua | 60 ++++++++++++++++--------------- supervisor/session/svsessions.lua | 12 ++++++- supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 2 +- 4 files changed, 44 insertions(+), 32 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 6f8cb5f..b4fbdd9 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -5,6 +5,7 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES +local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -- retry time constants in ms local INITIAL_WAIT = 1500 @@ -46,7 +47,7 @@ function new_session(id, for_reactor, in_queue, out_queue) }, -- when to next retry one of these requests retry_times = { - struct_req = 500, + struct_req = (util.time() + 500), scram_req = 0, enable_req = 0, burn_rate_req = 0, @@ -167,6 +168,30 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.mek_struct.max_burn = mek_data[8] end + -- send an RPLC packet + local _send = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local r_pkt = comms.rplc_packet() + + r_pkt.make(self.id, msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + + self.out_q.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end + + -- send a SCADA management packet + local _send_mgmt = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + + self.out_q.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end + -- get an ACK status local _get_ack = function (pkt) if pkt.length == 1 then @@ -246,6 +271,7 @@ function new_session(id, for_reactor, in_queue, out_queue) local status = pcall(_copy_struct, pkt.data) if status then -- copied in structure data OK + self.received_struct = true else -- error copying structure data log._error(log_header .. "failed to parse struct packet data") @@ -331,30 +357,6 @@ function new_session(id, for_reactor, in_queue, out_queue) end end - -- send an RPLC packet - local _send = function (msg_type, msg) - local s_pkt = comms.scada_packet() - local r_pkt = comms.rplc_packet() - - r_pkt.make(self.id, msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) - - self.out_q.push_packet(s_pkt) - self.seq_num = self.seq_num + 1 - end - - -- send a SCADA management packet - local _send_mgmt = function (msg_type, msg) - local s_pkt = comms.scada_packet() - local m_pkt = comms.mgmt_packet() - - m_pkt.make(msg_type, msg) - s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) - - self.out_q.push_packet(s_pkt) - self.seq_num = self.seq_num + 1 - end - -- PUBLIC FUNCTIONS -- -- get the session ID @@ -365,7 +367,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- close the connection local close = function () - self.plc_conn_watchdod.cancel() + self.plc_conn_watchdog.cancel() self.connected = false _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) end @@ -441,7 +443,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- exit if connection was closed if not self.connected then log._info(log_header .. "session closed by remote host") - return false + return self.connected end ---------------------- @@ -466,13 +468,13 @@ function new_session(id, for_reactor, in_queue, out_queue) -- attempt retries -- --------------------- - local rtimes = self.rtimes + local rtimes = self.retry_times -- struct request retry if not self.received_struct then if rtimes.struct_req - util.time() <= 0 then - _send(RPLC_TYPES.LINK_REQ, {}) + _send(RPLC_TYPES.MEK_STRUCT, {}) rtimes.struct_req = util.time() + RETRY_PERIOD end end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index f25427e..c111093 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -25,7 +25,7 @@ function link_modem(modem) end -- find a session by the remote port -function find_session(,remote_port) +function find_session(remote_port) -- check RTU sessions for i = 1, #self.rtu_sessions do if self.rtu_sessions[i].r_port == remote_port then @@ -191,6 +191,16 @@ local function _close(sessions) if session.open then session.open = false session.instance.close() + + -- send packets in out queue (namely the close packet) + while session.out_queue.ready() do + local msg = session.out_queue.pop() + if msg.qtype == mqueue.TYPE.PACKET then + self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + end + end + + log._debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) end end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 43599c7..fa8b06d 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.11" +local SUPERVISOR_VERSION = "alpha-v0.1.12" local print = util.print local println = util.println diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 9f8a3a6..204e136 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -93,7 +93,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then - pkt = mgmt_packet.get() + pkt = mgmt_pkt.get() end -- get as coordinator packet elseif s_pkt.protocol() == PROTOCOLS.COORD_DATA then From c76200b0e3a3ef821451e788e32b35a0264fd635 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 10:44:18 -0400 Subject: [PATCH 078/168] shared global types --- reactor-plc/plc.lua | 23 +++++++++++++---------- reactor-plc/startup.lua | 1 + rtu/startup.lua | 11 +++++++---- scada-common/types.lua | 20 ++++++++++++++++++++ 4 files changed, 41 insertions(+), 14 deletions(-) create mode 100644 scada-common/types.lua diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index a1e5225..cccced9 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,7 +1,10 @@ +-- #REQUIRES types.lua -- #REQUIRES comms.lua -- #REQUIRES ppm.lua -- #REQUIRES util.lua +local iss_status_t = types.iss_status_t + local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local RPLC_LINKING = comms.RPLC_LINKING @@ -113,7 +116,7 @@ function iss_init(reactor) -- check all safety conditions local check = function () - local status = "ok" + local status = iss_status_t.ok local was_tripped = self.tripped -- update cache @@ -132,32 +135,32 @@ function iss_init(reactor) status = self.trip_cause elseif self.cache[1] then log._warning("ISS: damage critical!") - status = "dmg_crit" + status = iss_status_t.dmg_crit elseif self.cache[4] then log._warning("ISS: high temperature!") - status = "high_temp" + status = iss_status_t.high_temp elseif self.cache[2] then log._warning("ISS: heated coolant backup!") - status = "heated_coolant_backup" + status = iss_status_t.ex_hcoolant elseif self.cache[6] then log._warning("ISS: no coolant!") - status = "no_coolant" + status = iss_status_t.no_coolant elseif self.cache[3] then log._warning("ISS: full waste!") - status = "full_waste" + status = iss_status_t.ex_waste elseif self.cache[5] then log._warning("ISS: no fuel!") - status = "no_fuel" + status = iss_status_t.no_fuel elseif self.cache[7] then log._warning("ISS: supervisor connection timeout!") - status = "timeout" + status = iss_status_t.timeout else self.tripped = false end -- if a new trip occured... local first_trip = false - if not was_tripped and status ~= "ok" then + if not was_tripped and status ~= iss_status_t.ok then log._warning("ISS: reactor SCRAM") first_trip = true @@ -181,7 +184,7 @@ function iss_init(reactor) local reset = function () self.timed_out = false self.tripped = false - self.trip_cause = "" + self.trip_cause = iss_status_t.ok end return { diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index cd68962..5c9df15 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -3,6 +3,7 @@ -- os.loadAPI("scada-common/log.lua") +os.loadAPI("scada-common/types.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") diff --git a/rtu/startup.lua b/rtu/startup.lua index 20177e6..72d1b7e 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -3,6 +3,7 @@ -- os.loadAPI("scada-common/log.lua") +os.loadAPI("scada-common/types.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") @@ -21,6 +22,8 @@ os.loadAPI("dev/turbine_rtu.lua") local RTU_VERSION = "alpha-v0.4.11" +local rtu_t = types.rtu_t + local print = util.print local println = util.println local print_ts = util.print_ts @@ -139,7 +142,7 @@ for reactor_idx = 1, #rtu_redstone do table.insert(units, { name = "redstone_io", - type = "redstone", + type = rtu_t.redstone, index = 1, reactor = rtu_redstone[reactor_idx].for_reactor, device = capabilities, -- use device field for redstone channels @@ -168,15 +171,15 @@ for i = 1, #rtu_devices do if type == "boiler" then -- boiler multiblock - rtu_type = "boiler" + rtu_type = rtu_t.boiler rtu_iface = boiler_rtu.new(device) elseif type == "turbine" then -- turbine multiblock - rtu_type = "turbine" + rtu_type = rtu_t.turbine rtu_iface = turbine_rtu.new(device) elseif type == "mekanismMachine" then -- assumed to be an induction matrix multiblock - rtu_type = "imatrix" + rtu_type = rtu_t.induction_matrix rtu_iface = imatrix_rtu.new(device) else local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")" diff --git a/scada-common/types.lua b/scada-common/types.lua new file mode 100644 index 0000000..072cd9a --- /dev/null +++ b/scada-common/types.lua @@ -0,0 +1,20 @@ +-- Global Types + +rtu_t = { + redstone = "redstone", + boiler = "boiler", + turbine = "turbine", + energy_machine = "emachine", + induction_matrix = "imatrix" +} + +iss_status_t = { + ok = "ok", + dmg_crit = "dmg_crit", + ex_hcoolant = "heated_coolant_backup", + ex_waste = "full_waste", + high_temp = "high_temp", + no_fuel = "no_fuel", + no_coolant = "no_coolant", + timeout = "timeout" +} From e2f7318922774e32e28f2b45c4737a2631d9bdf4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 10:45:35 -0400 Subject: [PATCH 079/168] #27 induction matrix RTU split into two RTUs, supporting pre and post Mekansim 10.1 --- rtu/dev/energymachine_rtu.lua | 33 +++++++++++++++++++++++++++++++++ rtu/dev/imatrix_rtu.lua | 9 +++++++++ rtu/startup.lua | 8 ++++++-- 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 rtu/dev/energymachine_rtu.lua diff --git a/rtu/dev/energymachine_rtu.lua b/rtu/dev/energymachine_rtu.lua new file mode 100644 index 0000000..b3f004a --- /dev/null +++ b/rtu/dev/energymachine_rtu.lua @@ -0,0 +1,33 @@ +-- #REQUIRES rtu.lua + +function new(machine) + local self = { + rtu = rtu.rtu_init(), + machine = machine + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + -- none + + -- input registers -- + -- build properties + self.rtu.connect_input_reg(self.machine.getTotalMaxEnergy) + -- containers + self.rtu.connect_input_reg(self.machine.getTotalEnergy) + self.rtu.connect_input_reg(self.machine.getTotalEnergyNeeded) + self.rtu.connect_input_reg(self.machine.getTotalEnergyFilledPercentage) + + -- holding registers -- + -- none + + return { + rtu_interface = rtu_interface + } +end diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua index 529a1f8..43f49b4 100644 --- a/rtu/dev/imatrix_rtu.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -17,12 +17,21 @@ function new(imatrix) -- none -- input registers -- + -- @todo check these on Mekanism 10.1+ -- build properties + self.rtu.connect_input_reg(self.imatrix.getTransferCap) + self.rtu.connect_input_reg(self.imatrix.getInstalledCells) + self.rtu.connect_input_reg(self.imatrix.getInstalledProviders) self.rtu.connect_input_reg(self.imatrix.getTotalMaxEnergy) -- containers self.rtu.connect_input_reg(self.imatrix.getTotalEnergy) self.rtu.connect_input_reg(self.imatrix.getTotalEnergyNeeded) self.rtu.connect_input_reg(self.imatrix.getTotalEnergyFilledPercentage) + -- additional fields? check these on 10.1 + self.rtu.connect_input_reg(self.imatrix.getInputItem) + self.rtu.connect_input_reg(self.imatrix.getOutputItem) + self.rtu.connect_input_reg(self.imatrix.getLastInput) + self.rtu.connect_input_reg(self.imatrix.getLastOutput) -- holding registers -- -- none diff --git a/rtu/startup.lua b/rtu/startup.lua index 72d1b7e..5f467e4 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -20,7 +20,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.11" +local RTU_VERSION = "alpha-v0.4.12" local rtu_t = types.rtu_t @@ -178,7 +178,11 @@ for i = 1, #rtu_devices do rtu_type = rtu_t.turbine rtu_iface = turbine_rtu.new(device) elseif type == "mekanismMachine" then - -- assumed to be an induction matrix multiblock + -- assumed to be an induction matrix multiblock, pre Mekanism 10.1 + rtu_type = rtu_t.energy_machine + rtu_iface = energymachine_rtu.new(device) + elseif type == "inductionMatrix" then + -- induction matrix multiblock, post Mekanism 10.1 rtu_type = rtu_t.induction_matrix rtu_iface = imatrix_rtu.new(device) else From dc1c1db5e6bdee90c8215e7eb0818fc515e8f63d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 11:27:40 -0400 Subject: [PATCH 080/168] MODBUS bugfixes, utilize new types file --- scada-common/modbus.lua | 83 +++++++++++++++++---------------------- scada-common/types.lua | 29 ++++++++++++++ supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 1 - 4 files changed, 66 insertions(+), 49 deletions(-) diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index a2a1cc3..dba239d 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -1,47 +1,25 @@ --- #REQUIRES comms.lua +-- #REQUIRES types.lua --- modbus function codes -local MODBUS_FCODE = { - READ_COILS = 0x01, - READ_DISCRETE_INPUTS = 0x02, - READ_MUL_HOLD_REGS = 0x03, - READ_INPUT_REGS = 0x04, - WRITE_SINGLE_COIL = 0x05, - WRITE_SINGLE_HOLD_REG = 0x06, - WRITE_MUL_COILS = 0x0F, - WRITE_MUL_HOLD_REGS = 0x10, - ERROR_FLAG = 0x80 -} - --- modbus exception codes -local MODBUS_EXCODE = { - ILLEGAL_FUNCTION = 0x01, - ILLEGAL_DATA_ADDR = 0x02, - ILLEGAL_DATA_VALUE = 0x03, - SERVER_DEVICE_FAIL = 0x04, - ACKNOWLEDGE = 0x05, - SERVER_DEVICE_BUSY = 0x06, - NEG_ACKNOWLEDGE = 0x07, - MEMORY_PARITY_ERROR = 0x08, - GATEWAY_PATH_UNAVAILABLE = 0x0A, - GATEWAY_TARGET_TIMEOUT = 0x0B -} +local MODBUS_FCODE = types.MODBUS_FCODE +local MODBUS_EXCODE = types.MODBUS_EXCODE -- new modbus comms handler object -function new(rtu_dev) +function new(rtu_dev, use_parallel_read) local self = { - rtu = rtu_dev + rtu = rtu_dev, + use_parallel = use_parallel_read } local _1_read_coils = function (c_addr_start, count) local readings = {} local access_fault = false local _, coils, _, _ = self.rtu.io_count() - local return_ok = (c_addr_start + count) <= coils + local return_ok = ((c_addr_start + count) <= coils) and (count > 0) if return_ok then - for i = 0, (count - 1) do - readings[i], access_fault = self.rtu.read_coil(c_addr_start + i) + for i = 1, count do + local addr = c_addr_start + i - 1 + readings[i], access_fault = self.rtu.read_coil(addr) if access_fault then return_ok = false @@ -60,11 +38,12 @@ function new(rtu_dev) local readings = {} local access_fault = false local discrete_inputs, _, _, _ = self.rtu.io_count() - local return_ok = (di_addr_start + count) <= discrete_inputs + local return_ok = ((di_addr_start + count) <= discrete_inputs) and (count > 0) if return_ok then - for i = 0, (count - 1) do - readings[i], access_fault = self.rtu.read_di(di_addr_start + i) + for i = 1, count do + local addr = di_addr_start + i - 1 + readings[i], access_fault = self.rtu.read_di(addr) if access_fault then return_ok = false @@ -83,11 +62,12 @@ function new(rtu_dev) local readings = {} local access_fault = false local _, _, _, hold_regs = self.rtu.io_count() - local return_ok = (hr_addr_start + count) <= hold_regs + local return_ok = ((hr_addr_start + count) <= hold_regs) and (count > 0) if return_ok then - for i = 0, (count - 1) do - readings[i], access_fault = self.rtu.read_holding_reg(hr_addr_start + i) + for i = 1, count do + local addr = hr_addr_start + i - 1 + readings[i], access_fault = self.rtu.read_holding_reg(addr) if access_fault then return_ok = false @@ -106,11 +86,12 @@ function new(rtu_dev) local readings = {} local access_fault = false local _, _, input_regs, _ = self.rtu.io_count() - local return_ok = (ir_addr_start + count) <= input_regs + local return_ok = ((ir_addr_start + count) <= input_regs) and (count > 0) if return_ok then - for i = 0, (count - 1) do - readings[i], access_fault = self.rtu.read_input_reg(ir_addr_start + i) + for i = 1, count do + local addr = ir_addr_start + i - 1 + readings[i], access_fault = self.rtu.read_input_reg(addr) if access_fault then return_ok = false @@ -156,6 +137,8 @@ function new(rtu_dev) return_ok = false readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL end + else + response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok @@ -165,11 +148,12 @@ function new(rtu_dev) local response = nil local _, coils, _, _ = self.rtu.io_count() local count = #values - local return_ok = (c_addr_start + count) <= coils + local return_ok = ((c_addr_start + count) <= coils) and (count > 0) if return_ok then - for i = 0, (count - 1) do - local access_fault = self.rtu.write_coil(c_addr_start + i, values[i + 1]) + for i = 1, count do + local addr = c_addr_start + i - 1 + local access_fault = self.rtu.write_coil(addr, values[i]) if access_fault then return_ok = false @@ -177,6 +161,8 @@ function new(rtu_dev) break end end + else + response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, response @@ -186,11 +172,12 @@ function new(rtu_dev) local response = nil local _, _, _, hold_regs = self.rtu.io_count() local count = #values - local return_ok = (hr_addr_start + count) <= hold_regs + local return_ok = ((hr_addr_start + count) <= hold_regs) and (count > 0) if return_ok then - for i = 0, (count - 1) do - local access_fault = self.rtu.write_coil(hr_addr_start + i, values[i + 1]) + for i = 1, count do + local addr = hr_addr_start + i - 1 + local access_fault = self.rtu.write_coil(addr, values[i]) if access_fault then return_ok = false @@ -198,6 +185,8 @@ function new(rtu_dev) break end end + else + response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, response diff --git a/scada-common/types.lua b/scada-common/types.lua index 072cd9a..866982a 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -18,3 +18,32 @@ iss_status_t = { no_coolant = "no_coolant", timeout = "timeout" } + +-- MODBUS + +-- modbus function codes +local MODBUS_FCODE = { + READ_COILS = 0x01, + READ_DISCRETE_INPUTS = 0x02, + READ_MUL_HOLD_REGS = 0x03, + READ_INPUT_REGS = 0x04, + WRITE_SINGLE_COIL = 0x05, + WRITE_SINGLE_HOLD_REG = 0x06, + WRITE_MUL_COILS = 0x0F, + WRITE_MUL_HOLD_REGS = 0x10, + ERROR_FLAG = 0x80 +} + +-- modbus exception codes +local MODBUS_EXCODE = { + ILLEGAL_FUNCTION = 0x01, + ILLEGAL_DATA_ADDR = 0x02, + ILLEGAL_DATA_VALUE = 0x03, + SERVER_DEVICE_FAIL = 0x04, + ACKNOWLEDGE = 0x05, + SERVER_DEVICE_BUSY = 0x06, + NEG_ACKNOWLEDGE = 0x07, + MEMORY_PARITY_ERROR = 0x08, + GATEWAY_PATH_UNAVAILABLE = 0x0A, + GATEWAY_TARGET_TIMEOUT = 0x0B +} diff --git a/supervisor/startup.lua b/supervisor/startup.lua index fa8b06d..8d174d4 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -3,10 +3,10 @@ -- os.loadAPI("scada-common/log.lua") +os.loadAPI("scada-common/types.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") -os.loadAPI("scada-common/modbus.lua") os.loadAPI("scada-common/mqueue.lua") os.loadAPI("config.lua") diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 204e136..edda143 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -1,5 +1,4 @@ -- #REQUIRES comms.lua --- #REQUIRES modbus.lua -- #REQUIRES mqueue.lua -- #REQUIRES util.lua -- #REQUIRES svsessions.lua From 635c70cffddea08960c8ee84dc995bc10a0a38d6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 11:28:29 -0400 Subject: [PATCH 081/168] moved MODBUS file thanks to utilizing types file --- {scada-common => rtu}/modbus.lua | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {scada-common => rtu}/modbus.lua (100%) diff --git a/scada-common/modbus.lua b/rtu/modbus.lua similarity index 100% rename from scada-common/modbus.lua rename to rtu/modbus.lua From 665b33fa054b2d9149a6ff3f0f964ebe8bdca218 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 11:39:03 -0400 Subject: [PATCH 082/168] #42 parallel RTU reads --- rtu/modbus.lua | 92 ++++++++++++++++++++++++++++++++++++++++++++----- rtu/startup.lua | 8 ++--- 2 files changed, 88 insertions(+), 12 deletions(-) diff --git a/rtu/modbus.lua b/rtu/modbus.lua index dba239d..7ea6108 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -11,6 +11,7 @@ function new(rtu_dev, use_parallel_read) } local _1_read_coils = function (c_addr_start, count) + local tasks = {} local readings = {} local access_fault = false local _, coils, _, _ = self.rtu.io_count() @@ -19,12 +20,30 @@ function new(rtu_dev, use_parallel_read) if return_ok then for i = 1, count do local addr = c_addr_start + i - 1 - readings[i], access_fault = self.rtu.read_coil(addr) + + if self.use_parallel then + table.insert(tasks, function () + local reading, fault = self.rtu.read_coil(addr) + if fault then access_fault = true else readings[i] = reading end + end) + else + readings[i], access_fault = self.rtu.read_coil(addr) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end + end + end + + -- run parallel tasks if configured + if self.use_parallel then + parallel.waitForAll(table.unpack(tasks)) if access_fault then return_ok = false readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL - break end end else @@ -35,6 +54,7 @@ function new(rtu_dev, use_parallel_read) end local _2_read_discrete_inputs = function (di_addr_start, count) + local tasks = {} local readings = {} local access_fault = false local discrete_inputs, _, _, _ = self.rtu.io_count() @@ -43,12 +63,30 @@ function new(rtu_dev, use_parallel_read) if return_ok then for i = 1, count do local addr = di_addr_start + i - 1 - readings[i], access_fault = self.rtu.read_di(addr) + + if self.use_parallel then + table.insert(tasks, function () + local reading, fault = self.rtu.read_di(addr) + if fault then access_fault = true else readings[i] = reading end + end) + else + readings[i], access_fault = self.rtu.read_di(addr) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end + end + end + + -- run parallel tasks if configured + if self.use_parallel then + parallel.waitForAll(table.unpack(tasks)) if access_fault then return_ok = false readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL - break end end else @@ -59,6 +97,7 @@ function new(rtu_dev, use_parallel_read) end local _3_read_multiple_holding_registers = function (hr_addr_start, count) + local tasks = {} local readings = {} local access_fault = false local _, _, _, hold_regs = self.rtu.io_count() @@ -67,12 +106,30 @@ function new(rtu_dev, use_parallel_read) if return_ok then for i = 1, count do local addr = hr_addr_start + i - 1 - readings[i], access_fault = self.rtu.read_holding_reg(addr) + + if self.use_parallel then + table.insert(tasks, function () + local reading, fault = self.rtu.read_holding_reg(addr) + if fault then access_fault = true else readings[i] = reading end + end) + else + readings[i], access_fault = self.rtu.read_holding_reg(addr) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end + end + end + + -- run parallel tasks if configured + if self.use_parallel then + parallel.waitForAll(table.unpack(tasks)) if access_fault then return_ok = false readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL - break end end else @@ -83,6 +140,7 @@ function new(rtu_dev, use_parallel_read) end local _4_read_input_registers = function (ir_addr_start, count) + local tasks = {} local readings = {} local access_fault = false local _, _, input_regs, _ = self.rtu.io_count() @@ -91,12 +149,30 @@ function new(rtu_dev, use_parallel_read) if return_ok then for i = 1, count do local addr = ir_addr_start + i - 1 - readings[i], access_fault = self.rtu.read_input_reg(addr) + + if self.use_parallel then + table.insert(tasks, function () + local reading, fault = self.rtu.read_input_reg(addr) + if fault then access_fault = true else readings[i] = reading end + end) + else + readings[i], access_fault = self.rtu.read_input_reg(addr) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end + end + end + + -- run parallel tasks if configured + if self.use_parallel then + parallel.waitForAll(table.unpack(tasks)) if access_fault then return_ok = false readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL - break end end else diff --git a/rtu/startup.lua b/rtu/startup.lua index 5f467e4..1679115 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -8,10 +8,10 @@ os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") os.loadAPI("scada-common/mqueue.lua") -os.loadAPI("scada-common/modbus.lua") os.loadAPI("scada-common/rsio.lua") os.loadAPI("config.lua") +os.loadAPI("modbus.lua") os.loadAPI("rtu.lua") os.loadAPI("threads.lua") @@ -20,7 +20,7 @@ os.loadAPI("dev/boiler_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") -local RTU_VERSION = "alpha-v0.4.12" +local RTU_VERSION = "alpha-v0.4.13" local rtu_t = types.rtu_t @@ -147,7 +147,7 @@ for reactor_idx = 1, #rtu_redstone do reactor = rtu_redstone[reactor_idx].for_reactor, device = capabilities, -- use device field for redstone channels rtu = rs_rtu, - modbus_io = modbus.new(rs_rtu), + modbus_io = modbus.new(rs_rtu, false), modbus_busy = false, pkt_queue = nil, thread = nil @@ -199,7 +199,7 @@ for i = 1, #rtu_devices do reactor = rtu_devices[i].for_reactor, device = device, rtu = rtu_iface, - modbus_io = modbus.new(rtu_iface), + modbus_io = modbus.new(rtu_iface, true), modbus_busy = false, pkt_queue = mqueue.new(), thread = nil From 25c6b311f5cd900e0976d7dcadafe1d22feec1f2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 17:10:42 -0400 Subject: [PATCH 083/168] clear status cache when connection is lost/reset, allow requesting of full status --- reactor-plc/plc.lua | 11 +++++++++-- reactor-plc/startup.lua | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index cccced9..a97e004 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -374,13 +374,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- reconnect a newly connected reactor local reconnect_reactor = function (reactor) self.reactor = reactor - _update_status_cache() + self.status_cache = nil end -- unlink from the server local unlink = function () self.linked = false self.r_seq_num = nil + self.status_cache = nil end -- close the connection to the server @@ -512,6 +513,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local link_ack = packet.data[1] if link_ack == RPLC_LINKING.ALLOW then + self.status_cache = nil _send_struct() send_status(plc_state.degraded) log._debug("re-sent initial status data") @@ -530,6 +532,10 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) else log._debug("RPLC link req packet length mismatch") end + elseif packet.type == RPLC_TYPES.STATUS then + -- request of full status, clear cache first + self.status_cache = nil + send_status(plc_state.degraded) elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure _send_struct() @@ -587,8 +593,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) println_ts("linked!") log._debug("RPLC link request approved") - -- reset remote sequence number + -- reset remote sequence number and cache self.r_seq_num = nil + self.status_cache = nil _send_struct() send_status(plc_state.degraded) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 5c9df15..66dc3b2 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.5.0" +local R_PLC_VERSION = "alpha-v0.5.1" local print = util.print local println = util.println From fe5059dd514912316b0c2570b832356e1c656cad Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 17:21:34 -0400 Subject: [PATCH 084/168] debug print --- reactor-plc/plc.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index a97e004..8aa65b1 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -536,6 +536,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- request of full status, clear cache first self.status_cache = nil send_status(plc_state.degraded) + log._debug("sent out status cache again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure _send_struct() From e253a7b4ff72aaa726f75927ff4ea4ba6bb29326 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 3 May 2022 17:25:31 -0400 Subject: [PATCH 085/168] supervisor PLC session closing, re-requesting status cache if missing --- supervisor/session/plc.lua | 59 +++++++++++++++++++++++-------- supervisor/session/svsessions.lua | 35 +++++++++--------- supervisor/startup.lua | 2 +- 3 files changed, 65 insertions(+), 31 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index b4fbdd9..09aa033 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -7,6 +7,11 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + -- retry time constants in ms local INITIAL_WAIT = 1500 local RETRY_PERIOD = 1000 @@ -38,6 +43,7 @@ function new_session(id, for_reactor, in_queue, out_queue) r_seq_num = nil, connected = true, received_struct = false, + received_status_cache = false, plc_conn_watchdog = util.new_watchdog(3), last_rtt = 0, -- periodic messages @@ -48,6 +54,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- when to next retry one of these requests retry_times = { struct_req = (util.time() + 500), + status_req = (util.time() + 500), scram_req = 0, enable_req = 0, burn_rate_req = 0, @@ -257,6 +264,7 @@ function new_session(id, for_reactor, in_queue, out_queue) local status = pcall(_copy_status, pkt.data[6]) if status then -- copied in status data OK + self.received_status_cache = true else -- error copying status data log._error(log_header .. "failed to parse status packet data") @@ -365,18 +373,6 @@ function new_session(id, for_reactor, in_queue, out_queue) -- get the session database local get_db = function () return self.sDB end - -- close the connection - local close = function () - self.plc_conn_watchdog.cancel() - self.connected = false - _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) - end - - -- check if a timer matches this session's watchdog - local check_wd = function (timer) - return timer == self.plc_conn_watchdog.get_timer() - end - -- get the reactor structure local get_struct = function () if self.received_struct then @@ -386,6 +382,29 @@ function new_session(id, for_reactor, in_queue, out_queue) end end + -- get the reactor structure + local get_status = function () + if self.received_status_cache then + return self.sDB.mek_status + else + return nil + end + end + + -- check if a timer matches this session's watchdog + local check_wd = function (timer) + return timer == self.plc_conn_watchdog.get_timer() + end + + -- close the connection + local close = function () + self.plc_conn_watchdog.cancel() + self.connected = false + _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + println("connection to reactor " .. self.for_reactor .. " PLC closed by server") + log._info(log_header .. "session closed by server") + end + -- iterate the session local iterate = function () if self.connected then @@ -442,6 +461,8 @@ function new_session(id, for_reactor, in_queue, out_queue) -- exit if connection was closed if not self.connected then + self.plc_conn_watchdog.cancel() + println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host") log._info(log_header .. "session closed by remote host") return self.connected end @@ -479,6 +500,15 @@ function new_session(id, for_reactor, in_queue, out_queue) end end + -- status cache request retry + + if not self.received_status_cache then + if rtimes.status_req - util.time() <= 0 then + _send(RPLC_TYPES.MEK_STATUS, {}) + rtimes.status_req = util.time() + RETRY_PERIOD + end + end + -- SCRAM request retry if not self.acks.scram then @@ -522,9 +552,10 @@ function new_session(id, for_reactor, in_queue, out_queue) return { get_id = get_id, get_db = get_db, - close = close, - check_wd = check_wd, get_struct = get_struct, + get_status = get_status, + check_wd = check_wd, + close = close, iterate = iterate } end diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index c111093..6612730 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -91,6 +91,22 @@ function establish_plc_session(local_port, remote_port, for_reactor) end end +-- cleanly close a session +local function _shutdown(session) + session.open = false + session.instance.close() + + -- send packets in out queue (namely the close packet) + while session.out_queue.ready() do + local msg = session.out_queue.pop() + if msg.qtype == mqueue.TYPE.PACKET then + self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + end + end + + log._debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) +end + -- check if a watchdog timer event matches that of one of the provided sessions local function _check_watchdogs(sessions, timer_event) for i = 1, #sessions do @@ -98,9 +114,8 @@ local function _check_watchdogs(sessions, timer_event) if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then - log._debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port) - session.open = false - session.instance.close() + log._debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port .. "...") + _shutdown(session) end end end @@ -134,7 +149,6 @@ local function _iterate(sessions) end else session.open = false - session.instance.close() end end end @@ -189,18 +203,7 @@ local function _close(sessions) for i = 1, #sessions do local session = sessions[i] if session.open then - session.open = false - session.instance.close() - - -- send packets in out queue (namely the close packet) - while session.out_queue.ready() do - local msg = session.out_queue.pop() - if msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) - end - end - - log._debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) + _shutdown(session) end end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 8d174d4..53f6cfc 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -18,7 +18,7 @@ os.loadAPI("session/svsessions.lua") os.loadAPI("supervisor.lua") -local SUPERVISOR_VERSION = "alpha-v0.1.12" +local SUPERVISOR_VERSION = "alpha-v0.2.0" local print = util.print local println = util.println From 8b7ef47aad5920d23760265e21f297c182c51bd4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 4 May 2022 10:00:21 -0400 Subject: [PATCH 086/168] removed references to alarms and now sends status on shutdown --- reactor-plc/startup.lua | 12 ++++++++++-- reactor-plc/threads.lua | 4 ---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 66dc3b2..21c8372 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ os.loadAPI("config.lua") os.loadAPI("plc.lua") os.loadAPI("threads.lua") -local R_PLC_VERSION = "alpha-v0.5.1" +local R_PLC_VERSION = "alpha-v0.5.2" local print = util.print local println = util.println @@ -145,11 +145,19 @@ if __shared_memory.networked then -- run threads parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec, sp_ctrl_thread.exec) + + if plc_state.init_ok then + -- send status one last time after ISS shutdown + plc_comms.send_status(plc_state.degraded) + plc_comms.send_iss_status() + + -- close connection + plc_comms.close(conn_watchdog) + end else -- run threads, excluding comms parallel.waitForAll(main_thread.exec, iss_thread.exec) end --- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? println_ts("exited") log._info("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index ee23710..4ba1ba5 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -92,7 +92,6 @@ function thread__main(smem, init) log._error("reactor disconnected!") plc_state.no_reactor = true plc_state.degraded = true - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? elseif networked and device.type == "modem" then -- we only care if this is our wireless modem if device.dev == plc_dev.modem then @@ -170,8 +169,6 @@ function thread__main(smem, init) -- check for termination request if event == "terminate" or ppm.should_terminate() then log._info("terminate requested, main thread exiting") - -- close connection - plc_comms.close(conn_watchdog) -- iss handles reactor shutdown plc_state.shutdown = true break @@ -293,7 +290,6 @@ function thread__iss(smem) println_ts("reactor disabled") log._info("iss thread reactor SCRAM OK") else - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? println_ts("exiting, reactor failed to disable") log._error("iss thread failed to SCRAM reactor on exit") end From 1cb5a0789eb87d03f87d6d09beaa8db72135b9df Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 4 May 2022 11:23:45 -0400 Subject: [PATCH 087/168] #27 mekanism 10.1+ RTU support --- rtu/dev/boilerv_rtu.lua | 56 ++++++++++++++++++++++++++++++++++++++++ rtu/dev/imatrix_rtu.lua | 18 +++++++------ rtu/dev/turbinev_rtu.lua | 55 +++++++++++++++++++++++++++++++++++++++ rtu/startup.lua | 18 ++++++++++--- scada-common/types.lua | 4 ++- 5 files changed, 139 insertions(+), 12 deletions(-) create mode 100644 rtu/dev/boilerv_rtu.lua create mode 100644 rtu/dev/turbinev_rtu.lua diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua new file mode 100644 index 0000000..c23b6e1 --- /dev/null +++ b/rtu/dev/boilerv_rtu.lua @@ -0,0 +1,56 @@ +-- #REQUIRES rtu.lua + +function new(boiler) + local self = { + rtu = rtu.rtu_init(), + boiler = boiler + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + -- none + + -- input registers -- + -- multiblock properties + self.rtu.connect_input_reg(self.boiler.isFormed) + self.rtu.connect_input_reg(self.boiler.getLength) + self.rtu.connect_input_reg(self.boiler.getWidth) + self.rtu.connect_input_reg(self.boiler.getHeight) + -- build properties + self.rtu.connect_input_reg(self.boiler.getBoilCapacity) + self.rtu.connect_input_reg(self.boiler.getSteamCapacity) + self.rtu.connect_input_reg(self.boiler.getWaterCapacity) + self.rtu.connect_input_reg(self.boiler.getHeatedCoolantCapacity) + self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity) + self.rtu.connect_input_reg(self.boiler.getSuperheaters) + self.rtu.connect_input_reg(self.boiler.getMaxBoilRate) + -- current state + self.rtu.connect_input_reg(self.boiler.getTemperature) + self.rtu.connect_input_reg(self.boiler.getBoilRate) + -- tanks + self.rtu.connect_input_reg(self.boiler.getSteam) + self.rtu.connect_input_reg(self.boiler.getSteamNeeded) + self.rtu.connect_input_reg(self.boiler.getSteamFilledPercentage) + self.rtu.connect_input_reg(self.boiler.getWater) + self.rtu.connect_input_reg(self.boiler.getWaterNeeded) + self.rtu.connect_input_reg(self.boiler.getWaterFilledPercentage) + self.rtu.connect_input_reg(self.boiler.getHeatedCoolant) + self.rtu.connect_input_reg(self.boiler.getHeatedCoolantNeeded) + self.rtu.connect_input_reg(self.boiler.getHeatedCoolantFilledPercentage) + self.rtu.connect_input_reg(self.boiler.getCooledCoolant) + self.rtu.connect_input_reg(self.boiler.getCooledCoolantNeeded) + self.rtu.connect_input_reg(self.boiler.getCooledCoolantFilledPercentage) + + -- holding registers -- + -- none + + return { + rtu_interface = rtu_interface + } +end diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua index 43f49b4..f646da2 100644 --- a/rtu/dev/imatrix_rtu.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -17,19 +17,21 @@ function new(imatrix) -- none -- input registers -- - -- @todo check these on Mekanism 10.1+ + -- multiblock properties + self.rtu.connect_input_reg(self.boiler.isFormed) + self.rtu.connect_input_reg(self.boiler.getLength) + self.rtu.connect_input_reg(self.boiler.getWidth) + self.rtu.connect_input_reg(self.boiler.getHeight) -- build properties + self.rtu.connect_input_reg(self.imatrix.getMaxEnergy) self.rtu.connect_input_reg(self.imatrix.getTransferCap) self.rtu.connect_input_reg(self.imatrix.getInstalledCells) self.rtu.connect_input_reg(self.imatrix.getInstalledProviders) - self.rtu.connect_input_reg(self.imatrix.getTotalMaxEnergy) -- containers - self.rtu.connect_input_reg(self.imatrix.getTotalEnergy) - self.rtu.connect_input_reg(self.imatrix.getTotalEnergyNeeded) - self.rtu.connect_input_reg(self.imatrix.getTotalEnergyFilledPercentage) - -- additional fields? check these on 10.1 - self.rtu.connect_input_reg(self.imatrix.getInputItem) - self.rtu.connect_input_reg(self.imatrix.getOutputItem) + self.rtu.connect_input_reg(self.imatrix.getEnergy) + self.rtu.connect_input_reg(self.imatrix.getEnergyNeeded) + self.rtu.connect_input_reg(self.imatrix.getEnergyFilledPercentage) + -- I/O rates self.rtu.connect_input_reg(self.imatrix.getLastInput) self.rtu.connect_input_reg(self.imatrix.getLastOutput) diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua new file mode 100644 index 0000000..21525c5 --- /dev/null +++ b/rtu/dev/turbinev_rtu.lua @@ -0,0 +1,55 @@ +-- #REQUIRES rtu.lua + +function new(turbine) + local self = { + rtu = rtu.rtu_init(), + turbine = turbine + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end), function () end) + self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end), function () end) + + -- input registers -- + -- multiblock properties + self.rtu.connect_input_reg(self.boiler.isFormed) + self.rtu.connect_input_reg(self.boiler.getLength) + self.rtu.connect_input_reg(self.boiler.getWidth) + self.rtu.connect_input_reg(self.boiler.getHeight) + -- build properties + self.rtu.connect_input_reg(self.turbine.getBlades) + self.rtu.connect_input_reg(self.turbine.getCoils) + self.rtu.connect_input_reg(self.turbine.getVents) + self.rtu.connect_input_reg(self.turbine.getDispersers) + self.rtu.connect_input_reg(self.turbine.getCondensers) + self.rtu.connect_input_reg(self.turbine.getDumpingMode) + self.rtu.connect_input_reg(self.turbine.getSteamCapacity) + self.rtu.connect_input_reg(self.turbine.getMaxFlowRate) + self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput) + self.rtu.connect_input_reg(self.turbine.getMaxProduction) + -- current state + self.rtu.connect_input_reg(self.turbine.getFlowRate) + self.rtu.connect_input_reg(self.turbine.getProductionRate) + self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate) + -- tanks/containers + self.rtu.connect_input_reg(self.turbine.getSteam) + self.rtu.connect_input_reg(self.turbine.getSteamNeeded) + self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage) + self.rtu.connect_input_reg(self.turbine.getEnergy) + self.rtu.connect_input_reg(self.turbine.getEnergyNeeded) + self.rtu.connect_input_reg(self.turbine.getEnergyFilledPercentage) + + -- holding registers -- + self.rtu.conenct_holding_reg(self.turbine.setDumpingMode, self.turbine.getDumpingMode) + + return { + rtu_interface = rtu_interface + } +end diff --git a/rtu/startup.lua b/rtu/startup.lua index 1679115..697a528 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,10 +17,13 @@ os.loadAPI("threads.lua") os.loadAPI("dev/redstone_rtu.lua") os.loadAPI("dev/boiler_rtu.lua") +os.loadAPI("dev/boilerv_rtu.lua") +os.loadAPI("dev/energymachine_rtu.lua") os.loadAPI("dev/imatrix_rtu.lua") os.loadAPI("dev/turbine_rtu.lua") +os.loadAPI("dev/turbinev_rtu.lua") -local RTU_VERSION = "alpha-v0.4.13" +local RTU_VERSION = "alpha-v0.5.0" local rtu_t = types.rtu_t @@ -173,16 +176,25 @@ for i = 1, #rtu_devices do -- boiler multiblock rtu_type = rtu_t.boiler rtu_iface = boiler_rtu.new(device) + elseif type == "boilerValve" then + -- boiler multiblock (10.1+) + rtu_type = rtu_t.boiler_valve + rtu_iface = boilerv_rtu.new(device) elseif type == "turbine" then -- turbine multiblock rtu_type = rtu_t.turbine rtu_iface = turbine_rtu.new(device) + elseif type == "turbineValve" then + -- turbine multiblock (10.1+) + rtu_type = rtu_t.turbine_valve + rtu_iface = turbinev_rtu.new(device) elseif type == "mekanismMachine" then -- assumed to be an induction matrix multiblock, pre Mekanism 10.1 + -- also works with energy cubes rtu_type = rtu_t.energy_machine rtu_iface = energymachine_rtu.new(device) - elseif type == "inductionMatrix" then - -- induction matrix multiblock, post Mekanism 10.1 + elseif type == "inductionPort" then + -- induction matrix multiblock (10.1+) rtu_type = rtu_t.induction_matrix rtu_iface = imatrix_rtu.new(device) else diff --git a/scada-common/types.lua b/scada-common/types.lua index 866982a..8ae93cc 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -3,9 +3,11 @@ rtu_t = { redstone = "redstone", boiler = "boiler", + boiler_valve = "boiler_valve", turbine = "turbine", + turbine_valve = "turbine_valve", energy_machine = "emachine", - induction_matrix = "imatrix" + induction_matrix = "induction_matrix" } iss_status_t = { From 7bcb2607126d0a5a865ac2318d3c6332dcaf8817 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 4 May 2022 12:03:07 -0400 Subject: [PATCH 088/168] #27 added getMaxEnergy for turbine --- rtu/dev/turbinev_rtu.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua index 21525c5..9a38a55 100644 --- a/rtu/dev/turbinev_rtu.lua +++ b/rtu/dev/turbinev_rtu.lua @@ -31,6 +31,7 @@ function new(turbine) self.rtu.connect_input_reg(self.turbine.getCondensers) self.rtu.connect_input_reg(self.turbine.getDumpingMode) self.rtu.connect_input_reg(self.turbine.getSteamCapacity) + self.rtu.connect_input_reg(self.turbine.getMaxEnergy) self.rtu.connect_input_reg(self.turbine.getMaxFlowRate) self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput) self.rtu.connect_input_reg(self.turbine.getMaxProduction) From b575899d46bc99cd049245f95f4849b1073777b1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 4 May 2022 13:37:01 -0400 Subject: [PATCH 089/168] #33 lua module/require architecture changeover --- coordinator/coordinator.lua | 2 +- coordinator/startup.lua | 19 ++- reactor-plc/config.lua | 16 ++- reactor-plc/plc.lua | 99 +++++++------- reactor-plc/startup.lua | 48 ++++--- reactor-plc/threads.lua | 72 +++++----- rtu/config.lua | 28 ++-- rtu/dev/boiler_rtu.lua | 10 +- rtu/dev/boilerv_rtu.lua | 10 +- rtu/dev/energymachine_rtu.lua | 10 +- rtu/dev/imatrix_rtu.lua | 10 +- rtu/dev/redstone_rtu.lua | 13 +- rtu/dev/turbine_rtu.lua | 10 +- rtu/dev/turbinev_rtu.lua | 14 +- rtu/modbus.lua | 9 +- rtu/rtu.lua | 31 +++-- rtu/startup.lua | 67 +++++----- rtu/threads.lua | 65 ++++++--- scada-common/alarm.lua | 46 ++++--- scada-common/comms.lua | 56 +++++--- scada-common/log.lua | 23 ++-- scada-common/mqueue.lua | 10 +- scada-common/ppm.lua | 64 +++++---- scada-common/rsio.lua | 37 ++++-- scada-common/types.lua | 14 +- scada-common/util.lua | 43 +++--- supervisor/config.lua | 14 +- supervisor/session/coordinator.lua | 3 + supervisor/session/plc.lua | 66 +++++----- supervisor/session/rtu.lua | 3 + supervisor/session/svsessions.lua | 204 +++++++++++++++-------------- supervisor/startup.lua | 48 ++++--- supervisor/supervisor.lua | 33 +++-- 33 files changed, 679 insertions(+), 518 deletions(-) diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 96d766e..8089be8 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -1,4 +1,4 @@ --- #REQUIRES comms.lua +local comms = require("scada-common.comms") -- coordinator communications function coord_comms() diff --git a/coordinator/startup.lua b/coordinator/startup.lua index fe0bf48..5ee3d17 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -2,15 +2,14 @@ -- Nuclear Generation Facility SCADA Coordinator -- -os.loadAPI("scada-common/log.lua") -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/ppm.lua") -os.loadAPI("scada-common/comms.lua") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") -os.loadAPI("coordinator/config.lua") -os.loadAPI("coordinator/coordinator.lua") +local config = require("config") +local coordinator = require("coordinator") -local COORDINATOR_VERSION = "alpha-v0.1.1" +local COORDINATOR_VERSION = "alpha-v0.1.2" local print = util.print local println = util.println @@ -19,9 +18,9 @@ local println_ts = util.println_ts log.init("/log.txt", log.MODE.APPEND) -log._info("========================================") -log._info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) -log._info("========================================") +log.info("========================================") +log.info("BOOTING coordinator.startup " .. COORDINATOR_VERSION) +log.info("========================================") println(">> SCADA Coordinator " .. COORDINATOR_VERSION .. " <<") -- mount connected devices diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index 43086d5..99edc92 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -1,14 +1,18 @@ +local config = {} + -- set to false to run in offline mode (safety regulation only) -NETWORKED = true +config.NETWORKED = true -- unique reactor ID -REACTOR_ID = 1 +config.REACTOR_ID = 1 -- port to send packets TO server -SERVER_PORT = 16000 +config.SERVER_PORT = 16000 -- port to listen to incoming packets FROM server -LISTEN_PORT = 14001 +config.LISTEN_PORT = 14001 -- log path -LOG_PATH = "/log.txt" +config.LOG_PATH = "/log.txt" -- log mode -- 0 = APPEND (adds to existing file on start) -- 1 = NEW (replaces existing file on start) -LOG_MODE = 0 +config.LOG_MODE = 0 + +return config diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 8aa65b1..54f47d2 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,7 +1,10 @@ --- #REQUIRES types.lua --- #REQUIRES comms.lua --- #REQUIRES ppm.lua --- #REQUIRES util.lua +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local plc = {} local iss_status_t = types.iss_status_t @@ -18,7 +21,7 @@ local println_ts = util.println_ts -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main SCADA supervisor/coordinator control -function iss_init(reactor) +plc.iss_init = function (reactor) local self = { reactor = reactor, cache = { false, false, false, false, false, false, false }, @@ -34,7 +37,7 @@ function iss_init(reactor) local damage_percent = self.reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor damage") + log.error("ISS: failed to check reactor damage") return false else return damage_percent >= 100 @@ -46,7 +49,7 @@ function iss_init(reactor) local hc_needed = self.reactor.getHeatedCoolantNeeded() if hc_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor heated coolant level") + log.error("ISS: failed to check reactor heated coolant level") return false else return hc_needed == 0 @@ -58,7 +61,7 @@ function iss_init(reactor) local w_needed = self.reactor.getWasteNeeded() if w_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor waste level") + log.error("ISS: failed to check reactor waste level") return false else return w_needed == 0 @@ -71,7 +74,7 @@ function iss_init(reactor) local temp = self.reactor.getTemperature() if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor temperature") + log.error("ISS: failed to check reactor temperature") return false else return temp >= 1200 @@ -83,7 +86,7 @@ function iss_init(reactor) local fuel = self.reactor.getFuel() if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor fuel level") + log.error("ISS: failed to check reactor fuel level") return false else return fuel == 0 @@ -95,7 +98,7 @@ function iss_init(reactor) local coolant_filled = self.reactor.getCoolantFilledPercentage() if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log._error("ISS: failed to check reactor coolant level") + log.error("ISS: failed to check reactor coolant level") return false else return coolant_filled < 0.02 @@ -134,25 +137,25 @@ function iss_init(reactor) if self.tripped then status = self.trip_cause elseif self.cache[1] then - log._warning("ISS: damage critical!") + log.warning("ISS: damage critical!") status = iss_status_t.dmg_crit elseif self.cache[4] then - log._warning("ISS: high temperature!") + log.warning("ISS: high temperature!") status = iss_status_t.high_temp elseif self.cache[2] then - log._warning("ISS: heated coolant backup!") + log.warning("ISS: heated coolant backup!") status = iss_status_t.ex_hcoolant elseif self.cache[6] then - log._warning("ISS: no coolant!") + log.warning("ISS: no coolant!") status = iss_status_t.no_coolant elseif self.cache[3] then - log._warning("ISS: full waste!") + log.warning("ISS: full waste!") status = iss_status_t.ex_waste elseif self.cache[5] then - log._warning("ISS: no fuel!") + log.warning("ISS: no fuel!") status = iss_status_t.no_fuel elseif self.cache[7] then - log._warning("ISS: supervisor connection timeout!") + log.warning("ISS: supervisor connection timeout!") status = iss_status_t.timeout else self.tripped = false @@ -161,7 +164,7 @@ function iss_init(reactor) -- if a new trip occured... local first_trip = false if not was_tripped and status ~= iss_status_t.ok then - log._warning("ISS: reactor SCRAM") + log.warning("ISS: reactor SCRAM") first_trip = true self.tripped = true @@ -169,7 +172,7 @@ function iss_init(reactor) self.reactor.scram() if self.reactor.__p_is_faulted() then - log._error("ISS: failed reactor SCRAM") + log.error("ISS: failed reactor SCRAM") end end @@ -198,7 +201,7 @@ function iss_init(reactor) end -- reactor PLC communications -function comms_init(id, modem, local_port, server_port, reactor, iss) +plc.comms = function (id, modem, local_port, server_port, reactor, iss) local self = { id = id, seq_num = 0, @@ -355,7 +358,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if not self.reactor.__p_is_faulted() then _send(RPLC_TYPES.MEK_STRUCT, mek_data) else - log._error("failed to send structure: PPM fault") + log.error("failed to send structure: PPM fault") end end @@ -417,7 +420,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if not self.reactor.__p_is_faulted() then _send(RPLC_TYPES.STATUS, sys_status) else - log._error("failed to send status: PPM fault") + log.error("failed to send status: PPM fault") end end end @@ -463,7 +466,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) pkt = mgmt_pkt.get() end else - log._error("illegal packet type " .. s_pkt.protocol(), true) + log.error("illegal packet type " .. s_pkt.protocol(), true) end end @@ -477,7 +480,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() elseif self.linked and self.r_seq_num >= packet.scada_frame.seq_num() then - log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return else self.r_seq_num = packet.scada_frame.seq_num() @@ -496,19 +499,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) local trip_time = util.time() - timestamp if trip_time > 500 then - log._warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") + log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") end - -- log._debug("RPLC RTT = ".. trip_time .. "ms") + -- log.debug("RPLC RTT = ".. trip_time .. "ms") _send_keep_alive_ack(timestamp) else - log._debug("RPLC keep alive packet length mismatch") + log.debug("RPLC keep alive packet length mismatch") end elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation if packet.length == 1 then - log._debug("received unsolicited link request response") + log.debug("received unsolicited link request response") local link_ack = packet.data[1] @@ -516,31 +519,31 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.status_cache = nil _send_struct() send_status(plc_state.degraded) - log._debug("re-sent initial status data") + log.debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then println_ts("received unsolicited link denial, unlinking") - log._debug("unsolicited RPLC link request denied") + log.debug("unsolicited RPLC link request denied") elseif link_ack == RPLC_LINKING.COLLISION then println_ts("received unsolicited link collision, unlinking") - log._warning("unsolicited RPLC link request collision") + log.warning("unsolicited RPLC link request collision") else println_ts("invalid unsolicited link response") - log._error("unsolicited unknown RPLC link request response") + log.error("unsolicited unknown RPLC link request response") end self.linked = link_ack == RPLC_LINKING.ALLOW else - log._debug("RPLC link req packet length mismatch") + log.debug("RPLC link req packet length mismatch") end elseif packet.type == RPLC_TYPES.STATUS then -- request of full status, clear cache first self.status_cache = nil send_status(plc_state.degraded) - 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 -- request for physical structure _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_SCRAM then -- disable the reactor self.scrammed = true @@ -576,14 +579,14 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_ack(packet.type, success) else - log._debug("RPLC set burn rate packet length mismatch") + log.debug("RPLC set burn rate packet length mismatch") end elseif packet.type == RPLC_TYPES.ISS_CLEAR then -- clear the ISS status iss.reset() _send_ack(packet.type, true) else - log._warning("received unknown RPLC packet type " .. packet.type) + log.warning("received unknown RPLC packet type " .. packet.type) end elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation @@ -592,7 +595,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) if link_ack == RPLC_LINKING.ALLOW then println_ts("linked!") - log._debug("RPLC link request approved") + log.debug("RPLC link request approved") -- reset remote sequence number and cache self.r_seq_num = nil @@ -601,24 +604,24 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) _send_struct() send_status(plc_state.degraded) - log._debug("sent initial status data") + log.debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then println_ts("link request denied, retrying...") - log._debug("RPLC link request denied") + log.debug("RPLC link request denied") elseif link_ack == RPLC_LINKING.COLLISION then println_ts("reactor PLC ID collision (check config), retrying...") - log._warning("RPLC link request collision") + log.warning("RPLC link request collision") else println_ts("invalid link response, bad channel? retrying...") - log._error("unknown RPLC link request response") + log.error("unknown RPLC link request response") end self.linked = link_ack == RPLC_LINKING.ALLOW else - log._debug("RPLC link req packet length mismatch") + log.debug("RPLC link req packet length mismatch") end else - log._debug("discarding non-link packet before linked") + log.debug("discarding non-link packet before linked") end elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then -- handle session close @@ -626,9 +629,9 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) conn_watchdog.cancel() unlink() println_ts("server connection closed by remote host") - log._warning("server connection closed by remote host") + log.warning("server connection closed by remote host") else - log._warning("received unknown SCADA_MGMT packet type " .. packet.type) + log.warning("received unknown SCADA_MGMT packet type " .. packet.type) end end end @@ -652,3 +655,5 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) is_linked = is_linked } end + +return plc diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 21c8372..dde16e7 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -2,18 +2,16 @@ -- Reactor Programmable Logic Controller -- -os.loadAPI("scada-common/log.lua") -os.loadAPI("scada-common/types.lua") -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/ppm.lua") -os.loadAPI("scada-common/comms.lua") -os.loadAPI("scada-common/mqueue.lua") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") -os.loadAPI("config.lua") -os.loadAPI("plc.lua") -os.loadAPI("threads.lua") +local config = require("config") +local plc = require("plc") +local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.5.2" +local R_PLC_VERSION = "alpha-v0.6.0" local print = util.print local println = util.println @@ -22,9 +20,9 @@ local println_ts = util.println_ts log.init(config.LOG_PATH, config.LOG_MODE) -log._info("========================================") -log._info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) -log._info("========================================") +log.info("========================================") +log.info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) +log.info("========================================") println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") -- mount connected devices @@ -78,7 +76,7 @@ local plc_state = __shared_memory.plc_state -- we need a reactor and a modem if smem_dev.reactor == nil then println("boot> fission reactor not found"); - log._warning("no reactor on startup") + log.warning("no reactor on startup") plc_state.init_ok = false plc_state.degraded = true @@ -86,7 +84,7 @@ if smem_dev.reactor == nil then end if networked and smem_dev.modem == nil then println("boot> wireless modem not found") - log._warning("no wireless modem on startup") + log.warning("no wireless modem on startup") if smem_dev.reactor ~= nil then smem_dev.reactor.scram() @@ -104,19 +102,19 @@ function init() -- init internal safety system smem_sys.iss = plc.iss_init(smem_dev.reactor) - log._debug("iss init") + log.debug("iss init") if __shared_memory.networked then -- start comms - smem_sys.plc_comms = plc.comms_init(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss) - log._debug("comms init") + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss) + log.debug("comms init") -- comms watchdog, 3 second timeout smem_sys.conn_watchdog = util.new_watchdog(3) - log._debug("conn watchdog started") + log.debug("conn watchdog started") else println("boot> starting in offline mode"); - log._debug("running without networking") + log.debug("running without networking") end os.queueEvent("clock_start") @@ -124,7 +122,7 @@ function init() println("boot> completed"); else println("boot> system in degraded state, awaiting devices...") - log._warning("booted in a degraded state, awaiting peripheral connections...") + log.warning("booted in a degraded state, awaiting peripheral connections...") end end @@ -148,11 +146,11 @@ if __shared_memory.networked then if plc_state.init_ok then -- send status one last time after ISS shutdown - plc_comms.send_status(plc_state.degraded) - plc_comms.send_iss_status() + smem_sys.plc_comms.send_status(plc_state.degraded) + smem_sys.plc_comms.send_iss_status() -- close connection - plc_comms.close(conn_watchdog) + smem_sys.plc_comms.close(smem_sys.conn_watchdog) end else -- run threads, excluding comms @@ -160,4 +158,4 @@ else end println_ts("exited") -log._info("exited") +log.info("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 4ba1ba5..f689955 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -1,7 +1,9 @@ --- #REQUIRES comms.lua --- #REQUIRES log.lua --- #REQUIRES ppm.lua --- #REQUIRES util.lua +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") + +local threads = {} local print = util.print local println = util.println @@ -28,10 +30,10 @@ local MQ__COMM_CMD = { } -- main thread -function thread__main(smem, init) +threads.thread__main = function (smem, init) -- execute thread local exec = function () - log._debug("main thread init, clock inactive") + log.debug("main thread init, clock inactive") -- send status updates at 2Hz (every 10 server ticks) (every loop tick) -- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks) @@ -89,14 +91,14 @@ function thread__main(smem, init) if device.type == "fissionReactor" then println_ts("reactor disconnected!") - log._error("reactor disconnected!") + log.error("reactor disconnected!") plc_state.no_reactor = true plc_state.degraded = true elseif networked and device.type == "modem" then -- we only care if this is our wireless modem if device.dev == plc_dev.modem then println_ts("wireless modem disconnected!") - log._error("comms modem disconnected!") + log.error("comms modem disconnected!") plc_state.no_modem = true if plc_state.init_ok then @@ -106,7 +108,7 @@ function thread__main(smem, init) plc_state.degraded = true else - log._warning("non-comms modem disconnected") + log.warning("non-comms modem disconnected") end end elseif event == "peripheral" then @@ -120,7 +122,7 @@ function thread__main(smem, init) smem.q.mq_iss.push_command(MQ__ISS_CMD.SCRAM) println_ts("reactor reconnected.") - log._info("reactor reconnected.") + log.info("reactor reconnected.") plc_state.no_reactor = false if plc_state.init_ok then @@ -144,7 +146,7 @@ function thread__main(smem, init) end println_ts("wireless modem reconnected.") - log._info("comms modem reconnected.") + log.info("comms modem reconnected.") plc_state.no_modem = false -- determine if we are still in a degraded state @@ -152,7 +154,7 @@ function thread__main(smem, init) plc_state.degraded = false end else - log._info("wired modem reconnected.") + log.info("wired modem reconnected.") end end @@ -163,12 +165,12 @@ function thread__main(smem, init) elseif event == "clock_start" then -- start loop clock loop_clock = os.startTimer(MAIN_CLOCK) - log._debug("main thread clock started") + log.debug("main thread clock started") end -- check for termination request if event == "terminate" or ppm.should_terminate() then - log._info("terminate requested, main thread exiting") + log.info("terminate requested, main thread exiting") -- iss handles reactor shutdown plc_state.shutdown = true break @@ -180,10 +182,10 @@ function thread__main(smem, init) end -- ISS monitor thread -function thread__iss(smem) +threads.thread__iss = function (smem) -- execute thread local exec = function () - log._debug("iss thread start") + log.debug("iss thread start") -- load in from shared memory local networked = smem.networked @@ -257,17 +259,17 @@ function thread__iss(smem) plc_state.scram = true if reactor.scram() then println_ts("successful reactor SCRAM") - log._error("successful reactor SCRAM") + log.error("successful reactor SCRAM") else println_ts("failed reactor SCRAM") - log._error("failed reactor SCRAM") + log.error("failed reactor SCRAM") end elseif msg.message == MQ__ISS_CMD.TRIP_TIMEOUT then -- watchdog tripped plc_state.scram = true iss.trip_timeout() println_ts("server timeout") - log._warning("server timeout") + log.warning("server timeout") end elseif msg.qtype == mqueue.TYPE.DATA then -- received data @@ -282,19 +284,19 @@ function thread__iss(smem) -- check for termination request if plc_state.shutdown then -- safe exit - log._info("iss thread shutdown initiated") + log.info("iss thread shutdown initiated") if plc_state.init_ok then plc_state.scram = true reactor.scram() if reactor.__p_is_ok() then println_ts("reactor disabled") - log._info("iss thread reactor SCRAM OK") + log.info("iss thread reactor SCRAM OK") else println_ts("exiting, reactor failed to disable") - log._error("iss thread failed to SCRAM reactor on exit") + log.error("iss thread failed to SCRAM reactor on exit") end end - log._info("iss thread exiting") + log.info("iss thread exiting") break end @@ -307,10 +309,10 @@ function thread__iss(smem) end -- communications sender thread -function thread__comms_tx(smem) +threads.thread__comms_tx = function (smem) -- execute thread local exec = function () - log._debug("comms tx thread start") + log.debug("comms tx thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -345,7 +347,7 @@ function thread__comms_tx(smem) -- check for termination request if plc_state.shutdown then - log._info("comms tx thread exiting") + log.info("comms tx thread exiting") break end @@ -358,10 +360,10 @@ function thread__comms_tx(smem) end -- communications handler thread -function thread__comms_rx(smem) +threads.thread__comms_rx = function (smem) -- execute thread local exec = function () - log._debug("comms rx thread start") + log.debug("comms rx thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -397,7 +399,7 @@ function thread__comms_rx(smem) -- check for termination request if plc_state.shutdown then - log._info("comms rx thread exiting") + log.info("comms rx thread exiting") break end @@ -410,10 +412,10 @@ function thread__comms_rx(smem) end -- apply setpoints -function thread__setpoint_control(smem) +threads.thread__setpoint_control = function (smem) -- execute thread local exec = function () - log._debug("setpoint control thread start") + log.debug("setpoint control thread start") -- load in from shared memory local plc_state = smem.plc_state @@ -434,10 +436,10 @@ function thread__setpoint_control(smem) if not plc_state.scram then if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then -- update without ramp if <= 5 mB/t change - log._debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") + log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") reactor.setBurnRate(setpoints.burn_rate) else - log._debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") + log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") running = true end @@ -489,7 +491,7 @@ function thread__setpoint_control(smem) -- check for termination request if plc_state.shutdown then - log._info("setpoint control thread exiting") + log.info("setpoint control thread exiting") break end @@ -500,3 +502,5 @@ function thread__setpoint_control(smem) return { exec = exec } end + +return threads diff --git a/rtu/config.lua b/rtu/config.lua index 6ba5653..ec2b047 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -1,47 +1,49 @@ --- #REQUIRES rsio.lua +local rsio = require("scada-common.rsio") + +local config = {} -- port to send packets TO server -SERVER_PORT = 16000 +config.SERVER_PORT = 16000 -- port to listen to incoming packets FROM server -LISTEN_PORT = 15001 +config.LISTEN_PORT = 15001 -- log path -LOG_PATH = "/log.txt" +config.LOG_PATH = "/log.txt" -- log mode -- 0 = APPEND (adds to existing file on start) -- 1 = NEW (replaces existing file on start) -LOG_MODE = 0 +config.LOG_MODE = 0 -- RTU peripheral devices (named: side/network device name) -RTU_DEVICES = { +config.RTU_DEVICES = { { - name = "boiler_0", + name = "boiler_1", index = 1, for_reactor = 1 }, { - name = "turbine_0", + name = "turbine_1", index = 1, for_reactor = 1 } } -- RTU redstone interface definitions -RTU_REDSTONE = { +config.RTU_REDSTONE = { { for_reactor = 1, io = { { - channel = rsio.RS_IO.WASTE_PO, + channel = rsio.IO.WASTE_PO, side = "top", bundled_color = colors.blue, for_reactor = 1 }, { - channel = rsio.RS_IO.WASTE_PU, + channel = rsio.IO.WASTE_PU, side = "top", bundled_color = colors.cyan, for_reactor = 1 }, { - channel = rsio.RS_IO.WASTE_AM, + channel = rsio.IO.WASTE_AM, side = "top", bundled_color = colors.purple, for_reactor = 1 @@ -49,3 +51,5 @@ RTU_REDSTONE = { } } } + +return config diff --git a/rtu/dev/boiler_rtu.lua b/rtu/dev/boiler_rtu.lua index 861a34f..322c511 100644 --- a/rtu/dev/boiler_rtu.lua +++ b/rtu/dev/boiler_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(boiler) +local boiler_rtu = {} + +boiler_rtu.new = function (boiler) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), boiler = boiler } @@ -49,3 +51,5 @@ function new(boiler) rtu_interface = rtu_interface } end + +return boiler_rtu diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua index c23b6e1..a609588 100644 --- a/rtu/dev/boilerv_rtu.lua +++ b/rtu/dev/boilerv_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(boiler) +local boilerv_rtu = {} + +boilerv_rtu.new = function (boiler) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), boiler = boiler } @@ -54,3 +56,5 @@ function new(boiler) rtu_interface = rtu_interface } end + +return boilerv_rtu diff --git a/rtu/dev/energymachine_rtu.lua b/rtu/dev/energymachine_rtu.lua index b3f004a..d2aee3f 100644 --- a/rtu/dev/energymachine_rtu.lua +++ b/rtu/dev/energymachine_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(machine) +local energymachine_rtu = {} + +energymachine_rtu.new = function (machine) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), machine = machine } @@ -31,3 +33,5 @@ function new(machine) rtu_interface = rtu_interface } end + +return energymachine_rtu diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua index f646da2..12fd942 100644 --- a/rtu/dev/imatrix_rtu.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(imatrix) +local imatrix_rtu = {} + +imatrix_rtu.new = function (imatrix) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), imatrix = imatrix } @@ -42,3 +44,5 @@ function new(imatrix) rtu_interface = rtu_interface } end + +return imatrix_rtu diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index d81cebb..163b749 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -1,13 +1,14 @@ --- #REQUIRES rtu.lua --- #REQUIRES rsio.lua --- note: this RTU makes extensive use of the programming concept of closures +local rtu = require("rtu") +local rsio = require("scada-common.rsio") + +local redstone_rtu = {} local digital_read = rsio.digital_read local digital_is_active = rsio.digital_is_active -function new() +redstone_rtu.new = function () local self = { - rtu = rtu.rtu_init() + rtu = rtu.init_unit() } local rtu_interface = function () @@ -91,3 +92,5 @@ function new() link_ao = link_ao } end + +return redstone_rtu diff --git a/rtu/dev/turbine_rtu.lua b/rtu/dev/turbine_rtu.lua index 7584270..1f1827f 100644 --- a/rtu/dev/turbine_rtu.lua +++ b/rtu/dev/turbine_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(turbine) +local turbine_rtu = {} + +turbine_rtu.new = function (turbine) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), turbine = turbine } @@ -44,3 +46,5 @@ function new(turbine) rtu_interface = rtu_interface } end + +return turbine_rtu diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua index 9a38a55..2be532b 100644 --- a/rtu/dev/turbinev_rtu.lua +++ b/rtu/dev/turbinev_rtu.lua @@ -1,8 +1,10 @@ --- #REQUIRES rtu.lua +local rtu = require("rtu") -function new(turbine) +local turbinev_rtu = {} + +turbinev_rtu.new = function (turbine) local self = { - rtu = rtu.rtu_init(), + rtu = rtu.init_unit(), turbine = turbine } @@ -14,8 +16,8 @@ function new(turbine) -- none -- coils -- - self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end), function () end) - self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end), function () end) + self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end, function () end) + self.rtu.connect_coil(function () self.turbine.decrementDumpingMode() end, function () end) -- input registers -- -- multiblock properties @@ -54,3 +56,5 @@ function new(turbine) rtu_interface = rtu_interface } end + +return turbinev_rtu diff --git a/rtu/modbus.lua b/rtu/modbus.lua index 7ea6108..ac26fc1 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -1,10 +1,13 @@ --- #REQUIRES types.lua +local comms = require("scada-common.comms") +local types = require("scada-common.types") + +local modbus = {} local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE -- new modbus comms handler object -function new(rtu_dev, use_parallel_read) +modbus.new = function (rtu_dev, use_parallel_read) local self = { rtu = rtu_dev, use_parallel = use_parallel_read @@ -401,3 +404,5 @@ function new(rtu_dev, use_parallel_read) reply__gw_unavailable = reply__gw_unavailable } end + +return modbus diff --git a/rtu/rtu.lua b/rtu/rtu.lua index b9a3061..9c22559 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,12 +1,15 @@ --- #REQUIRES comms.lua --- #REQUIRES modbus.lua --- #REQUIRES ppm.lua +local comms = require("scada-common.comms") +local ppm = require("scada-common.ppm") + +local modbus = require("modbus") + +local rtu = {} local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES -function rtu_init() +rtu.init_unit = function () local self = { discrete_inputs = {}, coils = {}, @@ -117,7 +120,7 @@ function rtu_init() } end -function rtu_comms(modem, local_port, server_port) +rtu.comms = function (modem, local_port, server_port) local self = { seq_num = 0, r_seq_num = nil, @@ -187,7 +190,7 @@ function rtu_comms(modem, local_port, server_port) pkt = mgmt_pkt.get() end else - log._error("illegal packet type " .. s_pkt.protocol(), true) + log.error("illegal packet type " .. s_pkt.protocol(), true) end end @@ -203,7 +206,7 @@ function rtu_comms(modem, local_port, server_port) if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() elseif rtu_state.linked and self.r_seq_num >= packet.scada_frame.seq_num() then - log._warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) + log.warning("sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. packet.scada_frame.seq_num()) return else self.r_seq_num = packet.scada_frame.seq_num() @@ -224,7 +227,7 @@ function rtu_comms(modem, local_port, server_port) -- immediately execute redstone RTU requests local return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then - log._warning("requested MODBUS operation failed") + log.warning("requested MODBUS operation failed") end else -- check validity then pass off to unit comms thread @@ -237,13 +240,13 @@ function rtu_comms(modem, local_port, server_port) unit.pkt_queue.push(packet) end else - log._warning("cannot perform requested MODBUS operation") + log.warning("cannot perform requested MODBUS operation") end end else -- unit ID out of range? reply = modbus.reply__gw_unavailable(packet) - log._error("MODBUS packet requesting non-existent unit") + log.error("MODBUS packet requesting non-existent unit") end send_modbus(reply) @@ -253,7 +256,7 @@ function rtu_comms(modem, local_port, server_port) -- close connection conn_watchdog.cancel() unlink(rtu_state) - if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then + elseif packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then -- acknowledgement rtu_state.linked = true self.r_seq_num = nil @@ -262,11 +265,11 @@ function rtu_comms(modem, local_port, server_port) send_advertisement(units) else -- not supported - log._warning("RTU got unexpected SCADA message type " .. packet.type, true) + log.warning("RTU got unexpected SCADA message type " .. packet.type, true) end else -- should be unreachable assuming packet is from parse_packet() - log._error("illegal packet type " .. protocol, true) + log.error("illegal packet type " .. protocol, true) end end end @@ -337,3 +340,5 @@ function rtu_comms(modem, local_port, server_port) close = close } end + +return rtu diff --git a/rtu/startup.lua b/rtu/startup.lua index 697a528..4edcbc9 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -2,28 +2,27 @@ -- RTU: Remote Terminal Unit -- -os.loadAPI("scada-common/log.lua") -os.loadAPI("scada-common/types.lua") -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/ppm.lua") -os.loadAPI("scada-common/comms.lua") -os.loadAPI("scada-common/mqueue.lua") -os.loadAPI("scada-common/rsio.lua") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local ppm = require("scada-common.ppm") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") +local util = require("scada-common.util") -os.loadAPI("config.lua") -os.loadAPI("modbus.lua") -os.loadAPI("rtu.lua") -os.loadAPI("threads.lua") +local config = require("config") +local modbus = require("modbus") +local rtu = require("rtu") +local threads = require("threads") -os.loadAPI("dev/redstone_rtu.lua") -os.loadAPI("dev/boiler_rtu.lua") -os.loadAPI("dev/boilerv_rtu.lua") -os.loadAPI("dev/energymachine_rtu.lua") -os.loadAPI("dev/imatrix_rtu.lua") -os.loadAPI("dev/turbine_rtu.lua") -os.loadAPI("dev/turbinev_rtu.lua") +local redstone_rtu = require("dev.redstone_rtu") +local boiler_rtu = require("dev.boiler_rtu") +local boilerv_rtu = require("dev.boilerv_rtu") +local energymachine_rtu = require("dev.energymachine_rtu") +local imatrix_rtu = require("dev.imatrix_rtu") +local turbine_rtu = require("dev.turbine_rtu") +local turbinev_rtu = require("dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.5.0" +local RTU_VERSION = "alpha-v0.6.0" local rtu_t = types.rtu_t @@ -34,9 +33,9 @@ local println_ts = util.println_ts log.init(config.LOG_PATH, config.LOG_MODE) -log._info("========================================") -log._info("BOOTING rtu.startup " .. RTU_VERSION) -log._info("========================================") +log.info("========================================") +log.info("BOOTING rtu.startup " .. RTU_VERSION) +log.info("========================================") println(">> RTU " .. RTU_VERSION .. " <<") ---------------------------------------- @@ -77,11 +76,11 @@ local smem_sys = __shared_memory.rtu_sys -- get modem if smem_dev.modem == nil then println("boot> wireless modem not found") - log._warning("no wireless modem on startup") + log.warning("no wireless modem on startup") return end -smem_sys.rtu_comms = rtu.rtu_comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT) +smem_sys.rtu_comms = rtu.comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT) ---------------------------------------- -- interpret config and init units @@ -99,7 +98,7 @@ for reactor_idx = 1, #rtu_redstone do local capabilities = {} - log._debug("init> starting redstone RTU I/O linking for reactor " .. rtu_redstone[reactor_idx].for_reactor .. "...") + log.debug("init> starting redstone RTU I/O linking for reactor " .. rtu_redstone[reactor_idx].for_reactor .. "...") for i = 1, #io_table do local valid = false @@ -118,7 +117,7 @@ for reactor_idx = 1, #rtu_redstone do local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. reactor_idx .. " (for reactor " .. rtu_redstone[reactor_idx].for_reactor .. ")" println_ts(message) - log._warning(message) + log.warning(message) else -- link redstone in RTU local mode = rsio.get_io_mode(conf.channel) @@ -132,13 +131,13 @@ for reactor_idx = 1, #rtu_redstone do rs_rtu.link_ao(conf.channel, conf.side) else -- should be unreachable code, we already validated channels - log._error("init> fell through if chain attempting to identify IO mode", true) + log.error("init> fell through if chain attempting to identify IO mode", true) break end table.insert(capabilities, conf.channel) - log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. + log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. ") for reactor " .. rtu_redstone[reactor_idx].for_reactor) end end @@ -156,7 +155,7 @@ for reactor_idx = 1, #rtu_redstone do thread = nil }) - log._debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) + log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) end -- mounted peripherals @@ -166,7 +165,7 @@ for i = 1, #rtu_devices do if device == nil then local message = "init> '" .. rtu_devices[i].name .. "' not found" println_ts(message) - log._warning(message) + log.warning(message) else local type = ppm.get_type(rtu_devices[i].name) local rtu_iface = nil @@ -200,7 +199,7 @@ for i = 1, #rtu_devices do else local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")" println_ts(message) - log._warning(message) + log.warning(message) end if rtu_iface ~= nil then @@ -221,7 +220,7 @@ for i = 1, #rtu_devices do table.insert(units, rtu_unit) - log._debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" .. + log.debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" .. rtu_devices[i].index .. "] for reactor " .. rtu_devices[i].for_reactor) end end @@ -237,7 +236,7 @@ local comms_thread = threads.thread__comms(__shared_memory) -- start connection watchdog smem_sys.conn_watchdog = util.new_watchdog(5) -log._debug("init> conn watchdog started") +log.debug("init> conn watchdog started") -- assemble thread list local _threads = { main_thread.exec, comms_thread.exec } @@ -251,4 +250,4 @@ end parallel.waitForAll(table.unpack(_threads)) println_ts("exited") -log._info("exited") +log.info("exited") diff --git a/rtu/threads.lua b/rtu/threads.lua index 9cb2923..12f90f7 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -1,7 +1,22 @@ --- #REQUIRES comms.lua --- #REQUIRES log.lua --- #REQUIRES ppm.lua --- #REQUIRES util.lua +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local redstone_rtu = require("dev.redstone_rtu") +local boiler_rtu = require("dev.boiler_rtu") +local boilerv_rtu = require("dev.boilerv_rtu") +local energymachine_rtu = require("dev.energymachine_rtu") +local imatrix_rtu = require("dev.imatrix_rtu") +local turbine_rtu = require("dev.turbine_rtu") +local turbinev_rtu = require("dev.turbinev_rtu") + +local modbus = require("modbus") + +local threads = {} + +local rtu_t = types.rtu_t local print = util.print local println = util.println @@ -14,10 +29,10 @@ local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) -- main thread -function thread__main(smem) +threads.thread__main = function (smem) -- execute thread local exec = function () - log._debug("main thread start") + log.debug("main thread start") -- advertisement/heartbeat clock local loop_clock = os.startTimer(MAIN_CLOCK) @@ -62,9 +77,9 @@ function thread__main(smem) -- we only care if this is our wireless modem if device.dev == rtu_dev.modem then println_ts("wireless modem disconnected!") - log._warning("comms modem disconnected!") + log.warning("comms modem disconnected!") else - log._warning("non-comms modem disconnected") + log.warning("non-comms modem disconnected") end else for i = 1, #units do @@ -88,9 +103,9 @@ function thread__main(smem) rtu_comms.reconnect_modem(rtu_dev.modem) println_ts("wireless modem reconnected.") - log._info("comms modem reconnected.") + log.info("comms modem reconnected.") else - log._info("wired modem reconnected.") + log.info("wired modem reconnected.") end else -- relink lost peripheral to correct unit entry @@ -102,11 +117,17 @@ function thread__main(smem) -- found, re-link unit.device = device - if unit.type == "boiler" then + if unit.type == rtu_t.boiler then unit.rtu = boiler_rtu.new(device) - elseif unit.type == "turbine" then + elseif unit.type == rtu_t.boiler_valve then + unit.rtu = boilerv_rtu.new(device) + elseif unit.type == rtu_t.turbine then unit.rtu = turbine_rtu.new(device) - elseif unit.type == "imatrix" then + elseif unit.type == rtu_t.turbine_valve then + unit.rtu = turbinev_rtu.new(device) + elseif unit.type == rtu_t.energy_machine then + unit.rtu = energymachine_rtu.new(device) + elseif unit.type == rtu_t.induction_matrix then unit.rtu = imatrix_rtu.new(device) end @@ -121,7 +142,7 @@ function thread__main(smem) -- check for termination request if event == "terminate" or ppm.should_terminate() then rtu_state.shutdown = true - log._info("terminate requested, main thread exiting") + log.info("terminate requested, main thread exiting") break end end @@ -131,10 +152,10 @@ function thread__main(smem) end -- communications handler thread -function thread__comms(smem) +threads.thread__comms = function (smem) -- execute thread local exec = function () - log._debug("comms thread start") + log.debug("comms thread start") -- load in from shared memory local rtu_state = smem.rtu_state @@ -169,8 +190,8 @@ function thread__comms(smem) -- check for termination request if rtu_state.shutdown then - rtu_comms.close() - log._info("comms thread exiting") + rtu_comms.close(rtu_state) + log.info("comms thread exiting") break end @@ -183,10 +204,10 @@ function thread__comms(smem) end -- per-unit communications handler thread -function thread__unit_comms(smem, unit) +threads.thread__unit_comms = function (smem, unit) -- execute thread local exec = function () - log._debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")") + log.debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")") -- load in from shared memory local rtu_state = smem.rtu_state @@ -219,7 +240,7 @@ function thread__unit_comms(smem, unit) -- check for termination request if rtu_state.shutdown then - log._info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")") + log.info("rtu unit thread exiting -> " .. unit.name .. "(" .. unit.type .. ")") break end @@ -230,3 +251,5 @@ function thread__unit_comms(smem, unit) return { exec = exec } end + +return threads diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua index e8464a5..7c39bc4 100644 --- a/scada-common/alarm.lua +++ b/scada-common/alarm.lua @@ -1,4 +1,6 @@ --- #REQUIRES util.lua +local util = require("scada-common.util") + +local alarm = {} SEVERITY = { INFO = 0, -- basic info message @@ -9,7 +11,27 @@ SEVERITY = { EMERGENCY = 5 -- critical safety alarm } -function scada_alarm(severity, device, message) +alarm.SEVERITY = SEVERITY + +alarm.severity_to_string = function (severity) + if severity == SEVERITY.INFO then + return "INFO" + elseif severity == SEVERITY.WARNING then + return "WARNING" + elseif severity == SEVERITY.ALERT then + return "ALERT" + elseif severity == SEVERITY.FACILITY then + return "FACILITY" + elseif severity == SEVERITY.SAFETY then + return "SAFETY" + elseif severity == SEVERITY.EMERGENCY then + return "EMERGENCY" + else + return "UNKNOWN" + end +end + +alarm.scada_alarm = function (severity, device, message) local self = { time = util.time(), ts_string = os.date("[%H:%M:%S]"), @@ -19,7 +41,7 @@ function scada_alarm(severity, device, message) } local format = function () - return self.ts_string .. " [" .. severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message + return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message end local properties = function () @@ -37,20 +59,4 @@ function scada_alarm(severity, device, message) } end -function severity_to_string(severity) - if severity == SEVERITY.INFO then - return "INFO" - elseif severity == SEVERITY.WARNING then - return "WARNING" - elseif severity == SEVERITY.ALERT then - return "ALERT" - elseif severity == SEVERITY.FACILITY then - return "FACILITY" - elseif severity == SEVERITY.SAFETY then - return "SAFETY" - elseif severity == SEVERITY.EMERGENCY then - return "EMERGENCY" - else - return "UNKNOWN" - end -end +return alarm diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 581e493..17050a8 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,4 +1,10 @@ -PROTOCOLS = { +-- +-- Communications +-- + +local comms = {} + +local PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol SCADA_MGMT = 2, -- SCADA supervisor management, device advertisements, etc @@ -6,7 +12,7 @@ PROTOCOLS = { COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } -RPLC_TYPES = { +local RPLC_TYPES = { KEEP_ALIVE = 0, -- keep alive packets LINK_REQ = 1, -- linking requests STATUS = 2, -- reactor/system status @@ -19,13 +25,13 @@ RPLC_TYPES = { ISS_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately) } -RPLC_LINKING = { +local RPLC_LINKING = { ALLOW = 0, -- link approved DENY = 1, -- link denied COLLISION = 2 -- link denied due to existing active link } -SCADA_MGMT_TYPES = { +local SCADA_MGMT_TYPES = { PING = 0, -- generic ping CLOSE = 1, -- close a connection REMOTE_LINKED = 2, -- remote device linked @@ -33,15 +39,21 @@ SCADA_MGMT_TYPES = { RTU_HEARTBEAT = 4 -- RTU heartbeat } -RTU_ADVERT_TYPES = { +local RTU_ADVERT_TYPES = { BOILER = 0, -- boiler TURBINE = 1, -- turbine IMATRIX = 2, -- induction matrix REDSTONE = 3 -- redstone I/O } +comms.PROTOCOLS = PROTOCOLS +comms.RPLC_TYPES = RPLC_TYPES +comms.RPLC_LINKING = RPLC_LINKING +comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES +comms.RTU_ADVERT_TYPES = RTU_ADVERT_TYPES + -- generic SCADA packet object -function scada_packet() +comms.scada_packet = function () local self = { modem_msg_in = nil, valid = false, @@ -124,7 +136,7 @@ end -- MODBUS packet -- modeled after MODBUS TCP packet -function modbus_packet() +comms.modbus_packet = function () local self = { frame = nil, raw = nil, @@ -165,11 +177,11 @@ function modbus_packet() return size_ok else - log._debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) + log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) return false end else - log._debug("nil frame encountered", true) + log.debug("nil frame encountered", true) return false end end @@ -201,7 +213,7 @@ function modbus_packet() end -- reactor PLC packet -function rplc_packet() +comms.rplc_packet = function () local self = { frame = nil, raw = nil, @@ -256,11 +268,11 @@ function rplc_packet() return ok else - log._debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) + log.debug("attempted RPLC parse of incorrect protocol " .. frame.protocol(), true) return false end else - log._debug("nil frame encountered", true) + log.debug("nil frame encountered", true) return false end end @@ -291,7 +303,7 @@ function rplc_packet() end -- SCADA management packet -function mgmt_packet() +comms.mgmt_packet = function () local self = { frame = nil, raw = nil, @@ -339,11 +351,11 @@ function mgmt_packet() return ok else - log._debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) + log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) return false end else - log._debug("nil frame encountered", true) + log.debug("nil frame encountered", true) return false end end @@ -374,7 +386,7 @@ end -- SCADA coordinator packet -- @todo -function coord_packet() +comms.coord_packet = function () local self = { frame = nil, raw = nil, @@ -418,11 +430,11 @@ function coord_packet() return ok else - log._debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true) + log.debug("attempted COORD_DATA parse of incorrect protocol " .. frame.protocol(), true) return false end else - log._debug("nil frame encountered", true) + log.debug("nil frame encountered", true) return false end end @@ -453,7 +465,7 @@ end -- coordinator API (CAPI) packet -- @todo -function capi_packet() +comms.capi_packet = function () local self = { frame = nil, raw = nil, @@ -497,11 +509,11 @@ function capi_packet() return ok else - log._debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true) + log.debug("attempted COORD_API parse of incorrect protocol " .. frame.protocol(), true) return false end else - log._debug("nil frame encountered", true) + log.debug("nil frame encountered", true) return false end end @@ -529,3 +541,5 @@ function capi_packet() get = get } end + +return comms diff --git a/scada-common/log.lua b/scada-common/log.lua index 1aafda3..39069ca 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -2,14 +2,17 @@ -- File System Logger -- --- we use extra short abbreviations since computer craft screens are very small --- underscores are used since some of these names are used elsewhere (e.g. 'debug' is a lua table) +local log = {} -MODE = { +-- we use extra short abbreviations since computer craft screens are very small + +local MODE = { APPEND = 0, NEW = 1 } +log.MODE = MODE + local LOG_DEBUG = true local log_path = "/log.txt" @@ -50,7 +53,7 @@ local _log = function (msg) end end -function init(path, write_mode) +log.init = function (path, write_mode) log_path = path mode = write_mode @@ -61,7 +64,7 @@ function init(path, write_mode) end end -function _debug(msg, trace) +log.debug = function (msg, trace) if LOG_DEBUG then local dbg_info = "" @@ -80,15 +83,15 @@ function _debug(msg, trace) end end -function _info(msg) +log.info = function (msg) _log("[INF] " .. msg) end -function _warning(msg) +log.warning = function (msg) _log("[WRN] " .. msg) end -function _error(msg, trace) +log.error = function (msg, trace) local dbg_info = "" if trace then @@ -105,6 +108,8 @@ function _error(msg, trace) _log("[ERR] " .. dbg_info .. msg) end -function _fatal(msg) +log.fatal = function (msg) _log("[FTL] " .. msg) end + +return log diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index dc3e47f..8ba14cd 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -2,13 +2,17 @@ -- Message Queue -- -TYPE = { +local mqueue = {} + +local TYPE = { COMMAND = 0, DATA = 1, PACKET = 2 } -function new() +mqueue.TYPE = TYPE + +mqueue.new = function () local queue = {} local length = function () @@ -57,3 +61,5 @@ function new() pop = pop } end + +return mqueue diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index f383bae..5e15724 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -1,10 +1,14 @@ --- #REQUIRES log.lua +local log = require("scada-common.log") -- -- Protected Peripheral Manager -- -ACCESS_FAULT = nil +local ppm = {} + +local ACCESS_FAULT = nil + +ppm.ACCESS_FAULT = ACCESS_FAULT ---------------------------- -- PRIVATE DATA/FUNCTIONS -- @@ -46,7 +50,7 @@ local peri_init = function (iface) _ppm_sys.faulted = true if not _ppm_sys.mute then - log._error("PPM: protected " .. key .. "() -> " .. result) + log.error("PPM: protected " .. key .. "() -> " .. result) end if result == "Terminated" then @@ -88,48 +92,48 @@ end -- REPORTING -- -- silence error prints -function disable_reporting() +ppm.disable_reporting = function () _ppm_sys.mute = true end -- allow error prints -function enable_reporting() +ppm.enable_reporting = function () _ppm_sys.mute = false end -- FAULT MEMORY -- -- enable automatically clearing fault flag -function enable_afc() +ppm.enable_afc = function () _ppm_sys.auto_cf = true end -- disable automatically clearing fault flag -function disable_afc() +ppm.disable_afc = function () _ppm_sys.auto_cf = false end -- check fault flag -function is_faulted() +ppm.is_faulted = function () return _ppm_sys.faulted end -- clear fault flag -function clear_fault() +ppm.clear_fault = function () _ppm_sys.faulted = false end -- TERMINATION -- -- if a caught error was a termination request -function should_terminate() +ppm.should_terminate = function () return _ppm_sys.terminate end -- MOUNTING -- -- mount all available peripherals (clears mounts first) -function mount_all() +ppm.mount_all = function () local ifaces = peripheral.getNames() _ppm_sys.mounts = {} @@ -137,23 +141,23 @@ function mount_all() for i = 1, #ifaces do _ppm_sys.mounts[ifaces[i]] = peri_init(ifaces[i]) - log._info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") + log.info("PPM: found a " .. _ppm_sys.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") end if #ifaces == 0 then - log._warning("PPM: mount_all() -> no devices found") + log.warning("PPM: mount_all() -> no devices found") end end -- mount a particular device -function mount(iface) +ppm.mount = function (iface) local ifaces = peripheral.getNames() local pm_dev = nil local pm_type = nil for i = 1, #ifaces do if iface == ifaces[i] then - log._info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) + log.info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) _ppm_sys.mounts[iface] = peri_init(iface) @@ -167,15 +171,15 @@ function mount(iface) end -- handle peripheral_detach event -function handle_unmount(iface) +ppm.handle_unmount = function (iface) -- what got disconnected? local lost_dev = _ppm_sys.mounts[iface] if lost_dev then local type = lost_dev.type - log._warning("PPM: lost device " .. type .. " mounted to " .. iface) + log.warning("PPM: lost device " .. type .. " mounted to " .. iface) else - log._error("PPM: lost device unknown to the PPM mounted to " .. iface) + log.error("PPM: lost device unknown to the PPM mounted to " .. iface) end return lost_dev @@ -184,31 +188,31 @@ end -- GENERAL ACCESSORS -- -- list all available peripherals -function list_avail() +ppm.list_avail = function () return peripheral.getNames() end -- list mounted peripherals -function list_mounts() +ppm.list_mounts = function () return _ppm_sys.mounts end -- get a mounted peripheral by side/interface -function get_periph(iface) +ppm.get_periph = function (iface) if _ppm_sys.mounts[iface] then return _ppm_sys.mounts[iface].dev else return nil end end -- get a mounted peripheral type by side/interface -function get_type(iface) +ppm.get_type = function (iface) if _ppm_sys.mounts[iface] then return _ppm_sys.mounts[iface].type else return nil end end -- get all mounted peripherals by type -function get_all_devices(name) +ppm.get_all_devices = function (name) local devices = {} for side, data in pairs(_ppm_sys.mounts) do @@ -221,7 +225,7 @@ function get_all_devices(name) end -- get a mounted peripheral by type (if multiple, returns the first) -function get_device(name) +ppm.get_device = function (name) local device = nil for side, data in pairs(_ppm_sys.mounts) do @@ -237,12 +241,12 @@ end -- SPECIFIC DEVICE ACCESSORS -- -- get the fission reactor (if multiple, returns the first) -function get_fission_reactor() - return get_device("fissionReactor") +ppm.get_fission_reactor = function () + return ppm.get_device("fissionReactor") end -- get the wireless modem (if multiple, returns the first) -function get_wireless_modem() +ppm.get_wireless_modem = function () local w_modem = nil for side, device in pairs(_ppm_sys.mounts) do @@ -256,6 +260,8 @@ function get_wireless_modem() end -- list all connected monitors -function list_monitors() - return get_all_devices("monitor") +ppm.list_monitors = function () + return ppm.get_all_devices("monitor") end + +return ppm diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index f56a8a4..fd71247 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -1,21 +1,27 @@ -IO_LVL = { +-- +-- Redstone I/O +-- + +local rsio = {} + +local IO_LVL = { LOW = 0, HIGH = 1 } -IO_DIR = { +local IO_DIR = { IN = 0, OUT = 1 } -IO_MODE = { +local IO_MODE = { DIGITAL_OUT = 0, DIGITAL_IN = 1, ANALOG_OUT = 2, ANALOG_IN = 3 } -RS_IO = { +local RS_IO = { -- digital inputs -- -- facility @@ -53,7 +59,12 @@ RS_IO = { A_T_FLOW_RATE = 21 -- turbine flow rate percentage } -function to_string(channel) +rsio.IO_LVL = IO_LVL +rsio.IO_DIR = IO_DIR +rsio.IO_MODE = IO_MODE +rsio.IO = RS_IO + +rsio.to_string = function (channel) local names = { "F_SCRAM", "F_AE2_LIVE", @@ -85,11 +96,11 @@ function to_string(channel) end end -function is_valid_channel(channel) +rsio.is_valid_channel = function (channel) return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE end -function is_valid_side(side) +rsio.is_valid_side = function (side) if side ~= nil then for _, s in pairs(rs.getSides()) do if s == side then return true end @@ -98,7 +109,7 @@ function is_valid_side(side) return false end -function is_color(color) +rsio.is_color = function (color) return (color > 0) and (bit.band(color, (color - 1)) == 0); end @@ -149,7 +160,7 @@ local RS_DIO_MAP = { { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT } } -function get_io_mode(channel) +rsio.get_io_mode = function (channel) local modes = { IO_MODE.DIGITAL_IN, -- F_SCRAM IO_MODE.DIGITAL_IN, -- F_AE2_LIVE @@ -182,7 +193,7 @@ function get_io_mode(channel) end -- get digital IO level reading -function digital_read(rs_value) +rsio.digital_read = function (rs_value) if rs_value then return IO_LVL.HIGH else @@ -191,7 +202,7 @@ function digital_read(rs_value) end -- returns the level corresponding to active -function digital_write(channel, active) +rsio.digital_write = function (channel, active) if channel < RS_IO.WASTE_PO or channel > RS_IO.R_PLC_TIMEOUT then return IO_LVL.LOW else @@ -200,10 +211,12 @@ function digital_write(channel, active) end -- returns true if the level corresponds to active -function digital_is_active(channel, level) +rsio.digital_is_active = function (channel, level) if channel > RS_IO.R_ENABLE or channel > RS_IO.R_PLC_TIMEOUT then return false else return RS_DIO_MAP[channel]._f(level) end end + +return rsio diff --git a/scada-common/types.lua b/scada-common/types.lua index 8ae93cc..5346bca 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -1,6 +1,10 @@ +-- -- Global Types +-- -rtu_t = { +local types = {} + +types.rtu_t = { redstone = "redstone", boiler = "boiler", boiler_valve = "boiler_valve", @@ -10,7 +14,7 @@ rtu_t = { induction_matrix = "induction_matrix" } -iss_status_t = { +types.iss_status_t = { ok = "ok", dmg_crit = "dmg_crit", ex_hcoolant = "heated_coolant_backup", @@ -24,7 +28,7 @@ iss_status_t = { -- MODBUS -- modbus function codes -local MODBUS_FCODE = { +types.MODBUS_FCODE = { READ_COILS = 0x01, READ_DISCRETE_INPUTS = 0x02, READ_MUL_HOLD_REGS = 0x03, @@ -37,7 +41,7 @@ local MODBUS_FCODE = { } -- modbus exception codes -local MODBUS_EXCODE = { +types.MODBUS_EXCODE = { ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDR = 0x02, ILLEGAL_DATA_VALUE = 0x03, @@ -49,3 +53,5 @@ local MODBUS_EXCODE = { GATEWAY_PATH_UNAVAILABLE = 0x0A, GATEWAY_TARGET_TIMEOUT = 0x0B } + +return types diff --git a/scada-common/util.lua b/scada-common/util.lua index a226e9f..a963a08 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -1,70 +1,69 @@ +local util = {} + -- PRINT -- --- we are overwriting 'print' so save it first -local _print = print - -- print -function print(message) +util.print = function (message) term.write(message) end -- print line -function println(message) - _print(message) +util.println = function (message) + print(message) end -- timestamped print -function print_ts(message) +util.print_ts = function (message) term.write(os.date("[%H:%M:%S] ") .. message) end -- timestamped print line -function println_ts(message) - _print(os.date("[%H:%M:%S] ") .. message) +util.println_ts = function (message) + print(os.date("[%H:%M:%S] ") .. message) end -- TIME -- -function time_ms() +util.time_ms = function () return os.epoch('local') end -function time_s() +util.time_s = function () return os.epoch('local') / 1000 end -function time() - return time_ms() +util.time = function () + return util.time_ms() end -- PARALLELIZATION -- -- protected sleep call so we still are in charge of catching termination -function psleep(t) +util.psleep = function (t) pcall(os.sleep, t) end -- no-op to provide a brief pause (and a yield) -- EVENT_CONSUMER: this function consumes events -function nop() - psleep(0.05) +util.nop = function () + util.psleep(0.05) end -- attempt to maintain a minimum loop timing (duration of execution) -function adaptive_delay(target_timing, last_update) - local sleep_for = target_timing - (time() - last_update) +util.adaptive_delay = function (target_timing, last_update) + local sleep_for = target_timing - (util.time() - last_update) -- only if >50ms since worker loops already yield 0.05s if sleep_for >= 50 then - psleep(sleep_for / 1000.0) + util.psleep(sleep_for / 1000.0) end - return time() + return util.time() end -- WATCHDOG -- -- ComputerCraft OS Timer based Watchdog -- triggers a timer event if not fed within 'timeout' seconds -function new_watchdog(timeout) +util.new_watchdog = function (timeout) local self = { _timeout = timeout, _wd_timer = os.startTimer(timeout) @@ -93,3 +92,5 @@ function new_watchdog(timeout) cancel = cancel } end + +return util diff --git a/supervisor/config.lua b/supervisor/config.lua index b8ba7fa..734e820 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -1,12 +1,16 @@ +local config = {} + -- scada network listen for PLC's and RTU's -SCADA_DEV_LISTEN = 16000 +config.SCADA_DEV_LISTEN = 16000 -- listen port for SCADA supervisor access by coordinators -SCADA_SV_LISTEN = 16100 +config.SCADA_SV_LISTEN = 16100 -- expected number of reactors -NUM_REACTORS = 4 +config.NUM_REACTORS = 4 -- log path -LOG_PATH = "/log.txt" +config.LOG_PATH = "/log.txt" -- log mode -- 0 = APPEND (adds to existing file on start) -- 1 = NEW (replaces existing file on start) -LOG_MODE = 0 +config.LOG_MODE = 0 + +return config diff --git a/supervisor/session/coordinator.lua b/supervisor/session/coordinator.lua index e69de29..afa28c0 100644 --- a/supervisor/session/coordinator.lua +++ b/supervisor/session/coordinator.lua @@ -0,0 +1,3 @@ +local coordinator = {} + +return coordinator diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 09aa033..ee11819 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -1,7 +1,9 @@ --- #REQUIRES mqueue.lua --- #REQUIRES comms.lua --- #REQUIRES log.lua --- #REQUIRES util.lua +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local util = require("scada-common.util") + +local plc = {} local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES @@ -16,19 +18,21 @@ local println_ts = util.println_ts local INITIAL_WAIT = 1500 local RETRY_PERIOD = 1000 -PLC_S_CMDS = { +local PLC_S_CMDS = { SCRAM = 0, ENABLE = 1, BURN_RATE = 2, ISS_CLEAR = 3 } +plc.PLC_S_CMDS = PLC_S_CMDS + local PERIODICS = { KEEP_ALIVE = 2.0 } -- PLC supervisor session -function new_session(id, for_reactor, in_queue, out_queue) +plc.new_session = function (id, for_reactor, in_queue, out_queue) local log_header = "plc_session(" .. id .. "): " local self = { @@ -204,7 +208,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if pkt.length == 1 then return pkt.data[1] else - log._warning(log_header .. "RPLC ACK length mismatch") + log.warning(log_header .. "RPLC ACK length mismatch") return nil end end @@ -215,7 +219,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if self.r_seq_num == nil then self.r_seq_num = pkt.scada_frame.seq_num() elseif self.r_seq_num >= pkt.scada_frame.seq_num() then - log._warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) + log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) return else self.r_seq_num = pkt.scada_frame.seq_num() @@ -225,7 +229,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if pkt.scada_frame.protocol() == PROTOCOLS.RPLC then -- check reactor ID if pkt.id ~= for_reactor then - log._warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id) + log.warning(log_header .. "RPLC packet with ID not matching reactor ID: reactor " .. self.for_reactor .. " != " .. pkt.id) return end @@ -242,13 +246,13 @@ function new_session(id, for_reactor, in_queue, out_queue) self.last_rtt = srv_now - srv_start if self.last_rtt > 500 then - log._warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")") + log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")") end - -- log._debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms") - -- log._debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms") + -- log.debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms") + -- log.debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms") else - log._debug(log_header .. "RPLC keep alive packet length mismatch") + log.debug(log_header .. "RPLC keep alive packet length mismatch") end elseif pkt.type == RPLC_TYPES.STATUS then -- status packet received, update data @@ -267,11 +271,11 @@ function new_session(id, for_reactor, in_queue, out_queue) self.received_status_cache = true else -- error copying status data - log._error(log_header .. "failed to parse status packet data") + log.error(log_header .. "failed to parse status packet data") end end else - log._debug(log_header .. "RPLC status packet length mismatch") + log.debug(log_header .. "RPLC status packet length mismatch") end elseif pkt.type == RPLC_TYPES.MEK_STRUCT then -- received reactor structure, record it @@ -282,10 +286,10 @@ function new_session(id, for_reactor, in_queue, out_queue) self.received_struct = true else -- error copying structure data - log._error(log_header .. "failed to parse struct packet data") + log.error(log_header .. "failed to parse struct packet data") end else - log._debug(log_header .. "RPLC struct packet length mismatch") + log.debug(log_header .. "RPLC struct packet length mismatch") end elseif pkt.type == RPLC_TYPES.MEK_SCRAM then -- SCRAM acknowledgement @@ -294,7 +298,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.acks.scram = true self.sDB.control_state = false elseif ack == false then - log._debug(log_header .. "SCRAM failed!") + log.debug(log_header .. "SCRAM failed!") end elseif pkt.type == RPLC_TYPES.MEK_ENABLE then -- enable acknowledgement @@ -303,7 +307,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.acks.enable = true self.sDB.control_state = true elseif ack == false then - log._debug(log_header .. "enable failed!") + log.debug(log_header .. "enable failed!") end elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then -- burn rate acknowledgement @@ -311,7 +315,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if ack then self.acks.burn_rate = true elseif ack == false then - log._debug(log_header .. "burn rate update failed!") + log.debug(log_header .. "burn rate update failed!") end elseif pkt.type == RPLC_TYPES.ISS_STATUS then -- ISS status packet received, copy data @@ -321,10 +325,10 @@ function new_session(id, for_reactor, in_queue, out_queue) -- copied in ISS status data OK else -- error copying ISS status data - log._error(log_header .. "failed to parse ISS status packet data") + log.error(log_header .. "failed to parse ISS status packet data") end else - log._debug(log_header .. "RPLC ISS status packet length mismatch") + log.debug(log_header .. "RPLC ISS status packet length mismatch") end elseif pkt.type == RPLC_TYPES.ISS_ALARM then -- ISS alarm @@ -337,10 +341,10 @@ function new_session(id, for_reactor, in_queue, out_queue) -- copied in ISS status data OK else -- error copying ISS status data - log._error(log_header .. "failed to parse ISS alarm status data") + log.error(log_header .. "failed to parse ISS alarm status data") end else - log._debug(log_header .. "RPLC ISS alarm packet length mismatch") + log.debug(log_header .. "RPLC ISS alarm packet length mismatch") end elseif pkt.type == RPLC_TYPES.ISS_CLEAR then -- ISS clear acknowledgement @@ -350,17 +354,17 @@ function new_session(id, for_reactor, in_queue, out_queue) self.sDB.iss_tripped = false self.sDB.iss_trip_cause = "ok" elseif ack == false then - log._debug(log_header .. "ISS clear failed") + log.debug(log_header .. "ISS clear failed") end 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 elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then if pkt.type == SCADA_MGMT_TYPES.CLOSE then -- close the session self.connected = false 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 end @@ -402,7 +406,7 @@ function new_session(id, for_reactor, in_queue, out_queue) self.connected = false _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) println("connection to reactor " .. self.for_reactor .. " PLC closed by server") - log._info(log_header .. "session closed by server") + log.info(log_header .. "session closed by server") end -- iterate the session @@ -454,7 +458,7 @@ function new_session(id, for_reactor, in_queue, out_queue) -- max 100ms spent processing queue if util.time() - handle_start > 100 then - log._warning(log_header .. "exceeded 100ms queue process limit") + log.warning(log_header .. "exceeded 100ms queue process limit") break end end @@ -463,7 +467,7 @@ function new_session(id, for_reactor, in_queue, out_queue) if not self.connected then self.plc_conn_watchdog.cancel() println("connection to reactor " .. self.for_reactor .. " 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 end @@ -559,3 +563,5 @@ function new_session(id, for_reactor, in_queue, out_queue) iterate = iterate } end + +return plc diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index e69de29..9051425 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -0,0 +1,3 @@ +local rtu = {} + +return rtu diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 6612730..578c8ae 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -1,14 +1,22 @@ --- #REQUIRES mqueue.lua --- #REQUIRES log.lua +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") + +local coordinator = require("session.coordinator") +local plc = require("session.plc") +local rtu = require("session.rtu") -- Supervisor Sessions Handler -SESSION_TYPE = { +local svsessions = {} + +local SESSION_TYPE = { RTU_SESSION = 0, PLC_SESSION = 1, COORD_SESSION = 2 } +svsessions.SESSION_TYPE = SESSION_TYPE + local self = { modem = nil, num_reactors = 0, @@ -20,12 +28,97 @@ local self = { next_coord_id = 0 } -function link_modem(modem) +-- PRIVATE FUNCTIONS -- + +-- iterate all the given sessions +local function _iterate(sessions) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + local ok = session.instance.iterate() + if ok then + -- send packets in out queue + while session.out_queue.ready() do + local msg = session.out_queue.pop() + if msg.qtype == mqueue.TYPE.PACKET then + self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + end + end + else + session.open = false + end + end + end +end + +-- cleanly close a session +local function _shutdown(session) + session.open = false + session.instance.close() + + -- send packets in out queue (namely the close packet) + while session.out_queue.ready() do + local msg = session.out_queue.pop() + if msg.qtype == mqueue.TYPE.PACKET then + self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) + end + end + + log.debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) +end + +-- close connections +local function _close(sessions) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + _shutdown(session) + end + end +end + +-- check if a watchdog timer event matches that of one of the provided sessions +local function _check_watchdogs(sessions, timer_event) + for i = 1, #sessions do + local session = sessions[i] + if session.open then + local triggered = session.instance.check_wd(timer_event) + if triggered then + log.debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port .. "...") + _shutdown(session) + end + end + end +end + +-- delete any closed sessions +local function _free_closed(sessions) + local move_to = 1 + for i = 1, #sessions do + local session = sessions[i] + if session ~= nil then + if sessions[i].open then + if sessions[move_to] == nil then + sessions[move_to] = session + sessions[i] = nil + end + move_to = move_to + 1 + else + log.debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) + sessions[i] = nil + end + end + end +end + +-- PUBLIC FUNCTIONS -- + +svsessions.link_modem = function (modem) self.modem = modem end -- find a session by the remote port -function find_session(remote_port) +svsessions.find_session = function (remote_port) -- check RTU sessions for i = 1, #self.rtu_sessions do if self.rtu_sessions[i].r_port == remote_port then @@ -51,7 +144,7 @@ function find_session(remote_port) end -- get a session by reactor ID -function get_reactor_session(reactor) +svsessions.get_reactor_session = function (reactor) local session = nil for i = 1, #self.plc_sessions do @@ -64,8 +157,8 @@ function get_reactor_session(reactor) end -- establish a new PLC session -function establish_plc_session(local_port, remote_port, for_reactor) - if get_reactor_session(for_reactor) == nil then +svsessions.establish_plc_session = function (local_port, remote_port, for_reactor) + if svsessions.get_reactor_session(for_reactor) == nil then local plc_s = { open = true, reactor = for_reactor, @@ -79,7 +172,7 @@ function establish_plc_session(local_port, remote_port, for_reactor) plc_s.instance = plc.new_session(self.next_plc_id, for_reactor, plc_s.in_queue, plc_s.out_queue) table.insert(self.plc_sessions, plc_s) - log._debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id) + log.debug("established new PLC session to " .. remote_port .. " with ID " .. self.next_plc_id) self.next_plc_id = self.next_plc_id + 1 @@ -91,38 +184,8 @@ function establish_plc_session(local_port, remote_port, for_reactor) end end --- cleanly close a session -local function _shutdown(session) - session.open = false - session.instance.close() - - -- send packets in out queue (namely the close packet) - while session.out_queue.ready() do - local msg = session.out_queue.pop() - if msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) - end - end - - log._debug("closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) -end - --- check if a watchdog timer event matches that of one of the provided sessions -local function _check_watchdogs(sessions, timer_event) - for i = 1, #sessions do - local session = sessions[i] - if session.open then - local triggered = session.instance.check_wd(timer_event) - if triggered then - log._debug("watchdog closing session " .. session.instance.get_id() .. " on remote port " .. session.r_port .. "...") - _shutdown(session) - end - end - end -end - -- attempt to identify which session's watchdog timer fired -function check_all_watchdogs(timer_event) +svsessions.check_all_watchdogs = function (timer_event) -- check RTU session watchdogs _check_watchdogs(self.rtu_sessions, timer_event) @@ -133,29 +196,8 @@ function check_all_watchdogs(timer_event) _check_watchdogs(self.coord_sessions, timer_event) end --- iterate all the given sessions -local function _iterate(sessions) - for i = 1, #sessions do - local session = sessions[i] - if session.open then - local ok = session.instance.iterate() - if ok then - -- send packets in out queue - while session.out_queue.ready() do - local msg = session.out_queue.pop() - if msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) - end - end - else - session.open = false - end - end - end -end - -- iterate all sessions -function iterate_all() +svsessions.iterate_all = function () -- iterate RTU sessions _iterate(self.rtu_sessions) @@ -166,28 +208,8 @@ function iterate_all() _iterate(self.coord_sessions) end --- delete any closed sessions -local function _free_closed(sessions) - local move_to = 1 - for i = 1, #sessions do - local session = sessions[i] - if session ~= nil then - if sessions[i].open then - if sessions[move_to] == nil then - sessions[move_to] = session - sessions[i] = nil - end - move_to = move_to + 1 - else - log._debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) - sessions[i] = nil - end - end - end -end - -- delete all closed sessions -function free_all_closed() +svsessions.free_all_closed = function () -- free closed RTU sessions _free_closed(self.rtu_sessions) @@ -198,23 +220,15 @@ function free_all_closed() _free_closed(self.coord_sessions) end --- close connections -local function _close(sessions) - for i = 1, #sessions do - local session = sessions[i] - if session.open then - _shutdown(session) - end - end -end - -- close all open connections -function close_all() +svsessions.close_all = function () -- close sessions _close(self.rtu_sessions) _close(self.plc_sessions) _close(self.coord_sessions) -- free sessions - free_all_closed() + svsessions.free_all_closed() end + +return svsessions diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 53f6cfc..3059bab 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -2,23 +2,19 @@ -- Nuclear Generation Facility SCADA Supervisor -- -os.loadAPI("scada-common/log.lua") -os.loadAPI("scada-common/types.lua") -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/ppm.lua") -os.loadAPI("scada-common/comms.lua") -os.loadAPI("scada-common/mqueue.lua") +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local util = require("scada-common.util") -os.loadAPI("config.lua") +local coordinator = require("session.coordinator") +local plc = require("session.plc") +local rtu = require("session.rtu") +local svsessions = require("session.svsessions") -os.loadAPI("session/rtu.lua") -os.loadAPI("session/plc.lua") -os.loadAPI("session/coordinator.lua") -os.loadAPI("session/svsessions.lua") +local config = require("config") +local supervisor = require("supervisor") -os.loadAPI("supervisor.lua") - -local SUPERVISOR_VERSION = "alpha-v0.2.0" +local SUPERVISOR_VERSION = "alpha-v0.3.0" local print = util.print local println = util.println @@ -27,9 +23,9 @@ local println_ts = util.println_ts log.init(config.LOG_PATH, config.LOG_MODE) -log._info("========================================") -log._info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) -log._info("========================================") +log.info("========================================") +log.info("BOOTING supervisor.startup " .. SUPERVISOR_VERSION) +log.info("========================================") println(">> SCADA Supervisor " .. SUPERVISOR_VERSION .. " <<") -- mount connected devices @@ -38,12 +34,12 @@ ppm.mount_all() local modem = ppm.get_wireless_modem() if modem == nil then println("boot> wireless modem not found") - log._warning("no wireless modem on startup") + log.warning("no wireless modem on startup") return end -- start comms, open all channels -local superv_comms = supervisor.superv_comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) +local superv_comms = supervisor.comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 @@ -61,9 +57,9 @@ while true do -- we only care if this is our wireless modem if device.dev == modem then println_ts("wireless modem disconnected!") - log._error("comms modem disconnected!") + log.error("comms modem disconnected!") else - log._warning("non-comms modem disconnected") + log.warning("non-comms modem disconnected") end end elseif event == "peripheral" then @@ -76,9 +72,9 @@ while true do superv_comms.reconnect_modem(modem) println_ts("wireless modem reconnected.") - log._info("comms modem reconnected.") + log.info("comms modem reconnected.") else - log._info("wired modem reconnected.") + log.info("wired modem reconnected.") end end elseif event == "timer" and param1 == loop_clock then @@ -103,12 +99,12 @@ while true do -- check for termination request if event == "terminate" or ppm.should_terminate() then println_ts("closing sessions...") - log._info("terminate requested, closing sessions...") + log.info("terminate requested, closing sessions...") svsessions.close_all() - log._info("sessions closed") + log.info("sessions closed") break end end println_ts("exited") -log._info("exited") +log.info("exited") diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index edda143..3d4fa17 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -1,7 +1,10 @@ --- #REQUIRES comms.lua --- #REQUIRES mqueue.lua --- #REQUIRES util.lua --- #REQUIRES svsessions.lua +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local util = require("scada-common.util") + +local svsessions = require("session.svsessions") + +local supervisor = {} local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES @@ -17,7 +20,7 @@ local print_ts = util.print_ts local println_ts = util.println_ts -- supervisory controller communications -function superv_comms(num_reactors, modem, dev_listen, coord_listen) +supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) local self = { ln_seq_num = 0, num_reactors = num_reactors, @@ -101,7 +104,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) pkt = coord_pkt.get() end else - log._debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) + log.debug("attempted parse of illegal packet type " .. s_pkt.protocol(), true) end end @@ -126,7 +129,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) if session then if packet.type == RPLC_TYPES.LINK_REQ then -- new device on this port? that's a collision - log._debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision") + log.debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision") _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) else -- pass the packet onto the session handler @@ -140,20 +143,20 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) local plc_id = svsessions.establish_plc_session(l_port, r_port, packet.data[1]) if plc_id == false then -- reactor already has a PLC assigned - log._debug("PLC_LNK: assignment collision with reactor " .. packet.data[1]) + log.debug("PLC_LNK: assignment collision with reactor " .. packet.data[1]) _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully println("connected to reactor " .. packet.data[1] .. " PLC (port " .. r_port .. ")") - log._debug("PLC_LNK: allowed for device at " .. r_port) + log.debug("PLC_LNK: allowed for device at " .. r_port) _send_plc_linking(r_port, { RPLC_LINKING.ALLOW }) end else - log._debug("PLC_LNK: new linking packet length mismatch") + log.debug("PLC_LNK: new linking packet length mismatch") end else -- force a re-link - log._debug("PLC_LNK: no session but not a link, force relink") + log.debug("PLC_LNK: no session but not a link, force relink") _send_plc_linking(r_port, { RPLC_LINKING.DENY }) end end @@ -164,7 +167,7 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) session.in_queue.push_packet(packet) end else - log._debug("illegal packet type " .. protocol .. " on device listening channel") + log.debug("illegal packet type " .. protocol .. " on device listening channel") end -- coordinator listening channel elseif l_port == self.coord_listen then @@ -173,10 +176,10 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) elseif protocol == PROTOCOLS.COORD_DATA then -- coordinator packet else - log._debug("illegal packet type " .. protocol .. " on coordinator listening channel") + log.debug("illegal packet type " .. protocol .. " on coordinator listening channel") end else - log._error("received packet on unused channel " .. l_port, true) + log.error("received packet on unused channel " .. l_port, true) end end end @@ -187,3 +190,5 @@ function superv_comms(num_reactors, modem, dev_listen, coord_listen) handle_packet = handle_packet } end + +return supervisor From c4df8eabf95c1c88c23b91871f161d7ff5a0a0dc Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 5 May 2022 11:55:04 -0400 Subject: [PATCH 090/168] #43 rename ISS to RPS --- reactor-plc/plc.lua | 92 +++++++++++++++++++------------------- reactor-plc/startup.lua | 24 +++++----- reactor-plc/threads.lua | 72 ++++++++++++++--------------- scada-common/comms.lua | 12 ++--- scada-common/types.lua | 2 +- supervisor/session/plc.lua | 92 +++++++++++++++++++------------------- supervisor/startup.lua | 2 +- 7 files changed, 148 insertions(+), 148 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 54f47d2..d53ad95 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -6,7 +6,7 @@ local util = require("scada-common.util") local plc = {} -local iss_status_t = types.iss_status_t +local rps_status_t = types.rps_status_t local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES @@ -18,10 +18,10 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts --- Internal Safety System +-- Reactor Protection System -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main SCADA supervisor/coordinator control -plc.iss_init = function (reactor) +plc.rps_init = function (reactor) local self = { reactor = reactor, cache = { false, false, false, false, false, false, false }, @@ -37,7 +37,7 @@ plc.iss_init = function (reactor) local damage_percent = self.reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor damage") + log.error("RPS: failed to check reactor damage") return false else return damage_percent >= 100 @@ -49,7 +49,7 @@ plc.iss_init = function (reactor) local hc_needed = self.reactor.getHeatedCoolantNeeded() if hc_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor heated coolant level") + log.error("RPS: failed to check reactor heated coolant level") return false else return hc_needed == 0 @@ -61,7 +61,7 @@ plc.iss_init = function (reactor) local w_needed = self.reactor.getWasteNeeded() if w_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor waste level") + log.error("RPS: failed to check reactor waste level") return false else return w_needed == 0 @@ -74,7 +74,7 @@ plc.iss_init = function (reactor) local temp = self.reactor.getTemperature() if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor temperature") + log.error("RPS: failed to check reactor temperature") return false else return temp >= 1200 @@ -86,7 +86,7 @@ plc.iss_init = function (reactor) local fuel = self.reactor.getFuel() if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor fuel level") + log.error("RPS: failed to check reactor fuel level") return false else return fuel == 0 @@ -98,7 +98,7 @@ plc.iss_init = function (reactor) local coolant_filled = self.reactor.getCoolantFilledPercentage() if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("ISS: failed to check reactor coolant level") + log.error("RPS: failed to check reactor coolant level") return false else return coolant_filled < 0.02 @@ -119,7 +119,7 @@ plc.iss_init = function (reactor) -- check all safety conditions local check = function () - local status = iss_status_t.ok + local status = rps_status_t.ok local was_tripped = self.tripped -- update cache @@ -137,34 +137,34 @@ plc.iss_init = function (reactor) if self.tripped then status = self.trip_cause elseif self.cache[1] then - log.warning("ISS: damage critical!") - status = iss_status_t.dmg_crit + log.warning("RPS: damage critical!") + status = rps_status_t.dmg_crit elseif self.cache[4] then - log.warning("ISS: high temperature!") - status = iss_status_t.high_temp + log.warning("RPS: high temperature!") + status = rps_status_t.high_temp elseif self.cache[2] then - log.warning("ISS: heated coolant backup!") - status = iss_status_t.ex_hcoolant + log.warning("RPS: heated coolant backup!") + status = rps_status_t.ex_hcoolant elseif self.cache[6] then - log.warning("ISS: no coolant!") - status = iss_status_t.no_coolant + log.warning("RPS: no coolant!") + status = rps_status_t.no_coolant elseif self.cache[3] then - log.warning("ISS: full waste!") - status = iss_status_t.ex_waste + log.warning("RPS: full waste!") + status = rps_status_t.ex_waste elseif self.cache[5] then - log.warning("ISS: no fuel!") - status = iss_status_t.no_fuel + log.warning("RPS: no fuel!") + status = rps_status_t.no_fuel elseif self.cache[7] then - log.warning("ISS: supervisor connection timeout!") - status = iss_status_t.timeout + log.warning("RPS: supervisor connection timeout!") + status = rps_status_t.timeout else self.tripped = false end -- if a new trip occured... local first_trip = false - if not was_tripped and status ~= iss_status_t.ok then - log.warning("ISS: reactor SCRAM") + if not was_tripped and status ~= rps_status_t.ok then + log.warning("RPS: reactor SCRAM") first_trip = true self.tripped = true @@ -172,22 +172,22 @@ plc.iss_init = function (reactor) self.reactor.scram() if self.reactor.__p_is_faulted() then - log.error("ISS: failed reactor SCRAM") + log.error("RPS: failed reactor SCRAM") end end return self.tripped, status, first_trip end - -- get the ISS status + -- get the RPS status local status = function () return self.cache end local is_tripped = function () return self.tripped end - -- reset the ISS + -- reset the RPS local reset = function () self.timed_out = false self.tripped = false - self.trip_cause = iss_status_t.ok + self.trip_cause = rps_status_t.ok end return { @@ -201,7 +201,7 @@ plc.iss_init = function (reactor) end -- reactor PLC communications -plc.comms = function (id, modem, local_port, server_port, reactor, iss) +plc.comms = function (id, modem, local_port, server_port, reactor, rps) local self = { id = id, seq_num = 0, @@ -210,7 +210,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, iss) s_port = server_port, l_port = local_port, reactor = reactor, - iss = iss, + rps = rps, scrammed = false, linked = false, status_cache = nil, @@ -411,7 +411,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, iss) local sys_status = { util.time(), -- timestamp (not self.scrammed), -- enabled - iss.is_tripped(), -- overridden + rps.is_tripped(), -- overridden degraded, -- degraded self.reactor.getHeatingRate(), -- heating rate mek_data -- mekanism status data @@ -425,22 +425,22 @@ plc.comms = function (id, modem, local_port, server_port, reactor, iss) end end - -- send safety system status - local send_iss_status = function () + -- send reactor protection system status + local send_rps_status = function () if self.linked then - _send(RPLC_TYPES.ISS_STATUS, iss.status()) + _send(RPLC_TYPES.RPS_STATUS, rps.status()) end end - -- send safety system alarm - local send_iss_alarm = function (cause) + -- send reactor protection system alarm + local send_rps_alarm = function (cause) if self.linked then - local iss_alarm = { + local rps_alarm = { cause, - table.unpack(iss.status()) + table.unpack(rps.status()) } - _send(RPLC_TYPES.ISS_ALARM, iss_alarm) + _send(RPLC_TYPES.RPS_ALARM, rps_alarm) end end @@ -581,9 +581,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, iss) else log.debug("RPLC set burn rate packet length mismatch") end - elseif packet.type == RPLC_TYPES.ISS_CLEAR then - -- clear the ISS status - iss.reset() + elseif packet.type == RPLC_TYPES.RPS_RESET then + -- reset the RPS status + rps.reset() _send_ack(packet.type, true) else log.warning("received unknown RPLC packet type " .. packet.type) @@ -647,8 +647,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, iss) close = close, send_link_req = send_link_req, send_status = send_status, - send_iss_status = send_iss_status, - send_iss_alarm = send_iss_alarm, + send_rps_status = send_rps_status, + send_rps_alarm = send_rps_alarm, parse_packet = parse_packet, handle_packet = handle_packet, is_scrammed = is_scrammed, diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index dde16e7..e8654dc 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.0" +local R_PLC_VERSION = "alpha-v0.6.1" local print = util.print local println = util.println @@ -55,14 +55,14 @@ local __shared_memory = { -- system objects plc_sys = { - iss = nil, + rps = nil, plc_comms = nil, conn_watchdog = nil }, -- message queues q = { - mq_iss = mqueue.new(), + mq_rps = mqueue.new(), mq_comms_tx = mqueue.new(), mq_comms_rx = mqueue.new() } @@ -100,13 +100,13 @@ function init() -- just booting up, no fission allowed (neutrons stay put thanks) smem_dev.reactor.scram() - -- init internal safety system - smem_sys.iss = plc.iss_init(smem_dev.reactor) - log.debug("iss init") + -- init reactor protection system + smem_sys.rps = plc.rps_init(smem_dev.reactor) + log.debug("rps init") if __shared_memory.networked then -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.iss) + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.rps) log.debug("comms init") -- comms watchdog, 3 second timeout @@ -131,7 +131,7 @@ init() -- init threads local main_thread = threads.thread__main(__shared_memory, init) -local iss_thread = threads.thread__iss(__shared_memory) +local rps_thread = threads.thread__rps(__shared_memory) if __shared_memory.networked then -- init comms threads @@ -142,19 +142,19 @@ if __shared_memory.networked then local sp_ctrl_thread = threads.thread__setpoint_control(__shared_memory) -- run threads - parallel.waitForAll(main_thread.exec, iss_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec, sp_ctrl_thread.exec) + parallel.waitForAll(main_thread.exec, rps_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec, sp_ctrl_thread.exec) if plc_state.init_ok then - -- send status one last time after ISS shutdown + -- send status one last time after RPS shutdown smem_sys.plc_comms.send_status(plc_state.degraded) - smem_sys.plc_comms.send_iss_status() + smem_sys.plc_comms.send_rps_status() -- close connection smem_sys.plc_comms.close(smem_sys.conn_watchdog) end else -- run threads, excluding comms - parallel.waitForAll(main_thread.exec, iss_thread.exec) + parallel.waitForAll(main_thread.exec, rps_thread.exec) end println_ts("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index f689955..5deaf10 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -13,13 +13,13 @@ local println_ts = util.println_ts local psleep = util.psleep local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) -local ISS_SLEEP = 500 -- (500ms, 10 ticks) +local RPS_SLEEP = 500 -- (500ms, 10 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks) local BURN_RATE_RAMP_mB_s = 5.0 -local MQ__ISS_CMD = { +local MQ__RPS_CMD = { SCRAM = 1, DEGRADED_SCRAM = 2, TRIP_TIMEOUT = 3 @@ -45,7 +45,7 @@ threads.thread__main = function (smem, init) local networked = smem.networked local plc_state = smem.plc_state local plc_dev = smem.plc_dev - local iss = smem.plc_sys.iss + local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms local conn_watchdog = smem.plc_sys.conn_watchdog @@ -84,7 +84,7 @@ threads.thread__main = function (smem, init) elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor plc_comms.unlink() - smem.q.mq_iss.push_command(MQ__ISS_CMD.TRIP_TIMEOUT) + smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) elseif event == "peripheral_detach" then -- peripheral disconnect local device = ppm.handle_unmount(param1) @@ -103,7 +103,7 @@ threads.thread__main = function (smem, init) if plc_state.init_ok then -- try to scram reactor if it is still connected - smem.q.mq_iss.push_command(MQ__ISS_CMD.DEGRADED_SCRAM) + smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) end plc_state.degraded = true @@ -119,14 +119,14 @@ threads.thread__main = function (smem, init) -- reconnected reactor plc_dev.reactor = device - smem.q.mq_iss.push_command(MQ__ISS_CMD.SCRAM) + smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) println_ts("reactor reconnected.") log.info("reactor reconnected.") plc_state.no_reactor = false if plc_state.init_ok then - iss.reconnect_reactor(plc_dev.reactor) + rps.reconnect_reactor(plc_dev.reactor) if networked then plc_comms.reconnect_reactor(plc_dev.reactor) end @@ -171,7 +171,7 @@ threads.thread__main = function (smem, init) -- check for termination request if event == "terminate" or ppm.should_terminate() then log.info("terminate requested, main thread exiting") - -- iss handles reactor shutdown + -- rps handles reactor shutdown plc_state.shutdown = true break end @@ -181,20 +181,20 @@ threads.thread__main = function (smem, init) return { exec = exec } end --- ISS monitor thread -threads.thread__iss = function (smem) +-- RPS operation thread +threads.thread__rps = function (smem) -- execute thread local exec = function () - log.debug("iss thread start") + log.debug("rps thread start") -- load in from shared memory local networked = smem.networked local plc_state = smem.plc_state local plc_dev = smem.plc_dev - local iss = smem.plc_sys.iss + local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms - local iss_queue = smem.q.mq_iss + local rps_queue = smem.q.mq_rps local was_linked = false local last_update = util.time() @@ -203,14 +203,14 @@ threads.thread__iss = function (smem) while true do local reactor = plc_dev.reactor - -- ISS checks + -- RPS checks if plc_state.init_ok then -- SCRAM if no open connection if networked and not plc_comms.is_linked() then plc_state.scram = true if was_linked then was_linked = false - iss.trip_timeout() + rps.trip_timeout() end else -- would do elseif not networked but there is no reason to do that extra operation @@ -223,38 +223,38 @@ threads.thread__iss = function (smem) reactor.scram() end - -- if we are in standalone mode, continuously reset ISS - -- ISS will trip again if there are faults, but if it isn't cleared, the user can't re-enable + -- if we are in standalone mode, continuously reset RPS + -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable if not networked then plc_state.scram = false - iss.reset() + rps.reset() end -- check safety (SCRAM occurs if tripped) if not plc_state.no_reactor then - local iss_tripped, iss_status_string, iss_first = iss.check() - plc_state.scram = plc_state.scram or iss_tripped + local rps_tripped, rps_status_string, rps_first = rps.check() + plc_state.scram = plc_state.scram or rps_tripped - if iss_first then - println_ts("[ISS] SCRAM! safety trip: " .. iss_status_string) + if rps_first then + println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) if networked and not plc_state.no_modem then - plc_comms.send_iss_alarm(iss_status_string) + plc_comms.send_rps_alarm(rps_status_string) end end end end -- check for messages in the message queue - while iss_queue.ready() and not plc_state.shutdown do - local msg = iss_queue.pop() + while rps_queue.ready() and not plc_state.shutdown do + local msg = rps_queue.pop() if msg.qtype == mqueue.TYPE.COMMAND then -- received a command - if msg.message == MQ__ISS_CMD.SCRAM then + if msg.message == MQ__RPS_CMD.SCRAM then -- basic SCRAM plc_state.scram = true reactor.scram() - elseif msg.message == MQ__ISS_CMD.DEGRADED_SCRAM then + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then -- SCRAM with print plc_state.scram = true if reactor.scram() then @@ -264,10 +264,10 @@ threads.thread__iss = function (smem) println_ts("failed reactor SCRAM") log.error("failed reactor SCRAM") end - elseif msg.message == MQ__ISS_CMD.TRIP_TIMEOUT then + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then -- watchdog tripped plc_state.scram = true - iss.trip_timeout() + rps.trip_timeout() println_ts("server timeout") log.warning("server timeout") end @@ -284,24 +284,24 @@ threads.thread__iss = function (smem) -- check for termination request if plc_state.shutdown then -- safe exit - log.info("iss thread shutdown initiated") + log.info("rps thread shutdown initiated") if plc_state.init_ok then plc_state.scram = true reactor.scram() if reactor.__p_is_ok() then println_ts("reactor disabled") - log.info("iss thread reactor SCRAM OK") + log.info("rps thread reactor SCRAM OK") else println_ts("exiting, reactor failed to disable") - log.error("iss thread failed to SCRAM reactor on exit") + log.error("rps thread failed to SCRAM reactor on exit") end end - log.info("iss thread exiting") + log.info("rps thread exiting") break end -- delay before next check - last_update = util.adaptive_delay(ISS_SLEEP, last_update) + last_update = util.adaptive_delay(RPS_SLEEP, last_update) end end @@ -331,9 +331,9 @@ threads.thread__comms_tx = function (smem) if msg.qtype == mqueue.TYPE.COMMAND then -- received a command if msg.message == MQ__COMM_CMD.SEND_STATUS then - -- send PLC/ISS status + -- send PLC/RPS status plc_comms.send_status(plc_state.degraded) - plc_comms.send_iss_status() + plc_comms.send_rps_status() end elseif msg.qtype == mqueue.TYPE.DATA then -- received data diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 17050a8..da6f5d3 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -20,9 +20,9 @@ local RPLC_TYPES = { MEK_SCRAM = 4, -- SCRAM reactor MEK_ENABLE = 5, -- enable reactor MEK_BURN_RATE = 6, -- set burn rate - ISS_STATUS = 7, -- ISS status - ISS_ALARM = 8, -- ISS alarm broadcast - ISS_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately) + RPS_STATUS = 7, -- RPS status + RPS_ALARM = 8, -- RPS alarm broadcast + RPS_RESET = 9 -- clear RPS trip (if in bad state, will trip immediately) } local RPLC_LINKING = { @@ -232,9 +232,9 @@ comms.rplc_packet = function () self.type == RPLC_TYPES.MEK_SCRAM or self.type == RPLC_TYPES.MEK_ENABLE or self.type == RPLC_TYPES.MEK_BURN_RATE or - self.type == RPLC_TYPES.ISS_ALARM or - self.type == RPLC_TYPES.ISS_STATUS or - self.type == RPLC_TYPES.ISS_CLEAR + self.type == RPLC_TYPES.RPS_ALARM or + self.type == RPLC_TYPES.RPS_STATUS or + self.type == RPLC_TYPES.RPS_RESET end -- make an RPLC packet diff --git a/scada-common/types.lua b/scada-common/types.lua index 5346bca..855334e 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -14,7 +14,7 @@ types.rtu_t = { induction_matrix = "induction_matrix" } -types.iss_status_t = { +types.rps_status_t = { ok = "ok", dmg_crit = "dmg_crit", ex_hcoolant = "heated_coolant_backup", diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index ee11819..bb79ac1 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -22,7 +22,7 @@ local PLC_S_CMDS = { SCRAM = 0, ENABLE = 1, BURN_RATE = 2, - ISS_CLEAR = 3 + RPS_RESET = 3 } plc.PLC_S_CMDS = PLC_S_CMDS @@ -62,14 +62,14 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) scram_req = 0, enable_req = 0, burn_rate_req = 0, - iss_clear_req = 0 + rps_reset_req = 0 }, -- command acknowledgements acks = { scram = true, enable = true, burn_rate = true, - iss_clear = true + rps_reset = true }, -- session database sDB = { @@ -77,9 +77,9 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) control_state = false, overridden = false, degraded = false, - iss_tripped = false, - iss_trip_cause = "ok", - iss_status = { + rps_tripped = false, + rps_trip_cause = "ok", + rps_status = { dmg_crit = false, ex_hcool = false, ex_waste = false, @@ -127,14 +127,14 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) } } - local _copy_iss_status = function (iss_status) - self.sDB.iss_status.dmg_crit = iss_status[1] - self.sDB.iss_status.ex_hcool = iss_status[2] - self.sDB.iss_status.ex_waste = iss_status[3] - self.sDB.iss_status.high_temp = iss_status[4] - self.sDB.iss_status.no_fuel = iss_status[5] - self.sDB.iss_status.no_cool = iss_status[6] - self.sDB.iss_status.timed_out = iss_status[7] + local _copy_rps_status = function (rps_status) + self.sDB.rps_status.dmg_crit = rps_status[1] + self.sDB.rps_status.ex_hcool = rps_status[2] + self.sDB.rps_status.ex_waste = rps_status[3] + self.sDB.rps_status.high_temp = rps_status[4] + self.sDB.rps_status.no_fuel = rps_status[5] + self.sDB.rps_status.no_cool = rps_status[6] + self.sDB.rps_status.timed_out = rps_status[7] end local _copy_status = function (mek_data) @@ -317,44 +317,44 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) elseif ack == false then log.debug(log_header .. "burn rate update failed!") end - elseif pkt.type == RPLC_TYPES.ISS_STATUS then - -- ISS status packet received, copy data + elseif pkt.type == RPLC_TYPES.RPS_STATUS then + -- RPS status packet received, copy data if pkt.length == 7 then - local status = pcall(_copy_iss_status, pkt.data) + local status = pcall(_copy_rps_status, pkt.data) if status then - -- copied in ISS status data OK + -- copied in RPS status data OK else - -- error copying ISS status data - log.error(log_header .. "failed to parse ISS status packet data") + -- error copying RPS status data + log.error(log_header .. "failed to parse RPS status packet data") end else - log.debug(log_header .. "RPLC ISS status packet length mismatch") + log.debug(log_header .. "RPLC RPS status packet length mismatch") end - elseif pkt.type == RPLC_TYPES.ISS_ALARM then - -- ISS alarm + elseif pkt.type == RPLC_TYPES.RPS_ALARM then + -- RPS alarm self.sDB.overridden = true if pkt.length == 8 then - self.sDB.iss_tripped = true - self.sDB.iss_trip_cause = pkt.data[1] - local status = pcall(_copy_iss_status, { table.unpack(pkt.data, 2, #pkt.length) }) + self.sDB.rps_tripped = true + self.sDB.rps_trip_cause = pkt.data[1] + local status = pcall(_copy_rps_status, { table.unpack(pkt.data, 2, #pkt.length) }) if status then - -- copied in ISS status data OK + -- copied in RPS status data OK else - -- error copying ISS status data - log.error(log_header .. "failed to parse ISS alarm status data") + -- error copying RPS status data + log.error(log_header .. "failed to parse RPS alarm status data") end else - log.debug(log_header .. "RPLC ISS alarm packet length mismatch") + log.debug(log_header .. "RPLC RPS alarm packet length mismatch") end - elseif pkt.type == RPLC_TYPES.ISS_CLEAR then - -- ISS clear acknowledgement + elseif pkt.type == RPLC_TYPES.RPS_RESET then + -- RPS reset acknowledgement local ack = _get_ack(pkt) if ack then - self.acks.iss_tripped = true - self.sDB.iss_tripped = false - self.sDB.iss_trip_cause = "ok" + self.acks.rps_tripped = true + self.sDB.rps_tripped = false + self.sDB.rps_trip_cause = "ok" elseif ack == false then - log.debug(log_header .. "ISS clear failed") + log.debug(log_header .. "RPS reset failed") end else log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) @@ -438,11 +438,11 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.acks.enable = false self.retry_times.enable_req = util.time() + INITIAL_WAIT _send(RPLC_TYPES.MEK_ENABLE, {}) - elseif cmd == PLC_S_CMDS.ISS_CLEAR then - -- clear ISS - self.acks.iss_clear = false - self.retry_times.iss_clear_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.ISS_CLEAR, {}) + elseif cmd == PLC_S_CMDS.RPS_RESET then + -- reset RPS + self.acks.rps_reset = false + self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.RPS_RESET, {}) end elseif message.qtype == mqueue.TYPE.DATA then -- instruction with body @@ -540,12 +540,12 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end end - -- ISS clear request retry + -- RPS reset request retry - if not self.acks.iss_clear then - if rtimes.iss_clear_req - util.time() <= 0 then - _send(RPLC_TYPES.ISS_CLEAR, {}) - rtimes.iss_clear_req = util.time() + RETRY_PERIOD + if not self.acks.rps_reset then + if rtimes.rps_reset_req - util.time() <= 0 then + _send(RPLC_TYPES.RPS_RESET, {}) + rtimes.rps_reset_req = util.time() + RETRY_PERIOD end end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 3059bab..a93dab9 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("session.svsessions") local config = require("config") local supervisor = require("supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.0" +local SUPERVISOR_VERSION = "alpha-v0.3.1" local print = util.print local println = util.println From 89be79192fccb9982699f34c1f6fc59707476eaa Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 5 May 2022 13:14:14 -0400 Subject: [PATCH 091/168] #44 RPS optimizations, manual trip, RPS handles all reactor state control --- reactor-plc/plc.lua | 257 +++++++++++++++++++++++-------------- reactor-plc/startup.lua | 3 +- reactor-plc/threads.lua | 58 ++++----- scada-common/comms.lua | 10 +- scada-common/types.lua | 10 +- supervisor/session/plc.lua | 38 +++--- supervisor/startup.lua | 2 +- 7 files changed, 217 insertions(+), 161 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index d53ad95..68671b9 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -22,49 +22,48 @@ local println_ts = util.println_ts -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main SCADA supervisor/coordinator control plc.rps_init = function (reactor) + local state_keys = { + dmg_crit = 1, + high_temp = 2, + no_coolant = 3, + ex_waste = 4, + ex_hcoolant = 5, + no_fuel = 6, + fault = 7, + timeout = 8, + manual = 9 + } + local self = { reactor = reactor, - cache = { false, false, false, false, false, false, false }, - timed_out = false, + state = { false, false, false, false, false, false, false, false, false }, + reactor_enabled = false, tripped = false, trip_cause = "" } -- PRIVATE FUNCTIONS -- + -- set reactor access fault flag + local _set_fault = function () + self.state[state_keys.fault] = true + end + + -- clear reactor access fault flag + local _clear_fault = function () + self.state[state_keys.fault] = false + end + -- check for critical damage local _damage_critical = function () local damage_percent = self.reactor.getDamagePercent() if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log.error("RPS: failed to check reactor damage") - return false + _set_fault() + self.state[state_keys.dmg_crit] = false else - return damage_percent >= 100 - end - end - - -- check for heated coolant backup - local _excess_heated_coolant = function () - local hc_needed = self.reactor.getHeatedCoolantNeeded() - if hc_needed == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - log.error("RPS: failed to check reactor heated coolant level") - return false - else - return hc_needed == 0 - end - end - - -- check for excess waste - local _excess_waste = function () - local w_needed = self.reactor.getWasteNeeded() - if w_needed == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - log.error("RPS: failed to check reactor waste level") - return false - else - return w_needed == 0 + self.state[state_keys.dmg_crit] = damage_percent >= 100 end end @@ -75,9 +74,49 @@ plc.rps_init = function (reactor) if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log.error("RPS: failed to check reactor temperature") - return false + _set_fault() + self.state[state_keys.high_temp] = false else - return temp >= 1200 + self.state[state_keys.high_temp] = temp >= 1200 + end + end + + -- check if there is no coolant (<2% filled) + local _no_coolant = function () + local coolant_filled = self.reactor.getCoolantFilledPercentage() + if coolant_filled == ppm.ACCESS_FAULT then + -- lost the peripheral or terminated, handled later + log.error("RPS: failed to check reactor coolant level") + _set_fault() + self.state[state_keys.no_coolant] = false + else + self.state[state_keys.no_coolant] = coolant_filled < 0.02 + end + end + + -- check for excess waste (>80% filled) + local _excess_waste = function () + local w_filled = self.reactor.getWasteFilledPercentage() + if w_filled == ppm.ACCESS_FAULT then + -- lost the peripheral or terminated, handled later + log.error("RPS: failed to check reactor waste level") + _set_fault() + self.state[state_keys.ex_waste] = false + else + self.state[state_keys.ex_waste] = w_filled > 0.8 + end + end + + -- check for heated coolant backup (>95% filled) + local _excess_heated_coolant = function () + local hc_filled = self.reactor.getHeatedCoolantFilledPercentage() + if hc_filled == ppm.ACCESS_FAULT then + -- lost the peripheral or terminated, handled later + log.error("RPS: failed to check reactor heated coolant level") + _set_fault() + state[state_keys.ex_hcoolant] = false + else + state[state_keys.ex_hcoolant] = hc_filled > 0.95 end end @@ -86,22 +125,11 @@ plc.rps_init = function (reactor) local fuel = self.reactor.getFuel() if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later - log.error("RPS: failed to check reactor fuel level") - return false + log.error("RPS: failed to check reactor fuel") + _set_fault() + state[state_keys.no_fuel] = false else - return fuel == 0 - end - end - - -- check if there is no coolant - local _no_coolant = function () - local coolant_filled = self.reactor.getCoolantFilledPercentage() - if coolant_filled == ppm.ACCESS_FAULT then - -- lost the peripheral or terminated, handled later - log.error("RPS: failed to check reactor coolant level") - return false - else - return coolant_filled < 0.02 + state[state_keys.no_fuel] = fuel.amount == 0 end end @@ -114,78 +142,114 @@ plc.rps_init = function (reactor) -- report a PLC comms timeout local trip_timeout = function () - self.timed_out = true + state[state_keys.timed_out] = true + end + + -- manually SCRAM the reactor + local trip_manual = function () + state[state_keys.manual] = true + end + + -- SCRAM the reactor now + local scram = function () + log.info("RPS: reactor SCRAM") + + self.reactor.scram() + if self.reactor.__p_is_faulted() then + log.error("RPS: failed reactor SCRAM") + return false + else + self.reactor_enabled = false + return true + end + end + + -- start the reactor + local activate = function () + if not self.tripped then + log.info("RPS: reactor start") + + self.reactor.activate() + if self.reactor.__p_is_faulted() then + log.error("RPS: failed reactor start") + else + self.reactor_enabled = true + return true + end + end + + return false end -- check all safety conditions local check = function () local status = rps_status_t.ok local was_tripped = self.tripped + local first_trip = false + + -- update state + parallel.waitForAll( + _damage_critical, + _high_temp, + _no_coolant, + _excess_waste, + _excess_heated_coolant, + _insufficient_fuel + ) - -- update cache - self.cache = { - _damage_critical(), - _excess_heated_coolant(), - _excess_waste(), - _high_temp(), - _insufficient_fuel(), - _no_coolant(), - self.timed_out - } - -- check system states in order of severity if self.tripped then status = self.trip_cause - elseif self.cache[1] then - log.warning("RPS: damage critical!") + elseif self.state[state_keys.dmg_crit] then + log.warning("RPS: damage critical") status = rps_status_t.dmg_crit - elseif self.cache[4] then - log.warning("RPS: high temperature!") + elseif self.state[state_keys.high_temp] then + log.warning("RPS: high temperature") status = rps_status_t.high_temp - elseif self.cache[2] then - log.warning("RPS: heated coolant backup!") - status = rps_status_t.ex_hcoolant - elseif self.cache[6] then - log.warning("RPS: no coolant!") + elseif self.state[state_keys.no_coolant] then + log.warning("RPS: no coolant") status = rps_status_t.no_coolant - elseif self.cache[3] then - log.warning("RPS: full waste!") + elseif self.state[state_keys.ex_waste] then + log.warning("RPS: full waste") status = rps_status_t.ex_waste - elseif self.cache[5] then - log.warning("RPS: no fuel!") + elseif self.state[state_keys.ex_hcoolant] then + log.warning("RPS: heated coolant backup") + status = rps_status_t.ex_hcoolant + elseif self.state[state_keys.no_fuel] then + log.warning("RPS: no fuel") status = rps_status_t.no_fuel - elseif self.cache[7] then - log.warning("RPS: supervisor connection timeout!") + elseif self.state[state_keys.fault] then + log.warning("RPS: reactor access fault") + status = rps_status_t.fault + elseif self.state[state_keys.timeout] then + log.warning("RPS: supervisor connection timeout") status = rps_status_t.timeout + elseif self.state[state_keys.manual] then + log.warning("RPS: manual SCRAM requested") + status = rps_status_t.manual else self.tripped = false end - - -- if a new trip occured... - local first_trip = false - if not was_tripped and status ~= rps_status_t.ok then - log.warning("RPS: reactor SCRAM") + -- if a new trip occured... + if (not was_tripped) and (status ~= rps_status_t.ok) then first_trip = true self.tripped = true self.trip_cause = status - self.reactor.scram() - if self.reactor.__p_is_faulted() then - log.error("RPS: failed reactor SCRAM") - end + scram() end - + return self.tripped, status, first_trip end -- get the RPS status - local status = function () return self.cache end + local status = function () return self.state end local is_tripped = function () return self.tripped end + local is_active = function () return self.reactor_enabled end -- reset the RPS local reset = function () - self.timed_out = false self.tripped = false self.trip_cause = rps_status_t.ok end @@ -193,9 +257,13 @@ plc.rps_init = function (reactor) return { reconnect_reactor = reconnect_reactor, trip_timeout = trip_timeout, + trip_manual = trip_manual, + scram = scram, + activate = activate, check = check, status = status, is_tripped = is_tripped, + is_active = is_active, reset = reset } end @@ -544,18 +612,6 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) -- request for physical structure _send_struct() log.debug("sent out structure again, did supervisor miss it?") - elseif packet.type == RPLC_TYPES.MEK_SCRAM then - -- disable the reactor - self.scrammed = true - plc_state.scram = true - self.reactor.scram() - _send_ack(packet.type, self.reactor.__p_is_ok()) - elseif packet.type == RPLC_TYPES.MEK_ENABLE then - -- enable the reactor - self.scrammed = false - plc_state.scram = false - self.reactor.activate() - _send_ack(packet.type, self.reactor.__p_is_ok()) elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate if packet.length == 1 then @@ -581,6 +637,15 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) else log.debug("RPLC set burn rate packet length mismatch") end + elseif packet.type == RPLC_TYPES.RPS_ENABLE then + -- enable the reactor + self.scrammed = false + _send_ack(packet.type, self.rps.activate()) + elseif packet.type == RPLC_TYPES.RPS_SCRAM then + -- disable the reactor + self.scrammed = true + self.rps.trip_manual() + _send_ack(packet.type, true) elseif packet.type == RPLC_TYPES.RPS_RESET then -- reset the RPS status rps.reset() diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e8654dc..cf2bb0f 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.1" +local R_PLC_VERSION = "alpha-v0.6.2" local print = util.print local println = util.println @@ -37,7 +37,6 @@ local __shared_memory = { plc_state = { init_ok = true, shutdown = false, - scram = true, degraded = false, no_reactor = false, no_modem = false diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 5deaf10..42d8e33 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -13,7 +13,7 @@ local println_ts = util.println_ts local psleep = util.psleep local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) -local RPS_SLEEP = 500 -- (500ms, 10 ticks) +local RPS_SLEEP = 250 -- (250ms, 5 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) local SP_CTRL_SLEEP = 250 -- (250ms, 5 ticks) @@ -207,7 +207,7 @@ threads.thread__rps = function (smem) if plc_state.init_ok then -- SCRAM if no open connection if networked and not plc_comms.is_linked() then - plc_state.scram = true + rps.scram() if was_linked then was_linked = false rps.trip_timeout() @@ -219,21 +219,17 @@ threads.thread__rps = function (smem) -- if we tried to SCRAM but failed, keep trying -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) - if not plc_state.no_reactor and plc_state.scram and reactor.getStatus() then - reactor.scram() + if not plc_state.no_reactor and rps.is_tripped() and reactor.getStatus() then + rps.scram() end -- if we are in standalone mode, continuously reset RPS -- RPS will trip again if there are faults, but if it isn't cleared, the user can't re-enable - if not networked then - plc_state.scram = false - rps.reset() - end + if not networked then rps.reset() end -- check safety (SCRAM occurs if tripped) if not plc_state.no_reactor then local rps_tripped, rps_status_string, rps_first = rps.check() - plc_state.scram = plc_state.scram or rps_tripped if rps_first then println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) @@ -250,26 +246,19 @@ threads.thread__rps = function (smem) if msg.qtype == mqueue.TYPE.COMMAND then -- received a command - if msg.message == MQ__RPS_CMD.SCRAM then - -- basic SCRAM - plc_state.scram = true - reactor.scram() - elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then - -- SCRAM with print - plc_state.scram = true - if reactor.scram() then - println_ts("successful reactor SCRAM") - log.error("successful reactor SCRAM") - else - println_ts("failed reactor SCRAM") - log.error("failed reactor SCRAM") + if plc_state.init_ok then + if msg.message == MQ__RPS_CMD.SCRAM then + -- SCRAM + rps.scram() + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then + -- lost peripheral(s) + rps.trip_degraded() + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + rps.trip_timeout() + println_ts("server timeout") + log.warning("server timeout") end - elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then - -- watchdog tripped - plc_state.scram = true - rps.trip_timeout() - println_ts("server timeout") - log.warning("server timeout") end elseif msg.qtype == mqueue.TYPE.DATA then -- received data @@ -286,9 +275,7 @@ threads.thread__rps = function (smem) -- safe exit log.info("rps thread shutdown initiated") if plc_state.init_ok then - plc_state.scram = true - reactor.scram() - if reactor.__p_is_ok() then + if rps.scram() then println_ts("reactor disabled") log.info("rps thread reactor SCRAM OK") else @@ -368,6 +355,8 @@ threads.thread__comms_rx = function (smem) -- load in from shared memory local plc_state = smem.plc_state local setpoints = smem.setpoints + local plc_dev = smem.plc_dev + local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms local conn_watchdog = smem.plc_sys.conn_watchdog @@ -388,7 +377,7 @@ threads.thread__comms_rx = function (smem) elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet -- handle the packet (setpoints passed to update burn rate setpoint) - -- (plc_state passed to allow clearing SCRAM flag and check if degraded) + -- (plc_state passed to check if degraded) -- (conn_watchdog passed to allow feeding the watchdog) plc_comms.handle_packet(msg.message, setpoints, plc_state, conn_watchdog) end @@ -421,6 +410,7 @@ threads.thread__setpoint_control = function (smem) local plc_state = smem.plc_state local setpoints = smem.setpoints local plc_dev = smem.plc_dev + local rps = smem.plc_sys.rps local last_update = util.time() local running = false @@ -433,7 +423,7 @@ threads.thread__setpoint_control = function (smem) -- check if we should start ramping if setpoints.burn_rate ~= last_sp_burn then - if not plc_state.scram then + if rps.is_active() then if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then -- update without ramp if <= 5 mB/t change log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") @@ -459,7 +449,7 @@ threads.thread__setpoint_control = function (smem) running = false -- adjust burn rate (setpoints.burn_rate) - if not plc_state.scram then + if rps.is_active() then local current_burn_rate = reactor.getBurnRate() if (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then -- calculate new burn rate diff --git a/scada-common/comms.lua b/scada-common/comms.lua index da6f5d3..f762c3b 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,9 +17,9 @@ local RPLC_TYPES = { LINK_REQ = 1, -- linking requests STATUS = 2, -- reactor/system status MEK_STRUCT = 3, -- mekanism build structure - MEK_SCRAM = 4, -- SCRAM reactor - MEK_ENABLE = 5, -- enable reactor - MEK_BURN_RATE = 6, -- set burn rate + MEK_BURN_RATE = 4, -- set burn rate + RPS_ENABLE = 5, -- enable reactor + RPS_SCRAM = 6, -- SCRAM reactor RPS_STATUS = 7, -- RPS status RPS_ALARM = 8, -- RPS alarm broadcast RPS_RESET = 9 -- clear RPS trip (if in bad state, will trip immediately) @@ -229,9 +229,9 @@ comms.rplc_packet = function () self.type == RPLC_TYPES.LINK_REQ or self.type == RPLC_TYPES.STATUS or self.type == RPLC_TYPES.MEK_STRUCT or - self.type == RPLC_TYPES.MEK_SCRAM or - self.type == RPLC_TYPES.MEK_ENABLE or self.type == RPLC_TYPES.MEK_BURN_RATE or + self.type == RPLC_TYPES.RPS_ENABLE or + self.type == RPLC_TYPES.RPS_SCRAM or self.type == RPLC_TYPES.RPS_ALARM or self.type == RPLC_TYPES.RPS_STATUS or self.type == RPLC_TYPES.RPS_RESET diff --git a/scada-common/types.lua b/scada-common/types.lua index 855334e..5bd747e 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -17,12 +17,14 @@ types.rtu_t = { types.rps_status_t = { ok = "ok", dmg_crit = "dmg_crit", - ex_hcoolant = "heated_coolant_backup", - ex_waste = "full_waste", high_temp = "high_temp", - no_fuel = "no_fuel", no_coolant = "no_coolant", - timeout = "timeout" + ex_waste = "full_waste", + ex_hcoolant = "heated_coolant_backup", + no_fuel = "no_fuel", + fault = "fault", + timeout = "timeout", + manual = "manual" } -- MODBUS diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index bb79ac1..2ee2770 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -291,16 +291,15 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) else log.debug(log_header .. "RPLC struct packet length mismatch") end - elseif pkt.type == RPLC_TYPES.MEK_SCRAM then - -- SCRAM acknowledgement + elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then + -- burn rate acknowledgement local ack = _get_ack(pkt) if ack then - self.acks.scram = true - self.sDB.control_state = false + self.acks.burn_rate = true elseif ack == false then - log.debug(log_header .. "SCRAM failed!") + log.debug(log_header .. "burn rate update failed!") end - elseif pkt.type == RPLC_TYPES.MEK_ENABLE then + elseif pkt.type == RPLC_TYPES.RPS_ENABLE then -- enable acknowledgement local ack = _get_ack(pkt) if ack then @@ -309,13 +308,14 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) elseif ack == false then log.debug(log_header .. "enable failed!") end - elseif pkt.type == RPLC_TYPES.MEK_BURN_RATE then - -- burn rate acknowledgement + elseif pkt.type == RPLC_TYPES.RPS_SCRAM then + -- SCRAM acknowledgement local ack = _get_ack(pkt) if ack then - self.acks.burn_rate = true + self.acks.scram = true + self.sDB.control_state = false elseif ack == false then - log.debug(log_header .. "burn rate update failed!") + log.debug(log_header .. "SCRAM failed!") end elseif pkt.type == RPLC_TYPES.RPS_STATUS then -- RPS status packet received, copy data @@ -428,16 +428,16 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) elseif message.qtype == mqueue.TYPE.COMMAND then -- handle instruction local cmd = message.message - if cmd == PLC_S_CMDS.SCRAM then - -- SCRAM reactor - self.acks.scram = false - self.retry_times.scram_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_SCRAM, {}) - elseif cmd == PLC_S_CMDS.ENABLE then + if cmd == PLC_S_CMDS.ENABLE then -- enable reactor self.acks.enable = false self.retry_times.enable_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_ENABLE, {}) + _send(RPLC_TYPES.RPS_ENABLE, {}) + elseif cmd == PLC_S_CMDS.SCRAM then + -- SCRAM reactor + self.acks.scram = false + self.retry_times.scram_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.RPS_SCRAM, {}) elseif cmd == PLC_S_CMDS.RPS_RESET then -- reset RPS self.acks.rps_reset = false @@ -517,7 +517,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) if not self.acks.scram then if rtimes.scram_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_SCRAM, {}) + _send(RPLC_TYPES.RPS_SCRAM, {}) rtimes.scram_req = util.time() + RETRY_PERIOD end end @@ -526,7 +526,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) if not self.acks.enable then if rtimes.enable_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_ENABLE, {}) + _send(RPLC_TYPES.RPS_ENABLE, {}) rtimes.enable_req = util.time() + RETRY_PERIOD end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index a93dab9..72ac729 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("session.svsessions") local config = require("config") local supervisor = require("supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.1" +local SUPERVISOR_VERSION = "alpha-v0.3.2" local print = util.print local println = util.println From 83fa41bbd055674b563f95cc67adbae7998b66de Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 5 May 2022 16:00:49 -0400 Subject: [PATCH 092/168] #45 burn rate ramping is optional now --- reactor-plc/plc.lua | 23 ++++++++++------- reactor-plc/startup.lua | 3 ++- reactor-plc/threads.lua | 52 ++++++++++++++++++++------------------ supervisor/session/plc.lua | 24 ++++++++++++++---- supervisor/startup.lua | 2 +- 5 files changed, 64 insertions(+), 40 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 68671b9..9010610 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -614,22 +614,27 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) log.debug("sent out structure again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate - if packet.length == 1 then + if packet.length == 2 then local success = false local burn_rate = packet.data[1] - local max_burn_rate = self.max_burn_rate + local ramp = packet.data[2] -- if no known max burn rate, check again - if max_burn_rate == nil then - max_burn_rate = self.reactor.getMaxBurnRate() - self.max_burn_rate = max_burn_rate + if self.max_burn_rate == nil then + self.max_burn_rate = self.reactor.getMaxBurnRate() end -- if we know our max burn rate, update current burn rate setpoint if in range - if max_burn_rate ~= ppm.ACCESS_FAULT then - if burn_rate > 0 and burn_rate <= max_burn_rate then - setpoints.burn_rate = burn_rate - success = true + if self.max_burn_rate ~= ppm.ACCESS_FAULT then + if burn_rate > 0 and burn_rate <= self.max_burn_rate then + if ramp then + setpoints.burn_rate_en = true + setpoints.burn_rate = burn_rate + success = true + else + self.reactor.setBurnRate(burn_rate) + success = not self.reactor.__p_is_faulted() + end end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index cf2bb0f..deb2b82 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.2" +local R_PLC_VERSION = "alpha-v0.6.3" local print = util.print local println = util.println @@ -43,6 +43,7 @@ local __shared_memory = { }, setpoints = { + burn_rate_en = false, burn_rate = 0.0 }, diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 42d8e33..e984b88 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -422,7 +422,7 @@ threads.thread__setpoint_control = function (smem) local reactor = plc_dev.reactor -- check if we should start ramping - if setpoints.burn_rate ~= last_sp_burn then + if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then if rps.is_active() then if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then -- update without ramp if <= 5 mB/t change @@ -449,33 +449,37 @@ threads.thread__setpoint_control = function (smem) running = false -- adjust burn rate (setpoints.burn_rate) - if rps.is_active() then - local current_burn_rate = reactor.getBurnRate() - if (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then - -- calculate new burn rate - local new_burn_rate = current_burn_rate + if setpoints.burn_rate_en then + if rps.is_active() then + local current_burn_rate = reactor.getBurnRate() - if setpoints.burn_rate > current_burn_rate then - -- need to ramp up - local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate > setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate - end - else - -- need to ramp down - local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate < setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate + -- we yielded, check enable again + if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then + -- calculate new burn rate + local new_burn_rate = current_burn_rate + + if setpoints.burn_rate > current_burn_rate then + -- need to ramp up + local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate > setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + else + -- need to ramp down + local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate < setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end end + + -- set the burn rate + reactor.setBurnRate(new_burn_rate) + + running = running or (new_burn_rate ~= setpoints.burn_rate) end - - -- set the burn rate - reactor.setBurnRate(new_burn_rate) - - running = running or (new_burn_rate ~= setpoints.burn_rate) + else + last_sp_burn = 0 end - else - last_sp_burn = 0 end end diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 2ee2770..1206ef6 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -21,11 +21,16 @@ local RETRY_PERIOD = 1000 local PLC_S_CMDS = { SCRAM = 0, ENABLE = 1, - BURN_RATE = 2, - RPS_RESET = 3 + RPS_RESET = 2 +} + +local PLC_S_DATA = { + BURN_RATE = 1, + RAMP_BURN_RATE = 2 } plc.PLC_S_CMDS = PLC_S_CMDS +plc.PLC_S_DATA = PLC_S_DATA local PERIODICS = { KEEP_ALIVE = 2.0 @@ -42,6 +47,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) out_q = out_queue, commanded_state = false, commanded_burn_rate = 0.0, + ramping_rate = false, -- connection properties seq_num = 0, r_seq_num = nil, @@ -447,12 +453,20 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) elseif message.qtype == mqueue.TYPE.DATA then -- instruction with body local cmd = message.message - if cmd.key == PLC_S_CMDS.BURN_RATE then + if cmd.key == PLC_S_DATA.BURN_RATE then -- update burn rate self.commanded_burn_rate = cmd.val + self.ramping_rate = false self.acks.burn_rate = false self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate }) + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then + -- ramp to burn rate + self.commanded_burn_rate = cmd.val + self.ramping_rate = true + self.acks.burn_rate = false + self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) end end @@ -535,7 +549,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) if not self.acks.burn_rate then if rtimes.burn_rate_req - util.time() <= 0 then - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate }) + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) rtimes.burn_rate_req = util.time() + RETRY_PERIOD end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 72ac729..a7ebcfe 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("session.svsessions") local config = require("config") local supervisor = require("supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.2" +local SUPERVISOR_VERSION = "alpha-v0.3.3" local print = util.print local println = util.println From b7e5ced2e8716d81bec604184e2c5f8521d80433 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 6 May 2022 09:10:50 -0400 Subject: [PATCH 093/168] PLC bugfixes --- reactor-plc/plc.lua | 12 ++++++------ reactor-plc/startup.lua | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 9010610..4ed8cb7 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -114,9 +114,9 @@ plc.rps_init = function (reactor) -- lost the peripheral or terminated, handled later log.error("RPS: failed to check reactor heated coolant level") _set_fault() - state[state_keys.ex_hcoolant] = false + self.state[state_keys.ex_hcoolant] = false else - state[state_keys.ex_hcoolant] = hc_filled > 0.95 + self.state[state_keys.ex_hcoolant] = hc_filled > 0.95 end end @@ -127,9 +127,9 @@ plc.rps_init = function (reactor) -- lost the peripheral or terminated, handled later log.error("RPS: failed to check reactor fuel") _set_fault() - state[state_keys.no_fuel] = false + self.state[state_keys.no_fuel] = false else - state[state_keys.no_fuel] = fuel.amount == 0 + self.state[state_keys.no_fuel] = fuel == 0 end end @@ -142,12 +142,12 @@ plc.rps_init = function (reactor) -- report a PLC comms timeout local trip_timeout = function () - state[state_keys.timed_out] = true + self.state[state_keys.timed_out] = true end -- manually SCRAM the reactor local trip_manual = function () - state[state_keys.manual] = true + self.state[state_keys.manual] = true end -- SCRAM the reactor now diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index deb2b82..e1b07ab 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.3" +local R_PLC_VERSION = "alpha-v0.6.4" local print = util.print local println = util.println From d0b2820160efccd2186a5bf70f268e5cbc58f1ce Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 6 May 2022 10:48:46 -0400 Subject: [PATCH 094/168] logging optimizations --- scada-common/log.lua | 77 +++++++++++++++++++++++++------------------- 1 file changed, 43 insertions(+), 34 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index 39069ca..a4f63b2 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -1,11 +1,11 @@ +local util = require("scada-common.util") + -- -- File System Logger -- local log = {} --- we use extra short abbreviations since computer craft screens are very small - local MODE = { APPEND = 0, NEW = 1 @@ -13,54 +13,63 @@ local MODE = { log.MODE = MODE +---------------------------- +-- PRIVATE DATA/FUNCTIONS -- +---------------------------- + local LOG_DEBUG = true -local log_path = "/log.txt" -local mode = MODE.APPEND -local file_handle = nil +local _log_sys = { + path = "/log.txt", + mode = MODE.APPEND, + file = nil +} local _log = function (msg) - local stamped = os.date("[%c] ") .. msg + local time_stamp = os.date("[%c] ") + local stamped = time_stamp .. msg -- attempt to write log local status, result = pcall(function () - file_handle.writeLine(stamped) - file_handle.flush() + _log_sys.file.writeLine(stamped) + _log_sys.file.flush() end) - -- if we don't have much space, we need to create a new log file - local delete_log = fs.getFreeSpace(log_path) < 100 + -- if we don't have space, we need to create a new log file if not status then if result == "Out of space" then - delete_log = true + -- will delete log file elseif result ~= nil then - print("unknown error writing to logfile: " .. result) + util.println("unknown error writing to logfile: " .. result) end end - if delete_log then + if (result == "Out of space") or (fs.getFreeSpace(_log_sys.path) < 100) then -- delete the old log file and open a new one - file_handle.close() - fs.delete(log_path) - init(log_path, mode) + _log_sys.file.close() + fs.delete(_log_sys.path) + init(_log_sys.path, _log_sys.mode) -- leave a message - local notif = os.date("[%c] ") .. "recycled log file" - file_handle.writeLine(notif) - file_handle.writeLine(stamped) - file_handle.flush() + _log_sys.file.writeLine(time_stamp .. "recycled log file") + _log_sys.file.writeLine(stamped) + _log_sys.file.flush() end end -log.init = function (path, write_mode) - log_path = path - mode = write_mode +---------------------- +-- PUBLIC FUNCTIONS -- +---------------------- - if mode == MODE.APPEND then - file_handle = fs.open(path, "a") +log.init = function (path, write_mode) + _log_sys.path = path + _log_sys.mode = write_mode + + if _log_sys.mode == MODE.APPEND then + _log_sys.file = fs.open(path, "a") else - file_handle = fs.open(path, "w+") + _log_sys.file = fs.open(path, "w+") end end @@ -69,14 +78,14 @@ log.debug = function (msg, trace) local dbg_info = "" if trace then + local info = debug.getinfo(2) local name = "" - if debug.getinfo(2).name ~= nil then - name = ":" .. debug.getinfo(2).name .. "():" + if info.name ~= nil then + name = ":" .. info.name .. "():" end - dbg_info = debug.getinfo(2).short_src .. ":" .. name .. - debug.getinfo(2).currentline .. " > " + dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " end _log("[DBG] " .. dbg_info .. msg) @@ -95,14 +104,14 @@ log.error = function (msg, trace) local dbg_info = "" if trace then + local info = debug.getinfo(2) local name = "" - if debug.getinfo(2).name ~= nil then - name = ":" .. debug.getinfo(2).name .. "():" + if info.name ~= nil then + name = ":" .. info.name .. "():" end - dbg_info = debug.getinfo(2).short_src .. ":" .. name .. - debug.getinfo(2).currentline .. " > " + dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " end _log("[ERR] " .. dbg_info .. msg) From 17a46ae642d48a482866e4895067f1804756b81e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 6 May 2022 10:53:12 -0400 Subject: [PATCH 095/168] mqueue optimizations --- scada-common/mqueue.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index 8ba14cd..c24c15c 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -15,6 +15,9 @@ mqueue.TYPE = TYPE mqueue.new = function () local queue = {} + local insert = table.insert + local remove = table.remove + local length = function () return #queue end @@ -24,11 +27,11 @@ mqueue.new = function () end local ready = function () - return #queue > 0 + return #queue ~= 0 end local _push = function (qtype, message) - table.insert(queue, { qtype = qtype, message = message }) + insert(queue, { qtype = qtype, message = message }) end local push_command = function (message) @@ -45,7 +48,7 @@ mqueue.new = function () local pop = function () if #queue > 0 then - return table.remove(queue, 1) + return remove(queue, 1) else return nil end From 4aab75b842a89128853e4c4ff37f41d1dbdaee0e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 6 May 2022 11:11:53 -0400 Subject: [PATCH 096/168] rsio optimizations --- scada-common/rsio.lua | 57 +++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index fd71247..faf9008 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -4,6 +4,10 @@ local rsio = {} +---------------------- +-- RS I/O CONSTANTS -- +---------------------- + local IO_LVL = { LOW = 0, HIGH = 1 @@ -64,6 +68,11 @@ rsio.IO_DIR = IO_DIR rsio.IO_MODE = IO_MODE rsio.IO = RS_IO +----------------------- +-- UTILITY FUNCTIONS -- +----------------------- + +-- channel to string rsio.to_string = function (channel) local names = { "F_SCRAM", @@ -96,22 +105,7 @@ rsio.to_string = function (channel) end end -rsio.is_valid_channel = function (channel) - return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE -end - -rsio.is_valid_side = function (side) - if side ~= nil then - for _, s in pairs(rs.getSides()) do - if s == side then return true end - end - end - return false -end - -rsio.is_color = function (color) - return (color > 0) and (bit.band(color, (color - 1)) == 0); -end +local _B_AND = bit.band local _TRINARY = function (cond, t, f) if cond then return t else return f end end @@ -160,6 +154,7 @@ local RS_DIO_MAP = { { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT } } +-- get the mode of a channel rsio.get_io_mode = function (channel) local modes = { IO_MODE.DIGITAL_IN, -- F_SCRAM @@ -192,6 +187,36 @@ rsio.get_io_mode = function (channel) end end +-------------------- +-- GENERIC CHECKS -- +-------------------- + +local RS_SIDES = rs.getSides() + +-- check if a channel is valid +rsio.is_valid_channel = function (channel) + return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE +end + +-- check if a side is valid +rsio.is_valid_side = function (side) + if side ~= nil then + for i = 0, #RS_SIDES do + if RS_SIDES[i] == side then return true end + end + end + return false +end + +-- check if a color is a valid single color +rsio.is_color = function (color) + return (color > 0) and (_B_AND(color, (color - 1)) == 0); +end + +----------------- +-- DIGITAL I/O -- +----------------- + -- get digital IO level reading rsio.digital_read = function (rs_value) if rs_value then From 96e535fdc4c9f98235564bb88e488c7d55606be8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 7 May 2022 13:39:12 -0400 Subject: [PATCH 097/168] global scope optimizations --- rtu/modbus.lua | 10 ++++++---- rtu/rtu.lua | 14 ++++++++------ scada-common/comms.lua | 12 +++++++----- 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/rtu/modbus.lua b/rtu/modbus.lua index ac26fc1..bb1817a 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -13,6 +13,8 @@ modbus.new = function (rtu_dev, use_parallel_read) use_parallel = use_parallel_read } + local insert = table.insert + local _1_read_coils = function (c_addr_start, count) local tasks = {} local readings = {} @@ -25,7 +27,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local addr = c_addr_start + i - 1 if self.use_parallel then - table.insert(tasks, function () + insert(tasks, function () local reading, fault = self.rtu.read_coil(addr) if fault then access_fault = true else readings[i] = reading end end) @@ -68,7 +70,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local addr = di_addr_start + i - 1 if self.use_parallel then - table.insert(tasks, function () + insert(tasks, function () local reading, fault = self.rtu.read_di(addr) if fault then access_fault = true else readings[i] = reading end end) @@ -111,7 +113,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local addr = hr_addr_start + i - 1 if self.use_parallel then - table.insert(tasks, function () + insert(tasks, function () local reading, fault = self.rtu.read_holding_reg(addr) if fault then access_fault = true else readings[i] = reading end end) @@ -154,7 +156,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local addr = ir_addr_start + i - 1 if self.use_parallel then - table.insert(tasks, function () + insert(tasks, function () local reading, fault = self.rtu.read_input_reg(addr) if fault then access_fault = true else readings[i] = reading end end) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 9c22559..1f5c049 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -18,6 +18,8 @@ rtu.init_unit = function () io_count_cache = { 0, 0, 0, 0 } } + local insert = table.insert + local _count_io = function () self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs } end @@ -31,7 +33,7 @@ rtu.init_unit = function () -- return : count of discrete inputs local connect_di = function (f) - table.insert(self.discrete_inputs, f) + insert(self.discrete_inputs, f) _count_io() return #self.discrete_inputs end @@ -47,7 +49,7 @@ rtu.init_unit = function () -- return : count of coils local connect_coil = function (f_read, f_write) - table.insert(self.coils, { read = f_read, write = f_write }) + insert(self.coils, { read = f_read, write = f_write }) _count_io() return #self.coils end @@ -70,7 +72,7 @@ rtu.init_unit = function () -- return : count of input registers local connect_input_reg = function (f) - table.insert(self.input_regs, f) + insert(self.input_regs, f) _count_io() return #self.input_regs end @@ -86,7 +88,7 @@ rtu.init_unit = function () -- return : count of holding registers local connect_holding_reg = function (f_read, f_write) - table.insert(self.holding_regs, { read = f_read, write = f_write }) + insert(self.holding_regs, { read = f_read, write = f_write }) _count_io() return #self.holding_regs end @@ -293,7 +295,7 @@ rtu.comms = function (modem, local_port, server_port) if type ~= nil then if type == RTU_ADVERT_TYPES.REDSTONE then - table.insert(advertisement, { + insert(advertisement, { unit = i, type = type, index = units[i].index, @@ -301,7 +303,7 @@ rtu.comms = function (modem, local_port, server_port) rsio = units[i].device }) else - table.insert(advertisement, { + insert(advertisement, { unit = i, type = type, index = units[i].index, diff --git a/scada-common/comms.lua b/scada-common/comms.lua index f762c3b..d0e6fdf 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -4,6 +4,8 @@ local comms = {} +local insert = table.insert + local PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol @@ -158,7 +160,7 @@ comms.modbus_packet = function () -- populate raw array self.raw = { self.txn_id, self.unit_id, self.func_code } for i = 1, self.length do - table.insert(self.raw, data[i]) + insert(self.raw, data[i]) end end @@ -248,7 +250,7 @@ comms.rplc_packet = function () -- populate raw array self.raw = { self.id, self.type } for i = 1, #data do - table.insert(self.raw, data[i]) + insert(self.raw, data[i]) end end @@ -331,7 +333,7 @@ comms.mgmt_packet = function () -- populate raw array self.raw = { self.type } for i = 1, #data do - table.insert(self.raw, data[i]) + insert(self.raw, data[i]) end end @@ -410,7 +412,7 @@ comms.coord_packet = function () -- populate raw array self.raw = { self.type } for i = 1, #data do - table.insert(self.raw, data[i]) + insert(self.raw, data[i]) end end @@ -489,7 +491,7 @@ comms.capi_packet = function () -- populate raw array self.raw = { self.type } for i = 1, #data do - table.insert(self.raw, data[i]) + insert(self.raw, data[i]) end end From 469ee29b5a73fb36a27022523e1c02869dc039ae Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 9 May 2022 09:34:26 -0400 Subject: [PATCH 098/168] cleanup of rtu comms --- rtu/rtu.lua | 28 ++++++++------------- scada-common/comms.lua | 56 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 1f5c049..3048e42 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,10 +1,13 @@ local comms = require("scada-common.comms") local ppm = require("scada-common.ppm") +local types = require("scada-common.types") local modbus = require("modbus") local rtu = {} +local rtu_t = types.rtu_t + local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES @@ -281,33 +284,22 @@ rtu.comms = function (modem, local_port, server_port) local advertisement = {} for i = 1, #units do - local type = nil - - if units[i].type == "boiler" then - type = RTU_ADVERT_TYPES.BOILER - elseif units[i].type == "turbine" then - type = RTU_ADVERT_TYPES.TURBINE - elseif units[i].type == "imatrix" then - type = RTU_ADVERT_TYPES.IMATRIX - elseif units[i].type == "redstone" then - type = RTU_ADVERT_TYPES.REDSTONE - end + local unit = units[i] + local type = comms.rtu_t_to_advert_type(unit.type) if type ~= nil then if type == RTU_ADVERT_TYPES.REDSTONE then insert(advertisement, { - unit = i, type = type, - index = units[i].index, - reactor = units[i].for_reactor, - rsio = units[i].device + index = unit.index, + reactor = unit.for_reactor, + rsio = unit.device }) else insert(advertisement, { - unit = i, type = type, - index = units[i].index, - reactor = units[i].for_reactor, + index = unit.index, + reactor = unit.for_reactor, rsio = nil }) end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index d0e6fdf..7a41ff7 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -2,8 +2,11 @@ -- Communications -- +local types = require("scada-common.types") + local comms = {} +local rtu_t = types.rtu_t local insert = table.insert local PROTOCOLS = { @@ -42,10 +45,13 @@ local SCADA_MGMT_TYPES = { } local RTU_ADVERT_TYPES = { - BOILER = 0, -- boiler - TURBINE = 1, -- turbine - IMATRIX = 2, -- induction matrix - REDSTONE = 3 -- redstone I/O + REDSTONE = 0, -- redstone I/O + BOILER = 1, -- boiler + BOILER_VALVE = 2, -- boiler mekanism 10.1+ + TURBINE = 3, -- turbine + TURBINE_VALVE = 4, -- turbine, mekanism 10.1+ + EMACHINE = 5, -- energy machine + IMATRIX = 6 -- induction matrix } comms.PROTOCOLS = PROTOCOLS @@ -544,4 +550,46 @@ comms.capi_packet = function () } end +-- convert rtu_t to RTU advertisement type +comms.rtu_t_to_advert_type = function (type) + if type == rtu_t.redstone then + return RTU_ADVERT_TYPES.REDSTONE + elseif type == rtu_t.boiler then + return RTU_ADVERT_TYPES.BOILER + elseif type == rtu_t.boiler_valve then + return RTU_ADVERT_TYPES.BOILER_VALVE + elseif type == rtu_t.turbine then + return RTU_ADVERT_TYPES.TURBINE + elseif type == rtu_t.turbine_valve then + return RTU_ADVERT_TYPES.TURBINE_VALVE + elseif type == rtu_t.energy_machine then + return RTU_ADVERT_TYPES.EMACHINE + elseif type == rtu_t.induction_matrix then + return RTU_ADVERT_TYPES.IMATRIX + end + + return nil +end + +-- convert RTU advertisement type to rtu_t +comms.advert_type_to_rtu_t = function (atype) + if atype == RTU_ADVERT_TYPES.REDSTONE then + return rtu_t.redstone + elseif atype == RTU_ADVERT_TYPES.BOILER then + return rtu_t.boiler + elseif atype == RTU_ADVERT_TYPES.BOILER_VALVE then + return rtu_t.boiler_valve + elseif atype == RTU_ADVERT_TYPES.TURBINE then + return rtu_t.turbine + elseif atype == RTU_ADVERT_TYPES.TURBINE_VALVE then + return rtu_t.turbine_valve + elseif atype == RTU_ADVERT_TYPES.EMACHINE then + return rtu_t.energy_machine + elseif atype == RTU_ADVERT_TYPES.IMATRIX then + return rtu_t.induction_matrix + end + + return nil +end + return comms From 679d98c8bf522a0a31edbfde3ad2b2c620e12d69 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 9 May 2022 09:35:39 -0400 Subject: [PATCH 099/168] #8 work in progress on RTU sessions and added unit object --- supervisor/session/rtu.lua | 180 +++++++++++++++++++++++++++++++++++++ supervisor/unit.lua | 82 +++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 supervisor/unit.lua diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 9051425..1cf4e76 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -1,3 +1,183 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") +local util = require("scada-common.util") + local rtu = {} +local PROTOCOLS = comms.PROTOCOLS +local RPLC_TYPES = comms.RPLC_TYPES +local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES + +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + +rtu.new_session = function (id, in_queue, out_queue) + local log_header = "rtu_session(" .. id .. "): " + + local self = { + id = id, + in_q = in_queue, + out_q = out_queue, + commanded_state = false, + commanded_burn_rate = 0.0, + ramping_rate = false, + -- connection properties + seq_num = 0, + r_seq_num = nil, + connected = true, + received_struct = false, + received_status_cache = false, + rtu_conn_watchdog = util.new_watchdog(3), + last_rtt = 0 + } + + -- send a MODBUS TCP packet + local send_modbus = function (m_pkt) + local s_pkt = comms.scada_packet() + s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end + + -- send a SCADA management packet + local _send_mgmt = function (msg_type, msg) + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(msg_type, msg) + s_pkt.make(self.seq_num, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) + + self.out_q.push_packet(s_pkt) + self.seq_num = self.seq_num + 1 + end + + -- handle a packet + local _handle_packet = function (pkt) + -- check sequence number + if self.r_seq_num == nil then + self.r_seq_num = pkt.scada_frame.seq_num() + elseif self.r_seq_num >= pkt.scada_frame.seq_num() then + log.warning(log_header .. "sequence out-of-order: last = " .. self.r_seq_num .. ", new = " .. pkt.scada_frame.seq_num()) + return + else + self.r_seq_num = pkt.scada_frame.seq_num() + end + + -- process packet + if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + -- feed watchdog + self.rtu_conn_watchdog.feed() + + elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + -- feed watchdog + self.rtu_conn_watchdog.feed() + + if pkt.type == SCADA_MGMT_TYPES.CLOSE then + -- close the session + self.connected = false + elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then + -- RTU unit advertisement + for i = 1, packet.length do + local unit = packet.data[i] + unit + end + elseif pkt.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT then + -- periodic RTU heartbeat + else + log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) + end + end + end + + -- PUBLIC FUNCTIONS -- + + -- get the session ID + local get_id = function () return self.id end + + -- check if a timer matches this session's watchdog + local check_wd = function (timer) + return timer == self.rtu_conn_watchdog.get_timer() + end + + -- close the connection + local close = function () + self.rtu_conn_watchdog.cancel() + self.connected = false + _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) + println(log_header .. "connection to RTU closed by server") + log.info(log_header .. "session closed by server") + end + + -- iterate the session + local iterate = function () + if self.connected then + ------------------ + -- handle queue -- + ------------------ + + local handle_start = util.time() + + while self.in_q.ready() and self.connected do + -- get a new message to process + local message = self.in_q.pop() + + if message.qtype == mqueue.TYPE.PACKET then + -- handle a packet + _handle_packet(message.message) + elseif message.qtype == mqueue.TYPE.COMMAND then + -- handle instruction + local cmd = message.message + elseif message.qtype == mqueue.TYPE.DATA then + -- instruction with body + local cmd = message.message + end + + -- max 100ms spent processing queue + if util.time() - handle_start > 100 then + log.warning(log_header .. "exceeded 100ms queue process limit") + break + end + end + + -- exit if connection was closed + if not self.connected then + self.rtu_conn_watchdog.cancel() + println(log_header .. "connection to RTU closed by remote host") + log.info(log_header .. "session closed by remote host") + return self.connected + end + + ---------------------- + -- update periodics -- + ---------------------- + + local elapsed = util.time() - self.periodics.last_update + + local periodics = self.periodics + + -- keep alive + + periodics.keep_alive = periodics.keep_alive + elapsed + if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then + -- _send(RPLC_TYPES.KEEP_ALIVE, { util.time() }) + periodics.keep_alive = 0 + end + + self.periodics.last_update = util.time() + end + + return self.connected + end + + return { + get_id = get_id, + check_wd = check_wd, + close = close, + iterate = iterate + } +end + return rtu diff --git a/supervisor/unit.lua b/supervisor/unit.lua new file mode 100644 index 0000000..e7569d6 --- /dev/null +++ b/supervisor/unit.lua @@ -0,0 +1,82 @@ +local unit = {} + +unit.new = function (for_reactor) + local public = {} + + local self = { + r_id = for_reactor, + plc_s = nil, + turbines = {}, + boilers = {}, + energy_storage = {}, + redstone = {}, + db = { + annunciator = { + -- RPS + -- reactor + PLCOnline = false, + ReactorTrip = false, + ManualReactorTrip = false, + RCPTrip = false, + RCSFlowLow = false, + ReactorTempHigh = false, + ReactorHighDeltaT = false, + ReactorOverPower = false, + HighStartupRate = false, + -- boiler + BoilerOnline = false, + HeatingRateLow = false, + CoolantFeedMismatch = false, + -- turbine + TurbineOnline = false, + SteamFeedMismatch = false, + SteamDumpOpen = false, + TurbineTrip = false, + TurbineOverUnderSpeed = false + } + } + } + + public.link_plc_session = function (plc_session) + self.plc_s = plc_session + end + + public.add_turbine = function (turbine) + table.insert(self.turbines, turbine) + end + + public.add_boiler = function (turbine) + table.insert(self.boilers, boiler) + end + + public.add_redstone = function (field, accessor) + -- ensure field exists + if redstone[field] == nil then + redstone[field] = {} + end + + -- insert into list + table.insert(redstone[field], accessor) + end + + local _update_annunciator = function () + self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) + self.db.annunciator.ReactorTrip = false + end + + public.update = function () + -- unlink PLC if session was closed + if not self.plc_s.open then + self.plc_s = nil + end + + -- update annunciator logic + _update_annunciator() + end + + public.get_annunciator = function () return self.db.annunciator end + + return public +end + +return unit From 25558df22d20935c7fc0468baf940589d46648a7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 9 May 2022 15:00:16 -0400 Subject: [PATCH 100/168] RTU/PLC code cleanup, #46 changed KEEP_ALIVE to scada message type and use it for the RTU too --- reactor-plc/plc.lua | 60 +++++++++-------- reactor-plc/startup.lua | 23 ++++--- rtu/rtu.lua | 135 ++++++++++++++++++++++--------------- rtu/startup.lua | 16 +++-- rtu/threads.lua | 4 +- scada-common/comms.lua | 26 ++++--- supervisor/session/plc.lua | 40 +++++------ supervisor/session/rtu.lua | 33 ++++++--- supervisor/startup.lua | 2 +- 9 files changed, 191 insertions(+), 148 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 4ed8cb7..290ca64 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -269,7 +269,7 @@ plc.rps_init = function (reactor) end -- reactor PLC communications -plc.comms = function (id, modem, local_port, server_port, reactor, rps) +plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_watchdog) local self = { id = id, seq_num = 0, @@ -279,6 +279,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) l_port = local_port, reactor = reactor, rps = rps, + conn_watchdog = conn_watchdog, scrammed = false, linked = false, status_cache = nil, @@ -398,7 +399,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) -- keep alive ack local _send_keep_alive_ack = function (srv_time) - _send(RPLC_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack @@ -456,8 +457,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) end -- close the connection to the server - local close = function (conn_watchdog) - conn_watchdog.cancel() + local close = function () + self.conn_watchdog.cancel() unlink() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) end @@ -478,7 +479,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) local sys_status = { util.time(), -- timestamp - (not self.scrammed), -- enabled + (not self.scrammed), -- requested control state rps.is_tripped(), -- overridden degraded, -- degraded self.reactor.getHeatingRate(), -- heating rate @@ -542,7 +543,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) end -- handle an RPLC packet - local handle_packet = function (packet, plc_state, setpoints, conn_watchdog) + local handle_packet = function (packet, plc_state, setpoints) if packet ~= nil then -- check sequence number if self.r_seq_num == nil then @@ -554,29 +555,13 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) self.r_seq_num = packet.scada_frame.seq_num() end - -- feed the watchdog first so it doesn't uhh...eat our packets - conn_watchdog.feed() + -- feed the watchdog first so it doesn't uhh...eat our packets :) + self.conn_watchdog.feed() -- handle packet if packet.scada_frame.protocol() == PROTOCOLS.RPLC then if self.linked then - if packet.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive request received, echo back - if packet.length == 1 then - local timestamp = packet.data[1] - local trip_time = util.time() - timestamp - - if trip_time > 500 then - log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. ")") - end - - -- log.debug("RPLC RTT = ".. trip_time .. "ms") - - _send_keep_alive_ack(timestamp) - else - log.debug("RPLC keep alive packet length mismatch") - end - elseif packet.type == RPLC_TYPES.LINK_REQ then + if packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation if packet.length == 1 then log.debug("received unsolicited link request response") @@ -694,15 +679,34 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps) log.debug("discarding non-link packet before linked") end elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - -- handle session close - if packet.type == SCADA_MGMT_TYPES.CLOSE then - conn_watchdog.cancel() + if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + -- keep alive request received, echo back + if packet.length == 1 then + local timestamp = packet.data[1] + local trip_time = util.time() - timestamp + + if trip_time > 500 then + log.warning("PLC KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)") + end + + -- log.debug("RPLC RTT = ".. trip_time .. "ms") + + _send_keep_alive_ack(timestamp) + else + log.debug("SCADA keep alive packet length mismatch") + end + elseif packet.type == SCADA_MGMT_TYPES.CLOSE then + -- handle session close + self.conn_watchdog.cancel() unlink() println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") else log.warning("received unknown SCADA_MGMT packet type " .. packet.type) end + else + -- should be unreachable assuming packet is from parse_packet() + log.error("illegal packet type " .. protocol, true) end end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e1b07ab..1f3604f 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.4" +local R_PLC_VERSION = "alpha-v0.6.5" local print = util.print local println = util.println @@ -102,30 +102,35 @@ function init() -- init reactor protection system smem_sys.rps = plc.rps_init(smem_dev.reactor) - log.debug("rps init") + log.debug("init> rps init") if __shared_memory.networked then - -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.rps) - log.debug("comms init") - -- comms watchdog, 3 second timeout smem_sys.conn_watchdog = util.new_watchdog(3) - log.debug("conn watchdog started") + log.debug("init> conn watchdog started") + + -- start comms + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) + log.debug("init> comms init") else println("boot> starting in offline mode"); - log.debug("running without networking") + log.debug("init> running without networking") end os.queueEvent("clock_start") println("boot> completed"); + log.debug("init> boot completed") else println("boot> system in degraded state, awaiting devices...") - log.warning("booted in a degraded state, awaiting peripheral connections...") + log.warning("init> booted in a degraded state, awaiting peripheral connections...") end end +---------------------------------------- +-- start system +---------------------------------------- + -- initialize PLC init() diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 3048e42..83359d7 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -125,14 +125,15 @@ rtu.init_unit = function () } end -rtu.comms = function (modem, local_port, server_port) +rtu.comms = function (modem, local_port, server_port, conn_watchdog) local self = { seq_num = 0, r_seq_num = nil, txn_id = 0, modem = modem, s_port = server_port, - l_port = local_port + l_port = local_port, + conn_watchdog = conn_watchdog } -- open modem @@ -153,8 +154,21 @@ rtu.comms = function (modem, local_port, server_port) self.seq_num = self.seq_num + 1 end + -- keep alive ack + local _send_keep_alive_ack = function (srv_time) + _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + end + -- PUBLIC FUNCTIONS -- + -- send a MODBUS TCP packet + local send_modbus = function (m_pkt) + local s_pkt = comms.scada_packet() + s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) + self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) + self.seq_num = self.seq_num + 1 + end + -- reconnect a newly connected modem local reconnect_modem = function (modem) self.modem = modem @@ -165,12 +179,47 @@ rtu.comms = function (modem, local_port, server_port) end end - -- send a MODBUS TCP packet - local send_modbus = function (m_pkt) - local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 + -- unlink from the server + local unlink = function (rtu_state) + rtu_state.linked = false + self.r_seq_num = nil + end + + -- close the connection to the server + local close = function (rtu_state) + self.conn_watchdog.cancel() + unlink(rtu_state) + _send(SCADA_MGMT_TYPES.CLOSE, {}) + end + + -- send capability advertisement + local send_advertisement = function (units) + local advertisement = {} + + for i = 1, #units do + local unit = units[i] + local type = comms.rtu_t_to_advert_type(unit.type) + + if type ~= nil then + if type == RTU_ADVERT_TYPES.REDSTONE then + insert(advertisement, { + type = type, + index = unit.index, + reactor = unit.for_reactor, + rsio = unit.device + }) + else + insert(advertisement, { + type = type, + index = unit.index, + reactor = unit.for_reactor, + rsio = nil + }) + end + end + end + + _send(SCADA_MGMT_TYPES.RTU_ADVERT, advertisement) end -- parse a MODBUS/SCADA packet @@ -203,7 +252,7 @@ rtu.comms = function (modem, local_port, server_port) end -- handle a MODBUS/SCADA packet - local handle_packet = function(packet, units, rtu_state, conn_watchdog) + local handle_packet = function(packet, units, rtu_state) if packet ~= nil then local seq_ok = true @@ -218,7 +267,7 @@ rtu.comms = function (modem, local_port, server_port) end -- feed watchdog on valid sequence number - conn_watchdog.feed() + self.conn_watchdog.feed() local protocol = packet.scada_frame.protocol() @@ -257,10 +306,28 @@ rtu.comms = function (modem, local_port, server_port) send_modbus(reply) elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet - if packet.type == SCADA_MGMT_TYPES.CLOSE then + if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + -- keep alive request received, echo back + if packet.length == 1 then + local timestamp = packet.data[1] + local trip_time = util.time() - timestamp + + if trip_time > 500 then + log.warning("RTU KEEP_ALIVE trip time > 500ms (" .. trip_time .. "ms)") + end + + -- log.debug("RTU RTT = ".. trip_time .. "ms") + + _send_keep_alive_ack(timestamp) + else + log.debug("SCADA keep alive packet length mismatch") + end + elseif packet.type == SCADA_MGMT_TYPES.CLOSE then -- close connection - conn_watchdog.cancel() + self.conn_watchdog.cancel() unlink(rtu_state) + println_ts("server connection closed by remote host") + log.warning("server connection closed by remote host") elseif packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then -- acknowledgement rtu_state.linked = true @@ -279,50 +346,6 @@ rtu.comms = function (modem, local_port, server_port) end end - -- send capability advertisement - local send_advertisement = function (units) - local advertisement = {} - - for i = 1, #units do - local unit = units[i] - local type = comms.rtu_t_to_advert_type(unit.type) - - if type ~= nil then - if type == RTU_ADVERT_TYPES.REDSTONE then - insert(advertisement, { - type = type, - index = unit.index, - reactor = unit.for_reactor, - rsio = unit.device - }) - else - insert(advertisement, { - type = type, - index = unit.index, - reactor = unit.for_reactor, - rsio = nil - }) - end - end - end - - _send(SCADA_MGMT_TYPES.RTU_ADVERT, advertisement) - end - - local send_heartbeat = function () - _send(SCADA_MGMT_TYPES.RTU_HEARTBEAT, {}) - end - - local unlink = function (rtu_state) - rtu_state.linked = false - self.r_seq_num = nil - end - - local close = function (rtu_state) - unlink(rtu_state) - _send(SCADA_MGMT_TYPES.CLOSE, {}) - end - return { send_modbus = send_modbus, reconnect_modem = reconnect_modem, diff --git a/rtu/startup.lua b/rtu/startup.lua index 4edcbc9..5c4a6f4 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -22,7 +22,7 @@ local imatrix_rtu = require("dev.imatrix_rtu") local turbine_rtu = require("dev.turbine_rtu") local turbinev_rtu = require("dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.0" +local RTU_VERSION = "alpha-v0.6.1" local rtu_t = types.rtu_t @@ -80,8 +80,6 @@ if smem_dev.modem == nil then return end -smem_sys.rtu_comms = rtu.comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT) - ---------------------------------------- -- interpret config and init units ---------------------------------------- @@ -230,14 +228,18 @@ end -- start system ---------------------------------------- +-- start connection watchdog +smem_sys.conn_watchdog = util.new_watchdog(5) +log.debug("boot> conn watchdog started") + +-- setup comms +smem_sys.rtu_comms = rtu.comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog) +log.debug("boot> comms init") + -- init threads local main_thread = threads.thread__main(__shared_memory) local comms_thread = threads.thread__comms(__shared_memory) --- start connection watchdog -smem_sys.conn_watchdog = util.new_watchdog(5) -log.debug("init> conn watchdog started") - -- assemble thread list local _threads = { main_thread.exec, comms_thread.exec } for i = 1, #units do diff --git a/rtu/threads.lua b/rtu/threads.lua index 12f90f7..db27c61 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -180,8 +180,7 @@ threads.thread__comms = function (smem) elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet -- handle the packet (rtu_state passed to allow setting link flag) - -- (conn_watchdog passed to allow feeding watchdog) - rtu_comms.handle_packet(msg.message, units, rtu_state, conn_watchdog) + rtu_comms.handle_packet(msg.message, units, rtu_state) end -- quick yield @@ -211,7 +210,6 @@ threads.thread__unit_comms = function (smem, unit) -- load in from shared memory local rtu_state = smem.rtu_state - local packet_queue = unit.pkt_queue local last_update = util.time() diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 7a41ff7..8685390 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -18,16 +18,15 @@ local PROTOCOLS = { } local RPLC_TYPES = { - KEEP_ALIVE = 0, -- keep alive packets - LINK_REQ = 1, -- linking requests - STATUS = 2, -- reactor/system status - MEK_STRUCT = 3, -- mekanism build structure - MEK_BURN_RATE = 4, -- set burn rate - RPS_ENABLE = 5, -- enable reactor - RPS_SCRAM = 6, -- SCRAM reactor - RPS_STATUS = 7, -- RPS status - RPS_ALARM = 8, -- RPS alarm broadcast - RPS_RESET = 9 -- clear RPS trip (if in bad state, will trip immediately) + LINK_REQ = 0, -- linking requests + STATUS = 1, -- reactor/system status + MEK_STRUCT = 2, -- mekanism build structure + MEK_BURN_RATE = 3, -- set burn rate + RPS_ENABLE = 4, -- enable reactor + RPS_SCRAM = 5, -- SCRAM reactor + RPS_STATUS = 6, -- RPS status + RPS_ALARM = 7, -- RPS alarm broadcast + RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately) } local RPLC_LINKING = { @@ -37,11 +36,10 @@ local RPLC_LINKING = { } local SCADA_MGMT_TYPES = { - PING = 0, -- generic ping + KEEP_ALIVE = 0, -- keep alive packet w/ RTT CLOSE = 1, -- close a connection - REMOTE_LINKED = 2, -- remote device linked - RTU_ADVERT = 3, -- RTU capability advertisement - RTU_HEARTBEAT = 4 -- RTU heartbeat + RTU_ADVERT = 2, -- RTU capability advertisement + REMOTE_LINKED = 3 -- remote device linked } local RTU_ADVERT_TYPES = { diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 1206ef6..22726d3 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -243,24 +243,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.plc_conn_watchdog.feed() -- handle packet by type - if pkt.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive reply - if pkt.length == 2 then - local srv_start = pkt.data[1] - local plc_send = pkt.data[2] - local srv_now = util.time() - self.last_rtt = srv_now - srv_start - - if self.last_rtt > 500 then - log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. ")") - end - - -- log.debug(log_header .. "RPLC RTT = ".. self.last_rtt .. "ms") - -- log.debug(log_header .. "RPLC TT = ".. (srv_now - plc_send) .. "ms") - else - log.debug(log_header .. "RPLC keep alive packet length mismatch") - end - elseif pkt.type == RPLC_TYPES.STATUS then + if pkt.type == RPLC_TYPES.STATUS then -- status packet received, update data if pkt.length >= 5 then self.sDB.last_status_update = pkt.data[1] @@ -366,7 +349,24 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type) end elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - if pkt.type == SCADA_MGMT_TYPES.CLOSE then + if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + -- keep alive reply + if pkt.length == 2 then + local srv_start = pkt.data[1] + local plc_send = pkt.data[2] + local srv_now = util.time() + self.last_rtt = srv_now - srv_start + + if self.last_rtt > 500 then + log.warning(log_header .. "PLC KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)") + end + + -- log.debug(log_header .. "PLC RTT = ".. self.last_rtt .. "ms") + -- log.debug(log_header .. "PLC TT = ".. (srv_now - plc_send) .. "ms") + else + log.debug(log_header .. "SCADA keep alive packet length mismatch") + end + elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then -- close the session self.connected = false else @@ -497,7 +497,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - _send(RPLC_TYPES.KEEP_ALIVE, { util.time() }) + _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 1cf4e76..90f6e95 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -35,7 +35,7 @@ rtu.new_session = function (id, in_queue, out_queue) } -- send a MODBUS TCP packet - local send_modbus = function (m_pkt) + local _send_modbus = function (m_pkt) local s_pkt = comms.scada_packet() s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) @@ -66,16 +66,31 @@ rtu.new_session = function (id, in_queue, out_queue) self.r_seq_num = pkt.scada_frame.seq_num() end + -- feed watchdog + self.rtu_conn_watchdog.feed() + -- process packet if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - -- feed watchdog - self.rtu_conn_watchdog.feed() - elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - -- feed watchdog - self.rtu_conn_watchdog.feed() - if pkt.type == SCADA_MGMT_TYPES.CLOSE then + if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then + -- keep alive reply + if pkt.length == 2 then + local srv_start = pkt.data[1] + local rtu_send = pkt.data[2] + local srv_now = util.time() + self.last_rtt = srv_now - srv_start + + if self.last_rtt > 500 then + log.warning(log_header .. "RTU KEEP_ALIVE round trip time > 500ms (" .. self.last_rtt .. "ms)") + end + + -- log.debug(log_header .. "RTU RTT = ".. self.last_rtt .. "ms") + -- log.debug(log_header .. "RTU TT = ".. (srv_now - rtu_send) .. "ms") + else + log.debug(log_header .. "SCADA keep alive packet length mismatch") + end + elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then -- close the session self.connected = false elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then @@ -84,8 +99,6 @@ rtu.new_session = function (id, in_queue, out_queue) local unit = packet.data[i] unit end - elseif pkt.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT then - -- periodic RTU heartbeat else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end @@ -162,7 +175,7 @@ rtu.new_session = function (id, in_queue, out_queue) periodics.keep_alive = periodics.keep_alive + elapsed if periodics.keep_alive >= PERIODICS.KEEP_ALIVE then - -- _send(RPLC_TYPES.KEEP_ALIVE, { util.time() }) + _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { util.time() }) periodics.keep_alive = 0 end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index a7ebcfe..825e30c 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("session.svsessions") local config = require("config") local supervisor = require("supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.3" +local SUPERVISOR_VERSION = "alpha-v0.3.4" local print = util.print local println = util.println From cd0d7aa5a366e3de5f72a8bc772b54dfde7b856e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 11:35:52 -0400 Subject: [PATCH 101/168] cleanup/fixes of scada common code --- scada-common/comms.lua | 11 ++++----- scada-common/log.lua | 49 +++++++++++++++++++++-------------------- scada-common/mqueue.lua | 6 ++--- scada-common/ppm.lua | 2 +- scada-common/rsio.lua | 2 +- scada-common/util.lua | 8 ++++--- 6 files changed, 41 insertions(+), 37 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 8685390..5a84fbc 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -2,6 +2,7 @@ -- Communications -- +local log = require("scada-common.log") local types = require("scada-common.types") local comms = {} @@ -146,11 +147,11 @@ comms.modbus_packet = function () local self = { frame = nil, raw = nil, - txn_id = txn_id, - length = length, - unit_id = unit_id, - func_code = func_code, - data = data + txn_id = nil, + length = nil, + unit_id = nil, + func_code = nil, + data = nil } -- make a MODBUS packet diff --git a/scada-common/log.lua b/scada-common/log.lua index a4f63b2..e841b23 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -13,10 +13,6 @@ local MODE = { log.MODE = MODE ----------------------------- --- PRIVATE DATA/FUNCTIONS -- ----------------------------- - local LOG_DEBUG = true local _log_sys = { @@ -25,12 +21,27 @@ local _log_sys = { file = nil } +local free_space = fs.getFreeSpace + +-- initialize logger +log.init = function (path, write_mode) + _log_sys.path = path + _log_sys.mode = write_mode + + if _log_sys.mode == MODE.APPEND then + _log_sys.file = fs.open(path, "a") + else + _log_sys.file = fs.open(path, "w+") + end +end + +-- private log write function local _log = function (msg) local time_stamp = os.date("[%c] ") local stamped = time_stamp .. msg -- attempt to write log - local status, result = pcall(function () + local status, result = pcall(function () _log_sys.file.writeLine(stamped) _log_sys.file.flush() end) @@ -45,11 +56,11 @@ local _log = function (msg) end end - if (result == "Out of space") or (fs.getFreeSpace(_log_sys.path) < 100) then + if (result == "Out of space") or (free_space(_log_sys.path) < 100) then -- delete the old log file and open a new one _log_sys.file.close() fs.delete(_log_sys.path) - init(_log_sys.path, _log_sys.mode) + log.init(_log_sys.path, _log_sys.mode) -- leave a message _log_sys.file.writeLine(time_stamp .. "recycled log file") @@ -58,21 +69,7 @@ local _log = function (msg) end end ----------------------- --- PUBLIC FUNCTIONS -- ----------------------- - -log.init = function (path, write_mode) - _log_sys.path = path - _log_sys.mode = write_mode - - if _log_sys.mode == MODE.APPEND then - _log_sys.file = fs.open(path, "a") - else - _log_sys.file = fs.open(path, "w+") - end -end - +-- log debug messages log.debug = function (msg, trace) if LOG_DEBUG then local dbg_info = "" @@ -92,17 +89,20 @@ log.debug = function (msg, trace) end end +-- log info messages log.info = function (msg) _log("[INF] " .. msg) end +-- log warning messages log.warning = function (msg) _log("[WRN] " .. msg) end +-- log error messages log.error = function (msg, trace) local dbg_info = "" - + if trace then local info = debug.getinfo(2) local name = "" @@ -110,13 +110,14 @@ log.error = function (msg, trace) if info.name ~= nil then name = ":" .. info.name .. "():" end - + dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " end _log("[ERR] " .. dbg_info .. msg) end +-- log fatal errors log.fatal = function (msg) _log("[FTL] " .. msg) end diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index c24c15c..d1ac5c1 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -29,11 +29,11 @@ mqueue.new = function () local ready = function () return #queue ~= 0 end - + local _push = function (qtype, message) insert(queue, { qtype = qtype, message = message }) end - + local push_command = function (message) _push(TYPE.COMMAND, message) end @@ -49,7 +49,7 @@ mqueue.new = function () local pop = function () if #queue > 0 then return remove(queue, 1) - else + else return nil end end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 5e15724..e834946 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -234,7 +234,7 @@ ppm.get_device = function (name) break end end - + return device end diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index faf9008..d5a3d5a 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -231,7 +231,7 @@ rsio.digital_write = function (channel, active) if channel < RS_IO.WASTE_PO or channel > RS_IO.R_PLC_TIMEOUT then return IO_LVL.LOW else - return RS_DIO_MAP[channel]._f(level) + return RS_DIO_MAP[channel]._f(active) end end diff --git a/scada-common/util.lua b/scada-common/util.lua index a963a08..4ee8567 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -39,6 +39,7 @@ end -- PARALLELIZATION -- -- protected sleep call so we still are in charge of catching termination +-- EVENT_CONSUMER: this function consumes events util.psleep = function (t) pcall(os.sleep, t) end @@ -50,6 +51,7 @@ util.nop = function () end -- attempt to maintain a minimum loop timing (duration of execution) +-- EVENT_CONSUMER: this function consumes events util.adaptive_delay = function (target_timing, last_update) local sleep_for = target_timing - (util.time() - last_update) -- only if >50ms since worker loops already yield 0.05s @@ -64,15 +66,15 @@ end -- ComputerCraft OS Timer based Watchdog -- triggers a timer event if not fed within 'timeout' seconds util.new_watchdog = function (timeout) - local self = { - _timeout = timeout, + local self = { + _timeout = timeout, _wd_timer = os.startTimer(timeout) } local get_timer = function () return self._wd_timer end - + local feed = function () if self._wd_timer ~= nil then os.cancelTimer(self._wd_timer) From d7e38d63930d34fe325282dc81c9517c166374db Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 11:41:49 -0400 Subject: [PATCH 102/168] supression of warnings, added lua diagnostics global list --- .vscode/settings.json | 9 +++++++++ scada-common/util.lua | 16 ++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7978039 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "Lua.diagnostics.globals": [ + "term", + "fs", + "peripheral", + "rs", + "bit" + ] +} \ No newline at end of file diff --git a/scada-common/util.lua b/scada-common/util.lua index 4ee8567..78ab422 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -25,10 +25,12 @@ end -- TIME -- util.time_ms = function () +---@diagnostic disable-next-line: undefined-field return os.epoch('local') end util.time_s = function () +---@diagnostic disable-next-line: undefined-field return os.epoch('local') / 1000 end @@ -41,6 +43,7 @@ end -- protected sleep call so we still are in charge of catching termination -- EVENT_CONSUMER: this function consumes events util.psleep = function (t) +---@diagnostic disable-next-line: undefined-field pcall(os.sleep, t) end @@ -66,9 +69,14 @@ end -- ComputerCraft OS Timer based Watchdog -- triggers a timer event if not fed within 'timeout' seconds util.new_watchdog = function (timeout) + ---@diagnostic disable-next-line: undefined-field + local start_timer = os.startTimer + ---@diagnostic disable-next-line: undefined-field + local cancel_timer = os.cancelTimer + local self = { _timeout = timeout, - _wd_timer = os.startTimer(timeout) + _wd_timer = start_timer(timeout) } local get_timer = function () @@ -77,14 +85,14 @@ util.new_watchdog = function (timeout) local feed = function () if self._wd_timer ~= nil then - os.cancelTimer(self._wd_timer) + cancel_timer(self._wd_timer) end - self._wd_timer = os.startTimer(self._timeout) + self._wd_timer = start_timer(self._timeout) end local cancel = function () if self._wd_timer ~= nil then - os.cancelTimer(self._wd_timer) + cancel_timer(self._wd_timer) end end From 168341db39b01a8884c2cbfd84b0e5244fb45640 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 12:01:56 -0400 Subject: [PATCH 103/168] code cleanup and bugfixes --- .vscode/settings.json | 4 +++- reactor-plc/plc.lua | 6 ++++-- reactor-plc/startup.lua | 12 +++++++----- reactor-plc/threads.lua | 3 ++- rtu/dev/redstone_rtu.lua | 4 ++-- rtu/modbus.lua | 12 ++++++------ rtu/rtu.lua | 12 ++++++++++-- rtu/startup.lua | 2 +- rtu/threads.lua | 5 ++++- supervisor/session/rtu.lua | 5 ++++- supervisor/session/svsessions.lua | 2 +- supervisor/unit.lua | 8 ++++---- 12 files changed, 48 insertions(+), 27 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 7978039..75bb696 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,8 @@ "fs", "peripheral", "rs", - "bit" + "bit", + "parallel", + "colors" ] } \ No newline at end of file diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 290ca64..5b84c7f 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -558,8 +558,10 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- feed the watchdog first so it doesn't uhh...eat our packets :) self.conn_watchdog.feed() + local protocol = packet.scada_frame.protocol() + -- handle packet - if packet.scada_frame.protocol() == PROTOCOLS.RPLC then + if protocol == PROTOCOLS.RPLC then if self.linked then if packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation @@ -678,7 +680,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat else log.debug("discarding non-link packet before linked") end - elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + elseif protocol == PROTOCOLS.SCADA_MGMT then if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then -- keep alive request received, echo back if packet.length == 1 then diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1f3604f..1082afd 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -11,7 +11,7 @@ local config = require("config") local plc = require("plc") local threads = require("threads") -local R_PLC_VERSION = "alpha-v0.6.5" +local R_PLC_VERSION = "alpha-v0.6.6" local print = util.print local println = util.println @@ -46,7 +46,7 @@ local __shared_memory = { burn_rate_en = false, burn_rate = 0.0 }, - + -- core PLC devices plc_dev = { reactor = ppm.get_fission_reactor(), @@ -82,7 +82,7 @@ if smem_dev.reactor == nil then plc_state.degraded = true plc_state.no_reactor = true end -if networked and smem_dev.modem == nil then +if __shared_memory.networked and smem_dev.modem == nil then println("boot> wireless modem not found") log.warning("no wireless modem on startup") @@ -95,7 +95,8 @@ if networked and smem_dev.modem == nil then plc_state.no_modem = true end -function init() +-- PLC init +local init = function () if plc_state.init_ok then -- just booting up, no fission allowed (neutrons stay put thanks) smem_dev.reactor.scram() @@ -117,6 +118,7 @@ function init() log.debug("init> running without networking") end +---@diagnostic disable-next-line: undefined-field os.queueEvent("clock_start") println("boot> completed"); @@ -155,7 +157,7 @@ if __shared_memory.networked then smem_sys.plc_comms.send_rps_status() -- close connection - smem_sys.plc_comms.close(smem_sys.conn_watchdog) + smem_sys.plc_comms.close() end else -- run threads, excluding comms diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index e984b88..9dcb26e 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -51,6 +51,7 @@ threads.thread__main = function (smem, init) -- event loop while true do +---@diagnostic disable-next-line: undefined-field local event, param1, param2, param3, param4, param5 = os.pullEventRaw() -- handle event @@ -443,7 +444,7 @@ threads.thread__setpoint_control = function (smem) if running then -- do not use the actual elapsed time, it could spike -- we do not want to have big jumps as that is what we are trying to avoid in the first place - local min_elapsed_s = SETPOINT_CTRL_SLEEP / 1000.0 + local min_elapsed_s = SP_CTRL_SLEEP / 1000.0 -- clear so we can later evaluate if we should keep running running = false diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 163b749..9683f57 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -27,7 +27,7 @@ redstone_rtu.new = function () return digital_read(rs.getInput(side)) end end - + self.rtu.connect_di(f_read) end @@ -61,7 +61,7 @@ redstone_rtu.new = function () rs.setOutput(side, digital_is_active(channel, level)) end end - + self.rtu.connect_coil(f_read, f_write) end diff --git a/rtu/modbus.lua b/rtu/modbus.lua index bb1817a..b64910f 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -64,7 +64,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local access_fault = false local discrete_inputs, _, _, _ = self.rtu.io_count() local return_ok = ((di_addr_start + count) <= discrete_inputs) and (count > 0) - + if return_ok then for i = 1, count do local addr = di_addr_start + i - 1 @@ -197,7 +197,7 @@ modbus.new = function (rtu_dev, use_parallel_read) if access_fault then return_ok = false - readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + response = MODBUS_EXCODE.SERVER_DEVICE_FAIL end else response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR @@ -210,13 +210,13 @@ modbus.new = function (rtu_dev, use_parallel_read) local response = nil local _, _, _, hold_regs = self.rtu.io_count() local return_ok = hr_addr <= hold_regs - + if return_ok then local access_fault = self.rtu.write_holding_reg(hr_addr, value) if access_fault then return_ok = false - readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + response = MODBUS_EXCODE.SERVER_DEVICE_FAIL end else response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR @@ -238,7 +238,7 @@ modbus.new = function (rtu_dev, use_parallel_read) if access_fault then return_ok = false - readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + response = MODBUS_EXCODE.SERVER_DEVICE_FAIL break end end @@ -262,7 +262,7 @@ modbus.new = function (rtu_dev, use_parallel_read) if access_fault then return_ok = false - readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + response = MODBUS_EXCODE.SERVER_DEVICE_FAIL break end end diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 83359d7..ec20c93 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,6 +1,8 @@ local comms = require("scada-common.comms") local ppm = require("scada-common.ppm") +local log = require("scada-common.log") local types = require("scada-common.types") +local util = require("scada-common.util") local modbus = require("modbus") @@ -12,6 +14,11 @@ local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES +local print = util.print +local println = util.println +local print_ts = util.print_ts +local println_ts = util.println_ts + rtu.init_unit = function () local self = { discrete_inputs = {}, @@ -136,6 +143,8 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) conn_watchdog = conn_watchdog } + local insert = table.insert + -- open modem if not self.modem.isOpen(self.l_port) then self.modem.open(self.l_port) @@ -337,7 +346,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) send_advertisement(units) else -- not supported - log.warning("RTU got unexpected SCADA message type " .. packet.type, true) + log.warning("RTU got unexpected SCADA message type " .. packet.type) end else -- should be unreachable assuming packet is from parse_packet() @@ -352,7 +361,6 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) parse_packet = parse_packet, handle_packet = handle_packet, send_advertisement = send_advertisement, - send_heartbeat = send_heartbeat, unlink = unlink, close = close } diff --git a/rtu/startup.lua b/rtu/startup.lua index 5c4a6f4..18119c0 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -22,7 +22,7 @@ local imatrix_rtu = require("dev.imatrix_rtu") local turbine_rtu = require("dev.turbine_rtu") local turbinev_rtu = require("dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.1" +local RTU_VERSION = "alpha-v0.6.2" local rtu_t = types.rtu_t diff --git a/rtu/threads.lua b/rtu/threads.lua index db27c61..c113e1d 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -1,5 +1,6 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") +local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") local types = require("scada-common.types") local util = require("scada-common.util") @@ -46,6 +47,7 @@ threads.thread__main = function (smem) -- event loop while true do +---@diagnostic disable-next-line: undefined-field local event, param1, param2, param3, param4, param5 = os.pullEventRaw() if event == "timer" and param1 == loop_clock then @@ -210,6 +212,7 @@ threads.thread__unit_comms = function (smem, unit) -- load in from shared memory local rtu_state = smem.rtu_state + local rtu_comms = smem.rtu_sys.rtu_comms local packet_queue = unit.pkt_queue local last_update = util.time() @@ -228,7 +231,7 @@ threads.thread__unit_comms = function (smem, unit) -- received a packet unit.modbus_busy = true local return_code, reply = unit.modbus_io.handle_packet(packet) - rtu.send_modbus(reply) + rtu_comms.send_modbus(reply) unit.modbus_busy = false end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 90f6e95..2e79ec8 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -14,6 +14,10 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +local PERIODICS = { + KEEP_ALIVE = 2.0 +} + rtu.new_session = function (id, in_queue, out_queue) local log_header = "rtu_session(" .. id .. "): " @@ -97,7 +101,6 @@ rtu.new_session = function (id, in_queue, out_queue) -- RTU unit advertisement for i = 1, packet.length do local unit = packet.data[i] - unit end else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 578c8ae..fa983a6 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -97,7 +97,7 @@ local function _free_closed(sessions) for i = 1, #sessions do local session = sessions[i] if session ~= nil then - if sessions[i].open then + if session.open then if sessions[move_to] == nil then sessions[move_to] = session sessions[i] = nil diff --git a/supervisor/unit.lua b/supervisor/unit.lua index e7569d6..95aa5f1 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -45,18 +45,18 @@ unit.new = function (for_reactor) table.insert(self.turbines, turbine) end - public.add_boiler = function (turbine) + public.add_boiler = function (boiler) table.insert(self.boilers, boiler) end public.add_redstone = function (field, accessor) -- ensure field exists - if redstone[field] == nil then - redstone[field] = {} + if self.redstone[field] == nil then + self.redstone[field] = {} end -- insert into list - table.insert(redstone[field], accessor) + table.insert(self.redstone[field], accessor) end local _update_annunciator = function () From 6e1ece8183c91670a1aa7a77858b6b69041cb92f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 13:06:13 -0400 Subject: [PATCH 104/168] watchdog cleanup and loop clock object --- reactor-plc/threads.lua | 12 +++---- rtu/threads.lua | 22 ++++++------ scada-common/util.lua | 71 ++++++++++++++++++++++++++++---------- supervisor/session/plc.lua | 2 +- supervisor/session/rtu.lua | 2 +- 5 files changed, 72 insertions(+), 37 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 9dcb26e..bf05f22 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -39,7 +39,7 @@ threads.thread__main = function (smem, init) -- send link requests at 0.5Hz (every 40 server ticks) (every 4 loop ticks) local LINK_TICKS = 4 local ticks_to_update = 0 - local loop_clock = nil + local loop_clock = util.new_clock(MAIN_CLOCK) -- load in from shared memory local networked = smem.networked @@ -47,7 +47,7 @@ threads.thread__main = function (smem, init) local plc_dev = smem.plc_dev local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog + local conn_watchdog = smem.plc_sys.conn_watchdog ---@type watchdog -- event loop while true do @@ -55,11 +55,11 @@ threads.thread__main = function (smem, init) local event, param1, param2, param3, param4, param5 = os.pullEventRaw() -- handle event - if event == "timer" and param1 == loop_clock then + if event == "timer" and loop_clock.is_clock(param1) then -- core clock tick if networked then -- start next clock timer - loop_clock = os.startTimer(MAIN_CLOCK) + loop_clock.start() -- send updated data if not plc_state.no_modem then @@ -82,7 +82,7 @@ threads.thread__main = function (smem, init) -- pass the packet onto the comms message queue smem.q.mq_comms_rx.push_packet(packet) end - elseif event == "timer" and networked and param1 == conn_watchdog.get_timer() then + elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then -- haven't heard from server recently? shutdown reactor plc_comms.unlink() smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) @@ -165,7 +165,7 @@ threads.thread__main = function (smem, init) end elseif event == "clock_start" then -- start loop clock - loop_clock = os.startTimer(MAIN_CLOCK) + loop_clock.start() log.debug("main thread clock started") end diff --git a/rtu/threads.lua b/rtu/threads.lua index c113e1d..ef30b45 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -35,29 +35,30 @@ threads.thread__main = function (smem) local exec = function () log.debug("main thread start") - -- advertisement/heartbeat clock - local loop_clock = os.startTimer(MAIN_CLOCK) + -- main loop clock + local loop_clock = util.new_clock(MAIN_CLOCK) -- load in from shared memory local rtu_state = smem.rtu_state local rtu_dev = smem.rtu_dev local rtu_comms = smem.rtu_sys.rtu_comms - local conn_watchdog = smem.rtu_sys.conn_watchdog + local conn_watchdog = smem.rtu_sys.conn_watchdog ---@type watchdog local units = smem.rtu_sys.units + -- start clock + loop_clock.start() + -- event loop while true do ---@diagnostic disable-next-line: undefined-field local event, param1, param2, param3, param4, param5 = os.pullEventRaw() - if event == "timer" and param1 == loop_clock then + if event == "timer" and loop_clock.is_clock(param1) then -- start next clock timer - loop_clock = os.startTimer(MAIN_CLOCK) + loop_clock.start() - -- period tick, if we are linked send heartbeat, if not send advertisement - if rtu_state.linked then - rtu_comms.send_heartbeat() - else + -- period tick, if we are not linked send advertisement + if not rtu_state.linked then -- advertise units rtu_comms.send_advertisement(units) end @@ -68,7 +69,7 @@ threads.thread__main = function (smem) -- pass the packet onto the comms message queue smem.q.mq_comms.push_packet(packet) end - elseif event == "timer" and param1 == conn_watchdog.get_timer() then + elseif event == "timer" and conn_watchdog.is_timer(param1) then -- haven't heard from server recently? unlink rtu_comms.unlink(rtu_state) elseif event == "peripheral_detach" then @@ -162,7 +163,6 @@ threads.thread__comms = function (smem) -- load in from shared memory local rtu_state = smem.rtu_state local rtu_comms = smem.rtu_sys.rtu_comms - local conn_watchdog = smem.rtu_sys.conn_watchdog local units = smem.rtu_sys.units local comms_queue = smem.q.mq_comms diff --git a/scada-common/util.lua b/scada-common/util.lua index 78ab422..77ad4c9 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -67,40 +67,75 @@ end -- WATCHDOG -- -- ComputerCraft OS Timer based Watchdog --- triggers a timer event if not fed within 'timeout' seconds +---@param timeout number timeout duration +--- +--- triggers a timer event if not fed within 'timeout' seconds util.new_watchdog = function (timeout) - ---@diagnostic disable-next-line: undefined-field +---@diagnostic disable-next-line: undefined-field local start_timer = os.startTimer - ---@diagnostic disable-next-line: undefined-field +---@diagnostic disable-next-line: undefined-field local cancel_timer = os.cancelTimer local self = { - _timeout = timeout, - _wd_timer = start_timer(timeout) + timeout = timeout, + wd_timer = start_timer(timeout) } - local get_timer = function () - return self._wd_timer + ---@class watchdog + local public = {} + + ---@param timer number timer event timer ID + public.is_timer = function (timer) + return self.wd_timer == timer end - local feed = function () - if self._wd_timer ~= nil then - cancel_timer(self._wd_timer) + -- satiate the beast + public.feed = function () + if self.wd_timer ~= nil then + cancel_timer(self.wd_timer) end - self._wd_timer = start_timer(self._timeout) + self.wd_timer = start_timer(self.timeout) end - local cancel = function () - if self._wd_timer ~= nil then - cancel_timer(self._wd_timer) + -- cancel the watchdog + public.cancel = function () + if self.wd_timer ~= nil then + cancel_timer(self.wd_timer) end end - return { - get_timer = get_timer, - feed = feed, - cancel = cancel + return public +end + +-- LOOP CLOCK -- + +-- ComputerCraft OS Timer based Loop Clock +---@param period number clock period +--- +--- fires a timer event at the specified period, does not start at construct time +util.new_clock = function (period) +---@diagnostic disable-next-line: undefined-field + local start_timer = os.startTimer + + local self = { + period = period, + timer = nil } + + ---@class clock + local public = {} + + ---@param timer number timer event timer ID + public.is_clock = function (timer) + return self.timer == timer + end + + -- start the clock + public.start = function () + self.timer = start_timer(self.period) + end + + return public end return util diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 22726d3..f6a12ca 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -403,7 +403,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- check if a timer matches this session's watchdog local check_wd = function (timer) - return timer == self.plc_conn_watchdog.get_timer() + return self.plc_conn_watchdog.is_timer(timer) end -- close the connection diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 2e79ec8..fb60904 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -115,7 +115,7 @@ rtu.new_session = function (id, in_queue, out_queue) -- check if a timer matches this session's watchdog local check_wd = function (timer) - return timer == self.rtu_conn_watchdog.get_timer() + return self.rtu_conn_watchdog.is_timer(timer) end -- close the connection From 3c688bfafa9e97e7994f5a3389314f7cb4e33799 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 17:06:27 -0400 Subject: [PATCH 105/168] #47 scada-common doc comments --- scada-common/alarm.lua | 23 +++-- scada-common/comms.lua | 222 ++++++++++++++++++++-------------------- scada-common/log.lua | 14 +++ scada-common/mqueue.lua | 59 ++++++----- scada-common/ppm.lua | 40 ++++++-- scada-common/rsio.lua | 23 ++++- scada-common/types.lua | 5 + scada-common/util.lua | 53 +++++++++- 8 files changed, 288 insertions(+), 151 deletions(-) diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua index 7c39bc4..2fcaa19 100644 --- a/scada-common/alarm.lua +++ b/scada-common/alarm.lua @@ -1,7 +1,9 @@ local util = require("scada-common.util") +---@class alarm local alarm = {} +---@alias SEVERITY integer SEVERITY = { INFO = 0, -- basic info message WARNING = 1, -- warning about some abnormal state @@ -13,6 +15,8 @@ SEVERITY = { alarm.SEVERITY = SEVERITY +-- severity integer to string +---@param severity SEVERITY alarm.severity_to_string = function (severity) if severity == SEVERITY.INFO then return "INFO" @@ -31,6 +35,10 @@ alarm.severity_to_string = function (severity) end end +-- create a new scada alarm entry +---@param severity SEVERITY +---@param device string +---@param message string alarm.scada_alarm = function (severity, device, message) local self = { time = util.time(), @@ -40,11 +48,17 @@ alarm.scada_alarm = function (severity, device, message) message = message } - local format = function () + ---@class scada_alarm + local public = {} + + -- format the alarm as a string + ---@return string message + public.format = function () return self.ts_string .. " [" .. alarm.severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message end - local properties = function () + -- get alarm properties + public.properties = function () return { time = self.time, severity = self.severity, @@ -53,10 +67,7 @@ alarm.scada_alarm = function (severity, device, message) } end - return { - format = format, - properties = properties - } + return public end return alarm diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 5a84fbc..9eebb2d 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -5,11 +5,13 @@ local log = require("scada-common.log") local types = require("scada-common.types") +---@class comms local comms = {} local rtu_t = types.rtu_t local insert = table.insert +---@alias PROTOCOLS integer local PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol @@ -18,6 +20,7 @@ local PROTOCOLS = { COORD_API = 4 -- data/control packets for pocket computers to/from coordinators } +---@alias RPLC_TYPES integer local RPLC_TYPES = { LINK_REQ = 0, -- linking requests STATUS = 1, -- reactor/system status @@ -30,12 +33,14 @@ local RPLC_TYPES = { RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately) } +---@alias RPLC_LINKING integer local RPLC_LINKING = { ALLOW = 0, -- link approved DENY = 1, -- link denied COLLISION = 2 -- link denied due to existing active link } +---@alias SCADA_MGMT_TYPES integer local SCADA_MGMT_TYPES = { KEEP_ALIVE = 0, -- keep alive packet w/ RTT CLOSE = 1, -- close a connection @@ -43,6 +48,7 @@ local SCADA_MGMT_TYPES = { REMOTE_LINKED = 3 -- remote device linked } +---@alias RTU_ADVERT_TYPES integer local RTU_ADVERT_TYPES = { REDSTONE = 0, -- redstone I/O BOILER = 1, -- boiler @@ -71,8 +77,14 @@ comms.scada_packet = function () payload = nil } + ---@class scada_packet + local public = {} + -- make a SCADA packet - local make = function (seq_num, protocol, payload) + ---@param seq_num integer + ---@param protocol PROTOCOLS + ---@param payload table + public.make = function (seq_num, protocol, payload) self.valid = true self.seq_num = seq_num self.protocol = protocol @@ -82,7 +94,12 @@ comms.scada_packet = function () end -- parse in a modem message as a SCADA packet - local receive = function (side, sender, reply_to, message, distance) + ---@param side string + ---@param sender integer + ---@param reply_to integer + ---@param message any + ---@param distance integer + public.receive = function (side, sender, reply_to, message, distance) self.modem_msg_in = { iface = side, s_port = sender, @@ -108,40 +125,23 @@ comms.scada_packet = function () -- public accessors -- - local modem_event = function () return self.modem_msg_in end - local raw_sendable = function () return self.raw end + public.modem_event = function () return self.modem_msg_in end + public.raw_sendable = function () return self.raw end - local local_port = function () return self.modem_msg_in.s_port end - local remote_port = function () return self.modem_msg_in.r_port end + public.local_port = function () return self.modem_msg_in.s_port end + public.remote_port = function () return self.modem_msg_in.r_port end - local is_valid = function () return self.valid end + public.is_valid = function () return self.valid end - local seq_num = function () return self.seq_num end - local protocol = function () return self.protocol end - local length = function () return self.length end - local data = function () return self.payload end + public.seq_num = function () return self.seq_num end + public.protocol = function () return self.protocol end + public.length = function () return self.length end + public.data = function () return self.payload end - return { - -- construct - make = make, - receive = receive, - -- raw access - modem_event = modem_event, - raw_sendable = raw_sendable, - -- ports - local_port = local_port, - remote_port = remote_port, - -- well-formed - is_valid = is_valid, - -- packet properties - seq_num = seq_num, - protocol = protocol, - length = length, - data = data - } + return public end --- MODBUS packet +-- MODBUS packet -- modeled after MODBUS TCP packet comms.modbus_packet = function () local self = { @@ -154,8 +154,15 @@ comms.modbus_packet = function () data = nil } + ---@class modbus_packet + local public = {} + -- make a MODBUS packet - local make = function (txn_id, unit_id, func_code, data) + ---@param txn_id integer + ---@param unit_id integer + ---@param func_code MODBUS_FCODE + ---@param data table + public.make = function (txn_id, unit_id, func_code, data) self.txn_id = txn_id self.length = #data self.unit_id = unit_id @@ -170,18 +177,20 @@ comms.modbus_packet = function () end -- decode a MODBUS packet from a SCADA frame - local decode = function (frame) + ---@param frame scada_packet + ---@return boolean success + public.decode = function (frame) if frame then self.frame = frame if frame.protocol() == PROTOCOLS.MODBUS_TCP then local size_ok = frame.length() >= 3 - + if size_ok then local data = frame.data() - make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) + public.make(data[1], data[2], data[3], { table.unpack(data, 4, #data) }) end - + return size_ok else log.debug("attempted MODBUS_TCP parse of incorrect protocol " .. frame.protocol(), true) @@ -194,10 +203,10 @@ comms.modbus_packet = function () end -- get raw to send - local raw_sendable = function () return self.raw end + public.raw_sendable = function () return self.raw end -- get this packet - local get = function () + public.get = function () return { scada_frame = self.frame, txn_id = self.txn_id, @@ -208,15 +217,7 @@ comms.modbus_packet = function () } end - return { - -- construct - make = make, - decode = decode, - -- raw access - raw_sendable = raw_sendable, - -- formatted access - get = get - } + return public end -- reactor PLC packet @@ -230,10 +231,12 @@ comms.rplc_packet = function () body = nil } + ---@class rplc_packet + local public = {} + -- check that type is known local _rplc_type_valid = function () - return self.type == RPLC_TYPES.KEEP_ALIVE or - self.type == RPLC_TYPES.LINK_REQ or + return self.type == RPLC_TYPES.LINK_REQ or self.type == RPLC_TYPES.STATUS or self.type == RPLC_TYPES.MEK_STRUCT or self.type == RPLC_TYPES.MEK_BURN_RATE or @@ -245,7 +248,10 @@ comms.rplc_packet = function () end -- make an RPLC packet - local make = function (id, packet_type, data) + ---@param id integer + ---@param packet_type RPLC_TYPES + ---@param data table + public.make = function (id, packet_type, data) -- packet accessor properties self.id = id self.type = packet_type @@ -260,7 +266,9 @@ comms.rplc_packet = function () end -- decode an RPLC packet from a SCADA frame - local decode = function (frame) + ---@param frame scada_packet + ---@return boolean success + public.decode = function (frame) if frame then self.frame = frame @@ -269,7 +277,7 @@ comms.rplc_packet = function () if ok then local data = frame.data() - make(data[1], data[2], { table.unpack(data, 3, #data) }) + public.make(data[1], data[2], { table.unpack(data, 3, #data) }) ok = _rplc_type_valid() end @@ -285,10 +293,10 @@ comms.rplc_packet = function () end -- get raw to send - local raw_sendable = function () return self.raw end + public.raw_sendable = function () return self.raw end -- get this packet - local get = function () + public.get = function () return { scada_frame = self.frame, id = self.id, @@ -298,15 +306,7 @@ comms.rplc_packet = function () } end - return { - -- construct - make = make, - decode = decode, - -- raw access - raw_sendable = raw_sendable, - -- formatted access - get = get - } + return public end -- SCADA management packet @@ -319,17 +319,21 @@ comms.mgmt_packet = function () data = nil } + ---@class mgmt_packet + local public = {} + -- check that type is known local _scada_type_valid = function () - return self.type == SCADA_MGMT_TYPES.PING or + return self.type == SCADA_MGMT_TYPES.KEEP_ALIVE or self.type == SCADA_MGMT_TYPES.CLOSE or self.type == SCADA_MGMT_TYPES.REMOTE_LINKED or - self.type == SCADA_MGMT_TYPES.RTU_ADVERT or - self.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT + self.type == SCADA_MGMT_TYPES.RTU_ADVERT end -- make a SCADA management packet - local make = function (packet_type, data) + ---@param packet_type SCADA_MGMT_TYPES + ---@param data table + public.make = function (packet_type, data) -- packet accessor properties self.type = packet_type self.length = #data @@ -343,19 +347,21 @@ comms.mgmt_packet = function () end -- decode a SCADA management packet from a SCADA frame - local decode = function (frame) + ---@param frame scada_packet + ---@return boolean success + public.decode = function (frame) if frame then self.frame = frame if frame.protocol() == PROTOCOLS.SCADA_MGMT then local ok = frame.length() >= 1 - + if ok then local data = frame.data() - make(data[1], { table.unpack(data, 2, #data) }) + public.make(data[1], { table.unpack(data, 2, #data) }) ok = _scada_type_valid() end - + return ok else log.debug("attempted SCADA_MGMT parse of incorrect protocol " .. frame.protocol(), true) @@ -368,10 +374,10 @@ comms.mgmt_packet = function () end -- get raw to send - local raw_sendable = function () return self.raw end + public.raw_sendable = function () return self.raw end -- get this packet - local get = function () + public.get = function () return { scada_frame = self.frame, type = self.type, @@ -380,15 +386,7 @@ comms.mgmt_packet = function () } end - return { - -- construct - make = make, - decode = decode, - -- raw access - raw_sendable = raw_sendable, - -- formatted access - get = get - } + return public end -- SCADA coordinator packet @@ -402,13 +400,18 @@ comms.coord_packet = function () data = nil } + ---@class coord_packet + local public = {} + local _coord_type_valid = function () -- @todo return false end -- make a coordinator packet - local make = function (packet_type, data) + ---@param packet_type any + ---@param data table + public.make = function (packet_type, data) -- packet accessor properties self.type = packet_type self.length = #data @@ -422,7 +425,9 @@ comms.coord_packet = function () end -- decode a coordinator packet from a SCADA frame - local decode = function (frame) + ---@param frame scada_packet + ---@return boolean success + public.decode = function (frame) if frame then self.frame = frame @@ -431,7 +436,7 @@ comms.coord_packet = function () if ok then local data = frame.data() - make(data[1], { table.unpack(data, 2, #data) }) + public.make(data[1], { table.unpack(data, 2, #data) }) ok = _coord_type_valid() end @@ -447,10 +452,10 @@ comms.coord_packet = function () end -- get raw to send - local raw_sendable = function () return self.raw end + public.raw_sendable = function () return self.raw end -- get this packet - local get = function () + public.get = function () return { scada_frame = self.frame, type = self.type, @@ -459,15 +464,7 @@ comms.coord_packet = function () } end - return { - -- construct - make = make, - decode = decode, - -- raw access - raw_sendable = raw_sendable, - -- formatted access - get = get - } + return public end -- coordinator API (CAPI) packet @@ -481,13 +478,18 @@ comms.capi_packet = function () data = nil } + ---@class capi_packet + local public = {} + local _coord_type_valid = function () -- @todo return false end - -- make a coordinator packet - local make = function (packet_type, data) + -- make a coordinator API packet + ---@param packet_type any + ---@param data table + public.make = function (packet_type, data) -- packet accessor properties self.type = packet_type self.length = #data @@ -500,8 +502,10 @@ comms.capi_packet = function () end end - -- decode a coordinator packet from a SCADA frame - local decode = function (frame) + -- decode a coordinator API packet from a SCADA frame + ---@param frame scada_packet + ---@return boolean success + public.decode = function (frame) if frame then self.frame = frame @@ -510,7 +514,7 @@ comms.capi_packet = function () if ok then local data = frame.data() - make(data[1], { table.unpack(data, 2, #data) }) + public.make(data[1], { table.unpack(data, 2, #data) }) ok = _coord_type_valid() end @@ -526,10 +530,10 @@ comms.capi_packet = function () end -- get raw to send - local raw_sendable = function () return self.raw end + public.raw_sendable = function () return self.raw end -- get this packet - local get = function () + public.get = function () return { scada_frame = self.frame, type = self.type, @@ -538,18 +542,12 @@ comms.capi_packet = function () } end - return { - -- construct - make = make, - decode = decode, - -- raw access - raw_sendable = raw_sendable, - -- formatted access - get = get - } + return public end -- convert rtu_t to RTU advertisement type +---@param type rtu_t +---@return RTU_ADVERT_TYPES|nil comms.rtu_t_to_advert_type = function (type) if type == rtu_t.redstone then return RTU_ADVERT_TYPES.REDSTONE @@ -571,6 +569,8 @@ comms.rtu_t_to_advert_type = function (type) end -- convert RTU advertisement type to rtu_t +---@param atype RTU_ADVERT_TYPES +---@return rtu_t|nil comms.advert_type_to_rtu_t = function (atype) if atype == RTU_ADVERT_TYPES.REDSTONE then return rtu_t.redstone diff --git a/scada-common/log.lua b/scada-common/log.lua index e841b23..71e4495 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -4,8 +4,10 @@ local util = require("scada-common.util") -- File System Logger -- +---@class log local log = {} +---@alias MODE integer local MODE = { APPEND = 0, NEW = 1 @@ -13,6 +15,7 @@ local MODE = { log.MODE = MODE +-- whether to log debug messages or not local LOG_DEBUG = true local _log_sys = { @@ -21,9 +24,12 @@ local _log_sys = { file = nil } +---@type function local free_space = fs.getFreeSpace -- initialize logger +---@param path string file path +---@param write_mode MODE log.init = function (path, write_mode) _log_sys.path = path _log_sys.mode = write_mode @@ -36,6 +42,7 @@ log.init = function (path, write_mode) end -- private log write function +---@param msg string local _log = function (msg) local time_stamp = os.date("[%c] ") local stamped = time_stamp .. msg @@ -70,6 +77,8 @@ local _log = function (msg) end -- log debug messages +---@param msg string message +---@param trace? boolean include file trace log.debug = function (msg, trace) if LOG_DEBUG then local dbg_info = "" @@ -90,16 +99,20 @@ log.debug = function (msg, trace) end -- log info messages +---@param msg string message log.info = function (msg) _log("[INF] " .. msg) end -- log warning messages +---@param msg string message log.warning = function (msg) _log("[WRN] " .. msg) end -- log error messages +---@param msg string message +---@param trace? boolean include file trace log.error = function (msg, trace) local dbg_info = "" @@ -118,6 +131,7 @@ log.error = function (msg, trace) end -- log fatal errors +---@param msg string message log.fatal = function (msg) _log("[FTL] " .. msg) end diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index d1ac5c1..db97df4 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -4,6 +4,7 @@ local mqueue = {} +---@alias TYPE integer local TYPE = { COMMAND = 0, DATA = 1, @@ -12,41 +13,61 @@ local TYPE = { mqueue.TYPE = TYPE +-- create a new message queue mqueue.new = function () local queue = {} local insert = table.insert local remove = table.remove - local length = function () - return #queue - end + ---@class queue_item + local queue_item = { + qtype = 0, ---@type TYPE + message = 0 ---@type any + } - local empty = function () - return #queue == 0 - end + ---@class mqueue + local public = {} - local ready = function () - return #queue ~= 0 - end + -- get queue length + public.length = function () return #queue end + -- check if queue is empty + ---@return boolean is_empty + public.empty = function () return #queue == 0 end + + -- check if queue has contents + public.ready = function () return #queue ~= 0 end + + -- push a new item onto the queue + ---@param qtype TYPE + ---@param message string local _push = function (qtype, message) insert(queue, { qtype = qtype, message = message }) end - local push_command = function (message) + -- push a command onto the queue + ---@param message any + public.push_command = function (message) _push(TYPE.COMMAND, message) end - local push_data = function (key, value) + -- push data onto the queue + ---@param key any + ---@param value any + public.push_data = function (key, value) _push(TYPE.DATA, { key = key, val = value }) end - local push_packet = function (message) - _push(TYPE.PACKET, message) + -- push a packet onto the queue + ---@param packet scada_packet|modbus_packet|rplc_packet|coord_packet|capi_packet + public.push_packet = function (packet) + _push(TYPE.PACKET, packet) end - local pop = function () + -- get an item off the queue + ---@return queue_item|nil + public.pop = function () if #queue > 0 then return remove(queue, 1) else @@ -54,15 +75,7 @@ mqueue.new = function () end end - return { - length = length, - empty = empty, - ready = ready, - push_packet = push_packet, - push_data = push_data, - push_command = push_command, - pop = pop - } + return public end return mqueue diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index e834946..c5026ea 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -4,9 +4,10 @@ local log = require("scada-common.log") -- Protected Peripheral Manager -- +---@class ppm local ppm = {} -local ACCESS_FAULT = nil +local ACCESS_FAULT = nil ---@type nil ppm.ACCESS_FAULT = ACCESS_FAULT @@ -22,9 +23,12 @@ local _ppm_sys = { mute = false } --- wrap peripheral calls with lua protected call --- we don't want a disconnect to crash a program --- also provides peripheral-specific fault checks (auto-clear fault defaults to true) +-- wrap peripheral calls with lua protected call as we don't want a disconnect to crash a program +--- +---also provides peripheral-specific fault checks (auto-clear fault defaults to true) +--- +---assumes iface is a valid peripheral +---@param iface string CC peripheral interface local peri_init = function (iface) local self = { faulted = false, @@ -150,6 +154,8 @@ ppm.mount_all = function () end -- mount a particular device +---@param iface string CC peripheral interface +---@return string|nil type, table|nil device ppm.mount = function (iface) local ifaces = peripheral.getNames() local pm_dev = nil @@ -171,33 +177,44 @@ ppm.mount = function (iface) end -- handle peripheral_detach event +---@param iface string CC peripheral interface +---@return string|nil type, table|nil device ppm.handle_unmount = function (iface) + local pm_dev = nil + local pm_type = nil + -- what got disconnected? local lost_dev = _ppm_sys.mounts[iface] if lost_dev then - local type = lost_dev.type - log.warning("PPM: lost device " .. type .. " mounted to " .. iface) + pm_type = lost_dev.type + pm_dev = lost_dev.dev + + log.warning("PPM: lost device " .. pm_type .. " mounted to " .. iface) else log.error("PPM: lost device unknown to the PPM mounted to " .. iface) end - return lost_dev + return pm_type, pm_dev end -- GENERAL ACCESSORS -- -- list all available peripherals +---@return table names ppm.list_avail = function () return peripheral.getNames() end -- list mounted peripherals +---@return table mounts ppm.list_mounts = function () return _ppm_sys.mounts end -- get a mounted peripheral by side/interface +---@param iface string CC peripheral interface +---@return table|nil device function table ppm.get_periph = function (iface) if _ppm_sys.mounts[iface] then return _ppm_sys.mounts[iface].dev @@ -205,6 +222,8 @@ ppm.get_periph = function (iface) end -- get a mounted peripheral type by side/interface +---@param iface string CC peripheral interface +---@return string|nil type ppm.get_type = function (iface) if _ppm_sys.mounts[iface] then return _ppm_sys.mounts[iface].type @@ -212,6 +231,8 @@ ppm.get_type = function (iface) end -- get all mounted peripherals by type +---@param name string type name +---@return table devices device function tables ppm.get_all_devices = function (name) local devices = {} @@ -225,6 +246,8 @@ ppm.get_all_devices = function (name) end -- get a mounted peripheral by type (if multiple, returns the first) +---@param name string type name +---@return table|nil device function table ppm.get_device = function (name) local device = nil @@ -241,11 +264,13 @@ end -- SPECIFIC DEVICE ACCESSORS -- -- get the fission reactor (if multiple, returns the first) +---@return table|nil reactor function table ppm.get_fission_reactor = function () return ppm.get_device("fissionReactor") end -- get the wireless modem (if multiple, returns the first) +---@return table|nil modem function table ppm.get_wireless_modem = function () local w_modem = nil @@ -260,6 +285,7 @@ ppm.get_wireless_modem = function () end -- list all connected monitors +---@return table monitors ppm.list_monitors = function () return ppm.get_all_devices("monitor") end diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index d5a3d5a..d71d777 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -8,16 +8,19 @@ local rsio = {} -- RS I/O CONSTANTS -- ---------------------- +---@alias IO_LVL integer local IO_LVL = { LOW = 0, HIGH = 1 } +---@alias IO_DIR integer local IO_DIR = { IN = 0, OUT = 1 } +---@alias IO_MODE integer local IO_MODE = { DIGITAL_OUT = 0, DIGITAL_IN = 1, @@ -25,6 +28,7 @@ local IO_MODE = { ANALOG_IN = 3 } +---@alias RS_IO integer local RS_IO = { -- digital inputs -- @@ -73,6 +77,7 @@ rsio.IO = RS_IO ----------------------- -- channel to string +---@param channel RS_IO rsio.to_string = function (channel) local names = { "F_SCRAM", @@ -155,6 +160,8 @@ local RS_DIO_MAP = { } -- get the mode of a channel +---@param channel RS_IO +---@return IO_MODE rsio.get_io_mode = function (channel) local modes = { IO_MODE.DIGITAL_IN, -- F_SCRAM @@ -194,11 +201,15 @@ end local RS_SIDES = rs.getSides() -- check if a channel is valid +---@param channel RS_IO +---@return boolean valid rsio.is_valid_channel = function (channel) - return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE + return (channel ~= nil) and (channel > 0) and (channel <= RS_IO.A_T_FLOW_RATE) end -- check if a side is valid +---@param side string +---@return boolean valid rsio.is_valid_side = function (side) if side ~= nil then for i = 0, #RS_SIDES do @@ -209,6 +220,8 @@ rsio.is_valid_side = function (side) end -- check if a color is a valid single color +---@param color integer +---@return boolean valid rsio.is_color = function (color) return (color > 0) and (_B_AND(color, (color - 1)) == 0); end @@ -218,6 +231,8 @@ end ----------------- -- get digital IO level reading +---@param rs_value boolean +---@return IO_LVL rsio.digital_read = function (rs_value) if rs_value then return IO_LVL.HIGH @@ -227,6 +242,9 @@ rsio.digital_read = function (rs_value) end -- returns the level corresponding to active +---@param channel RS_IO +---@param active boolean +---@return IO_LVL rsio.digital_write = function (channel, active) if channel < RS_IO.WASTE_PO or channel > RS_IO.R_PLC_TIMEOUT then return IO_LVL.LOW @@ -236,6 +254,9 @@ rsio.digital_write = function (channel, active) end -- returns true if the level corresponds to active +---@param channel RS_IO +---@param level IO_LVL +---@return boolean rsio.digital_is_active = function (channel, level) if channel > RS_IO.R_ENABLE or channel > RS_IO.R_PLC_TIMEOUT then return false diff --git a/scada-common/types.lua b/scada-common/types.lua index 5bd747e..372b1d3 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -2,8 +2,10 @@ -- Global Types -- +---@class types local types = {} +---@alias rtu_t string types.rtu_t = { redstone = "redstone", boiler = "boiler", @@ -14,6 +16,7 @@ types.rtu_t = { induction_matrix = "induction_matrix" } +---@alias rps_status_t string types.rps_status_t = { ok = "ok", dmg_crit = "dmg_crit", @@ -30,6 +33,7 @@ types.rps_status_t = { -- MODBUS -- modbus function codes +---@alias MODBUS_FCODE integer types.MODBUS_FCODE = { READ_COILS = 0x01, READ_DISCRETE_INPUTS = 0x02, @@ -43,6 +47,7 @@ types.MODBUS_FCODE = { } -- modbus exception codes +---@alias MODBUS_EXCODE integer types.MODBUS_EXCODE = { ILLEGAL_FUNCTION = 0x01, ILLEGAL_DATA_ADDR = 0x02, diff --git a/scada-common/util.lua b/scada-common/util.lua index 77ad4c9..8dffd40 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -1,3 +1,8 @@ +-- +-- Utility Functions +-- + +---@class util local util = {} -- PRINT -- @@ -24,16 +29,22 @@ end -- TIME -- +-- current time +---@return integer milliseconds util.time_ms = function () ---@diagnostic disable-next-line: undefined-field return os.epoch('local') end +-- current time +---@return integer seconds util.time_s = function () ---@diagnostic disable-next-line: undefined-field return os.epoch('local') / 1000 end +-- current time +---@return integer milliseconds util.time = function () return util.time_ms() end @@ -41,19 +52,24 @@ end -- PARALLELIZATION -- -- protected sleep call so we still are in charge of catching termination --- EVENT_CONSUMER: this function consumes events +---@param t integer seconds +--- EVENT_CONSUMER: this function consumes events util.psleep = function (t) ---@diagnostic disable-next-line: undefined-field pcall(os.sleep, t) end --- no-op to provide a brief pause (and a yield) --- EVENT_CONSUMER: this function consumes events +-- no-op to provide a brief pause (1 tick) to yield +--- +--- EVENT_CONSUMER: this function consumes events util.nop = function () util.psleep(0.05) end -- attempt to maintain a minimum loop timing (duration of execution) +---@param target_timing integer minimum amount of milliseconds to wait for +---@param last_update integer millisecond time of last update +---@return integer time_now -- EVENT_CONSUMER: this function consumes events util.adaptive_delay = function (target_timing, last_update) local sleep_for = target_timing - (util.time() - last_update) @@ -64,6 +80,37 @@ util.adaptive_delay = function (target_timing, last_update) return util.time() end +-- MEKANISM POWER -- + +-- function kFE(fe) return fe / 1000 end +-- function MFE(fe) return fe / 1000000 end +-- function GFE(fe) return fe / 1000000000 end +-- function TFE(fe) return fe / 1000000000000 end + +-- -- FLOATING POINT PRINTS -- + +-- local function fractional_1s(number) +-- return number == math.round(number) +-- end + +-- local function fractional_10ths(number) +-- number = number * 10 +-- return number == math.round(number) +-- end + +-- local function fractional_100ths(number) +-- number = number * 100 +-- return number == math.round(number) +-- end + +-- function power_format(fe) +-- if fe < 1000 then +-- return string.format("%.2f FE", fe) +-- elseif fe < 1000000 then +-- return string.format("%.3f kFE", kFE(fe)) +-- end +-- end + -- WATCHDOG -- -- ComputerCraft OS Timer based Watchdog From e3a4ed53631d78603bbcab7d108d8ddcbb648e60 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 17:08:38 -0400 Subject: [PATCH 106/168] catch nil cases, supervisor use loop clock --- reactor-plc/threads.lua | 110 +++++++++++++++++++++------------------- rtu/threads.lua | 102 +++++++++++++++++++------------------ supervisor/startup.lua | 47 +++++++++-------- 3 files changed, 136 insertions(+), 123 deletions(-) diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index bf05f22..7231f2a 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -88,74 +88,78 @@ threads.thread__main = function (smem, init) smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) elseif event == "peripheral_detach" then -- peripheral disconnect - local device = ppm.handle_unmount(param1) - - if device.type == "fissionReactor" then - println_ts("reactor disconnected!") - log.error("reactor disconnected!") - plc_state.no_reactor = true - plc_state.degraded = true - elseif networked and device.type == "modem" then - -- we only care if this is our wireless modem - if device.dev == plc_dev.modem then - println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") - plc_state.no_modem = true - - if plc_state.init_ok then - -- try to scram reactor if it is still connected - smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) - end + local type, device = ppm.handle_unmount(param1) + if type ~= nil and device ~= nil then + if type == "fissionReactor" then + println_ts("reactor disconnected!") + log.error("reactor disconnected!") + plc_state.no_reactor = true plc_state.degraded = true - else - log.warning("non-comms modem disconnected") + elseif networked and type == "modem" then + -- we only care if this is our wireless modem + if device == plc_dev.modem then + println_ts("wireless modem disconnected!") + log.error("comms modem disconnected!") + plc_state.no_modem = true + + if plc_state.init_ok then + -- try to scram reactor if it is still connected + smem.q.mq_rps.push_command(MQ__RPS_CMD.DEGRADED_SCRAM) + end + + plc_state.degraded = true + else + log.warning("non-comms modem disconnected") + end end end elseif event == "peripheral" then -- peripheral connect local type, device = ppm.mount(param1) - if type == "fissionReactor" then - -- reconnected reactor - plc_dev.reactor = device + if type ~= nil and device ~= nil then + if type == "fissionReactor" then + -- reconnected reactor + plc_dev.reactor = device - smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) + smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) - println_ts("reactor reconnected.") - log.info("reactor reconnected.") - plc_state.no_reactor = false - - if plc_state.init_ok then - rps.reconnect_reactor(plc_dev.reactor) - if networked then - plc_comms.reconnect_reactor(plc_dev.reactor) - end - end - - -- determine if we are still in a degraded state - if not networked or ppm.get_device("modem") ~= nil then - plc_state.degraded = false - end - elseif networked and type == "modem" then - if device.isWireless() then - -- reconnected modem - plc_dev.modem = device + println_ts("reactor reconnected.") + log.info("reactor reconnected.") + plc_state.no_reactor = false if plc_state.init_ok then - plc_comms.reconnect_modem(plc_dev.modem) + rps.reconnect_reactor(plc_dev.reactor) + if networked then + plc_comms.reconnect_reactor(plc_dev.reactor) + end end - println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") - plc_state.no_modem = false - -- determine if we are still in a degraded state - if ppm.get_device("fissionReactor") ~= nil then + if not networked or ppm.get_device("modem") ~= nil then plc_state.degraded = false end - else - log.info("wired modem reconnected.") + elseif networked and type == "modem" then + if device.isWireless() then + -- reconnected modem + plc_dev.modem = device + + if plc_state.init_ok then + plc_comms.reconnect_modem(plc_dev.modem) + end + + println_ts("wireless modem reconnected.") + log.info("comms modem reconnected.") + plc_state.no_modem = false + + -- determine if we are still in a degraded state + if ppm.get_device("fissionReactor") ~= nil then + plc_state.degraded = false + end + else + log.info("wired modem reconnected.") + end end end @@ -203,7 +207,7 @@ threads.thread__rps = function (smem) -- thread loop while true do local reactor = plc_dev.reactor - + -- RPS checks if plc_state.init_ok then -- SCRAM if no open connection @@ -240,7 +244,7 @@ threads.thread__rps = function (smem) end end end - + -- check for messages in the message queue while rps_queue.ready() and not plc_state.shutdown do local msg = rps_queue.pop() diff --git a/rtu/threads.lua b/rtu/threads.lua index ef30b45..5799efb 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -74,24 +74,26 @@ threads.thread__main = function (smem) rtu_comms.unlink(rtu_state) elseif event == "peripheral_detach" then -- handle loss of a device - local device = ppm.handle_unmount(param1) + local type, device = ppm.handle_unmount(param1) - if device.type == "modem" then - -- we only care if this is our wireless modem - if device.dev == rtu_dev.modem then - println_ts("wireless modem disconnected!") - log.warning("comms modem disconnected!") + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only care if this is our wireless modem + if device == rtu_dev.modem then + println_ts("wireless modem disconnected!") + log.warning("comms modem disconnected!") + else + log.warning("non-comms modem disconnected") + end else - log.warning("non-comms modem disconnected") - end - else - for i = 1, #units do - -- find disconnected device - if units[i].device == device.dev then - -- we are going to let the PPM prevent crashes - -- return fault flags/codes to MODBUS queries - local unit = units[i] - println_ts("lost the " .. unit.type .. " on interface " .. unit.name) + for i = 1, #units do + -- find disconnected device + if units[i].device == device then + -- we are going to let the PPM prevent crashes + -- return fault flags/codes to MODBUS queries + local unit = units[i] + println_ts("lost the " .. unit.type .. " on interface " .. unit.name) + end end end end @@ -99,44 +101,46 @@ threads.thread__main = function (smem) -- peripheral connect local type, device = ppm.mount(param1) - if type == "modem" then - if device.isWireless() then - -- reconnected modem - rtu_dev.modem = device - rtu_comms.reconnect_modem(rtu_dev.modem) + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() then + -- reconnected modem + rtu_dev.modem = device + rtu_comms.reconnect_modem(rtu_dev.modem) - println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") + println_ts("wireless modem reconnected.") + log.info("comms modem reconnected.") + else + log.info("wired modem reconnected.") + end else - log.info("wired modem reconnected.") - end - else - -- relink lost peripheral to correct unit entry - for i = 1, #units do - local unit = units[i] + -- relink lost peripheral to correct unit entry + for i = 1, #units do + local unit = units[i] - -- find disconnected device to reconnect - if unit.name == param1 then - -- found, re-link - unit.device = device + -- find disconnected device to reconnect + if unit.name == param1 then + -- found, re-link + unit.device = device - if unit.type == rtu_t.boiler then - unit.rtu = boiler_rtu.new(device) - elseif unit.type == rtu_t.boiler_valve then - unit.rtu = boilerv_rtu.new(device) - elseif unit.type == rtu_t.turbine then - unit.rtu = turbine_rtu.new(device) - elseif unit.type == rtu_t.turbine_valve then - unit.rtu = turbinev_rtu.new(device) - elseif unit.type == rtu_t.energy_machine then - unit.rtu = energymachine_rtu.new(device) - elseif unit.type == rtu_t.induction_matrix then - unit.rtu = imatrix_rtu.new(device) + if unit.type == rtu_t.boiler then + unit.rtu = boiler_rtu.new(device) + elseif unit.type == rtu_t.boiler_valve then + unit.rtu = boilerv_rtu.new(device) + elseif unit.type == rtu_t.turbine then + unit.rtu = turbine_rtu.new(device) + elseif unit.type == rtu_t.turbine_valve then + unit.rtu = turbinev_rtu.new(device) + elseif unit.type == rtu_t.energy_machine then + unit.rtu = energymachine_rtu.new(device) + elseif unit.type == rtu_t.induction_matrix then + unit.rtu = imatrix_rtu.new(device) + end + + unit.modbus_io = modbus.new(unit.rtu) + + println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end - - unit.modbus_io = modbus.new(unit.rtu) - - println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end end end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 825e30c..bf0eabc 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -43,41 +43,46 @@ local superv_comms = supervisor.comms(config.NUM_REACTORS, modem, config.SCADA_D -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 -local loop_clock = os.startTimer(MAIN_CLOCK) +local loop_clock = util.new_clock(MAIN_CLOCK) -- event loop while true do +---@diagnostic disable-next-line: undefined-field local event, param1, param2, param3, param4, param5 = os.pullEventRaw() -- handle event if event == "peripheral_detach" then - local device = ppm.handle_unmount(param1) + local type, device = ppm.handle_unmount(param1) - if device.type == "modem" then - -- we only care if this is our wireless modem - if device.dev == modem then - println_ts("wireless modem disconnected!") - log.error("comms modem disconnected!") - else - log.warning("non-comms modem disconnected") + if type ~= nil and device ~= nil then + if type == "modem" then + -- we only care if this is our wireless modem + if device == modem then + println_ts("wireless modem disconnected!") + log.error("comms modem disconnected!") + else + log.warning("non-comms modem disconnected") + end end end elseif event == "peripheral" then local type, device = ppm.mount(param1) - if type == "modem" then - if device.isWireless() then - -- reconnected modem - modem = device - superv_comms.reconnect_modem(modem) + if type ~= nil and device ~= nil then + if type == "modem" then + if device.isWireless() then + -- reconnected modem + modem = device + superv_comms.reconnect_modem(modem) - println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") - else - log.info("wired modem reconnected.") + println_ts("wireless modem reconnected.") + log.info("comms modem reconnected.") + else + log.info("wired modem reconnected.") + end end end - elseif event == "timer" and param1 == loop_clock then + elseif event == "timer" and loop_clock.is_clock(param1) then -- main loop tick -- iterate sessions @@ -86,9 +91,9 @@ while true do -- free any closed sessions svsessions.free_all_closed() - loop_clock = os.startTimer(MAIN_CLOCK) + loop_clock.start() elseif event == "timer" then - -- another timer event, check watchdogs + -- a non-clock timer event, check watchdogs svsessions.check_all_watchdogs(param1) elseif event == "modem_message" then -- got a packet From 87de804a9e5fc2400097d267d75533fe5ac7730d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 17:09:02 -0400 Subject: [PATCH 107/168] proper module format --- coordinator/coordinator.lua | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 8089be8..a6bf236 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -1,8 +1,12 @@ local comms = require("scada-common.comms") +local coordinator = {} + -- coordinator communications -function coord_comms() +coordinator.coord_comms = function () local self = { reactor_struct_cache = nil } end + +return coordinator From f4e397ebb15df9ca7a9a1ecfab1eddf335485208 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 17:16:34 -0400 Subject: [PATCH 108/168] removed old controller code to not pollute workspace with globals --- coordinator/old-controller/controller.lua | 135 -------- coordinator/old-controller/defs.lua | 23 -- coordinator/old-controller/log.lua | 52 --- coordinator/old-controller/reactor.lua | 28 -- coordinator/old-controller/regulator.lua | 128 -------- coordinator/old-controller/render.lua | 370 ---------------------- coordinator/old-controller/server.lua | 109 ------- 7 files changed, 845 deletions(-) delete mode 100644 coordinator/old-controller/controller.lua delete mode 100644 coordinator/old-controller/defs.lua delete mode 100644 coordinator/old-controller/log.lua delete mode 100644 coordinator/old-controller/reactor.lua delete mode 100644 coordinator/old-controller/regulator.lua delete mode 100644 coordinator/old-controller/render.lua delete mode 100644 coordinator/old-controller/server.lua diff --git a/coordinator/old-controller/controller.lua b/coordinator/old-controller/controller.lua deleted file mode 100644 index b0e18b7..0000000 --- a/coordinator/old-controller/controller.lua +++ /dev/null @@ -1,135 +0,0 @@ --- mekanism reactor controller --- monitors and regulates mekanism reactors - -os.loadAPI("reactor.lua") -os.loadAPI("defs.lua") -os.loadAPI("log.lua") -os.loadAPI("render.lua") -os.loadAPI("server.lua") -os.loadAPI("regulator.lua") - --- constants, aliases, properties -local header = "MEKANISM REACTOR CONTROLLER - v" .. defs.CTRL_VERSION -local monitor_0 = peripheral.wrap(defs.MONITOR_0) -local monitor_1 = peripheral.wrap(defs.MONITOR_1) -local monitor_2 = peripheral.wrap(defs.MONITOR_2) -local monitor_3 = peripheral.wrap(defs.MONITOR_3) - -monitor_0.setBackgroundColor(colors.black) -monitor_0.setTextColor(colors.white) -monitor_0.clear() - -monitor_1.setBackgroundColor(colors.black) -monitor_1.setTextColor(colors.white) -monitor_1.clear() - -monitor_2.setBackgroundColor(colors.black) -monitor_2.setTextColor(colors.white) -monitor_2.clear() - -log.init(monitor_3) - -local main_w, main_h = monitor_0.getSize() -local view = window.create(monitor_0, 1, 1, main_w, main_h) -view.setBackgroundColor(colors.black) -view.clear() - -local stat_w, stat_h = monitor_1.getSize() -local stat_view = window.create(monitor_1, 1, 1, stat_w, stat_h) -stat_view.setBackgroundColor(colors.black) -stat_view.clear() - -local reactors = { - reactor.create(1, view, stat_view, 62, 3, 63, 2), - reactor.create(2, view, stat_view, 42, 3, 43, 2), - reactor.create(3, view, stat_view, 22, 3, 23, 2), - reactor.create(4, view, stat_view, 2, 3, 3, 2) -} -print("[debug] reactor tables created") - -server.init(reactors) -print("[debug] modem server started") - -regulator.init(reactors) -print("[debug] regulator started") - --- header -view.setBackgroundColor(colors.white) -view.setTextColor(colors.black) -view.setCursorPos(1, 1) -local header_pad_x = (main_w - string.len(header)) / 2 -view.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x)) - --- inital draw of each reactor -for key, rctr in pairs(reactors) do - render.draw_reactor_system(rctr) - render.draw_reactor_status(rctr) -end - --- inital draw of clock -monitor_2.setTextScale(2) -monitor_2.setCursorPos(1, 1) -monitor_2.write(os.date("%Y/%m/%d %H:%M:%S")) - -local clock_update_timer = os.startTimer(1) - -while true do - event, param1, param2, param3, param4, param5 = os.pullEvent() - - if event == "redstone" then - -- redstone state change - regulator.handle_redstone() - elseif event == "modem_message" then - -- received signal router packet - packet = { - side = param1, - sender = param2, - reply = param3, - message = param4, - distance = param5 - } - - server.handle_message(packet, reactors) - elseif event == "monitor_touch" then - if param1 == "monitor_5" then - local tap_x = param2 - local tap_y = param3 - - for key, rctr in pairs(reactors) do - if tap_x >= rctr.render.stat_x and tap_x <= (rctr.render.stat_x + 15) then - local old_val = rctr.waste_production - -- width in range - if tap_y == (rctr.render.stat_y + 12) then - rctr.waste_production = "plutonium" - elseif tap_y == (rctr.render.stat_y + 14) then - rctr.waste_production = "polonium" - elseif tap_y == (rctr.render.stat_y + 16) then - rctr.waste_production = "antimatter" - end - - -- notify reactor of changes - if old_val ~= rctr.waste_production then - server.send(rctr.id, rctr.waste_production) - end - end - end - end - elseif event == "timer" then - -- update the clock about every second - monitor_2.setCursorPos(1, 1) - monitor_2.write(os.date("%Y/%m/%d %H:%M:%S")) - clock_update_timer = os.startTimer(1) - - -- send keep-alive - server.broadcast(1) - end - - -- update reactor display - for key, rctr in pairs(reactors) do - render.draw_reactor_system(rctr) - render.draw_reactor_status(rctr) - end - - -- update system status monitor - render.update_system_monitor(monitor_2, regulator.is_scrammed(), reactors) -end diff --git a/coordinator/old-controller/defs.lua b/coordinator/old-controller/defs.lua deleted file mode 100644 index 13f6803..0000000 --- a/coordinator/old-controller/defs.lua +++ /dev/null @@ -1,23 +0,0 @@ --- configuration definitions - -CTRL_VERSION = "0.7" - --- monitors -MONITOR_0 = "monitor_6" -MONITOR_1 = "monitor_5" -MONITOR_2 = "monitor_7" -MONITOR_3 = "monitor_8" - --- modem server -LISTEN_PORT = 1000 - --- regulator (should match the number of reactors present) -BUNDLE_DEF = { colors.red, colors.orange, colors.yellow, colors.lime } - --- stats calculation -REACTOR_MB_T = 39 -TURBINE_MRF_T = 3.114 -PLUTONIUM_PER_WASTE = 0.1 -POLONIUM_PER_WASTE = 0.1 -SPENT_PER_BYPRODUCT = 1 -ANTIMATTER_PER_POLONIUM = 0.001 diff --git a/coordinator/old-controller/log.lua b/coordinator/old-controller/log.lua deleted file mode 100644 index c4e1cbb..0000000 --- a/coordinator/old-controller/log.lua +++ /dev/null @@ -1,52 +0,0 @@ -os.loadAPI("defs.lua") - -local out, out_w, out_h -local output_full = false - --- initialize the logger to the given monitor --- monitor: monitor to write to (in addition to calling print()) -function init(monitor) - out = monitor - out_w, out_h = out.getSize() - - out.clear() - out.setTextColor(colors.white) - out.setBackgroundColor(colors.black) - - out.setCursorPos(1, 1) - out.write("version " .. defs.CTRL_VERSION) - out.setCursorPos(1, 2) - out.write("system startup at " .. os.date("%Y/%m/%d %H:%M:%S")) - - print("server v" .. defs.CTRL_VERSION .. " started at " .. os.date("%Y/%m/%d %H:%M:%S")) -end - --- write a log message to the log screen and console --- msg: message to write --- color: (optional) color to print in, defaults to white -function write(msg, color) - color = color or colors.white - local _x, _y = out.getCursorPos() - - if output_full then - out.scroll(1) - out.setCursorPos(1, _y) - else - if _y == out_h then - output_full = true - out.scroll(1) - out.setCursorPos(1, _y) - else - out.setCursorPos(1, _y + 1) - end - end - - -- output to screen - out.setTextColor(colors.lightGray) - out.write(os.date("[%H:%M:%S] ")) - out.setTextColor(color) - out.write(msg) - - -- output to console - print(os.date("[%H:%M:%S] ") .. msg) -end diff --git a/coordinator/old-controller/reactor.lua b/coordinator/old-controller/reactor.lua deleted file mode 100644 index 137b46c..0000000 --- a/coordinator/old-controller/reactor.lua +++ /dev/null @@ -1,28 +0,0 @@ --- create a new reactor 'object' --- reactor_id: the ID for this reactor --- main_view: the parent window/monitor for the main display (components) --- status_view: the parent window/monitor for the status display --- main_x: where to create the main window, x coordinate --- main_y: where to create the main window, y coordinate --- status_x: where to create the status window, x coordinate --- status_y: where to create the status window, y coordinate -function create(reactor_id, main_view, status_view, main_x, main_y, status_x, status_y) - return { - id = reactor_id, - render = { - win_main = window.create(main_view, main_x, main_y, 20, 60, true), - win_stat = window.create(status_view, status_x, status_y, 20, 20, true), - stat_x = status_x, - stat_y = status_y - }, - control_state = false, - waste_production = "antimatter", -- "plutonium", "polonium", "antimatter" - state = { - run = false, - no_fuel = false, - full_waste = false, - high_temp = false, - damage_crit = false - } - } -end diff --git a/coordinator/old-controller/regulator.lua b/coordinator/old-controller/regulator.lua deleted file mode 100644 index e8acf55..0000000 --- a/coordinator/old-controller/regulator.lua +++ /dev/null @@ -1,128 +0,0 @@ -os.loadAPI("defs.lua") -os.loadAPI("log.lua") -os.loadAPI("server.lua") - -local reactors -local scrammed -local auto_scram - --- initialize the system regulator which provides safety measures, SCRAM functionality, and handles redstone --- _reactors: reactor table -function init(_reactors) - reactors = _reactors - scrammed = false - auto_scram = false - - -- scram all reactors - server.broadcast(false, reactors) - - -- check initial states - regulator.handle_redstone() -end - --- check if the system is scrammed -function is_scrammed() - return scrammed -end - --- handle redstone state changes -function handle_redstone() - -- check scram button - if not rs.getInput("right") then - if not scrammed then - log.write("user SCRAM", colors.red) - scram() - end - - -- toggling scram will release auto scram state - auto_scram = false - else - scrammed = false - end - - -- check individual control buttons - local input = rs.getBundledInput("left") - for key, rctr in pairs(reactors) do - if colors.test(input, defs.BUNDLE_DEF[key]) ~= rctr.control_state then - -- state changed - rctr.control_state = colors.test(input, defs.BUNDLE_DEF[key]) - if not scrammed then - local safe = true - - if rctr.control_state then - safe = check_enable_safety(reactors[key]) - if safe then - log.write("reactor " .. reactors[key].id .. " enabled", colors.lime) - end - else - log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan) - end - - -- start/stop reactor - if safe then - server.send(rctr.id, rctr.control_state) - end - elseif colors.test(input, defs.BUNDLE_DEF[key]) then - log.write("scrammed: state locked off", colors.yellow) - end - end - end -end - --- make sure enabling the provided reactor is safe --- reactor: reactor to check -function check_enable_safety(reactor) - if reactor.state.no_fuel or reactor.state.full_waste or reactor.state.high_temp or reactor.state.damage_crit then - log.write("RCT-" .. reactor.id .. ": unsafe enable denied", colors.yellow) - return false - else - return true - end -end - --- make sure no running reactors are in a bad state -function enforce_safeties() - for key, reactor in pairs(reactors) do - local overridden = false - local state = reactor.state - - -- check for problems - if state.damage_crit and state.run then - reactor.control_state = false - log.write("RCT-" .. reactor.id .. ": shut down (damage)", colors.yellow) - - -- scram all, so ignore setting overridden - log.write("auto SCRAM all reactors", colors.red) - auto_scram = true - scram() - elseif state.high_temp and state.run then - reactor.control_state = false - overridden = true - log.write("RCT-" .. reactor.id .. ": shut down (temp)", colors.yellow) - elseif state.full_waste and state.run then - reactor.control_state = false - overridden = true - log.write("RCT-" .. reactor.id .. ": shut down (waste)", colors.yellow) - elseif state.no_fuel and state.run then - reactor.control_state = false - overridden = true - log.write("RCT-" .. reactor.id .. ": shut down (fuel)", colors.yellow) - end - - if overridden then - server.send(reactor.id, false) - end - end -end - --- shut down all reactors and prevent enabling them until the scram button is toggled/released -function scram() - scrammed = true - server.broadcast(false, reactors) - - for key, rctr in pairs(reactors) do - if rctr.control_state then - log.write("reactor " .. reactors[key].id .. " disabled", colors.cyan) - end - end -end diff --git a/coordinator/old-controller/render.lua b/coordinator/old-controller/render.lua deleted file mode 100644 index e10614d..0000000 --- a/coordinator/old-controller/render.lua +++ /dev/null @@ -1,370 +0,0 @@ -os.loadAPI("defs.lua") - --- draw pipes between machines --- win: window to render in --- x: starting x coord --- y: starting y coord --- spacing: spacing between the pipes --- color_out: output pipe contents color --- color_ret: return pipe contents color --- tick: tick the pipes for an animation -function draw_pipe(win, x, y, spacing, color_out, color_ret, tick) - local _color - local _off - tick = tick or 0 - - for i = 0, 4, 1 - do - _off = (i + tick) % 2 == 0 or (tick == 1 and i == 0) or (tick == 3 and i == 4) - - if _off then - _color = colors.lightGray - else - _color = color_out - end - - win.setBackgroundColor(_color) - win.setCursorPos(x, y + i) - win.write(" ") - - if not _off then - _color = color_ret - end - - win.setBackgroundColor(_color) - win.setCursorPos(x + spacing, y + i) - win.write(" ") - end -end - --- draw a reactor view consisting of the reactor, boiler, turbine, and pipes --- data: reactor table -function draw_reactor_system(data) - local win = data.render.win_main - local win_w, win_h = win.getSize() - - win.setBackgroundColor(colors.black) - win.setTextColor(colors.black) - win.clear() - win.setCursorPos(1, 1) - - -- draw header -- - - local header = "REACTOR " .. data.id - local header_pad_x = (win_w - string.len(header) - 2) / 2 - local header_color - if data.state.no_fuel then - if data.state.run then - header_color = colors.purple - else - header_color = colors.brown - end - elseif data.state.full_waste then - header_color = colors.yellow - elseif data.state.high_temp then - header_color = colors.orange - elseif data.state.damage_crit then - header_color = colors.red - elseif data.state.run then - header_color = colors.green - else - header_color = colors.lightGray - end - - local running = data.state.run and not data.state.no_fuel - - win.write(" ") - win.setBackgroundColor(header_color) - win.write(string.rep(" ", win_w - 2)) - win.setBackgroundColor(colors.black) - win.write(" ") - win.setCursorPos(1, 2) - win.write(" ") - win.setBackgroundColor(header_color) - win.write(string.rep(" ", header_pad_x) .. header .. string.rep(" ", header_pad_x)) - win.setBackgroundColor(colors.black) - win.write(" ") - - -- create strings for use in blit - local line_text = string.rep(" ", 14) - local line_text_color = string.rep("0", 14) - - -- draw components -- - - -- draw reactor - local rod = "88" - if data.state.high_temp then - rod = "11" - elseif running then - rod = "99" - end - - win.setCursorPos(4, 4) - win.setBackgroundColor(colors.gray) - win.write(line_text) - win.setCursorPos(4, 5) - win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77") - win.setCursorPos(4, 6) - win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777") - win.setCursorPos(4, 7) - win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77") - win.setCursorPos(4, 8) - win.blit(line_text, line_text_color, "7777" .. rod .. "77" .. rod .. "7777") - win.setCursorPos(4, 9) - win.blit(line_text, line_text_color, "77" .. rod .. "77" .. rod .. "77" .. rod .. "77") - win.setCursorPos(4, 10) - win.write(line_text) - - -- boiler - local steam = "ffffffffff" - if running then - steam = "0000000000" - end - - win.setCursorPos(4, 16) - win.setBackgroundColor(colors.gray) - win.write(line_text) - win.setCursorPos(4, 17) - win.blit(line_text, line_text_color, "77" .. steam .. "77") - win.setCursorPos(4, 18) - win.blit(line_text, line_text_color, "77" .. steam .. "77") - win.setCursorPos(4, 19) - win.blit(line_text, line_text_color, "77888888888877") - win.setCursorPos(4, 20) - win.blit(line_text, line_text_color, "77bbbbbbbbbb77") - win.setCursorPos(4, 21) - win.blit(line_text, line_text_color, "77bbbbbbbbbb77") - win.setCursorPos(4, 22) - win.blit(line_text, line_text_color, "77bbbbbbbbbb77") - win.setCursorPos(4, 23) - win.setBackgroundColor(colors.gray) - win.write(line_text) - - -- turbine - win.setCursorPos(4, 29) - win.setBackgroundColor(colors.gray) - win.write(line_text) - win.setCursorPos(4, 30) - if running then - win.blit(line_text, line_text_color, "77000000000077") - else - win.blit(line_text, line_text_color, "77ffffffffff77") - end - win.setCursorPos(4, 31) - if running then - win.blit(line_text, line_text_color, "77008000080077") - else - win.blit(line_text, line_text_color, "77ff8ffff8ff77") - end - win.setCursorPos(4, 32) - if running then - win.blit(line_text, line_text_color, "77000800800077") - else - win.blit(line_text, line_text_color, "77fff8ff8fff77") - end - win.setCursorPos(4, 33) - if running then - win.blit(line_text, line_text_color, "77000088000077") - else - win.blit(line_text, line_text_color, "77ffff88ffff77") - end - win.setCursorPos(4, 34) - if running then - win.blit(line_text, line_text_color, "77000800800077") - else - win.blit(line_text, line_text_color, "77fff8ff8fff77") - end - win.setCursorPos(4, 35) - if running then - win.blit(line_text, line_text_color, "77008000080077") - else - win.blit(line_text, line_text_color, "77ff8ffff8ff77") - end - win.setCursorPos(4, 36) - if running then - win.blit(line_text, line_text_color, "77000000000077") - else - win.blit(line_text, line_text_color, "77ffffffffff77") - end - win.setCursorPos(4, 37) - win.setBackgroundColor(colors.gray) - win.write(line_text) - - -- draw reactor coolant pipes - draw_pipe(win, 7, 11, 6, colors.orange, colors.lightBlue) - - -- draw turbine pipes - draw_pipe(win, 7, 24, 6, colors.white, colors.blue) -end - --- draw the reactor statuses on the status screen --- data: reactor table -function draw_reactor_status(data) - local win = data.render.win_stat - - win.setBackgroundColor(colors.black) - win.setTextColor(colors.white) - win.clear() - - -- show control state - win.setCursorPos(1, 1) - if data.control_state then - win.blit(" + ENABLED", "00000000000", "dddffffffff") - else - win.blit(" - DISABLED", "000000000000", "eeefffffffff") - end - - -- show run state - win.setCursorPos(1, 2) - if data.state.run then - win.blit(" + RUNNING", "00000000000", "dddffffffff") - else - win.blit(" - STOPPED", "00000000000", "888ffffffff") - end - - -- show fuel state - win.setCursorPos(1, 4) - if data.state.no_fuel then - win.blit(" - NO FUEL", "00000000000", "eeeffffffff") - else - win.blit(" + FUEL OK", "00000000000", "999ffffffff") - end - - -- show waste state - win.setCursorPos(1, 5) - if data.state.full_waste then - win.blit(" - WASTE FULL", "00000000000000", "eeefffffffffff") - else - win.blit(" + WASTE OK", "000000000000", "999fffffffff") - end - - -- show high temp state - win.setCursorPos(1, 6) - if data.state.high_temp then - win.blit(" - HIGH TEMP", "0000000000000", "eeeffffffffff") - else - win.blit(" + TEMP OK", "00000000000", "999ffffffff") - end - - -- show damage state - win.setCursorPos(1, 7) - if data.state.damage_crit then - win.blit(" - CRITICAL DAMAGE", "0000000000000000000", "eeeffffffffffffffff") - else - win.blit(" + CASING INTACT", "00000000000000000", "999ffffffffffffff") - end - - -- waste processing options -- - win.setTextColor(colors.black) - win.setBackgroundColor(colors.white) - - win.setCursorPos(1, 10) - win.write(" ") - win.setCursorPos(1, 11) - win.write(" WASTE OUTPUT ") - - win.setCursorPos(1, 13) - win.setBackgroundColor(colors.cyan) - if data.waste_production == "plutonium" then - win.write(" > plutonium ") - else - win.write(" plutonium ") - end - - win.setCursorPos(1, 15) - win.setBackgroundColor(colors.green) - if data.waste_production == "polonium" then - win.write(" > polonium ") - else - win.write(" polonium ") - end - - win.setCursorPos(1, 17) - win.setBackgroundColor(colors.purple) - if data.waste_production == "antimatter" then - win.write(" > antimatter ") - else - win.write(" antimatter ") - end -end - --- update the system monitor screen --- mon: monitor to update --- is_scrammed: -function update_system_monitor(mon, is_scrammed, reactors) - if is_scrammed then - -- display scram banner - mon.setTextColor(colors.white) - mon.setBackgroundColor(colors.black) - mon.setCursorPos(1, 2) - mon.clearLine() - mon.setBackgroundColor(colors.red) - mon.setCursorPos(1, 3) - mon.write(" ") - mon.setCursorPos(1, 4) - mon.write(" SCRAM ") - mon.setCursorPos(1, 5) - mon.write(" ") - mon.setBackgroundColor(colors.black) - mon.setCursorPos(1, 6) - mon.clearLine() - mon.setTextColor(colors.white) - else - -- clear where scram banner would be - mon.setCursorPos(1, 3) - mon.clearLine() - mon.setCursorPos(1, 4) - mon.clearLine() - mon.setCursorPos(1, 5) - mon.clearLine() - - -- show production statistics-- - - local mrf_t = 0 - local mb_t = 0 - local plutonium = 0 - local polonium = 0 - local spent_waste = 0 - local antimatter = 0 - - -- determine production values - for key, rctr in pairs(reactors) do - if rctr.state.run then - mrf_t = mrf_t + defs.TURBINE_MRF_T - mb_t = mb_t + defs.REACTOR_MB_T - - if rctr.waste_production == "plutonium" then - plutonium = plutonium + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE) - spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.PLUTONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT) - elseif rctr.waste_production == "polonium" then - polonium = polonium + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE) - spent_waste = spent_waste + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.SPENT_PER_BYPRODUCT) - elseif rctr.waste_production == "antimatter" then - antimatter = antimatter + (defs.REACTOR_MB_T * defs.POLONIUM_PER_WASTE * defs.ANTIMATTER_PER_POLONIUM) - end - end - end - - -- draw stats - mon.setTextColor(colors.lightGray) - mon.setCursorPos(1, 2) - mon.clearLine() - mon.write("ENERGY: " .. string.format("%0.2f", mrf_t) .. " MRF/t") - -- mon.setCursorPos(1, 3) - -- mon.clearLine() - -- mon.write("FUEL: " .. mb_t .. " mB/t") - mon.setCursorPos(1, 3) - mon.clearLine() - mon.write("Pu: " .. string.format("%0.2f", plutonium) .. " mB/t") - mon.setCursorPos(1, 4) - mon.clearLine() - mon.write("Po: " .. string.format("%0.2f", polonium) .. " mB/t") - mon.setCursorPos(1, 5) - mon.clearLine() - mon.write("SPENT: " .. string.format("%0.2f", spent_waste) .. " mB/t") - mon.setCursorPos(1, 6) - mon.clearLine() - mon.write("ANTI-M: " .. string.format("%0.2f", antimatter * 1000) .. " uB/t") - mon.setTextColor(colors.white) - end -end diff --git a/coordinator/old-controller/server.lua b/coordinator/old-controller/server.lua deleted file mode 100644 index 61ad386..0000000 --- a/coordinator/old-controller/server.lua +++ /dev/null @@ -1,109 +0,0 @@ -os.loadAPI("defs.lua") -os.loadAPI("log.lua") -os.loadAPI("regulator.lua") - -local modem -local reactors - --- initalize the listener running on the wireless modem --- _reactors: reactor table -function init(_reactors) - modem = peripheral.wrap("top") - reactors = _reactors - - -- open listening port - if not modem.isOpen(defs.LISTEN_PORT) then - modem.open(defs.LISTEN_PORT) - end - - -- send out a greeting to solicit responses for clients that are already running - broadcast(0, reactors) -end - --- handle an incoming message from the modem --- packet: table containing message fields -function handle_message(packet) - if type(packet.message) == "number" then - -- this is a greeting - log.write("reactor " .. packet.message .. " connected", colors.green) - - -- send current control command - for key, rctr in pairs(reactors) do - if rctr.id == packet.message then - send(rctr.id, rctr.control_state) - break - end - end - else - -- got reactor status - local eval_safety = false - - for key, value in pairs(reactors) do - if value.id == packet.message.id then - local tag = "RCT-" .. value.id .. ": " - - if value.state.run ~= packet.message.run then - value.state.run = packet.message.run - if value.state.run then - eval_safety = true - log.write(tag .. "running", colors.green) - end - end - - if value.state.no_fuel ~= packet.message.no_fuel then - value.state.no_fuel = packet.message.no_fuel - if value.state.no_fuel then - eval_safety = true - log.write(tag .. "insufficient fuel", colors.gray) - end - end - - if value.state.full_waste ~= packet.message.full_waste then - value.state.full_waste = packet.message.full_waste - if value.state.full_waste then - eval_safety = true - log.write(tag .. "waste tank full", colors.brown) - end - end - - if value.state.high_temp ~= packet.message.high_temp then - value.state.high_temp = packet.message.high_temp - if value.state.high_temp then - eval_safety = true - log.write(tag .. "high temperature", colors.orange) - end - end - - if value.state.damage_crit ~= packet.message.damage_crit then - value.state.damage_crit = packet.message.damage_crit - if value.state.damage_crit then - eval_safety = true - log.write(tag .. "critical damage", colors.red) - end - end - - break - end - end - - -- check to ensure safe operation - if eval_safety then - regulator.enforce_safeties() - end - end -end - --- send a message to a given reactor --- dest: reactor ID --- message: true or false for enable control or another value for other functionality, like 0 for greeting -function send(dest, message) - modem.transmit(dest + defs.LISTEN_PORT, defs.LISTEN_PORT, message) -end - --- broadcast a message to all reactors --- message: true or false for enable control or another value for other functionality, like 0 for greeting -function broadcast(message) - for key, value in pairs(reactors) do - modem.transmit(value.id + defs.LISTEN_PORT, defs.LISTEN_PORT, message) - end -end From 22a615952032ed2ebca8ad2a26df0281f5ce4542 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 17:17:55 -0400 Subject: [PATCH 109/168] updated globals list, fixed packet references that were linking to old controller mistakenly --- .vscode/settings.json | 3 ++- rtu/threads.lua | 2 +- supervisor/session/rtu.lua | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 75bb696..77e08ef 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "rs", "bit", "parallel", - "colors" + "colors", + "textutils" ] } \ No newline at end of file diff --git a/rtu/threads.lua b/rtu/threads.lua index 5799efb..213781e 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -234,7 +234,7 @@ threads.thread__unit_comms = function (smem, unit) elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet unit.modbus_busy = true - local return_code, reply = unit.modbus_io.handle_packet(packet) + local return_code, reply = unit.modbus_io.handle_packet(msg.message) rtu_comms.send_modbus(reply) unit.modbus_busy = false end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index fb60904..a360be9 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -99,8 +99,8 @@ rtu.new_session = function (id, in_queue, out_queue) self.connected = false elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- RTU unit advertisement - for i = 1, packet.length do - local unit = packet.data[i] + for i = 1, pkt.length do + local unit = pkt.data[i] end else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) From faac421b63b6be16578e96df60f4d56998715c5b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 21:49:14 -0400 Subject: [PATCH 110/168] #47 reactor plc docs and bugfixes --- reactor-plc/plc.lua | 148 ++++++++++++++----------- reactor-plc/startup.lua | 12 ++- reactor-plc/threads.lua | 234 +++++++++++++++++++++------------------- 3 files changed, 219 insertions(+), 175 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5b84c7f..04d63fd 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: redefined-local local comms = require("scada-common.comms") local log = require("scada-common.log") local ppm = require("scada-common.ppm") @@ -18,9 +19,11 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts --- Reactor Protection System --- identifies dangerous states and SCRAMs reactor if warranted --- autonomous from main SCADA supervisor/coordinator control +--- RPS: Reactor Protection System +--- +--- identifies dangerous states and SCRAMs reactor if warranted +--- +--- autonomous from main SCADA supervisor/coordinator control plc.rps_init = function (reactor) local state_keys = { dmg_crit = 1, @@ -42,6 +45,9 @@ plc.rps_init = function (reactor) trip_cause = "" } + ---@class rps + local public = {} + -- PRIVATE FUNCTIONS -- -- set reactor access fault flag @@ -136,22 +142,28 @@ plc.rps_init = function (reactor) -- PUBLIC FUNCTIONS -- -- re-link a reactor after a peripheral re-connect - local reconnect_reactor = function (reactor) + public.reconnect_reactor = function (reactor) self.reactor = reactor end - -- report a PLC comms timeout - local trip_timeout = function () + -- trip for lost peripheral + public.trip_fault = function () + _set_fault() + end + + -- trip for a PLC comms timeout + public.trip_timeout = function () self.state[state_keys.timed_out] = true end -- manually SCRAM the reactor - local trip_manual = function () + public.trip_manual = function () self.state[state_keys.manual] = true end -- SCRAM the reactor now - local scram = function () + ---@return boolean success + public.scram = function () log.info("RPS: reactor SCRAM") self.reactor.scram() @@ -165,7 +177,8 @@ plc.rps_init = function (reactor) end -- start the reactor - local activate = function () + ---@return boolean success + public.activate = function () if not self.tripped then log.info("RPS: reactor start") @@ -182,7 +195,8 @@ plc.rps_init = function (reactor) end -- check all safety conditions - local check = function () + ---@return boolean tripped, rps_status_t trip_status, boolean first_trip + public.check = function () local status = rps_status_t.ok local was_tripped = self.tripped local first_trip = false @@ -237,38 +251,37 @@ plc.rps_init = function (reactor) self.tripped = true self.trip_cause = status - scram() + public.scram() end return self.tripped, status, first_trip end - -- get the RPS status - local status = function () return self.state end - local is_tripped = function () return self.tripped end - local is_active = function () return self.reactor_enabled end + public.status = function () return self.state end + public.is_tripped = function () return self.tripped end + public.is_active = function () return self.reactor_enabled end -- reset the RPS - local reset = function () + public.reset = function () self.tripped = false self.trip_cause = rps_status_t.ok + + for i = 1, #self.state do + self.state[i] = false + end end - return { - reconnect_reactor = reconnect_reactor, - trip_timeout = trip_timeout, - trip_manual = trip_manual, - scram = scram, - activate = activate, - check = check, - status = status, - is_tripped = is_tripped, - is_active = is_active, - reset = reset - } + return public end --- reactor PLC communications +-- Reactor PLC Communications +---@param id integer +---@param modem table +---@param local_port integer +---@param server_port integer +---@param reactor table +---@param rps rps +---@param conn_watchdog watchdog plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_watchdog) local self = { id = id, @@ -286,6 +299,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat max_burn_rate = nil } + ---@class plc_comms + local public = {} + -- open modem if not self.modem.isOpen(self.l_port) then self.modem.open(self.l_port) @@ -293,6 +309,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- PRIVATE FUNCTIONS -- + -- send an RPLC packet + ---@param msg_type RPLC_TYPES + ---@param msg string local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() @@ -304,6 +323,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat self.seq_num = self.seq_num + 1 end + -- send a SCADA management packet + ---@param msg_type SCADA_MGMT_TYPES + ---@param msg string local _send_mgmt = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() @@ -316,6 +338,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- variable reactor status information, excluding heating rate + ---@return table data_table, boolean faulted local _reactor_status = function () local coolant = nil local hcoolant = nil @@ -373,6 +396,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat return data_table, self.reactor.__p_is_faulted() end + -- update the status cache if changed + ---@return boolean changed local _update_status_cache = function () local status, faulted = _reactor_status() local changed = false @@ -398,11 +423,14 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- keep alive ack + ---@param srv_time integer local _send_keep_alive_ack = function (srv_time) _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack + ---@param msg_type RPLC_TYPES + ---@param succeeded boolean local _send_ack = function (msg_type, succeeded) _send(msg_type, { succeeded }) end @@ -434,7 +462,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem - local reconnect_modem = function (modem) + ---@param modem table + public.reconnect_modem = function (modem) self.modem = modem -- open modem @@ -444,32 +473,34 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- reconnect a newly connected reactor - local reconnect_reactor = function (reactor) + ---@param reactor table + public.reconnect_reactor = function (reactor) self.reactor = reactor self.status_cache = nil end -- unlink from the server - local unlink = function () + public.unlink = function () self.linked = false self.r_seq_num = nil self.status_cache = nil end -- close the connection to the server - local close = function () + public.close = function () self.conn_watchdog.cancel() - unlink() + public.unlink() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) end -- attempt to establish link with supervisor - local send_link_req = function () + public.send_link_req = function () _send(RPLC_TYPES.LINK_REQ, { self.id }) end -- send live status information - local send_status = function (degraded) + ---@param degraded boolean + public.send_status = function (degraded) if self.linked then local mek_data = nil @@ -495,14 +526,15 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- send reactor protection system status - local send_rps_status = function () + public.send_rps_status = function () if self.linked then _send(RPLC_TYPES.RPS_STATUS, rps.status()) end end -- send reactor protection system alarm - local send_rps_alarm = function (cause) + ---@param cause rps_status_t + public.send_rps_alarm = function (cause) if self.linked then local rps_alarm = { cause, @@ -514,7 +546,13 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- parse an RPLC packet - local parse_packet = function(side, sender, reply_to, message, distance) + ---@param side string + ---@param sender integer + ---@param reply_to integer + ---@param message any + ---@param distance integer + ---@return rplc_frame|mgmt_frame|nil packet + public.parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil local s_pkt = comms.scada_packet() @@ -543,7 +581,10 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- handle an RPLC packet - local handle_packet = function (packet, plc_state, setpoints) + ---@param packet rplc_frame|mgmt_frame + ---@param plc_state plc_state + ---@param setpoints setpoints + public.handle_packet = function (packet, plc_state, setpoints) if packet ~= nil then -- check sequence number if self.r_seq_num == nil then @@ -573,7 +614,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat if link_ack == RPLC_LINKING.ALLOW then self.status_cache = nil _send_struct() - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then println_ts("received unsolicited link denial, unlinking") @@ -593,7 +634,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat elseif packet.type == RPLC_TYPES.STATUS then -- request of full status, clear cache first self.status_cache = nil - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("sent out status cache again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure @@ -659,7 +700,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat self.status_cache = nil _send_struct() - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then @@ -700,7 +741,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat elseif packet.type == SCADA_MGMT_TYPES.CLOSE then -- handle session close self.conn_watchdog.cancel() - unlink() + public.unlink() println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") else @@ -713,23 +754,10 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end end - local is_scrammed = function () return self.scrammed end - local is_linked = function () return self.linked end + public.is_scrammed = function () return self.scrammed end + public.is_linked = function () return self.linked end - return { - reconnect_modem = reconnect_modem, - reconnect_reactor = reconnect_reactor, - unlink = unlink, - close = close, - send_link_req = send_link_req, - send_status = send_status, - send_rps_status = send_rps_status, - send_rps_alarm = send_rps_alarm, - parse_packet = parse_packet, - handle_packet = handle_packet, - is_scrammed = is_scrammed, - is_linked = is_linked - } + return public end return plc diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1082afd..9d79a9c 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -29,11 +29,13 @@ println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") ppm.mount_all() -- shared memory across threads +---@class plc_shared_memory local __shared_memory = { -- networked setting - networked = config.NETWORKED, + networked = config.NETWORKED, ---@type boolean -- PLC system state flags + ---@class plc_state plc_state = { init_ok = true, shutdown = false, @@ -42,6 +44,8 @@ local __shared_memory = { no_modem = false }, + -- control setpoints + ---@class setpoints setpoints = { burn_rate_en = false, burn_rate = 0.0 @@ -55,9 +59,9 @@ local __shared_memory = { -- system objects plc_sys = { - rps = nil, - plc_comms = nil, - conn_watchdog = nil + rps = nil, ---@type rps + plc_comms = nil, ---@type plc_comms + conn_watchdog = nil ---@type watchdog }, -- message queues diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 7231f2a..9531792 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -10,8 +10,6 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local psleep = util.psleep - local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) local RPS_SLEEP = 250 -- (250ms, 5 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) @@ -30,6 +28,8 @@ local MQ__COMM_CMD = { } -- main thread +---@param smem plc_shared_memory +---@param init function threads.thread__main = function (smem, init) -- execute thread local exec = function () @@ -47,7 +47,7 @@ threads.thread__main = function (smem, init) local plc_dev = smem.plc_dev local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog ---@type watchdog + local conn_watchdog = smem.plc_sys.conn_watchdog -- event loop while true do @@ -187,6 +187,7 @@ threads.thread__main = function (smem, init) end -- RPS operation thread +---@param smem plc_shared_memory threads.thread__rps = function (smem) -- execute thread local exec = function () @@ -224,6 +225,7 @@ threads.thread__rps = function (smem) -- if we tried to SCRAM but failed, keep trying -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) +---@diagnostic disable-next-line: need-check-nil if not plc_state.no_reactor and rps.is_tripped() and reactor.getStatus() then rps.scram() end @@ -249,26 +251,28 @@ threads.thread__rps = function (smem) while rps_queue.ready() and not plc_state.shutdown do local msg = rps_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - if plc_state.init_ok then - if msg.message == MQ__RPS_CMD.SCRAM then - -- SCRAM - rps.scram() - elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then - -- lost peripheral(s) - rps.trip_degraded() - elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then - -- watchdog tripped - rps.trip_timeout() - println_ts("server timeout") - log.warning("server timeout") + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if plc_state.init_ok then + if msg.message == MQ__RPS_CMD.SCRAM then + -- SCRAM + rps.scram() + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then + -- lost peripheral(s) + rps.trip_fault() + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + rps.trip_timeout() + println_ts("server timeout") + log.warning("server timeout") + end end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet end - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet end -- quick yield @@ -301,6 +305,7 @@ threads.thread__rps = function (smem) end -- communications sender thread +---@param smem plc_shared_memory threads.thread__comms_tx = function (smem) -- execute thread local exec = function () @@ -320,17 +325,19 @@ threads.thread__comms_tx = function (smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - if msg.message == MQ__COMM_CMD.SEND_STATUS then - -- send PLC/RPS status - plc_comms.send_status(plc_state.degraded) - plc_comms.send_rps_status() + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__COMM_CMD.SEND_STATUS then + -- send PLC/RPS status + plc_comms.send_status(plc_state.degraded) + plc_comms.send_rps_status() + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet end - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet end -- quick yield @@ -352,22 +359,20 @@ threads.thread__comms_tx = function (smem) end -- communications handler thread +---@param smem plc_shared_memory threads.thread__comms_rx = function (smem) -- execute thread local exec = function () log.debug("comms rx thread start") -- load in from shared memory - local plc_state = smem.plc_state - local setpoints = smem.setpoints - local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps - local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog + local plc_state = smem.plc_state + local setpoints = smem.setpoints + local plc_comms = smem.plc_sys.plc_comms - local comms_queue = smem.q.mq_comms_rx + local comms_queue = smem.q.mq_comms_rx - local last_update = util.time() + local last_update = util.time() -- thread loop while true do @@ -375,16 +380,17 @@ threads.thread__comms_rx = function (smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet - -- handle the packet (setpoints passed to update burn rate setpoint) - -- (plc_state passed to check if degraded) - -- (conn_watchdog passed to allow feeding the watchdog) - plc_comms.handle_packet(msg.message, setpoints, plc_state, conn_watchdog) + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (setpoints passed to update burn rate setpoint) + -- (plc_state passed to check if degraded) + plc_comms.handle_packet(msg.message, setpoints, plc_state) + end end -- quick yield @@ -406,84 +412,90 @@ threads.thread__comms_rx = function (smem) end -- apply setpoints +---@param smem plc_shared_memory threads.thread__setpoint_control = function (smem) -- execute thread local exec = function () log.debug("setpoint control thread start") -- load in from shared memory - local plc_state = smem.plc_state - local setpoints = smem.setpoints - local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps + local plc_state = smem.plc_state + local setpoints = smem.setpoints + local plc_dev = smem.plc_dev + local rps = smem.plc_sys.rps - local last_update = util.time() - local running = false + local last_update = util.time() + local running = false - local last_sp_burn = 0 + local last_sp_burn = 0.0 + + -- do not use the actual elapsed time, it could spike + -- we do not want to have big jumps as that is what we are trying to avoid in the first place + local min_elapsed_s = SP_CTRL_SLEEP / 1000.0 -- thread loop while true do local reactor = plc_dev.reactor - -- check if we should start ramping - if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then - if rps.is_active() then - if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then - -- update without ramp if <= 5 mB/t change - log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") - reactor.setBurnRate(setpoints.burn_rate) - else - log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") - running = true - end - - last_sp_burn = setpoints.burn_rate - else - last_sp_burn = 0 - end - end - - -- only check I/O if active to save on processing time - if running then - -- do not use the actual elapsed time, it could spike - -- we do not want to have big jumps as that is what we are trying to avoid in the first place - local min_elapsed_s = SP_CTRL_SLEEP / 1000.0 - - -- clear so we can later evaluate if we should keep running - running = false - - -- adjust burn rate (setpoints.burn_rate) - if setpoints.burn_rate_en then + if plc_state.init_ok and not plc_state.no_reactor then + -- check if we should start ramping + if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then if rps.is_active() then - local current_burn_rate = reactor.getBurnRate() - - -- we yielded, check enable again - if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then - -- calculate new burn rate - local new_burn_rate = current_burn_rate - - if setpoints.burn_rate > current_burn_rate then - -- need to ramp up - local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate > setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate - end - else - -- need to ramp down - local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate < setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate - end - end - - -- set the burn rate - reactor.setBurnRate(new_burn_rate) - - running = running or (new_burn_rate ~= setpoints.burn_rate) + if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then + -- update without ramp if <= 5 mB/t change + log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") +---@diagnostic disable-next-line: need-check-nil + reactor.setBurnRate(setpoints.burn_rate) + else + log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") + running = true end + + last_sp_burn = setpoints.burn_rate else - last_sp_burn = 0 + last_sp_burn = 0.0 + end + end + + -- only check I/O if active to save on processing time + if running then + -- clear so we can later evaluate if we should keep running + running = false + + -- adjust burn rate (setpoints.burn_rate) + if setpoints.burn_rate_en then + if rps.is_active() then +---@diagnostic disable-next-line: need-check-nil + local current_burn_rate = reactor.getBurnRate() + + -- we yielded, check enable again + if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then + -- calculate new burn rate + local new_burn_rate = current_burn_rate + + if setpoints.burn_rate > current_burn_rate then + -- need to ramp up + local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate > setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + else + -- need to ramp down + local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate < setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + end + + -- set the burn rate +---@diagnostic disable-next-line: need-check-nil + reactor.setBurnRate(new_burn_rate) + + running = running or (new_burn_rate ~= setpoints.burn_rate) + end + else + last_sp_burn = 0.0 + end end end end From bced8bf56636d1fc5c3dba6d8ff99002fbbaad37 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 21:51:04 -0400 Subject: [PATCH 111/168] #47 packet frames --- scada-common/comms.lua | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 9eebb2d..6b4deef 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -207,7 +207,8 @@ comms.modbus_packet = function () -- get this packet public.get = function () - return { + ---@class modbus_frame + local frame = { scada_frame = self.frame, txn_id = self.txn_id, length = self.length, @@ -215,6 +216,8 @@ comms.modbus_packet = function () func_code = self.func_code, data = self.data } + + return frame end return public @@ -297,13 +300,16 @@ comms.rplc_packet = function () -- get this packet public.get = function () - return { + ---@class rplc_frame + local frame = { scada_frame = self.frame, id = self.id, type = self.type, length = self.length, data = self.data } + + return frame end return public @@ -378,12 +384,15 @@ comms.mgmt_packet = function () -- get this packet public.get = function () - return { + ---@class mgmt_frame + local frame = { scada_frame = self.frame, type = self.type, length = self.length, data = self.data } + + return frame end return public @@ -456,12 +465,15 @@ comms.coord_packet = function () -- get this packet public.get = function () - return { + ---@class coord_frame + local frame = { scada_frame = self.frame, type = self.type, length = self.length, data = self.data } + + return frame end return public @@ -534,12 +546,15 @@ comms.capi_packet = function () -- get this packet public.get = function () - return { + ---@class capi_frame + local frame = { scada_frame = self.frame, type = self.type, length = self.length, data = self.data } + + return frame end return public From 02541184bd3fe944fad01308d7aa343105064d94 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 11:31:02 -0400 Subject: [PATCH 112/168] bootloader --- .vscode/settings.json | 5 +-- coordinator/startup.lua | 4 +-- reactor-plc/startup.lua | 6 ++-- rtu/rtu.lua | 2 +- rtu/startup.lua | 22 ++++++------- rtu/threads.lua | 16 +++++----- startup.lua | 52 +++++++++++++++++++++++++++++++ supervisor/session/svsessions.lua | 6 ++-- supervisor/startup.lua | 12 +++---- supervisor/supervisor.lua | 2 +- 10 files changed, 90 insertions(+), 37 deletions(-) create mode 100644 startup.lua diff --git a/.vscode/settings.json b/.vscode/settings.json index 77e08ef..d2812b6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,6 +7,7 @@ "bit", "parallel", "colors", - "textutils" + "textutils", + "shell" ] -} \ No newline at end of file +} diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 5ee3d17..ab90f72 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -6,8 +6,8 @@ local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") -local config = require("config") -local coordinator = require("coordinator") +local config = require("coordinator.config") +local coordinator = require("coordinator.coordinator") local COORDINATOR_VERSION = "alpha-v0.1.2" diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 9d79a9c..0aaaeba 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -7,9 +7,9 @@ local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") local util = require("scada-common.util") -local config = require("config") -local plc = require("plc") -local threads = require("threads") +local config = require("reactor-plc.config") +local plc = require("reactor-plc.plc") +local threads = require("reactor-plc.threads") local R_PLC_VERSION = "alpha-v0.6.6" diff --git a/rtu/rtu.lua b/rtu/rtu.lua index ec20c93..f7b0432 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -4,7 +4,7 @@ local log = require("scada-common.log") local types = require("scada-common.types") local util = require("scada-common.util") -local modbus = require("modbus") +local modbus = require("rtu.modbus") local rtu = {} diff --git a/rtu/startup.lua b/rtu/startup.lua index 18119c0..ad31460 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -9,18 +9,18 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") -local config = require("config") -local modbus = require("modbus") -local rtu = require("rtu") -local threads = require("threads") +local config = require("rtu.config") +local modbus = require("rtu.modbus") +local rtu = require("rtu.rtu") +local threads = require("rtu.threads") -local redstone_rtu = require("dev.redstone_rtu") -local boiler_rtu = require("dev.boiler_rtu") -local boilerv_rtu = require("dev.boilerv_rtu") -local energymachine_rtu = require("dev.energymachine_rtu") -local imatrix_rtu = require("dev.imatrix_rtu") -local turbine_rtu = require("dev.turbine_rtu") -local turbinev_rtu = require("dev.turbinev_rtu") +local redstone_rtu = require("rtu.dev.redstone_rtu") +local boiler_rtu = require("rtu.dev.boiler_rtu") +local boilerv_rtu = require("rtu.dev.boilerv_rtu") +local energymachine_rtu = require("rtu.dev.energymachine_rtu") +local imatrix_rtu = require("rtu.dev.imatrix_rtu") +local turbine_rtu = require("rtu.dev.turbine_rtu") +local turbinev_rtu = require("rtu.dev.turbinev_rtu") local RTU_VERSION = "alpha-v0.6.2" diff --git a/rtu/threads.lua b/rtu/threads.lua index 213781e..447178b 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -5,15 +5,15 @@ local ppm = require("scada-common.ppm") local types = require("scada-common.types") local util = require("scada-common.util") -local redstone_rtu = require("dev.redstone_rtu") -local boiler_rtu = require("dev.boiler_rtu") -local boilerv_rtu = require("dev.boilerv_rtu") -local energymachine_rtu = require("dev.energymachine_rtu") -local imatrix_rtu = require("dev.imatrix_rtu") -local turbine_rtu = require("dev.turbine_rtu") -local turbinev_rtu = require("dev.turbinev_rtu") +local redstone_rtu = require("rtu.dev.redstone_rtu") +local boiler_rtu = require("rtu.dev.boiler_rtu") +local boilerv_rtu = require("rtu.dev.boilerv_rtu") +local energymachine_rtu = require("rtu.dev.energymachine_rtu") +local imatrix_rtu = require("rtu.dev.imatrix_rtu") +local turbine_rtu = require("rtu.dev.turbine_rtu") +local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local modbus = require("modbus") +local modbus = require("rtu.modbus") local threads = {} diff --git a/startup.lua b/startup.lua new file mode 100644 index 0000000..0a5cffb --- /dev/null +++ b/startup.lua @@ -0,0 +1,52 @@ +local util = require("scada-common.util") + +local BOOTLOADER_VERSION = "0.1" + +local println = util.println +local println_ts = util.println_ts + +println("SCADA BOOTLOADER V" .. BOOTLOADER_VERSION) + +local exit_code = false + +println_ts("BOOT> SCANNING FOR APPLICATIONS...") + +if fs.exists("reactor-plc/startup.lua") then + -- found reactor-plc application + println("BOOT> FOUND REACTOR PLC APPLICATION") + println("BOOT> EXEC STARTUP") + exit_code = shell.execute("reactor-plc/startup") +elseif fs.exists("rtu/startup.lua") then + -- found rtu application + println("BOOT> FOUND RTU APPLICATION") + println("BOOT> EXEC STARTUP") + exit_code = shell.execute("rtu/startup") +elseif fs.exists("supervisor/startup.lua") then + -- found supervisor application + println("BOOT> FOUND SUPERVISOR APPLICATION") + println("BOOT> EXEC STARTUP") + exit_code = shell.execute("supervisor/startup") +elseif fs.exists("coordinator/startup.lua") then + -- found coordinator application + println("BOOT> FOUND COORDINATOR APPLICATION") + println("BOOT> EXEC STARTUP") + exit_code = shell.execute("coordinator/startup") +elseif fs.exists("pocket/startup.lua") then + -- found pocket application + println("BOOT> FOUND POCKET APPLICATION") + println("BOOT> EXEC STARTUP") + exit_code = shell.execute("pocket/startup") +else + -- no known applications found + println("BOOT> NO SCADA STARTUP APPLICATION FOUND") + println("BOOT> EXIT") + return false +end + +if exit_code then + println_ts("BOOT> APPLICATION EXITED OK") +else + println_ts("BOOT> APPLICATION CRASHED") +end + +return exit_code diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index fa983a6..e596985 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -1,9 +1,9 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") -local coordinator = require("session.coordinator") -local plc = require("session.plc") -local rtu = require("session.rtu") +local coordinator = require("supervisor.session.coordinator") +local plc = require("supervisor.session.plc") +local rtu = require("supervisor.session.rtu") -- Supervisor Sessions Handler diff --git a/supervisor/startup.lua b/supervisor/startup.lua index bf0eabc..be9549e 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -6,13 +6,13 @@ local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") -local coordinator = require("session.coordinator") -local plc = require("session.plc") -local rtu = require("session.rtu") -local svsessions = require("session.svsessions") +local coordinator = require("supervisor.session.coordinator") +local plc = require("supervisor.session.plc") +local rtu = require("supervisor.session.rtu") +local svsessions = require("supervisor.session.svsessions") -local config = require("config") -local supervisor = require("supervisor") +local config = require("supervisor.config") +local supervisor = require("supervisor.supervisor") local SUPERVISOR_VERSION = "alpha-v0.3.4" diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 3d4fa17..f9df71e 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -2,7 +2,7 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local util = require("scada-common.util") -local svsessions = require("session.svsessions") +local svsessions = require("supervisor.session.svsessions") local supervisor = {} From 5ad14205f30d62ef481c811de0eb15d352705a66 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 12:01:18 -0400 Subject: [PATCH 113/168] #47 not going to do file level diagnostic disables --- reactor-plc/plc.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 04d63fd..b54354c 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,4 +1,3 @@ ----@diagnostic disable: redefined-local local comms = require("scada-common.comms") local log = require("scada-common.log") local ppm = require("scada-common.ppm") @@ -142,6 +141,7 @@ plc.rps_init = function (reactor) -- PUBLIC FUNCTIONS -- -- re-link a reactor after a peripheral re-connect +---@diagnostic disable-next-line: redefined-local public.reconnect_reactor = function (reactor) self.reactor = reactor end @@ -463,6 +463,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- reconnect a newly connected modem ---@param modem table +---@diagnostic disable-next-line: redefined-local public.reconnect_modem = function (modem) self.modem = modem @@ -474,6 +475,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- reconnect a newly connected reactor ---@param reactor table +---@diagnostic disable-next-line: redefined-local public.reconnect_reactor = function (reactor) self.reactor = reactor self.status_cache = nil From c6987f6f67b31f7b4f55ab4d080ee8ca6b1cdfc6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 12:03:15 -0400 Subject: [PATCH 114/168] #47 RTU luadoc, bugfixes --- rtu/modbus.lua | 56 +++++++++++---- rtu/rtu.lua | 183 ++++++++++++++++++++++++++++-------------------- rtu/startup.lua | 18 +++-- rtu/threads.lua | 54 +++++++------- 4 files changed, 192 insertions(+), 119 deletions(-) diff --git a/rtu/modbus.lua b/rtu/modbus.lua index b64910f..0e5e16c 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -7,14 +7,22 @@ local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE -- new modbus comms handler object +---@param rtu_dev rtu RTU device +---@param use_parallel_read boolean whether or not to use parallel calls when reading modbus.new = function (rtu_dev, use_parallel_read) local self = { rtu = rtu_dev, use_parallel = use_parallel_read } + ---@class modbus + local public = {} + local insert = table.insert + ---@param c_addr_start integer + ---@param count integer + ---@return boolean ok, table readings local _1_read_coils = function (c_addr_start, count) local tasks = {} local readings = {} @@ -58,6 +66,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, readings end + ---@param di_addr_start integer + ---@param count integer + ---@return boolean ok, table readings local _2_read_discrete_inputs = function (di_addr_start, count) local tasks = {} local readings = {} @@ -101,6 +112,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, readings end + ---@param hr_addr_start integer + ---@param count integer + ---@return boolean ok, table readings local _3_read_multiple_holding_registers = function (hr_addr_start, count) local tasks = {} local readings = {} @@ -144,6 +158,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, readings end + ---@param ir_addr_start integer + ---@param count integer + ---@return boolean ok, table readings local _4_read_input_registers = function (ir_addr_start, count) local tasks = {} local readings = {} @@ -187,6 +204,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, readings end + ---@param c_addr integer + ---@param value any + ---@return boolean ok, MODBUS_EXCODE|nil local _5_write_single_coil = function (c_addr, value) local response = nil local _, coils, _, _ = self.rtu.io_count() @@ -206,6 +226,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, response end + ---@param hr_addr integer + ---@param value any + ---@return boolean ok, MODBUS_EXCODE|nil local _6_write_single_holding_register = function (hr_addr, value) local response = nil local _, _, _, hold_regs = self.rtu.io_count() @@ -222,9 +245,12 @@ modbus.new = function (rtu_dev, use_parallel_read) response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end - return return_ok + return return_ok, response end + ---@param c_addr_start integer + ---@param values any + ---@return boolean ok, MODBUS_EXCODE|nil local _15_write_multiple_coils = function (c_addr_start, values) local response = nil local _, coils, _, _ = self.rtu.io_count() @@ -249,6 +275,9 @@ modbus.new = function (rtu_dev, use_parallel_read) return return_ok, response end + ---@param hr_addr_start integer + ---@param values any + ---@return boolean ok, MODBUS_EXCODE|nil local _16_write_multiple_holding_registers = function (hr_addr_start, values) local response = nil local _, _, _, hold_regs = self.rtu.io_count() @@ -274,7 +303,9 @@ modbus.new = function (rtu_dev, use_parallel_read) end -- validate a request without actually executing it - local check_request = function (packet) + ---@param packet modbus_frame + ---@return boolean return_code, modbus_packet reply + public.check_request = function (packet) local return_code = true local response = { MODBUS_EXCODE.ACKNOWLEDGE } @@ -314,7 +345,9 @@ modbus.new = function (rtu_dev, use_parallel_read) end -- handle a MODBUS TCP packet and generate a reply - local handle_packet = function (packet) + ---@param packet modbus_frame + ---@return boolean return_code, modbus_packet reply + public.handle_packet = function (packet) local return_code = true local response = nil @@ -369,7 +402,8 @@ modbus.new = function (rtu_dev, use_parallel_read) end -- return a SERVER_DEVICE_BUSY error reply - local reply__srv_device_busy = function (packet) + ---@return modbus_packet reply + public.reply__srv_device_busy = function (packet) -- reply back with error flag and exception code local reply = comms.modbus_packet() local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) @@ -379,7 +413,8 @@ modbus.new = function (rtu_dev, use_parallel_read) end -- return a NEG_ACKNOWLEDGE error reply - local reply__neg_ack = function (packet) + ---@return modbus_packet reply + public.reply__neg_ack = function (packet) -- reply back with error flag and exception code local reply = comms.modbus_packet() local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) @@ -389,7 +424,8 @@ modbus.new = function (rtu_dev, use_parallel_read) end -- return a GATEWAY_PATH_UNAVAILABLE error reply - local reply__gw_unavailable = function (packet) + ---@return modbus_packet reply + public.reply__gw_unavailable = function (packet) -- reply back with error flag and exception code local reply = comms.modbus_packet() local fcode = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) @@ -398,13 +434,7 @@ modbus.new = function (rtu_dev, use_parallel_read) return reply end - return { - check_request = check_request, - handle_packet = handle_packet, - reply__srv_device_busy = reply__srv_device_busy, - reply__neg_ack = reply__neg_ack, - reply__gw_unavailable = reply__gw_unavailable - } + return public end return modbus diff --git a/rtu/rtu.lua b/rtu/rtu.lua index f7b0432..ab47b0a 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -19,6 +19,7 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +-- create a new RTU rtu.init_unit = function () local self = { discrete_inputs = {}, @@ -28,28 +29,36 @@ rtu.init_unit = function () io_count_cache = { 0, 0, 0, 0 } } + ---@class rtu + local public = {} + local insert = table.insert local _count_io = function () self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs } end - -- return : IO count table - local io_count = function () + -- return IO counts + ---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs + public.io_count = function () return self.io_count_cache[0], self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3] end -- discrete inputs: single bit read-only - -- return : count of discrete inputs - local connect_di = function (f) + -- connect discrete input + ---@param f function + ---@return integer count count of discrete inputs + public.connect_di = function (f) insert(self.discrete_inputs, f) _count_io() return #self.discrete_inputs end - -- return : value, access fault - local read_di = function (di_addr) + -- read discrete input + ---@param di_addr integer + ---@return any value, boolean access_fault + public.read_di = function (di_addr) ppm.clear_fault() local value = self.discrete_inputs[di_addr]() return value, ppm.is_faulted() @@ -57,22 +66,30 @@ rtu.init_unit = function () -- coils: single bit read-write - -- return : count of coils - local connect_coil = function (f_read, f_write) + -- connect coil + ---@param f_read function + ---@param f_write function + ---@return integer count count of coils + public.connect_coil = function (f_read, f_write) insert(self.coils, { read = f_read, write = f_write }) _count_io() return #self.coils end - -- return : value, access fault - local read_coil = function (coil_addr) + -- read coil + ---@param coil_addr integer + ---@return any value, boolean access_fault + public.read_coil = function (coil_addr) ppm.clear_fault() local value = self.coils[coil_addr].read() return value, ppm.is_faulted() end - -- return : access fault - local write_coil = function (coil_addr, value) + -- write coil + ---@param coil_addr integer + ---@param value any + ---@return boolean access_fault + public.write_coil = function (coil_addr, value) ppm.clear_fault() self.coils[coil_addr].write(value) return ppm.is_faulted() @@ -80,15 +97,19 @@ rtu.init_unit = function () -- input registers: multi-bit read-only - -- return : count of input registers - local connect_input_reg = function (f) + -- connect input register + ---@param f function + ---@return integer count count of input registers + public.connect_input_reg = function (f) insert(self.input_regs, f) _count_io() return #self.input_regs end - -- return : value, access fault - local read_input_reg = function (reg_addr) + -- read input register + ---@param reg_addr integer + ---@return any value, boolean access_fault + public.read_input_reg = function (reg_addr) ppm.clear_fault() local value = self.coils[reg_addr]() return value, ppm.is_faulted() @@ -96,42 +117,43 @@ rtu.init_unit = function () -- holding registers: multi-bit read-write - -- return : count of holding registers - local connect_holding_reg = function (f_read, f_write) + -- connect holding register + ---@param f_read function + ---@param f_write function + ---@return integer count count of holding registers + public.connect_holding_reg = function (f_read, f_write) insert(self.holding_regs, { read = f_read, write = f_write }) _count_io() return #self.holding_regs end - -- return : value, access fault - local read_holding_reg = function (reg_addr) + -- read holding register + ---@param reg_addr integer + ---@return any value, boolean access_fault + public.read_holding_reg = function (reg_addr) ppm.clear_fault() local value = self.coils[reg_addr].read() return value, ppm.is_faulted() end - -- return : access fault - local write_holding_reg = function (reg_addr, value) + -- write holding register + ---@param reg_addr integer + ---@param value any + ---@return boolean access_fault + public.write_holding_reg = function (reg_addr, value) ppm.clear_fault() self.coils[reg_addr].write(value) return ppm.is_faulted() end - return { - io_count = io_count, - connect_di = connect_di, - read_di = read_di, - connect_coil = connect_coil, - read_coil = read_coil, - write_coil = write_coil, - connect_input_reg = connect_input_reg, - read_input_reg = read_input_reg, - connect_holding_reg = connect_holding_reg, - read_holding_reg = read_holding_reg, - write_holding_reg = write_holding_reg - } + return public end +-- RTU Communications +---@param modem table +---@param local_port integer +---@param server_port integer +---@param conn_watchdog watchdog rtu.comms = function (modem, local_port, server_port, conn_watchdog) local self = { seq_num = 0, @@ -143,6 +165,9 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) conn_watchdog = conn_watchdog } + ---@class rtu_comms + local public = {} + local insert = table.insert -- open modem @@ -152,6 +177,9 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) -- PRIVATE FUNCTIONS -- + -- send a scada management packet + ---@param msg_type SCADA_MGMT_TYPES + ---@param msg any local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() @@ -164,6 +192,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end -- keep alive ack + ---@param srv_time integer local _send_keep_alive_ack = function (srv_time) _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end @@ -171,7 +200,8 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) -- PUBLIC FUNCTIONS -- -- send a MODBUS TCP packet - local send_modbus = function (m_pkt) + ---@param m_pkt modbus_packet + public.send_modbus = function (m_pkt) local s_pkt = comms.scada_packet() s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) @@ -179,7 +209,9 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end -- reconnect a newly connected modem - local reconnect_modem = function (modem) + ---@param modem table +---@diagnostic disable-next-line: redefined-local + public.reconnect_modem = function (modem) self.modem = modem -- open modem @@ -189,42 +221,43 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end -- unlink from the server - local unlink = function (rtu_state) + ---@param rtu_state rtu_state + public.unlink = function (rtu_state) rtu_state.linked = false self.r_seq_num = nil end -- close the connection to the server - local close = function (rtu_state) + ---@param rtu_state rtu_state + public.close = function (rtu_state) self.conn_watchdog.cancel() - unlink(rtu_state) + public.unlink(rtu_state) _send(SCADA_MGMT_TYPES.CLOSE, {}) end -- send capability advertisement - local send_advertisement = function (units) + ---@param units table + public.send_advertisement = function (units) local advertisement = {} for i = 1, #units do - local unit = units[i] + local unit = units[i] ---@type rtu_unit_registry_entry local type = comms.rtu_t_to_advert_type(unit.type) if type ~= nil then + ---@class rtu_advertisement + local advert = { + type = type, ---@type integer + index = unit.index, ---@type integer + reactor = unit.reactor, ---@type integer + rsio = nil ---@type table|nil + } + if type == RTU_ADVERT_TYPES.REDSTONE then - insert(advertisement, { - type = type, - index = unit.index, - reactor = unit.for_reactor, - rsio = unit.device - }) - else - insert(advertisement, { - type = type, - index = unit.index, - reactor = unit.for_reactor, - rsio = nil - }) + advert.rsio = unit.device end + + insert(advertisement, advert) end end @@ -232,7 +265,13 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end -- parse a MODBUS/SCADA packet - local parse_packet = function(side, sender, reply_to, message, distance) + ---@param side string + ---@param sender integer + ---@param reply_to integer + ---@param message any + ---@param distance integer + ---@return modbus_frame|mgmt_frame|nil packet + public.parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil local s_pkt = comms.scada_packet() @@ -261,10 +300,11 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end -- handle a MODBUS/SCADA packet - local handle_packet = function(packet, units, rtu_state) + ---@param packet modbus_frame|mgmt_frame + ---@param units table + ---@param rtu_state rtu_state + public.handle_packet = function(packet, units, rtu_state) if packet ~= nil then - local seq_ok = true - -- check sequence number if self.r_seq_num == nil then self.r_seq_num = packet.scada_frame.seq_num() @@ -281,26 +321,27 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) local protocol = packet.scada_frame.protocol() if protocol == PROTOCOLS.MODBUS_TCP then + local return_code = false local reply = modbus.reply__neg_ack(packet) -- handle MODBUS instruction if packet.unit_id <= #units then - local unit = units[packet.unit_id] + local unit = units[packet.unit_id] ---@type rtu_unit_registry_entry if unit.name == "redstone_io" then -- immediately execute redstone RTU requests - local return_code, reply = unit.modbus_io.handle_packet(packet) + return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then log.warning("requested MODBUS operation failed") end else -- check validity then pass off to unit comms thread - local return_code, reply = unit.modbus_io.check_request(packet) + return_code, reply = unit.modbus_io.check_request(packet) if return_code then -- check if an operation is already in progress for this unit if unit.modbus_busy then reply = unit.modbus_io.reply__srv_device_busy(packet) else - unit.pkt_queue.push(packet) + unit.pkt_queue.push_packet(packet) end else log.warning("cannot perform requested MODBUS operation") @@ -312,7 +353,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) log.error("MODBUS packet requesting non-existent unit") end - send_modbus(reply) + public.send_modbus(reply) elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet if packet.type == SCADA_MGMT_TYPES.KEEP_ALIVE then @@ -334,7 +375,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) elseif packet.type == SCADA_MGMT_TYPES.CLOSE then -- close connection self.conn_watchdog.cancel() - unlink(rtu_state) + public.unlink(rtu_state) println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") elseif packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then @@ -343,7 +384,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) self.r_seq_num = nil elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- request for capabilities again - send_advertisement(units) + public.send_advertisement(units) else -- not supported log.warning("RTU got unexpected SCADA message type " .. packet.type) @@ -355,15 +396,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) end end - return { - send_modbus = send_modbus, - reconnect_modem = reconnect_modem, - parse_packet = parse_packet, - handle_packet = handle_packet, - send_advertisement = send_advertisement, - unlink = unlink, - close = close - } + return public end return rtu diff --git a/rtu/startup.lua b/rtu/startup.lua index ad31460..3b34670 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -22,7 +22,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.2" +local RTU_VERSION = "alpha-v0.6.3" local rtu_t = types.rtu_t @@ -45,8 +45,10 @@ println(">> RTU " .. RTU_VERSION .. " <<") -- mount connected devices ppm.mount_all() +---@class rtu_shared_memory local __shared_memory = { -- RTU system state flags + ---@class rtu_state rtu_state = { linked = false, shutdown = false @@ -59,9 +61,9 @@ local __shared_memory = { -- system objects rtu_sys = { - rtu_comms = nil, - conn_watchdog = nil, - units = {} + rtu_comms = nil, ---@type rtu_comms + conn_watchdog = nil, ---@type watchdog + units = {} ---@type table }, -- message queues @@ -140,7 +142,8 @@ for reactor_idx = 1, #rtu_redstone do end end - table.insert(units, { + ---@class rtu_unit_registry_entry + local unit = { name = "redstone_io", type = rtu_t.redstone, index = 1, @@ -151,7 +154,9 @@ for reactor_idx = 1, #rtu_redstone do modbus_busy = false, pkt_queue = nil, thread = nil - }) + } + + table.insert(units, unit) log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) end @@ -201,6 +206,7 @@ for i = 1, #rtu_devices do end if rtu_iface ~= nil then + ---@class rtu_unit_registry_entry local rtu_unit = { name = rtu_devices[i].name, type = rtu_type, diff --git a/rtu/threads.lua b/rtu/threads.lua index 447178b..27f5c27 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -1,11 +1,9 @@ -local comms = require("scada-common.comms") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") local types = require("scada-common.types") local util = require("scada-common.util") -local redstone_rtu = require("rtu.dev.redstone_rtu") local boiler_rtu = require("rtu.dev.boiler_rtu") local boilerv_rtu = require("rtu.dev.boilerv_rtu") local energymachine_rtu = require("rtu.dev.energymachine_rtu") @@ -24,12 +22,11 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local psleep = util.psleep - local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) -- main thread +---@param smem rtu_shared_memory threads.thread__main = function (smem) -- execute thread local exec = function () @@ -42,7 +39,7 @@ threads.thread__main = function (smem) local rtu_state = smem.rtu_state local rtu_dev = smem.rtu_dev local rtu_comms = smem.rtu_sys.rtu_comms - local conn_watchdog = smem.rtu_sys.conn_watchdog ---@type watchdog + local conn_watchdog = smem.rtu_sys.conn_watchdog local units = smem.rtu_sys.units -- start clock @@ -116,7 +113,7 @@ threads.thread__main = function (smem) else -- relink lost peripheral to correct unit entry for i = 1, #units do - local unit = units[i] + local unit = units[i] ---@type rtu_unit_registry_entry -- find disconnected device to reconnect if unit.name == param1 then @@ -137,7 +134,7 @@ threads.thread__main = function (smem) unit.rtu = imatrix_rtu.new(device) end - unit.modbus_io = modbus.new(unit.rtu) + unit.modbus_io = modbus.new(unit.rtu, true) println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end @@ -159,6 +156,7 @@ threads.thread__main = function (smem) end -- communications handler thread +---@param smem rtu_shared_memory threads.thread__comms = function (smem) -- execute thread local exec = function () @@ -179,14 +177,16 @@ threads.thread__comms = function (smem) while comms_queue.ready() and not rtu_state.shutdown do local msg = comms_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet - -- handle the packet (rtu_state passed to allow setting link flag) - rtu_comms.handle_packet(msg.message, units, rtu_state) + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (rtu_state passed to allow setting link flag) + rtu_comms.handle_packet(msg.message, units, rtu_state) + end end -- quick yield @@ -209,6 +209,8 @@ threads.thread__comms = function (smem) end -- per-unit communications handler thread +---@param smem rtu_shared_memory +---@param unit rtu_unit_registry_entry threads.thread__unit_comms = function (smem, unit) -- execute thread local exec = function () @@ -227,16 +229,18 @@ threads.thread__unit_comms = function (smem, unit) while packet_queue.ready() and not rtu_state.shutdown do local msg = packet_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet - unit.modbus_busy = true - local return_code, reply = unit.modbus_io.handle_packet(msg.message) - rtu_comms.send_modbus(reply) - unit.modbus_busy = false + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + unit.modbus_busy = true + local _, reply = unit.modbus_io.handle_packet(msg.message) + rtu_comms.send_modbus(reply) + unit.modbus_busy = false + end end -- quick yield From 95c4d51e67f2b16239f902bb3387292c067c2678 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 12:09:04 -0400 Subject: [PATCH 115/168] #47 RTU send should be table not any --- rtu/rtu.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index ab47b0a..9a353f9 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -179,7 +179,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) -- send a scada management packet ---@param msg_type SCADA_MGMT_TYPES - ---@param msg any + ---@param msg table local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() From 0d090fe9e2f5cca4fd169d76abac704885395bbe Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 12:31:19 -0400 Subject: [PATCH 116/168] #47 supervisor luadoc, bugfixes --- supervisor/session/plc.lua | 183 +++++++++++++++++------------- supervisor/session/rtu.lua | 2 +- supervisor/session/svsessions.lua | 32 ++++-- supervisor/startup.lua | 5 +- supervisor/supervisor.lua | 35 ++++-- supervisor/unit.lua | 30 +++-- 6 files changed, 176 insertions(+), 111 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index f6a12ca..42bb51c 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -37,6 +37,10 @@ local PERIODICS = { } -- PLC supervisor session +---@param id integer +---@param for_reactor integer +---@param in_queue mqueue +---@param out_queue mqueue plc.new_session = function (id, for_reactor, in_queue, out_queue) local log_header = "plc_session(" .. id .. "): " @@ -78,6 +82,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) rps_reset = true }, -- session database + ---@class reactor_db sDB = { last_status_update = 0, control_state = false, @@ -85,6 +90,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) degraded = false, rps_tripped = false, rps_trip_cause = "ok", + ---@class rps_status rps_status = { dmg_crit = false, ex_hcool = false, @@ -94,45 +100,52 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) no_cool = false, timed_out = false }, + ---@class mek_status mek_status = { - heating_rate = 0, + heating_rate = 0.0, status = false, - burn_rate = 0, - act_burn_rate = 0, - temp = 0, - damage = 0, - boil_eff = 0, - env_loss = 0, + burn_rate = 0.0, + act_burn_rate = 0.0, + temp = 0.0, + damage = 0.0, + boil_eff = 0.0, + env_loss = 0.0, - fuel = 0, - fuel_need = 0, - fuel_fill = 0, - waste = 0, - waste_need = 0, - waste_fill = 0, + fuel = 0.0, + fuel_need = 0.0, + fuel_fill = 0.0, + waste = 0.0, + waste_need = 0.0, + waste_fill = 0.0, cool_type = "?", - cool_amnt = 0, - cool_need = 0, - cool_fill = 0, + cool_amnt = 0.0, + cool_need = 0.0, + cool_fill = 0.0, hcool_type = "?", - hcool_amnt = 0, - hcool_need = 0, - hcool_fill = 0 + hcool_amnt = 0.0, + hcool_need = 0.0, + hcool_fill = 0.0 }, + ---@class mek_struct mek_struct = { - heat_cap = 0, - fuel_asm = 0, - fuel_sa = 0, - fuel_cap = 0, - waste_cap = 0, - cool_cap = 0, - hcool_cap = 0, - max_burn = 0 + heat_cap = 0.0, + fuel_asm = 0.0, + fuel_sa = 0.0, + fuel_cap = 0.0, + waste_cap = 0.0, + cool_cap = 0.0, + hcool_cap = 0.0, + max_burn = 0.0 } } } + ---@class plc_session + local public = {} + + -- copy in the RPS status + ---@param rps_status table local _copy_rps_status = function (rps_status) self.sDB.rps_status.dmg_crit = rps_status[1] self.sDB.rps_status.ex_hcool = rps_status[2] @@ -143,6 +156,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.sDB.rps_status.timed_out = rps_status[7] end + -- copy in the reactor status + ---@param mek_data table local _copy_status = function (mek_data) -- copy status information self.sDB.mek_status.status = mek_data[1] @@ -174,6 +189,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end end + -- copy in the reactor structure + ---@param mek_data table local _copy_struct = function (mek_data) self.sDB.mek_struct.heat_cap = mek_data[1] self.sDB.mek_struct.fuel_asm = mek_data[2] @@ -186,6 +203,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- send an RPLC packet + ---@param msg_type RPLC_TYPES + ---@param msg table local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() @@ -198,6 +217,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- send a SCADA management packet + ---@param msg_type SCADA_MGMT_TYPES + ---@param msg table local _send_mgmt = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() @@ -210,6 +231,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- get an ACK status + ---@param pkt rplc_frame + ---@return boolean|nil ack local _get_ack = function (pkt) if pkt.length == 1 then return pkt.data[1] @@ -220,6 +243,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- handle a packet + ---@param pkt rplc_frame local _handle_packet = function (pkt) -- check sequence number if self.r_seq_num == nil then @@ -378,13 +402,13 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- PUBLIC FUNCTIONS -- -- get the session ID - local get_id = function () return self.id end + public.get_id = function () return self.id end -- get the session database - local get_db = function () return self.sDB end + public.get_db = function () return self.sDB end -- get the reactor structure - local get_struct = function () + public.get_struct = function () if self.received_struct then return self.sDB.mek_struct else @@ -393,7 +417,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- get the reactor structure - local get_status = function () + public.get_status = function () if self.received_status_cache then return self.sDB.mek_status else @@ -402,12 +426,12 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- check if a timer matches this session's watchdog - local check_wd = function (timer) + public.check_wd = function (timer) return self.plc_conn_watchdog.is_timer(timer) end -- close the connection - local close = function () + public.close = function () self.plc_conn_watchdog.cancel() self.connected = false _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) @@ -416,7 +440,8 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end -- iterate the session - local iterate = function () + ---@return boolean connected + public.iterate = function () if self.connected then ------------------ -- handle queue -- @@ -428,45 +453,47 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- get a new message to process local message = self.in_q.pop() - if message.qtype == mqueue.TYPE.PACKET then - -- handle a packet - _handle_packet(message.message) - elseif message.qtype == mqueue.TYPE.COMMAND then - -- handle instruction - local cmd = message.message - if cmd == PLC_S_CMDS.ENABLE then - -- enable reactor - self.acks.enable = false - self.retry_times.enable_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_ENABLE, {}) - elseif cmd == PLC_S_CMDS.SCRAM then - -- SCRAM reactor - self.acks.scram = false - self.retry_times.scram_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_SCRAM, {}) - elseif cmd == PLC_S_CMDS.RPS_RESET then - -- reset RPS - self.acks.rps_reset = false - self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.RPS_RESET, {}) - end - elseif message.qtype == mqueue.TYPE.DATA then - -- instruction with body - local cmd = message.message - if cmd.key == PLC_S_DATA.BURN_RATE then - -- update burn rate - self.commanded_burn_rate = cmd.val - self.ramping_rate = false - self.acks.burn_rate = false - self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) - elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then - -- ramp to burn rate - self.commanded_burn_rate = cmd.val - self.ramping_rate = true - self.acks.burn_rate = false - self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT - _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + if message ~= nil then + if message.qtype == mqueue.TYPE.PACKET then + -- handle a packet + _handle_packet(message.message) + elseif message.qtype == mqueue.TYPE.COMMAND then + -- handle instruction + local cmd = message.message + if cmd == PLC_S_CMDS.ENABLE then + -- enable reactor + self.acks.enable = false + self.retry_times.enable_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.RPS_ENABLE, {}) + elseif cmd == PLC_S_CMDS.SCRAM then + -- SCRAM reactor + self.acks.scram = false + self.retry_times.scram_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.RPS_SCRAM, {}) + elseif cmd == PLC_S_CMDS.RPS_RESET then + -- reset RPS + self.acks.rps_reset = false + self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.RPS_RESET, {}) + end + elseif message.qtype == mqueue.TYPE.DATA then + -- instruction with body + local cmd = message.message + if cmd.key == PLC_S_DATA.BURN_RATE then + -- update burn rate + self.commanded_burn_rate = cmd.val + self.ramping_rate = false + self.acks.burn_rate = false + self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + elseif cmd.key == PLC_S_DATA.RAMP_BURN_RATE then + -- ramp to burn rate + self.commanded_burn_rate = cmd.val + self.ramping_rate = true + self.acks.burn_rate = false + self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT + _send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate }) + end end end @@ -567,15 +594,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) return self.connected end - return { - get_id = get_id, - get_db = get_db, - get_struct = get_struct, - get_status = get_status, - check_wd = check_wd, - close = close, - iterate = iterate - } + return public end return plc diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index a360be9..eed5cdc 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -100,7 +100,7 @@ rtu.new_session = function (id, in_queue, out_queue) elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- RTU unit advertisement for i = 1, pkt.length do - local unit = pkt.data[i] + local unit = pkt.data[i] ---@type rtu_advertisement end else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index e596985..37fc89e 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -31,16 +31,17 @@ local self = { -- PRIVATE FUNCTIONS -- -- iterate all the given sessions +---@param sessions table local function _iterate(sessions) for i = 1, #sessions do - local session = sessions[i] + local session = sessions[i] ---@type plc_session_struct if session.open then local ok = session.instance.iterate() if ok then -- send packets in out queue while session.out_queue.ready() do local msg = session.out_queue.pop() - if msg.qtype == mqueue.TYPE.PACKET then + if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) end end @@ -52,6 +53,7 @@ local function _iterate(sessions) end -- cleanly close a session +---@param session plc_session_struct local function _shutdown(session) session.open = false session.instance.close() @@ -59,7 +61,7 @@ local function _shutdown(session) -- send packets in out queue (namely the close packet) while session.out_queue.ready() do local msg = session.out_queue.pop() - if msg.qtype == mqueue.TYPE.PACKET then + if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then self.modem.transmit(session.r_port, session.l_port, msg.message.raw_sendable()) end end @@ -68,9 +70,10 @@ local function _shutdown(session) end -- close connections +---@param sessions table local function _close(sessions) for i = 1, #sessions do - local session = sessions[i] + local session = sessions[i] ---@type plc_session_struct if session.open then _shutdown(session) end @@ -78,9 +81,11 @@ local function _close(sessions) end -- check if a watchdog timer event matches that of one of the provided sessions +---@param sessions table +---@param timer_event number local function _check_watchdogs(sessions, timer_event) for i = 1, #sessions do - local session = sessions[i] + local session = sessions[i] ---@type plc_session_struct if session.open then local triggered = session.instance.check_wd(timer_event) if triggered then @@ -92,10 +97,11 @@ local function _check_watchdogs(sessions, timer_event) end -- delete any closed sessions +---@param sessions table local function _free_closed(sessions) local move_to = 1 for i = 1, #sessions do - local session = sessions[i] + local session = sessions[i] ---@type plc_session_struct if session ~= nil then if session.open then if sessions[move_to] == nil then @@ -113,11 +119,15 @@ end -- PUBLIC FUNCTIONS -- +-- link the modem +---@param modem table svsessions.link_modem = function (modem) self.modem = modem end -- find a session by the remote port +---@param remote_port integer +---@return plc_session_struct|nil svsessions.find_session = function (remote_port) -- check RTU sessions for i = 1, #self.rtu_sessions do @@ -144,6 +154,8 @@ svsessions.find_session = function (remote_port) end -- get a session by reactor ID +---@param reactor integer +---@return plc_session_struct session svsessions.get_reactor_session = function (reactor) local session = nil @@ -157,8 +169,13 @@ svsessions.get_reactor_session = function (reactor) end -- establish a new PLC session +---@param local_port integer +---@param remote_port integer +---@param for_reactor integer +---@return integer|false session_id svsessions.establish_plc_session = function (local_port, remote_port, for_reactor) - if svsessions.get_reactor_session(for_reactor) == nil then + if svsessions.get_reactor_session(for_reactor) == nil then + ---@class plc_session_struct local plc_s = { open = true, reactor = for_reactor, @@ -185,6 +202,7 @@ svsessions.establish_plc_session = function (local_port, remote_port, for_reacto end -- attempt to identify which session's watchdog timer fired +---@param timer_event number svsessions.check_all_watchdogs = function (timer_event) -- check RTU session watchdogs _check_watchdogs(self.rtu_sessions, timer_event) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index be9549e..5923887 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -6,15 +6,12 @@ local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") -local coordinator = require("supervisor.session.coordinator") -local plc = require("supervisor.session.plc") -local rtu = require("supervisor.session.rtu") local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.4" +local SUPERVISOR_VERSION = "alpha-v0.3.5" local print = util.print local println = util.println diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index f9df71e..a534882 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -20,6 +20,10 @@ local print_ts = util.print_ts local println_ts = util.println_ts -- supervisory controller communications +---@param num_reactors integer +---@param modem table +---@param dev_listen integer +---@param coord_listen integer supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) local self = { ln_seq_num = 0, @@ -30,6 +34,9 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) reactor_struct_cache = nil } + ---@class superv_comms + local public = {} + -- PRIVATE FUNCTIONS -- -- open all channels @@ -50,6 +57,8 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) svsessions.link_modem(self.modem) -- send PLC link request responses + ---@param dest integer + ---@param msg table local _send_plc_linking = function (dest, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() @@ -64,14 +73,22 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem - local reconnect_modem = function (modem) + ---@param modem table +---@diagnostic disable-next-line: redefined-local + public.reconnect_modem = function (modem) self.modem = modem svsessions.link_modem(self.modem) _open_channels() end -- parse a packet - local parse_packet = function(side, sender, reply_to, message, distance) + ---@param side string + ---@param sender integer + ---@param reply_to integer + ---@param message any + ---@param distance integer + ---@return modbus_frame|rplc_frame|mgmt_frame|coord_frame|nil packet + public.parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil local s_pkt = comms.scada_packet() @@ -111,7 +128,9 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) return pkt end - local handle_packet = function(packet) + -- handle a packet + ---@param packet modbus_frame|rplc_frame|mgmt_frame|coord_frame + public.handle_packet = function(packet) if packet ~= nil then local l_port = packet.scada_frame.local_port() local r_port = packet.scada_frame.remote_port() @@ -126,7 +145,7 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) -- MODBUS response elseif protocol == PROTOCOLS.RPLC then -- reactor PLC packet - if session then + if session ~= nil then if packet.type == RPLC_TYPES.LINK_REQ then -- new device on this port? that's a collision log.debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision") @@ -162,7 +181,7 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) end elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet - if session then + if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) end @@ -184,11 +203,7 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) end end - return { - reconnect_modem = reconnect_modem, - parse_packet = parse_packet, - handle_packet = handle_packet - } + return public end return supervisor diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 95aa5f1..a246ebc 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -1,8 +1,8 @@ local unit = {} +-- create a new reactor unit +---@param for_reactor integer unit.new = function (for_reactor) - local public = {} - local self = { r_id = for_reactor, plc_s = nil, @@ -11,6 +11,7 @@ unit.new = function (for_reactor) energy_storage = {}, redstone = {}, db = { + ---@class annunciator annunciator = { -- RPS -- reactor @@ -37,18 +38,36 @@ unit.new = function (for_reactor) } } + ---@class reactor_unit + local public = {} + + -- PRIVATE FUNCTIONS -- + + -- update the annunciator + local _update_annunciator = function () + self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) + self.db.annunciator.ReactorTrip = false + end + + -- PUBLIC FUNCTIONS -- + + -- link the PLC + ---@param plc_session plc_session_struct public.link_plc_session = function (plc_session) self.plc_s = plc_session end + -- link a turbine RTU public.add_turbine = function (turbine) table.insert(self.turbines, turbine) end + -- link a boiler RTU public.add_boiler = function (boiler) table.insert(self.boilers, boiler) end + -- link a redstone RTU capability public.add_redstone = function (field, accessor) -- ensure field exists if self.redstone[field] == nil then @@ -59,11 +78,7 @@ unit.new = function (for_reactor) table.insert(self.redstone[field], accessor) end - local _update_annunciator = function () - self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) - self.db.annunciator.ReactorTrip = false - end - + -- update (iterate) this session public.update = function () -- unlink PLC if session was closed if not self.plc_s.open then @@ -74,6 +89,7 @@ unit.new = function (for_reactor) _update_annunciator() end + -- get the annunciator status public.get_annunciator = function () return self.db.annunciator end return public From b985362757a9ced0a5249882535857d05bc59f7c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 13:02:21 -0400 Subject: [PATCH 117/168] #8 RTU session for boiler, added transaction controller --- supervisor/session/rtu.lua | 3 + supervisor/session/rtu/boiler.lua | 210 +++++++++++++++++++++++++++++ supervisor/session/rtu/txnctrl.lua | 95 +++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 supervisor/session/rtu/boiler.lua create mode 100644 supervisor/session/rtu/txnctrl.lua diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index eed5cdc..bb86b05 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -3,6 +3,9 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local util = require("scada-common.util") +-- supervisor rtu sessions (svrs) +local svrs_boiler = require("supervisor.session.rtu.boiler") + local rtu = {} local PROTOCOLS = comms.PROTOCOLS diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua new file mode 100644 index 0000000..83ecd8b --- /dev/null +++ b/supervisor/session/rtu/boiler.lua @@ -0,0 +1,210 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local txnctrl = require("supervisor.session.rtu.txnctrl") + +local boiler = {} + +local PROTOCOLS = comms.PROTOCOLS +local MODBUS_FCODE = types.MODBUS_FCODE + +local rtu_t = types.rtu_t + +local TXN_TYPES = { + BUILD = 0, + STATE = 1, + TANKS = 2 +} + +local PERIODICS = { + BUILD = 1000, + STATE = 500, + TANKS = 1000 +} + +-- create a new boiler rtu session runner +---@param advert rtu_advertisement +---@param out_queue mqueue +boiler.new = function (advert, out_queue) + -- type check + if advert.type ~= rtu_t.boiler then + log.error("attempt to instantiate boiler RTU for non boiler type '" .. advert.type .. "'. this is a bug.") + return nil + end + + local log_tag = "session.rtu.boiler(" .. advert.index .. "): " + + local self = { + uid = advert.index, + reactor = advert.reactor, + out_q = out_queue, + transaction_controller = txnctrl.new(), + has_build = false, + periodics = { + next_build_req = 0, + next_state_req = 0, + next_tanks_req = 0, + }, + ---@class boiler_session_db + db = { + build = { + boil_cap = 0.0, + steam_cap = 0, + water_cap = 0, + hcoolant_cap = 0, + ccoolant_cap = 0, + superheaters = 0, + max_boil_rate = 0.0 + }, + state = { + temperature = 0.0, + boil_rate = 0.0 + }, + tanks = { + steam = 0, + steam_need = 0, + steam_fill = 0.0, + water = 0, + water_need = 0, + water_fill = 0.0, + hcool = 0, + hcool_need = 0, + hcool_fill = 0.0, + ccool = 0, + ccool_need = 0, + ccool_fill = 0.0 + } + } + } + + ---@class rtu_session__boiler + local public = {} + + -- PRIVATE FUNCTIONS -- + + -- query the build of the device + local _request_build = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.BUILD) + + -- read input registers 1 through 7 (start = 1, count = 7) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) + + self.out_q.push_packet(m_pkt) + end + + -- query the state of the device + local _request_state = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.STATE) + + -- read input registers 8 through 9 (start = 8, count = 2) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) + + self.out_q.push_packet(m_pkt) + end + + -- query the tanks of the device + local _request_tanks = function () + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(TXN_TYPES.TANKS) + + -- read input registers 10 through 21 (start = 10, count = 12) + m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) + + self.out_q.push_packet(m_pkt) + end + + -- PUBLIC FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame + public.handle_packet = function (m_pkt) + local success = false + + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + if txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 7 then + self.db.build.boil_cap = m_pkt.data[1] + self.db.build.steam_cap = m_pkt.data[2] + self.db.build.water_cap = m_pkt.data[3] + self.db.build.hcoolant_cap = m_pkt.data[4] + self.db.build.ccoolant_cap = m_pkt.data[5] + self.db.build.superheaters = m_pkt.data[6] + self.db.build.max_boil_rate = m_pkt.data[7] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)") + end + elseif txn_type == TXN_TYPES.STATE then + -- state response + if m_pkt.length == 2 then + self.db.state.temperature = m_pkt.data[1] + self.db.state.boil_rate = m_pkt.data[2] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)") + end + elseif txn_type == TXN_TYPES.TANKS then + -- tanks response + if m_pkt.length == 12 then + self.db.tanks.steam = m_pkt.data[1] + self.db.tanks.steam_need = m_pkt.data[2] + self.db.tanks.steam_fill = m_pkt.data[3] + self.db.tanks.water = m_pkt.data[4] + self.db.tanks.water_need = m_pkt.data[5] + self.db.tanks.water_fill = m_pkt.data[6] + self.db.tanks.hcool = m_pkt.data[7] + self.db.tanks.hcool_need = m_pkt.data[8] + self.db.tanks.hcool_fill = m_pkt.data[9] + self.db.tanks.ccool = m_pkt.data[10] + self.db.tanks.ccool_need = m_pkt.data[11] + self.db.tanks.ccool_fill = m_pkt.data[12] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") + else + log.error(log_tag .. "unknown transaction type " .. txn_type) + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + return success + end + + public.get_uid = function () return self.uid end + public.get_reactor = function () return self.reactor end + public.get_db = function () return self.db end + + -- update this runner + ---@param time_now integer milliseconds + public.update = function (time_now) + if not self.has_build and self.next_build_req <= time_now then + _request_build() + self.next_build_req = time_now + PERIODICS.BUILD + end + + if self.next_state_req <= time_now then + _request_state() + self.next_state_req = time_now + PERIODICS.STATE + end + + if self.next_tanks_req <= time_now then + _request_tanks() + self.next_tanks_req = time_now + PERIODICS.TANKS + end + end + + return public +end + +return boiler diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua new file mode 100644 index 0000000..3d74484 --- /dev/null +++ b/supervisor/session/rtu/txnctrl.lua @@ -0,0 +1,95 @@ +-- +-- MODBUS Transaction Controller +-- + +local util = require("scada-common.util") + +local txnctrl = {} + +local TIMEOUT = 3000 -- 3000ms max wait + +-- create a new transaction controller +txnctrl.new = function () + local self = { + list = {}, + next_id = 0 + } + + ---@class transaction_controller + local public = {} + + local insert = table.insert + + -- get the length of the transaction list + public.length = function () + return #self.list + end + + -- check if there are no active transactions + public.empty = function () + return #self.list == 0 + end + + -- create a new transaction of the given type + ---@param txn_type integer + ---@return integer txn_id + public.create = function (txn_type) + local txn_id = self.next_id + + insert(self.list, { + txn_id = txn_id, + txn_type = txn_type, + expiry = util.time() + TIMEOUT + }) + + self.next_id = self.next_id + 1 + + return txn_id + end + + -- mark a transaction as resolved to get its transaction type + ---@param txn_id integer + ---@return integer txn_type + public.resolve = function (txn_id) + local txn_type = nil + + for i = 1, public.length() do + if self.list[i].txn_id == txn_id then + txn_type = self.list[i].txn_type + self.list[i] = nil + end + end + + return txn_type + end + + -- close timed-out transactions + public.cleanup = function () + local now = util.time() + + local move_to = 1 + for i = 1, public.length() do + local txn = self.list[i] + if txn ~= nil then + if txn.expiry <= now then + self.list[i] = nil + else + if self.list[move_to] == nil then + self.list[move_to] = txn + self.list[i] = nil + end + move_to = move_to + 1 + end + end + end + end + + -- clear the transaction list + public.clear = function () + self.list = {} + end + + return public +end + +return txnctrl From 9695e946081e71d6bff557d00ff62e167051d476 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 11 May 2022 13:05:20 -0400 Subject: [PATCH 118/168] plc session terminology change, changed number/integer types --- supervisor/session/plc.lua | 44 +++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 42bb51c..c36dbda 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -112,30 +112,30 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) boil_eff = 0.0, env_loss = 0.0, - fuel = 0.0, - fuel_need = 0.0, + fuel = 0, + fuel_need = 0, fuel_fill = 0.0, - waste = 0.0, - waste_need = 0.0, + waste = 0, + waste_need = 0, waste_fill = 0.0, - cool_type = "?", - cool_amnt = 0.0, - cool_need = 0.0, - cool_fill = 0.0, + ccool_type = "?", + ccool_amnt = 0, + ccool_need = 0, + ccool_fill = 0.0, hcool_type = "?", - hcool_amnt = 0.0, - hcool_need = 0.0, + hcool_amnt = 0, + hcool_need = 0, hcool_fill = 0.0 }, ---@class mek_struct mek_struct = { - heat_cap = 0.0, - fuel_asm = 0.0, - fuel_sa = 0.0, - fuel_cap = 0.0, - waste_cap = 0.0, - cool_cap = 0.0, - hcool_cap = 0.0, + heat_cap = 0, + fuel_asm = 0, + fuel_sa = 0, + fuel_cap = 0, + waste_cap = 0, + ccool_cap = 0, + hcool_cap = 0, max_burn = 0.0 } } @@ -173,9 +173,9 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.sDB.mek_status.fuel_fill = mek_data[9] self.sDB.mek_status.waste = mek_data[10] self.sDB.mek_status.waste_fill = mek_data[11] - self.sDB.mek_status.cool_type = mek_data[12] - self.sDB.mek_status.cool_amnt = mek_data[13] - self.sDB.mek_status.cool_fill = mek_data[14] + self.sDB.mek_status.ccool_type = mek_data[12] + self.sDB.mek_status.ccool_amnt = mek_data[13] + self.sDB.mek_status.ccool_fill = mek_data[14] self.sDB.mek_status.hcool_type = mek_data[15] self.sDB.mek_status.hcool_amnt = mek_data[16] self.sDB.mek_status.hcool_fill = mek_data[17] @@ -184,7 +184,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) if self.received_struct then self.sDB.mek_status.fuel_need = self.sDB.mek_struct.fuel_cap - self.sDB.mek_status.fuel_fill self.sDB.mek_status.waste_need = self.sDB.mek_struct.waste_cap - self.sDB.mek_status.waste_fill - self.sDB.mek_status.cool_need = self.sDB.mek_struct.cool_cap - self.sDB.mek_status.cool_fill + self.sDB.mek_status.cool_need = self.sDB.mek_struct.ccool_cap - self.sDB.mek_status.ccool_fill self.sDB.mek_status.hcool_need = self.sDB.mek_struct.hcool_cap - self.sDB.mek_status.hcool_fill end end @@ -197,7 +197,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.sDB.mek_struct.fuel_sa = mek_data[3] self.sDB.mek_struct.fuel_cap = mek_data[4] self.sDB.mek_struct.waste_cap = mek_data[5] - self.sDB.mek_struct.cool_cap = mek_data[6] + self.sDB.mek_struct.ccool_cap = mek_data[6] self.sDB.mek_struct.hcool_cap = mek_data[7] self.sDB.mek_struct.max_burn = mek_data[8] end From 969abca95dfbf97023ec6ebbca7bdc30c56117cd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 12 May 2022 15:36:27 -0400 Subject: [PATCH 119/168] RTU device changes, bugfixes, docs --- rtu/dev/boiler_rtu.lua | 12 +++------ rtu/dev/boilerv_rtu.lua | 18 ++++++-------- rtu/dev/energymachine_rtu.lua | 16 ++++++------ rtu/dev/imatrix_rtu.lua | 17 ++++++------- rtu/dev/redstone_rtu.lua | 47 ++++++++++++++++++++++++----------- rtu/dev/turbine_rtu.lua | 12 +++------ rtu/dev/turbinev_rtu.lua | 19 ++++++-------- rtu/modbus.lua | 2 +- rtu/rtu.lua | 29 ++++++++++++++------- rtu/startup.lua | 10 ++++---- 10 files changed, 98 insertions(+), 84 deletions(-) diff --git a/rtu/dev/boiler_rtu.lua b/rtu/dev/boiler_rtu.lua index 322c511..26b5ebc 100644 --- a/rtu/dev/boiler_rtu.lua +++ b/rtu/dev/boiler_rtu.lua @@ -1,17 +1,15 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local boiler_rtu = {} +-- create new boiler (mek 10.0) device +---@param boiler table boiler_rtu.new = function (boiler) local self = { rtu = rtu.init_unit(), boiler = boiler } - local rtu_interface = function () - return self.rtu - end - -- discrete inputs -- -- none @@ -47,9 +45,7 @@ boiler_rtu.new = function (boiler) -- holding registers -- -- none - return { - rtu_interface = rtu_interface - } + return self.rtu.interface() end return boiler_rtu diff --git a/rtu/dev/boilerv_rtu.lua b/rtu/dev/boilerv_rtu.lua index a609588..fca1f09 100644 --- a/rtu/dev/boilerv_rtu.lua +++ b/rtu/dev/boilerv_rtu.lua @@ -1,29 +1,28 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local boilerv_rtu = {} +-- create new boiler (mek 10.1+) device +---@param boiler table boilerv_rtu.new = function (boiler) local self = { rtu = rtu.init_unit(), boiler = boiler } - local rtu_interface = function () - return self.rtu - end - -- discrete inputs -- - -- none + self.rtu.connect_di(self.boiler.isFormed) -- coils -- -- none -- input registers -- -- multiblock properties - self.rtu.connect_input_reg(self.boiler.isFormed) self.rtu.connect_input_reg(self.boiler.getLength) self.rtu.connect_input_reg(self.boiler.getWidth) self.rtu.connect_input_reg(self.boiler.getHeight) + self.rtu.connect_input_reg(self.boiler.getMinPos) + self.rtu.connect_input_reg(self.boiler.getMaxPos) -- build properties self.rtu.connect_input_reg(self.boiler.getBoilCapacity) self.rtu.connect_input_reg(self.boiler.getSteamCapacity) @@ -32,6 +31,7 @@ boilerv_rtu.new = function (boiler) self.rtu.connect_input_reg(self.boiler.getCooledCoolantCapacity) self.rtu.connect_input_reg(self.boiler.getSuperheaters) self.rtu.connect_input_reg(self.boiler.getMaxBoilRate) + self.rtu.connect_input_reg(self.boiler.getEnvironmentalLoss) -- current state self.rtu.connect_input_reg(self.boiler.getTemperature) self.rtu.connect_input_reg(self.boiler.getBoilRate) @@ -52,9 +52,7 @@ boilerv_rtu.new = function (boiler) -- holding registers -- -- none - return { - rtu_interface = rtu_interface - } + return self.rtu.interface() end return boilerv_rtu diff --git a/rtu/dev/energymachine_rtu.lua b/rtu/dev/energymachine_rtu.lua index d2aee3f..e0e05af 100644 --- a/rtu/dev/energymachine_rtu.lua +++ b/rtu/dev/energymachine_rtu.lua @@ -1,16 +1,20 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local energymachine_rtu = {} +-- create new energy machine device +---@param machine table energymachine_rtu.new = function (machine) local self = { rtu = rtu.init_unit(), machine = machine } - local rtu_interface = function () - return self.rtu - end + ---@class rtu_device + local public = {} + + -- get the RTU interface + public.rtu_interface = function () return self.rtu end -- discrete inputs -- -- none @@ -29,9 +33,7 @@ energymachine_rtu.new = function (machine) -- holding registers -- -- none - return { - rtu_interface = rtu_interface - } + return public end return energymachine_rtu diff --git a/rtu/dev/imatrix_rtu.lua b/rtu/dev/imatrix_rtu.lua index 12fd942..56498e5 100644 --- a/rtu/dev/imatrix_rtu.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -1,29 +1,28 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local imatrix_rtu = {} +-- create new induction matrix (mek 10.1+) device +---@param imatrix table imatrix_rtu.new = function (imatrix) local self = { rtu = rtu.init_unit(), imatrix = imatrix } - local rtu_interface = function () - return self.rtu - end - -- discrete inputs -- - -- none + self.rtu.connect_di(self.boiler.isFormed) -- coils -- -- none -- input registers -- -- multiblock properties - self.rtu.connect_input_reg(self.boiler.isFormed) self.rtu.connect_input_reg(self.boiler.getLength) self.rtu.connect_input_reg(self.boiler.getWidth) self.rtu.connect_input_reg(self.boiler.getHeight) + self.rtu.connect_input_reg(self.boiler.getMinPos) + self.rtu.connect_input_reg(self.boiler.getMaxPos) -- build properties self.rtu.connect_input_reg(self.imatrix.getMaxEnergy) self.rtu.connect_input_reg(self.imatrix.getTransferCap) @@ -40,9 +39,7 @@ imatrix_rtu.new = function (imatrix) -- holding registers -- -- none - return { - rtu_interface = rtu_interface - } + return self.rtu.interface() end return imatrix_rtu diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 9683f57..2763e98 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -1,4 +1,4 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local rsio = require("scada-common.rsio") local redstone_rtu = {} @@ -6,16 +6,31 @@ local redstone_rtu = {} local digital_read = rsio.digital_read local digital_is_active = rsio.digital_is_active +-- create new redstone device redstone_rtu.new = function () local self = { rtu = rtu.init_unit() } - local rtu_interface = function () - return self.rtu - end + -- get RTU interface + local interface = self.rtu.interface() - local link_di = function (channel, side, color) + ---@class rtu_rs_device + --- extends rtu_device; fields added manually to please Lua diagnostics + local public = { + io_count = interface.io_count, + read_coil = interface.read_coil, + read_di = interface.read_di, + read_holding_reg = interface.read_holding_reg, + read_input_reg = interface.read_input_reg, + write_coil = interface.write_coil, + write_holding_reg = interface.write_holding_reg + } + + -- link digital input + ---@param side string + ---@param color integer + public.link_di = function (side, color) local f_read = nil if color then @@ -31,7 +46,11 @@ redstone_rtu.new = function () self.rtu.connect_di(f_read) end - local link_do = function (channel, side, color) + -- link digital output + ---@param channel RS_IO + ---@param side string + ---@param color integer + public.link_do = function (channel, side, color) local f_read = nil local f_write = nil @@ -65,7 +84,9 @@ redstone_rtu.new = function () self.rtu.connect_coil(f_read, f_write) end - local link_ai = function (channel, side) + -- link analog input + ---@param side string + public.link_ai = function (side) self.rtu.connect_input_reg( function () return rs.getAnalogInput(side) @@ -73,7 +94,9 @@ redstone_rtu.new = function () ) end - local link_ao = function (channel, side) + -- link analog output + ---@param side string + public.link_ao = function (side) self.rtu.connect_holding_reg( function () return rs.getAnalogOutput(side) @@ -84,13 +107,7 @@ redstone_rtu.new = function () ) end - return { - rtu_interface = rtu_interface, - link_di = link_di, - link_do = link_do, - link_ai = link_ai, - link_ao = link_ao - } + return public end return redstone_rtu diff --git a/rtu/dev/turbine_rtu.lua b/rtu/dev/turbine_rtu.lua index 1f1827f..5ff71a7 100644 --- a/rtu/dev/turbine_rtu.lua +++ b/rtu/dev/turbine_rtu.lua @@ -1,17 +1,15 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local turbine_rtu = {} +-- create new turbine (mek 10.0) device +---@param turbine table turbine_rtu.new = function (turbine) local self = { rtu = rtu.init_unit(), turbine = turbine } - local rtu_interface = function () - return self.rtu - end - -- discrete inputs -- -- none @@ -42,9 +40,7 @@ turbine_rtu.new = function (turbine) -- holding registers -- -- none - return { - rtu_interface = rtu_interface - } + return self.rtu.interface() end return turbine_rtu diff --git a/rtu/dev/turbinev_rtu.lua b/rtu/dev/turbinev_rtu.lua index 2be532b..aa7a108 100644 --- a/rtu/dev/turbinev_rtu.lua +++ b/rtu/dev/turbinev_rtu.lua @@ -1,19 +1,17 @@ -local rtu = require("rtu") +local rtu = require("rtu.rtu") local turbinev_rtu = {} +-- create new turbine (mek 10.1+) device +---@param turbine table turbinev_rtu.new = function (turbine) local self = { rtu = rtu.init_unit(), turbine = turbine } - local rtu_interface = function () - return self.rtu - end - -- discrete inputs -- - -- none + self.rtu.connect_di(self.boiler.isFormed) -- coils -- self.rtu.connect_coil(function () self.turbine.incrementDumpingMode() end, function () end) @@ -21,10 +19,11 @@ turbinev_rtu.new = function (turbine) -- input registers -- -- multiblock properties - self.rtu.connect_input_reg(self.boiler.isFormed) self.rtu.connect_input_reg(self.boiler.getLength) self.rtu.connect_input_reg(self.boiler.getWidth) self.rtu.connect_input_reg(self.boiler.getHeight) + self.rtu.connect_input_reg(self.boiler.getMinPos) + self.rtu.connect_input_reg(self.boiler.getMaxPos) -- build properties self.rtu.connect_input_reg(self.turbine.getBlades) self.rtu.connect_input_reg(self.turbine.getCoils) @@ -50,11 +49,9 @@ turbinev_rtu.new = function (turbine) self.rtu.connect_input_reg(self.turbine.getEnergyFilledPercentage) -- holding registers -- - self.rtu.conenct_holding_reg(self.turbine.setDumpingMode, self.turbine.getDumpingMode) + self.rtu.connect_holding_reg(self.turbine.setDumpingMode, self.turbine.getDumpingMode) - return { - rtu_interface = rtu_interface - } + return self.rtu.interface() end return turbinev_rtu diff --git a/rtu/modbus.lua b/rtu/modbus.lua index 0e5e16c..efc0c84 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -7,7 +7,7 @@ local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE -- new modbus comms handler object ----@param rtu_dev rtu 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 modbus.new = function (rtu_dev, use_parallel_read) local self = { diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 9a353f9..cafb645 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -29,16 +29,20 @@ rtu.init_unit = function () io_count_cache = { 0, 0, 0, 0 } } - ---@class rtu - local public = {} - local insert = table.insert + ---@class rtu_device + local public = {} + + ---@class rtu + local protected = {} + + -- refresh IO count local _count_io = function () self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs } end - -- return IO counts + -- return IO count ---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs public.io_count = function () return self.io_count_cache[0], self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3] @@ -49,7 +53,7 @@ rtu.init_unit = function () -- connect discrete input ---@param f function ---@return integer count count of discrete inputs - public.connect_di = function (f) + protected.connect_di = function (f) insert(self.discrete_inputs, f) _count_io() return #self.discrete_inputs @@ -70,7 +74,7 @@ rtu.init_unit = function () ---@param f_read function ---@param f_write function ---@return integer count count of coils - public.connect_coil = function (f_read, f_write) + protected.connect_coil = function (f_read, f_write) insert(self.coils, { read = f_read, write = f_write }) _count_io() return #self.coils @@ -100,7 +104,7 @@ rtu.init_unit = function () -- connect input register ---@param f function ---@return integer count count of input registers - public.connect_input_reg = function (f) + protected.connect_input_reg = function (f) insert(self.input_regs, f) _count_io() return #self.input_regs @@ -121,7 +125,7 @@ rtu.init_unit = function () ---@param f_read function ---@param f_write function ---@return integer count count of holding registers - public.connect_holding_reg = function (f_read, f_write) + protected.connect_holding_reg = function (f_read, f_write) insert(self.holding_regs, { read = f_read, write = f_write }) _count_io() return #self.holding_regs @@ -146,7 +150,14 @@ rtu.init_unit = function () return ppm.is_faulted() end - return public + -- public RTU device access + + -- get the public interface to this RTU + protected.interface = function () + return public + end + + return protected end -- RTU Communications diff --git a/rtu/startup.lua b/rtu/startup.lua index 3b34670..b37e3fd 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -22,7 +22,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.3" +local RTU_VERSION = "alpha-v0.6.4" local rtu_t = types.rtu_t @@ -122,13 +122,13 @@ for reactor_idx = 1, #rtu_redstone do -- link redstone in RTU local mode = rsio.get_io_mode(conf.channel) if mode == rsio.IO_MODE.DIGITAL_IN then - rs_rtu.link_di(conf.channel, conf.side, conf.bundled_color) + rs_rtu.link_di(conf.side, conf.bundled_color) elseif mode == rsio.IO_MODE.DIGITAL_OUT then rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) elseif mode == rsio.IO_MODE.ANALOG_IN then - rs_rtu.link_ai(conf.channel, conf.side) + rs_rtu.link_ai(conf.side) elseif mode == rsio.IO_MODE.ANALOG_OUT then - rs_rtu.link_ao(conf.channel, conf.side) + rs_rtu.link_ao(conf.side) else -- should be unreachable code, we already validated channels log.error("init> fell through if chain attempting to identify IO mode", true) @@ -171,7 +171,7 @@ for i = 1, #rtu_devices do log.warning(message) else local type = ppm.get_type(rtu_devices[i].name) - local rtu_iface = nil + local rtu_iface = nil ---@type rtu_device local rtu_type = "" if type == "boiler" then From e624dd431b60593b547da52af920aff53b6c1c36 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 12 May 2022 15:37:42 -0400 Subject: [PATCH 120/168] tank_fluid and coordinate table types --- scada-common/types.lua | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/scada-common/types.lua b/scada-common/types.lua index 372b1d3..95312d3 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -5,6 +5,19 @@ ---@class types local types = {} +---@class tank_fluid +local tank_fluid = { + name = "mekanism:empty_gas", + amount = 0 +} + +---@class coordinate +local coordinate = { + x = 0, + y = 0, + z = 0 +} + ---@alias rtu_t string types.rtu_t = { redstone = "redstone", From 8b43c81fc06569e17aef02a18b441dad945fe332 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 09:38:10 -0400 Subject: [PATCH 121/168] class definition in only comments --- scada-common/mqueue.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index db97df4..30c21e8 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -21,10 +21,8 @@ mqueue.new = function () local remove = table.remove ---@class queue_item - local queue_item = { - qtype = 0, ---@type TYPE - message = 0 ---@type any - } + ---@field qtype TYPE + ---@field message any ---@class mqueue local public = {} From 13fcf265b7e40915e3bd5b34eae272577516c2e0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 09:38:32 -0400 Subject: [PATCH 122/168] updated types, added dumping mode and rtu_advertisement --- scada-common/types.lua | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/scada-common/types.lua b/scada-common/types.lua index 95312d3..b7f115b 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -5,18 +5,24 @@ ---@class types local types = {} +-- CLASSES -- + ---@class tank_fluid -local tank_fluid = { - name = "mekanism:empty_gas", - amount = 0 -} +---@field name string +---@field amount integer ---@class coordinate -local coordinate = { - x = 0, - y = 0, - z = 0 -} +---@field x integer +---@field y integer +---@field z integer + +---@class rtu_advertisement +---@field type integer +---@field index integer +---@field reactor integer +---@field rsio table|nil + +-- STRING TYPES -- ---@alias rtu_t string types.rtu_t = { @@ -43,6 +49,14 @@ types.rps_status_t = { manual = "manual" } +-- turbine steam dumping modes +---@alias DUMPING_MODE string +types.DUMPING_MODE = { + IDLE = "IDLE", + DUMPING = "DUMPING", + DUMPING_EXCESS = "DUMPING_EXCESS" +} + -- MODBUS -- modbus function codes From 635e7b7f59c68a407db49b0ce87b287e443ff267 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 09:39:28 -0400 Subject: [PATCH 123/168] RTU advertisement sends as basic array, re-ordered input registers on turbine RTU --- rtu/dev/turbine_rtu.lua | 4 ++-- rtu/rtu.lua | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/rtu/dev/turbine_rtu.lua b/rtu/dev/turbine_rtu.lua index 5ff71a7..476a50c 100644 --- a/rtu/dev/turbine_rtu.lua +++ b/rtu/dev/turbine_rtu.lua @@ -23,15 +23,15 @@ turbine_rtu.new = function (turbine) self.rtu.connect_input_reg(self.turbine.getVents) self.rtu.connect_input_reg(self.turbine.getDispersers) self.rtu.connect_input_reg(self.turbine.getCondensers) - self.rtu.connect_input_reg(self.turbine.getDumpingMode) self.rtu.connect_input_reg(self.turbine.getSteamCapacity) self.rtu.connect_input_reg(self.turbine.getMaxFlowRate) - self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput) self.rtu.connect_input_reg(self.turbine.getMaxProduction) + self.rtu.connect_input_reg(self.turbine.getMaxWaterOutput) -- current state self.rtu.connect_input_reg(self.turbine.getFlowRate) self.rtu.connect_input_reg(self.turbine.getProductionRate) self.rtu.connect_input_reg(self.turbine.getLastSteamInputRate) + self.rtu.connect_input_reg(self.turbine.getDumpingMode) -- tanks self.rtu.connect_input_reg(self.turbine.getSteam) self.rtu.connect_input_reg(self.turbine.getSteamNeeded) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index cafb645..637be54 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -256,16 +256,14 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) local type = comms.rtu_t_to_advert_type(unit.type) if type ~= nil then - ---@class rtu_advertisement local advert = { - type = type, ---@type integer - index = unit.index, ---@type integer - reactor = unit.reactor, ---@type integer - rsio = nil ---@type table|nil + type, + unit.index, + unit.reactor } if type == RTU_ADVERT_TYPES.REDSTONE then - advert.rsio = unit.device + insert(advert, unit.device) end insert(advertisement, advert) From fc39588b2e1e5470bec3a2bef07cdfb4c171ea7c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 09:45:11 -0400 Subject: [PATCH 124/168] #8 RTU session for emachine and turbine, RTU session creation, adjusted sequence number logic in svsessions --- supervisor/session/rtu.lua | 124 +++++++++++++----- supervisor/session/rtu/boiler.lua | 42 +++--- supervisor/session/rtu/emachine.lua | 149 +++++++++++++++++++++ supervisor/session/rtu/turbine.lua | 195 ++++++++++++++++++++++++++++ supervisor/session/svsessions.lua | 31 ++++- supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 48 +++++-- 7 files changed, 525 insertions(+), 66 deletions(-) create mode 100644 supervisor/session/rtu/emachine.lua create mode 100644 supervisor/session/rtu/turbine.lua diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index bb86b05..30844dd 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -5,12 +5,14 @@ local util = require("scada-common.util") -- supervisor rtu sessions (svrs) local svrs_boiler = require("supervisor.session.rtu.boiler") +local svrs_emachine = require("supervisor.session.rtu.emachine") +local svrs_turbine = require("supervisor.session.rtu.turbine") local rtu = {} local PROTOCOLS = comms.PROTOCOLS -local RPLC_TYPES = comms.RPLC_TYPES local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES local print = util.print local println = util.println @@ -21,27 +23,76 @@ local PERIODICS = { KEEP_ALIVE = 2.0 } -rtu.new_session = function (id, in_queue, out_queue) +-- create a new RTU session +---@param id integer +---@param in_queue mqueue +---@param out_queue mqueue +---@param advertisement table +rtu.new_session = function (id, in_queue, out_queue, advertisement) local log_header = "rtu_session(" .. id .. "): " local self = { id = id, in_q = in_queue, out_q = out_queue, - commanded_state = false, - commanded_burn_rate = 0.0, - ramping_rate = false, + advert = advertisement, -- connection properties seq_num = 0, r_seq_num = nil, connected = true, - received_struct = false, - received_status_cache = false, rtu_conn_watchdog = util.new_watchdog(3), - last_rtt = 0 + last_rtt = 0, + units = {} } + ---@class rtu_session + local public = {} + + -- parse the recorded advertisement + local _parse_advertisement = function () + self.units = {} + for i = 1, #self.advert do + local unit = nil + + ---@type rtu_advertisement + local unit_advert = { + type = self.advert[i][0], + index = self.advert[i][1], + reactor = self.advert[i][2], + rsio = self.advert[i][3] + } + + local u_type = unit_advert.type + + -- create unit by type + if u_type == RTU_ADVERT_TYPES.REDSTONE then + + elseif u_type == RTU_ADVERT_TYPES.BOILER then + unit = svrs_boiler.new(self.id, unit_advert, self.out_q) + elseif u_type == RTU_ADVERT_TYPES.BOILER_VALVE then + -- @todo Mekanism 10.1+ + elseif u_type == RTU_ADVERT_TYPES.TURBINE then + unit = svrs_turbine.new(self.id, unit_advert, self.out_q) + elseif u_type == RTU_ADVERT_TYPES.TURBINE_VALVE then + -- @todo Mekanism 10.1+ + elseif u_type == RTU_ADVERT_TYPES.EMACHINE then + unit = svrs_emachine.new(self.id, unit_advert, self.out_q) + elseif u_type == RTU_ADVERT_TYPES.IMATRIX then + -- @todo Mekanism 10.1+ + end + + if unit ~= nil then + table.insert(self.units, unit) + else + self.units = {} + log.error(log_header .. "bad advertisement; encountered unsupported RTU type") + break + end + end + end + -- send a MODBUS TCP packet + ---@param m_pkt modbus_packet local _send_modbus = function (m_pkt) local s_pkt = comms.scada_packet() s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) @@ -50,6 +101,8 @@ rtu.new_session = function (id, in_queue, out_queue) end -- send a SCADA management packet + ---@param msg_type SCADA_MGMT_TYPES + ---@param msg table local _send_mgmt = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() @@ -62,6 +115,7 @@ rtu.new_session = function (id, in_queue, out_queue) end -- handle a packet + ---@param pkt modbus_frame|mgmt_frame local _handle_packet = function (pkt) -- check sequence number if self.r_seq_num == nil then @@ -78,6 +132,10 @@ rtu.new_session = function (id, in_queue, out_queue) -- process packet if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if self.units[pkt.unit_id] ~= nil then + local unit = self.units[pkt.unit_id] ---@type rtu_session_unit + unit.handle_packet(pkt) + end elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then @@ -102,9 +160,8 @@ rtu.new_session = function (id, in_queue, out_queue) self.connected = false elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- RTU unit advertisement - for i = 1, pkt.length do - local unit = pkt.data[i] ---@type rtu_advertisement - end + self.advert = pkt.data + _parse_advertisement() else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end @@ -114,15 +171,16 @@ rtu.new_session = function (id, in_queue, out_queue) -- PUBLIC FUNCTIONS -- -- get the session ID - local get_id = function () return self.id end + public.get_id = function () return self.id end -- check if a timer matches this session's watchdog - local check_wd = function (timer) + ---@param timer number + public.check_wd = function (timer) return self.rtu_conn_watchdog.is_timer(timer) end -- close the connection - local close = function () + public.close = function () self.rtu_conn_watchdog.cancel() self.connected = false _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) @@ -131,8 +189,17 @@ rtu.new_session = function (id, in_queue, out_queue) end -- iterate the session - local iterate = function () + ---@return boolean connected + public.iterate = function () if self.connected then + ------------------ + -- update units -- + ------------------ + + for i = 1, #self.units do + self.units[i].update() + end + ------------------ -- handle queue -- ------------------ @@ -141,17 +208,17 @@ rtu.new_session = function (id, in_queue, out_queue) while self.in_q.ready() and self.connected do -- get a new message to process - local message = self.in_q.pop() + local msg = self.in_q.pop() - if message.qtype == mqueue.TYPE.PACKET then - -- handle a packet - _handle_packet(message.message) - elseif message.qtype == mqueue.TYPE.COMMAND then - -- handle instruction - local cmd = message.message - elseif message.qtype == mqueue.TYPE.DATA then - -- instruction with body - local cmd = message.message + if msg ~= nil then + if msg.qtype == mqueue.TYPE.PACKET then + -- handle a packet + _handle_packet(msg.message) + elseif msg.qtype == mqueue.TYPE.COMMAND then + -- handle instruction + elseif msg.qtype == mqueue.TYPE.DATA then + -- instruction with body + end end -- max 100ms spent processing queue @@ -191,12 +258,7 @@ rtu.new_session = function (id, in_queue, out_queue) return self.connected end - return { - get_id = get_id, - check_wd = check_wd, - close = close, - iterate = iterate - } + return public end return rtu diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index 83ecd8b..8804bda 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -1,7 +1,6 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") -local util = require("scada-common.util") local txnctrl = require("supervisor.session.rtu.txnctrl") @@ -25,16 +24,17 @@ local PERIODICS = { } -- create a new boiler rtu session runner +---@param session_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue -boiler.new = function (advert, out_queue) +boiler.new = function (session_id, advert, out_queue) -- type check if advert.type ~= rtu_t.boiler then - log.error("attempt to instantiate boiler RTU for non boiler type '" .. advert.type .. "'. this is a bug.") + log.error("attempt to instantiate boiler RTU for type '" .. advert.type .. "'. this is a bug.") return nil end - local log_tag = "session.rtu.boiler(" .. advert.index .. "): " + local log_tag = "session.rtu(" .. session_id .. ").boiler(" .. advert.index .. "): " local self = { uid = advert.index, @@ -69,52 +69,46 @@ boiler.new = function (advert, out_queue) water = 0, water_need = 0, water_fill = 0.0, - hcool = 0, + hcool = {}, ---@type tank_fluid hcool_need = 0, hcool_fill = 0.0, - ccool = 0, + ccool = {}, ---@type tank_fluid ccool_need = 0, ccool_fill = 0.0 } } } - ---@class rtu_session__boiler + ---@class rtu_session_unit local public = {} -- PRIVATE FUNCTIONS -- - -- query the build of the device - local _request_build = function () + local _send_request = function (txn_type, f_code, register_range) local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(TXN_TYPES.BUILD) + local txn_id = self.transaction_controller.create(txn_type) - -- read input registers 1 through 7 (start = 1, count = 7) - m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) + m_pkt.make(txn_id, self.uid, f_code, register_range) self.out_q.push_packet(m_pkt) end + -- query the build of the device + local _request_build = function () + -- read input registers 1 through 7 (start = 1, count = 7) + _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) + end + -- query the state of the device local _request_state = function () - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(TXN_TYPES.STATE) - -- read input registers 8 through 9 (start = 8, count = 2) - m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) - - self.out_q.push_packet(m_pkt) + _send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) end -- query the tanks of the device local _request_tanks = function () - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(TXN_TYPES.TANKS) - -- read input registers 10 through 21 (start = 10, count = 12) - m_pkt.make(txn_id, self.uid, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) - - self.out_q.push_packet(m_pkt) + _send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) end -- PUBLIC FUNCTIONS -- diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua new file mode 100644 index 0000000..340315e --- /dev/null +++ b/supervisor/session/rtu/emachine.lua @@ -0,0 +1,149 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local types = require("scada-common.types") + +local txnctrl = require("supervisor.session.rtu.txnctrl") + +local emachine = {} + +local PROTOCOLS = comms.PROTOCOLS +local MODBUS_FCODE = types.MODBUS_FCODE + +local rtu_t = types.rtu_t + +local TXN_TYPES = { + BUILD = 0, + STORAGE = 1 +} + +local PERIODICS = { + BUILD = 1000, + STORAGE = 500 +} + +-- create a new energy machine rtu session runner +---@param session_id integer +---@param advert rtu_advertisement +---@param out_queue mqueue +emachine.new = function (session_id, advert, out_queue) + -- type check + if advert.type ~= rtu_t.energy_machine then + log.error("attempt to instantiate emachine RTU for type '" .. advert.type .. "'. this is a bug.") + return nil + end + + local log_tag = "session.rtu(" .. session_id .. ").emachine(" .. advert.index .. "): " + + local self = { + uid = advert.index, + -- reactor = advert.reactor, + reactor = 0, + out_q = out_queue, + transaction_controller = txnctrl.new(), + has_build = false, + periodics = { + next_build_req = 0, + next_storage_req = 0, + }, + ---@class emachine_session_db + db = { + build = { + max_energy = 0 + }, + storage = { + energy = 0, + energy_need = 0, + energy_fill = 0.0 + } + } + } + + ---@class rtu_session_unit + local public = {} + + -- PRIVATE FUNCTIONS -- + + local _send_request = function (txn_type, f_code, register_range) + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(txn_type) + + m_pkt.make(txn_id, self.uid, f_code, register_range) + + self.out_q.push_packet(m_pkt) + end + + -- query the build of the device + local _request_build = function () + -- read input register 1 (start = 1, count = 1) + _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 1 }) + end + + -- query the state of the energy storage + local _request_storage = function () + -- read input registers 2 through 4 (start = 2, count = 3) + _send_request(TXN_TYPES.STORAGE, MODBUS_FCODE.READ_INPUT_REGS, { 2, 3 }) + end + + -- PUBLIC FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame + public.handle_packet = function (m_pkt) + local success = false + + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + if txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 1 then + self.db.build.max_energy = m_pkt.data[1] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.build)") + end + elseif txn_type == TXN_TYPES.STORAGE then + -- storage response + if m_pkt.length == 3 then + self.db.storage.energy = m_pkt.data[1] + self.db.storage.energy_need = m_pkt.data[2] + self.db.storage.energy_fill = m_pkt.data[3] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.storage)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") + else + log.error(log_tag .. "unknown transaction type " .. txn_type) + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + return success + end + + public.get_uid = function () return self.uid end + public.get_reactor = function () return self.reactor end + public.get_db = function () return self.db end + + -- update this runner + ---@param time_now integer milliseconds + public.update = function (time_now) + if not self.has_build and self.next_build_req <= time_now then + _request_build() + self.next_build_req = time_now + PERIODICS.BUILD + end + + if self.next_storage_req <= time_now then + _request_storage() + self.next_storage_req = time_now + PERIODICS.STORAGE + end + end + + return public +end + +return emachine diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua new file mode 100644 index 0000000..23c3d61 --- /dev/null +++ b/supervisor/session/rtu/turbine.lua @@ -0,0 +1,195 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local types = require("scada-common.types") + +local txnctrl = require("supervisor.session.rtu.txnctrl") + +local turbine = {} + +local PROTOCOLS = comms.PROTOCOLS +local DUMPING_MODE = types.DUMPING_MODE +local MODBUS_FCODE = types.MODBUS_FCODE + +local rtu_t = types.rtu_t + +local TXN_TYPES = { + BUILD = 0, + STATE = 1, + TANKS = 2 +} + +local PERIODICS = { + BUILD = 1000, + STATE = 500, + TANKS = 1000 +} + +-- create a new turbine rtu session runner +---@param session_id integer +---@param advert rtu_advertisement +---@param out_queue mqueue +turbine.new = function (session_id, advert, out_queue) + -- type check + if advert.type ~= rtu_t.turbine then + log.error("attempt to instantiate turbine RTU for type '" .. advert.type .. "'. this is a bug.") + return nil + end + + local log_tag = "session.rtu(" .. session_id .. ").turbine(" .. advert.index .. "): " + + local self = { + uid = advert.index, + reactor = advert.reactor, + out_q = out_queue, + transaction_controller = txnctrl.new(), + has_build = false, + periodics = { + next_build_req = 0, + next_state_req = 0, + next_tanks_req = 0, + }, + ---@class turbine_session_db + db = { + build = { + blades = 0, + coils = 0, + vents = 0, + dispersers = 0, + condensers = 0, + steam_cap = 0, + max_flow_rate = 0, + max_production = 0, + max_water_output = 0 + }, + state = { + flow_rate = 0.0, + prod_rate = 0.0, + steam_input_rate = 0.0, + dumping_mode = DUMPING_MODE.IDLE ---@type DUMPING_MODE + }, + tanks = { + steam = 0, + steam_need = 0, + steam_fill = 0.0 + } + } + } + + ---@class rtu_session_unit + local public = {} + + -- PRIVATE FUNCTIONS -- + + local _send_request = function (txn_type, f_code, register_range) + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(txn_type) + + m_pkt.make(txn_id, self.uid, f_code, register_range) + + self.out_q.push_packet(m_pkt) + end + + -- query the build of the device + local _request_build = function () + -- read input registers 1 through 9 (start = 1, count = 9) + _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) + end + + -- query the state of the device + local _request_state = function () + -- read input registers 10 through 13 (start = 10, count = 4) + _send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 4 }) + end + + -- query the tanks of the device + local _request_tanks = function () + -- read input registers 14 through 16 (start = 14, count = 3) + _send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 14, 3 }) + end + + -- PUBLIC FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame + public.handle_packet = function (m_pkt) + local success = false + + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + if txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 9 then + self.db.build.blades = m_pkt.data[1] + self.db.build.coils = m_pkt.data[2] + self.db.build.vents = m_pkt.data[3] + self.db.build.dispersers = m_pkt.data[4] + self.db.build.condensers = m_pkt.data[5] + self.db.build.steam_cap = m_pkt.data[6] + self.db.build.max_flow_rate = m_pkt.data[7] + self.db.build.max_production = m_pkt.data[8] + self.db.build.max_water_output = m_pkt.data[9] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.build)") + end + elseif txn_type == TXN_TYPES.STATE then + -- state response + if m_pkt.length == 4 then + self.db.state.flow_rate = m_pkt.data[1] + self.db.state.prod_rate = m_pkt.data[2] + self.db.state.steam_input_rate = m_pkt.data[3] + self.db.state.dumping_mode = m_pkt.data[4] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.state)") + end + elseif txn_type == TXN_TYPES.TANKS then + -- tanks response + if m_pkt.length == 3 then + self.db.tanks.steam = m_pkt.data[1] + self.db.tanks.steam_need = m_pkt.data[2] + self.db.tanks.steam_fill = m_pkt.data[3] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.tanks)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") + else + log.error(log_tag .. "unknown transaction type " .. txn_type) + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + return success + end + + public.get_uid = function () return self.uid end + public.get_reactor = function () return self.reactor end + public.get_db = function () return self.db end + + -- update this runner + ---@param time_now integer milliseconds + public.update = function (time_now) + if not self.has_build and self.next_build_req <= time_now then + _request_build() + self.next_build_req = time_now + PERIODICS.BUILD + end + + if self.next_state_req <= time_now then + _request_state() + self.next_state_req = time_now + PERIODICS.STATE + end + + if self.next_tanks_req <= time_now then + _request_tanks() + self.next_tanks_req = time_now + PERIODICS.TANKS + end + end + + return public +end + +return turbine diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 37fc89e..635e81e 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -53,7 +53,7 @@ local function _iterate(sessions) end -- cleanly close a session ----@param session plc_session_struct +---@param session plc_session_struct|rtu_session_struct local function _shutdown(session) session.open = false session.instance.close() @@ -127,7 +127,7 @@ end -- find a session by the remote port ---@param remote_port integer ----@return plc_session_struct|nil +---@return plc_session_struct|rtu_session_struct|nil svsessions.find_session = function (remote_port) -- check RTU sessions for i = 1, #self.rtu_sessions do @@ -201,6 +201,33 @@ svsessions.establish_plc_session = function (local_port, remote_port, for_reacto end end +-- establish a new RTU session +---@param local_port integer +---@param remote_port integer +---@param advertisement table +---@return integer session_id +svsessions.establish_rtu_session = function (local_port, remote_port, advertisement) + ---@class rtu_session_struct + local rtu_s = { + open = true, + l_port = local_port, + r_port = remote_port, + in_queue = mqueue.new(), + out_queue = mqueue.new(), + instance = nil + } + + rtu_s.instance = rtu.new_session(self.next_rtu_id, rtu_s.in_queue, rtu_s.out_queue, advertisement) + table.insert(self.rtu_sessions, rtu_s) + + log.debug("established new RTU session to " .. remote_port .. " with ID " .. self.next_rtu_id) + + self.next_rtu_id = self.next_rtu_id + 1 + + -- success + return rtu_s.instance.get_id() +end + -- attempt to identify which session's watchdog timer fired ---@param timer_event number svsessions.check_all_watchdogs = function (timer_event) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 5923887..b559bf0 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -11,7 +11,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.5" +local SUPERVISOR_VERSION = "alpha-v0.3.6" local print = util.print local println = util.println diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index a534882..7924e9f 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -26,7 +26,6 @@ local println_ts = util.println_ts ---@param coord_listen integer supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) local self = { - ln_seq_num = 0, num_reactors = num_reactors, modem = modem, dev_listen = dev_listen, @@ -59,15 +58,27 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) -- send PLC link request responses ---@param dest integer ---@param msg table - local _send_plc_linking = function (dest, msg) + local _send_plc_linking = function (seq_id, dest, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() r_pkt.make(0, RPLC_TYPES.LINK_REQ, msg) - s_pkt.make(self.ln_seq_num, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + s_pkt.make(seq_id, PROTOCOLS.RPLC, r_pkt.raw_sendable()) + + self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable()) + end + + -- send RTU advertisement responses + ---@param seq_id integer + ---@param dest integer + local _send_remote_linked = function (seq_id, dest) + local s_pkt = comms.scada_packet() + local m_pkt = comms.mgmt_packet() + + m_pkt.make(SCADA_MGMT_TYPES.REMOTE_LINKED, {}) + s_pkt.make(seq_id, PROTOCOLS.SCADA_MGMT, m_pkt.raw_sendable()) self.modem.transmit(dest, self.dev_listen, s_pkt.raw_sendable()) - self.ln_seq_num = self.ln_seq_num + 1 end -- PUBLIC FUNCTIONS -- @@ -143,18 +154,27 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) if protocol == PROTOCOLS.MODBUS_TCP then -- MODBUS response + if session ~= nil then + -- pass the packet onto the session handler + session.in_queue.push_packet(packet) + else + -- any other packet should be session related, discard it + log.debug("discarding MODBUS_TCP packet without a known session") + end elseif protocol == PROTOCOLS.RPLC then -- reactor PLC packet if session ~= nil then if packet.type == RPLC_TYPES.LINK_REQ then -- new device on this port? that's a collision log.debug("PLC_LNK: request from existing connection received on " .. r_port .. ", responding with collision") - _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) + _send_plc_linking(packet.scada_frame.seq_num() + 1, r_port, { RPLC_LINKING.COLLISION }) else -- pass the packet onto the session handler session.in_queue.push_packet(packet) end else + local next_seq_id = packet.scada_frame.seq_num() + 1 + -- unknown session, is this a linking request? if packet.type == RPLC_TYPES.LINK_REQ then if packet.length == 1 then @@ -163,12 +183,12 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) if plc_id == false then -- reactor already has a PLC assigned log.debug("PLC_LNK: assignment collision with reactor " .. packet.data[1]) - _send_plc_linking(r_port, { RPLC_LINKING.COLLISION }) + _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully println("connected to reactor " .. packet.data[1] .. " PLC (port " .. r_port .. ")") log.debug("PLC_LNK: allowed for device at " .. r_port) - _send_plc_linking(r_port, { RPLC_LINKING.ALLOW }) + _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.ALLOW }) end else log.debug("PLC_LNK: new linking packet length mismatch") @@ -176,7 +196,7 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) else -- force a re-link log.debug("PLC_LNK: no session but not a link, force relink") - _send_plc_linking(r_port, { RPLC_LINKING.DENY }) + _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.DENY }) end end elseif protocol == PROTOCOLS.SCADA_MGMT then @@ -184,6 +204,18 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) + else + -- is this an RTU advertisement? + if packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then + local rtu_id = svsessions.establish_rtu_session(l_port, r_port, packet.data) + + println("connected to RTU (port " .. r_port .. ")") + log.debug("RTU_ADVERT: linked " .. r_port) + _send_remote_linked(packet.scada_frame.seq_num() + 1, r_port) + else + -- any other packet should be session related, discard it + log.debug("discarding SCADA_MGMT packet without a known session") + end end else log.debug("illegal packet type " .. protocol .. " on device listening channel") From 45f58435988fa4fdf37158c8d3e4e96fa7607ba5 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 09:49:24 -0400 Subject: [PATCH 125/168] #8 renamed rtu_session_unit type to unit_session --- supervisor/session/rtu.lua | 4 ++-- supervisor/session/rtu/boiler.lua | 2 +- supervisor/session/rtu/emachine.lua | 2 +- supervisor/session/rtu/turbine.lua | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 30844dd..fd30fd0 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -52,7 +52,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) local _parse_advertisement = function () self.units = {} for i = 1, #self.advert do - local unit = nil + local unit = nil ---@type unit_session ---@type rtu_advertisement local unit_advert = { @@ -133,7 +133,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- process packet if pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then if self.units[pkt.unit_id] ~= nil then - local unit = self.units[pkt.unit_id] ---@type rtu_session_unit + local unit = self.units[pkt.unit_id] ---@type unit_session unit.handle_packet(pkt) end elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index 8804bda..78931ec 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -79,7 +79,7 @@ boiler.new = function (session_id, advert, out_queue) } } - ---@class rtu_session_unit + ---@class unit_session local public = {} -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index 340315e..b6c89b6 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -58,7 +58,7 @@ emachine.new = function (session_id, advert, out_queue) } } - ---@class rtu_session_unit + ---@class unit_session local public = {} -- PRIVATE FUNCTIONS -- diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index 23c3d61..ccca068 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -75,7 +75,7 @@ turbine.new = function (session_id, advert, out_queue) } } - ---@class rtu_session_unit + ---@class unit_session local public = {} -- PRIVATE FUNCTIONS -- From c53ddf16385c8b19a991fa9c266bd3b6d9bf7b71 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 10:27:57 -0400 Subject: [PATCH 126/168] renamed RTU_ADVERT_TYPES to RTU_UNIT_TYPES --- rtu/rtu.lua | 4 ++-- scada-common/comms.lua | 38 +++++++++++++++++++------------------- supervisor/session/rtu.lua | 16 ++++++++-------- supervisor/supervisor.lua | 2 +- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 637be54..ec963b8 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -12,7 +12,7 @@ local rtu_t = types.rtu_t local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local print = util.print local println = util.println @@ -262,7 +262,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) unit.reactor } - if type == RTU_ADVERT_TYPES.REDSTONE then + if type == RTU_UNIT_TYPES.REDSTONE then insert(advert, unit.device) end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 6b4deef..bffd98f 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -48,8 +48,8 @@ local SCADA_MGMT_TYPES = { REMOTE_LINKED = 3 -- remote device linked } ----@alias RTU_ADVERT_TYPES integer -local RTU_ADVERT_TYPES = { +---@alias RTU_UNIT_TYPES integer +local RTU_UNIT_TYPES = { REDSTONE = 0, -- redstone I/O BOILER = 1, -- boiler BOILER_VALVE = 2, -- boiler mekanism 10.1+ @@ -63,7 +63,7 @@ comms.PROTOCOLS = PROTOCOLS comms.RPLC_TYPES = RPLC_TYPES comms.RPLC_LINKING = RPLC_LINKING comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES -comms.RTU_ADVERT_TYPES = RTU_ADVERT_TYPES +comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES -- generic SCADA packet object comms.scada_packet = function () @@ -562,44 +562,44 @@ end -- convert rtu_t to RTU advertisement type ---@param type rtu_t ----@return RTU_ADVERT_TYPES|nil +---@return RTU_UNIT_TYPES|nil comms.rtu_t_to_advert_type = function (type) if type == rtu_t.redstone then - return RTU_ADVERT_TYPES.REDSTONE + return RTU_UNIT_TYPES.REDSTONE elseif type == rtu_t.boiler then - return RTU_ADVERT_TYPES.BOILER + return RTU_UNIT_TYPES.BOILER elseif type == rtu_t.boiler_valve then - return RTU_ADVERT_TYPES.BOILER_VALVE + return RTU_UNIT_TYPES.BOILER_VALVE elseif type == rtu_t.turbine then - return RTU_ADVERT_TYPES.TURBINE + return RTU_UNIT_TYPES.TURBINE elseif type == rtu_t.turbine_valve then - return RTU_ADVERT_TYPES.TURBINE_VALVE + return RTU_UNIT_TYPES.TURBINE_VALVE elseif type == rtu_t.energy_machine then - return RTU_ADVERT_TYPES.EMACHINE + return RTU_UNIT_TYPES.EMACHINE elseif type == rtu_t.induction_matrix then - return RTU_ADVERT_TYPES.IMATRIX + return RTU_UNIT_TYPES.IMATRIX end return nil end -- convert RTU advertisement type to rtu_t ----@param atype RTU_ADVERT_TYPES +---@param atype RTU_UNIT_TYPES ---@return rtu_t|nil comms.advert_type_to_rtu_t = function (atype) - if atype == RTU_ADVERT_TYPES.REDSTONE then + if atype == RTU_UNIT_TYPES.REDSTONE then return rtu_t.redstone - elseif atype == RTU_ADVERT_TYPES.BOILER then + elseif atype == RTU_UNIT_TYPES.BOILER then return rtu_t.boiler - elseif atype == RTU_ADVERT_TYPES.BOILER_VALVE then + elseif atype == RTU_UNIT_TYPES.BOILER_VALVE then return rtu_t.boiler_valve - elseif atype == RTU_ADVERT_TYPES.TURBINE then + elseif atype == RTU_UNIT_TYPES.TURBINE then return rtu_t.turbine - elseif atype == RTU_ADVERT_TYPES.TURBINE_VALVE then + elseif atype == RTU_UNIT_TYPES.TURBINE_VALVE then return rtu_t.turbine_valve - elseif atype == RTU_ADVERT_TYPES.EMACHINE then + elseif atype == RTU_UNIT_TYPES.EMACHINE then return rtu_t.energy_machine - elseif atype == RTU_ADVERT_TYPES.IMATRIX then + elseif atype == RTU_UNIT_TYPES.IMATRIX then return rtu_t.induction_matrix end diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index fd30fd0..5dab40c 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -12,7 +12,7 @@ local rtu = {} local PROTOCOLS = comms.PROTOCOLS local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local print = util.print local println = util.println @@ -65,19 +65,19 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) local u_type = unit_advert.type -- create unit by type - if u_type == RTU_ADVERT_TYPES.REDSTONE then + if u_type == RTU_UNIT_TYPES.REDSTONE then - elseif u_type == RTU_ADVERT_TYPES.BOILER then + elseif u_type == RTU_UNIT_TYPES.BOILER then unit = svrs_boiler.new(self.id, unit_advert, self.out_q) - elseif u_type == RTU_ADVERT_TYPES.BOILER_VALVE then + elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then -- @todo Mekanism 10.1+ - elseif u_type == RTU_ADVERT_TYPES.TURBINE then + elseif u_type == RTU_UNIT_TYPES.TURBINE then unit = svrs_turbine.new(self.id, unit_advert, self.out_q) - elseif u_type == RTU_ADVERT_TYPES.TURBINE_VALVE then + elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then -- @todo Mekanism 10.1+ - elseif u_type == RTU_ADVERT_TYPES.EMACHINE then + elseif u_type == RTU_UNIT_TYPES.EMACHINE then unit = svrs_emachine.new(self.id, unit_advert, self.out_q) - elseif u_type == RTU_ADVERT_TYPES.IMATRIX then + elseif u_type == RTU_UNIT_TYPES.IMATRIX then -- @todo Mekanism 10.1+ end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 7924e9f..5596cb8 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -10,7 +10,7 @@ local PROTOCOLS = comms.PROTOCOLS local RPLC_TYPES = comms.RPLC_TYPES local RPLC_LINKING = comms.RPLC_LINKING local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES -local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local SESSION_TYPE = svsessions.SESSION_TYPE From bf0e92d6e436a86f19a1ea55de98ced6302cb595 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 11:08:22 -0400 Subject: [PATCH 127/168] refactoring --- rtu/rtu.lua | 4 ++-- scada-common/comms.lua | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index ec963b8..f46b560 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -252,8 +252,8 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) local advertisement = {} for i = 1, #units do - local unit = units[i] ---@type rtu_unit_registry_entry - local type = comms.rtu_t_to_advert_type(unit.type) + local unit = units[i] --@type rtu_unit_registry_entry + local type = comms.rtu_t_to_unit_type(unit.type) if type ~= nil then local advert = { diff --git a/scada-common/comms.lua b/scada-common/comms.lua index bffd98f..c305b29 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -560,10 +560,10 @@ comms.capi_packet = function () return public end --- convert rtu_t to RTU advertisement type +-- convert rtu_t to RTU unit type ---@param type rtu_t ---@return RTU_UNIT_TYPES|nil -comms.rtu_t_to_advert_type = function (type) +comms.rtu_t_to_unit_type = function (type) if type == rtu_t.redstone then return RTU_UNIT_TYPES.REDSTONE elseif type == rtu_t.boiler then @@ -583,23 +583,23 @@ comms.rtu_t_to_advert_type = function (type) return nil end --- convert RTU advertisement type to rtu_t ----@param atype RTU_UNIT_TYPES +-- convert RTU unit type to rtu_t +---@param utype RTU_UNIT_TYPES ---@return rtu_t|nil -comms.advert_type_to_rtu_t = function (atype) - if atype == RTU_UNIT_TYPES.REDSTONE then +comms.advert_type_to_rtu_t = function (utype) + if utype == RTU_UNIT_TYPES.REDSTONE then return rtu_t.redstone - elseif atype == RTU_UNIT_TYPES.BOILER then + elseif utype == RTU_UNIT_TYPES.BOILER then return rtu_t.boiler - elseif atype == RTU_UNIT_TYPES.BOILER_VALVE then + elseif utype == RTU_UNIT_TYPES.BOILER_VALVE then return rtu_t.boiler_valve - elseif atype == RTU_UNIT_TYPES.TURBINE then + elseif utype == RTU_UNIT_TYPES.TURBINE then return rtu_t.turbine - elseif atype == RTU_UNIT_TYPES.TURBINE_VALVE then + elseif utype == RTU_UNIT_TYPES.TURBINE_VALVE then return rtu_t.turbine_valve - elseif atype == RTU_UNIT_TYPES.EMACHINE then + elseif utype == RTU_UNIT_TYPES.EMACHINE then return rtu_t.energy_machine - elseif atype == RTU_UNIT_TYPES.IMATRIX then + elseif utype == RTU_UNIT_TYPES.IMATRIX then return rtu_t.induction_matrix end From 72da7180158c2c566f78dce38b11323c4fe9ef15 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 11:38:56 -0400 Subject: [PATCH 128/168] optimized session lookup --- supervisor/session/svsessions.lua | 41 ++++++++++++++++++++++++++++--- supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 15 ++++++++--- 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 635e81e..88c57cd 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -125,10 +125,38 @@ svsessions.link_modem = function (modem) self.modem = modem end --- find a session by the remote port +-- find an RTU session by the remote port +---@param remote_port integer +---@return rtu_session_struct|nil +svsessions.find_rtu_session = function (remote_port) + -- check RTU sessions + for i = 1, #self.rtu_sessions do + if self.rtu_sessions[i].r_port == remote_port then + return self.rtu_sessions[i] + end + end + + return nil +end + +-- find a PLC session by the remote port +---@param remote_port integer +---@return plc_session_struct|nil +svsessions.find_plc_session = function (remote_port) + -- check PLC sessions + for i = 1, #self.plc_sessions do + if self.plc_sessions[i].r_port == remote_port then + return self.plc_sessions[i] + end + end + + return nil +end + +-- find a PLC/RTU session by the remote port ---@param remote_port integer ---@return plc_session_struct|rtu_session_struct|nil -svsessions.find_session = function (remote_port) +svsessions.find_device_session = function (remote_port) -- check RTU sessions for i = 1, #self.rtu_sessions do if self.rtu_sessions[i].r_port == remote_port then @@ -143,6 +171,13 @@ svsessions.find_session = function (remote_port) end end + return nil +end + +-- find a coordinator session by the remote port +---@param remote_port integer +---@return nil +svsessions.find_coord_session = function (remote_port) -- check coordinator sessions for i = 1, #self.coord_sessions do if self.coord_sessions[i].r_port == remote_port then @@ -155,7 +190,7 @@ end -- get a session by reactor ID ---@param reactor integer ----@return plc_session_struct session +---@return plc_session_struct|nil session svsessions.get_reactor_session = function (reactor) local session = nil diff --git a/supervisor/startup.lua b/supervisor/startup.lua index b559bf0..16725b6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -11,7 +11,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.6" +local SUPERVISOR_VERSION = "alpha-v0.3.7" local print = util.print local println = util.println diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 5596cb8..3a176cb 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -149,10 +149,10 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) -- device (RTU/PLC) listening channel if l_port == self.dev_listen then - -- look for an associated session - local session = svsessions.find_session(r_port) - if protocol == PROTOCOLS.MODBUS_TCP then + -- look for an associated session + local session = svsessions.find_rtu_session(r_port) + -- MODBUS response if session ~= nil then -- pass the packet onto the session handler @@ -162,6 +162,9 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) log.debug("discarding MODBUS_TCP packet without a known session") end elseif protocol == PROTOCOLS.RPLC then + -- look for an associated session + local session = svsessions.find_plc_session(r_port) + -- reactor PLC packet if session ~= nil then if packet.type == RPLC_TYPES.LINK_REQ then @@ -200,6 +203,9 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) end end elseif protocol == PROTOCOLS.SCADA_MGMT then + -- look for an associated session + local session = svsessions.find_device_session(r_port) + -- SCADA management packet if session ~= nil then -- pass the packet onto the session handler @@ -222,6 +228,9 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) end -- coordinator listening channel elseif l_port == self.coord_listen then + -- look for an associated session + local session = svsessions.find_coord_session(r_port) + if protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet elseif protocol == PROTOCOLS.COORD_DATA then From 282d3fcd99f022cab2a5c09b37a48807dbe943c2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 12:23:01 -0400 Subject: [PATCH 129/168] added queue_data class --- scada-common/mqueue.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scada-common/mqueue.lua b/scada-common/mqueue.lua index 30c21e8..8069ecb 100644 --- a/scada-common/mqueue.lua +++ b/scada-common/mqueue.lua @@ -24,6 +24,10 @@ mqueue.new = function () ---@field qtype TYPE ---@field message any + ---@class queue_data + ---@field key any + ---@field val any + ---@class mqueue local public = {} From 6b74db70bd35efc246117d8792080b8523178259 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 13 May 2022 12:23:30 -0400 Subject: [PATCH 130/168] #8 fixed RTU unit type check --- supervisor/session/rtu/boiler.lua | 3 ++- supervisor/session/rtu/emachine.lua | 3 ++- supervisor/session/rtu/turbine.lua | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index 78931ec..320d7e4 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -7,6 +7,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl") local boiler = {} local PROTOCOLS = comms.PROTOCOLS +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE local rtu_t = types.rtu_t @@ -29,7 +30,7 @@ local PERIODICS = { ---@param out_queue mqueue boiler.new = function (session_id, advert, out_queue) -- type check - if advert.type ~= rtu_t.boiler then + if advert.type ~= RTU_UNIT_TYPES.BOILER then log.error("attempt to instantiate boiler RTU for type '" .. advert.type .. "'. this is a bug.") return nil end diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index b6c89b6..9428806 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -7,6 +7,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl") local emachine = {} local PROTOCOLS = comms.PROTOCOLS +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE local rtu_t = types.rtu_t @@ -27,7 +28,7 @@ local PERIODICS = { ---@param out_queue mqueue emachine.new = function (session_id, advert, out_queue) -- type check - if advert.type ~= rtu_t.energy_machine then + if advert.type ~= RTU_UNIT_TYPES.EMACHINE then log.error("attempt to instantiate emachine RTU for type '" .. advert.type .. "'. this is a bug.") return nil end diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index ccca068..66429f3 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -7,6 +7,7 @@ local txnctrl = require("supervisor.session.rtu.txnctrl") local turbine = {} local PROTOCOLS = comms.PROTOCOLS +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local DUMPING_MODE = types.DUMPING_MODE local MODBUS_FCODE = types.MODBUS_FCODE @@ -30,7 +31,7 @@ local PERIODICS = { ---@param out_queue mqueue turbine.new = function (session_id, advert, out_queue) -- type check - if advert.type ~= rtu_t.turbine then + if advert.type ~= RTU_UNIT_TYPES.TURBINE then log.error("attempt to instantiate turbine RTU for type '" .. advert.type .. "'. this is a bug.") return nil end From d3a926b25aa549693f693644ce399a295fd3fee1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 14 May 2022 13:32:42 -0400 Subject: [PATCH 131/168] fixed require issues caused by using bootloader --- coordinator/startup.lua | 2 ++ initenv.lua | 18 ++++++++++++++++++ pocket/startup.lua | 4 +++- reactor-plc/startup.lua | 2 ++ rtu/startup.lua | 2 ++ startup.lua | 6 ++---- supervisor/startup.lua | 2 ++ 7 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 initenv.lua diff --git a/coordinator/startup.lua b/coordinator/startup.lua index ab90f72..aa65be4 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -2,6 +2,8 @@ -- Nuclear Generation Facility SCADA Coordinator -- +require("/initenv").init_env() + local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") diff --git a/initenv.lua b/initenv.lua new file mode 100644 index 0000000..257af39 --- /dev/null +++ b/initenv.lua @@ -0,0 +1,18 @@ +-- +-- Initialize the Boot Environment +-- + +-- initialize booted environment +local init_env = function () + local _require = require("cc.require") + local _env = setmetatable({}, { __index = _ENV }) + + -- overwrite require/package globals + require, package = _require.make(_env, "/") + + -- reset terminal + term.clear() + term.setCursorPos(1, 1) +end + +return { init_env = init_env } diff --git a/pocket/startup.lua b/pocket/startup.lua index aeeaef4..40a0777 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -1,3 +1,5 @@ -- -- SCADA Coordinator Access on a Pocket Computer --- \ No newline at end of file +-- + +require("/initenv").init_env() diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 0aaaeba..aad57f7 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -2,6 +2,8 @@ -- Reactor Programmable Logic Controller -- +require("/initenv").init_env() + local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") diff --git a/rtu/startup.lua b/rtu/startup.lua index b37e3fd..94ae506 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -2,6 +2,8 @@ -- RTU: Remote Terminal Unit -- +require("/initenv").init_env() + local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") local ppm = require("scada-common.ppm") diff --git a/startup.lua b/startup.lua index 0a5cffb..482c919 100644 --- a/startup.lua +++ b/startup.lua @@ -1,6 +1,6 @@ local util = require("scada-common.util") -local BOOTLOADER_VERSION = "0.1" +local BOOTLOADER_VERSION = "0.2" local println = util.println local println_ts = util.println_ts @@ -43,9 +43,7 @@ else return false end -if exit_code then - println_ts("BOOT> APPLICATION EXITED OK") -else +if not exit_code then println_ts("BOOT> APPLICATION CRASHED") end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 16725b6..53f3e1a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -2,6 +2,8 @@ -- Nuclear Generation Facility SCADA Supervisor -- +require("/initenv").init_env() + local log = require("scada-common.log") local ppm = require("scada-common.ppm") local util = require("scada-common.util") From 533d398f9d1bd558ee2fd5d01202158db106389b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 14 May 2022 13:34:51 -0400 Subject: [PATCH 132/168] comment change --- initenv.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/initenv.lua b/initenv.lua index 257af39..c66e4c3 100644 --- a/initenv.lua +++ b/initenv.lua @@ -1,5 +1,5 @@ -- --- Initialize the Boot Environment +-- Initialize the Post-Boot Module Environment -- -- initialize booted environment From bc6453de2baa1ed6f8b8e5337e0f67a3c5250090 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 14 May 2022 20:07:26 -0400 Subject: [PATCH 133/168] RTU bugfixes, adjusted comms sleep timer --- rtu/rtu.lua | 2 +- rtu/startup.lua | 21 +++++++++++---------- rtu/threads.lua | 2 +- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index f46b560..de96c66 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -45,7 +45,7 @@ rtu.init_unit = function () -- return IO count ---@return integer discrete_inputs, integer coils, integer input_regs, integer holding_regs public.io_count = function () - return self.io_count_cache[0], self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3] + return self.io_count_cache[1], self.io_count_cache[2], self.io_count_cache[3], self.io_count_cache[4] end -- discrete inputs: single bit read-only diff --git a/rtu/startup.lua b/rtu/startup.lua index 94ae506..654f7a4 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.4" +local RTU_VERSION = "alpha-v0.6.5" local rtu_t = types.rtu_t @@ -94,13 +94,14 @@ local rtu_redstone = config.RTU_REDSTONE local rtu_devices = config.RTU_DEVICES -- redstone interfaces -for reactor_idx = 1, #rtu_redstone do +for entry_idx = 1, #rtu_redstone do local rs_rtu = redstone_rtu.new() - local io_table = rtu_redstone[reactor_idx].io + local io_table = rtu_redstone[entry_idx].io + local io_reactor = rtu_redstone[entry_idx].for_reactor local capabilities = {} - log.debug("init> starting redstone RTU I/O linking for reactor " .. rtu_redstone[reactor_idx].for_reactor .. "...") + log.debug("init> starting redstone RTU I/O linking for reactor " .. io_reactor .. "...") for i = 1, #io_table do local valid = false @@ -116,8 +117,8 @@ for reactor_idx = 1, #rtu_redstone do end if not valid then - local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. reactor_idx .. - " (for reactor " .. rtu_redstone[reactor_idx].for_reactor .. ")" + local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. entry_idx .. + " (for reactor " .. io_reactor .. ")" println_ts(message) log.warning(message) else @@ -140,7 +141,7 @@ for reactor_idx = 1, #rtu_redstone do table.insert(capabilities, conf.channel) log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. - ") for reactor " .. rtu_redstone[reactor_idx].for_reactor) + ") for reactor " .. io_reactor) end end @@ -148,8 +149,8 @@ for reactor_idx = 1, #rtu_redstone do local unit = { name = "redstone_io", type = rtu_t.redstone, - index = 1, - reactor = rtu_redstone[reactor_idx].for_reactor, + index = entry_idx, + reactor = io_reactor, device = capabilities, -- use device field for redstone channels rtu = rs_rtu, modbus_io = modbus.new(rs_rtu, false), @@ -160,7 +161,7 @@ for reactor_idx = 1, #rtu_redstone do table.insert(units, unit) - log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) + log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. io_reactor) end -- mounted peripherals diff --git a/rtu/threads.lua b/rtu/threads.lua index 27f5c27..617f3d2 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -23,7 +23,7 @@ local print_ts = util.print_ts local println_ts = util.println_ts local MAIN_CLOCK = 2 -- (2Hz, 40 ticks) -local COMMS_SLEEP = 150 -- (150ms, 3 ticks) +local COMMS_SLEEP = 100 -- (100ms, 2 ticks) -- main thread ---@param smem rtu_shared_memory From 94931ef5a216462cfc597c58d65164e6f6a80cd3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 14 May 2022 20:27:06 -0400 Subject: [PATCH 134/168] #8 bugfixes, redstone RTU session --- scada-common/rsio.lua | 3 +- supervisor/session/rtu.lua | 73 ++++++----- supervisor/session/rtu/boiler.lua | 12 +- supervisor/session/rtu/emachine.lua | 8 +- supervisor/session/rtu/redstone.lua | 183 ++++++++++++++++++++++++++++ supervisor/session/rtu/turbine.lua | 12 +- 6 files changed, 246 insertions(+), 45 deletions(-) create mode 100644 supervisor/session/rtu/redstone.lua diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index d71d777..92e1713 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -11,7 +11,8 @@ local rsio = {} ---@alias IO_LVL integer local IO_LVL = { LOW = 0, - HIGH = 1 + HIGH = 1, + DISCONNECT = 2 -- use for RTU session to indicate this RTU is not connected to this channel } ---@alias IO_DIR integer diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 5dab40c..fab3a96 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -6,6 +6,7 @@ local util = require("scada-common.util") -- supervisor rtu sessions (svrs) local svrs_boiler = require("supervisor.session.rtu.boiler") local svrs_emachine = require("supervisor.session.rtu.emachine") +local svrs_redstone = require("supervisor.session.rtu.redstone") local svrs_turbine = require("supervisor.session.rtu.turbine") local rtu = {} @@ -19,10 +20,26 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts +local RTU_S_CMDS = { +} + +local RTU_S_DATA = { + RS_COMMAND = 1, + UNIT_COMMAND = 2 +} + +rtu.RTU_S_CMDS = RTU_S_CMDS +rtu.RTU_S_DATA = RTU_S_DATA + local PERIODICS = { KEEP_ALIVE = 2.0 } +---@class rs_session_command +---@field reactor integer +---@field channel RS_IO +---@field active boolean + -- create a new RTU session ---@param id integer ---@param in_queue mqueue @@ -42,31 +59,32 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) connected = true, rtu_conn_watchdog = util.new_watchdog(3), last_rtt = 0, + rs_io = {}, units = {} } ---@class rtu_session local public = {} - -- parse the recorded advertisement - local _parse_advertisement = function () + -- parse the recorded advertisement and create unit sub-sessions + local _handle_advertisement = function () self.units = {} for i = 1, #self.advert do - local unit = nil ---@type unit_session + local unit = nil ---@type unit_session|nil ---@type rtu_advertisement local unit_advert = { - type = self.advert[i][0], - index = self.advert[i][1], - reactor = self.advert[i][2], - rsio = self.advert[i][3] + type = self.advert[i][1], + index = self.advert[i][2], + reactor = self.advert[i][3], + rsio = self.advert[i][4] } local u_type = unit_advert.type -- create unit by type if u_type == RTU_UNIT_TYPES.REDSTONE then - + unit = svrs_redstone.new(self.id, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER then unit = svrs_boiler.new(self.id, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then @@ -79,27 +97,20 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) unit = svrs_emachine.new(self.id, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.IMATRIX then -- @todo Mekanism 10.1+ + else + log.error(log_header .. "bad advertisement: encountered unsupported RTU type") end if unit ~= nil then table.insert(self.units, unit) else self.units = {} - log.error(log_header .. "bad advertisement; encountered unsupported RTU type") + log.error(log_header .. "bad advertisement: error occured while creating a unit") break end end end - -- send a MODBUS TCP packet - ---@param m_pkt modbus_packet - local _send_modbus = function (m_pkt) - local s_pkt = comms.scada_packet() - s_pkt.make(self.seq_num, PROTOCOLS.MODBUS_TCP, m_pkt.raw_sendable()) - self.modem.transmit(self.s_port, self.l_port, s_pkt.raw_sendable()) - self.seq_num = self.seq_num + 1 - end - -- send a SCADA management packet ---@param msg_type SCADA_MGMT_TYPES ---@param msg table @@ -137,7 +148,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) unit.handle_packet(pkt) end elseif pkt.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then - + -- handle management packet if pkt.type == SCADA_MGMT_TYPES.KEEP_ALIVE then -- keep alive reply if pkt.length == 2 then @@ -160,8 +171,9 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) self.connected = false elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- RTU unit advertisement + -- handle advertisement; this will re-create all unit sub-sessions self.advert = pkt.data - _parse_advertisement() + _handle_advertisement() else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end @@ -192,14 +204,6 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) ---@return boolean connected public.iterate = function () if self.connected then - ------------------ - -- update units -- - ------------------ - - for i = 1, #self.units do - self.units[i].update() - end - ------------------ -- handle queue -- ------------------ @@ -218,6 +222,11 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- handle instruction elseif msg.qtype == mqueue.TYPE.DATA then -- instruction with body + local cmd = msg.message ---@type queue_data + + if cmd.key == RTU_S_DATA.RS_COMMAND then + local rs_cmd = cmd.val ---@type rs_session_command + end end end @@ -236,6 +245,14 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) return self.connected end + ------------------ + -- update units -- + ------------------ + + for i = 1, #self.units do + self.units[i].update() + end + ---------------------- -- update periodics -- ---------------------- diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index 320d7e4..c0dd0e6 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -183,19 +183,19 @@ boiler.new = function (session_id, advert, out_queue) -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) - if not self.has_build and self.next_build_req <= time_now then + if not self.periodics.has_build and self.next_build_req <= time_now then _request_build() - self.next_build_req = time_now + PERIODICS.BUILD + self.periodics.next_build_req = time_now + PERIODICS.BUILD end - if self.next_state_req <= time_now then + if self.periodics.next_state_req <= time_now then _request_state() - self.next_state_req = time_now + PERIODICS.STATE + self.periodics.next_state_req = time_now + PERIODICS.STATE end - if self.next_tanks_req <= time_now then + if self.periodics.next_tanks_req <= time_now then _request_tanks() - self.next_tanks_req = time_now + PERIODICS.TANKS + self.periodics.next_tanks_req = time_now + PERIODICS.TANKS end end diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index 9428806..cdbf21e 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -133,14 +133,14 @@ emachine.new = function (session_id, advert, out_queue) -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) - if not self.has_build and self.next_build_req <= time_now then + if not self.has_build and self.periodics.next_build_req <= time_now then _request_build() - self.next_build_req = time_now + PERIODICS.BUILD + self.periodics.next_build_req = time_now + PERIODICS.BUILD end - if self.next_storage_req <= time_now then + if self.periodics.next_storage_req <= time_now then _request_storage() - self.next_storage_req = time_now + PERIODICS.STORAGE + self.periodics.next_storage_req = time_now + PERIODICS.STORAGE end end diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua new file mode 100644 index 0000000..1e1c1b9 --- /dev/null +++ b/supervisor/session/rtu/redstone.lua @@ -0,0 +1,183 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local rsio = require("scada-common.rsio") +local types = require("scada-common.types") + +local txnctrl = require("supervisor.session.rtu.txnctrl") + +local redstone = {} + +local PROTOCOLS = comms.PROTOCOLS +local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES +local MODBUS_FCODE = types.MODBUS_FCODE + +local RS_IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_DIR = rsio.IO_DIR +local IO_MODE = rsio.IO_MODE + +local rtu_t = types.rtu_t + +local TXN_TYPES = { + DI_READ = 0, + INPUT_REG_READ = 1 +} + +local PERIODICS = { + INPUT_READ = 200 +} + +-- create a new redstone rtu session runner +---@param session_id integer +---@param advert rtu_advertisement +---@param out_queue mqueue +redstone.new = function (session_id, advert, out_queue) + -- type check + if advert.type ~= RTU_UNIT_TYPES.REDSTONE then + log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.") + return nil + end + + local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. advert.index .. "): " + + local self = { + uid = advert.index, + reactor = advert.reactor, + out_q = out_queue, + transaction_controller = txnctrl.new(), + has_di = false, + has_ai = false, + periodics = { + next_di_req = 0, + next_ir_req = 0, + }, + io_list = { + digital_in = {}, -- discrete inputs + digital_out = {}, -- coils + analog_in = {}, -- input registers + analog_out = {} -- holding registers + }, + db = {} + } + + ---@class unit_session + local public = {} + + -- INITIALIZE -- + + for _ = 1, #RS_IO do + table.insert(self.db, IO_LVL.DISCONNECT) + end + + for i = 1, #advert.rsio do + local channel = advert.rsio[i] + local mode = rsio.get_io_mode(channel) + + if mode == IO_MODE.DIGITAL_IN then + self.has_di = true + table.insert(self.io_list.digital_in, channel) + elseif mode == IO_MODE.DIGITAL_OUT then + table.insert(self.io_list.digital_out, channel) + elseif mode == IO_MODE.ANALOG_IN then + self.has_ai = true + table.insert(self.io_list.analog_in, channel) + elseif mode == IO_MODE.ANALOG_OUT then + table.insert(self.io_list.analog_out, channel) + else + -- should be unreachable code, we already validated channels + log.error(log_tag .. "failed to identify advertisement channel IO mode (" .. channel .. ")", true) + return nil + end + + self.db[channel] = IO_LVL.LOW + end + + + -- PRIVATE FUNCTIONS -- + + local _send_request = function (txn_type, f_code, register_range) + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(txn_type) + + m_pkt.make(txn_id, self.uid, f_code, register_range) + + self.out_q.push_packet(m_pkt) + end + + -- query discrete inputs + local _request_discrete_inputs = function () + _send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) + end + + -- query input registers + local _request_input_registers = function () + _send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) + end + + -- PUBLIC FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame + public.handle_packet = function (m_pkt) + local success = false + + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + if txn_type == TXN_TYPES.DI_READ then + -- build response + if m_pkt.length == 1 then + self.db.build.max_energy = m_pkt.data[1] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.build)") + end + elseif txn_type == TXN_TYPES.INPUT_REG_READ then + -- storage response + if m_pkt.length == 3 then + self.db.storage.energy = m_pkt.data[1] + self.db.storage.energy_need = m_pkt.data[2] + self.db.storage.energy_fill = m_pkt.data[3] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.storage)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") + else + log.error(log_tag .. "unknown transaction type " .. txn_type) + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + return success + end + + public.get_uid = function () return self.uid end + public.get_reactor = function () return self.reactor end + public.get_db = function () return self.db end + + -- update this runner + ---@param time_now integer milliseconds + public.update = function (time_now) + if self.has_di then + if self.periodics.next_di_req <= time_now then + _request_discrete_inputs() + self.periodics.next_di_req = time_now + PERIODICS.BUILD + end + end + + if self.has_ai then + if self.periodics.next_ir_req <= time_now then + _request_input_registers() + self.periodics.next_ir_req = time_now + PERIODICS.STORAGE + end + end + end + + return public +end + +return redstone diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index 66429f3..292ff95 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -174,19 +174,19 @@ turbine.new = function (session_id, advert, out_queue) -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) - if not self.has_build and self.next_build_req <= time_now then + if not self.has_build and self.periodics.next_build_req <= time_now then _request_build() - self.next_build_req = time_now + PERIODICS.BUILD + self.periodics.next_build_req = time_now + PERIODICS.BUILD end - if self.next_state_req <= time_now then + if self.periodics.next_state_req <= time_now then _request_state() - self.next_state_req = time_now + PERIODICS.STATE + self.periodics.next_state_req = time_now + PERIODICS.STATE end - if self.next_tanks_req <= time_now then + if self.periodics.next_tanks_req <= time_now then _request_tanks() - self.next_tanks_req = time_now + PERIODICS.TANKS + self.periodics.next_tanks_req = time_now + PERIODICS.TANKS end end From 374bfb7a19c283e3ab12647d6a1d83ba7df74024 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 14 May 2022 20:42:28 -0400 Subject: [PATCH 135/168] #8 handle redstone RTU MODBUS replies, bugfixes --- supervisor/session/rtu/redstone.lua | 30 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 1e1c1b9..1477773 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -125,20 +125,26 @@ redstone.new = function (session_id, advert, out_queue) if m_pkt.unit_id == self.uid then local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) if txn_type == TXN_TYPES.DI_READ then - -- build response - if m_pkt.length == 1 then - self.db.build.max_energy = m_pkt.data[1] + -- discrete input read response + if m_pkt.length == #self.io_list.digital_in then + for i = 1, m_pkt.length do + local channel = self.io_list.digital_in[i] + local value = m_pkt.data[i] + self.db[channel] = value + end else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.build)") + log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.discrete_input_read)") end elseif txn_type == TXN_TYPES.INPUT_REG_READ then - -- storage response - if m_pkt.length == 3 then - self.db.storage.energy = m_pkt.data[1] - self.db.storage.energy_need = m_pkt.data[2] - self.db.storage.energy_fill = m_pkt.data[3] + -- input register read response + if m_pkt.length == #self.io_list.analog_in then + for i = 1, m_pkt.length do + local channel = self.io_list.analog_in[i] + local value = m_pkt.data[i] + self.db[channel] = value + end else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.storage)") + log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.input_reg_read)") end elseif txn_type == nil then log.error(log_tag .. "unknown transaction reply") @@ -165,14 +171,14 @@ redstone.new = function (session_id, advert, out_queue) if self.has_di then if self.periodics.next_di_req <= time_now then _request_discrete_inputs() - self.periodics.next_di_req = time_now + PERIODICS.BUILD + self.periodics.next_di_req = time_now + PERIODICS.INPUT_READ end end if self.has_ai then if self.periodics.next_ir_req <= time_now then _request_input_registers() - self.periodics.next_ir_req = time_now + PERIODICS.STORAGE + self.periodics.next_ir_req = time_now + PERIODICS.INPUT_READ end end end From 4834dbf7811fc707aa4ec3de4aaa1c828d7e5f71 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 10:38:47 -0400 Subject: [PATCH 136/168] changed redstone I/O capabilities, added analog read/write scaling functions --- scada-common/rsio.lua | 98 +++++++++++++++++++++++++++---------------- 1 file changed, 61 insertions(+), 37 deletions(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 92e1713..4acc3d7 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -23,10 +23,10 @@ local IO_DIR = { ---@alias IO_MODE integer local IO_MODE = { - DIGITAL_OUT = 0, - DIGITAL_IN = 1, - ANALOG_OUT = 2, - ANALOG_IN = 3 + DIGITAL_IN = 0, + DIGITAL_OUT = 1, + ANALOG_IN = 2, + ANALOG_OUT = 3 } ---@alias RS_IO integer @@ -35,37 +35,35 @@ local RS_IO = { -- facility F_SCRAM = 1, -- active low, facility-wide scram - F_AE2_LIVE = 2, -- active high, indicates whether AE2 network is online (hint: use redstone P2P) -- reactor - R_SCRAM = 3, -- active low, reactor scram - R_ENABLE = 4, -- active high, reactor enable + R_SCRAM = 2, -- active low, reactor scram + R_ENABLE = 3, -- active high, reactor enable -- digital outputs -- + -- facility + F_ALARM = 4, -- active high, facility safety alarm + -- waste WASTE_PO = 5, -- active low, polonium routing WASTE_PU = 6, -- active low, plutonium routing WASTE_AM = 7, -- active low, antimatter routing -- reactor - R_SCRAMMED = 8, -- active high, if the reactor is scrammed - R_AUTO_SCRAM = 9, -- active high, if the reactor was automatically scrammed - R_ACTIVE = 10, -- active high, if the reactor is active - R_AUTO_CTRL = 11, -- active high, if the reactor burn rate is automatic - R_DMG_CRIT = 12, -- active high, if the reactor damage is critical - R_HIGH_TEMP = 13, -- active high, if the reactor is at a high temperature - R_NO_COOLANT = 14, -- active high, if the reactor has no coolant - R_EXCESS_HC = 15, -- active high, if the reactor has excess heated coolant - R_EXCESS_WS = 16, -- active high, if the reactor has excess waste - R_INSUFF_FUEL = 17, -- active high, if the reactor has insufficent fuel - R_PLC_TIMEOUT = 18, -- active high, if the reactor PLC has not been heard from - - -- analog outputs -- - - A_R_BURN_RATE = 19, -- reactor burn rate percentage - A_B_BOIL_RATE = 20, -- boiler boil rate percentage - A_T_FLOW_RATE = 21 -- turbine flow rate percentage + R_ALARM = 8, -- active high, reactor safety alarm + R_SCRAMMED = 9, -- active high, if the reactor is scrammed + R_AUTO_SCRAM = 10, -- active high, if the reactor was automatically scrammed + R_ACTIVE = 11, -- active high, if the reactor is active + R_AUTO_CTRL = 12, -- active high, if the reactor burn rate is automatic + R_DMG_CRIT = 13, -- active high, if the reactor damage is critical + R_HIGH_TEMP = 14, -- active high, if the reactor is at a high temperature + R_NO_COOLANT = 15, -- active high, if the reactor has no coolant + R_EXCESS_HC = 16, -- active high, if the reactor has excess heated coolant + R_EXCESS_WS = 17, -- active high, if the reactor has excess waste + R_INSUFF_FUEL = 18, -- active high, if the reactor has insufficent fuel + R_PLC_FAULT = 19, -- active high, if the reactor PLC reports a device access fault + R_PLC_TIMEOUT = 20 -- active high, if the reactor PLC has not been heard from } rsio.IO_LVL = IO_LVL @@ -82,12 +80,13 @@ rsio.IO = RS_IO rsio.to_string = function (channel) local names = { "F_SCRAM", - "F_AE2_LIVE", "R_SCRAM", "R_ENABLE", + "F_ALARM", "WASTE_PO", "WASTE_PU", "WASTE_AM", + "R_ALARM", "R_SCRAMMED", "R_AUTO_SCRAM", "R_ACTIVE", @@ -98,10 +97,8 @@ rsio.to_string = function (channel) "R_EXCESS_HC", "R_EXCESS_WS", "R_INSUFF_FUEL", - "R_PLC_TIMEOUT", - "A_R_BURN_RATE", - "A_B_BOIL_RATE", - "A_T_FLOW_RATE" + "R_PLC_FAULT", + "R_PLC_TIMEOUT" } if channel > 0 and channel <= #names then @@ -124,18 +121,20 @@ local _DO_ACTIVE_LOW = function (on) return _TRINARY(on, IO_LVL.LOW, IO_LVL.HIGH local RS_DIO_MAP = { -- F_SCRAM { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, - -- F_AE2_LIVE - { _f = _DI_ACTIVE_HIGH, mode = IO_DIR.IN }, -- R_SCRAM { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, -- R_ENABLE { _f = _DI_ACTIVE_HIGH, mode = IO_DIR.IN }, + -- F_ALARM + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- WASTE_PO { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_PU { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_AM { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, + -- R_ALARM + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_SCRAMMED { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_SCRAM @@ -156,6 +155,8 @@ local RS_DIO_MAP = { { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_INSUFF_FUEL { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + -- R_PLC_FAULT + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_TIMEOUT { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT } } @@ -166,12 +167,13 @@ local RS_DIO_MAP = { rsio.get_io_mode = function (channel) local modes = { IO_MODE.DIGITAL_IN, -- F_SCRAM - IO_MODE.DIGITAL_IN, -- F_AE2_LIVE IO_MODE.DIGITAL_IN, -- R_SCRAM IO_MODE.DIGITAL_IN, -- R_ENABLE + IO_MODE.DIGITAL_OUT, -- F_ALARM IO_MODE.DIGITAL_OUT, -- WASTE_PO IO_MODE.DIGITAL_OUT, -- WASTE_PU IO_MODE.DIGITAL_OUT, -- WASTE_AM + IO_MODE.DIGITAL_OUT, -- R_ALARM IO_MODE.DIGITAL_OUT, -- R_SCRAMMED IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM IO_MODE.DIGITAL_OUT, -- R_ACTIVE @@ -182,10 +184,8 @@ rsio.get_io_mode = function (channel) IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL - IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT - IO_MODE.ANALOG_OUT, -- A_R_BURN_RATE - IO_MODE.ANALOG_OUT, -- A_B_BOIL_RATE - IO_MODE.ANALOG_OUT -- A_T_FLOW_RATE + IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT + IO_MODE.DIGITAL_OUT -- R_PLC_TIMEOUT } if channel > 0 and channel <= #modes then @@ -205,7 +205,7 @@ local RS_SIDES = rs.getSides() ---@param channel RS_IO ---@return boolean valid rsio.is_valid_channel = function (channel) - return (channel ~= nil) and (channel > 0) and (channel <= RS_IO.A_T_FLOW_RATE) + return (channel ~= nil) and (channel > 0) and (channel <= #RS_IO) end -- check if a side is valid @@ -266,4 +266,28 @@ rsio.digital_is_active = function (channel, level) end end +---------------- +-- ANALOG I/O -- +---------------- + +-- read an analog value scaled from min to max +---@param rs_value number redstone reading (0 to 15) +---@param min number minimum of range +---@param max number maximum of range +---@return number value scaled reading (min to max) +rsio.analog_read = function (rs_value, min, max) + local value = rs_value / 15 + return (value * (max - min)) + min +end + +-- write an analog value from the provided scale range +---@param value number value to write (from min to max range) +---@param min number minimum of range +---@param max number maximum of range +---@return number rs_value scaled redstone reading (0 to 15) +rsio.analog_write = function (value, min, max) + local scaled_value = (value - min) / (max - min) + return scaled_value * 15 +end + return rsio From 530a40b0aad66087ea4b87a9dc4103ba2aadb726 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 11:52:03 -0400 Subject: [PATCH 137/168] changed DISCONNECT constant value to -1 to not conflict with redstone values --- scada-common/rsio.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 4acc3d7..5b9970a 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -12,7 +12,7 @@ local rsio = {} local IO_LVL = { LOW = 0, HIGH = 1, - DISCONNECT = 2 -- use for RTU session to indicate this RTU is not connected to this channel + DISCONNECT = -1 -- use for RTU session to indicate this RTU is not connected to this channel } ---@alias IO_DIR integer From 11b89b679da7ec731d9167f73918d1534981bb4b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 11:54:34 -0400 Subject: [PATCH 138/168] #8 redstone RTU output commands --- supervisor/session/rtu.lua | 45 ++++++++++++-- supervisor/session/rtu/redstone.lua | 91 +++++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 9 deletions(-) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index fab3a96..59cf0b1 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -1,6 +1,7 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") +local rsio = require("scada-common.rsio") local util = require("scada-common.util") -- supervisor rtu sessions (svrs) @@ -38,7 +39,7 @@ local PERIODICS = { ---@class rs_session_command ---@field reactor integer ---@field channel RS_IO ----@field active boolean +---@field value integer|boolean -- create a new RTU session ---@param id integer @@ -59,7 +60,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) connected = true, rtu_conn_watchdog = util.new_watchdog(3), last_rtt = 0, - rs_io = {}, + rs_io_q = {}, units = {} } @@ -69,8 +70,11 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- parse the recorded advertisement and create unit sub-sessions local _handle_advertisement = function () self.units = {} + self.rs_io_q = {} + for i = 1, #self.advert do local unit = nil ---@type unit_session|nil + local rs_in_q = nil ---@type mqueue|nil ---@type rtu_advertisement local unit_advert = { @@ -84,7 +88,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- create unit by type if u_type == RTU_UNIT_TYPES.REDSTONE then - unit = svrs_redstone.new(self.id, unit_advert, self.out_q) + unit, rs_in_q = svrs_redstone.new(self.id, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER then unit = svrs_boiler.new(self.id, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then @@ -103,9 +107,23 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) if unit ~= nil then table.insert(self.units, unit) + + if self.rs_io_q[unit_advert.reactor] == nil then + self.rs_io_q[unit_advert.reactor] = rs_in_q + else + self.units = {} + self.rs_io_q = {} + log.error(log_header .. "bad advertisement: duplicate redstone RTU for reactor " .. unit_advert.reactor) + break + end else self.units = {} - log.error(log_header .. "bad advertisement: error occured while creating a unit") + self.rs_io_q = {} + + local type_string = comms.advert_type_to_rtu_t(u_type) + if type_string == nil then type_string = "unknown" end + + log.error(log_header .. "bad advertisement: error occured while creating a unit (type is " .. type_string .. ")") break end end @@ -226,6 +244,21 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) if cmd.key == RTU_S_DATA.RS_COMMAND then local rs_cmd = cmd.val ---@type rs_session_command + + if rsio.is_valid_channel(rs_cmd.channel) then + cmd.key = svrs_redstone.RS_RTU_S_DATA.RS_COMMAND + if rs_cmd.reactor == nil then + -- for all reactors (facility) + for i = 1, #self.rs_io_q do + local q = self.rs_io.q[i] ---@type mqueue + q.push_data(msg) + end + elseif self.rs_io_q[rs_cmd.reactor] ~= nil then + -- for just one reactor + local q = self.rs_io.q[rs_cmd.reactor] ---@type mqueue + q.push_data(msg) + end + end end end end @@ -249,8 +282,10 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- update units -- ------------------ + local time_now = util.time() + for i = 1, #self.units do - self.units[i].update() + self.units[i].update(time_now) end ---------------------- diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 1477773..4380567 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -1,7 +1,9 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") +local mqueue= require("scada-common.mqueue") local rsio = require("scada-common.rsio") local types = require("scada-common.types") +local util = require("scada-common.util") local txnctrl = require("supervisor.session.rtu.txnctrl") @@ -18,9 +20,21 @@ local IO_MODE = rsio.IO_MODE local rtu_t = types.rtu_t +local RS_RTU_S_CMDS = { +} + +local RS_RTU_S_DATA = { + RS_COMMAND = 1 +} + +redstone.RS_RTU_S_CMDS = RS_RTU_S_CMDS +redstone.RS_RTU_S_DATA = RS_RTU_S_DATA + local TXN_TYPES = { DI_READ = 0, - INPUT_REG_READ = 1 + COIL_WRITE = 1, + INPUT_REG_READ = 2, + HOLD_REG_WRITE = 3 } local PERIODICS = { @@ -43,6 +57,7 @@ redstone.new = function (session_id, advert, out_queue) local self = { uid = advert.index, reactor = advert.reactor, + in_q = mqueue.new(), out_q = out_queue, transaction_controller = txnctrl.new(), has_di = false, @@ -65,10 +80,12 @@ redstone.new = function (session_id, advert, out_queue) -- INITIALIZE -- + -- create all channels as disconnected for _ = 1, #RS_IO do table.insert(self.db, IO_LVL.DISCONNECT) end + -- setup I/O for i = 1, #advert.rsio do local channel = advert.rsio[i] local mode = rsio.get_io_mode(channel) @@ -95,11 +112,11 @@ redstone.new = function (session_id, advert, out_queue) -- PRIVATE FUNCTIONS -- - local _send_request = function (txn_type, f_code, register_range) + local _send_request = function (txn_type, f_code, parameters) local m_pkt = comms.modbus_packet() local txn_id = self.transaction_controller.create(txn_type) - m_pkt.make(txn_id, self.uid, f_code, register_range) + m_pkt.make(txn_id, self.uid, f_code, parameters) self.out_q.push_packet(m_pkt) end @@ -114,6 +131,16 @@ redstone.new = function (session_id, advert, out_queue) _send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) end + -- write coil output + local _write_coil = function (coil, value) + _send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, { coil, value }) + end + + -- write holding register output + local _write_holding_register = function (reg, value) + _send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, { reg, value }) + end + -- PUBLIC FUNCTIONS -- -- handle a packet @@ -168,6 +195,61 @@ redstone.new = function (session_id, advert, out_queue) -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) + -- check command queue + while self.in_q.ready() do + -- get a new message to process + local msg = self.in_q.pop() + + if msg ~= nil then + if msg.qtype == mqueue.TYPE.DATA then + -- instruction with body + local cmd = msg.message ---@type queue_data + if cmd.key == RS_RTU_S_DATA.RS_COMMAND then + local rs_cmd = cmd.val ---@type rs_session_command + + if self.db[rs_cmd.channel] ~= IO_LVL.DISCONNECT then + -- we have this as a connected channel + local mode = rsio.get_io_mode(rs_cmd.channel) + if mode == IO_MODE.DIGITAL_OUT then + -- record the value for retries + self.db[rs_cmd.channel] = rs_cmd.value + + -- find the coil address then write to it + for i = 0, #self.digital_out do + if self.digital_out[i] == rs_cmd.channel then + _write_coil(i, rs_cmd.value) + break + end + end + elseif mode == IO_MODE.ANALOG_OUT then + -- record the value for retries + self.db[rs_cmd.channel] = rs_cmd.value + + -- find the holding register address then write to it + for i = 0, #self.analog_out do + if self.analog_out[i] == rs_cmd.channel then + _write_holding_register(i, rs_cmd.value) + break + end + end + elseif mode ~= nil then + log.debug(log_tag .. "attemted write to non D/O or A/O mode " .. mode) + end + end + end + end + end + + -- max 100ms spent processing queue + if util.time() - time_now > 100 then + log.warning(log_tag .. "exceeded 100ms queue process limit") + break + end + end + + time_now = util.time() + + -- poll digital inputs if self.has_di then if self.periodics.next_di_req <= time_now then _request_discrete_inputs() @@ -175,6 +257,7 @@ redstone.new = function (session_id, advert, out_queue) end end + -- poll analog inputs if self.has_ai then if self.periodics.next_ir_req <= time_now then _request_input_registers() @@ -183,7 +266,7 @@ redstone.new = function (session_id, advert, out_queue) end end - return public + return public, self.in_q end return redstone From bdd8af18730e7b93433f62a693dcdc435105f577 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 12:50:51 -0400 Subject: [PATCH 139/168] dmesg logging --- scada-common/log.lua | 70 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index 71e4495..3ea4262 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -21,7 +21,8 @@ local LOG_DEBUG = true local _log_sys = { path = "/log.txt", mode = MODE.APPEND, - file = nil + file = nil, + dmesg_out = nil } ---@type function @@ -30,7 +31,8 @@ local free_space = fs.getFreeSpace -- initialize logger ---@param path string file path ---@param write_mode MODE -log.init = function (path, write_mode) +---@param dmesg_redirect? table terminal/window to direct dmesg to +log.init = function (path, write_mode, dmesg_redirect) _log_sys.path = path _log_sys.mode = write_mode @@ -39,6 +41,12 @@ log.init = function (path, write_mode) else _log_sys.file = fs.open(path, "w+") end + + if dmesg_redirect then + _log_sys.dmesg_out = dmesg_redirect + else + _log_sys.dmesg_out = term.current() + end end -- private log write function @@ -76,6 +84,64 @@ local _log = function (msg) end end +-- write a message to the dmesg output +---@param msg string message to write +local _write = function (msg) + local out = _log_sys.dmesg_out + local out_w, out_h = out.getSize() + + local lines = { msg } + + -- wrap if needed + if string.len(msg) > out_w then + local remaining = true + local s_start = 1 + local s_end = out_w + local i = 1 + + lines = {} + + while remaining do + local line = string.sub(msg, s_start, s_end) + + if line == "" then + remaining = false + else + lines[i] = line + + s_start = s_end + 1 + s_end = s_end + out_w + i = i + 1 + end + end + end + + -- output message + for i = 1, #lines do + local cur_x, cur_y = out.getCursorPos() + + if cur_x > 1 then + if cur_y == out_h then + out.scroll(1) + out.setCursorPos(1, cur_y) + else + out.setCursorPos(1, cur_y + 1) + end + end + + out.write(lines[i]) + end +end + +-- dmesg style logging for boot because I like linux-y things +---@param msg string message +---@param show_term? boolean whether or not to show on terminal output +log.dmesg = function (msg, show_term) + local message = string.format("[%10.3f] ", os.clock()) .. msg + if show_term then _write(message) end + _log(message) +end + -- log debug messages ---@param msg string message ---@param trace? boolean include file trace From 136b09d7f2999ddf95bb77f244215fc7f50e4a11 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 17:11:46 -0400 Subject: [PATCH 140/168] util filter table --- scada-common/util.lua | 27 +++++++++++++++++++++++++++ supervisor/session/rtu/txnctrl.lua | 17 +---------------- supervisor/session/svsessions.lua | 21 +++++---------------- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index 8dffd40..38cfc1f 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -80,6 +80,33 @@ util.adaptive_delay = function (target_timing, last_update) return util.time() end +-- TABLE UTILITIES -- + +-- delete elements from a table if the passed function returns false when passed a table element +-- +-- put briefly: deletes elements that return false, keeps elements that return true +---@param t table table to remove elements from +---@param f function should return false to delete an element when passed the element: f(elem) = true|false +---@param on_delete? function optional function to execute on deletion, passed the table element to be deleted as the parameter +util.filter_table = function (t, f, on_delete) + local move_to = 1 + for i = 1, #t do + local element = t[i] + if element ~= nil then + if f(element) then + if t[move_to] == nil then + t[move_to] = element + t[i] = nil + end + move_to = move_to + 1 + else + if on_delete then on_delete(element) end + t[i] = nil + end + end + end +end + -- MEKANISM POWER -- -- function kFE(fe) return fe / 1000 end diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua index 3d74484..2d6be2e 100644 --- a/supervisor/session/rtu/txnctrl.lua +++ b/supervisor/session/rtu/txnctrl.lua @@ -66,22 +66,7 @@ txnctrl.new = function () -- close timed-out transactions public.cleanup = function () local now = util.time() - - local move_to = 1 - for i = 1, public.length() do - local txn = self.list[i] - if txn ~= nil then - if txn.expiry <= now then - self.list[i] = nil - else - if self.list[move_to] == nil then - self.list[move_to] = txn - self.list[i] = nil - end - move_to = move_to + 1 - end - end - end + util.filter_table(self.list, function (txn) return txn.expiry > now end) end -- clear the transaction list diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 88c57cd..d0b76cc 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -1,5 +1,6 @@ local log = require("scada-common.log") local mqueue = require("scada-common.mqueue") +local util = require("scada-common.util") local coordinator = require("supervisor.session.coordinator") local plc = require("supervisor.session.plc") @@ -99,22 +100,10 @@ end -- delete any closed sessions ---@param sessions table local function _free_closed(sessions) - local move_to = 1 - for i = 1, #sessions do - local session = sessions[i] ---@type plc_session_struct - if session ~= nil then - if session.open then - if sessions[move_to] == nil then - sessions[move_to] = session - sessions[i] = nil - end - move_to = move_to + 1 - else - log.debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) - sessions[i] = nil - end - end - end + local f = function (session) return session.open end + local on_delete = function (session) log.debug("free'ing closed session " .. session.instance.get_id() .. " on remote port " .. session.r_port) end + + util.filter_table(sessions, f, on_delete) end -- PUBLIC FUNCTIONS -- From 0eff8a3e6ab47f1065d78acbea9e3a87b96b3ea1 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 16 May 2022 17:13:54 -0400 Subject: [PATCH 141/168] #8 cleaned up closing logic, added connected flags to RTU unit sessions --- supervisor/session/plc.lua | 14 +++++++++----- supervisor/session/rtu.lua | 19 ++++++++++++++----- supervisor/session/rtu/boiler.lua | 4 ++++ supervisor/session/rtu/emachine.lua | 4 ++++ supervisor/session/rtu/redstone.lua | 4 ++++ supervisor/session/rtu/turbine.lua | 4 ++++ 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index c36dbda..f799987 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -202,6 +202,12 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) self.sDB.mek_struct.max_burn = mek_data[8] end + -- mark this PLC session as closed, stop watchdog + local _close = function () + self.rtu_conn_watchdog.cancel() + self.connected = false + end + -- send an RPLC packet ---@param msg_type RPLC_TYPES ---@param msg table @@ -392,7 +398,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then -- close the session - self.connected = false + _close() else log.debug(log_header .. "handler received unsupported SCADA_MGMT packet type " .. pkt.type) end @@ -427,13 +433,12 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- check if a timer matches this session's watchdog public.check_wd = function (timer) - return self.plc_conn_watchdog.is_timer(timer) + return self.plc_conn_watchdog.is_timer(timer) and self.connected end -- close the connection public.close = function () - self.plc_conn_watchdog.cancel() - self.connected = false + _close() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) println("connection to reactor " .. self.for_reactor .. " PLC closed by server") log.info(log_header .. "session closed by server") @@ -506,7 +511,6 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- exit if connection was closed if not self.connected then - self.plc_conn_watchdog.cancel() println("connection to reactor " .. self.for_reactor .. " PLC closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 59cf0b1..33a66d7 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -129,6 +129,17 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) end end + -- mark this RTU session as closed, stop watchdog + local _close = function () + self.rtu_conn_watchdog.cancel() + self.connected = false + + -- mark all RTU unit sessions as closed so the reactor unit knows + for i = 1, #self.units do + self.units[i].close() + end + end + -- send a SCADA management packet ---@param msg_type SCADA_MGMT_TYPES ---@param msg table @@ -186,7 +197,7 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) end elseif pkt.type == SCADA_MGMT_TYPES.CLOSE then -- close the session - self.connected = false + _close() elseif pkt.type == SCADA_MGMT_TYPES.RTU_ADVERT then -- RTU unit advertisement -- handle advertisement; this will re-create all unit sub-sessions @@ -206,13 +217,12 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- check if a timer matches this session's watchdog ---@param timer number public.check_wd = function (timer) - return self.rtu_conn_watchdog.is_timer(timer) + return self.rtu_conn_watchdog.is_timer(timer) and self.connected end -- close the connection public.close = function () - self.rtu_conn_watchdog.cancel() - self.connected = false + _close() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) println(log_header .. "connection to RTU closed by server") log.info(log_header .. "session closed by server") @@ -272,7 +282,6 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- exit if connection was closed if not self.connected then - self.rtu_conn_watchdog.cancel() println(log_header .. "connection to RTU closed by remote host") log.info(log_header .. "session closed by remote host") return self.connected diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index c0dd0e6..2b8047f 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -42,6 +42,7 @@ boiler.new = function (session_id, advert, out_queue) reactor = advert.reactor, out_q = out_queue, transaction_controller = txnctrl.new(), + connected = true, has_build = false, periodics = { next_build_req = 0, @@ -180,6 +181,9 @@ boiler.new = function (session_id, advert, out_queue) public.get_reactor = function () return self.reactor end public.get_db = function () return self.db end + public.close = function () self.connected = false end + public.is_connected = function () return self.connected end + -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index cdbf21e..8e2a2e8 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -41,6 +41,7 @@ emachine.new = function (session_id, advert, out_queue) reactor = 0, out_q = out_queue, transaction_controller = txnctrl.new(), + connected = true, has_build = false, periodics = { next_build_req = 0, @@ -130,6 +131,9 @@ emachine.new = function (session_id, advert, out_queue) public.get_reactor = function () return self.reactor end public.get_db = function () return self.db end + public.close = function () self.connected = false end + public.is_connected = function () return self.connected end + -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 4380567..b5f44ea 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -60,6 +60,7 @@ redstone.new = function (session_id, advert, out_queue) in_q = mqueue.new(), out_q = out_queue, transaction_controller = txnctrl.new(), + connected = true, has_di = false, has_ai = false, periodics = { @@ -192,6 +193,9 @@ redstone.new = function (session_id, advert, out_queue) public.get_reactor = function () return self.reactor end public.get_db = function () return self.db end + public.close = function () self.connected = false end + public.is_connected = function () return self.connected end + -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index 292ff95..e6d41c2 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -43,6 +43,7 @@ turbine.new = function (session_id, advert, out_queue) reactor = advert.reactor, out_q = out_queue, transaction_controller = txnctrl.new(), + connected = true, has_build = false, periodics = { next_build_req = 0, @@ -171,6 +172,9 @@ turbine.new = function (session_id, advert, out_queue) public.get_reactor = function () return self.reactor end public.get_db = function () return self.db end + public.close = function () self.connected = false end + public.is_connected = function () return self.connected end + -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) From 31ede51c42ff0fc60d94be9f859b68f39479494a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 17 May 2022 10:35:55 -0400 Subject: [PATCH 142/168] still queue packets if RTU is busy, determine busy state by queue length rather than flag --- rtu/rtu.lua | 20 +++++++++++++------- rtu/startup.lua | 4 +--- rtu/threads.lua | 2 -- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index de96c66..d02bc6c 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -336,30 +336,36 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) -- handle MODBUS instruction if packet.unit_id <= #units then local unit = units[packet.unit_id] ---@type rtu_unit_registry_entry + local unit_dbg_tag = " (unit " .. packet.unit_id .. ")" + if unit.name == "redstone_io" then -- immediately execute redstone RTU requests return_code, reply = unit.modbus_io.handle_packet(packet) if not return_code then - log.warning("requested MODBUS operation failed") + log.warning("requested MODBUS operation failed" .. unit_dbg_tag) end else -- check validity then pass off to unit comms thread return_code, reply = unit.modbus_io.check_request(packet) if return_code then - -- check if an operation is already in progress for this unit - if unit.modbus_busy then + -- check if there are more than 3 active transactions + -- still queue the packet, but this may indicate a problem + if unit.pkt_queue.length() > 3 then reply = unit.modbus_io.reply__srv_device_busy(packet) - else - unit.pkt_queue.push_packet(packet) + log.debug("queueing new request with " .. unit.pkt_queue.length() .. + " transactions already in the queue" .. unit_dbg_tag) end + + -- always queue the command even if busy + unit.pkt_queue.push_packet(packet) else - log.warning("cannot perform requested MODBUS operation") + log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag) end end else -- unit ID out of range? reply = modbus.reply__gw_unavailable(packet) - log.error("MODBUS packet requesting non-existent unit") + log.error("received MODBUS packet for non-existent unit") end public.send_modbus(reply) diff --git a/rtu/startup.lua b/rtu/startup.lua index 654f7a4..8293d39 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.5" +local RTU_VERSION = "alpha-v0.6.6" local rtu_t = types.rtu_t @@ -154,7 +154,6 @@ for entry_idx = 1, #rtu_redstone do device = capabilities, -- use device field for redstone channels rtu = rs_rtu, modbus_io = modbus.new(rs_rtu, false), - modbus_busy = false, pkt_queue = nil, thread = nil } @@ -218,7 +217,6 @@ for i = 1, #rtu_devices do device = device, rtu = rtu_iface, modbus_io = modbus.new(rtu_iface, true), - modbus_busy = false, pkt_queue = mqueue.new(), thread = nil } diff --git a/rtu/threads.lua b/rtu/threads.lua index 617f3d2..a3d19d7 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -236,10 +236,8 @@ threads.thread__unit_comms = function (smem, unit) -- received data elseif msg.qtype == mqueue.TYPE.PACKET then -- received a packet - unit.modbus_busy = true local _, reply = unit.modbus_io.handle_packet(msg.message) rtu_comms.send_modbus(reply) - unit.modbus_busy = false end end From 9c034c366b7bd30c4d8f8c89bd06b92c506f5a8b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 17 May 2022 17:16:04 -0400 Subject: [PATCH 143/168] #8 base class for RTU unit sessions, handle MODBUS error responses --- supervisor/session/rtu/boiler.lua | 152 ++++++++++-------------- supervisor/session/rtu/emachine.lua | 97 ++++++--------- supervisor/session/rtu/redstone.lua | 120 ++++++++----------- supervisor/session/rtu/turbine.lua | 137 +++++++++------------ supervisor/session/rtu/txnctrl.lua | 13 +- supervisor/session/rtu/unit_session.lua | 132 ++++++++++++++++++++ supervisor/startup.lua | 2 +- 7 files changed, 349 insertions(+), 304 deletions(-) create mode 100644 supervisor/session/rtu/unit_session.lua diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index 2b8047f..c45e22c 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -2,20 +2,23 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") -local txnctrl = require("supervisor.session.rtu.txnctrl") +local unit_session = require("supervisor.session.rtu.unit_session") local boiler = {} -local PROTOCOLS = comms.PROTOCOLS local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE -local rtu_t = types.rtu_t - local TXN_TYPES = { - BUILD = 0, - STATE = 1, - TANKS = 2 + BUILD = 1, + STATE = 2, + TANKS = 3 +} + +local TXN_TAGS = { + "boiler.build", + "boiler.state", + "boiler.tanks", } local PERIODICS = { @@ -38,11 +41,7 @@ boiler.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").boiler(" .. advert.index .. "): " local self = { - uid = advert.index, - reactor = advert.reactor, - out_q = out_queue, - transaction_controller = txnctrl.new(), - connected = true, + session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, @@ -81,36 +80,26 @@ boiler.new = function (session_id, advert, out_queue) } } - ---@class unit_session - local public = {} + local public = self.session.get() -- PRIVATE FUNCTIONS -- - local _send_request = function (txn_type, f_code, register_range) - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(txn_type) - - m_pkt.make(txn_id, self.uid, f_code, register_range) - - self.out_q.push_packet(m_pkt) - end - -- query the build of the device local _request_build = function () -- read input registers 1 through 7 (start = 1, count = 7) - _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) + self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) end -- query the state of the device local _request_state = function () -- read input registers 8 through 9 (start = 8, count = 2) - _send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) + self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) end -- query the tanks of the device local _request_tanks = function () -- read input registers 10 through 21 (start = 10, count = 12) - _send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) + self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 10, 12 }) end -- PUBLIC FUNCTIONS -- @@ -118,76 +107,62 @@ boiler.new = function (session_id, advert, out_queue) -- handle a packet ---@param m_pkt modbus_frame public.handle_packet = function (m_pkt) - local success = false - - if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.uid then - local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - if txn_type == TXN_TYPES.BUILD then - -- build response - if m_pkt.length == 7 then - self.db.build.boil_cap = m_pkt.data[1] - self.db.build.steam_cap = m_pkt.data[2] - self.db.build.water_cap = m_pkt.data[3] - self.db.build.hcoolant_cap = m_pkt.data[4] - self.db.build.ccoolant_cap = m_pkt.data[5] - self.db.build.superheaters = m_pkt.data[6] - self.db.build.max_boil_rate = m_pkt.data[7] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)") - end - elseif txn_type == TXN_TYPES.STATE then - -- state response - if m_pkt.length == 2 then - self.db.state.temperature = m_pkt.data[1] - self.db.state.boil_rate = m_pkt.data[2] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)") - end - elseif txn_type == TXN_TYPES.TANKS then - -- tanks response - if m_pkt.length == 12 then - self.db.tanks.steam = m_pkt.data[1] - self.db.tanks.steam_need = m_pkt.data[2] - self.db.tanks.steam_fill = m_pkt.data[3] - self.db.tanks.water = m_pkt.data[4] - self.db.tanks.water_need = m_pkt.data[5] - self.db.tanks.water_fill = m_pkt.data[6] - self.db.tanks.hcool = m_pkt.data[7] - self.db.tanks.hcool_need = m_pkt.data[8] - self.db.tanks.hcool_fill = m_pkt.data[9] - self.db.tanks.ccool = m_pkt.data[10] - self.db.tanks.ccool_need = m_pkt.data[11] - self.db.tanks.ccool_fill = m_pkt.data[12] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)") - end - elseif txn_type == nil then - log.error(log_tag .. "unknown transaction reply") - else - log.error(log_tag .. "unknown transaction type " .. txn_type) - end + local txn_type = self.session.try_resolve(m_pkt.txn_id) + if txn_type == false then + -- nothing to do + elseif txn_type == TXN_TYPES.BUILD then + -- build response + -- load in data if correct length + if m_pkt.length == 7 then + self.db.build.boil_cap = m_pkt.data[1] + self.db.build.steam_cap = m_pkt.data[2] + self.db.build.water_cap = m_pkt.data[3] + self.db.build.hcoolant_cap = m_pkt.data[4] + self.db.build.ccoolant_cap = m_pkt.data[5] + self.db.build.superheaters = m_pkt.data[6] + self.db.build.max_boil_rate = m_pkt.data[7] else - log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.build)") end + elseif txn_type == TXN_TYPES.STATE then + -- state response + -- load in data if correct length + if m_pkt.length == 2 then + self.db.state.temperature = m_pkt.data[1] + self.db.state.boil_rate = m_pkt.data[2] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.state)") + end + elseif txn_type == TXN_TYPES.TANKS then + -- tanks response + -- load in data if correct length + if m_pkt.length == 12 then + self.db.tanks.steam = m_pkt.data[1] + self.db.tanks.steam_need = m_pkt.data[2] + self.db.tanks.steam_fill = m_pkt.data[3] + self.db.tanks.water = m_pkt.data[4] + self.db.tanks.water_need = m_pkt.data[5] + self.db.tanks.water_fill = m_pkt.data[6] + self.db.tanks.hcool = m_pkt.data[7] + self.db.tanks.hcool_need = m_pkt.data[8] + self.db.tanks.hcool_fill = m_pkt.data[9] + self.db.tanks.ccool = m_pkt.data[10] + self.db.tanks.ccool_need = m_pkt.data[11] + self.db.tanks.ccool_fill = m_pkt.data[12] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (boiler.tanks)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") else - log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + log.error(log_tag .. "unknown transaction type " .. txn_type) end - - return success end - public.get_uid = function () return self.uid end - public.get_reactor = function () return self.reactor end - public.get_db = function () return self.db end - - public.close = function () self.connected = false end - public.is_connected = function () return self.connected end - -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) - if not self.periodics.has_build and self.next_build_req <= time_now then + if not self.periodics.has_build and self.periodics.next_build_req <= time_now then _request_build() self.periodics.next_build_req = time_now + PERIODICS.BUILD end @@ -203,6 +178,9 @@ boiler.new = function (session_id, advert, out_queue) end end + -- get the unit session database + public.get_db = function () return self.db end + return public end diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index 8e2a2e8..c9c6481 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -2,19 +2,21 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") -local txnctrl = require("supervisor.session.rtu.txnctrl") +local unit_session = require("supervisor.session.rtu.unit_session") local emachine = {} -local PROTOCOLS = comms.PROTOCOLS local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE -local rtu_t = types.rtu_t - local TXN_TYPES = { - BUILD = 0, - STORAGE = 1 + BUILD = 1, + STORAGE = 2 +} + +local TXN_TAGS = { + "emachine.build", + "emachine.storage" } local PERIODICS = { @@ -36,12 +38,7 @@ emachine.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").emachine(" .. advert.index .. "): " local self = { - uid = advert.index, - -- reactor = advert.reactor, - reactor = 0, - out_q = out_queue, - transaction_controller = txnctrl.new(), - connected = true, + session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, @@ -60,30 +57,20 @@ emachine.new = function (session_id, advert, out_queue) } } - ---@class unit_session - local public = {} + local public = self.session.get() -- PRIVATE FUNCTIONS -- - local _send_request = function (txn_type, f_code, register_range) - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(txn_type) - - m_pkt.make(txn_id, self.uid, f_code, register_range) - - self.out_q.push_packet(m_pkt) - end - -- query the build of the device local _request_build = function () -- read input register 1 (start = 1, count = 1) - _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 1 }) + self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 1 }) end -- query the state of the energy storage local _request_storage = function () -- read input registers 2 through 4 (start = 2, count = 3) - _send_request(TXN_TYPES.STORAGE, MODBUS_FCODE.READ_INPUT_REGS, { 2, 3 }) + self.session.send_request(TXN_TYPES.STORAGE, MODBUS_FCODE.READ_INPUT_REGS, { 2, 3 }) end -- PUBLIC FUNCTIONS -- @@ -91,49 +78,32 @@ emachine.new = function (session_id, advert, out_queue) -- handle a packet ---@param m_pkt modbus_frame public.handle_packet = function (m_pkt) - local success = false - - if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.uid then - local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - if txn_type == TXN_TYPES.BUILD then - -- build response - if m_pkt.length == 1 then - self.db.build.max_energy = m_pkt.data[1] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.build)") - end - elseif txn_type == TXN_TYPES.STORAGE then - -- storage response - if m_pkt.length == 3 then - self.db.storage.energy = m_pkt.data[1] - self.db.storage.energy_need = m_pkt.data[2] - self.db.storage.energy_fill = m_pkt.data[3] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.storage)") - end - elseif txn_type == nil then - log.error(log_tag .. "unknown transaction reply") - else - log.error(log_tag .. "unknown transaction type " .. txn_type) - end + local txn_type = self.session.try_resolve(m_pkt.txn_id) + if txn_type == false then + -- nothing to do + elseif txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 1 then + self.db.build.max_energy = m_pkt.data[1] else - log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.build)") end + elseif txn_type == TXN_TYPES.STORAGE then + -- storage response + if m_pkt.length == 3 then + self.db.storage.energy = m_pkt.data[1] + self.db.storage.energy_need = m_pkt.data[2] + self.db.storage.energy_fill = m_pkt.data[3] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (emachine.storage)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") else - log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + log.error(log_tag .. "unknown transaction type " .. txn_type) end - - return success end - public.get_uid = function () return self.uid end - public.get_reactor = function () return self.reactor end - public.get_db = function () return self.db end - - public.close = function () self.connected = false end - public.is_connected = function () return self.connected end - -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) @@ -148,6 +118,9 @@ emachine.new = function (session_id, advert, out_queue) end end + -- get the unit session database + public.get_db = function () return self.db end + return public end diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index b5f44ea..57848d9 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -5,11 +5,10 @@ local rsio = require("scada-common.rsio") local types = require("scada-common.types") local util = require("scada-common.util") -local txnctrl = require("supervisor.session.rtu.txnctrl") +local unit_session = require("supervisor.session.rtu.unit_session") local redstone = {} -local PROTOCOLS = comms.PROTOCOLS local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local MODBUS_FCODE = types.MODBUS_FCODE @@ -18,8 +17,6 @@ local IO_LVL = rsio.IO_LVL local IO_DIR = rsio.IO_DIR local IO_MODE = rsio.IO_MODE -local rtu_t = types.rtu_t - local RS_RTU_S_CMDS = { } @@ -31,10 +28,17 @@ redstone.RS_RTU_S_CMDS = RS_RTU_S_CMDS redstone.RS_RTU_S_DATA = RS_RTU_S_DATA local TXN_TYPES = { - DI_READ = 0, - COIL_WRITE = 1, - INPUT_REG_READ = 2, - HOLD_REG_WRITE = 3 + DI_READ = 1, + COIL_WRITE = 2, + INPUT_REG_READ = 3, + HOLD_REG_WRITE = 4 +} + +local TXN_TAGS = { + "redstone.di_read", + "redstone.coil_write", + "redstone.input_reg_write", + "redstone.hold_reg_write" } local PERIODICS = { @@ -55,12 +59,7 @@ redstone.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. advert.index .. "): " local self = { - uid = advert.index, - reactor = advert.reactor, - in_q = mqueue.new(), - out_q = out_queue, - transaction_controller = txnctrl.new(), - connected = true, + session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), has_di = false, has_ai = false, periodics = { @@ -76,8 +75,7 @@ redstone.new = function (session_id, advert, out_queue) db = {} } - ---@class unit_session - local public = {} + local public = self.session.get() -- INITIALIZE -- @@ -110,36 +108,26 @@ redstone.new = function (session_id, advert, out_queue) self.db[channel] = IO_LVL.LOW end - -- PRIVATE FUNCTIONS -- - local _send_request = function (txn_type, f_code, parameters) - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(txn_type) - - m_pkt.make(txn_id, self.uid, f_code, parameters) - - self.out_q.push_packet(m_pkt) - end - -- query discrete inputs local _request_discrete_inputs = function () - _send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) + self.session.send_request(TXN_TYPES.DI_READ, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, #self.io_list.digital_in }) end -- query input registers local _request_input_registers = function () - _send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) + self.session.send_request(TXN_TYPES.INPUT_REG_READ, MODBUS_FCODE.READ_INPUT_REGS, { 1, #self.io_list.analog_in }) end -- write coil output local _write_coil = function (coil, value) - _send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, { coil, value }) + self.session.send_request(TXN_TYPES.COIL_WRITE, MODBUS_FCODE.WRITE_MUL_COILS, { coil, value }) end -- write holding register output local _write_holding_register = function (reg, value) - _send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, { reg, value }) + self.session.send_request(TXN_TYPES.HOLD_REG_WRITE, MODBUS_FCODE.WRITE_MUL_HOLD_REGS, { reg, value }) end -- PUBLIC FUNCTIONS -- @@ -147,55 +135,40 @@ redstone.new = function (session_id, advert, out_queue) -- handle a packet ---@param m_pkt modbus_frame public.handle_packet = function (m_pkt) - local success = false - - if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.uid then - local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - if txn_type == TXN_TYPES.DI_READ then - -- discrete input read response - if m_pkt.length == #self.io_list.digital_in then - for i = 1, m_pkt.length do - local channel = self.io_list.digital_in[i] - local value = m_pkt.data[i] - self.db[channel] = value - end - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.discrete_input_read)") - end - elseif txn_type == TXN_TYPES.INPUT_REG_READ then - -- input register read response - if m_pkt.length == #self.io_list.analog_in then - for i = 1, m_pkt.length do - local channel = self.io_list.analog_in[i] - local value = m_pkt.data[i] - self.db[channel] = value - end - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.input_reg_read)") - end - elseif txn_type == nil then - log.error(log_tag .. "unknown transaction reply") - else - log.error(log_tag .. "unknown transaction type " .. txn_type) + local txn_type = self.session.try_resolve(m_pkt.txn_id) + if txn_type == false then + -- nothing to do + elseif txn_type == TXN_TYPES.DI_READ then + -- discrete input read response + if m_pkt.length == #self.io_list.digital_in then + for i = 1, m_pkt.length do + local channel = self.io_list.digital_in[i] + local value = m_pkt.data[i] + self.db[channel] = value end else - log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.di_read)") end + elseif txn_type == TXN_TYPES.INPUT_REG_READ then + -- input register read response + if m_pkt.length == #self.io_list.analog_in then + for i = 1, m_pkt.length do + local channel = self.io_list.analog_in[i] + local value = m_pkt.data[i] + self.db[channel] = value + end + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (redstone.input_reg_read)") + end + elseif txn_type == TXN_TYPES.COIL_WRITE or txn_type == TXN_TYPES.HOLD_REG_WRITE then + -- successful acknowledgement + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") else - log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + log.error(log_tag .. "unknown transaction type " .. txn_type) end - - return success end - public.get_uid = function () return self.uid end - public.get_reactor = function () return self.reactor end - public.get_db = function () return self.db end - - public.close = function () self.connected = false end - public.is_connected = function () return self.connected end - -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) @@ -270,6 +243,9 @@ redstone.new = function (session_id, advert, out_queue) end end + -- get the unit session database + public.get_db = function () return self.db end + return public, self.in_q end diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index e6d41c2..d45d244 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -2,21 +2,24 @@ local comms = require("scada-common.comms") local log = require("scada-common.log") local types = require("scada-common.types") -local txnctrl = require("supervisor.session.rtu.txnctrl") +local unit_session = require("supervisor.session.rtu.unit_session") local turbine = {} -local PROTOCOLS = comms.PROTOCOLS local RTU_UNIT_TYPES = comms.RTU_UNIT_TYPES local DUMPING_MODE = types.DUMPING_MODE local MODBUS_FCODE = types.MODBUS_FCODE -local rtu_t = types.rtu_t - local TXN_TYPES = { - BUILD = 0, - STATE = 1, - TANKS = 2 + BUILD = 1, + STATE = 2, + TANKS = 3 +} + +local TXN_TAGS = { + "turbine.build", + "turbine.state", + "turbine.tanks", } local PERIODICS = { @@ -39,11 +42,7 @@ turbine.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").turbine(" .. advert.index .. "): " local self = { - uid = advert.index, - reactor = advert.reactor, - out_q = out_queue, - transaction_controller = txnctrl.new(), - connected = true, + session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, @@ -77,36 +76,26 @@ turbine.new = function (session_id, advert, out_queue) } } - ---@class unit_session - local public = {} + local public = self.session.get() -- PRIVATE FUNCTIONS -- - local _send_request = function (txn_type, f_code, register_range) - local m_pkt = comms.modbus_packet() - local txn_id = self.transaction_controller.create(txn_type) - - m_pkt.make(txn_id, self.uid, f_code, register_range) - - self.out_q.push_packet(m_pkt) - end - -- query the build of the device local _request_build = function () -- read input registers 1 through 9 (start = 1, count = 9) - _send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) + self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) end -- query the state of the device local _request_state = function () -- read input registers 10 through 13 (start = 10, count = 4) - _send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 4 }) + self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 4 }) end -- query the tanks of the device local _request_tanks = function () -- read input registers 14 through 16 (start = 14, count = 3) - _send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 14, 3 }) + self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 14, 3 }) end -- PUBLIC FUNCTIONS -- @@ -114,67 +103,50 @@ turbine.new = function (session_id, advert, out_queue) -- handle a packet ---@param m_pkt modbus_frame public.handle_packet = function (m_pkt) - local success = false - - if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.uid then - local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) - if txn_type == TXN_TYPES.BUILD then - -- build response - if m_pkt.length == 9 then - self.db.build.blades = m_pkt.data[1] - self.db.build.coils = m_pkt.data[2] - self.db.build.vents = m_pkt.data[3] - self.db.build.dispersers = m_pkt.data[4] - self.db.build.condensers = m_pkt.data[5] - self.db.build.steam_cap = m_pkt.data[6] - self.db.build.max_flow_rate = m_pkt.data[7] - self.db.build.max_production = m_pkt.data[8] - self.db.build.max_water_output = m_pkt.data[9] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.build)") - end - elseif txn_type == TXN_TYPES.STATE then - -- state response - if m_pkt.length == 4 then - self.db.state.flow_rate = m_pkt.data[1] - self.db.state.prod_rate = m_pkt.data[2] - self.db.state.steam_input_rate = m_pkt.data[3] - self.db.state.dumping_mode = m_pkt.data[4] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.state)") - end - elseif txn_type == TXN_TYPES.TANKS then - -- tanks response - if m_pkt.length == 3 then - self.db.tanks.steam = m_pkt.data[1] - self.db.tanks.steam_need = m_pkt.data[2] - self.db.tanks.steam_fill = m_pkt.data[3] - else - log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.tanks)") - end - elseif txn_type == nil then - log.error(log_tag .. "unknown transaction reply") - else - log.error(log_tag .. "unknown transaction type " .. txn_type) - end + local txn_type = self.session.try_resolve(m_pkt.txn_id) + if txn_type == false then + -- nothing to do + elseif txn_type == TXN_TYPES.BUILD then + -- build response + if m_pkt.length == 9 then + self.db.build.blades = m_pkt.data[1] + self.db.build.coils = m_pkt.data[2] + self.db.build.vents = m_pkt.data[3] + self.db.build.dispersers = m_pkt.data[4] + self.db.build.condensers = m_pkt.data[5] + self.db.build.steam_cap = m_pkt.data[6] + self.db.build.max_flow_rate = m_pkt.data[7] + self.db.build.max_production = m_pkt.data[8] + self.db.build.max_water_output = m_pkt.data[9] else - log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.build)") end + elseif txn_type == TXN_TYPES.STATE then + -- state response + if m_pkt.length == 4 then + self.db.state.flow_rate = m_pkt.data[1] + self.db.state.prod_rate = m_pkt.data[2] + self.db.state.steam_input_rate = m_pkt.data[3] + self.db.state.dumping_mode = m_pkt.data[4] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.state)") + end + elseif txn_type == TXN_TYPES.TANKS then + -- tanks response + if m_pkt.length == 3 then + self.db.tanks.steam = m_pkt.data[1] + self.db.tanks.steam_need = m_pkt.data[2] + self.db.tanks.steam_fill = m_pkt.data[3] + else + log.debug(log_tag .. "MODBUS transaction reply length mismatch (turbine.tanks)") + end + elseif txn_type == nil then + log.error(log_tag .. "unknown transaction reply") else - log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + log.error(log_tag .. "unknown transaction type " .. txn_type) end - - return success end - public.get_uid = function () return self.uid end - public.get_reactor = function () return self.reactor end - public.get_db = function () return self.db end - - public.close = function () self.connected = false end - public.is_connected = function () return self.connected end - -- update this runner ---@param time_now integer milliseconds public.update = function (time_now) @@ -194,6 +166,9 @@ turbine.new = function (session_id, advert, out_queue) end end + -- get the unit session database + public.get_db = function () return self.db end + return public end diff --git a/supervisor/session/rtu/txnctrl.lua b/supervisor/session/rtu/txnctrl.lua index 2d6be2e..a19dee6 100644 --- a/supervisor/session/rtu/txnctrl.lua +++ b/supervisor/session/rtu/txnctrl.lua @@ -6,7 +6,7 @@ local util = require("scada-common.util") local txnctrl = {} -local TIMEOUT = 3000 -- 3000ms max wait +local TIMEOUT = 2000 -- 2000ms max wait -- create a new transaction controller txnctrl.new = function () @@ -63,6 +63,17 @@ txnctrl.new = function () return txn_type end + -- renew a transaction by re-inserting it with its ID and type + ---@param txn_id integer + ---@param txn_type integer + public.renew = function (txn_id, txn_type) + insert(self.list, { + txn_id = txn_id, + txn_type = txn_type, + expiry = util.time() + TIMEOUT + }) + end + -- close timed-out transactions public.cleanup = function () local now = util.time() diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua new file mode 100644 index 0000000..67f83c0 --- /dev/null +++ b/supervisor/session/rtu/unit_session.lua @@ -0,0 +1,132 @@ +local comms = require("scada-common.comms") +local log = require("scada-common.log") +local types = require("scada-common.types") + +local txnctrl = require("supervisor.session.rtu.txnctrl") + +local unit_session = {} + +local PROTOCOLS = comms.PROTOCOLS +local MODBUS_FCODE = types.MODBUS_FCODE +local MODBUS_EXCODE = types.MODBUS_EXCODE + +-- create a new unit session runner +---@param log_tag string +---@param advert rtu_advertisement +---@param out_queue mqueue +---@param txn_tags table +unit_session.new = function (log_tag, advert, out_queue, txn_tags) + local self = { + log_tag = log_tag, + txn_tags = txn_tags, + uid = advert.index, + reactor = advert.reactor, + out_q = out_queue, + transaction_controller = txnctrl.new(), + connected = true, + device_fail = false + } + + ---@class _unit_session + local protected = {} + + ---@class unit_session + local public = {} + + -- PROTECTED FUNCTIONS -- + + -- send a MODBUS message, creating a transaction in the process + ---@param txn_type integer transaction type + ---@param f_code MODBUS_FCODE function code + ---@param register_param table register range or register and values + protected.send_request = function (txn_type, f_code, register_param) + local m_pkt = comms.modbus_packet() + local txn_id = self.transaction_controller.create(txn_type) + + m_pkt.make(txn_id, self.uid, f_code, register_param) + + self.out_q.push_packet(m_pkt) + end + + -- try to resolve a MODBUS transaction + ---@param m_pkt modbus_frame MODBUS packet + ---@return integer|false txn_type transaction type or false on error/busy + protected.try_resolve = function (m_pkt) + if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then + if m_pkt.unit_id == self.uid then + local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) + local txn_tag = " (" .. self.txn_tags[txn_type] .. ")" + + if bit.band(m_pkt.func_code, MODBUS_FCODE.ERROR_FLAG) ~= 0 then + -- transaction incomplete or failed + local ex = m_pkt.data[1] + if ex == MODBUS_EXCODE.ILLEGAL_FUNCTION then + log.error(log_tag .. "MODBUS: illegal function" .. txn_tag) + elseif ex == MODBUS_EXCODE.ILLEGAL_DATA_ADDR then + log.error(log_tag .. "MODBUS: illegal data address" .. txn_tag) + elseif ex == MODBUS_EXCODE.SERVER_DEVICE_FAIL then + if self.device_fail then + log.debug(log_tag .. "MODBUS: repeated device failure" .. txn_tag) + else + self.device_fail = true + log.warning(log_tag .. "MODBUS: device failure" .. txn_tag) + end + elseif ex == MODBUS_EXCODE.ACKNOWLEDGE then + -- will have to wait on reply, renew the transaction + self.transaction_controller.renew(m_pkt.txn_id, txn_type) + elseif ex == MODBUS_EXCODE.SERVER_DEVICE_BUSY then + -- will have to wait on reply, renew the transaction + self.transaction_controller.renew(m_pkt.txn_id, txn_type) + log.debug(log_tag .. "MODBUS: device busy" .. txn_tag) + elseif ex == MODBUS_EXCODE.NEG_ACKNOWLEDGE then + -- general failure + log.error(log_tag .. "MODBUS: negative acknowledge (bad request)" .. txn_tag) + elseif ex == MODBUS_EXCODE.GATEWAY_PATH_UNAVAILABLE then + -- RTU gateway has no known unit with the given ID + log.error(log_tag .. "MODBUS: gateway path unavailable (unknown unit)" .. txn_tag) + elseif ex ~= nil then + -- unsupported exception code + log.debug(log_tag .. "MODBUS: unsupported error " .. ex .. txn_tag) + else + -- nil exception code + log.debug(log_tag .. "MODBUS: nil exception code" .. txn_tag) + end + else + -- clear device fail flag + self.device_fail = false + + -- no error, return the transaction type + return txn_type + end + else + log.error(log_tag .. "wrong unit ID: " .. m_pkt.unit_id, true) + end + else + log.error(log_tag .. "illegal packet type " .. m_pkt.scada_frame.protocol(), true) + end + + -- error or transaction in progress, return false + return false + end + + -- get the public interface + protected.get = function () return public end + + -- PUBLIC FUNCTIONS -- + + -- get the unit ID + public.get_uid = function () return self.uid end + -- get the reactor ID + public.get_reactor = function () return self.reactor end + + -- close this unit + public.close = function () self.connected = false end + -- check if this unit is connected + public.is_connected = function () return self.connected end + -- check if this unit is faulted + public.is_faulted = function () return self.device_fail end + + return protected +end + +return unit_session diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 53f3e1a..d647f99 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.7" +local SUPERVISOR_VERSION = "alpha-v0.3.8" local print = util.print local println = util.println From 6184078c3ff0ca4447e0826e28ec316a55c1bb91 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 18 May 2022 13:28:43 -0400 Subject: [PATCH 144/168] #52 work in progress on reactor units --- supervisor/config.lua | 7 ++ supervisor/unit.lua | 150 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 145 insertions(+), 12 deletions(-) diff --git a/supervisor/config.lua b/supervisor/config.lua index 734e820..b98ceb2 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -6,6 +6,13 @@ config.SCADA_DEV_LISTEN = 16000 config.SCADA_SV_LISTEN = 16100 -- expected number of reactors config.NUM_REACTORS = 4 +-- expected number of boilers/turbines for each reactor +config.REACTOR_COOLING = { + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 1 + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 2 + { BOILERS = 1, TURBINES = 1 }, -- reactor unit 3 + { BOILERS = 1, TURBINES = 1 } -- reactor unit 4 +} -- log path config.LOG_PATH = "/log.txt" -- log mode diff --git a/supervisor/unit.lua b/supervisor/unit.lua index a246ebc..0afd7e5 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -1,19 +1,35 @@ +local types = require "scada-common.types" +local util = require "scada-common.util" + local unit = {} +---@alias TRI_FAIL integer +local TRI_FAIL = { + OK = 0, + PARTIAL = 1, + FULL = 2 +} + -- create a new reactor unit ----@param for_reactor integer -unit.new = function (for_reactor) +---@param for_reactor integer reactor unit number +---@param num_boilers integer number of boilers expected +---@param num_turbines integer number of turbines expected +unit.new = function (for_reactor, num_boilers, num_turbines) local self = { r_id = for_reactor, - plc_s = nil, + plc_s = nil, ---@class plc_session + counts = { boilers = num_boilers, turbines = num_turbines }, turbines = {}, boilers = {}, energy_storage = {}, redstone = {}, + deltas = { + last_reactor_temp = nil, + last_reactor_temp_time = 0 + }, db = { ---@class annunciator annunciator = { - -- RPS -- reactor PLCOnline = false, ReactorTrip = false, @@ -22,18 +38,18 @@ unit.new = function (for_reactor) RCSFlowLow = false, ReactorTempHigh = false, ReactorHighDeltaT = false, - ReactorOverPower = false, HighStartupRate = false, -- boiler - BoilerOnline = false, + BoilerOnline = TRI_FAIL.OK, HeatingRateLow = false, + BoilRateMismatch = false, CoolantFeedMismatch = false, -- turbine - TurbineOnline = false, + TurbineOnline = TRI_FAIL.OK, SteamFeedMismatch = false, SteamDumpOpen = false, - TurbineTrip = false, - TurbineOverUnderSpeed = false + TurbineOverSpeed = false, + TurbineTrip = false } } } @@ -45,8 +61,110 @@ unit.new = function (for_reactor) -- update the annunciator local _update_annunciator = function () + -- check PLC status self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) - self.db.annunciator.ReactorTrip = false + + if self.plc_s ~= nil then + ------------- + -- REACTOR -- + ------------- + + local plc_db = self.plc_s.get_db() + + -- compute deltas + local reactor_delta_t = 0 + if self.deltas.last_reactor_temp ~= nil then + reactor_delta_t = (plc_db.mek_status.temp - self.deltas.last_reactor_temp) / (util.time_s() - self.deltas.last_reactor_temp_time) + else + self.deltas.last_reactor_temp = plc_db.mek_status.temp + self.deltas.last_reactor_temp_time = util.time_s() + end + + -- update annunciator + self.db.annunciator.ReactorTrip = plc_db.rps_tripped + self.db.annunciator.ManualReactorTrip = plc_db.rps_trip_cause == types.rps_status_t.manual + 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 = plc_db.mek_status.ccool_fill < 0.75 or plc_db.mek_status.hcool_fill > 0.25 + self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 + self.db.annunciator.ReactorHighDeltaT = reactor_delta_t > 100 + -- @todo this is dependent on setup, i.e. how much coolant is buffered and the turbine setup + self.db.annunciator.HighStartupRate = not plc_db.control_state and plc_db.mek_status.burn_rate > 40 + end + + ------------- + -- BOILERS -- + ------------- + + -- check boiler online status + local connected_boilers = #self.boilers + if connected_boilers == 0 and self.num_boilers > 0 then + self.db.annunciator.BoilerOnline = TRI_FAIL.FULL + elseif connected_boilers > 0 and connected_boilers ~= self.num_boilers then + self.db.annunciator.BoilerOnline = TRI_FAIL.PARTIAL + else + self.db.annunciator.BoilerOnline = TRI_FAIL.OK + end + + local total_boil_rate = 0.0 + local no_boil_count = 0 + for i = 1, #self.boilers do + local boiler = self.boilers[i].get_db() ---@type boiler_session_db + local boil_rate = boiler.state.boil_rate + if boil_rate == 0 then + no_boil_count = no_boil_count + 1 + else + total_boil_rate = total_boil_rate + boiler.state.boil_rate + end + end + + if no_boil_count == 0 and self.num_boilers > 0 then + self.db.annunciator.HeatingRateLow = TRI_FAIL.FULL + elseif no_boil_count > 0 and no_boil_count ~= self.num_boilers then + self.db.annunciator.HeatingRateLow = TRI_FAIL.PARTIAL + else + self.db.annunciator.HeatingRateLow = TRI_FAIL.OK + end + + if self.plc_s ~= nil then + local expected_boil_rate = self.plc_s.get_db().mek_status.heating_rate / 10.0 + self.db.annunciator.BoilRateMismatch = math.abs(expected_boil_rate - total_boil_rate) > 25.0 + else + self.db.annunciator.BoilRateMismatch = false + end + + -------------- + -- TURBINES -- + -------------- + + -- check turbine online status + local connected_turbines = #self.turbines + if connected_turbines == 0 and self.num_turbines > 0 then + self.db.annunciator.TurbineOnline = TRI_FAIL.FULL + elseif connected_turbines > 0 and connected_turbines ~= self.num_turbines then + self.db.annunciator.TurbineOnline = TRI_FAIL.PARTIAL + else + self.db.annunciator.TurbineOnline = TRI_FAIL.OK + end + + --[[ + Turbine Under/Over Speed + ]]-- + + --[[ + Turbine Trip + a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool + this can be identified by these conditions: + - the current flow rate is 0 mB/t and it should not be + - it should not be if the boiler or reactor has a non-zero heating rate + - can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up + - can later identified by presence of steam in tank with a 0 flow rate + ]]-- + end + + -- unlink disconnected units + ---@param sessions table + local _unlink_disconnected_units = function (sessions) + util.filter_table(sessions, function (u) return u.is_connected() end) end -- PUBLIC FUNCTIONS -- @@ -55,14 +173,18 @@ unit.new = function (for_reactor) ---@param plc_session plc_session_struct public.link_plc_session = function (plc_session) self.plc_s = plc_session + self.deltas.last_reactor_temp = self.plc_s.get_db().mek_status.temp + self.deltas.last_reactor_temp_time = util.time_s() end - -- link a turbine RTU + -- link a turbine RTU session + ---@param turbine unit_session public.add_turbine = function (turbine) table.insert(self.turbines, turbine) end - -- link a boiler RTU + -- link a boiler RTU session + ---@param boiler unit_session public.add_boiler = function (boiler) table.insert(self.boilers, boiler) end @@ -85,6 +207,10 @@ unit.new = function (for_reactor) self.plc_s = nil end + -- unlink RTU unit sessions if they are closed + _unlink_disconnected_units(self.boilers) + _unlink_disconnected_units(self.turbines) + -- update annunciator logic _update_annunciator() end From cc856d4d801359efd9c94f14fd2335fd205becc8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 18 May 2022 13:32:44 -0400 Subject: [PATCH 145/168] redundant 'for_reactor' field removed from redstone RTU config --- rtu/config.lua | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rtu/config.lua b/rtu/config.lua index ec2b047..3d2e889 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -33,20 +33,17 @@ config.RTU_REDSTONE = { { channel = rsio.IO.WASTE_PO, side = "top", - bundled_color = colors.blue, - for_reactor = 1 + bundled_color = colors.blue }, { channel = rsio.IO.WASTE_PU, side = "top", - bundled_color = colors.cyan, - for_reactor = 1 + bundled_color = colors.cyan }, { channel = rsio.IO.WASTE_AM, side = "top", - bundled_color = colors.purple, - for_reactor = 1 + bundled_color = colors.purple } } } From 790571b6fcb0ba7c43582aa34d5e73797f7ae998 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 18 May 2022 13:49:04 -0400 Subject: [PATCH 146/168] #55 correctly use device IDs vs unit IDs --- supervisor/session/rtu.lua | 8 +++--- supervisor/session/rtu/boiler.lua | 5 ++-- supervisor/session/rtu/emachine.lua | 5 ++-- supervisor/session/rtu/redstone.lua | 8 ++++-- supervisor/session/rtu/turbine.lua | 5 ++-- supervisor/session/rtu/unit_session.lua | 38 +++++++++++++++++++------ supervisor/startup.lua | 2 +- 7 files changed, 48 insertions(+), 23 deletions(-) diff --git a/supervisor/session/rtu.lua b/supervisor/session/rtu.lua index 33a66d7..ff95087 100644 --- a/supervisor/session/rtu.lua +++ b/supervisor/session/rtu.lua @@ -88,17 +88,17 @@ rtu.new_session = function (id, in_queue, out_queue, advertisement) -- create unit by type if u_type == RTU_UNIT_TYPES.REDSTONE then - unit, rs_in_q = svrs_redstone.new(self.id, unit_advert, self.out_q) + unit, rs_in_q = svrs_redstone.new(self.id, i, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER then - unit = svrs_boiler.new(self.id, unit_advert, self.out_q) + unit = svrs_boiler.new(self.id, i, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.BOILER_VALVE then -- @todo Mekanism 10.1+ elseif u_type == RTU_UNIT_TYPES.TURBINE then - unit = svrs_turbine.new(self.id, unit_advert, self.out_q) + unit = svrs_turbine.new(self.id, i, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.TURBINE_VALVE then -- @todo Mekanism 10.1+ elseif u_type == RTU_UNIT_TYPES.EMACHINE then - unit = svrs_emachine.new(self.id, unit_advert, self.out_q) + unit = svrs_emachine.new(self.id, i, unit_advert, self.out_q) elseif u_type == RTU_UNIT_TYPES.IMATRIX then -- @todo Mekanism 10.1+ else diff --git a/supervisor/session/rtu/boiler.lua b/supervisor/session/rtu/boiler.lua index c45e22c..c3fa28e 100644 --- a/supervisor/session/rtu/boiler.lua +++ b/supervisor/session/rtu/boiler.lua @@ -29,9 +29,10 @@ local PERIODICS = { -- create a new boiler rtu session runner ---@param session_id integer +---@param unit_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue -boiler.new = function (session_id, advert, out_queue) +boiler.new = function (session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPES.BOILER then log.error("attempt to instantiate boiler RTU for type '" .. advert.type .. "'. this is a bug.") @@ -41,7 +42,7 @@ boiler.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").boiler(" .. advert.index .. "): " local self = { - session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), + session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, diff --git a/supervisor/session/rtu/emachine.lua b/supervisor/session/rtu/emachine.lua index c9c6481..e47293a 100644 --- a/supervisor/session/rtu/emachine.lua +++ b/supervisor/session/rtu/emachine.lua @@ -26,9 +26,10 @@ local PERIODICS = { -- create a new energy machine rtu session runner ---@param session_id integer +---@param unit_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue -emachine.new = function (session_id, advert, out_queue) +emachine.new = function (session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPES.EMACHINE then log.error("attempt to instantiate emachine RTU for type '" .. advert.type .. "'. this is a bug.") @@ -38,7 +39,7 @@ emachine.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").emachine(" .. advert.index .. "): " local self = { - session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), + session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, diff --git a/supervisor/session/rtu/redstone.lua b/supervisor/session/rtu/redstone.lua index 57848d9..b41e223 100644 --- a/supervisor/session/rtu/redstone.lua +++ b/supervisor/session/rtu/redstone.lua @@ -47,19 +47,21 @@ local PERIODICS = { -- create a new redstone rtu session runner ---@param session_id integer +---@param unit_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue -redstone.new = function (session_id, advert, out_queue) +redstone.new = function (session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPES.REDSTONE then log.error("attempt to instantiate redstone RTU for type '" .. advert.type .. "'. this is a bug.") return nil end - local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. advert.index .. "): " + -- for redstone, use unit ID not device index + local log_tag = "session.rtu(" .. session_id .. ").redstone(" .. unit_id .. "): " local self = { - session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), + session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), has_di = false, has_ai = false, periodics = { diff --git a/supervisor/session/rtu/turbine.lua b/supervisor/session/rtu/turbine.lua index d45d244..d62626e 100644 --- a/supervisor/session/rtu/turbine.lua +++ b/supervisor/session/rtu/turbine.lua @@ -30,9 +30,10 @@ local PERIODICS = { -- create a new turbine rtu session runner ---@param session_id integer +---@param unit_id integer ---@param advert rtu_advertisement ---@param out_queue mqueue -turbine.new = function (session_id, advert, out_queue) +turbine.new = function (session_id, unit_id, advert, out_queue) -- type check if advert.type ~= RTU_UNIT_TYPES.TURBINE then log.error("attempt to instantiate turbine RTU for type '" .. advert.type .. "'. this is a bug.") @@ -42,7 +43,7 @@ turbine.new = function (session_id, advert, out_queue) local log_tag = "session.rtu(" .. session_id .. ").turbine(" .. advert.index .. "): " local self = { - session = unit_session.new(log_tag, advert, out_queue, TXN_TAGS), + session = unit_session.new(unit_id, advert, out_queue, log_tag, TXN_TAGS), has_build = false, periodics = { next_build_req = 0, diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 67f83c0..4f50120 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -11,15 +11,17 @@ local MODBUS_FCODE = types.MODBUS_FCODE local MODBUS_EXCODE = types.MODBUS_EXCODE -- create a new unit session runner ----@param log_tag string ----@param advert rtu_advertisement ----@param out_queue mqueue ----@param txn_tags table -unit_session.new = function (log_tag, advert, out_queue, txn_tags) +---@param unit_id integer MODBUS unit ID +---@param advert rtu_advertisement RTU advertisement for this unit +---@param out_queue mqueue send queue +---@param log_tag string logging tag +---@param txn_tags table transaction log tags +unit_session.new = function (unit_id, advert, out_queue, log_tag, txn_tags) local self = { log_tag = log_tag, txn_tags = txn_tags, - uid = advert.index, + unit_id = unit_id, + device_index = advert.index, reactor = advert.reactor, out_q = out_queue, transaction_controller = txnctrl.new(), @@ -43,7 +45,7 @@ unit_session.new = function (log_tag, advert, out_queue, txn_tags) local m_pkt = comms.modbus_packet() local txn_id = self.transaction_controller.create(txn_type) - m_pkt.make(txn_id, self.uid, f_code, register_param) + m_pkt.make(txn_id, self.unit_id, f_code, register_param) self.out_q.push_packet(m_pkt) end @@ -53,7 +55,7 @@ unit_session.new = function (log_tag, advert, out_queue, txn_tags) ---@return integer|false txn_type transaction type or false on error/busy protected.try_resolve = function (m_pkt) if m_pkt.scada_frame.protocol() == PROTOCOLS.MODBUS_TCP then - if m_pkt.unit_id == self.uid then + if m_pkt.unit_id == self.unit_id then local txn_type = self.transaction_controller.resolve(m_pkt.txn_id) local txn_tag = " (" .. self.txn_tags[txn_type] .. ")" @@ -115,7 +117,9 @@ unit_session.new = function (log_tag, advert, out_queue, txn_tags) -- PUBLIC FUNCTIONS -- -- get the unit ID - public.get_uid = function () return self.uid end + public.get_unit_id = function () return self.unit_id end + -- get the device index + public.get_device_idx = function () return self.device_index end -- get the reactor ID public.get_reactor = function () return self.reactor end @@ -126,6 +130,22 @@ unit_session.new = function (log_tag, advert, out_queue, txn_tags) -- check if this unit is faulted public.is_faulted = function () return self.device_fail end + -- PUBLIC TEMPLATE FUNCTIONS -- + + -- handle a packet + ---@param m_pkt modbus_frame +---@diagnostic disable-next-line: unused-local + public.handle_packet = function (m_pkt) + log.debug("template unit_session.handle_packet() called", true) + end + + -- update this runner + ---@param time_now integer milliseconds +---@diagnostic disable-next-line: unused-local + public.update = function (time_now) + log.debug("template unit_session.update() called", true) + end + return protected end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d647f99..e6e800a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.8" +local SUPERVISOR_VERSION = "alpha-v0.3.9" local print = util.print local println = util.println From 62d5490dc823fa73460c0fd6fce10b0a092fa994 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 18 May 2022 14:30:48 -0400 Subject: [PATCH 147/168] #53 RTU redstone parse checks --- rtu/startup.lua | 128 +++++++++++++++++++++++++----------------- scada-common/util.lua | 11 ++++ 2 files changed, 87 insertions(+), 52 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 8293d39..2c9b8d8 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -103,64 +103,88 @@ for entry_idx = 1, #rtu_redstone do log.debug("init> starting redstone RTU I/O linking for reactor " .. io_reactor .. "...") - for i = 1, #io_table do - local valid = false - local conf = io_table[i] + local continue = true - -- verify configuration - if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then - if conf.bundled_color then - valid = rsio.is_color(conf.bundled_color) - else - valid = true - end - end - - if not valid then - local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. entry_idx .. - " (for reactor " .. io_reactor .. ")" - println_ts(message) - log.warning(message) - else - -- link redstone in RTU - local mode = rsio.get_io_mode(conf.channel) - if mode == rsio.IO_MODE.DIGITAL_IN then - rs_rtu.link_di(conf.side, conf.bundled_color) - elseif mode == rsio.IO_MODE.DIGITAL_OUT then - rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) - elseif mode == rsio.IO_MODE.ANALOG_IN then - rs_rtu.link_ai(conf.side) - elseif mode == rsio.IO_MODE.ANALOG_OUT then - rs_rtu.link_ao(conf.side) - else - -- should be unreachable code, we already validated channels - log.error("init> fell through if chain attempting to identify IO mode", true) - break - end - - table.insert(capabilities, conf.channel) - - log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. - ") for reactor " .. io_reactor) + for i = 1, #units do + local unit = units[i] ---@type rtu_unit_registry_entry + if unit.reactor == io_reactor and unit.type == rtu_t.redstone then + -- duplicate entry + log.warning("init> skipping definition block #" .. entry_idx .. " for reactor " .. io_reactor .. " with already defined redstone I/O") + continue = false + break end end - ---@class rtu_unit_registry_entry - local unit = { - name = "redstone_io", - type = rtu_t.redstone, - index = entry_idx, - reactor = io_reactor, - device = capabilities, -- use device field for redstone channels - rtu = rs_rtu, - modbus_io = modbus.new(rs_rtu, false), - pkt_queue = nil, - thread = nil - } + if continue then + for i = 1, #io_table do + local valid = false + local conf = io_table[i] - table.insert(units, unit) + -- verify configuration + if rsio.is_valid_channel(conf.channel) and rsio.is_valid_side(conf.side) then + if conf.bundled_color then + valid = rsio.is_color(conf.bundled_color) + else + valid = true + end + end - log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. io_reactor) + if not valid then + local message = "init> invalid redstone definition at index " .. i .. " in definition block #" .. entry_idx .. + " (for reactor " .. io_reactor .. ")" + println_ts(message) + log.warning(message) + else + -- link redstone in RTU + local mode = rsio.get_io_mode(conf.channel) + if mode == rsio.IO_MODE.DIGITAL_IN then + -- can't have duplicate inputs + if util.table_contains(capabilities, conf.channel) then + log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side) + else + rs_rtu.link_di(conf.side, conf.bundled_color) + end + elseif mode == rsio.IO_MODE.DIGITAL_OUT then + rs_rtu.link_do(conf.channel, conf.side, conf.bundled_color) + elseif mode == rsio.IO_MODE.ANALOG_IN then + -- can't have duplicate inputs + if util.table_contains(capabilities, conf.channel) then + log.warning("init> skipping duplicate input for channel " .. rsio.to_string(conf.channel) .. " on side " .. conf.side) + else + rs_rtu.link_ai(conf.side) + end + elseif mode == rsio.IO_MODE.ANALOG_OUT then + rs_rtu.link_ao(conf.side) + else + -- should be unreachable code, we already validated channels + log.error("init> fell through if chain attempting to identify IO mode", true) + break + end + + table.insert(capabilities, conf.channel) + + log.debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(conf.channel) .. " (" .. conf.side .. + ") for reactor " .. io_reactor) + end + end + + ---@class rtu_unit_registry_entry + local unit = { + name = "redstone_io", + type = rtu_t.redstone, + index = entry_idx, + reactor = io_reactor, + device = capabilities, -- use device field for redstone channels + rtu = rs_rtu, + modbus_io = modbus.new(rs_rtu, false), + pkt_queue = nil, + thread = nil + } + + table.insert(units, unit) + + log.debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. io_reactor) + end end -- mounted peripherals diff --git a/scada-common/util.lua b/scada-common/util.lua index 38cfc1f..9e7a4a7 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -107,6 +107,17 @@ util.filter_table = function (t, f, on_delete) end end +-- check if a table contains the provided element +---@param t table table to check +---@param element any element to check for +util.table_contains = function (t, element) + for i = 1, #t do + if t[i] == element then return true end + end + + return false +end + -- MEKANISM POWER -- -- function kFE(fe) return fe / 1000 end From dd553125d62b8f00cb9443712e86afb22f85e757 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 19 May 2022 09:19:51 -0400 Subject: [PATCH 148/168] #54 don't trip RPS fault on terminate as it ends up being redundant with shutdown sequence --- reactor-plc/plc.lua | 4 +++- reactor-plc/startup.lua | 2 +- scada-common/ppm.lua | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index b54354c..1123399 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -51,7 +51,9 @@ plc.rps_init = function (reactor) -- set reactor access fault flag local _set_fault = function () - self.state[state_keys.fault] = true + if self.reactor.__p_last_fault() ~= "Terminated" then + self.state[state_keys.fault] = true + end end -- clear reactor access fault flag diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index aad57f7..214b167 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.6.6" +local R_PLC_VERSION = "alpha-v0.6.7" local print = util.print local println = util.println diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index c5026ea..6efa33c 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -19,6 +19,7 @@ local _ppm_sys = { mounts = {}, auto_cf = false, faulted = false, + last_fault = "", terminate = false, mute = false } @@ -32,6 +33,7 @@ local _ppm_sys = { local peri_init = function (iface) local self = { faulted = false, + last_fault = "", auto_cf = true, type = peripheral.getType(iface), device = peripheral.wrap(iface) @@ -51,7 +53,10 @@ local peri_init = function (iface) else -- function failed self.faulted = true + self.last_fault = result + _ppm_sys.faulted = true + _ppm_sys.last_fault = result if not _ppm_sys.mute then log.error("PPM: protected " .. key .. "() -> " .. result) @@ -69,6 +74,7 @@ local peri_init = function (iface) -- fault management functions local clear_fault = function () self.faulted = false end + local get_last_fault = function () return self.last_fault end local is_faulted = function () return self.faulted end local is_ok = function () return not self.faulted end @@ -78,6 +84,7 @@ local peri_init = function (iface) -- append to device functions self.device.__p_clear_fault = clear_fault + self.device.__p_last_fault = get_last_fault self.device.__p_is_faulted = is_faulted self.device.__p_is_ok = is_ok self.device.__p_enable_afc = enable_afc @@ -117,14 +124,19 @@ ppm.disable_afc = function () _ppm_sys.auto_cf = false end +-- clear fault flag +ppm.clear_fault = function () + _ppm_sys.faulted = false +end + -- check fault flag ppm.is_faulted = function () return _ppm_sys.faulted end --- clear fault flag -ppm.clear_fault = function () - _ppm_sys.faulted = false +-- get the last fault message +ppm.get_last_fault = function () + return _ppm_sys.last_fault end -- TERMINATION -- From 6a168c884dd2ce765f15dde805521d822fafa3c6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 19 May 2022 10:21:04 -0400 Subject: [PATCH 149/168] #23 device version reporting --- reactor-plc/plc.lua | 6 ++++-- reactor-plc/startup.lua | 5 +++-- rtu/rtu.lua | 6 ++++-- rtu/startup.lua | 4 ++-- supervisor/session/svsessions.lua | 8 +++++++- supervisor/startup.lua | 2 +- supervisor/supervisor.lua | 27 ++++++++++++++++----------- 7 files changed, 37 insertions(+), 21 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 1123399..db04467 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -278,15 +278,17 @@ end -- Reactor PLC Communications ---@param id integer +---@param version string ---@param modem table ---@param local_port integer ---@param server_port integer ---@param reactor table ---@param rps rps ---@param conn_watchdog watchdog -plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_watchdog) +plc.comms = function (id, version, modem, local_port, server_port, reactor, rps, conn_watchdog) local self = { id = id, + version = version, seq_num = 0, r_seq_num = nil, modem = modem, @@ -499,7 +501,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- attempt to establish link with supervisor public.send_link_req = function () - _send(RPLC_TYPES.LINK_REQ, { self.id }) + _send(RPLC_TYPES.LINK_REQ, { self.id, self.version }) end -- send live status information diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 214b167..c1fb666 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.6.7" +local R_PLC_VERSION = "alpha-v0.6.8" local print = util.print local println = util.println @@ -117,7 +117,8 @@ local init = function () log.debug("init> conn watchdog started") -- start comms - smem_sys.plc_comms = plc.comms(config.REACTOR_ID, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) + smem_sys.plc_comms = plc.comms(config.REACTOR_ID, R_PLC_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, + smem_dev.reactor, smem_sys.rps, smem_sys.conn_watchdog) log.debug("init> comms init") else println("boot> starting in offline mode"); diff --git a/rtu/rtu.lua b/rtu/rtu.lua index d02bc6c..2e2d445 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -161,12 +161,14 @@ rtu.init_unit = function () end -- RTU Communications +---@param version string ---@param modem table ---@param local_port integer ---@param server_port integer ---@param conn_watchdog watchdog -rtu.comms = function (modem, local_port, server_port, conn_watchdog) +rtu.comms = function (version, modem, local_port, server_port, conn_watchdog) local self = { + version = version, seq_num = 0, r_seq_num = nil, txn_id = 0, @@ -249,7 +251,7 @@ rtu.comms = function (modem, local_port, server_port, conn_watchdog) -- send capability advertisement ---@param units table public.send_advertisement = function (units) - local advertisement = {} + local advertisement = { self.version } for i = 1, #units do local unit = units[i] --@type rtu_unit_registry_entry diff --git a/rtu/startup.lua b/rtu/startup.lua index 2c9b8d8..26abaf9 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.6" +local RTU_VERSION = "alpha-v0.6.7" local rtu_t = types.rtu_t @@ -264,7 +264,7 @@ smem_sys.conn_watchdog = util.new_watchdog(5) log.debug("boot> conn watchdog started") -- setup comms -smem_sys.rtu_comms = rtu.comms(smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog) +smem_sys.rtu_comms = rtu.comms(RTU_VERSION, smem_dev.modem, config.LISTEN_PORT, config.SERVER_PORT, smem_sys.conn_watchdog) log.debug("boot> comms init") -- init threads diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index d0b76cc..b004a38 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -196,13 +196,15 @@ end ---@param local_port integer ---@param remote_port integer ---@param for_reactor integer +---@param version string ---@return integer|false session_id -svsessions.establish_plc_session = function (local_port, remote_port, for_reactor) +svsessions.establish_plc_session = function (local_port, remote_port, for_reactor, version) if svsessions.get_reactor_session(for_reactor) == nil then ---@class plc_session_struct local plc_s = { open = true, reactor = for_reactor, + version = version, l_port = local_port, r_port = remote_port, in_queue = mqueue.new(), @@ -231,9 +233,13 @@ end ---@param advertisement table ---@return integer session_id svsessions.establish_rtu_session = function (local_port, remote_port, advertisement) + -- pull version from advertisement + local version = table.remove(advertisement, 1) + ---@class rtu_session_struct local rtu_s = { open = true, + version = version, l_port = local_port, r_port = remote_port, in_queue = mqueue.new(), diff --git a/supervisor/startup.lua b/supervisor/startup.lua index e6e800a..e175f3a 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -38,7 +38,7 @@ if modem == nil then end -- start comms, open all channels -local superv_comms = supervisor.comms(config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) +local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, modem, config.SCADA_DEV_LISTEN, config.SCADA_SV_LISTEN) -- base loop clock (6.67Hz, 3 ticks) local MAIN_CLOCK = 0.15 diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 3a176cb..5bc3ce8 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -20,12 +20,14 @@ local print_ts = util.print_ts local println_ts = util.println_ts -- supervisory controller communications +---@param version string ---@param num_reactors integer ---@param modem table ---@param dev_listen integer ---@param coord_listen integer -supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) +supervisor.comms = function (version, num_reactors, modem, dev_listen, coord_listen) local self = { + version = version, num_reactors = num_reactors, modem = modem, dev_listen = dev_listen, @@ -180,16 +182,16 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) -- unknown session, is this a linking request? if packet.type == RPLC_TYPES.LINK_REQ then - if packet.length == 1 then + if packet.length == 2 then -- this is a linking request - local plc_id = svsessions.establish_plc_session(l_port, r_port, packet.data[1]) + local plc_id = svsessions.establish_plc_session(l_port, r_port, packet.data[1], packet.data[2]) if plc_id == false then -- reactor already has a PLC assigned log.debug("PLC_LNK: assignment collision with reactor " .. packet.data[1]) _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully - println("connected to reactor " .. packet.data[1] .. " PLC (port " .. r_port .. ")") + println("connected to reactor " .. packet.data[1] .. " PLC v " .. packet.data[2] .. " (port " .. r_port .. ")") log.debug("PLC_LNK: allowed for device at " .. r_port) _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.ALLOW }) end @@ -210,18 +212,21 @@ supervisor.comms = function (num_reactors, modem, dev_listen, coord_listen) if session ~= nil then -- pass the packet onto the session handler session.in_queue.push_packet(packet) - else - -- is this an RTU advertisement? - if packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then - local rtu_id = svsessions.establish_rtu_session(l_port, r_port, packet.data) + elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then + if packet.length >= 1 then + -- this is an RTU advertisement for a new session + println("connected to RTU v " .. packet.data[1] .. " (port " .. r_port .. ")") + + svsessions.establish_rtu_session(l_port, r_port, packet.data) - println("connected to RTU (port " .. r_port .. ")") log.debug("RTU_ADVERT: linked " .. r_port) _send_remote_linked(packet.scada_frame.seq_num() + 1, r_port) else - -- any other packet should be session related, discard it - log.debug("discarding SCADA_MGMT packet without a known session") + log.debug("RTU_ADVERT: advertisement packet empty") end + else + -- any other packet should be session related, discard it + log.debug("discarding SCADA_MGMT packet without a known session") end else log.debug("illegal packet type " .. protocol .. " on device listening channel") From 44d30ae5839738f1ef181ad44f9672a371d20800 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 19 May 2022 10:35:56 -0400 Subject: [PATCH 150/168] #48 only log every 20 PPM faults (per function) --- reactor-plc/startup.lua | 2 +- rtu/startup.lua | 2 +- scada-common/ppm.lua | 18 ++++++++++++++++-- supervisor/startup.lua | 2 +- 4 files changed, 19 insertions(+), 5 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index c1fb666..86e7722 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.6.8" +local R_PLC_VERSION = "alpha-v0.6.9" local print = util.print local println = util.println diff --git a/rtu/startup.lua b/rtu/startup.lua index 26abaf9..90578ce 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.7" +local RTU_VERSION = "alpha-v0.6.8" local rtu_t = types.rtu_t diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 6efa33c..fd84503 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -15,6 +15,8 @@ ppm.ACCESS_FAULT = ACCESS_FAULT -- PRIVATE DATA/FUNCTIONS -- ---------------------------- +local REPORT_FREQUENCY = 20 -- log every 20 faults per function + local _ppm_sys = { mounts = {}, auto_cf = false, @@ -34,6 +36,7 @@ local peri_init = function (iface) local self = { faulted = false, last_fault = "", + fault_counts = {}, auto_cf = true, type = peripheral.getType(iface), device = peripheral.wrap(iface) @@ -42,6 +45,7 @@ local peri_init = function (iface) -- initialization process (re-map) for key, func in pairs(self.device) do + self.fault_counts[key] = 0 self.device[key] = function (...) local status, result = pcall(func, ...) @@ -49,6 +53,9 @@ local peri_init = function (iface) -- auto fault clear if self.auto_cf then self.faulted = false end if _ppm_sys.auto_cf then _ppm_sys.faulted = false end + + self.fault_counts[key] = 0 + return result else -- function failed @@ -58,10 +65,17 @@ local peri_init = function (iface) _ppm_sys.faulted = true _ppm_sys.last_fault = result - if not _ppm_sys.mute then - log.error("PPM: protected " .. key .. "() -> " .. result) + if not _ppm_sys.mute and (self.fault_counts[key] % REPORT_FREQUENCY == 0) then + local count_str = "" + if self.fault_counts[key] > 0 then + count_str = " [" .. self.fault_counts[key] .. " total faults]" + end + + log.error("PPM: protected " .. key .. "() -> " .. result .. count_str) end + self.fault_counts[key] = self.fault_counts[key] + 1 + if result == "Terminated" then _ppm_sys.terminate = true end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index e175f3a..d23436d 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.9" +local SUPERVISOR_VERSION = "alpha-v0.3.10" local print = util.print local println = util.println From 61965f295dfcfc83b6cb5ac92c8f570de15f3223 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 19 May 2022 10:49:17 -0400 Subject: [PATCH 151/168] added #29 to known issues --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 1aa8880..d0a34c1 100644 --- a/README.md +++ b/README.md @@ -54,3 +54,8 @@ TBD, I am planning on AES symmetric encryption for security + HMAC to prevent re This is somewhat important here as otherwise anyone can just control your setup, which is undeseriable. Unlike normal Minecraft PVP chaos, it would be very difficult to identify who is messing with your system, as with an Ender Modem they can do it from effectively anywhere and the server operators would have to check every computer's filesystem to find suspicious code. The only other possible security mitigation for commanding (no effect on monitoring) is to enforce a maximum authorized transmission range (which I will probably also do, or maybe fall back to), as modem message events contain the transmission distance. + +## Known Issues + +GitHub issue \#29: +It appears that with Mekanism 10.0, a boiler peripheral may rapidly disconnect/reconnect constantly while running. This will prevent that RTU from operating correctly while also filling up the log file. This may be due to a very specific version interaction of CC: Tweaked and Mekansim, so you are welcome to try this on Mekanism 10.0 servers, but do be aware it may not work. From 3f4fb630299a89ae1af945fb0836337381a30dc4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 21 May 2022 12:24:43 -0400 Subject: [PATCH 152/168] #52 basic reactor unit object --- scada-common/types.lua | 9 + supervisor/session/plc.lua | 19 +- supervisor/session/rtu/unit_session.lua | 3 + supervisor/unit.lua | 346 ++++++++++++++++++++---- 4 files changed, 321 insertions(+), 56 deletions(-) diff --git a/scada-common/types.lua b/scada-common/types.lua index b7f115b..b1b5e8c 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -22,6 +22,15 @@ local types = {} ---@field reactor integer ---@field rsio table|nil +-- ENUMERATION TYPES -- + +---@alias TRI_FAIL integer +types.TRI_FAIL = { + OK = 0, + PARTIAL = 1, + FULL = 2 +} + -- STRING TYPES -- ---@alias rtu_t string diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index f799987..bf79f23 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -422,7 +422,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end end - -- get the reactor structure + -- get the reactor status public.get_status = function () if self.received_status_cache then return self.sDB.mek_status @@ -431,6 +431,23 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) end end + -- get the reactor RPS status + public.get_rps = function () + return self.sDB.rps_status + end + + -- get the general status information + public.get_general_status = function () + return { + last_status_update = self.sDB.last_status_update, + control_state = self.sDB.control_state, + overridden = self.sDB.overridden, + degraded = self.sDB.degraded, + rps_tripped = self.sDB.rps_tripped, + rps_trip_cause = self.sDB.rps_trip_cause + } + end + -- check if a timer matches this session's watchdog public.check_wd = function (timer) return self.plc_conn_watchdog.is_timer(timer) and self.connected diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 4f50120..83a0766 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -146,6 +146,9 @@ unit_session.new = function (unit_id, advert, out_queue, log_tag, txn_tags) log.debug("template unit_session.update() called", true) end + -- get the unit session database + public.get_db = function () return {} end + return protected end diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 0afd7e5..628de67 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -3,11 +3,20 @@ local util = require "scada-common.util" local unit = {} ----@alias TRI_FAIL integer -local TRI_FAIL = { - OK = 0, - PARTIAL = 1, - FULL = 2 +local TRI_FAIL = types.TRI_FAIL +local DUMPING_MODE = types.DUMPING_MODE + +local DT_KEYS = { + ReactorTemp = "RTP", + ReactorFuel = "RFL", + ReactorWaste = "RWS", + ReactorCCool = "RCC", + ReactorHCool = "RHC", + BoilerWater = "BWR", + BoilerSteam = "BST", + BoilerCCool = "BCC", + BoilerHCool = "BHC", + TurbineSteam = "TST" } -- create a new reactor unit @@ -21,12 +30,8 @@ unit.new = function (for_reactor, num_boilers, num_turbines) counts = { boilers = num_boilers, turbines = num_turbines }, turbines = {}, boilers = {}, - energy_storage = {}, redstone = {}, - deltas = { - last_reactor_temp = nil, - last_reactor_temp_time = 0 - }, + deltas = {}, db = { ---@class annunciator annunciator = { @@ -38,55 +43,137 @@ unit.new = function (for_reactor, num_boilers, num_turbines) RCSFlowLow = false, ReactorTempHigh = false, ReactorHighDeltaT = false, + FuelInputRateLow = false, + WasteLineOcclusion = false, HighStartupRate = false, -- boiler BoilerOnline = TRI_FAIL.OK, - HeatingRateLow = false, + HeatingRateLow = {}, BoilRateMismatch = false, CoolantFeedMismatch = false, -- turbine TurbineOnline = TRI_FAIL.OK, SteamFeedMismatch = false, - SteamDumpOpen = false, - TurbineOverSpeed = false, - TurbineTrip = false + MaxWaterReturnFeed = false, + SteamDumpOpen = {}, + TurbineOverSpeed = {}, + TurbineTrip = {} } } } + -- init boiler table fields + for _ = 1, self.num_boilers do + table.insert(self.db.annunciator.HeatingRateLow, false) + end + + -- init turbine table fields + for _ = 1, self.num_turbines do + table.insert(self.db.annunciator.SteamDumpOpen, TRI_FAIL.OK) + table.insert(self.db.annunciator.TurbineOverSpeed, false) + table.insert(self.db.annunciator.TurbineTrip, false) + end + ---@class reactor_unit local public = {} -- PRIVATE FUNCTIONS -- + -- compute a change with respect to time of the given value + ---@param key string value key + ---@param value number value + local _compute_dt = function (key, value) + if self.deltas[key] then + local data = self.deltas[key] + + data.dt = (value - data.last_v) / (util.time_s() - data.last_t) + + data.last_v = value + data.last_t = util.time_s() + else + self.deltas[key] = { + last_t = util.time_s(), + last_v = value, + dt = 0.0 + } + end + end + + -- clear a delta + ---@param key string value key + local _reset_dt = function (key) + self.deltas[key] = nil + end + + -- get the delta t of a value + ---@param key string value key + ---@return number + local _get_dt = function (key) + if self.deltas[key] then + return self.deltas[key].dt + else + return 0.0 + end + end + + -- update all delta computations + local _dt__compute_all = function () + if self.plc_s ~= nil then + local plc_db = self.plc_s.get_db() + + -- @todo Meknaism 10.1+ will change fuel/waste to need _amnt + _compute_dt(DT_KEYS.ReactorTemp, plc_db.mek_status.temp) + _compute_dt(DT_KEYS.ReactorFuel, plc_db.mek_status.fuel) + _compute_dt(DT_KEYS.ReactorWaste, plc_db.mek_status.waste) + _compute_dt(DT_KEYS.ReactorCCool, plc_db.mek_status.ccool_amnt) + _compute_dt(DT_KEYS.ReactorHCool, plc_db.mek_status.hcool_amnt) + end + + for i = 1, #self.boilers do + local boiler = self.boilers[i] ---@type unit_session + local db = boiler.get_db() ---@type boiler_session_db + + -- @todo Meknaism 10.1+ will change water/steam to need .amount + _compute_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx(), db.tanks.water) + _compute_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx(), db.tanks.steam) + _compute_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx(), db.tanks.ccool.amount) + _compute_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx(), db.tanks.hcool.amount) + end + + for i = 1, #self.turbines do + local turbine = self.turbines[i] ---@type unit_session + local db = turbine.get_db() ---@type turbine_session_db + + _compute_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx(), db.tanks.steam) + -- @todo Mekanism 10.1+ needed + -- _compute_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx(), db.?) + end + end + -- update the annunciator local _update_annunciator = function () + -- update deltas + _dt__compute_all() + + ------------- + -- REACTOR -- + ------------- + -- check PLC status self.db.annunciator.PLCOnline = (self.plc_s ~= nil) and (self.plc_s.open) if self.plc_s ~= nil then - ------------- - -- REACTOR -- - ------------- - local plc_db = self.plc_s.get_db() - -- compute deltas - local reactor_delta_t = 0 - if self.deltas.last_reactor_temp ~= nil then - reactor_delta_t = (plc_db.mek_status.temp - self.deltas.last_reactor_temp) / (util.time_s() - self.deltas.last_reactor_temp_time) - else - self.deltas.last_reactor_temp = plc_db.mek_status.temp - self.deltas.last_reactor_temp_time = util.time_s() - end - -- update annunciator self.db.annunciator.ReactorTrip = plc_db.rps_tripped self.db.annunciator.ManualReactorTrip = plc_db.rps_trip_cause == types.rps_status_t.manual 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 = plc_db.mek_status.ccool_fill < 0.75 or plc_db.mek_status.hcool_fill > 0.25 self.db.annunciator.ReactorTempHigh = plc_db.mek_status.temp > 1000 - self.db.annunciator.ReactorHighDeltaT = reactor_delta_t > 100 + self.db.annunciator.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > 100 + self.db.annunciator.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < 0.0 or plc_db.mek_status.fuel_fill <= 0.01 + self.db.annunciator.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 0.0 or plc_db.mek_status.waste_fill >= 0.99 -- @todo this is dependent on setup, i.e. how much coolant is buffered and the turbine setup self.db.annunciator.HighStartupRate = not plc_db.control_state and plc_db.mek_status.burn_rate > 40 end @@ -105,33 +192,52 @@ unit.new = function (for_reactor, num_boilers, num_turbines) self.db.annunciator.BoilerOnline = TRI_FAIL.OK end + -- compute aggregated statistics local total_boil_rate = 0.0 - local no_boil_count = 0 + local boiler_steam_dt_sum = 0.0 + local boiler_water_dt_sum = 0.0 for i = 1, #self.boilers do local boiler = self.boilers[i].get_db() ---@type boiler_session_db - local boil_rate = boiler.state.boil_rate - if boil_rate == 0 then - no_boil_count = no_boil_count + 1 - else - total_boil_rate = total_boil_rate + boiler.state.boil_rate - end - end - - if no_boil_count == 0 and self.num_boilers > 0 then - self.db.annunciator.HeatingRateLow = TRI_FAIL.FULL - elseif no_boil_count > 0 and no_boil_count ~= self.num_boilers then - self.db.annunciator.HeatingRateLow = TRI_FAIL.PARTIAL - else - self.db.annunciator.HeatingRateLow = TRI_FAIL.OK + total_boil_rate = total_boil_rate + boiler.state.boil_rate + boiler_steam_dt_sum = _get_dt(DT_KEYS.BoilerSteam .. self.boilers[i].get_device_idx()) + boiler_water_dt_sum = _get_dt(DT_KEYS.BoilerWater .. self.boilers[i].get_device_idx()) end + -- check heating rate low if self.plc_s ~= nil then + -- check for inactive boilers while reactor is active + for i = 1, #self.boilers do + local boiler = self.boilers[i] ---@type unit_session + local idx = boiler.get_device_idx() + local db = boiler.get_db() ---@type boiler_session_db + + if self.plc_s.get_db().mek_status.status then + self.db.annunciator.HeatingRateLow[idx] = db.state.boil_rate == 0 + else + self.db.annunciator.HeatingRateLow[idx] = false + end + end + + -- check for rate mismatch local expected_boil_rate = self.plc_s.get_db().mek_status.heating_rate / 10.0 self.db.annunciator.BoilRateMismatch = math.abs(expected_boil_rate - total_boil_rate) > 25.0 - else - self.db.annunciator.BoilRateMismatch = false end + -- check coolant feed mismatch + local cfmismatch = false + for i = 1, #self.boilers do + local boiler = self.boilers[i] ---@type unit_session + local idx = boiler.get_device_idx() + local db = boiler.get_db() ---@type boiler_session_db + + -- gaining heated coolant + cfmismatch = cfmismatch or _get_dt(DT_KEYS.BoilerHCool .. idx) > 0 or db.tanks.hcool_fill == 1 + -- losing cooled coolant + cfmismatch = cfmismatch or _get_dt(DT_KEYS.BoilerCCool .. idx) < 0 or db.tanks.ccool_fill == 0 + end + + self.db.annunciator.CoolantFeedMismatch = cfmismatch + -------------- -- TURBINES -- -------------- @@ -146,19 +252,62 @@ unit.new = function (for_reactor, num_boilers, num_turbines) self.db.annunciator.TurbineOnline = TRI_FAIL.OK end - --[[ - Turbine Under/Over Speed - ]]-- + -- compute aggregated statistics + local total_flow_rate = 0 + local total_input_rate = 0 + local max_water_return_rate = 0 + for i = 1, #self.turbines do + local turbine = self.turbines[i].get_db() ---@type turbine_session_db + total_flow_rate = total_flow_rate + turbine.state.flow_rate + total_input_rate = total_input_rate + turbine.state.steam_input_rate + max_water_return_rate = max_water_return_rate + turbine.build.max_water_output + end + + -- check for steam feed mismatch and max return rate + local sfmismatch = math.abs(total_flow_rate - total_input_rate) > 10 + sfmismatch = sfmismatch or boiler_steam_dt_sum > 0 or boiler_water_dt_sum < 0 + self.db.annunciator.SteamFeedMismatch = sfmismatch + self.db.annunciator.MaxWaterReturnFeed = max_water_return_rate == total_flow_rate + + -- check if steam dumps are open + for i = 1, #self.turbines do + local turbine = self.turbines[i] ---@type unit_session + local db = turbine.get_db() ---@type turbine_session_db + local idx = turbine.get_device_idx() + + if db.state.dumping_mode == DUMPING_MODE.IDLE then + self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.OK + elseif db.state.dumping_mode == DUMPING_MODE.DUMPING_EXCESS then + self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.PARTIAL + else + self.db.annunciator.SteamDumpOpen[idx] = TRI_FAIL.FULL + end + end + + -- check if turbines are at max speed but not keeping up + for i = 1, #self.turbines do + local turbine = self.turbines[i] ---@type unit_session + local db = turbine.get_db() ---@type turbine_session_db + local idx = turbine.get_device_idx() + + self.db.annunciator.TurbineOverSpeed[idx] = (db.state.flow_rate == db.build.max_flow_rate) and (_get_dt(DT_KEYS.TurbineSteam .. idx) > 0) + end --[[ Turbine Trip - a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool + a turbine trip is when the turbine stops, which means we are no longer receiving water and lose the ability to cool. this can be identified by these conditions: - the current flow rate is 0 mB/t and it should not be - - it should not be if the boiler or reactor has a non-zero heating rate - can initially catch this by detecting a 0 flow rate with a non-zero input rate, but eventually the steam will fill up - can later identified by presence of steam in tank with a 0 flow rate ]]-- + for i = 1, #self.turbines do + local turbine = self.turbines[i] ---@type unit_session + local db = turbine.get_db() ---@type turbine_session_db + + local has_steam = db.state.steam_input_rate > 0 or db.tanks.steam_fill > 0.01 + self.db.annunciator.TurbineTrip[turbine.get_device_idx()] = has_steam and db.state.flow_rate == 0 + end end -- unlink disconnected units @@ -173,20 +322,47 @@ unit.new = function (for_reactor, num_boilers, num_turbines) ---@param plc_session plc_session_struct public.link_plc_session = function (plc_session) self.plc_s = plc_session - self.deltas.last_reactor_temp = self.plc_s.get_db().mek_status.temp - self.deltas.last_reactor_temp_time = util.time_s() + + -- reset deltas + _reset_dt(DT_KEYS.ReactorTemp) + _reset_dt(DT_KEYS.ReactorFuel) + _reset_dt(DT_KEYS.ReactorWaste) + _reset_dt(DT_KEYS.ReactorCCool) + _reset_dt(DT_KEYS.ReactorHCool) end -- link a turbine RTU session ---@param turbine unit_session public.add_turbine = function (turbine) - table.insert(self.turbines, turbine) + if #self.turbines < self.num_turbines and turbine.get_device_idx() <= self.num_turbines then + table.insert(self.turbines, turbine) + + -- reset deltas + _reset_dt(DT_KEYS.TurbineSteam .. turbine.get_device_idx()) + _reset_dt(DT_KEYS.TurbinePower .. turbine.get_device_idx()) + + return true + else + return false + end end -- link a boiler RTU session ---@param boiler unit_session public.add_boiler = function (boiler) - table.insert(self.boilers, boiler) + if #self.boilers < self.num_boilers and boiler.get_device_idx() <= self.num_boilers then + table.insert(self.boilers, boiler) + + -- reset deltas + _reset_dt(DT_KEYS.BoilerWater .. boiler.get_device_idx()) + _reset_dt(DT_KEYS.BoilerSteam .. boiler.get_device_idx()) + _reset_dt(DT_KEYS.BoilerCCool .. boiler.get_device_idx()) + _reset_dt(DT_KEYS.BoilerHCool .. boiler.get_device_idx()) + + return true + else + return false + end end -- link a redstone RTU capability @@ -200,7 +376,7 @@ unit.new = function (for_reactor, num_boilers, num_turbines) table.insert(self.redstone[field], accessor) end - -- update (iterate) this session + -- update (iterate) this unit public.update = function () -- unlink PLC if session was closed if not self.plc_s.open then @@ -215,6 +391,66 @@ unit.new = function (for_reactor, num_boilers, num_turbines) _update_annunciator() end + -- get build properties of all machines + public.get_build = function () + local build = {} + + if self.plc_s ~= nil then + build.reactor = self.plc_s.get_struct() + end + + build.boilers = {} + for i = 1, #self.boilers do + table.insert(build.boilers, self.boilers[i].get_db().build) + end + + build.turbines = {} + for i = 1, #self.turbines do + table.insert(build.turbines, self.turbines[i].get_db().build) + end + + return build + end + + -- get reactor status + public.get_reactor_status = function () + local status = {} + + if self.plc_s ~= nil then + local reactor = self.plc_s + status.mek = reactor.get_status() + status.rps = reactor.get_rps() + status.general = reactor.get_general_status() + end + + return status + end + + -- get RTU statuses + public.get_rtu_statuses = function () + local status = {} + + -- status of boilers (including tanks) + status.boilers = {} + for i = 1, #self.boilers do + table.insert(status.boilers, { + state = self.boilers[i].get_db().state, + tanks = self.boilers[i].get_db().tanks, + }) + end + + -- status of turbines (including tanks) + status.turbines = {} + for i = 1, #self.turbines do + table.insert(status.turbines, { + state = self.turbines[i].get_db().state, + tanks = self.turbines[i].get_db().tanks, + }) + end + + return status + end + -- get the annunciator status public.get_annunciator = function () return self.db.annunciator end From 940ddf0d0083c2bea95721737ced158538b5ac24 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 21 May 2022 12:30:14 -0400 Subject: [PATCH 153/168] function for duplicate session search code --- supervisor/session/svsessions.lua | 49 +++++++++++-------------------- 1 file changed, 17 insertions(+), 32 deletions(-) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index b004a38..409753c 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -106,6 +106,17 @@ local function _free_closed(sessions) util.filter_table(sessions, f, on_delete) end +-- find a session by remote port +---@param list table +---@param port integer +---@return plc_session_struct|rtu_session_struct|nil +local function _find_session(list, port) + for i = 1, #list do + if list[i].r_port == port then return list[i] end + end + return nil +end + -- PUBLIC FUNCTIONS -- -- link the modem @@ -119,13 +130,7 @@ end ---@return rtu_session_struct|nil svsessions.find_rtu_session = function (remote_port) -- check RTU sessions - for i = 1, #self.rtu_sessions do - if self.rtu_sessions[i].r_port == remote_port then - return self.rtu_sessions[i] - end - end - - return nil + return _find_session(self.rtu_sessions, remote_port) end -- find a PLC session by the remote port @@ -133,13 +138,7 @@ end ---@return plc_session_struct|nil svsessions.find_plc_session = function (remote_port) -- check PLC sessions - for i = 1, #self.plc_sessions do - if self.plc_sessions[i].r_port == remote_port then - return self.plc_sessions[i] - end - end - - return nil + return _find_session(self.plc_sessions, remote_port) end -- find a PLC/RTU session by the remote port @@ -147,20 +146,12 @@ end ---@return plc_session_struct|rtu_session_struct|nil svsessions.find_device_session = function (remote_port) -- check RTU sessions - for i = 1, #self.rtu_sessions do - if self.rtu_sessions[i].r_port == remote_port then - return self.rtu_sessions[i] - end - end + local s = _find_session(self.rtu_sessions, remote_port) -- check PLC sessions - for i = 1, #self.plc_sessions do - if self.plc_sessions[i].r_port == remote_port then - return self.plc_sessions[i] - end - end + if s == nil then s = _find_session(self.plc_sessions, remote_port) end - return nil + return s end -- find a coordinator session by the remote port @@ -168,13 +159,7 @@ end ---@return nil svsessions.find_coord_session = function (remote_port) -- check coordinator sessions - for i = 1, #self.coord_sessions do - if self.coord_sessions[i].r_port == remote_port then - return self.coord_sessions[i] - end - end - - return nil + return _find_session(self.coord_sessions, remote_port) end -- get a session by reactor ID From 3b16d783d3e6fa4633ed31a75063c49cd2b974af Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 21 May 2022 13:55:22 -0400 Subject: [PATCH 154/168] fixed bug with RSIO channel valid check --- scada-common/rsio.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 5b9970a..8f981cd 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -205,7 +205,7 @@ local RS_SIDES = rs.getSides() ---@param channel RS_IO ---@return boolean valid rsio.is_valid_channel = function (channel) - return (channel ~= nil) and (channel > 0) and (channel <= #RS_IO) + return (channel ~= nil) and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT) end -- check if a side is valid From 26c6010ce0f32943616647d52dddeab721e62362 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 21 May 2022 13:56:14 -0400 Subject: [PATCH 155/168] #56 pcall threads and restart on crash (unless shutting down) --- reactor-plc/startup.lua | 6 +- reactor-plc/threads.lua | 122 ++++++++++++++++++++++++++++++++++++---- rtu/startup.lua | 6 +- rtu/threads.lua | 69 +++++++++++++++++++++-- 4 files changed, 181 insertions(+), 22 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 86e7722..c401961 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.6.9" +local R_PLC_VERSION = "alpha-v0.7.0" local print = util.print local println = util.println @@ -156,7 +156,7 @@ if __shared_memory.networked then local sp_ctrl_thread = threads.thread__setpoint_control(__shared_memory) -- run threads - parallel.waitForAll(main_thread.exec, rps_thread.exec, comms_thread_tx.exec, comms_thread_rx.exec, sp_ctrl_thread.exec) + parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec, comms_thread_tx.p_exec, comms_thread_rx.p_exec, sp_ctrl_thread.p_exec) if plc_state.init_ok then -- send status one last time after RPS shutdown @@ -168,7 +168,7 @@ if __shared_memory.networked then end else -- run threads, excluding comms - parallel.waitForAll(main_thread.exec, rps_thread.exec) + parallel.waitForAll(main_thread.p_exec, rps_thread.p_exec) end println_ts("exited") diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 9531792..561f72c 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -31,8 +31,10 @@ local MQ__COMM_CMD = { ---@param smem plc_shared_memory ---@param init function threads.thread__main = function (smem, init) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("main thread init, clock inactive") -- send status updates at 2Hz (every 10 server ticks) (every loop tick) @@ -183,14 +185,38 @@ threads.thread__main = function (smem, init) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local plc_state = smem.plc_state + + while not plc_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + -- if status is true, then we are probably exiting, so this won't matter + -- if not, we need to restart the clock + -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) + if not plc_state.shutdown then + log.info("main thread restarting now...") + +---@diagnostic disable-next-line: undefined-field + os.queueEvent("clock_start") + end + end + end + + return public end -- RPS operation thread ---@param smem plc_shared_memory threads.thread__rps = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("rps thread start") -- load in from shared memory @@ -301,14 +327,35 @@ threads.thread__rps = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local plc_state = smem.plc_state + local rps = smem.plc_sys.rps + + while not plc_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not plc_state.shutdown then + if plc_state.init_ok then rps.scram() end + log.info("rps thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end -- communications sender thread ---@param smem plc_shared_memory threads.thread__comms_tx = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("comms tx thread start") -- load in from shared memory @@ -355,14 +402,33 @@ threads.thread__comms_tx = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local plc_state = smem.plc_state + + while not plc_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not plc_state.shutdown then + log.info("comms tx thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end -- communications handler thread ---@param smem plc_shared_memory threads.thread__comms_rx = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("comms rx thread start") -- load in from shared memory @@ -408,14 +474,33 @@ threads.thread__comms_rx = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local plc_state = smem.plc_state + + while not plc_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not plc_state.shutdown then + log.info("comms rx thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end -- apply setpoints ---@param smem plc_shared_memory threads.thread__setpoint_control = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("setpoint control thread start") -- load in from shared memory @@ -511,7 +596,24 @@ threads.thread__setpoint_control = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local plc_state = smem.plc_state + + while not plc_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not plc_state.shutdown then + log.info("setpoint control thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end return threads diff --git a/rtu/startup.lua b/rtu/startup.lua index 90578ce..003a2d6 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.6.8" +local RTU_VERSION = "alpha-v0.7.0" local rtu_t = types.rtu_t @@ -272,10 +272,10 @@ local main_thread = threads.thread__main(__shared_memory) local comms_thread = threads.thread__comms(__shared_memory) -- assemble thread list -local _threads = { main_thread.exec, comms_thread.exec } +local _threads = { main_thread.p_exec, comms_thread.p_exec } for i = 1, #units do if units[i].thread ~= nil then - table.insert(_threads, units[i].thread.exec) + table.insert(_threads, units[i].thread.p_exec) end end diff --git a/rtu/threads.lua b/rtu/threads.lua index a3d19d7..3d83acf 100644 --- a/rtu/threads.lua +++ b/rtu/threads.lua @@ -28,8 +28,10 @@ local COMMS_SLEEP = 100 -- (100ms, 2 ticks) -- main thread ---@param smem rtu_shared_memory threads.thread__main = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("main thread start") -- main loop clock @@ -152,14 +154,33 @@ threads.thread__main = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local rtu_state = smem.rtu_state + + while not rtu_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not rtu_state.shutdown then + log.info("main thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end -- communications handler thread ---@param smem rtu_shared_memory threads.thread__comms = function (smem) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("comms thread start") -- load in from shared memory @@ -205,15 +226,34 @@ threads.thread__comms = function (smem) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local rtu_state = smem.rtu_state + + while not rtu_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not rtu_state.shutdown then + log.info("comms thread restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end -- per-unit communications handler thread ---@param smem rtu_shared_memory ---@param unit rtu_unit_registry_entry threads.thread__unit_comms = function (smem, unit) + local public = {} ---@class thread + -- execute thread - local exec = function () + public.exec = function () log.debug("rtu unit thread start -> " .. unit.name .. "(" .. unit.type .. ")") -- load in from shared memory @@ -256,7 +296,24 @@ threads.thread__unit_comms = function (smem, unit) end end - return { exec = exec } + -- execute the thread in a protected mode, retrying it on return if not shutting down + public.p_exec = function () + local rtu_state = smem.rtu_state + + while not rtu_state.shutdown do + local status, result = pcall(public.exec) + if status == false then + log.fatal(result) + end + + if not rtu_state.shutdown then + log.info("rtu unit thread " .. unit.name .. "(" .. unit.type .. ") restarting in 5 seconds...") + util.psleep(5) + end + end + end + + return public end return threads From a93f0a4452cf796be3172cabd46480dc1d65b131 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 22 May 2022 17:57:24 -0400 Subject: [PATCH 156/168] #57 updates per safety pass, fixed plc_sys fields staying nil on degraded start, fixed repeated SCRAM messages when unlinked --- reactor-plc/plc.lua | 2 +- reactor-plc/startup.lua | 2 +- reactor-plc/threads.lua | 54 +++++++++++++++++++++++------------------ scada-common/log.lua | 6 +++++ scada-common/ppm.lua | 8 +++--- scada-common/util.lua | 6 +++-- 6 files changed, 47 insertions(+), 31 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index db04467..b5d36b5 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -155,7 +155,7 @@ plc.rps_init = function (reactor) -- trip for a PLC comms timeout public.trip_timeout = function () - self.state[state_keys.timed_out] = true + self.state[state_keys.timeout] = true end -- manually SCRAM the reactor diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index c401961..8a2d296 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.7.0" +local R_PLC_VERSION = "alpha-v0.7.1" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 561f72c..f28c775 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -47,12 +47,14 @@ threads.thread__main = function (smem, init) local networked = smem.networked local plc_state = smem.plc_state local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps - local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog -- event loop while true do + -- get plc_sys fields (may have been set late due to degraded boot) + local rps = smem.plc_sys.rps + local plc_comms = smem.plc_sys.plc_comms + local conn_watchdog = smem.plc_sys.conn_watchdog + ---@diagnostic disable-next-line: undefined-field local event, param1, param2, param3, param4, param5 = os.pullEventRaw() @@ -77,14 +79,14 @@ threads.thread__main = function (smem, init) end end end - elseif event == "modem_message" and networked and not plc_state.no_modem then + elseif event == "modem_message" and networked and plc_state.init_ok and not plc_state.no_modem then -- got a packet local packet = plc_comms.parse_packet(param1, param2, param3, param4, param5) if packet ~= nil then -- pass the packet onto the comms message queue smem.q.mq_comms_rx.push_packet(packet) end - elseif event == "timer" and networked and conn_watchdog.is_timer(param1) then + elseif event == "timer" and networked and plc_state.init_ok and conn_watchdog.is_timer(param1) then -- haven't heard from server recently? shutdown reactor plc_comms.unlink() smem.q.mq_rps.push_command(MQ__RPS_CMD.TRIP_TIMEOUT) @@ -128,7 +130,7 @@ threads.thread__main = function (smem, init) smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM) println_ts("reactor reconnected.") - log.info("reactor reconnected.") + log.info("reactor reconnected") plc_state.no_reactor = false if plc_state.init_ok then @@ -139,7 +141,7 @@ threads.thread__main = function (smem, init) end -- determine if we are still in a degraded state - if not networked or ppm.get_device("modem") ~= nil then + if not networked or not plc_state.no_modem then plc_state.degraded = false end elseif networked and type == "modem" then @@ -152,19 +154,20 @@ threads.thread__main = function (smem, init) end println_ts("wireless modem reconnected.") - log.info("comms modem reconnected.") + log.info("comms modem reconnected") plc_state.no_modem = false -- determine if we are still in a degraded state - if ppm.get_device("fissionReactor") ~= nil then + if not plc_state.no_reactor then plc_state.degraded = false end else - log.info("wired modem reconnected.") + log.info("wired modem reconnected") end end end + -- if not init'd and no longer degraded, proceed to init if not plc_state.init_ok and not plc_state.degraded then plc_state.init_ok = true init() @@ -223,8 +226,6 @@ threads.thread__rps = function (smem) local networked = smem.networked local plc_state = smem.plc_state local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps - local plc_comms = smem.plc_sys.plc_comms local rps_queue = smem.q.mq_rps @@ -233,13 +234,16 @@ threads.thread__rps = function (smem) -- thread loop while true do - local reactor = plc_dev.reactor + -- get plc_sys fields (may have been set late due to degraded boot) + local rps = smem.plc_sys.rps + local plc_comms = smem.plc_sys.plc_comms + -- get reactor, may have changed do to disconnect/reconnect + local reactor = plc_dev.reactor -- RPS checks if plc_state.init_ok then -- SCRAM if no open connection if networked and not plc_comms.is_linked() then - rps.scram() if was_linked then was_linked = false rps.trip_timeout() @@ -264,7 +268,7 @@ threads.thread__rps = function (smem) if not plc_state.no_reactor then local rps_tripped, rps_status_string, rps_first = rps.check() - if rps_first then + if rps_tripped and rps_first then println_ts("[RPS] SCRAM! safety trip: " .. rps_status_string) if networked and not plc_state.no_modem then plc_comms.send_rps_alarm(rps_status_string) @@ -330,7 +334,6 @@ threads.thread__rps = function (smem) -- execute the thread in a protected mode, retrying it on return if not shutting down public.p_exec = function () local plc_state = smem.plc_state - local rps = smem.plc_sys.rps while not plc_state.shutdown do local status, result = pcall(public.exec) @@ -339,7 +342,7 @@ threads.thread__rps = function (smem) end if not plc_state.shutdown then - if plc_state.init_ok then rps.scram() end + if plc_state.init_ok then smem.plc_sys.rps.scram() end log.info("rps thread restarting in 5 seconds...") util.psleep(5) end @@ -360,19 +363,20 @@ threads.thread__comms_tx = function (smem) -- load in from shared memory local plc_state = smem.plc_state - local plc_comms = smem.plc_sys.plc_comms - local comms_queue = smem.q.mq_comms_tx local last_update = util.time() -- thread loop while true do + -- get plc_sys fields (may have been set late due to degraded boot) + local plc_comms = smem.plc_sys.plc_comms + -- check for messages in the message queue while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg ~= nil then + if msg ~= nil and plc_state.init_ok then if msg.qtype == mqueue.TYPE.COMMAND then -- received a command if msg.message == MQ__COMM_CMD.SEND_STATUS then @@ -434,7 +438,6 @@ threads.thread__comms_rx = function (smem) -- load in from shared memory local plc_state = smem.plc_state local setpoints = smem.setpoints - local plc_comms = smem.plc_sys.plc_comms local comms_queue = smem.q.mq_comms_rx @@ -442,11 +445,14 @@ threads.thread__comms_rx = function (smem) -- thread loop while true do + -- get plc_sys fields (may have been set late due to degraded boot) + local plc_comms = smem.plc_sys.plc_comms + -- check for messages in the message queue while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg ~= nil then + if msg ~= nil and plc_state.init_ok then if msg.qtype == mqueue.TYPE.COMMAND then -- received a command elseif msg.qtype == mqueue.TYPE.DATA then @@ -507,7 +513,6 @@ threads.thread__setpoint_control = function (smem) local plc_state = smem.plc_state local setpoints = smem.setpoints local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps local last_update = util.time() local running = false @@ -520,6 +525,9 @@ threads.thread__setpoint_control = function (smem) -- thread loop while true do + -- get plc_sys fields (may have been set late due to degraded boot) + local rps = smem.plc_sys.rps + -- get reactor, may have changed do to disconnect/reconnect local reactor = plc_dev.reactor if plc_state.init_ok and not plc_state.no_reactor then diff --git a/scada-common/log.lua b/scada-common/log.lua index 3ea4262..d8d245b 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -137,6 +137,7 @@ end ---@param msg string message ---@param show_term? boolean whether or not to show on terminal output log.dmesg = function (msg, show_term) + if msg == nil then return end local message = string.format("[%10.3f] ", os.clock()) .. msg if show_term then _write(message) end _log(message) @@ -146,6 +147,7 @@ end ---@param msg string message ---@param trace? boolean include file trace log.debug = function (msg, trace) + if msg == nil then return end if LOG_DEBUG then local dbg_info = "" @@ -167,12 +169,14 @@ end -- log info messages ---@param msg string message log.info = function (msg) + if msg == nil then return end _log("[INF] " .. msg) end -- log warning messages ---@param msg string message log.warning = function (msg) + if msg == nil then return end _log("[WRN] " .. msg) end @@ -180,6 +184,7 @@ end ---@param msg string message ---@param trace? boolean include file trace log.error = function (msg, trace) + if msg == nil then return end local dbg_info = "" if trace then @@ -199,6 +204,7 @@ end -- log fatal errors ---@param msg string message log.fatal = function (msg) + if msg == nil then return end _log("[FTL] " .. msg) end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index fd84503..d07e703 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -189,12 +189,12 @@ ppm.mount = function (iface) for i = 1, #ifaces do if iface == ifaces[i] then - log.info("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) - _ppm_sys.mounts[iface] = peri_init(iface) pm_type = _ppm_sys.mounts[iface].type pm_dev = _ppm_sys.mounts[iface].dev + + log.info("PPM: mount(" .. iface .. ") -> found a " .. pm_type) break end end @@ -262,7 +262,7 @@ end ppm.get_all_devices = function (name) local devices = {} - for side, data in pairs(_ppm_sys.mounts) do + for _, data in pairs(_ppm_sys.mounts) do if data.type == name then table.insert(devices, data.dev) end @@ -300,7 +300,7 @@ end ppm.get_wireless_modem = function () local w_modem = nil - for side, device in pairs(_ppm_sys.mounts) do + for _, device in pairs(_ppm_sys.mounts) do if device.type == "modem" and device.dev.isWireless() then w_modem = device.dev break diff --git a/scada-common/util.lua b/scada-common/util.lua index 9e7a4a7..34c10ee 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -19,11 +19,13 @@ end -- timestamped print util.print_ts = function (message) + if message == nil then return end term.write(os.date("[%H:%M:%S] ") .. message) end -- timestamped print line util.println_ts = function (message) + if message == nil then return end print(os.date("[%H:%M:%S] ") .. message) end @@ -37,10 +39,10 @@ util.time_ms = function () end -- current time ----@return integer seconds +---@return number seconds util.time_s = function () ---@diagnostic disable-next-line: undefined-field - return os.epoch('local') / 1000 + return os.epoch('local') / 1000.0 end -- current time From 9fb6b7a8800a67ab9d043908dd773a88245a1bff Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 23 May 2022 17:36:54 -0400 Subject: [PATCH 157/168] #9 rsio test code, fixes per test results --- scada-common/rsio.lua | 12 ++-- test/rstest.lua | 149 ++++++++++++++++++++++++++++++++++++++++++ test/testutils.lua | 65 ++++++++++++++++++ 3 files changed, 220 insertions(+), 6 deletions(-) create mode 100644 test/rstest.lua create mode 100644 test/testutils.lua diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 8f981cd..fc76e0b 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -101,7 +101,7 @@ rsio.to_string = function (channel) "R_PLC_TIMEOUT" } - if channel > 0 and channel <= #names then + if type(channel) == "number" and channel > 0 and channel <= #names then return names[channel] else return "" @@ -188,7 +188,7 @@ rsio.get_io_mode = function (channel) IO_MODE.DIGITAL_OUT -- R_PLC_TIMEOUT } - if channel > 0 and channel <= #modes then + if type(channel) == "number" and channel > 0 and channel <= #modes then return modes[channel] else return IO_MODE.ANALOG_IN @@ -205,7 +205,7 @@ local RS_SIDES = rs.getSides() ---@param channel RS_IO ---@return boolean valid rsio.is_valid_channel = function (channel) - return (channel ~= nil) and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT) + return (type(channel) == "number") and (channel > 0) and (channel <= RS_IO.R_PLC_TIMEOUT) end -- check if a side is valid @@ -224,7 +224,7 @@ end ---@param color integer ---@return boolean valid rsio.is_color = function (color) - return (color > 0) and (_B_AND(color, (color - 1)) == 0); + return (type(color) == "number") and (color > 0) and (_B_AND(color, (color - 1)) == 0); end ----------------- @@ -247,7 +247,7 @@ end ---@param active boolean ---@return IO_LVL rsio.digital_write = function (channel, active) - if channel < RS_IO.WASTE_PO or channel > RS_IO.R_PLC_TIMEOUT then + if type(channel) ~= "number" or channel < RS_IO.F_ALARM or channel > RS_IO.R_PLC_TIMEOUT then return IO_LVL.LOW else return RS_DIO_MAP[channel]._f(active) @@ -259,7 +259,7 @@ end ---@param level IO_LVL ---@return boolean rsio.digital_is_active = function (channel, level) - if channel > RS_IO.R_ENABLE or channel > RS_IO.R_PLC_TIMEOUT then + if type(channel) ~= "number" or channel > RS_IO.R_ENABLE then return false else return RS_DIO_MAP[channel]._f(level) diff --git a/test/rstest.lua b/test/rstest.lua new file mode 100644 index 0000000..629baae --- /dev/null +++ b/test/rstest.lua @@ -0,0 +1,149 @@ +require("/initenv").init_env() + +local rsio = require("scada-common.rsio") +local util = require("scada-common.util") + +local testutils = require("test.testutils") + +local print = util.print +local println = util.println + +local IO = rsio.IO +local IO_LVL = rsio.IO_LVL +local IO_DIR = rsio.IO_DIR +local IO_MODE = rsio.IO_MODE + +println("starting RSIO tester") +println("") + +println(">>> checking valid channels:") + +-- channel function tests +local cid = 0 +local max_value = 1 +for key, value in pairs(IO) do + if value > max_value then max_value = value end + cid = cid + 1 + + local c_name = rsio.to_string(value) + local io_mode = rsio.get_io_mode(value) + local mode = "" + + if io_mode == IO_MODE.DIGITAL_IN then + mode = " (DIGITAL_IN)" + elseif io_mode == IO_MODE.DIGITAL_OUT then + mode = " (DIGITAL_OUT)" + elseif io_mode == IO_MODE.ANALOG_IN then + mode = " (ANALOG_IN)" + elseif io_mode == IO_MODE.ANALOG_OUT then + mode = " (ANALOG_OUT)" + else + error("unknown mode for channel " .. key) + end + + assert(key == c_name, c_name .. " != " .. key .. ": " .. value .. mode) + println(c_name .. ": " .. value .. mode) +end + +assert(max_value == cid, "RS_IO last IDx out-of-sync with count: " .. max_value .. " (count " .. cid .. ")") + +---@diagnostic disable-next-line: undefined-field +os.sleep(1) + +println(">>> checking invalid channels:") + +testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "") +testutils.test_func_nil("rsio.to_string", rsio.to_string, "") +testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN) +testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN) + +---@diagnostic disable-next-line: undefined-field +os.sleep(1) + +println(">>> checking validity checks:") + +local ivc_t_list = { 0, -1, 100 } +testutils.test_func("rsio.is_valid_channel", rsio.is_valid_channel, ivc_t_list, false) +testutils.test_func_nil("rsio.is_valid_channel", rsio.is_valid_channel, false) + +local ivs_t_list = rs.getSides() +testutils.test_func("rsio.is_valid_side", rsio.is_valid_side, ivs_t_list, true) +testutils.test_func("rsio.is_valid_side", rsio.is_valid_side, { "" }, false) +testutils.test_func_nil("rsio.is_valid_side", rsio.is_valid_side, false) + +local ic_t_list = { colors.white, colors.purple, colors.blue, colors.cyan, colors.black } +testutils.test_func("rsio.is_color", rsio.is_color, ic_t_list, true) +testutils.test_func("rsio.is_color", rsio.is_color, { 0, 999999, colors.combine(colors.red, colors.blue, colors.black) }, false) +testutils.test_func_nil("rsio.is_color", rsio.is_color, false) + +---@diagnostic disable-next-line: undefined-field +os.sleep(1) + +println(">>> checking channel-independent I/O wrappers:") + +testutils.test_func("rsio.digital_read", rsio.digital_read, { true, false }, { IO_LVL.HIGH, IO_LVL.LOW }) + +print("rsio.analog_read(): ") +assert(rsio.analog_read(0, 0, 100) == 0, "RS_READ_0_100") +assert(rsio.analog_read(7.5, 0, 100) == 50, "RS_READ_7_5_100") +assert(rsio.analog_read(15, 0, 100) == 100, "RS_READ_15_100") +assert(rsio.analog_read(4, 0, 15) == 4, "RS_READ_4_15") +assert(rsio.analog_read(12, 0, 15) == 12, "RS_READ_12_15") +println("PASS") + +print("rsio.analog_write(): ") +assert(rsio.analog_write(0, 0, 100) == 0, "RS_WRITE_0_100") +assert(rsio.analog_write(100, 0, 100) == 15, "RS_WRITE_100_100") +assert(rsio.analog_write(4, 0, 15) == 4, "RS_WRITE_4_15") +assert(rsio.analog_write(12, 0, 15) == 12, "RS_WRITE_12_15") +println("PASS") + +---@diagnostic disable-next-line: undefined-field +os.sleep(1) + +println(">>> checking channel I/O:") + +print("rsio.digital_is_active(...): ") + +-- check input channels +assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH") +assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW") +assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH") +assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.HIGH) == false, "IO_R_SCRAM_LOW") +assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.LOW) == false, "IO_R_ENABLE_HIGH") +assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.HIGH) == true, "IO_R_ENABLE_LOW") + +-- non-inputs should always return LOW +assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.LOW) == false, "IO_OUT_READ_LOW") +assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HIGH") + +println("PASS") + +-- check output channels + +print("rsio.digital_write(...): ") + +-- check output channels +assert(rsio.digital_write(IO.F_ALARM, false) == IO_LVL.LOW, "IO_F_ALARM_FALSE") +assert(rsio.digital_write(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_TRUE") +assert(rsio.digital_write(IO.WASTE_PO, false) == IO_LVL.HIGH, "IO_WASTE_PO_FALSE") +assert(rsio.digital_write(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_TRUE") +assert(rsio.digital_write(IO.WASTE_PU, false) == IO_LVL.HIGH, "IO_WASTE_PU_FALSE") +assert(rsio.digital_write(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_TRUE") +assert(rsio.digital_write(IO.WASTE_AM, false) == IO_LVL.HIGH, "IO_WASTE_AM_FALSE") +assert(rsio.digital_write(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_TRUE") + +-- check all reactor output channels (all are active high) +for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do + assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_CHANNEL") + assert(rsio.digital_write(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_FALSE") + assert(rsio.digital_write(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_TRUE") +end + +-- non-outputs should always return false +assert(rsio.digital_write(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_LOW") +assert(rsio.digital_write(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_HIGH") + +println("PASS") + +println("TEST COMPLETE") diff --git a/test/testutils.lua b/test/testutils.lua new file mode 100644 index 0000000..a27875d --- /dev/null +++ b/test/testutils.lua @@ -0,0 +1,65 @@ +local util = require("scada-common.util") + +local print = util.print +local println = util.println + +local testutils = {} + +-- get a value as a string +---@param val any +---@return string value value as string or "%VALSTR_UNKNOWN%" +local function valstr(val) + local t = type(val) + + if t == "nil" then + return "nil" + elseif t == "number" then + return "" .. val + elseif t == "boolean" then + if val then return "true" else return "false" end + elseif t == "string" then + return val + elseif t == "table" or t == "function" then + return val + else + return "%VALSTR_UNKNOWN%" + end +end + +-- test a function +---@param name string function name +---@param f function function +---@param values table input values, one per function call +---@param results any table of values or a single value for all tests +function testutils.test_func(name, f, values, results) + -- if only one value was given, use that for all checks + if type(results) ~= "table" then + local _r = {} + for _ = 1, #values do + table.insert(_r, results) + end + results = _r + end + + assert(#values == #results, "test_func(" .. name .. ") #values ~= #results") + + for i = 1, #values do + local check = values[i] + local expect = results[i] + print(name .. "(" .. valstr(check) .. ") => ") + assert(f(check) == expect, "FAIL") + println("PASS") + end +end + +-- test a function with nil as a parameter +---@param name string function name +---@param f function function +---@param result any expected result +function testutils.test_func_nil(name, f, result) + print(name .. "(" .. valstr(nil) .. ") => ") + assert(f(nil) == result, "FAIL") + println("PASS") +end + +return testutils From 0cf81040fb66d1635366fc310b32714e325633a6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 24 May 2022 22:48:31 -0400 Subject: [PATCH 158/168] moved string value to util and added sprtinf, adjusted prints to use tostring to prevent concatentation errors on some types --- scada-common/util.lua | 46 +++++++++++++++++++++++++++++++++++++------ test/testutils.lua | 25 ++--------------------- 2 files changed, 42 insertions(+), 29 deletions(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index 34c10ee..c87ff3b 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -8,25 +8,59 @@ local util = {} -- PRINT -- -- print +---@param message any util.print = function (message) - term.write(message) + term.write(tostring(message)) end -- print line +---@param message any util.println = function (message) - print(message) + print(tostring(message)) end -- timestamped print +---@param message any util.print_ts = function (message) - if message == nil then return end - term.write(os.date("[%H:%M:%S] ") .. message) + term.write(os.date("[%H:%M:%S] ") .. tostring(message)) end -- timestamped print line +---@param message any util.println_ts = function (message) - if message == nil then return end - print(os.date("[%H:%M:%S] ") .. message) + print(os.date("[%H:%M:%S] ") .. tostring(message)) +end + +-- STRING TOOLS -- + +-- get a value as a string +---@param val any +---@return string +util.strval = function (val) + local t = type(val) + if t == "table" or t == "function" then + return "[" .. tostring(val) .. "]" + else + return tostring(val) + end +end + +-- concatenation with built-in to string +---@vararg any +---@return string +util.concat = function (...) + local str = "" + for _, v in ipairs(arg) do + str = str .. util.strval(v) + end + return str +end + +-- sprintf implementation +---@param format string +---@vararg any +util.sprintf = function (format, ...) + return string.format(format, table.unpack(arg)) end -- TIME -- diff --git a/test/testutils.lua b/test/testutils.lua index a27875d..12c07f0 100644 --- a/test/testutils.lua +++ b/test/testutils.lua @@ -5,27 +5,6 @@ local println = util.println local testutils = {} --- get a value as a string ----@param val any ----@return string value value as string or "%VALSTR_UNKNOWN%" -local function valstr(val) - local t = type(val) - - if t == "nil" then - return "nil" - elseif t == "number" then - return "" .. val - elseif t == "boolean" then - if val then return "true" else return "false" end - elseif t == "string" then - return val - elseif t == "table" or t == "function" then - return val - else - return "%VALSTR_UNKNOWN%" - end -end - -- test a function ---@param name string function name ---@param f function function @@ -46,7 +25,7 @@ function testutils.test_func(name, f, values, results) for i = 1, #values do local check = values[i] local expect = results[i] - print(name .. "(" .. valstr(check) .. ") => ") + print(name .. "(" .. util.strval(check) .. ") => ") assert(f(check) == expect, "FAIL") println("PASS") end @@ -57,7 +36,7 @@ end ---@param f function function ---@param result any expected result function testutils.test_func_nil(name, f, result) - print(name .. "(" .. valstr(nil) .. ") => ") + print(name .. "(nil) => ") assert(f(nil) == result, "FAIL") println("PASS") end From 4b6a1c59020bc6f222d641f0e8331cc6dbfbab9d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 24 May 2022 22:55:27 -0400 Subject: [PATCH 159/168] fixed incorrect watchdog call --- supervisor/session/plc.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index bf79f23..5279208 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -204,7 +204,7 @@ plc.new_session = function (id, for_reactor, in_queue, out_queue) -- mark this PLC session as closed, stop watchdog local _close = function () - self.rtu_conn_watchdog.cancel() + self.plc_conn_watchdog.cancel() self.connected = false end From ffc997b84ea3724a729f30d762a9e9933458473b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 24 May 2022 22:56:41 -0400 Subject: [PATCH 160/168] removed redundant version tag --- supervisor/supervisor.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 5bc3ce8..5236e0e 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -191,7 +191,7 @@ supervisor.comms = function (version, num_reactors, modem, dev_listen, coord_lis _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully - println("connected to reactor " .. packet.data[1] .. " PLC v " .. packet.data[2] .. " (port " .. r_port .. ")") + println("connected to reactor " .. packet.data[1] .. " PLC (" .. packet.data[2] .. ") [port " .. r_port .. "]") log.debug("PLC_LNK: allowed for device at " .. r_port) _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.ALLOW }) end @@ -215,7 +215,7 @@ supervisor.comms = function (version, num_reactors, modem, dev_listen, coord_lis elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then if packet.length >= 1 then -- this is an RTU advertisement for a new session - println("connected to RTU v " .. packet.data[1] .. " (port " .. r_port .. ")") + println("connected to RTU (" .. packet.data[1] .. ") [port " .. r_port .. "]") svsessions.establish_rtu_session(l_port, r_port, packet.data) From 4d7d3be93ba87e27a6f5d7d976302e2a11debfa6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 24 May 2022 22:58:42 -0400 Subject: [PATCH 161/168] power related utility functions put under util table --- scada-common/util.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index c87ff3b..7073d38 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -156,10 +156,10 @@ end -- MEKANISM POWER -- --- function kFE(fe) return fe / 1000 end --- function MFE(fe) return fe / 1000000 end --- function GFE(fe) return fe / 1000000000 end --- function TFE(fe) return fe / 1000000000000 end +-- function util.kFE(fe) return fe / 1000.0 end +-- function util.MFE(fe) return fe / 1000000.0 end +-- function util.GFE(fe) return fe / 1000000000.0 end +-- function util.TFE(fe) return fe / 1000000000000.0 end -- -- FLOATING POINT PRINTS -- @@ -177,7 +177,7 @@ end -- return number == math.round(number) -- end --- function power_format(fe) +-- function util.power_format(fe) -- if fe < 1000 then -- return string.format("%.2f FE", fe) -- elseif fe < 1000000 then From 7d7eecaa5e07262cd92b36a16879fa22e5b97e4e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 25 May 2022 23:24:15 -0400 Subject: [PATCH 162/168] log use strval --- scada-common/log.lua | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index d8d245b..8369d22 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -53,7 +53,7 @@ end ---@param msg string local _log = function (msg) local time_stamp = os.date("[%c] ") - local stamped = time_stamp .. msg + local stamped = time_stamp .. util.strval(msg) -- attempt to write log local status, result = pcall(function () @@ -137,8 +137,7 @@ end ---@param msg string message ---@param show_term? boolean whether or not to show on terminal output log.dmesg = function (msg, show_term) - if msg == nil then return end - local message = string.format("[%10.3f] ", os.clock()) .. msg + local message = string.format("[%10.3f] ", os.clock()) .. util.strval(msg) if show_term then _write(message) end _log(message) end @@ -147,7 +146,6 @@ end ---@param msg string message ---@param trace? boolean include file trace log.debug = function (msg, trace) - if msg == nil then return end if LOG_DEBUG then local dbg_info = "" @@ -162,29 +160,26 @@ log.debug = function (msg, trace) dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " end - _log("[DBG] " .. dbg_info .. msg) + _log("[DBG] " .. dbg_info .. util.strval(msg)) end end -- log info messages ---@param msg string message log.info = function (msg) - if msg == nil then return end - _log("[INF] " .. msg) + _log("[INF] " .. util.strval(msg)) end -- log warning messages ---@param msg string message log.warning = function (msg) - if msg == nil then return end - _log("[WRN] " .. msg) + _log("[WRN] " .. util.strval(msg)) end -- log error messages ---@param msg string message ---@param trace? boolean include file trace log.error = function (msg, trace) - if msg == nil then return end local dbg_info = "" if trace then @@ -198,14 +193,13 @@ log.error = function (msg, trace) dbg_info = info.short_src .. ":" .. name .. info.currentline .. " > " end - _log("[ERR] " .. dbg_info .. msg) + _log("[ERR] " .. dbg_info .. util.strval(msg)) end -- log fatal errors ---@param msg string message log.fatal = function (msg) - if msg == nil then return end - _log("[FTL] " .. msg) + _log("[FTL] " .. util.strval(msg)) end return log From 78ddd4d7823edae733ab5a9a30adc3856d40659a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 26 May 2022 17:49:43 -0400 Subject: [PATCH 163/168] #58 fixed bug with KEEP_ALIVE being sent as a LINK_REQ due to wrong protocol --- reactor-plc/plc.lua | 2 +- reactor-plc/startup.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index b5d36b5..9620636 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -429,7 +429,7 @@ plc.comms = function (id, version, modem, local_port, server_port, reactor, rps, -- keep alive ack ---@param srv_time integer local _send_keep_alive_ack = function (srv_time) - _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) + _send_mgmt(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 8a2d296..062db60 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -13,7 +13,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "alpha-v0.7.1" +local R_PLC_VERSION = "alpha-v0.7.2" local print = util.print local println = util.println From 214f2d90285b4885c19baf877dc99a896f1f043d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 26 May 2022 17:49:53 -0400 Subject: [PATCH 164/168] fixed supervisor clock not starting --- supervisor/session/svsessions.lua | 2 +- supervisor/startup.lua | 5 ++++- supervisor/supervisor.lua | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index 409753c..02bf70b 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -35,7 +35,7 @@ local self = { ---@param sessions table local function _iterate(sessions) for i = 1, #sessions do - local session = sessions[i] ---@type plc_session_struct + local session = sessions[i] ---@type plc_session_struct|rtu_session_struct if session.open then local ok = session.instance.iterate() if ok then diff --git a/supervisor/startup.lua b/supervisor/startup.lua index d23436d..307b8e6 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -13,7 +13,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "alpha-v0.3.10" +local SUPERVISOR_VERSION = "alpha-v0.4.0" local print = util.print local println = util.println @@ -44,6 +44,9 @@ local superv_comms = supervisor.comms(SUPERVISOR_VERSION, config.NUM_REACTORS, m local MAIN_CLOCK = 0.15 local loop_clock = util.new_clock(MAIN_CLOCK) +-- start clock +loop_clock.start() + -- event loop while true do ---@diagnostic disable-next-line: undefined-field diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 5236e0e..ba07d31 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -191,7 +191,7 @@ supervisor.comms = function (version, num_reactors, modem, dev_listen, coord_lis _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.COLLISION }) else -- got an ID; assigned to a reactor successfully - println("connected to reactor " .. packet.data[1] .. " PLC (" .. packet.data[2] .. ") [port " .. r_port .. "]") + println("connected to reactor " .. packet.data[1] .. " PLC (" .. packet.data[2] .. ") [:" .. r_port .. "]") log.debug("PLC_LNK: allowed for device at " .. r_port) _send_plc_linking(next_seq_id, r_port, { RPLC_LINKING.ALLOW }) end @@ -215,7 +215,7 @@ supervisor.comms = function (version, num_reactors, modem, dev_listen, coord_lis elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then if packet.length >= 1 then -- this is an RTU advertisement for a new session - println("connected to RTU (" .. packet.data[1] .. ") [port " .. r_port .. "]") + println("connected to RTU (" .. packet.data[1] .. ") [:" .. r_port .. "]") svsessions.establish_rtu_session(l_port, r_port, packet.data) From 51111f707f8a8a9f21defa1784bb0d2d8540f4b7 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 26 May 2022 19:37:19 -0400 Subject: [PATCH 165/168] more descriptive comments --- scada-common/comms.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index c305b29..775127f 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -205,7 +205,7 @@ comms.modbus_packet = function () -- get raw to send public.raw_sendable = function () return self.raw end - -- get this packet + -- get this packet as a frame with an immutable relation to this object public.get = function () ---@class modbus_frame local frame = { @@ -298,7 +298,7 @@ comms.rplc_packet = function () -- get raw to send public.raw_sendable = function () return self.raw end - -- get this packet + -- get this packet as a frame with an immutable relation to this object public.get = function () ---@class rplc_frame local frame = { @@ -382,7 +382,7 @@ comms.mgmt_packet = function () -- get raw to send public.raw_sendable = function () return self.raw end - -- get this packet + -- get this packet as a frame with an immutable relation to this object public.get = function () ---@class mgmt_frame local frame = { @@ -463,7 +463,7 @@ comms.coord_packet = function () -- get raw to send public.raw_sendable = function () return self.raw end - -- get this packet + -- get this packet as a frame with an immutable relation to this object public.get = function () ---@class coord_frame local frame = { @@ -544,7 +544,7 @@ comms.capi_packet = function () -- get raw to send public.raw_sendable = function () return self.raw end - -- get this packet + -- get this packet as a frame with an immutable relation to this object public.get = function () ---@class capi_frame local frame = { From 6df0a1d149c309be722b022014e6175d9e611cfb Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 27 May 2022 18:10:06 -0400 Subject: [PATCH 166/168] #9 MODBUS test code; fixed rtu, modbus, redstone_rtu, and rsio bugs --- rtu/dev/redstone_rtu.lua | 8 +- rtu/modbus.lua | 35 +++--- rtu/rtu.lua | 12 +- rtu/startup.lua | 2 +- scada-common/rsio.lua | 58 +++++----- test/modbustest.lua | 236 +++++++++++++++++++++++++++++++++++++++ test/rstest.lua | 36 +++--- test/testutils.lua | 78 +++++++++++++ 8 files changed, 385 insertions(+), 80 deletions(-) create mode 100644 test/modbustest.lua diff --git a/rtu/dev/redstone_rtu.lua b/rtu/dev/redstone_rtu.lua index 2763e98..563886e 100644 --- a/rtu/dev/redstone_rtu.lua +++ b/rtu/dev/redstone_rtu.lua @@ -4,6 +4,7 @@ local rsio = require("scada-common.rsio") local redstone_rtu = {} local digital_read = rsio.digital_read +local digital_write = rsio.digital_write local digital_is_active = rsio.digital_is_active -- create new redstone device @@ -61,12 +62,11 @@ redstone_rtu.new = function () f_write = function (level) local output = rs.getBundledOutput(side) - local active = digital_is_active(channel, level) - if active then - colors.combine(output, color) + if digital_write(channel, level) then + output = colors.combine(output, color) else - colors.subtract(output, color) + output = colors.subtract(output, color) end rs.setBundledOutput(side, output) diff --git a/rtu/modbus.lua b/rtu/modbus.lua index efc0c84..2654405 100644 --- a/rtu/modbus.lua +++ b/rtu/modbus.lua @@ -28,7 +28,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local readings = {} local access_fault = false local _, coils, _, _ = self.rtu.io_count() - local return_ok = ((c_addr_start + count) <= coils) and (count > 0) + local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) if return_ok then for i = 1, count do @@ -74,7 +74,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local readings = {} local access_fault = false local discrete_inputs, _, _, _ = self.rtu.io_count() - local return_ok = ((di_addr_start + count) <= discrete_inputs) and (count > 0) + local return_ok = ((di_addr_start + count) <= (discrete_inputs + 1)) and (count > 0) if return_ok then for i = 1, count do @@ -120,7 +120,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local readings = {} local access_fault = false local _, _, _, hold_regs = self.rtu.io_count() - local return_ok = ((hr_addr_start + count) <= hold_regs) and (count > 0) + local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) if return_ok then for i = 1, count do @@ -166,7 +166,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local readings = {} local access_fault = false local _, _, input_regs, _ = self.rtu.io_count() - local return_ok = ((ir_addr_start + count) <= input_regs) and (count > 0) + local return_ok = ((ir_addr_start + count) <= (input_regs + 1)) and (count > 0) if return_ok then for i = 1, count do @@ -255,7 +255,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local response = nil local _, coils, _, _ = self.rtu.io_count() local count = #values - local return_ok = ((c_addr_start + count) <= coils) and (count > 0) + local return_ok = ((c_addr_start + count) <= (coils + 1)) and (count > 0) if return_ok then for i = 1, count do @@ -282,12 +282,12 @@ modbus.new = function (rtu_dev, use_parallel_read) local response = nil local _, _, _, hold_regs = self.rtu.io_count() local count = #values - local return_ok = ((hr_addr_start + count) <= hold_regs) and (count > 0) + local return_ok = ((hr_addr_start + count) <= (hold_regs + 1)) and (count > 0) if return_ok then for i = 1, count do local addr = hr_addr_start + i - 1 - local access_fault = self.rtu.write_coil(addr, values[i]) + local access_fault = self.rtu.write_holding_reg(addr, values[i]) if access_fault then return_ok = false @@ -309,12 +309,12 @@ modbus.new = function (rtu_dev, use_parallel_read) local return_code = true local response = { MODBUS_EXCODE.ACKNOWLEDGE } - if #packet.data == 2 then + if packet.length == 2 then -- handle by function code if packet.func_code == MODBUS_FCODE.READ_COILS then elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then - elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGISTERS then + elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then @@ -351,7 +351,7 @@ modbus.new = function (rtu_dev, use_parallel_read) local return_code = true local response = nil - if #packet.data == 2 then + if packet.length == 2 then -- handle by function code if packet.func_code == MODBUS_FCODE.READ_COILS then return_code, response = _1_read_coils(packet.data[1], packet.data[2]) @@ -359,7 +359,7 @@ modbus.new = function (rtu_dev, use_parallel_read) return_code, response = _2_read_discrete_inputs(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then return_code, response = _3_read_multiple_holding_registers(packet.data[1], packet.data[2]) - elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGISTERS then + elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGS then return_code, response = _4_read_input_registers(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then return_code, response = _5_write_single_coil(packet.data[1], packet.data[2]) @@ -384,14 +384,13 @@ modbus.new = function (rtu_dev, use_parallel_read) if not return_code then -- echo back with error flag func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + end - if type(response) == "nil" then - response = { } - elseif type(response) == "number" then - response = { response } - elseif type(response) == "table" then - response = response - end + if type(response) == "table" then + elseif type(response) == "nil" then + response = {} + else + response = { response } end -- create reply diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 2e2d445..2309be5 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -54,7 +54,7 @@ rtu.init_unit = function () ---@param f function ---@return integer count count of discrete inputs protected.connect_di = function (f) - insert(self.discrete_inputs, f) + insert(self.discrete_inputs, { read = f }) _count_io() return #self.discrete_inputs end @@ -64,7 +64,7 @@ rtu.init_unit = function () ---@return any value, boolean access_fault public.read_di = function (di_addr) ppm.clear_fault() - local value = self.discrete_inputs[di_addr]() + local value = self.discrete_inputs[di_addr].read() return value, ppm.is_faulted() end @@ -105,7 +105,7 @@ rtu.init_unit = function () ---@param f function ---@return integer count count of input registers protected.connect_input_reg = function (f) - insert(self.input_regs, f) + insert(self.input_regs, { read = f }) _count_io() return #self.input_regs end @@ -115,7 +115,7 @@ rtu.init_unit = function () ---@return any value, boolean access_fault public.read_input_reg = function (reg_addr) ppm.clear_fault() - local value = self.coils[reg_addr]() + local value = self.input_regs[reg_addr].read() return value, ppm.is_faulted() end @@ -136,7 +136,7 @@ rtu.init_unit = function () ---@return any value, boolean access_fault public.read_holding_reg = function (reg_addr) ppm.clear_fault() - local value = self.coils[reg_addr].read() + local value = self.holding_regs[reg_addr].read() return value, ppm.is_faulted() end @@ -146,7 +146,7 @@ rtu.init_unit = function () ---@return boolean access_fault public.write_holding_reg = function (reg_addr, value) ppm.clear_fault() - self.coils[reg_addr].write(value) + self.holding_regs[reg_addr].write(value) return ppm.is_faulted() end diff --git a/rtu/startup.lua b/rtu/startup.lua index 003a2d6..6f5730e 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -24,7 +24,7 @@ local imatrix_rtu = require("rtu.dev.imatrix_rtu") local turbine_rtu = require("rtu.dev.turbine_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "alpha-v0.7.0" +local RTU_VERSION = "alpha-v0.7.1" local rtu_t = types.rtu_t diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index fc76e0b..9ba6878 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -110,55 +110,51 @@ end local _B_AND = bit.band -local _TRINARY = function (cond, t, f) if cond then return t else return f end end - -local _DI_ACTIVE_HIGH = function (level) return level == IO_LVL.HIGH end -local _DI_ACTIVE_LOW = function (level) return level == IO_LVL.LOW end -local _DO_ACTIVE_HIGH = function (on) return _TRINARY(on, IO_LVL.HIGH, IO_LVL.LOW) end -local _DO_ACTIVE_LOW = function (on) return _TRINARY(on, IO_LVL.LOW, IO_LVL.HIGH) end +local function _ACTIVE_HIGH(level) return level == IO_LVL.HIGH end +local function _ACTIVE_LOW(level) return level == IO_LVL.LOW end -- I/O mappings to I/O function and I/O mode local RS_DIO_MAP = { -- F_SCRAM - { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, + { _f = _ACTIVE_LOW, mode = IO_DIR.IN }, -- R_SCRAM - { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, + { _f = _ACTIVE_LOW, mode = IO_DIR.IN }, -- R_ENABLE - { _f = _DI_ACTIVE_HIGH, mode = IO_DIR.IN }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.IN }, -- F_ALARM - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- WASTE_PO - { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, + { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_PU - { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, + { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_AM - { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, + { _f = _ACTIVE_LOW, mode = IO_DIR.OUT }, -- R_ALARM - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_SCRAMMED - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_SCRAM - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_ACTIVE - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_CTRL - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_DMG_CRIT - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_HIGH_TEMP - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_NO_COOLANT - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_HC - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_WS - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_INSUFF_FUEL - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_FAULT - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_TIMEOUT - { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT } + { _f = _ACTIVE_HIGH, mode = IO_DIR.OUT } } -- get the mode of a channel @@ -244,13 +240,13 @@ end -- returns the level corresponding to active ---@param channel RS_IO ----@param active boolean ----@return IO_LVL -rsio.digital_write = function (channel, active) +---@param level IO_LVL +---@return boolean +rsio.digital_write = function (channel, level) if type(channel) ~= "number" or channel < RS_IO.F_ALARM or channel > RS_IO.R_PLC_TIMEOUT then - return IO_LVL.LOW + return false else - return RS_DIO_MAP[channel]._f(active) + return RS_DIO_MAP[channel]._f(level) end end diff --git a/test/modbustest.lua b/test/modbustest.lua new file mode 100644 index 0000000..1d0b9ea --- /dev/null +++ b/test/modbustest.lua @@ -0,0 +1,236 @@ +require("/initenv").init_env() + +local types = require("scada-common.types") +local util = require("scada-common.util") + +local testutils = require("test.testutils") + +local modbus = require("rtu.modbus") +local redstone_rtu = require("rtu.dev.redstone_rtu") + +local rsio = require("scada-common.rsio") + +local print = util.print +local println = util.println + +local MODBUS_FCODE = types.MODBUS_FCODE +local MODBUS_EXCODE = types.MODBUS_EXCODE + +println("starting redstone RTU and MODBUS tester") +println("") + +-- RTU init -- + +print(">>> init redstone RTU: ") + +local rs_rtu = redstone_rtu.new() + +local di, c, ir, hr = rs_rtu.io_count() +assert(di == 0 and c == 0 and ir == 0 and hr == 0, "IOCOUNT_0") + +rs_rtu.link_di("back", colors.black) +rs_rtu.link_di("back", colors.blue) + +rs_rtu.link_do(rsio.IO.F_ALARM, "back", colors.red) +rs_rtu.link_do(rsio.IO.WASTE_AM, "back", colors.purple) + +rs_rtu.link_ai("right") +rs_rtu.link_ao("left") + +di, c, ir, hr = rs_rtu.io_count() +assert(di == 2, "IOCOUNT_DI") +assert(c == 2, "IOCOUNT_C") +assert(ir == 1, "IOCOUNT_IR") +assert(hr == 1, "IOCOUNT_HR") + +println("OK") + +-- MODBUS testing -- + +local rs_modbus = modbus.new(rs_rtu, false) + +local mbt = testutils.modbus_tester(rs_modbus, MODBUS_FCODE.ERROR_FLAG) + +------------------------- +--- CHECKING REQUESTS --- +------------------------- + +println(">>> checking MODBUS requests:") + +print("read c {0}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {0}) +mbt.test_error__check_request(MODBUS_EXCODE.NEG_ACKNOWLEDGE) +println("PASS") + +print("99 {1,2}: ") +mbt.pkt_set(99, {1, 2}) +mbt.test_error__check_request(MODBUS_EXCODE.ILLEGAL_FUNCTION) +println("PASS") + +print("read c {1,2}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {1, 2}) +mbt.test_success__check_request(MODBUS_EXCODE.ACKNOWLEDGE) +println("PASS") + +testutils.pause() + +-------------------- +--- BAD REQUESTS --- +-------------------- + +println(">>> trying bad requests:") + +print("read di {1,10}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {1, 10}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read di {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read di {1,0}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {1, 0}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read c {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read c {1,0}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {1, 0}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read ir {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read ir {1,0}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {1, 0}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("read hr {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_MUL_HOLD_REGS, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("write c {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_COIL, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("write mul c {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_COIL, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("write mul c {5,{1}}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_COIL, {5, {1}}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("write hr {5,1}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, {5, 1}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +print("write mul hr {5,{1}}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, {5, {1}}) +mbt.test_error__handle_packet(MODBUS_EXCODE.ILLEGAL_DATA_ADDR) +println("PASS") + +testutils.pause() + +---------------------- +--- READING INPUTS --- +---------------------- + +println(">>> reading inputs:") + +print("read di {1,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {1, 1}) +mbt.test_success__handle_packet() + +print("read di {2,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {2, 1}) +mbt.test_success__handle_packet() + +print("read di {1,2}: ") +mbt.pkt_set(MODBUS_FCODE.READ_DISCRETE_INPUTS, {1, 2}) +mbt.test_success__handle_packet() + +print("read ir {1,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {1, 1}) +mbt.test_success__handle_packet() + +testutils.pause() + +----------------------- +--- WRITING OUTPUTS --- +----------------------- + +println(">>> writing outputs:") + +print("write mul c {1,{LOW,LOW}}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_MUL_COILS, {1, {rsio.IO_LVL.LOW, rsio.IO_LVL.LOW}}) +mbt.test_success__handle_packet() + +testutils.pause() + +print("write c {1,HIGH}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_COIL, {1, rsio.IO_LVL.HIGH}) +mbt.test_success__handle_packet() + +testutils.pause() + +print("write c {2,HIGH}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_COIL, {2, rsio.IO_LVL.HIGH}) +mbt.test_success__handle_packet() + +testutils.pause() + +print("write hr {1,7}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, {1, 7}) +mbt.test_success__handle_packet() + +testutils.pause() + +print("write mul hr {1,{4}}: ") +mbt.pkt_set(MODBUS_FCODE.WRITE_MUL_HOLD_REGS, {1, {4}}) +mbt.test_success__handle_packet() + +println("PASS") + +testutils.pause() + +----------------------- +--- READING OUTPUTS --- +----------------------- + +println(">>> reading outputs:") + +print("read c {1,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {1, 1}) +mbt.test_success__handle_packet() + +print("read c {2,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {2, 1}) +mbt.test_success__handle_packet() + +print("read c {1,2}: ") +mbt.pkt_set(MODBUS_FCODE.READ_COILS, {1, 2}) +mbt.test_success__handle_packet() + +print("read hr {1,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_MUL_HOLD_REGS, {1, 1}) +mbt.test_success__handle_packet() + +println("PASS") + +println("TEST COMPLETE") diff --git a/test/rstest.lua b/test/rstest.lua index 629baae..1ed6827 100644 --- a/test/rstest.lua +++ b/test/rstest.lua @@ -47,8 +47,7 @@ end assert(max_value == cid, "RS_IO last IDx out-of-sync with count: " .. max_value .. " (count " .. cid .. ")") ----@diagnostic disable-next-line: undefined-field -os.sleep(1) +testutils.pause() println(">>> checking invalid channels:") @@ -57,8 +56,7 @@ testutils.test_func_nil("rsio.to_string", rsio.to_string, "") testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN) testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN) ----@diagnostic disable-next-line: undefined-field -os.sleep(1) +testutils.pause() println(">>> checking validity checks:") @@ -76,8 +74,7 @@ testutils.test_func("rsio.is_color", rsio.is_color, ic_t_list, true) testutils.test_func("rsio.is_color", rsio.is_color, { 0, 999999, colors.combine(colors.red, colors.blue, colors.black) }, false) testutils.test_func_nil("rsio.is_color", rsio.is_color, false) ----@diagnostic disable-next-line: undefined-field -os.sleep(1) +testutils.pause() println(">>> checking channel-independent I/O wrappers:") @@ -98,8 +95,7 @@ assert(rsio.analog_write(4, 0, 15) == 4, "RS_WRITE_4_15") assert(rsio.analog_write(12, 0, 15) == 12, "RS_WRITE_12_15") println("PASS") ----@diagnostic disable-next-line: undefined-field -os.sleep(1) +testutils.pause() println(">>> checking channel I/O:") @@ -124,25 +120,25 @@ println("PASS") print("rsio.digital_write(...): ") -- check output channels -assert(rsio.digital_write(IO.F_ALARM, false) == IO_LVL.LOW, "IO_F_ALARM_FALSE") -assert(rsio.digital_write(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_TRUE") -assert(rsio.digital_write(IO.WASTE_PO, false) == IO_LVL.HIGH, "IO_WASTE_PO_FALSE") -assert(rsio.digital_write(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_TRUE") -assert(rsio.digital_write(IO.WASTE_PU, false) == IO_LVL.HIGH, "IO_WASTE_PU_FALSE") -assert(rsio.digital_write(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_TRUE") -assert(rsio.digital_write(IO.WASTE_AM, false) == IO_LVL.HIGH, "IO_WASTE_AM_FALSE") -assert(rsio.digital_write(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_TRUE") +assert(rsio.digital_write(IO.F_ALARM, IO_LVL.LOW) == false, "IO_F_ALARM_FALSE") +assert(rsio.digital_write(IO.F_ALARM, IO_LVL.HIGH) == true, "IO_F_ALARM_TRUE") +assert(rsio.digital_write(IO.WASTE_PO, IO_LVL.HIGH) == false, "IO_WASTE_PO_FALSE") +assert(rsio.digital_write(IO.WASTE_PO, IO_LVL.LOW) == true, "IO_WASTE_PO_TRUE") +assert(rsio.digital_write(IO.WASTE_PU, IO_LVL.HIGH) == false, "IO_WASTE_PU_FALSE") +assert(rsio.digital_write(IO.WASTE_PU, IO_LVL.LOW) == true, "IO_WASTE_PU_TRUE") +assert(rsio.digital_write(IO.WASTE_AM, IO_LVL.HIGH) == false, "IO_WASTE_AM_FALSE") +assert(rsio.digital_write(IO.WASTE_AM, IO_LVL.LOW) == true, "IO_WASTE_AM_TRUE") -- check all reactor output channels (all are active high) for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_CHANNEL") - assert(rsio.digital_write(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_FALSE") - assert(rsio.digital_write(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_TRUE") + assert(rsio.digital_write(i, IO_LVL.LOW) == false, "IO_" .. rsio.to_string(i) .. "_FALSE") + assert(rsio.digital_write(i, IO_LVL.HIGH) == true, "IO_" .. rsio.to_string(i) .. "_TRUE") end -- non-outputs should always return false -assert(rsio.digital_write(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_LOW") -assert(rsio.digital_write(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_HIGH") +assert(rsio.digital_write(IO.F_SCRAM, IO_LVL.LOW) == false, "IO_IN_WRITE_LOW") +assert(rsio.digital_write(IO.F_SCRAM, IO_LVL.LOW) == false, "IO_IN_WRITE_HIGH") println("PASS") diff --git a/test/testutils.lua b/test/testutils.lua index 12c07f0..aa9f45f 100644 --- a/test/testutils.lua +++ b/test/testutils.lua @@ -41,4 +41,82 @@ function testutils.test_func_nil(name, f, result) println("PASS") end +-- get something as a string +---@param result any +---@return string +function testutils.stringify(result) + return textutils.serialize(result, { allow_repetitions = true, compact = true }) +end + +-- pause for 1 second, or the provided seconds +---@param seconds? number +function testutils.pause(seconds) + seconds = seconds or 1.0 +---@diagnostic disable-next-line: undefined-field + os.sleep(seconds) +end + +-- create a new MODBUS tester +---@param modbus modbus modbus object +---@param error_flag MODBUS_FCODE MODBUS_FCODE.ERROR_FLAG +function testutils.modbus_tester(modbus, error_flag) + -- test packet + ---@type modbus_frame + local packet = { + txn_id = 0, + length = 0, + unit_id = 0, + func_code = 0, + data = {}, + scada_frame = nil + } + + ---@class modbus_tester + local public = {} + + -- set the packet function and data for the next test + ---@param func MODBUS_FCODE function code + ---@param data table + function public.pkt_set(func, data) + packet.length = #data + packet.data = data + packet.func_code = func + end + + -- check the current packet, expecting an error + ---@param excode MODBUS_EXCODE exception code to expect + function public.test_error__check_request(excode) + local rcode, reply = modbus.check_request(packet) + assert(rcode == false, "CHECK_NOT_FAIL") + assert(reply.get().func_code == bit.bor(packet.func_code, error_flag), "WRONG_FCODE") + assert(reply.get().data[1] == excode, "EXCODE_MISMATCH") + end + + -- test the current packet, expecting an error + ---@param excode MODBUS_EXCODE exception code to expect + function public.test_error__handle_packet(excode) + local rcode, reply = modbus.handle_packet(packet) + assert(rcode == false, "CHECK_NOT_FAIL") + assert(reply.get().func_code == bit.bor(packet.func_code, error_flag), "WRONG_FCODE") + assert(reply.get().data[1] == excode, "EXCODE_MISMATCH") + end + + -- check the current packet, expecting success + ---@param excode MODBUS_EXCODE exception code to expect + function public.test_success__check_request(excode) + local rcode, reply = modbus.check_request(packet) + assert(rcode, "CHECK_NOT_OK") + assert(reply.get().data[1] == excode, "EXCODE_MISMATCH") + end + + -- test the current packet, expecting success + function public.test_success__handle_packet() + local rcode, reply = modbus.handle_packet(packet) + assert(rcode, "CHECK_NOT_OK") + println(testutils.stringify(reply.get().data)) + end + + return public +end + return testutils From 4d16d64cdcde3a742b9c813f708c89a43e6b1a3b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 27 May 2022 18:17:52 -0400 Subject: [PATCH 167/168] log bugfix --- scada-common/log.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index 8369d22..2fa0b32 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -39,7 +39,7 @@ log.init = function (path, write_mode, dmesg_redirect) if _log_sys.mode == MODE.APPEND then _log_sys.file = fs.open(path, "a") else - _log_sys.file = fs.open(path, "w+") + _log_sys.file = fs.open(path, "w") end if dmesg_redirect then From 706bf4d3ba2daa3fe1a731cc6db1f6085e47afa9 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 27 May 2022 18:18:12 -0400 Subject: [PATCH 168/168] #9 turbine RTU tester --- test/turbine_modbustest.lua | 68 +++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 test/turbine_modbustest.lua diff --git a/test/turbine_modbustest.lua b/test/turbine_modbustest.lua new file mode 100644 index 0000000..fe167d7 --- /dev/null +++ b/test/turbine_modbustest.lua @@ -0,0 +1,68 @@ +require("/initenv").init_env() + +local log = require("scada-common.log") +local ppm = require("scada-common.ppm") +local types = require("scada-common.types") +local util = require("scada-common.util") + +local testutils = require("test.testutils") + +local modbus = require("rtu.modbus") +local turbine_rtu = require("rtu.dev.turbine_rtu") + +local print = util.print +local println = util.println + +local MODBUS_FCODE = types.MODBUS_FCODE + +println("starting turbine RTU MODBUS tester") +println("note: use rs_modbustest to fully test RTU/MODBUS") +println(" this only tests a turbine/parallel read") +println("") + +-- RTU init -- + +log.init("/log.txt", log.MODE.NEW) + +print(">>> init turbine RTU: ") + +ppm.mount_all() + +local dev = ppm.get_device("turbine") +assert(dev ~= nil, "NO_TURBINE") + +local t_rtu = turbine_rtu.new(dev) + +local di, c, ir, hr = t_rtu.io_count() +assert(di == 0, "IOCOUNT_DI") +assert(c == 0, "IOCOUNT_C") +assert(ir == 16, "IOCOUNT_IR") +assert(hr == 0, "IOCOUNT_HR") + +println("OK") + +local t_modbus = modbus.new(t_rtu, true) + +local mbt = testutils.modbus_tester(t_modbus, MODBUS_FCODE.ERROR_FLAG) + +---------------------- +--- READING INPUTS --- +---------------------- + +println(">>> reading inputs:") + +print("read ir {1,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {1, 1}) +mbt.test_success__handle_packet() + +print("read ir {2,1}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {2, 1}) +mbt.test_success__handle_packet() + +print("read ir {1,16}: ") +mbt.pkt_set(MODBUS_FCODE.READ_INPUT_REGS, {1, 16}) +mbt.test_success__handle_packet() + +println("PASS") + +println("TEST COMPLETE")