From 0dac25d9e7d792761018adb44b439078bb572fcd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 28 Dec 2021 21:46:38 -0500 Subject: [PATCH 01/63] reorganization --- {main => controller}/controller.lua | 0 {main => controller}/defs.lua | 0 {main => controller}/log.lua | 0 {main => controller}/reactor.lua | 0 {main => controller}/regulator.lua | 0 {main => controller}/render.lua | 0 {main => controller}/server.lua | 0 signal-router.lua => rcss/signal-router.lua | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {main => controller}/controller.lua (100%) rename {main => controller}/defs.lua (100%) rename {main => controller}/log.lua (100%) rename {main => controller}/reactor.lua (100%) rename {main => controller}/regulator.lua (100%) rename {main => controller}/render.lua (100%) rename {main => controller}/server.lua (100%) rename signal-router.lua => rcss/signal-router.lua (100%) diff --git a/main/controller.lua b/controller/controller.lua similarity index 100% rename from main/controller.lua rename to controller/controller.lua diff --git a/main/defs.lua b/controller/defs.lua similarity index 100% rename from main/defs.lua rename to controller/defs.lua diff --git a/main/log.lua b/controller/log.lua similarity index 100% rename from main/log.lua rename to controller/log.lua diff --git a/main/reactor.lua b/controller/reactor.lua similarity index 100% rename from main/reactor.lua rename to controller/reactor.lua diff --git a/main/regulator.lua b/controller/regulator.lua similarity index 100% rename from main/regulator.lua rename to controller/regulator.lua diff --git a/main/render.lua b/controller/render.lua similarity index 100% rename from main/render.lua rename to controller/render.lua diff --git a/main/server.lua b/controller/server.lua similarity index 100% rename from main/server.lua rename to controller/server.lua diff --git a/signal-router.lua b/rcss/signal-router.lua similarity index 100% rename from signal-router.lua rename to rcss/signal-router.lua From 26cce3a46adabc5f858e83fb268852bfcd2e27cd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 1 Jan 2022 19:45:33 -0500 Subject: [PATCH 02/63] reactor control and safety system attempting server connection --- common/comms.lua | 157 ++++++++++++++++++++++++++++++ common/util.lua | 29 ++++++ rcass/config.lua | 6 ++ rcass/rcass.lua | 122 +++++++++++++++++++++++ rcass/safety.lua | 80 +++++++++++++++ {rcss => rcass}/signal-router.lua | 0 rcass/startup.lua | 13 +++ 7 files changed, 407 insertions(+) create mode 100644 common/comms.lua create mode 100644 common/util.lua create mode 100644 rcass/config.lua create mode 100644 rcass/rcass.lua create mode 100644 rcass/safety.lua rename {rcss => rcass}/signal-router.lua (100%) create mode 100644 rcass/startup.lua diff --git a/common/comms.lua b/common/comms.lua new file mode 100644 index 0000000..4e47eb3 --- /dev/null +++ b/common/comms.lua @@ -0,0 +1,157 @@ + +function server_comms() + local self = { + reactor_struct_cache = nil + } + + local record_struct = function (id, mek_data) + end + + -- send the structure data by request to pocket computers + local send_struct = function () + end + + local command_waste = function () + end +end + +function rcass_comms(id, modem, local_port, server_port, reactor) + local self = { + _id = id, + _modem = modem, + _server = server_port, + _local = local_port, + _reactor = reactor, + _status_cache = nil, + + _send = function (msg) + self._modem.transmit(self._server, self._local, msg) + end + } + + local _send = function (msg) + self._modem.transmit(self._server, self._local, msg) + end + + -- variable reactor status information, excluding heating rate + local _reactor_status = function () + 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(), + + 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() + } + end + + local _status_changed = function () + local status = self._reactor_status() + local changed = false + + for key, value in pairs() do + if value ~= _status_cache[key] then + changed = true + break + end + end + + return changed + end + + -- attempt to establish link with + local send_link_req = function () + local linking_data = { + id = self._id, + type = "link_req" + } + + _send(linking_data) + end + + -- send structure properties (these should not change) + -- server will cache these + local send_struct = function () + 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() + } + + local struct_packet = { + id = self._id, + type = "struct_data", + mek_data = mek_data + } + + _send(struct_packet) + end + + -- send live status information + local send_status = function () + local mek_data = self._reactor_status() + + local sys_data = { + timestamp = os.time(), + control_state = false, + overridden = false, + faults = {}, + waste_production = "antimatter" -- "plutonium", "polonium", "antimatter" + } + end + + local send_keep_alive = function () + -- heating rate is volatile, so it is skipped in status + -- send it with keep alive packets + local mek_data = { + heating_rate = self._reactor.getHeatingRate() + } + + -- basic keep alive packet to server + local keep_alive_packet = { + id = self._id, + type = "keep_alive", + timestamp = os.time(), + mek_data = mek_data + } + + _send(keep_alive_packet) + end + + local handle_link = function (packet) + if packet.type == "link_response" then + return packet.accepted + else + return "wrong_type" + end + end + + return { + send_link_req = send_link_req, + send_struct = send_struct, + send_status = send_status, + send_keep_alive = send_keep_alive, + handle_link = handle_link + } +end \ No newline at end of file diff --git a/common/util.lua b/common/util.lua new file mode 100644 index 0000000..f6fd611 --- /dev/null +++ b/common/util.lua @@ -0,0 +1,29 @@ +-- timestamped print +function print_ts(message) + term.write(os.date("[%H:%M:%S] ") .. message) +end + +-- ComputerCraft OS Timer based Watchdog +-- triggers a timer event if not fed within 'timeout' seconds +function new_watchdog(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) + end + self._wd_timer = os.startTimer(self._timeout) + end + + return { + get_timer = get_timer, + feed = feed + } +end diff --git a/rcass/config.lua b/rcass/config.lua new file mode 100644 index 0000000..f79895c --- /dev/null +++ b/rcass/config.lua @@ -0,0 +1,6 @@ +-- unique reactor ID +REACTOR_ID = 1 +-- port to send packets TO server +SERVER_PORT = 1000 +-- port to listen to incoming packets FROM server +LISTEN_PORT = 1001 diff --git a/rcass/rcass.lua b/rcass/rcass.lua new file mode 100644 index 0000000..8738896 --- /dev/null +++ b/rcass/rcass.lua @@ -0,0 +1,122 @@ +-- +-- RCaSS: Reactor Controller and Safety Subsystem +-- + +os.loadAPI("common/util.lua") +os.loadAPI("common/comms.lua") +os.loadAPI("rcass/config.lua") +os.loadAPI("rcass/safety.lua") + +local RCASS_VERSION = "alpha-v0.1" + +local print_ts = util.print_ts + +local reactor = peripheral.find("fissionReactor") +local modem = peripheral.find("modem") + +print(">> RCaSS " .. RCASS_VERSION .. " <<") + +-- we need a reactor and a modem +if reactor == nil then + print("Fission reactor not found, exiting..."); + return +elseif modem == nil then + print("No modem found, disabling reactor and exiting...") + reactor.scram() + return +end + +-- just booting up, no fission allowed (neutrons stay put thanks) +reactor.scram() + +-- init internal safety system +local iss = safety.iss_init(reactor) +local iss_status = "ok" +local iss_tripped = false + +-- read config + +-- start comms +if not modem.isOpen(config.LISTEN_PORT) then + modem.open(config.LISTEN_PORT) +end + +local comms = comms.rcass_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) + +-- attempt server connection +local linked = false +local link_timeout = os.startTimer(5) +comms.send_link_req() +print_ts("sent link request") +repeat + local event, param1, param2, param3, param4, param5 = os.pullEvent() + + -- handle event + if event == "timer" and param1 == link_timeout then + -- no response yet + print("...no response"); + comms.send_link_req() + print_ts("sent link request") + link_timeout = os.startTimer(5) + elseif event == "modem_message" then + -- server response? cancel timeout + if link_timeout ~= nil then + os.cancelTimer(link_timeout) + end + + local packet = { + side = param1, + sender = param2, + reply_to = param3, + message = param4, + distance = param5 + } + + -- handle response + response = comms.handle_link(packet) + if response == "wrong_type" then + print_ts("invalid link response, bad channel?\n") + return + elseif response == true then + print_ts("...linked!\n") + linked = true + else + print_ts("...denied, exiting\n") + return + end + end +until linked + +-- comms watchdog, 3 second timeout +local conn_watchdog = watchdog.new_watchdog(3) + +-- loop clock (10Hz, 2 ticks) +-- send status updates at 4Hz (every 5 ticks) +local loop_tick = os.startTimer(0.05) +local ticks_to_update = 5 + +-- event loop +while true do + local event, param1, param2, param3, param4, param5 = os.pullEvent() + + -- check safety (SCRAM occurs if tripped) + iss_status, iss_tripped = iss.check() + + -- handle event + if event == "timer" and param1 == loop_tick then + -- basic event tick, send updated data if it is time + ticks_to_update = ticks_to_update - 1 + if ticks_to_update == 0 then + ticks_to_update = 5 + end + elseif event == "modem_message" then + -- got a packet + -- feed the watchdog first so it doesn't eat our packets + conn_watchdog.feed() + + elseif event == "timer" and param1 == conn_watchdog.get_timer() then + -- haven't heard from server recently? shutdown + reactor.scram() + print_ts("[alert] server timeout, reactor disabled\n") + end +end diff --git a/rcass/safety.lua b/rcass/safety.lua new file mode 100644 index 0000000..29733ab --- /dev/null +++ b/rcass/safety.lua @@ -0,0 +1,80 @@ +-- Internal Safety System +-- identifies dangerous states and SCRAMs reactor if warranted +-- autonomous from main control +function iss_init(reactor) + local self = { + _reactor = reactor, + _tripped = false, + _trip_cause = "" + } + + local check = function () + local status = "ok" + + -- check system states in order of severity + if self.damage_critical() then + status = "dmg_crit" + elseif self.high_temp() then + status = "high_temp" + elseif self.excess_heated_coolant() then + status = "heated_coolant_backup" + elseif self.excess_waste() then + status = "full_waste" + elseif self.insufficient_fuel() then + status = "no_fuel" + elseif self._tripped then + status = self._trip_cause + else + self._tripped = false + end + + if status ~= "ok" then + self._tripped = true + self._trip_cause = status + self._reactor.scram() + end + + return self._tripped, status + end + + local reset = function () + self._tripped = false + self._trip_cause = "" + end + + local damage_critical = function () + return self._reactor.getDamagePercent() >= 100 + end + + local excess_heated_coolant = function () + return self._reactor.getHeatedCoolantNeeded() == 0 + end + + local excess_waste = function () + return self._reactor.getWasteNeeded() == 0 + end + + local high_temp = function () + -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 + return self._reactor.getTemperature() >= 1200 + end + + local insufficient_fuel = function () + return self._reactor.getFuel() == 0 + end + + local no_coolant = function() + return self._reactor.getCoolantFilledPercentage() < 2 + end + + return { + check = check, + reset = reset, + 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 + } +end diff --git a/rcss/signal-router.lua b/rcass/signal-router.lua similarity index 100% rename from rcss/signal-router.lua rename to rcass/signal-router.lua diff --git a/rcass/startup.lua b/rcass/startup.lua new file mode 100644 index 0000000..a0b746a --- /dev/null +++ b/rcass/startup.lua @@ -0,0 +1,13 @@ +print(">>RCASS LOADER START<<") +print(">>CHECKING SETTINGS...") +loaded = settings.load("rcass.settings") +if loaded then + print(">>SETTINGS FOUND, VERIFIYING INTEGRITY...") + settings.getNames() +else + print(">>SETTINGS NOT FOUND") + print(">>LAUNCHING CONFIGURATOR...") + shell.run("config") +end +print(">>LAUNCHING RCASS...") +shell.run("rcass") From ab49322fec3eff984f9237ba9f2c7a7402eb7c83 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 1 Jan 2022 21:01:05 -0500 Subject: [PATCH 03/63] archive old controller --- controller/{ => old-controller}/controller.lua | 0 controller/{ => old-controller}/defs.lua | 0 controller/{ => old-controller}/log.lua | 0 controller/{ => old-controller}/reactor.lua | 0 controller/{ => old-controller}/regulator.lua | 0 controller/{ => old-controller}/render.lua | 0 controller/{ => old-controller}/server.lua | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename controller/{ => old-controller}/controller.lua (100%) rename controller/{ => old-controller}/defs.lua (100%) rename controller/{ => old-controller}/log.lua (100%) rename controller/{ => old-controller}/reactor.lua (100%) rename controller/{ => old-controller}/regulator.lua (100%) rename controller/{ => old-controller}/render.lua (100%) rename controller/{ => old-controller}/server.lua (100%) diff --git a/controller/controller.lua b/controller/old-controller/controller.lua similarity index 100% rename from controller/controller.lua rename to controller/old-controller/controller.lua diff --git a/controller/defs.lua b/controller/old-controller/defs.lua similarity index 100% rename from controller/defs.lua rename to controller/old-controller/defs.lua diff --git a/controller/log.lua b/controller/old-controller/log.lua similarity index 100% rename from controller/log.lua rename to controller/old-controller/log.lua diff --git a/controller/reactor.lua b/controller/old-controller/reactor.lua similarity index 100% rename from controller/reactor.lua rename to controller/old-controller/reactor.lua diff --git a/controller/regulator.lua b/controller/old-controller/regulator.lua similarity index 100% rename from controller/regulator.lua rename to controller/old-controller/regulator.lua diff --git a/controller/render.lua b/controller/old-controller/render.lua similarity index 100% rename from controller/render.lua rename to controller/old-controller/render.lua diff --git a/controller/server.lua b/controller/old-controller/server.lua similarity index 100% rename from controller/server.lua rename to controller/old-controller/server.lua From 3b492ead929c17713254761a593872fe421fb5cc Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 13 Jan 2022 10:06:55 -0500 Subject: [PATCH 04/63] changed to SCADA terminology, changed RCaSS to reactor PLC, maybe changed other things --- common/comms.lua | 157 ------------------ common/util.lua | 29 ---- .../old-controller/controller.lua | 0 .../old-controller/defs.lua | 0 .../old-controller/log.lua | 0 .../old-controller/reactor.lua | 0 .../old-controller/regulator.lua | 0 .../old-controller/render.lua | 0 .../old-controller/server.lua | 0 coordinator/scada-coordinator.lua | 0 {rcass => reactor-plc}/config.lua | 4 +- rcass/rcass.lua => reactor-plc/plc.lua | 16 +- {rcass => reactor-plc}/safety.lua | 0 {rcass => reactor-plc}/signal-router.lua | 0 {rcass => reactor-plc}/startup.lua | 0 15 files changed, 10 insertions(+), 196 deletions(-) delete mode 100644 common/comms.lua delete mode 100644 common/util.lua rename {controller => coordinator}/old-controller/controller.lua (100%) rename {controller => coordinator}/old-controller/defs.lua (100%) rename {controller => coordinator}/old-controller/log.lua (100%) rename {controller => coordinator}/old-controller/reactor.lua (100%) rename {controller => coordinator}/old-controller/regulator.lua (100%) rename {controller => coordinator}/old-controller/render.lua (100%) rename {controller => coordinator}/old-controller/server.lua (100%) create mode 100644 coordinator/scada-coordinator.lua rename {rcass => reactor-plc}/config.lua (75%) rename rcass/rcass.lua => reactor-plc/plc.lua (88%) rename {rcass => reactor-plc}/safety.lua (100%) rename {rcass => reactor-plc}/signal-router.lua (100%) rename {rcass => reactor-plc}/startup.lua (100%) diff --git a/common/comms.lua b/common/comms.lua deleted file mode 100644 index 4e47eb3..0000000 --- a/common/comms.lua +++ /dev/null @@ -1,157 +0,0 @@ - -function server_comms() - local self = { - reactor_struct_cache = nil - } - - local record_struct = function (id, mek_data) - end - - -- send the structure data by request to pocket computers - local send_struct = function () - end - - local command_waste = function () - end -end - -function rcass_comms(id, modem, local_port, server_port, reactor) - local self = { - _id = id, - _modem = modem, - _server = server_port, - _local = local_port, - _reactor = reactor, - _status_cache = nil, - - _send = function (msg) - self._modem.transmit(self._server, self._local, msg) - end - } - - local _send = function (msg) - self._modem.transmit(self._server, self._local, msg) - end - - -- variable reactor status information, excluding heating rate - local _reactor_status = function () - 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(), - - 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() - } - end - - local _status_changed = function () - local status = self._reactor_status() - local changed = false - - for key, value in pairs() do - if value ~= _status_cache[key] then - changed = true - break - end - end - - return changed - end - - -- attempt to establish link with - local send_link_req = function () - local linking_data = { - id = self._id, - type = "link_req" - } - - _send(linking_data) - end - - -- send structure properties (these should not change) - -- server will cache these - local send_struct = function () - 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() - } - - local struct_packet = { - id = self._id, - type = "struct_data", - mek_data = mek_data - } - - _send(struct_packet) - end - - -- send live status information - local send_status = function () - local mek_data = self._reactor_status() - - local sys_data = { - timestamp = os.time(), - control_state = false, - overridden = false, - faults = {}, - waste_production = "antimatter" -- "plutonium", "polonium", "antimatter" - } - end - - local send_keep_alive = function () - -- heating rate is volatile, so it is skipped in status - -- send it with keep alive packets - local mek_data = { - heating_rate = self._reactor.getHeatingRate() - } - - -- basic keep alive packet to server - local keep_alive_packet = { - id = self._id, - type = "keep_alive", - timestamp = os.time(), - mek_data = mek_data - } - - _send(keep_alive_packet) - end - - local handle_link = function (packet) - if packet.type == "link_response" then - return packet.accepted - else - return "wrong_type" - end - end - - return { - send_link_req = send_link_req, - send_struct = send_struct, - send_status = send_status, - send_keep_alive = send_keep_alive, - handle_link = handle_link - } -end \ No newline at end of file diff --git a/common/util.lua b/common/util.lua deleted file mode 100644 index f6fd611..0000000 --- a/common/util.lua +++ /dev/null @@ -1,29 +0,0 @@ --- timestamped print -function print_ts(message) - term.write(os.date("[%H:%M:%S] ") .. message) -end - --- ComputerCraft OS Timer based Watchdog --- triggers a timer event if not fed within 'timeout' seconds -function new_watchdog(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) - end - self._wd_timer = os.startTimer(self._timeout) - end - - return { - get_timer = get_timer, - feed = feed - } -end diff --git a/controller/old-controller/controller.lua b/coordinator/old-controller/controller.lua similarity index 100% rename from controller/old-controller/controller.lua rename to coordinator/old-controller/controller.lua diff --git a/controller/old-controller/defs.lua b/coordinator/old-controller/defs.lua similarity index 100% rename from controller/old-controller/defs.lua rename to coordinator/old-controller/defs.lua diff --git a/controller/old-controller/log.lua b/coordinator/old-controller/log.lua similarity index 100% rename from controller/old-controller/log.lua rename to coordinator/old-controller/log.lua diff --git a/controller/old-controller/reactor.lua b/coordinator/old-controller/reactor.lua similarity index 100% rename from controller/old-controller/reactor.lua rename to coordinator/old-controller/reactor.lua diff --git a/controller/old-controller/regulator.lua b/coordinator/old-controller/regulator.lua similarity index 100% rename from controller/old-controller/regulator.lua rename to coordinator/old-controller/regulator.lua diff --git a/controller/old-controller/render.lua b/coordinator/old-controller/render.lua similarity index 100% rename from controller/old-controller/render.lua rename to coordinator/old-controller/render.lua diff --git a/controller/old-controller/server.lua b/coordinator/old-controller/server.lua similarity index 100% rename from controller/old-controller/server.lua rename to coordinator/old-controller/server.lua diff --git a/coordinator/scada-coordinator.lua b/coordinator/scada-coordinator.lua new file mode 100644 index 0000000..e69de29 diff --git a/rcass/config.lua b/reactor-plc/config.lua similarity index 75% rename from rcass/config.lua rename to reactor-plc/config.lua index f79895c..539946e 100644 --- a/rcass/config.lua +++ b/reactor-plc/config.lua @@ -1,6 +1,6 @@ -- unique reactor ID REACTOR_ID = 1 -- port to send packets TO server -SERVER_PORT = 1000 +SERVER_PORT = 16000 -- port to listen to incoming packets FROM server -LISTEN_PORT = 1001 +LISTEN_PORT = 14001 diff --git a/rcass/rcass.lua b/reactor-plc/plc.lua similarity index 88% rename from rcass/rcass.lua rename to reactor-plc/plc.lua index 8738896..b30a00e 100644 --- a/rcass/rcass.lua +++ b/reactor-plc/plc.lua @@ -1,20 +1,20 @@ -- --- RCaSS: Reactor Controller and Safety Subsystem +-- Reactor Programmable Logic Controller -- -os.loadAPI("common/util.lua") -os.loadAPI("common/comms.lua") -os.loadAPI("rcass/config.lua") -os.loadAPI("rcass/safety.lua") +os.loadAPI("scada-common/util.lua") +os.loadAPI("scada-common/comms.lua") +os.loadAPI("reactor-plc/config.lua") +os.loadAPI("reactor-plc/safety.lua") -local RCASS_VERSION = "alpha-v0.1" +local R_PLC_VERSION = "alpha-v0.1" local print_ts = util.print_ts local reactor = peripheral.find("fissionReactor") local modem = peripheral.find("modem") -print(">> RCaSS " .. RCASS_VERSION .. " <<") +print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") -- we need a reactor and a modem if reactor == nil then @@ -41,7 +41,7 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local comms = comms.rcass_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) -- attempt server connection local linked = false diff --git a/rcass/safety.lua b/reactor-plc/safety.lua similarity index 100% rename from rcass/safety.lua rename to reactor-plc/safety.lua diff --git a/rcass/signal-router.lua b/reactor-plc/signal-router.lua similarity index 100% rename from rcass/signal-router.lua rename to reactor-plc/signal-router.lua diff --git a/rcass/startup.lua b/reactor-plc/startup.lua similarity index 100% rename from rcass/startup.lua rename to reactor-plc/startup.lua From c78db71b1494d63303b99ca3fa17dc262994f8de Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 13 Jan 2022 10:11:42 -0500 Subject: [PATCH 05/63] comms and util files --- scada-common/comms.lua | 157 +++++++++++++++++++++++++++++++++++++++++ scada-common/util.lua | 29 ++++++++ 2 files changed, 186 insertions(+) create mode 100644 scada-common/comms.lua create mode 100644 scada-common/util.lua diff --git a/scada-common/comms.lua b/scada-common/comms.lua new file mode 100644 index 0000000..f1ed07f --- /dev/null +++ b/scada-common/comms.lua @@ -0,0 +1,157 @@ + +function server_comms() + local self = { + reactor_struct_cache = nil + } + + local record_struct = function (id, mek_data) + end + + -- send the structure data by request to pocket computers + local send_struct = function () + end + + local command_waste = function () + end +end + +function rplc_comms(id, modem, local_port, server_port, reactor) + local self = { + _id = id, + _modem = modem, + _server = server_port, + _local = local_port, + _reactor = reactor, + _status_cache = nil, + + _send = function (msg) + self._modem.transmit(self._server, self._local, msg) + end + } + + local _send = function (msg) + self._modem.transmit(self._server, self._local, msg) + end + + -- variable reactor status information, excluding heating rate + local _reactor_status = function () + 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(), + + 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() + } + end + + local _status_changed = function () + local status = self._reactor_status() + local changed = false + + for key, value in pairs() do + if value ~= _status_cache[key] then + changed = true + break + end + end + + return changed + end + + -- attempt to establish link with + local send_link_req = function () + local linking_data = { + id = self._id, + type = "link_req" + } + + _send(linking_data) + end + + -- send structure properties (these should not change) + -- server will cache these + local send_struct = function () + 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() + } + + local struct_packet = { + id = self._id, + type = "struct_data", + mek_data = mek_data + } + + _send(struct_packet) + end + + -- send live status information + local send_status = function () + local mek_data = self._reactor_status() + + local sys_data = { + timestamp = os.time(), + control_state = false, + overridden = false, + faults = {}, + waste_production = "antimatter" -- "plutonium", "polonium", "antimatter" + } + end + + local send_keep_alive = function () + -- heating rate is volatile, so it is skipped in status + -- send it with keep alive packets + local mek_data = { + heating_rate = self._reactor.getHeatingRate() + } + + -- basic keep alive packet to server + local keep_alive_packet = { + id = self._id, + type = "keep_alive", + timestamp = os.time(), + mek_data = mek_data + } + + _send(keep_alive_packet) + end + + local handle_link = function (packet) + if packet.type == "link_response" then + return packet.accepted + else + return "wrong_type" + end + end + + return { + send_link_req = send_link_req, + send_struct = send_struct, + send_status = send_status, + send_keep_alive = send_keep_alive, + handle_link = handle_link + } +end \ No newline at end of file diff --git a/scada-common/util.lua b/scada-common/util.lua new file mode 100644 index 0000000..f6fd611 --- /dev/null +++ b/scada-common/util.lua @@ -0,0 +1,29 @@ +-- timestamped print +function print_ts(message) + term.write(os.date("[%H:%M:%S] ") .. message) +end + +-- ComputerCraft OS Timer based Watchdog +-- triggers a timer event if not fed within 'timeout' seconds +function new_watchdog(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) + end + self._wd_timer = os.startTimer(self._timeout) + end + + return { + get_timer = get_timer, + feed = feed + } +end From 78cbb9e67d3f8ade3e96d42443a007d6bd84da59 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 13 Jan 2022 10:12:44 -0500 Subject: [PATCH 06/63] RTU object and started modbus --- rtu/config.lua | 10 ++++++ rtu/rtu.lua | 78 +++++++++++++++++++++++++++++++++++++++++ rtu/startup.lua | 6 ++++ scada-common/modbus.lua | 14 ++++++++ 4 files changed, 108 insertions(+) create mode 100644 rtu/config.lua create mode 100644 rtu/rtu.lua create mode 100644 rtu/startup.lua create mode 100644 scada-common/modbus.lua diff --git a/rtu/config.lua b/rtu/config.lua new file mode 100644 index 0000000..39b23ad --- /dev/null +++ b/rtu/config.lua @@ -0,0 +1,10 @@ +RTU__DEVICES = { + { + name = "boiler_0", + reactor_owner = 1 + }, + { + name = "turbine_0", + reactor_owner = 1 + } +} diff --git a/rtu/rtu.lua b/rtu/rtu.lua new file mode 100644 index 0000000..97e11f7 --- /dev/null +++ b/rtu/rtu.lua @@ -0,0 +1,78 @@ +function rtu_init() + local self = { + discrete_inputs = {}, + coils = {}, + input_regs = {}, + holding_regs = {} + } + + local count_io = function () + return #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs + end + + -- discrete inputs: single bit read-only + + local connect_di = function (f) + table.insert(self.discrete_inputs, f) + return #self.discrete_inputs + end + + local read_di = function (di_addr) + return self.discrete_inputs[di_addr]() + end + + -- coils: single bit read-write + + local connect_coil = function (f_read, f_write) + table.insert(self.coils, { read = f_read, write = f_write }) + return #self.coils + end + + local read_coil = function (coil_addr) + return self.coils[coil_addr].read() + end + + local write_coil = function (coil_addr, value) + self.coils[coil_addr].write(value) + end + + -- input registers: multi-bit read-only + + local connect_input_reg = function (f) + table.insert(self.input_regs, f) + return #self.input_regs + end + + local read_input_reg = function (reg_addr) + return self.coils[reg_addr]() + end + + -- holding registers: multi-bit read-write + + local connect_holding_reg = function (f_read, f_write) + table.insert(self.holding_regs, { read = f_read, write = f_write }) + return #self.holding_regs + end + + local read_holding_reg = function (reg_addr) + return self.coils[reg_addr].read() + end + + local write_holding_reg = function (reg_addr, value) + self.coils[reg_addr].write(value) + end + + return { + count_io = count_io, + 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 + } +end diff --git a/rtu/startup.lua b/rtu/startup.lua new file mode 100644 index 0000000..de9a784 --- /dev/null +++ b/rtu/startup.lua @@ -0,0 +1,6 @@ +-- +-- RTU: Remote Terminal Unit +-- + +os.loadAPI("config.lua") +os.loadAPI("rtu.lua") diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua new file mode 100644 index 0000000..1e47f9c --- /dev/null +++ b/scada-common/modbus.lua @@ -0,0 +1,14 @@ +function modbus_init(rtu_dev) + local self = { + rtu = rtu_dev + } + + function _1_read_coils(c_channel_start, count) + end + + function _2_read_discrete_inputs(di_channel_start, count) + end + + function _3_read_multiple_holding_registers(hr_channel_start, count) + end +end From 4dfdb218e271bc46786c849f38ce4a4ac2deaa7b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 13 Jan 2022 10:23:38 -0500 Subject: [PATCH 07/63] SCADA supervisor code started --- scada-common/comms.lua | 16 +++++------- supervisor/config.lua | 18 +++++++++++++ supervisor/scada-supervisor.lua | 45 +++++++++++++++++++++++++++++++++ supervisor/startup.lua | 3 +++ 4 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 supervisor/config.lua create mode 100644 supervisor/scada-supervisor.lua create mode 100644 supervisor/startup.lua diff --git a/scada-common/comms.lua b/scada-common/comms.lua index f1ed07f..34de9cb 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,18 +1,14 @@ -function server_comms() +function coord_comms() local self = { reactor_struct_cache = nil } +end - local record_struct = function (id, mek_data) - end - - -- send the structure data by request to pocket computers - local send_struct = function () - end - - local command_waste = function () - end +function superv_comms() + local self = { + reactor_struct_cache = nil + } end function rplc_comms(id, modem, local_port, server_port, reactor) diff --git a/supervisor/config.lua b/supervisor/config.lua new file mode 100644 index 0000000..39adeef --- /dev/null +++ b/supervisor/config.lua @@ -0,0 +1,18 @@ +-- 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 +SCADA_NET_PFX = 16000 +-- failover synchronization +SCADA_FO_CHANNEL = 16001 +-- listen port for SCADA supervisor access +SCADA_SV_CHANNEL = 16002 +-- listen port for PLC's +SCADA_PLC_LISTEN = 16003 + +-- expected number of reactors +NUM_REACTORS = 4 diff --git a/supervisor/scada-supervisor.lua b/supervisor/scada-supervisor.lua new file mode 100644 index 0000000..772c572 --- /dev/null +++ b/supervisor/scada-supervisor.lua @@ -0,0 +1,45 @@ +-- +-- Nuclear Generation Facility SCADA Supervisor +-- + +os.loadAPI("scada-common/util.lua") +os.loadAPI("scada-common/comms.lua") +os.loadAPI("supervisor/config.lua") + +local SUPERVISOR_VERSION = "alpha-v0.1" + +local print_ts = util.print_ts + +local modem = peripheral.find("modem") + +print("| SCADA Supervisor - " .. SUPERVISOR_VERSION .. " |") + +-- we need a modem +if modem == nil then + print("No modem found, exiting...") + return +end + +-- read config + +-- start comms +if not modem.isOpen(config.LISTEN_PORT) then + modem.open(config.LISTEN_PORT) +end + +local comms = comms.superv_comms(config.NUM_REACTORS, modem, config.SCADA_PLC_LISTEN, config.SCADA_SV_CHANNEL) + +-- base loop clock (4Hz, 5 ticks) +local loop_tick = os.startTimer(0.25) + +-- event loop +while true do + local event, param1, param2, param3, param4, param5 = os.pullEvent() + + -- handle event + if event == "timer" and param1 == loop_tick then + -- basic event tick, send keep-alives + elseif event == "modem_message" then + -- got a packet + end +end diff --git a/supervisor/startup.lua b/supervisor/startup.lua new file mode 100644 index 0000000..2ee4d40 --- /dev/null +++ b/supervisor/startup.lua @@ -0,0 +1,3 @@ +-- +-- Multi-Reactor Controller Server & GUI +-- \ No newline at end of file From e47b4d795987629e096a171e03bd0ad680b5060c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 13 Jan 2022 10:23:56 -0500 Subject: [PATCH 08/63] placeholders for pocket computer access in the future --- pocket/config.lua | 0 pocket/startup.lua | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 pocket/config.lua create mode 100644 pocket/startup.lua diff --git a/pocket/config.lua b/pocket/config.lua new file mode 100644 index 0000000..e69de29 diff --git a/pocket/startup.lua b/pocket/startup.lua new file mode 100644 index 0000000..aeeaef4 --- /dev/null +++ b/pocket/startup.lua @@ -0,0 +1,3 @@ +-- +-- SCADA Coordinator Access on a Pocket Computer +-- \ No newline at end of file From 00a81ab4f0d69b24d58c98d52eb23baf5f006843 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 14 Jan 2022 12:42:11 -0500 Subject: [PATCH 09/63] modbus comms implementation --- rtu/rtu.lua | 17 +++- scada-common/modbus.lua | 201 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 211 insertions(+), 7 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 97e11f7..32600a1 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -3,17 +3,23 @@ function rtu_init() discrete_inputs = {}, coils = {}, input_regs = {}, - holding_regs = {} + holding_regs = {}, + io_count_cache = { 0, 0, 0, 0 } } - local count_io = function () - return #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs + local __count_io = function () + self.io_count_cache = { #self.discrete_inputs, #self.coils, #self.input_regs, #self.holding_regs } + end + + local 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 local connect_di = function (f) table.insert(self.discrete_inputs, f) + __count_io() return #self.discrete_inputs end @@ -25,6 +31,7 @@ function rtu_init() local connect_coil = function (f_read, f_write) table.insert(self.coils, { read = f_read, write = f_write }) + __count_io() return #self.coils end @@ -40,6 +47,7 @@ function rtu_init() local connect_input_reg = function (f) table.insert(self.input_regs, f) + __count_io() return #self.input_regs end @@ -51,6 +59,7 @@ function rtu_init() local connect_holding_reg = function (f_read, f_write) table.insert(self.holding_regs, { read = f_read, write = f_write }) + __count_io() return #self.holding_regs end @@ -63,7 +72,7 @@ function rtu_init() end return { - count_io = count_io, + io_count = io_count, connect_di = connect_di, read_di = read_di, connect_coil = connect_coil, diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index 1e47f9c..e6d5eba 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -1,14 +1,209 @@ +-- 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 +} + +-- new modbus comms handler object function modbus_init(rtu_dev) local self = { rtu = rtu_dev } - function _1_read_coils(c_channel_start, count) + local _1_read_coils = function (c_addr_start, count) + local readings = {} + local _, coils, _, _ = self.rtu.io_count() + local return_ok = (c_addr_start + count) <= coils + + if return_ok then + for i = 0, (count - 1) do + readings[i] = self.rtu.read_coil(c_addr_start + i) + end + end + + return return_ok, readings end - function _2_read_discrete_inputs(di_channel_start, count) + local _2_read_discrete_inputs = function (di_addr_start, count) + local readings = {} + local discrete_inputs, _, _, _ = self.rtu.io_count() + local return_ok = (di_addr_start + count) <= discrete_inputs + + if return_ok then + for i = 0, (count - 1) do + readings[i] = self.rtu.read_di(di_addr_start + i) + end + end + + return return_ok, readings end - function _3_read_multiple_holding_registers(hr_channel_start, count) + local _3_read_multiple_holding_registers = function (hr_addr_start, count) + local readings = {} + local _, _, _, hold_regs = self.rtu.io_count() + local return_ok = (hr_addr_start + count) <= hold_regs + + if return_ok then + for i = 0, (count - 1) do + readings[i] = self.rtu.read_holding_reg(hr_addr_start + i) + end + end + + return return_ok, readings + end + + local _4_read_input_registers = function (ir_addr_start, count) + local readings = {} + local _, _, input_regs, _ = self.rtu.io_count() + local return_ok = (ir_addr_start + count) <= input_regs + + if return_ok then + for i = 0, (count - 1) do + readings[i] = self.rtu.read_input_reg(ir_addr_start + i) + end + end + + return return_ok, readings + end + + local _5_write_single_coil = function (c_addr, value) + local _, coils, _, _ = self.rtu.io_count() + local return_ok = c_addr <= coils + + if return_ok then + self.rtu.write_coil(c_addr, value) + end + + return return_ok + end + + local _6_write_single_holding_register = function (hr_addr, value) + local _, _, _, hold_regs = self.rtu.io_count() + local return_ok = hr_addr <= hold_regs + + if return_ok then + self.rtu.write_holding_reg(hr_addr, value) + end + + return return_ok + end + + local _15_write_multiple_coils = function (c_addr_start, values) + local _, coils, _, _ = self.rtu.io_count() + local count = #values + local return_ok = (c_addr_start + count) <= coils + + if return_ok then + for i = 0, (count - 1) do + self.rtu.write_coil(c_addr_start + i, values[i + 1]) + end + end + + return return_ok + end + + local _16_write_multiple_holding_registers = function (hr_addr_start, values) + local _, _, _, hold_regs = self.rtu.io_count() + local count = #values + local return_ok = (hr_addr_start + count) <= hold_regs + + if return_ok then + for i = 0, (count - 1) do + self.rtu.write_coil(hr_addr_start + i, values[i + 1]) + end + end + + return return_ok + end + + local handle_packet = function (packet) + local return_code = true + local readings = nil + + if #packet.data == 2 then + -- handle by function code + if packet.func_code == MODBUS_FCODE.READ_COILS then + return_code, readings = _1_read_coils(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then + return_code, readings = _2_read_discrete_inputs(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.READ_MUL_HOLD_REGS then + return_code, readings = _3_read_multiple_holding_registers(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGISTERS then + return_code, readings = _4_read_input_registers(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_COIL then + return_code = _5_write_single_coil(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then + return_code = _6_write_single_holding_register(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then + return_code = _15_write_multiple_coils(packet.data[1], packet.data[2]) + elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then + return_code = _16_write_multiple_holding_registers(packet.data[1], packet.data[2]) + else + -- unknown function + return_code = false + end + else + -- invalid length + return_code = false + end + + if return_code then + -- response (default is to echo back) + response = packet + if readings ~= nil then + response.length = #readings + response.data = readings + end + else + -- echo back with error flag + response = packet + response.func_code = bit.bor(packet.func_code, ERROR_FLAG) + end + + return return_code, response + end + + return { + handle_packet = handle_packet + } +end + +-- create new modbus packet +function new_modbus_packet(txn_id, protocol, length, unit_id, func_code, data) + return { + txn_id = txn_id, + protocol = protocol, + length = length, + unit_id = unit_id, + func_code = func_code, + data = data + } +end + +-- parse raw table data as a modbus packet +function parse_modbus_packet(raw) + if #raw ~= 6 then + return nil + else + return new_modbus_packet(raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]) end end + +-- create raw table data from a modbus packet +function modbus_to_raw(packet) + return { + packet.txn_id, + packet.protocol, + packet.length, + packet.unit_id, + packet.func_code, + packet.data + } +end From 018b22897670a32d93fe2e10ec3c25291a858b92 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 14 Jan 2022 16:32:20 -0500 Subject: [PATCH 10/63] some comms cleanup and added wrapper for generic packet --- scada-common/comms.lua | 72 ++++++++++++++++++++++++++++++++++++++++- scada-common/modbus.lua | 54 ++++++++++++++++++------------- 2 files changed, 103 insertions(+), 23 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 34de9cb..3007cf9 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,16 +1,86 @@ +PROTOCOLS = { + MODBUS_TCP = 0, -- our "modbus tcp"-esque protocol + RPLC = 1, -- reactor plc protocol + SCADA_MGMT = 2, -- SCADA supervisor intercommunication + COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller +} +-- generic SCADA packet object +function scada_packet() + local self = { + modem_msg_in = nil, + raw = nil + seq_id = nil, + protocol = nil, + length = nil + } + + local receive = function (side, sender, reply_to, message, distance) + self.modem_msg_in = { + iface = side, + s_port = sender, + r_port = reply_to, + msg = message, + dist = distance + } + + self.raw = self.modem_msg_in.msg + + if #self.raw < 3 then + -- malformed + return false + else + self.seq_id = self.raw[0] + self.protocol = self.raw[1] + self.length = self.raw[2] + end + end + + local seq_id = function (packet) + return self.seq_id + end + + local protocol = function (packet) + return self.protocol + end + + local length = function (packet) + return self.length + end + + local raw = function (packet) + return self.raw + end + + local modem_event = function (packet) + return self.modem_msg_in + end + + return { + receive = receive, + seq_id = seq_id, + protocol = protocol, + length = length, + raw = raw, + modem_event = modem_event + } +end + +-- coordinator communications function coord_comms() local self = { reactor_struct_cache = nil } end +-- supervisory controller communications function superv_comms() local self = { reactor_struct_cache = nil } end +-- reactor PLC communications function rplc_comms(id, modem, local_port, server_port, reactor) local self = { _id = id, @@ -150,4 +220,4 @@ function rplc_comms(id, modem, local_port, server_port, reactor) send_keep_alive = send_keep_alive, handle_link = handle_link } -end \ No newline at end of file +end diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index e6d5eba..6721c44 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, @@ -175,9 +177,8 @@ function modbus_init(rtu_dev) } end --- create new modbus packet -function new_modbus_packet(txn_id, protocol, length, unit_id, func_code, data) - return { +function modbus_packet() + local self = { txn_id = txn_id, protocol = protocol, length = length, @@ -185,25 +186,34 @@ function new_modbus_packet(txn_id, protocol, length, unit_id, func_code, data) func_code = func_code, data = data } -end --- parse raw table data as a modbus packet -function parse_modbus_packet(raw) - if #raw ~= 6 then - return nil - else - return new_modbus_packet(raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]) + local receive = function (raw) + local size_ok = #raw ~= 6 + + if size_ok then + set(raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]) + end + + return size_ok and self.protocol == comms.PROTOCOLS.MODBUS_TCP + end + + local set = 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 + + local get = function () + return { + 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 end - --- create raw table data from a modbus packet -function modbus_to_raw(packet) - return { - packet.txn_id, - packet.protocol, - packet.length, - packet.unit_id, - packet.func_code, - packet.data - } -end From b3a2cfabc63ac98d829359298a99688a65c747a3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 14 Jan 2022 16:33:09 -0500 Subject: [PATCH 11/63] reactor plc reorganization and some comms updates --- reactor-plc/plc.lua | 221 ++++++++++++++++++++-------------------- reactor-plc/safety.lua | 80 --------------- reactor-plc/startup.lua | 89 +++++++++++++--- scada-common/comms.lua | 8 +- 4 files changed, 189 insertions(+), 209 deletions(-) delete mode 100644 reactor-plc/safety.lua diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index b30a00e..ae6b21c 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,122 +1,121 @@ --- --- Reactor Programmable Logic Controller --- +function scada_link(comms) + local linked = false + local link_timeout = os.startTimer(5) -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/comms.lua") -os.loadAPI("reactor-plc/config.lua") -os.loadAPI("reactor-plc/safety.lua") + comms.send_link_req() + print_ts("sent link request") + + repeat + local event, p1, p2, p3, p4, p5 = os.pullEvent() + + -- handle event + if event == "timer" and param1 == link_timeout then + -- no response yet + print("...no response"); + comms.send_link_req() + print_ts("sent link request") + link_timeout = os.startTimer(5) + elseif event == "modem_message" then + -- server response? cancel timeout + if link_timeout ~= nil then + os.cancelTimer(link_timeout) + end -local R_PLC_VERSION = "alpha-v0.1" + local packet = comms.make_packet(p1, p2, p3, p4, p5) -local print_ts = util.print_ts - -local reactor = peripheral.find("fissionReactor") -local modem = peripheral.find("modem") - -print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") - --- we need a reactor and a modem -if reactor == nil then - print("Fission reactor not found, exiting..."); - return -elseif modem == nil then - print("No modem found, disabling reactor and exiting...") - reactor.scram() - return -end - --- just booting up, no fission allowed (neutrons stay put thanks) -reactor.scram() - --- init internal safety system -local iss = safety.iss_init(reactor) -local iss_status = "ok" -local iss_tripped = false - --- read config - --- start comms -if not modem.isOpen(config.LISTEN_PORT) then - modem.open(config.LISTEN_PORT) -end - -local comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) - --- attempt server connection -local linked = false -local link_timeout = os.startTimer(5) -comms.send_link_req() -print_ts("sent link request") -repeat - local event, param1, param2, param3, param4, param5 = os.pullEvent() - - -- handle event - if event == "timer" and param1 == link_timeout then - -- no response yet - print("...no response"); - comms.send_link_req() - print_ts("sent link request") - link_timeout = os.startTimer(5) - elseif event == "modem_message" then - -- server response? cancel timeout - if link_timeout ~= nil then - os.cancelTimer(link_timeout) + -- handle response + local response = comms.handle_link(packet) + if response == nil then + print_ts("invalid link response, bad channel?\n") + return + elseif response == true then + print_ts("...linked!\n") + linked = true + else + print_ts("...denied, exiting\n") + return + end end + until linked +end - local packet = { - side = param1, - sender = param2, - reply_to = param3, - message = param4, - distance = param5 - } +-- Internal Safety System +-- identifies dangerous states and SCRAMs reactor if warranted +-- autonomous from main control +function iss_init(reactor) + local self = { + _reactor = reactor, + _tripped = false, + _trip_cause = "" + } - -- handle response - response = comms.handle_link(packet) - if response == "wrong_type" then - print_ts("invalid link response, bad channel?\n") - return - elseif response == true then - print_ts("...linked!\n") - linked = true + local check = function () + local status = "ok" + + -- check system states in order of severity + if self.damage_critical() then + status = "dmg_crit" + elseif self.high_temp() then + status = "high_temp" + elseif self.excess_heated_coolant() then + status = "heated_coolant_backup" + elseif self.excess_waste() then + status = "full_waste" + elseif self.insufficient_fuel() then + status = "no_fuel" + elseif self._tripped then + status = self._trip_cause else - print_ts("...denied, exiting\n") - return + self._tripped = false end - end -until linked - --- comms watchdog, 3 second timeout -local conn_watchdog = watchdog.new_watchdog(3) - --- loop clock (10Hz, 2 ticks) --- send status updates at 4Hz (every 5 ticks) -local loop_tick = os.startTimer(0.05) -local ticks_to_update = 5 - --- event loop -while true do - local event, param1, param2, param3, param4, param5 = os.pullEvent() - - -- check safety (SCRAM occurs if tripped) - iss_status, iss_tripped = iss.check() - - -- handle event - if event == "timer" and param1 == loop_tick then - -- basic event tick, send updated data if it is time - ticks_to_update = ticks_to_update - 1 - if ticks_to_update == 0 then - ticks_to_update = 5 + + if status ~= "ok" then + self._tripped = true + self._trip_cause = status + self._reactor.scram() end - elseif event == "modem_message" then - -- got a packet - -- feed the watchdog first so it doesn't eat our packets - conn_watchdog.feed() - - elseif event == "timer" and param1 == conn_watchdog.get_timer() then - -- haven't heard from server recently? shutdown - reactor.scram() - print_ts("[alert] server timeout, reactor disabled\n") + + return self._tripped, status end + + local reset = function () + self._tripped = false + self._trip_cause = "" + end + + local damage_critical = function () + return self._reactor.getDamagePercent() >= 100 + end + + local excess_heated_coolant = function () + return self._reactor.getHeatedCoolantNeeded() == 0 + end + + local excess_waste = function () + return self._reactor.getWasteNeeded() == 0 + end + + local high_temp = function () + -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 + return self._reactor.getTemperature() >= 1200 + end + + local insufficient_fuel = function () + return self._reactor.getFuel() == 0 + end + + local no_coolant = function() + return self._reactor.getCoolantFilledPercentage() < 2 + end + + return { + check = check, + reset = reset, + 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 + } end diff --git a/reactor-plc/safety.lua b/reactor-plc/safety.lua deleted file mode 100644 index 29733ab..0000000 --- a/reactor-plc/safety.lua +++ /dev/null @@ -1,80 +0,0 @@ --- Internal Safety System --- identifies dangerous states and SCRAMs reactor if warranted --- autonomous from main control -function iss_init(reactor) - local self = { - _reactor = reactor, - _tripped = false, - _trip_cause = "" - } - - local check = function () - local status = "ok" - - -- check system states in order of severity - if self.damage_critical() then - status = "dmg_crit" - elseif self.high_temp() then - status = "high_temp" - elseif self.excess_heated_coolant() then - status = "heated_coolant_backup" - elseif self.excess_waste() then - status = "full_waste" - elseif self.insufficient_fuel() then - status = "no_fuel" - elseif self._tripped then - status = self._trip_cause - else - self._tripped = false - end - - if status ~= "ok" then - self._tripped = true - self._trip_cause = status - self._reactor.scram() - end - - return self._tripped, status - end - - local reset = function () - self._tripped = false - self._trip_cause = "" - end - - local damage_critical = function () - return self._reactor.getDamagePercent() >= 100 - end - - local excess_heated_coolant = function () - return self._reactor.getHeatedCoolantNeeded() == 0 - end - - local excess_waste = function () - return self._reactor.getWasteNeeded() == 0 - end - - local high_temp = function () - -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - return self._reactor.getTemperature() >= 1200 - end - - local insufficient_fuel = function () - return self._reactor.getFuel() == 0 - end - - local no_coolant = function() - return self._reactor.getCoolantFilledPercentage() < 2 - end - - return { - check = check, - reset = reset, - 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 - } -end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index a0b746a..e10024a 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -1,13 +1,78 @@ -print(">>RCASS LOADER START<<") -print(">>CHECKING SETTINGS...") -loaded = settings.load("rcass.settings") -if loaded then - print(">>SETTINGS FOUND, VERIFIYING INTEGRITY...") - settings.getNames() -else - print(">>SETTINGS NOT FOUND") - print(">>LAUNCHING CONFIGURATOR...") - shell.run("config") +-- +-- Reactor Programmable Logic Controller +-- + +os.loadAPI("scada-common/util.lua") +os.loadAPI("scada-common/comms.lua") +os.loadAPI("reactor-plc/config.lua") +os.loadAPI("reactor-plc/plc.lua") + +local R_PLC_VERSION = "alpha-v0.1" + +local print_ts = util.print_ts + +local reactor = peripheral.find("fissionReactor") +local modem = peripheral.find("modem") + +print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") + +-- we need a reactor and a modem +if reactor == nil then + print("Fission reactor not found, exiting..."); + return +elseif modem == nil then + print("No modem found, disabling reactor and exiting...") + reactor.scram() + return +end + +-- just booting up, no fission allowed (neutrons stay put thanks) +reactor.scram() + +-- init internal safety system +local iss = plc.iss_init(reactor) + +-- start comms +if not modem.isOpen(config.LISTEN_PORT) then + modem.open(config.LISTEN_PORT) +end + +local comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) + +-- attempt server connection +-- exits application if connection is denied +plc.scada_link(comms) + +-- comms watchdog, 3 second timeout +local conn_watchdog = watchdog.new_watchdog(3) + +-- loop clock (10Hz, 2 ticks) +-- send status updates at 4Hz (every 5 ticks) +local loop_tick = os.startTimer(0.05) +local ticks_to_update = 5 + +-- event loop +while true do + local event, param1, param2, param3, param4, param5 = os.pullEvent() + + -- check safety (SCRAM occurs if tripped) + local iss_status, iss_tripped = iss.check() + + -- handle event + if event == "timer" and param1 == loop_tick then + -- basic event tick, send updated data if it is time + ticks_to_update = ticks_to_update - 1 + if ticks_to_update == 0 then + ticks_to_update = 5 + end + elseif event == "modem_message" then + -- got a packet + -- feed the watchdog first so it doesn't eat our packets + conn_watchdog.feed() + + elseif event == "timer" and param1 == conn_watchdog.get_timer() then + -- haven't heard from server recently? shutdown + reactor.scram() + print_ts("[alert] server timeout, reactor disabled\n") + end end -print(">>LAUNCHING RCASS...") -shell.run("rcass") diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 3007cf9..aa2ab38 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -88,11 +88,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _server = server_port, _local = local_port, _reactor = reactor, - _status_cache = nil, - - _send = function (msg) - self._modem.transmit(self._server, self._local, msg) - end + _status_cache = nil } local _send = function (msg) @@ -209,7 +205,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor) if packet.type == "link_response" then return packet.accepted else - return "wrong_type" + return nil end end From c6722c4cbe28ed6d1ad338f9c9ccdc37549e9b24 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 14 Jan 2022 16:34:40 -0500 Subject: [PATCH 12/63] updated README for repo rename --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b9668b6..2ce9de1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,2 @@ -# cc-mek-reactor-controller -Configurable ComputerCraft multi-reactor control for Mekanism with a GUI, automatic safety features, waste processing control, and more! +# 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! From ffca88845bb8bce3c61a3e010993f077e6253846 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 22 Jan 2022 14:26:25 -0500 Subject: [PATCH 13/63] work on PLC comms --- reactor-plc/plc.lua | 128 +++++++++++++++------- reactor-plc/startup.lua | 35 ++++-- scada-common/comms.lua | 235 +++++++++++++++++++++++++++------------- 3 files changed, 277 insertions(+), 121 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index ae6b21c..d1fda80 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,8 +1,8 @@ -function scada_link(comms) +function scada_link(plc_comms) local linked = false local link_timeout = os.startTimer(5) - comms.send_link_req() + plc_comms.send_link_req() print_ts("sent link request") repeat @@ -12,31 +12,40 @@ function scada_link(comms) if event == "timer" and param1 == link_timeout then -- no response yet print("...no response"); - comms.send_link_req() - print_ts("sent link request") - link_timeout = os.startTimer(5) elseif event == "modem_message" then -- server response? cancel timeout if link_timeout ~= nil then os.cancelTimer(link_timeout) end - local packet = comms.make_packet(p1, p2, p3, p4, p5) - - -- handle response - local response = comms.handle_link(packet) - if response == nil then - print_ts("invalid link response, bad channel?\n") - return - elseif response == true then - print_ts("...linked!\n") - linked = true - else - print_ts("...denied, exiting\n") - return + local s_packet = comms.scada_packet() + s_packet.receive(p1, p2, p3, p4, p5) + local packet = s_packet.as_rplc() + if packet then + -- handle response + local response = plc_comms.handle_link(packet) + if response == nil then + print_ts("invalid link response, bad channel?\n") + break + elseif response == comms.RPLC_LINKING.COLLISION then + print_ts("...reactor PLC ID collision (check config), exiting...\n") + break + elseif response == comms.RPLC_LINKING.ALLOW then + print_ts("...linked!\n") + linked = true + plc_comms.send_rs_io_conns() + plc_comms.send_struct() + plc_comms.send_status() + print_ts("sent initial data\n") + else + print_ts("...denied, exiting...\n") + break + end end end until linked + + return linked end -- Internal Safety System @@ -44,13 +53,15 @@ end -- autonomous from main control function iss_init(reactor) local self = { - _reactor = reactor, - _tripped = false, - _trip_cause = "" + reactor = reactor, + timed_out = false, + tripped = false, + trip_cause = "" } local check = function () local status = "ok" + local was_tripped = self.tripped -- check system states in order of severity if self.damage_critical() then @@ -63,59 +74,100 @@ function iss_init(reactor) status = "full_waste" elseif self.insufficient_fuel() then status = "no_fuel" - elseif self._tripped then - status = self._trip_cause + elseif self.tripped then + status = self.trip_cause else - self._tripped = false + self.tripped = false end if status ~= "ok" then - self._tripped = true - self._trip_cause = status - self._reactor.scram() + self.tripped = true + self.trip_cause = status + self.reactor.scram() end + + local first_trip = ~was_tripped and self.tripped - return self._tripped, status + return self.tripped, status, first_trip + end + + local trip_timeout = function () + self.tripped = false + self.trip_cause = "timeout" + self.timed_out = true + self.reactor.scram() end local reset = function () - self._tripped = false - self._trip_cause = "" + self.timed_out = false + self.tripped = false + self.trip_cause = "" + end + + 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 end local damage_critical = function () - return self._reactor.getDamagePercent() >= 100 + return self.reactor.getDamagePercent() >= 100 end local excess_heated_coolant = function () - return self._reactor.getHeatedCoolantNeeded() == 0 + return self.reactor.getHeatedCoolantNeeded() == 0 end local excess_waste = function () - return self._reactor.getWasteNeeded() == 0 + return self.reactor.getWasteNeeded() == 0 end local high_temp = function () -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - return self._reactor.getTemperature() >= 1200 + return self.reactor.getTemperature() >= 1200 end local insufficient_fuel = function () - return self._reactor.getFuel() == 0 + return self.reactor.getFuel() == 0 end - local no_coolant = function() - return self._reactor.getCoolantFilledPercentage() < 2 + local no_coolant = function () + return self.reactor.getCoolantFilledPercentage() < 2 + end + + local timed_out = function () + return self.timed_out end return { check = check, + trip_timeout = trip_timeout, 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 + no_coolant = no_coolant, + timed_out = timed_out } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index e10024a..6d2d9de 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -37,11 +37,13 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local plc_comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) -- attempt server connection --- exits application if connection is denied -plc.scada_link(comms) +-- exit application if connection is denied +if ~plc.scada_link(plc_comms) then + return +end -- comms watchdog, 3 second timeout local conn_watchdog = watchdog.new_watchdog(3) @@ -51,28 +53,47 @@ local conn_watchdog = watchdog.new_watchdog(3) local loop_tick = os.startTimer(0.05) local ticks_to_update = 5 +-- runtime variables +local control_state = false + -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEvent() + if event == "peripheral_detach" then + print_ts("[fatal] lost a peripheral, stopping...\n") + -- todo: determine which disconnected and what is left + -- hopefully it wasn't the reactor + reactor.scram() + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_DC) ? + return + end + -- check safety (SCRAM occurs if tripped) - local iss_status, iss_tripped = iss.check() + local iss_status, iss_tripped, iss_first = iss.check() + if iss_first then + plc_comms.send_iss_alarm(iss_status) + end -- handle event if event == "timer" and param1 == loop_tick then - -- basic event tick, send updated data if it is time + -- basic event tick, send updated data if it is time (4Hz) ticks_to_update = ticks_to_update - 1 if ticks_to_update == 0 then + plc_comms.send_status(control_state, iss_tripped) ticks_to_update = 5 end elseif event == "modem_message" then -- got a packet - -- feed the watchdog first so it doesn't eat our packets + -- feed the watchdog first so it doesn't uhh,,,eat our packets conn_watchdog.feed() + local packet = comms.make_packet(p1, p2, p3, p4, p5) + plc_comms.handle_packet(packet) + elseif event == "timer" and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown - reactor.scram() + iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index aa2ab38..23af375 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -5,16 +5,47 @@ PROTOCOLS = { COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } +RPLC_TYPES = { + KEEP_ALIVE = 0, -- keep alive packets + LINK_REQ = 1, -- linking requests + STATUS = 2, -- reactor/system status + MEK_STRUCT = 3, -- mekanism build structure + RS_IO_CONNS = 4, -- redstone I/O connections + RS_IO_SET = 5, -- set redstone outputs + RS_IO_GET = 6, -- get redstone inputs + MEK_SCRAM = 7, -- SCRAM reactor + MEK_ENABLE = 8, -- enable reactor + MEK_BURN_RATE = 9, -- set burn rate + ISS_ALARM = 10, -- ISS alarm broadcast + ISS_GET = 11, -- get ISS status + ISS_CLEAR = 12 -- clear ISS trip (if in bad state, will trip immideatly) +} + +RPLC_LINKING = { + ALLOW = 0, + DENY = 1, + COLLISION = 2 +} + -- generic SCADA packet object function scada_packet() local self = { modem_msg_in = nil, - raw = nil + valid = false, seq_id = nil, protocol = nil, - length = nil + length = nil, + raw = nil } + local make = function (seq_id, protocol, payload) + self.valid = true + self.seq_id = seq_id + self.protocol = protocol + self.length = #payload + self.raw = { self.seq_id, self.protocol, self.length, payload } + end + local receive = function (side, sender, reply_to, message, distance) self.modem_msg_in = { iface = side, @@ -30,9 +61,10 @@ function scada_packet() -- malformed return false else - self.seq_id = self.raw[0] - self.protocol = self.raw[1] - self.length = self.raw[2] + self.valid = true + self.seq_id = self.raw[1] + self.protocol = self.raw[2] + self.length = self.raw[3] end end @@ -48,6 +80,14 @@ function scada_packet() return self.length end + local data = function (packet) + local subset = nil + if self.valid then + subset = { table.unpack(self.raw, 4, 3 + self.length) } + end + return subset + end + local raw = function (packet) return self.raw end @@ -56,7 +96,24 @@ function scada_packet() return self.modem_msg_in end + local as_rplc = function () + local pkt = nil + if self.valid and self.protocol == PROTOCOLS.RPLC then + local body = data() + if #body > 2 then + pkt = { + id = body[1], + type = body[2], + length = #body - 2, + body = { table.unpack(body, 3, 2 + #body) } + } + end + end + return pkt + end + return { + make = make, receive = receive, seq_id = seq_id, protocol = protocol, @@ -83,65 +140,77 @@ end -- reactor PLC communications function rplc_comms(id, modem, local_port, server_port, reactor) local self = { - _id = id, - _modem = modem, - _server = server_port, - _local = local_port, - _reactor = reactor, - _status_cache = nil + id = id, + seq_id = 0, + modem = modem, + s_port = server_port, + l_port = local_port, + reactor = reactor, + status_cache = nil } + -- PRIVATE FUNCTIONS -- + local _send = function (msg) - self._modem.transmit(self._server, self._local, msg) + local packet = scada_packet() + packet.make(self.seq_id, PROTOCOLS.RPLC, msg) + self.modem.transmit(self.s_port, self.l_port, packet.raw()) + self.seq_id = self.seq_id + 1 end -- variable reactor status information, excluding heating rate local _reactor_status = function () 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(), + 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(), - 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() + 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() } end - local _status_changed = function () - local status = self._reactor_status() + local _update_status_cache = function () + local status = _reactor_status() local changed = false - for key, value in pairs() do - if value ~= _status_cache[key] then + for key, value in pairs(status) do + if value ~= self.status_cache[key] then changed = true break end end + if changed then + self.status_cache = status + end + return changed end - -- attempt to establish link with + -- PUBLIC FUNCTIONS -- + + -- attempt to establish link with supervisor local send_link_req = function () local linking_data = { - id = self._id, - type = "link_req" + id = self.id, + type = RPLC_TYPES.LINK_REQ } _send(linking_data) @@ -151,19 +220,19 @@ function rplc_comms(id, modem, local_port, server_port, reactor) -- server will cache these local send_struct = function () 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() + 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() } local struct_packet = { - id = self._id, - type = "struct_data", + id = self.id, + type = RPLC_TYPES.MEK_STRUCT, mek_data = mek_data } @@ -171,49 +240,63 @@ function rplc_comms(id, modem, local_port, server_port, reactor) end -- send live status information - local send_status = function () - local mek_data = self._reactor_status() + -- control_state: acknowledged control state from supervisor + -- overridden: if ISS force disabled reactor + local send_status = function (control_state, overridden) + local mek_data = nil - local sys_data = { - timestamp = os.time(), - control_state = false, - overridden = false, - faults = {}, - waste_production = "antimatter" -- "plutonium", "polonium", "antimatter" - } - end - - local send_keep_alive = function () - -- heating rate is volatile, so it is skipped in status - -- send it with keep alive packets - local mek_data = { - heating_rate = self._reactor.getHeatingRate() - } - - -- basic keep alive packet to server - local keep_alive_packet = { - id = self._id, - type = "keep_alive", + if _update_status_cache() then + mek_data = self.status_cache + end + + local sys_status = { + id = self.id, + type = RPLC_TYPES.STATUS, timestamp = os.time(), + control_state = control_state, + overridden = overridden, + heating_rate = self.reactor.getHeatingRate(), mek_data = mek_data } - _send(keep_alive_packet) + _send(sys_status) + end + + local send_rs_io_conns = function () end local handle_link = function (packet) - if packet.type == "link_response" then - return packet.accepted + if packet.type == RPLC_TYPES.LINK_REQ then + return packet.data[1] == RPLC_LINKING.ALLOW else return nil end end + local handle_packet = function (packet) + if packet.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive request received, nothing to do except feed watchdog + elseif packet.type == RPLC_TYPES.MEK_STRUCT then + -- request for physical structure + send_struct() + elseif packet.type == RPLC_TYPES.RS_IO_CONNS then + -- request for redstone connections + send_rs_io_conns() + elseif packet.type == RPLC_TYPES.RS_IO_GET then + elseif packet.type == RPLC_TYPES.RS_IO_SET then + elseif packet.type == RPLC_TYPES.MEK_SCRAM then + elseif packet.type == RPLC_TYPES.MEK_ENABLE then + elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPES.ISS_GET then + elseif packet.type == RPLC_TYPES.ISS_CLEAR then + end + end + return { send_link_req = send_link_req, send_struct = send_struct, send_status = send_status, - send_keep_alive = send_keep_alive, + send_rs_io_conns = send_rs_io_conns, handle_link = handle_link } end From 14cb7f96fccec529b0215b05f48eb618290c37f3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 22 Jan 2022 14:47:54 -0500 Subject: [PATCH 14/63] supervisor comms init --- scada-common/comms.lua | 14 +++++++++++++- supervisor/config.lua | 8 +++----- supervisor/scada-supervisor.lua | 22 ++++++++++++++++------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 23af375..5641cba 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -5,6 +5,11 @@ PROTOCOLS = { COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } +SCADA_SV_MODES = { + ACTIVE = 0, + BACKUP = 1 +} + RPLC_TYPES = { KEEP_ALIVE = 0, -- keep alive packets LINK_REQ = 1, -- linking requests @@ -131,8 +136,15 @@ function coord_comms() end -- supervisory controller communications -function superv_comms() +function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_channel) local self = { + mode = mode, + seq_id = 0, + num_reactors = num_reactors, + modem = modem, + dev_listen = dev_listen, + fo_channel = fo_channel, + sv_channel = sv_channel, reactor_struct_cache = nil } end diff --git a/supervisor/config.lua b/supervisor/config.lua index 39adeef..bc02177 100644 --- a/supervisor/config.lua +++ b/supervisor/config.lua @@ -5,14 +5,12 @@ -- instant failover if active goes offline without re-sync SYSTEM_TYPE = 'active' --- scada network -SCADA_NET_PFX = 16000 +-- scada network listen for PLC's and RTU's +SCADA_DEV_LISTEN = 16000 -- failover synchronization SCADA_FO_CHANNEL = 16001 --- listen port for SCADA supervisor access +-- listen port for SCADA supervisor access by coordinators SCADA_SV_CHANNEL = 16002 --- listen port for PLC's -SCADA_PLC_LISTEN = 16003 -- expected number of reactors NUM_REACTORS = 4 diff --git a/supervisor/scada-supervisor.lua b/supervisor/scada-supervisor.lua index 772c572..28a7e40 100644 --- a/supervisor/scada-supervisor.lua +++ b/supervisor/scada-supervisor.lua @@ -20,14 +20,24 @@ if modem == nil then return end --- read config - --- start comms -if not modem.isOpen(config.LISTEN_PORT) then - modem.open(config.LISTEN_PORT) +-- determine active/backup mode +local mode = comms.SCADA_SV_MODES.BACKUP +if config.SYSTEM_TYPE == "active" then + mode = comms.SCADA_SV_MODES.ACTIVE end -local comms = comms.superv_comms(config.NUM_REACTORS, modem, config.SCADA_PLC_LISTEN, config.SCADA_SV_CHANNEL) +-- 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 = comms.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) From 8429cbfd6e39ae43c5ac855ffd223092f8fe59c5 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 25 Jan 2022 13:51:43 -0500 Subject: [PATCH 15/63] scada alarms --- scada-common/alarm.lua | 54 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 scada-common/alarm.lua diff --git a/scada-common/alarm.lua b/scada-common/alarm.lua new file mode 100644 index 0000000..b93df73 --- /dev/null +++ b/scada-common/alarm.lua @@ -0,0 +1,54 @@ +SEVERITY = { + INFO = 0, -- basic info message + WARNING = 1, -- warning about some abnormal state + ALERT = 2, -- important device state changes + FACILITY = 3, -- facility-wide alert + SAFETY = 4, -- safety alerts + EMERGENCY = 5 -- critical safety alarm +} + +function scada_alarm(severity, device, message) + local self = { + time = os.time(), + ts_string = os.date("[%H:%M:%S]"), + severity = severity, + device = device, + message = message + } + + local format = function () + return self.ts_string .. " [" .. severity_to_string(self.severity) .. "] (" .. self.device ") >> " .. self.message + end + + local properties = function () + return { + time = self.time, + severity = self.severity, + device = self.device, + message = self.message + } + end + + return { + format = format, + properties = properties + } +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 From d6a68ee3d954193d1c4d04feb5843068840a88f0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 25 Jan 2022 14:51:33 -0500 Subject: [PATCH 16/63] rtu's for boiler, induction matrix, and turbine --- rtu/dev/boiler.lua | 51 +++++++++++++++++++++++++++++++++++++++++++++ rtu/dev/imatrix.lua | 33 +++++++++++++++++++++++++++++ rtu/dev/turbine.lua | 46 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 130 insertions(+) create mode 100644 rtu/dev/boiler.lua create mode 100644 rtu/dev/imatrix.lua create mode 100644 rtu/dev/turbine.lua diff --git a/rtu/dev/boiler.lua b/rtu/dev/boiler.lua new file mode 100644 index 0000000..d76107a --- /dev/null +++ b/rtu/dev/boiler.lua @@ -0,0 +1,51 @@ +-- #REQUIRES rtu.lua + +function boiler_rtu(boiler) + local self = { + rtu = rtu_init(), + boiler = boiler + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + -- none + + -- input registers -- + -- 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.lua b/rtu/dev/imatrix.lua new file mode 100644 index 0000000..f87bdbf --- /dev/null +++ b/rtu/dev/imatrix.lua @@ -0,0 +1,33 @@ +-- #REQUIRES rtu.lua + +function imatrix_rtu(imatrix) + local self = { + rtu = rtu_init(), + imatrix = imatrix + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + -- none + + -- input registers -- + -- build properties + 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) + + -- holding registers -- + -- none + + return { + rtu_interface = rtu_interface + } +end diff --git a/rtu/dev/turbine.lua b/rtu/dev/turbine.lua new file mode 100644 index 0000000..4ecc156 --- /dev/null +++ b/rtu/dev/turbine.lua @@ -0,0 +1,46 @@ +-- #REQUIRES rtu.lua + +function turbine_rtu(turbine) + local self = { + rtu = rtu_init(), + turbine = turbine + } + + local rtu_interface = function () + return self.rtu + end + + -- discrete inputs -- + -- none + + -- coils -- + -- none + + -- input registers -- + -- 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 + self.rtu.connect_input_reg(self.turbine.getSteam) + self.rtu.connect_input_reg(self.turbine.getSteamNeeded) + self.rtu.connect_input_reg(self.turbine.getSteamFilledPercentage) + + -- holding registers -- + -- none + + return { + rtu_interface = rtu_interface + } +end From 9cd0079d9e76e35c2141a936c19f79af837a327d Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 25 Jan 2022 15:48:01 -0500 Subject: [PATCH 17/63] updated README --- README.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/README.md b/README.md index 2ce9de1..abc3fda 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,54 @@ # 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! + + +## [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. + +This project implements concepts of a SCADA system in ComputerCraft (because why not? ..okay don't answer that). I recommend reviewing that linked wikipedia page on SCADA if you want to understand the concepts used here. + +![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 +- 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 + +There can be one or more of these. They can be either an Advanced Computer or a Pocket Computer. + +### 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. + +### RTUs + +RTUs are effectively basic connections between a device and the SCADA system with no internal logic providing the system with I/O capabilities. A single Advanced Computer can represent multiple RTUs as instead I am modeling an RTU as the wired modems connected to that computer rather than the computer itself. Each RTU is referenced separately with an identifier in the modbus communications (see Communications section), so a single computer can distribute instructions to multiple devices. This should save on having a pile of computers everywhere (but if you want to have that, no one's stopping you). + +The RTU control code is relatively unique, as instead of having instructions be decoded simply, due to using modbus, I implemented a generalized RTU interface. To fulfill this, each type of I/O operation is linked to a function rather than implementing the logic itself. For example, to connect an input register to a turbine getFlowRate call, the function reference itself is passed to the `connect_input_reg()` function. A call to `read_input_reg()` on that register address will call the `turbine.getFlowRate()` function and return the result. + +### PLCs + +PLCs are advanced devices that allow for both reporting and control to/from the SCADA system in addition to programed behaviors independent of the SCADA system. Currently there is only one type of PLC, and that is the reactor PLC. This is responsible for reporting on and controlling the reactor as a part of the SCADA system, and independently regulating the safety of the reactor. It checks the status for multiple hazard scenarios and shuts down the reactor if any condition is satisfied. + +There can and should only be one of these per reactor. A single Advanced Computer will act as the PLC, with either a direct connection (physical contact) or a wired modem connection to the reactor logic port. + +## Communications + +A vaguely-modbus [modbus](https://en.wikipedia.org/wiki/Modbus) communication protocol is used for communication with RTUs. Useful terminology for you to know: +- Discrete Inputs: Single Bit Read-Only (digital inputs) +- Coils: Single Bit Read/Write (digital I/O) +- Input Registers: Multi-Byte Read-Only (analog inputs) +- Holding Registers: Multi-Byte Read/Write (analog I/O) + +### Security and Encryption + +TBD, I am planning on AES symmetric encryption for security + HMAC to prevent replay attacks. This will be done utilizing this codebase: https://github.com/somesocks/lua-lockbox. + +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. From 1c6244d2357b9ec3b80307f038cba98e741f0fae Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 25 Jan 2022 17:07:42 -0500 Subject: [PATCH 18/63] README formatting --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index abc3fda..37b9124 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ There can be at most two of these in an active-backup configuration. If a backup RTUs are effectively basic connections between a device and the SCADA system with no internal logic providing the system with I/O capabilities. A single Advanced Computer can represent multiple RTUs as instead I am modeling an RTU as the wired modems connected to that computer rather than the computer itself. Each RTU is referenced separately with an identifier in the modbus communications (see Communications section), so a single computer can distribute instructions to multiple devices. This should save on having a pile of computers everywhere (but if you want to have that, no one's stopping you). -The RTU control code is relatively unique, as instead of having instructions be decoded simply, due to using modbus, I implemented a generalized RTU interface. To fulfill this, each type of I/O operation is linked to a function rather than implementing the logic itself. For example, to connect an input register to a turbine getFlowRate call, the function reference itself is passed to the `connect_input_reg()` function. A call to `read_input_reg()` on that register address will call the `turbine.getFlowRate()` function and return the result. +The RTU control code is relatively unique, as instead of having instructions be decoded simply, due to using modbus, I implemented a generalized RTU interface. To fulfill this, each type of I/O operation is linked to a function rather than implementing the logic itself. For example, to connect an input register to a turbine `getFlowRate()` call, the function reference itself is passed to the `connect_input_reg()` function. A call to `read_input_reg()` on that register address will call the `turbine.getFlowRate()` function and return the result. ### PLCs From 3c67ee08a867cb5fc50e6fd665f9860b339153c3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 8 Feb 2022 15:42:06 -0500 Subject: [PATCH 19/63] redstone RTU I/O --- rtu/dev/redstone.lua | 88 +++++++++++++++++++++++++++++++++++++++++++ scada-common/rsio.lua | 37 ++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 rtu/dev/redstone.lua create mode 100644 scada-common/rsio.lua diff --git a/rtu/dev/redstone.lua b/rtu/dev/redstone.lua new file mode 100644 index 0000000..0ec1283 --- /dev/null +++ b/rtu/dev/redstone.lua @@ -0,0 +1,88 @@ +-- #REQUIRES rtu.lua +-- note: this RTU makes extensive use of the programming concept of closures + +function redstone_rtu() + local self = { + rtu = rtu_init() + } + + local rtu_interface = function () + return self.rtu + end + + local link_di = function (side, color) + local f_read = nil + + if color then + f_read = function () + return rs.testBundledInput(side, color) + end + else + f_read = function () + return rs.getInput(side) + end + end + + self.rtu.connect_di(f_read) + end + + local link_do = function (side, color) + local f_read = nil + local f_write = nil + + if color then + f_read = function () + return colors.test(rs.getBundledOutput(side), color) + end + + f_write = function (value) + local output = rs.getBundledOutput(side) + + if value then + colors.combine(output, value) + else + colors.subtract(output, value) + end + + rs.setBundledOutput(side, output) + end + else + f_read = function () + return rs.getOutput(side) + end + + f_write = function (value) + rs.setOutput(side, color) + end + end + + self.rtu.connect_coil(f_read, f_write) + end + + local link_ai = function (side) + self.rtu.connect_input_reg( + function () + return rs.getAnalogInput(side) + end + ) + end + + local link_ao = function (side) + self.rtu.connect_holding_reg( + function () + return rs.getAnalogOutput(side) + end, + function (value) + rs.setAnalogOutput(side, value) + end + ) + end + + return { + rtu_interface = rtu_interface, + link_di = link_di, + link_do = link_do, + link_ai = link_ai, + link_ao = link_ao + } +end diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua new file mode 100644 index 0000000..ebc91f4 --- /dev/null +++ b/scada-common/rsio.lua @@ -0,0 +1,37 @@ +RS_IO = { + -- digital inputs -- + + -- facility + F_SCRAM, -- active high, facility-wide scram + F_AE2_LIVE, -- active high, indicates whether AE2 network is online (hint: use redstone P2P) + + -- reactor + R_SCRAM, -- active high, reactor scram + R_ENABLE, -- active high, reactor enable + + -- digital outputs -- + + -- waste + WASTE_PO, -- active low, polonium routing + WASTE_PU, -- active low, plutonium routing + WASTE_AM, -- active low, antimatter routing + + -- reactor + R_SCRAMMED, -- if the reactor is scrammed + R_AUTO_SCRAM, -- if the reactor was automatically scrammed + R_ACTIVE, -- if the reactor is active + R_AUTO_CTRL, -- if the reactor burn rate is automatic + R_DMG_CRIT, -- if the reactor damage is critical + R_HIGH_TEMP, -- if the reactor is at a high temperature + R_NO_COOLANT, -- if the reactor has no coolant + R_EXCESS_HC, -- if the reactor has excess heated coolant + R_EXCESS_WS, -- if the reactor has excess waste + R_INSUFF_FUEL, -- if the reactor has insufficent fuel + R_PLC_TIMEOUT, -- if the reactor PLC has not been heard from + + -- analog outputs -- + + A_R_BURN_RATE, -- reactor burn rate percentage + A_B_BOIL_RATE, -- boiler boil rate percentage + A_T_FLOW_RATE -- turbine flow rate percentage +} From ea84563bb430bee503da323036a7bd0a17f37992 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 10 Mar 2022 14:09:21 -0500 Subject: [PATCH 20/63] added protected peripheral manager and file system logger --- scada-common/log.lua | 43 +++++++++++++++ scada-common/ppm.lua | 127 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 scada-common/log.lua create mode 100644 scada-common/ppm.lua diff --git a/scada-common/log.lua b/scada-common/log.lua new file mode 100644 index 0000000..29fc688 --- /dev/null +++ b/scada-common/log.lua @@ -0,0 +1,43 @@ +-- +-- 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 file_handle = fs.open("/log.txt", "a") + +local _log = function (msg) + local stamped = os.date("[%c] ") .. msg + file_handle.writeLine(stamped) + file_handle.flush() +end + +function _debug(msg, trace) + local dbg_info = "" + + if trace then + local name = "" + + if debug.getinfo(2).name ~= nil then + name = ":" .. debug.getinfo(2).name .. "():" + end + + dbg_info = debug.getinfo(2).short_src .. ":" .. name .. + debug.getinfo(2).currentline .. " > " + end + + _log("[DBG] " .. dbg_info .. msg) +end + +function _warning(msg) + _log("[WRN] " .. msg) +end + +function _error(msg) + _log("[ERR] " .. msg) +end + +function _fatal(msg) + _log("[FTL] " .. msg) +end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua new file mode 100644 index 0000000..32b05ae --- /dev/null +++ b/scada-common/ppm.lua @@ -0,0 +1,127 @@ +-- #REQUIRES log.lua + +-- +-- Protected Peripheral Manager +-- + +function ppm() + local self = { + mounts = {} + } + + -- 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 (...) + local status, result = pcall(func, ...) + + if status then + return result + else + -- function failed + log._error("protected " .. key .. "() -> " .. result) + return nil + end + end + end + end + + -- mount all available peripherals (clears mounts first) + local mount_all = function () + local ifaces = peripheral.getNames() + + self.mounts = {} + + for i = 1, #ifaces do + local pm_dev = peripheral.wrap(ifaces[i]) + peri_init(pm_dev) + self.mounts[ifaces[i]] = { peripheral.getType(ifaces[i]), pm_dev } + end + end + + -- mount a particular device + local mount = function (name) + local ifaces = peripheral.getNames() + local pm_dev = nil + + for i = 1, #ifaces do + if name == peripheral.getType(ifaces[i]) then + pm_dev = peripheral.wrap(ifaces[i]) + peri_init(pm_dev) + + self.mounts[ifaces[i]] = { + type = peripheral.getType(ifaces[i]), + device = pm_dev + } + break + end + end + + return pm_dev + end + + -- handle peripheral_detach event + local unmount_handler = function (iface) + -- what got disconnected? + local lost_dev = self.mounts[iface] + local type = lost_dev.type + + log._warning("PMGR: lost device " .. type .. " mounted to " .. iface) + + return self.mounts[iface] + end + + -- list all available peripherals + local list_avail = function () + return peripheral.getNames() + end + + -- list mounted peripherals + local list_mounts = function () + return self.mounts + end + + -- get a mounted peripheral by side/interface + local get_periph = function (iface) + return self.mounts[iface].device + end + + -- get a mounted peripheral by type + local get_device = function (name) + local device = nil + + for side, data in pairs(self.mounts) do + if data.type == name then + device = data.device + break + end + end + + return device + end + + -- list all connected monitors + local list_monitors = function () + local monitors = {} + + for side, data in pairs(self.mounts) do + if data.type == "monitor" then + monitors[side] = data.device + end + end + + return monitors + end + + return { + mount_all = mount_all, + mount = mount, + umount = unmount_handler, + list_avail = list_avail, + list_mounts = list_mounts, + get_periph = get_periph, + get_device = get_device, + list_monitors = list_monitors + } +end From a0b2c1f3e2dbc872b9f921b69cde80c237d9b016 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 10 Mar 2022 14:12:07 -0500 Subject: [PATCH 21/63] changed ppm to not wrap under ppm() function --- scada-common/ppm.lua | 217 ++++++++++++++++++++----------------------- 1 file changed, 102 insertions(+), 115 deletions(-) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 32b05ae..2eaa9ab 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -4,124 +4,111 @@ -- Protected Peripheral Manager -- -function ppm() - local self = { - mounts = {} - } +local self = { + mounts = {} +} - -- 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 (...) - local status, result = pcall(func, ...) +-- 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 (...) + local status, result = pcall(func, ...) - if status then - return result - else - -- function failed - log._error("protected " .. key .. "() -> " .. result) - return nil - end + if status then + return result + else + -- function failed + log._error("protected " .. key .. "() -> " .. result) + return nil end end end - - -- mount all available peripherals (clears mounts first) - local mount_all = function () - local ifaces = peripheral.getNames() - - self.mounts = {} - - for i = 1, #ifaces do - local pm_dev = peripheral.wrap(ifaces[i]) - peri_init(pm_dev) - self.mounts[ifaces[i]] = { peripheral.getType(ifaces[i]), pm_dev } - end - end - - -- mount a particular device - local mount = function (name) - local ifaces = peripheral.getNames() - local pm_dev = nil - - for i = 1, #ifaces do - if name == peripheral.getType(ifaces[i]) then - pm_dev = peripheral.wrap(ifaces[i]) - peri_init(pm_dev) - - self.mounts[ifaces[i]] = { - type = peripheral.getType(ifaces[i]), - device = pm_dev - } - break - end - end - - return pm_dev - end - - -- handle peripheral_detach event - local unmount_handler = function (iface) - -- what got disconnected? - local lost_dev = self.mounts[iface] - local type = lost_dev.type - - log._warning("PMGR: lost device " .. type .. " mounted to " .. iface) - - return self.mounts[iface] - end - - -- list all available peripherals - local list_avail = function () - return peripheral.getNames() - end - - -- list mounted peripherals - local list_mounts = function () - return self.mounts - end - - -- get a mounted peripheral by side/interface - local get_periph = function (iface) - return self.mounts[iface].device - end - - -- get a mounted peripheral by type - local get_device = function (name) - local device = nil - - for side, data in pairs(self.mounts) do - if data.type == name then - device = data.device - break - end - end - - return device - end - - -- list all connected monitors - local list_monitors = function () - local monitors = {} - - for side, data in pairs(self.mounts) do - if data.type == "monitor" then - monitors[side] = data.device - end - end - - return monitors - end - - return { - mount_all = mount_all, - mount = mount, - umount = unmount_handler, - list_avail = list_avail, - list_mounts = list_mounts, - get_periph = get_periph, - get_device = get_device, - list_monitors = list_monitors - } +end + +-- mount all available peripherals (clears mounts first) +function mount_all() + local ifaces = peripheral.getNames() + + self.mounts = {} + + for i = 1, #ifaces do + local pm_dev = peripheral.wrap(ifaces[i]) + peri_init(pm_dev) + self.mounts[ifaces[i]] = { peripheral.getType(ifaces[i]), pm_dev } + end +end + +-- mount a particular device +function mount(name) + local ifaces = peripheral.getNames() + local pm_dev = nil + + for i = 1, #ifaces do + if name == peripheral.getType(ifaces[i]) then + pm_dev = peripheral.wrap(ifaces[i]) + peri_init(pm_dev) + + self.mounts[ifaces[i]] = { + type = peripheral.getType(ifaces[i]), + device = pm_dev + } + break + end + end + + return pm_dev +end + +-- handle peripheral_detach event +function unmount_handler(iface) + -- what got disconnected? + local lost_dev = self.mounts[iface] + local type = lost_dev.type + + log._warning("PMGR: lost device " .. type .. " mounted to " .. iface) + + return self.mounts[iface] +end + +-- list all available peripherals +function list_avail() + return peripheral.getNames() +end + +-- list mounted peripherals +function list_mounts() + return self.mounts +end + +-- get a mounted peripheral by side/interface +function get_periph(iface) + return self.mounts[iface].device +end + +-- get a mounted peripheral by type +function get_device(name) + local device = nil + + for side, data in pairs(self.mounts) do + if data.type == name then + device = data.device + break + end + end + + return device +end + +-- list all connected monitors +function list_monitors() + local monitors = {} + + for side, data in pairs(self.mounts) do + if data.type == "monitor" then + monitors[side] = data.device + end + end + + return monitors end From 5cff346cb55085909d239cfce2c2c7ff47f5b542 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 10 Mar 2022 14:21:03 -0500 Subject: [PATCH 22/63] ppm function renames, edited log messages, and changed protected calls to return true if function has no return --- scada-common/ppm.lua | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 2eaa9ab..4b9e2f1 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -16,10 +16,15 @@ local peri_init = function (device) local status, result = pcall(func, ...) if status then - return result + -- assume nil is only for functions with no return, so return status + if result == nil then + return true + else + return result + end else -- function failed - log._error("protected " .. key .. "() -> " .. result) + log._error("PPM: protected " .. key .. "() -> " .. result) return nil end end @@ -37,6 +42,10 @@ function mount_all() peri_init(pm_dev) self.mounts[ifaces[i]] = { peripheral.getType(ifaces[i]), pm_dev } end + + if #ifaces == 0 then + log._warning("PPM: mount_all() -> no devices found") + end end -- mount a particular device @@ -61,12 +70,12 @@ function mount(name) end -- handle peripheral_detach event -function unmount_handler(iface) +function handle_unmount(iface) -- what got disconnected? local lost_dev = self.mounts[iface] local type = lost_dev.type - log._warning("PMGR: lost device " .. type .. " mounted to " .. iface) + log._warning("PPM: lost device " .. type .. " mounted to " .. iface) return self.mounts[iface] end From ac4ca3e56e485b7caf0dced05ec2b7b01c55287b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 10 Mar 2022 14:23:14 -0500 Subject: [PATCH 23/63] reactor plc utilizes ppm and is now changed to use pullEventRaw --- reactor-plc/startup.lua | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 6d2d9de..01c80a8 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -2,6 +2,8 @@ -- Reactor Programmable Logic Controller -- +os.loadAPI("scada-common/log.lua") +os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/comms.lua") os.loadAPI("reactor-plc/config.lua") @@ -11,8 +13,10 @@ local R_PLC_VERSION = "alpha-v0.1" local print_ts = util.print_ts -local reactor = peripheral.find("fissionReactor") -local modem = peripheral.find("modem") +ppm.mount_all() + +local reactor = ppm.get_device("fissionReactor") +local modem = ppm.get_device("modem") print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") @@ -58,14 +62,19 @@ local control_state = false -- event loop while true do - local event, param1, param2, param3, param4, param5 = os.pullEvent() + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() if event == "peripheral_detach" then - print_ts("[fatal] lost a peripheral, stopping...\n") - -- todo: determine which disconnected and what is left - -- hopefully it wasn't the reactor - reactor.scram() - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_DC) ? + ppm.handle_unmount(param1) + + -- try to scram reactor if it is still connected + if reactor.scram() then + print_ts("[fatal] PLC lost a peripheral: successful SCRAM, now exiting...\n") + else + print_ts("[fatal] PLC lost a peripheral: failed SCRAM, now exiting...\n") + end + + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? return end @@ -95,5 +104,11 @@ while true do -- haven't heard from server recently? shutdown iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") + elseif event == "terminate" then + -- safe exit + reactor.scram() + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? + print_ts("[alert] exiting, reactor disabled\n") + return end end From 17874c46584617cf7daa25e7e2fe750e711b2240 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 14 Mar 2022 14:19:14 -0400 Subject: [PATCH 24/63] cleanup/improvements to PLC comms --- reactor-plc/plc.lua | 4 +- reactor-plc/startup.lua | 15 ++-- scada-common/comms.lua | 169 +++++++++++++++++++++------------------- scada-common/modbus.lua | 2 +- 4 files changed, 101 insertions(+), 89 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index d1fda80..decfd0b 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -18,9 +18,7 @@ function scada_link(plc_comms) os.cancelTimer(link_timeout) end - local s_packet = comms.scada_packet() - s_packet.receive(p1, p2, p3, p4, p5) - local packet = s_packet.as_rplc() + local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) if packet then -- handle response local response = plc_comms.handle_link(packet) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 01c80a8..3f1c29a 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -3,13 +3,14 @@ -- os.loadAPI("scada-common/log.lua") -os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/util.lua") +os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") + os.loadAPI("reactor-plc/config.lua") os.loadAPI("reactor-plc/plc.lua") -local R_PLC_VERSION = "alpha-v0.1" +local R_PLC_VERSION = "alpha-v0.1.0" local print_ts = util.print_ts @@ -97,7 +98,7 @@ while true do -- feed the watchdog first so it doesn't uhh,,,eat our packets conn_watchdog.feed() - local packet = comms.make_packet(p1, p2, p3, p4, p5) + local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) elseif event == "timer" and param1 == conn_watchdog.get_timer() then @@ -106,9 +107,13 @@ while true do print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then -- safe exit - reactor.scram() + if reactor.scram() then + print_ts("[alert] exiting, reactor disabled\n") + else + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? + print_ts("[alert] exiting, reactor failed to disable\n") + end -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? - print_ts("[alert] exiting, reactor disabled\n") return end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 5641cba..ec0dd9b 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,7 +1,7 @@ PROTOCOLS = { MODBUS_TCP = 0, -- our "modbus tcp"-esque protocol RPLC = 1, -- reactor plc protocol - SCADA_MGMT = 2, -- SCADA supervisor intercommunication + SCADA_MGMT = 2, -- SCADA supervisor intercommunication and device advertisements COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } @@ -37,18 +37,18 @@ function scada_packet() local self = { modem_msg_in = nil, valid = false, - seq_id = nil, + seq_num = nil, protocol = nil, length = nil, raw = nil } - local make = function (seq_id, protocol, payload) + local make = function (seq_num, protocol, payload) self.valid = true - self.seq_id = seq_id + self.seq_num = seq_num self.protocol = protocol self.length = #payload - self.raw = { self.seq_id, self.protocol, self.length, payload } + self.raw = { self.seq_num, self.protocol, self.length, payload } end local receive = function (side, sender, reply_to, message, distance) @@ -67,23 +67,18 @@ function scada_packet() return false else self.valid = true - self.seq_id = self.raw[1] + self.seq_num = self.raw[1] self.protocol = self.raw[2] self.length = self.raw[3] end end - local seq_id = function (packet) - return self.seq_id - end - - local protocol = function (packet) - return self.protocol - end - - local length = function (packet) - return self.length - end + -- basic gets + local modem_event = function (packet) return self.modem_msg_in end + local raw = function (packet) return self.raw end + local seq_num = function (packet) return self.seq_num end + local protocol = function (packet) return self.protocol end + local length = function (packet) return self.length end local data = function (packet) local subset = nil @@ -93,38 +88,15 @@ function scada_packet() return subset end - local raw = function (packet) - return self.raw - end - - local modem_event = function (packet) - return self.modem_msg_in - end - - local as_rplc = function () - local pkt = nil - if self.valid and self.protocol == PROTOCOLS.RPLC then - local body = data() - if #body > 2 then - pkt = { - id = body[1], - type = body[2], - length = #body - 2, - body = { table.unpack(body, 3, 2 + #body) } - } - end - end - return pkt - end - return { make = make, receive = receive, - seq_id = seq_id, + modem_event = modem_event, + raw = raw + seq_num = seq_num, protocol = protocol, length = length, - raw = raw, - modem_event = modem_event + data = data } end @@ -139,7 +111,7 @@ end function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_channel) local self = { mode = mode, - seq_id = 0, + seq_num = 0, num_reactors = num_reactors, modem = modem, dev_listen = dev_listen, @@ -149,11 +121,20 @@ function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_chan } end +function rtu_comms(modem, local_port, server_port) + local self = { + txn_id = 0, + modem = modem, + s_port = server_port, + l_port = local_port + } +end + -- reactor PLC communications function rplc_comms(id, modem, local_port, server_port, reactor) local self = { id = id, - seq_id = 0, + seq_num = 0, modem = modem, s_port = server_port, l_port = local_port, @@ -165,9 +146,9 @@ function rplc_comms(id, modem, local_port, server_port, reactor) local _send = function (msg) local packet = scada_packet() - packet.make(self.seq_id, PROTOCOLS.RPLC, msg) + packet.make(self.seq_num, PROTOCOLS.RPLC, msg) self.modem.transmit(self.s_port, self.l_port, packet.raw()) - self.seq_id = self.seq_id + 1 + self.seq_num = self.seq_num + 1 end -- variable reactor status information, excluding heating rate @@ -218,6 +199,59 @@ function rplc_comms(id, modem, local_port, server_port, reactor) -- PUBLIC FUNCTIONS -- + -- parse an RPLC 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) + + -- get using RPLC protocol format + if self.valid and self.protocol == PROTOCOLS.RPLC then + local body = data() + if #body > 2 then + pkt = { + id = body[1], + type = body[2], + length = #body - 2, + body = { table.unpack(body, 3, 2 + #body) } + } + end + end + + return pkt + end + + -- handle a linking packet + local handle_link = function (packet) + if packet.type == RPLC_TYPES.LINK_REQ then + return packet.data[1] == RPLC_LINKING.ALLOW + else + return nil + end + end + + -- handle an RPLC packet + local handle_packet = function (packet) + if packet.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive request received, nothing to do except feed watchdog + elseif packet.type == RPLC_TYPES.MEK_STRUCT then + -- request for physical structure + send_struct() + elseif packet.type == RPLC_TYPES.RS_IO_CONNS then + -- request for redstone connections + send_rs_io_conns() + elseif packet.type == RPLC_TYPES.RS_IO_GET then + elseif packet.type == RPLC_TYPES.RS_IO_SET then + elseif packet.type == RPLC_TYPES.MEK_SCRAM then + elseif packet.type == RPLC_TYPES.MEK_ENABLE then + elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPES.ISS_GET then + elseif packet.type == RPLC_TYPES.ISS_CLEAR then + end + end + -- attempt to establish link with supervisor local send_link_req = function () local linking_data = { @@ -229,7 +263,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor) end -- send structure properties (these should not change) - -- server will cache these + -- (server will cache these) local send_struct = function () local mek_data = { heat_cap = self.reactor.getHeatCapacity(), @@ -252,8 +286,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor) end -- send live status information - -- control_state: acknowledged control state from supervisor - -- overridden: if ISS force disabled reactor + -- control_state : acknowledged control state from supervisor + -- overridden : if ISS force disabled reactor local send_status = function (control_state, overridden) local mek_data = nil @@ -277,38 +311,13 @@ function rplc_comms(id, modem, local_port, server_port, reactor) local send_rs_io_conns = function () end - local handle_link = function (packet) - if packet.type == RPLC_TYPES.LINK_REQ then - return packet.data[1] == RPLC_LINKING.ALLOW - else - return nil - end - end - - local handle_packet = function (packet) - if packet.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive request received, nothing to do except feed watchdog - elseif packet.type == RPLC_TYPES.MEK_STRUCT then - -- request for physical structure - send_struct() - elseif packet.type == RPLC_TYPES.RS_IO_CONNS then - -- request for redstone connections - send_rs_io_conns() - elseif packet.type == RPLC_TYPES.RS_IO_GET then - elseif packet.type == RPLC_TYPES.RS_IO_SET then - elseif packet.type == RPLC_TYPES.MEK_SCRAM then - elseif packet.type == RPLC_TYPES.MEK_ENABLE then - elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then - elseif packet.type == RPLC_TYPES.ISS_GET then - elseif packet.type == RPLC_TYPES.ISS_CLEAR then - end - end - return { + parse_packet = parse_packet, + handle_link = handle_link, + handle_packet = handle_packet, send_link_req = send_link_req, send_struct = send_struct, send_status = send_status, - send_rs_io_conns = send_rs_io_conns, - handle_link = handle_link + send_rs_io_conns = send_rs_io_conns } end diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index 6721c44..d2b18f0 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -1,4 +1,4 @@ --- #REQUIRES comms.lua +-- #REQUIRES rtu.lua -- modbus function codes local MODBUS_FCODE = { From a9d4458103dc2f16b2f715613cf4732e48419757 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 15 Mar 2022 11:58:08 -0400 Subject: [PATCH 25/63] redstone I/O constants defined, digital I/O functions with active high/low mappings added --- scada-common/rsio.lua | 120 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 99 insertions(+), 21 deletions(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index ebc91f4..468a1fa 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -1,37 +1,115 @@ +IO_LVL = { + LOW = 0, + HIGH = 1 +} + RS_IO = { -- digital inputs -- -- facility - F_SCRAM, -- active high, facility-wide scram - F_AE2_LIVE, -- active high, indicates whether AE2 network is online (hint: use redstone P2P) + 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, -- active high, reactor scram - R_ENABLE, -- active high, reactor enable + R_SCRAM = 3, -- active low, reactor scram + R_ENABLE = 4, -- active high, reactor enable -- digital outputs -- -- waste - WASTE_PO, -- active low, polonium routing - WASTE_PU, -- active low, plutonium routing - WASTE_AM, -- active low, antimatter routing + WASTE_PO = 5, -- active low, polonium routing + WASTE_PU = 6, -- active low, plutonium routing + WASTE_AM = 7, -- active low, antimatter routing -- reactor - R_SCRAMMED, -- if the reactor is scrammed - R_AUTO_SCRAM, -- if the reactor was automatically scrammed - R_ACTIVE, -- if the reactor is active - R_AUTO_CTRL, -- if the reactor burn rate is automatic - R_DMG_CRIT, -- if the reactor damage is critical - R_HIGH_TEMP, -- if the reactor is at a high temperature - R_NO_COOLANT, -- if the reactor has no coolant - R_EXCESS_HC, -- if the reactor has excess heated coolant - R_EXCESS_WS, -- if the reactor has excess waste - R_INSUFF_FUEL, -- if the reactor has insufficent fuel - R_PLC_TIMEOUT, -- if the reactor PLC has not been heard from + 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, -- reactor burn rate percentage - A_B_BOIL_RATE, -- boiler boil rate percentage - A_T_FLOW_RATE -- turbine flow rate percentage + 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 } + +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 RS_DIO_MAP = { + -- F_SCRAM + _DI_ACTIVE_LOW, + -- F_AE2_LIVE + _DI_ACTIVE_HIGH, + -- R_SCRAM + _DI_ACTIVE_LOW, + -- R_ENABLE + _DI_ACTIVE_HIGH, + -- WASTE_PO + _DO_ACTIVE_LOW, + -- WASTE_PU + _DO_ACTIVE_LOW, + -- WASTE_AM + _DO_ACTIVE_LOW, + -- R_SCRAMMED + _DO_ACTIVE_HIGH, + -- R_AUTO_SCRAM + _DO_ACTIVE_HIGH, + -- R_ACTIVE + _DO_ACTIVE_HIGH, + -- R_AUTO_CTRL + _DO_ACTIVE_HIGH, + -- R_DMG_CRIT + _DO_ACTIVE_HIGH, + -- R_HIGH_TEMP + _DO_ACTIVE_HIGH, + -- R_NO_COOLANT + _DO_ACTIVE_HIGH, + -- R_EXCESS_HC + _DO_ACTIVE_HIGH, + -- R_EXCESS_WS + _DO_ACTIVE_HIGH, + -- R_INSUFF_FUEL + _DO_ACTIVE_HIGH, + -- R_PLC_TIMEOUT + _DO_ACTIVE_HIGH +} + +-- get digital IO level reading +function digital_input_read(rs_value) + if rs_value then + return IO_LVL.HIGH + else + return IO_LVL.LOW + end +end + +-- returns true if the level corresponds to active +function digital_input_is_active(channel, level) + if channel > RS_IO.R_ENABLE then + return false + else + return RS_DIO_MAP[channel](level) + end +end + +-- returns the level corresponding to active +function digital_output_write(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](level) + end +end From 6e1e4c4685f89119b1f9d3d192d67f874d8b83be Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 15 Mar 2022 11:58:22 -0400 Subject: [PATCH 26/63] ppm includes get_type function now --- scada-common/ppm.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 4b9e2f1..5e19636 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -95,6 +95,11 @@ function get_periph(iface) return self.mounts[iface].device end +-- get a mounted peripheral type by side/interface +function get_type(iface) + return self.mounts[iface].type +end + -- get a mounted peripheral by type function get_device(name) local device = nil From 5642e3283dfc35f9a63edea75be6eea9e9b64ce4 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 15 Mar 2022 11:58:52 -0400 Subject: [PATCH 27/63] fixes to modbus_packet() --- scada-common/modbus.lua | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index d2b18f0..de21a2e 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -1,5 +1,3 @@ --- #REQUIRES rtu.lua - -- modbus function codes local MODBUS_FCODE = { READ_COILS = 0x01, @@ -178,6 +176,8 @@ function modbus_init(rtu_dev) end function modbus_packet() + local MODBUS_TCP = 0 + local self = { txn_id = txn_id, protocol = protocol, @@ -187,17 +187,7 @@ function modbus_packet() data = data } - local receive = function (raw) - local size_ok = #raw ~= 6 - - if size_ok then - set(raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]) - end - - return size_ok and self.protocol == comms.PROTOCOLS.MODBUS_TCP - end - - local set = function (txn_id, protocol, length, unit_id, func_code, data) + local make = function (txn_id, protocol, length, unit_id, func_code, data) self.txn_id = txn_id self.protocol = protocol self.length = length @@ -206,6 +196,16 @@ function modbus_packet() self.data = data end + local receive = function (raw) + local size_ok = #raw ~= 6 + + if size_ok then + make(raw[1], raw[2], raw[3], raw[4], raw[5], raw[6]) + end + + return size_ok and self.protocol == MODBUS_TCP + end + local get = function () return { txn_id = self.txn_id, @@ -216,4 +216,10 @@ function modbus_packet() data = self.data } end + + return { + make = make, + receive = receive, + get = get + } end From 1e23a2fd67dbfc554dd702e8c3dc489cd473d967 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 15 Mar 2022 12:02:31 -0400 Subject: [PATCH 28/63] work on RTU startup and comms --- rtu/config.lua | 27 +++++++- rtu/startup.lua | 86 +++++++++++++++++++++++++ scada-common/comms.lua | 140 +++++++++++++++++++++++++++++++++++------ 3 files changed, 230 insertions(+), 23 deletions(-) diff --git a/rtu/config.lua b/rtu/config.lua index 39b23ad..66b2154 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -1,10 +1,31 @@ -RTU__DEVICES = { +-- #REQUIRES rsio.lua + +SCADA_SERVER = 16000 + +RTU_DEVICES = { { name = "boiler_0", - reactor_owner = 1 + index = 1, + for_reactor = 1 }, { name = "turbine_0", - reactor_owner = 1 + index = 1, + for_reactor = 1 } } + +RTU_REDSTONE = { + { + io = RS_IO.WASTE_PO, + for_reactor = 1 + }, + { + io = RS_IO.WASTE_PU, + for_reactor = 1 + }, + { + io = RS_IO.WASTE_AM, + for_reactor = 1 + }, +} diff --git a/rtu/startup.lua b/rtu/startup.lua index de9a784..dfbad8b 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -2,5 +2,91 @@ -- RTU: Remote Terminal Unit -- +os.loadAPI("scada-common/log.lua") +os.loadAPI("scada-common/util.lua") +os.loadAPI("scada-common/ppm.lua") +os.loadAPI("scada-common/modbus.lua") +os.loadAPI("scada-common/rsio.lua") + os.loadAPI("config.lua") os.loadAPI("rtu.lua") + +os.loadAPI("dev/boiler.lua") +os.loadAPI("dev/imatrix.lua") +os.loadAPI("dev/turbine.lua") + +local RTU_VERSION = "alpha-v0.1.0" + +local print_ts = util.print_ts + +-- mount connected devices +ppm.mount_all() + +-- get modem +local modem = ppm.get_device("modem") +if modem == nil then + print("No modem found, exiting...") + return +end + +-- start comms +if not modem.isOpen(config.LISTEN_PORT) then + modem.open(config.LISTEN_PORT) +end + +local rtu_comms = comms.rtu_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) + +-- determine configuration +local units = {} + +-- mounted peripherals +for i = 1, #RTU_DEVICES do + local device = ppm.get_periph(RTU_DEVICES[i].name) + + if device == nil then + local message = "'" .. RTU_DEVICES[i].name .. "' not found" + print_ts(message) + log._warning(message) + else + local type = ppm.get_type(RTU_DEVICES[i].name) + local rtu_iface = nil + local rtu_type = "" + + if type == "boiler" then + -- boiler multiblock + rtu_type = "boiler" + rtu_iface = boiler_rtu(device) + elseif type == "turbine" then + -- turbine multiblock + rtu_type = "turbine" + rtu_iface = turbine_rtu(device) + elseif type == "mekanismMachine" then + -- assumed to be an induction matrix multiblock + rtu_type = "imatrix" + rtu_iface = imatrix_rtu(device) + else + local message = "device '" .. RTU_DEVICES[i].name .. "' is not a known type (" .. type .. ")" + print_ts(message) + log._warning(message) + end + + if rtu_iface ~= nil then + table.insert(units, { + name = RTU_DEVICES[i].name, + type = rtu_type, + index = RTU_DEVICES[i].index, + reactor = RTU_DEVICES[i].for_reactor, + device = device, + rtu = rtu_iface + }) + end + end +end + +-- redstone devices +for i = 1, #RTU_REDSTONE do +end + +-- advertise units +rtu_comms.send_advertisement(units) + diff --git a/scada-common/comms.lua b/scada-common/comms.lua index ec0dd9b..6ea907b 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,13 +1,15 @@ +-- #REQUIRES modbus.lua + PROTOCOLS = { - MODBUS_TCP = 0, -- our "modbus tcp"-esque protocol - RPLC = 1, -- reactor plc protocol - SCADA_MGMT = 2, -- SCADA supervisor intercommunication and device advertisements - COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller + MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol + RPLC = 1, -- reactor PLC protocol + SCADA_MGMT = 2, -- SCADA supervisor intercommunication, device advertisements, etc + COORD_DATA = 3 -- data packets for coordinators to/from supervisory controller } SCADA_SV_MODES = { - ACTIVE = 0, - BACKUP = 1 + ACTIVE = 0, -- supervisor running as primary + BACKUP = 1 -- supervisor running as hot backup } RPLC_TYPES = { @@ -23,13 +25,26 @@ RPLC_TYPES = { MEK_BURN_RATE = 9, -- set burn rate ISS_ALARM = 10, -- ISS alarm broadcast ISS_GET = 11, -- get ISS status - ISS_CLEAR = 12 -- clear ISS trip (if in bad state, will trip immideatly) + ISS_CLEAR = 12 -- clear ISS trip (if in bad state, will trip immediately) } RPLC_LINKING = { - ALLOW = 0, - DENY = 1, - COLLISION = 2 + ALLOW = 0, -- link approved + DENY = 1, -- link denied + COLLISION = 2 -- link denied due to existing active link +} + +SCADA_MGMT_TYPES = { + PING = 0, -- generic ping + SV_HEARTBEAT = 1, -- supervisor heartbeat + RTU_HEARTBEAT = 2, -- RTU heartbeat + RTU_ADVERT = 3 -- RTU capability advertisement +} + +RTU_ADVERT_TYPES = { + BOILER = 0, -- boiler + TURBINE = 1, -- turbine + IMATRIX = 2 -- induction matrix } -- generic SCADA packet object @@ -73,14 +88,16 @@ function scada_packet() end end - -- basic gets - local modem_event = function (packet) return self.modem_msg_in end - local raw = function (packet) return self.raw end - local seq_num = function (packet) return self.seq_num end - local protocol = function (packet) return self.protocol end - local length = function (packet) return self.length end + local modem_event = function () return self.modem_msg_in end + local raw = function () return self.raw end - local data = function (packet) + local 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 () local subset = nil if self.valid then subset = { table.unpack(self.raw, 4, 3 + self.length) } @@ -92,7 +109,8 @@ function scada_packet() make = make, receive = receive, modem_event = modem_event, - raw = raw + raw = raw, + is_valid = is_valid, seq_num = seq_num, protocol = protocol, length = length, @@ -123,11 +141,92 @@ end function rtu_comms(modem, local_port, server_port) local self = { + seq_num = 0, txn_id = 0, modem = modem, s_port = server_port, l_port = local_port } + + -- PRIVATE FUNCTIONS -- + + local _send = function (protocol, msg) + local packet = scada_packet() + packet.make(self.seq_num, protocol, msg) + self.modem.transmit(self.s_port, self.l_port, packet.raw()) + self.seq_num = self.seq_num + 1 + end + + -- PUBLIC FUNCTIONS -- + + -- parse a MODBUS/SCADA 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 = modbus_packet() + m_pkt.receive(s_pkt.data()) + + pkt = { + scada_frame = s_pkt, + modbus_frame = m_pkt + } + -- get as SCADA management packet + elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + local body = s_pkt.data() + if #body > 1 then + pkt = { + scada_frame = s_pkt, + type = body[1], + length = #body - 1, + body = { table.unpack(body, 2, 1 + #body) } + } + end + end + end + + return pkt + end + + -- send capability advertisement + local send_advertisement = function (units) + local advertisement = { + type = SCADA_MGMT_TYPES.RTU_ADVERT, + units = {} + } + + 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 + end + + if type ~= nil then + table.insert(advertisement.units, { + type = type, + index = units[i].index, + reactor = units[i].for_reactor + }) + end + end + + _send(advertisement, PROTOCOLS.SCADA_MGMT) + end + + return { + parse_packet = parse_packet + } end -- reactor PLC communications @@ -208,10 +307,11 @@ function rplc_comms(id, modem, local_port, server_port, reactor) s_pkt.recieve(side, sender, reply_to, message, distance) -- get using RPLC protocol format - if self.valid and self.protocol == PROTOCOLS.RPLC then - local body = data() + if s_pkt.is_valid() and s_pkt.protocol() == PROTOCOLS.RPLC then + local body = s_pkt.data() if #body > 2 then pkt = { + scada_frame = s_pkt, id = body[1], type = body[2], length = #body - 2, From 74ae57324b43a1517312f650fb663a3a20009cec Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 23 Mar 2022 15:36:14 -0400 Subject: [PATCH 29/63] redstone I/O rework --- rtu/dev/redstone.lua | 33 +++++---- scada-common/rsio.lua | 152 +++++++++++++++++++++++++++++++++--------- 2 files changed, 141 insertions(+), 44 deletions(-) diff --git a/rtu/dev/redstone.lua b/rtu/dev/redstone.lua index 0ec1283..da2f7e2 100644 --- a/rtu/dev/redstone.lua +++ b/rtu/dev/redstone.lua @@ -1,6 +1,10 @@ -- #REQUIRES rtu.lua +-- #REQUIRES rsio.lua -- note: this RTU makes extensive use of the programming concept of closures +local digital_read = rsio.digital_read +local digital_is_active = rsio.digital_is_active + function redstone_rtu() local self = { rtu = rtu_init() @@ -10,56 +14,57 @@ function redstone_rtu() return self.rtu end - local link_di = function (side, color) + local link_di = function (channel, side, color) local f_read = nil if color then f_read = function () - return rs.testBundledInput(side, color) + return digital_read(rs.testBundledInput(side, color)) end else f_read = function () - return rs.getInput(side) + return digital_read(rs.getInput(side)) end end self.rtu.connect_di(f_read) end - local link_do = function (side, color) + local link_do = function (channel, side, color) local f_read = nil local f_write = nil if color then f_read = function () - return colors.test(rs.getBundledOutput(side), color) + return digital_read(colors.test(rs.getBundledOutput(side), color)) end - f_write = function (value) + f_write = function (level) local output = rs.getBundledOutput(side) + local active = digital_is_active(channel, level) - if value then - colors.combine(output, value) + if active then + colors.combine(output, color) else - colors.subtract(output, value) + colors.subtract(output, color) end rs.setBundledOutput(side, output) end else f_read = function () - return rs.getOutput(side) + return digital_read(rs.getOutput(side)) end - f_write = function (value) - rs.setOutput(side, color) + f_write = function (level) + rs.setOutput(side, digital_is_active(channel, level)) end end self.rtu.connect_coil(f_read, f_write) end - local link_ai = function (side) + local link_ai = function (channel, side) self.rtu.connect_input_reg( function () return rs.getAnalogInput(side) @@ -67,7 +72,7 @@ function redstone_rtu() ) end - local link_ao = function (side) + local link_ao = function (channel, side) self.rtu.connect_holding_reg( function () return rs.getAnalogOutput(side) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 468a1fa..9dbe9ef 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -3,6 +3,18 @@ IO_LVL = { HIGH = 1 } +IO_DIR = { + IN = 0, + OUT = 1 +} + +IO_MODE = { + DIGITAL_OUT = 0, + DIGITAL_IN = 1, + ANALOG_OUT = 2, + ANALOG_IN = 3 +} + RS_IO = { -- digital inputs -- @@ -41,6 +53,53 @@ RS_IO = { A_T_FLOW_RATE = 21 -- turbine flow rate percentage } +function to_string(channel) + local names = { + "F_SCRAM", + "F_AE2_LIVE", + "R_SCRAM", + "R_ENABLE", + "WASTE_PO", + "WASTE_PU", + "WASTE_AM", + "R_SCRAMMED", + "R_AUTO_SCRAM", + "R_ACTIVE", + "R_AUTO_CTRL", + "R_DMG_CRIT", + "R_HIGH_TEMP", + "R_NO_COOLANT", + "R_EXCESS_HC", + "R_EXCESS_WS", + "R_INSUFF_FUEL", + "R_PLC_TIMEOUT", + "A_R_BURN_RATE", + "A_B_BOIL_RATE", + "A_T_FLOW_RATE" + } + + if channel > 0 and channel <= #names then + return names[channel] + else + return "" + end +end + +function is_valid_channel(channel) + return channel > 0 and channel <= A_T_FLOW_RATE +end + +function is_valid_side(side) + for _, s in pairs(redstone.getSides()) do + if s == side then return true end + end + return false +end + +function is_color(color) + return (color > 0) and (bit.band(color, (color - 1)) == 0); +end + 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 @@ -48,47 +107,80 @@ 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 +-- I/O mappings to I/O function and I/O mode local RS_DIO_MAP = { -- F_SCRAM - _DI_ACTIVE_LOW, + { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, -- F_AE2_LIVE - _DI_ACTIVE_HIGH, + { _f = _DI_ACTIVE_HIGH, mode = IO_DIR.IN }, -- R_SCRAM - _DI_ACTIVE_LOW, + { _f = _DI_ACTIVE_LOW, mode = IO_DIR.IN }, -- R_ENABLE - _DI_ACTIVE_HIGH, + { _f = _DI_ACTIVE_HIGH, mode = IO_DIR.IN }, -- WASTE_PO - _DO_ACTIVE_LOW, + { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_PU - _DO_ACTIVE_LOW, + { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, -- WASTE_AM - _DO_ACTIVE_LOW, + { _f = _DO_ACTIVE_LOW, mode = IO_DIR.OUT }, -- R_SCRAMMED - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_SCRAM - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_ACTIVE - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_AUTO_CTRL - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_DMG_CRIT - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_HIGH_TEMP - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_NO_COOLANT - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_HC - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_EXCESS_WS - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_INSUFF_FUEL - _DO_ACTIVE_HIGH, + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT }, -- R_PLC_TIMEOUT - _DO_ACTIVE_HIGH + { _f = _DO_ACTIVE_HIGH, mode = IO_DIR.OUT } } +function get_io_mode(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, -- WASTE_PO + IO_MODE.DIGITAL_OUT, -- WASTE_PU + IO_MODE.DIGITAL_OUT, -- WASTE_AM + IO_MODE.DIGITAL_OUT, -- R_SCRAMMED + IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM + IO_MODE.DIGITAL_OUT, -- R_ACTIVE + IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL + IO_MODE.DIGITAL_OUT, -- R_DMG_CRIT + IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP + IO_MODE.DIGITAL_OUT, -- R_NO_COOLANT + 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 + } + + if channel > 0 and channel <= #modes then + return modes[channel] + else + return IO_MODE.ANALOG_IN + end +end + -- get digital IO level reading -function digital_input_read(rs_value) +function digital_read(rs_value) if rs_value then return IO_LVL.HIGH else @@ -96,20 +188,20 @@ function digital_input_read(rs_value) end end --- returns true if the level corresponds to active -function digital_input_is_active(channel, level) - if channel > RS_IO.R_ENABLE then - return false - else - return RS_DIO_MAP[channel](level) - end -end - -- returns the level corresponding to active -function digital_output_write(channel, active) +function digital_write(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](level) + return RS_DIO_MAP[channel]._f(level) + end +end + +-- returns true if the level corresponds to active +function digital_is_active(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 From 60674ec95c399edc660b75077c5775fb45b3f15a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 23 Mar 2022 15:41:08 -0400 Subject: [PATCH 30/63] RTU startup code and comms --- rtu/config.lua | 33 ++++++++---- rtu/startup.lua | 117 +++++++++++++++++++++++++++++++++++++---- scada-common/comms.lua | 98 ++++++++++++++++++++++++++-------- scada-common/log.lua | 37 +++++++++---- 4 files changed, 233 insertions(+), 52 deletions(-) diff --git a/rtu/config.lua b/rtu/config.lua index 66b2154..b06305e 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -17,15 +17,26 @@ RTU_DEVICES = { RTU_REDSTONE = { { - io = RS_IO.WASTE_PO, - for_reactor = 1 - }, - { - io = RS_IO.WASTE_PU, - for_reactor = 1 - }, - { - io = RS_IO.WASTE_AM, - for_reactor = 1 - }, + for_reactor = 1, + io = { + { + channel = RS_IO.WASTE_PO, + side = "top", + bundled_color = colors.blue, + for_reactor = 1 + }, + { + channel = RS_IO.WASTE_PU, + side = "top", + bundled_color = colors.cyan, + for_reactor = 1 + }, + { + channel = RS_IO.WASTE_AM, + side = "top", + bundled_color = colors.purple, + for_reactor = 1 + } + } + } } diff --git a/rtu/startup.lua b/rtu/startup.lua index dfbad8b..483aaee 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -19,6 +19,13 @@ local RTU_VERSION = "alpha-v0.1.0" local print_ts = util.print_ts +---------------------------------------- +-- startup +---------------------------------------- + +local units = {} +local linked = false + -- mount connected devices ppm.mount_all() @@ -36,8 +43,68 @@ end local rtu_comms = comms.rtu_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +---------------------------------------- -- determine configuration -local units = {} +---------------------------------------- + +-- redstone interfaces +for reactor_idx = 1, #RTU_REDSTONE do + local rs_rtu = redstone_rtu() + local io_table = RTU_REDSTONE[reactor_idx].io + + local capabilities = {} + + for i = 1, #io_table do + local valid = false + local config = io_table[i] + + -- verify configuration + if is_valid_channel(config.channel) and is_valid_side(config.side) then + if config.bundled_color then + valid = is_color(config.bundled_color) + else + valid = true + end + end + + if ~valid then + local message = "invalid redstone configuration at index " .. i + print_ts(message .. "\n") + log._warning(message) + else + -- link redstone in RTU + local mode = rsio.get_io_mode(config.channel) + if mode == rsio.IO_MODE.DIGITAL_IN then + rs_rtu.link_di(config.channel, config.side, config.bundled_color) + elseif mode == rsio.IO_MODE.DIGITAL_OUT then + rs_rtu.link_do(config.channel, config.side, config.bundled_color) + elseif mode == rsio.IO_MODE.ANALOG_IN then + rs_rtu.link_ai(config.channel, config.side) + elseif mode == rsio.IO_MODE.ANALOG_OUT then + rs_rtu.link_ao(config.channel, config.side) + else + -- should be unreachable code, we already validated channels + log._error("fell through if chain attempting to identify IO mode", true) + break + end + + table.insert(capabilities, config.channel) + + log._debug("startup> linked redstone " .. #capabilities .. ": " .. rsio.to_string(config.channel) .. " (" .. config.side .. + ") for reactor " .. RTU_REDSTONE[reactor_idx].for_reactor) + end + end + + table.insert(units, { + name = "redstone_io", + type = "redstone", + index = 1, + reactor = RTU_REDSTONE[reactor_idx].for_reactor, + device = capabilities, -- use device field for redstone channels + rtu = rs_rtu, + modbus_io = modbus_init(rs_rtu) + }) +end -- mounted peripherals for i = 1, #RTU_DEVICES do @@ -45,7 +112,7 @@ for i = 1, #RTU_DEVICES do if device == nil then local message = "'" .. RTU_DEVICES[i].name .. "' not found" - print_ts(message) + print_ts(message .. "\n") log._warning(message) else local type = ppm.get_type(RTU_DEVICES[i].name) @@ -66,7 +133,7 @@ for i = 1, #RTU_DEVICES do rtu_iface = imatrix_rtu(device) else local message = "device '" .. RTU_DEVICES[i].name .. "' is not a known type (" .. type .. ")" - print_ts(message) + print_ts(message .. "\n") log._warning(message) end @@ -77,16 +144,46 @@ for i = 1, #RTU_DEVICES do index = RTU_DEVICES[i].index, reactor = RTU_DEVICES[i].for_reactor, device = device, - rtu = rtu_iface + rtu = rtu_iface, + modbus_io = modbus_init(rtu_iface) }) + + log._debug("startup> initialized RTU unit #" .. #units .. ": " .. RTU_DEVICES[i].name .. " (" .. rtu_type .. ") [" .. + RTU_DEVICES[i].index .. "] for reactor " .. RTU_DEVICES[i].for_reactor) end end end --- redstone devices -for i = 1, #RTU_REDSTONE do +---------------------------------------- +-- main loop +---------------------------------------- + +-- advertisement/heartbeat clock (every 2 seconds) +local loop_tick = os.startTimer(2) + +-- event loop +while true do + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + + if event == "peripheral_detach" then + ppm.handle_unmount(param1) + + -- todo: handle unit change + elseif event == "timer" and param1 == loop_tick then + -- 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 packet = rtu_comms.parse_packet(p1, p2, p3, p4, p5) + rtu_comms.handle_packet(packet) + + elseif event == "terminate" then + return + end end - --- advertise units -rtu_comms.send_advertisement(units) - diff --git a/scada-common/comms.lua b/scada-common/comms.lua index 6ea907b..dfea4bf 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,15 +17,12 @@ RPLC_TYPES = { LINK_REQ = 1, -- linking requests STATUS = 2, -- reactor/system status MEK_STRUCT = 3, -- mekanism build structure - RS_IO_CONNS = 4, -- redstone I/O connections - RS_IO_SET = 5, -- set redstone outputs - RS_IO_GET = 6, -- get redstone inputs - MEK_SCRAM = 7, -- SCRAM reactor - MEK_ENABLE = 8, -- enable reactor - MEK_BURN_RATE = 9, -- set burn rate - ISS_ALARM = 10, -- ISS alarm broadcast - ISS_GET = 11, -- get ISS status - ISS_CLEAR = 12 -- clear ISS trip (if in bad state, will trip immediately) + 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_CLEAR = 9 -- clear ISS trip (if in bad state, will trip immediately) } RPLC_LINKING = { @@ -44,7 +41,8 @@ SCADA_MGMT_TYPES = { RTU_ADVERT_TYPES = { BOILER = 0, -- boiler TURBINE = 1, -- turbine - IMATRIX = 2 -- induction matrix + IMATRIX = 2, -- induction matrix + REDSTONE = 3 -- redstone I/O } -- generic SCADA packet object @@ -187,13 +185,50 @@ function rtu_comms(modem, local_port, server_port) length = #body - 1, body = { table.unpack(body, 2, 1 + #body) } } + elseif #body == 1 then + pkt = { + scada_frame = s_pkt, + type = body[1], + length = #body - 1, + body = nil + } + else + log._error("Malformed SCADA packet has no length field") end + else + log._error("Illegal packet type " .. s_pkt.protocol(), true) end end return pkt end + local handle_packet = function(packet, units) + if packet ~= nil then + local protocol = packet.scada_frame.protocol() + + if protocol == PROTOCOLS.MODBUS_TCP then + -- MODBUS instruction + if packet.modbus_frame.unit_id <= #units then + local return_code, response = units.modbus_io.handle_packet(packet.modbus_frame) + _send(response, PROTOCOLS.MODBUS_TCP) + + if not return_code then + log._warning("MODBUS operation failed") + end + else + -- unit ID out of range? + log._error("MODBUS packet requesting non-existent unit") + end + elseif protocol == PROTOCOLS.SCADA_MGMT then + -- SCADA management packet + else + -- should be unreachable assuming packet is from parse_packet() + log._error("Illegal packet type " .. protocol, true) + end + end + end + -- send capability advertisement local send_advertisement = function (units) local advertisement = { @@ -210,22 +245,47 @@ function rtu_comms(modem, local_port, server_port) 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 if type ~= nil then - table.insert(advertisement.units, { - type = type, - index = units[i].index, - reactor = units[i].for_reactor - }) + if type == RTU_ADVERT_TYPES.REDSTONE then + table.insert(advertisement.units, { + unit = i, + type = type, + index = units[i].index, + reactor = units[i].for_reactor, + rsio = units[i].device + }) + else + table.insert(advertisement.units, { + unit = i, + type = type, + index = units[i].index, + reactor = units[i].for_reactor, + rsio = nil + }) + end end end _send(advertisement, PROTOCOLS.SCADA_MGMT) end + local send_heartbeat = function () + local heartbeat = { + type = SCADA_MGMT_TYPES.RTU_HEARTBEAT + } + + _send(heartbeat, PROTOCOLS.SCADA_MGMT) + end + return { - parse_packet = parse_packet + parse_packet = parse_packet, + handle_packet = handle_packet, + send_advertisement = send_advertisement, + send_heartbeat = send_heartbeat } end @@ -408,16 +468,12 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _send(sys_status) end - local send_rs_io_conns = function () - end - return { parse_packet = parse_packet, handle_link = handle_link, handle_packet = handle_packet, send_link_req = send_link_req, send_struct = send_struct, - send_status = send_status, - send_rs_io_conns = send_rs_io_conns + send_status = send_status } end diff --git a/scada-common/log.lua b/scada-common/log.lua index 29fc688..3a56919 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -5,6 +5,8 @@ -- 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 file_handle = fs.open("/log.txt", "a") local _log = function (msg) @@ -14,6 +16,29 @@ local _log = function (msg) end function _debug(msg, trace) + if LOG_DEBUG then + local dbg_info = "" + + if trace then + local name = "" + + if debug.getinfo(2).name ~= nil then + name = ":" .. debug.getinfo(2).name .. "():" + end + + dbg_info = debug.getinfo(2).short_src .. ":" .. name .. + debug.getinfo(2).currentline .. " > " + end + + _log("[DBG] " .. dbg_info .. msg .. "\n") + end +end + +function _warning(msg) + _log("[WRN] " .. msg .. "\n") +end + +function _error(msg, trace) local dbg_info = "" if trace then @@ -27,17 +52,9 @@ function _debug(msg, trace) debug.getinfo(2).currentline .. " > " end - _log("[DBG] " .. dbg_info .. msg) -end - -function _warning(msg) - _log("[WRN] " .. msg) -end - -function _error(msg) - _log("[ERR] " .. msg) + _log("[ERR] " .. dbg_info .. msg .. "\n") end function _fatal(msg) - _log("[FTL] " .. msg) + _log("[FTL] " .. msg .. "\n") end From be73b17d46fd22d3af367dfaf684a7a68d3df029 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Wed, 23 Mar 2022 16:17:58 -0400 Subject: [PATCH 31/63] RTU linking and requesting advertisement --- rtu/startup.lua | 8 ++++++-- scada-common/comms.lua | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 483aaee..ce6adb8 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -179,11 +179,15 @@ while true do end 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) + rtu_comms.handle_packet(packet, units, link_ref) + + -- if linked, stop sending advertisements + linked = link_ref.linked elseif event == "terminate" then + print_ts("Exiting...\n") return end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index dfea4bf..bbcedde 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -34,8 +34,9 @@ RPLC_LINKING = { SCADA_MGMT_TYPES = { PING = 0, -- generic ping SV_HEARTBEAT = 1, -- supervisor heartbeat - RTU_HEARTBEAT = 2, -- RTU heartbeat - RTU_ADVERT = 3 -- RTU capability advertisement + REMOTE_LINKED = 2, -- remote device linked + RTU_ADVERT = 3, -- RTU capability advertisement + RTU_HEARTBEAT = 4, -- RTU heartbeat } RTU_ADVERT_TYPES = { @@ -203,7 +204,7 @@ function rtu_comms(modem, local_port, server_port) return pkt end - local handle_packet = function(packet, units) + local handle_packet = function(packet, units, ref) if packet ~= nil then local protocol = packet.scada_frame.protocol() @@ -222,6 +223,16 @@ function rtu_comms(modem, local_port, server_port) end elseif protocol == PROTOCOLS.SCADA_MGMT then -- SCADA management packet + if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then + -- acknowledgement + ref.linked = true + elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then + -- request for capabilities again + send_advertisement(units) + else + -- not supported + 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) From 2ee503946c625364e93857d5d6b371277198518b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 25 Mar 2022 11:50:03 -0400 Subject: [PATCH 32/63] plc cleanup, removed old code --- reactor-plc/signal-router.lua | 159 ---------------------------------- reactor-plc/startup.lua | 4 +- 2 files changed, 2 insertions(+), 161 deletions(-) delete mode 100644 reactor-plc/signal-router.lua diff --git a/reactor-plc/signal-router.lua b/reactor-plc/signal-router.lua deleted file mode 100644 index 37a7610..0000000 --- a/reactor-plc/signal-router.lua +++ /dev/null @@ -1,159 +0,0 @@ --- reactor signal router --- transmits status information and controls enable state - --- bundeled redstone key --- top: --- black (in): insufficent fuel --- brown (in): excess waste --- orange (in): overheat --- red (in): damage critical --- right: --- cyan (out): plutonium/plutonium pellet pipe --- green (out): polonium pipe --- magenta (out): polonium pellet pipe --- purple (out): antimatter pipe --- white (out): reactor enable - --- constants -REACTOR_ID = 1 -DEST_PORT = 1000 - -local state = { - id = REACTOR_ID, - run = false, - no_fuel = false, - full_waste = false, - high_temp = false, - damage_crit = false -} - -local waste_production = "antimatter" - -local listen_port = 1000 + REACTOR_ID -local modem = peripheral.wrap("left") - -print("Reactor Signal Router v1.0") -print("Configured for Reactor #" .. REACTOR_ID) - -if not modem.isOpen(listen_port) then - modem.open(listen_port) -end - --- greeting -modem.transmit(DEST_PORT, listen_port, REACTOR_ID) - --- queue event to read initial state and make sure reactor starts off -os.queueEvent("redstone") -rs.setBundledOutput("right", colors.white) -rs.setBundledOutput("right", 0) -re_eval_output = true - -local connection_timeout = os.startTimer(3) - --- event loop -while true do - local event, param1, param2, param3, param4, param5 = os.pullEvent() - - if event == "redstone" then - -- redstone state change - input = rs.getBundledInput("top") - - if state.no_fuel ~= colors.test(input, colors.black) then - state.no_fuel = colors.test(input, colors.black) - if state.no_fuel then - print("insufficient fuel") - end - end - - if state.full_waste ~= colors.test(input, colors.brown) then - state.full_waste = colors.test(input, colors.brown) - if state.full_waste then - print("waste tank full") - end - end - - if state.high_temp ~= colors.test(input, colors.orange) then - state.high_temp = colors.test(input, colors.orange) - if state.high_temp then - print("high temperature") - end - end - - if state.damage_crit ~= colors.test(input, colors.red) then - state.damage_crit = colors.test(input, colors.red) - if state.damage_crit then - print("damage critical") - end - end - elseif event == "modem_message" then - -- got data, reset timer - if connection_timeout ~= nil then - os.cancelTimer(connection_timeout) - end - connection_timeout = os.startTimer(3) - - if type(param4) == "number" and param4 == 0 then - print("[info] controller server startup detected") - modem.transmit(DEST_PORT, listen_port, REACTOR_ID) - elseif type(param4) == "number" and param4 == 1 then - -- keep-alive, do nothing, just had to reset timer - elseif type(param4) == "boolean" then - state.run = param4 - - if state.run then - print("[alert] reactor enabled") - else - print("[alert] reactor disabled") - end - - re_eval_output = true - elseif type(param4) == "string" then - if param4 == "plutonium" then - print("[alert] switching to plutonium production") - waste_production = param4 - re_eval_output = true - elseif param4 == "polonium" then - print("[alert] switching to polonium production") - waste_production = param4 - re_eval_output = true - elseif param4 == "antimatter" then - print("[alert] switching to antimatter production") - waste_production = param4 - re_eval_output = true - end - else - print("[error] got unknown packet (" .. param4 .. ")") - end - elseif event == "timer" and param1 == connection_timeout then - -- haven't heard from server in 3 seconds? shutdown - -- timer won't be restarted until next packet, so no need to do anything with it - print("[alert] server timeout, reactor disabled") - state.run = false - re_eval_output = true - end - - -- check for control state changes - if re_eval_output then - re_eval_output = false - - local run_color = 0 - if state.run then - run_color = colors.white - end - - -- values are swapped, as on disables and off enables - local waste_color - if waste_production == "plutonium" then - waste_color = colors.green - elseif waste_production == "polonium" then - waste_color = colors.cyan + colors.purple - else - -- antimatter (default) - waste_color = colors.cyan + colors.magenta - end - - rs.setBundledOutput("right", run_color + waste_color) - end - - modem.transmit(DEST_PORT, listen_port, state) -end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 3f1c29a..11a9982 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -95,14 +95,14 @@ while true do end elseif event == "modem_message" then -- got a packet - -- feed the watchdog first so it doesn't uhh,,,eat our packets + -- feed the watchdog first so it doesn't uhh...eat our packets conn_watchdog.feed() local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) elseif event == "timer" and param1 == conn_watchdog.get_timer() then - -- haven't heard from server recently? shutdown + -- haven't heard from server recently? shutdown reactor iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then From 5eaeb50000fa67567dfd7cb7cebe1f400d9ee6dd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 25 Mar 2022 12:17:46 -0400 Subject: [PATCH 33/63] broke up comms file, setup base coordinator code --- coordinator/coordinator.lua | 8 + coordinator/scada-coordinator.lua | 0 coordinator/startup.lua | 27 +++ reactor-plc/plc.lua | 191 +++++++++++++++ reactor-plc/startup.lua | 2 +- rtu/rtu.lua | 165 +++++++++++++ rtu/startup.lua | 2 +- scada-common/comms.lua | 374 ------------------------------ 8 files changed, 393 insertions(+), 376 deletions(-) create mode 100644 coordinator/coordinator.lua delete mode 100644 coordinator/scada-coordinator.lua create mode 100644 coordinator/startup.lua diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua new file mode 100644 index 0000000..96d766e --- /dev/null +++ b/coordinator/coordinator.lua @@ -0,0 +1,8 @@ +-- #REQUIRES comms.lua + +-- coordinator communications +function coord_comms() + local self = { + reactor_struct_cache = nil + } +end diff --git a/coordinator/scada-coordinator.lua b/coordinator/scada-coordinator.lua deleted file mode 100644 index e69de29..0000000 diff --git a/coordinator/startup.lua b/coordinator/startup.lua new file mode 100644 index 0000000..20be7a3 --- /dev/null +++ b/coordinator/startup.lua @@ -0,0 +1,27 @@ +-- +-- 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") + +os.loadAPI("coordinator/config.lua") +os.loadAPI("coordinator/coordinator.lua") + +local COORDINATOR_VERSION = "alpha-v0.1.0" + +local print_ts = util.print_ts + +ppm.mount_all() + +local modem = ppm.get_device("modem") + +print("| SCADA Coordinator - " .. COORDINATOR_VERSION .. " |") + +-- we need a modem +if modem == nil then + print("Please connect a modem.") + return +end diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index decfd0b..675bc70 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,3 +1,5 @@ +-- #REQUIRES comms.lua + function scada_link(plc_comms) local linked = false local link_timeout = os.startTimer(5) @@ -169,3 +171,192 @@ function iss_init(reactor) timed_out = timed_out } end + +-- reactor PLC communications +function rplc_comms(id, modem, local_port, server_port, reactor) + local self = { + id = id, + seq_num = 0, + modem = modem, + s_port = server_port, + l_port = local_port, + reactor = reactor, + status_cache = nil + } + + -- 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()) + self.seq_num = self.seq_num + 1 + end + + -- variable reactor status information, excluding heating rate + local _reactor_status = function () + 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(), + + 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() + } + end + + local _update_status_cache = function () + local status = _reactor_status() + local changed = false + + for key, value in pairs(status) do + if value ~= self.status_cache[key] then + changed = true + break + end + end + + if changed then + self.status_cache = status + end + + return changed + end + + -- PUBLIC FUNCTIONS -- + + -- parse an RPLC 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) + + -- get using RPLC protocol format + if s_pkt.is_valid() and s_pkt.protocol() == PROTOCOLS.RPLC then + local body = s_pkt.data() + if #body > 2 then + pkt = { + scada_frame = s_pkt, + id = body[1], + type = body[2], + length = #body - 2, + body = { table.unpack(body, 3, 2 + #body) } + } + end + end + + return pkt + end + + -- handle a linking packet + local handle_link = function (packet) + if packet.type == RPLC_TYPES.LINK_REQ then + return packet.data[1] == RPLC_LINKING.ALLOW + else + return nil + end + end + + -- handle an RPLC packet + local handle_packet = function (packet) + if packet.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive request received, nothing to do except feed watchdog + elseif packet.type == RPLC_TYPES.MEK_STRUCT then + -- request for physical structure + send_struct() + elseif packet.type == RPLC_TYPES.RS_IO_CONNS then + -- request for redstone connections + send_rs_io_conns() + elseif packet.type == RPLC_TYPES.RS_IO_GET then + elseif packet.type == RPLC_TYPES.RS_IO_SET then + elseif packet.type == RPLC_TYPES.MEK_SCRAM then + elseif packet.type == RPLC_TYPES.MEK_ENABLE then + elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPES.ISS_GET then + elseif packet.type == RPLC_TYPES.ISS_CLEAR then + end + end + + -- 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) + end + + -- send structure properties (these should not change) + -- (server will cache these) + local send_struct = function () + 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() + } + + local struct_packet = { + id = self.id, + type = RPLC_TYPES.MEK_STRUCT, + mek_data = mek_data + } + + _send(struct_packet) + end + + -- send live status information + -- control_state : acknowledged control state from supervisor + -- overridden : if ISS force disabled reactor + local send_status = function (control_state, overridden) + local mek_data = nil + + if _update_status_cache() then + mek_data = self.status_cache + end + + local sys_status = { + id = self.id, + type = RPLC_TYPES.STATUS, + timestamp = os.time(), + control_state = control_state, + overridden = overridden, + heating_rate = self.reactor.getHeatingRate(), + mek_data = mek_data + } + + _send(sys_status) + end + + return { + parse_packet = parse_packet, + handle_link = handle_link, + handle_packet = handle_packet, + send_link_req = send_link_req, + send_struct = send_struct, + send_status = send_status + } +end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 11a9982..6875bcb 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -42,7 +42,7 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local plc_comms = comms.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local plc_comms = plc.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) -- attempt server connection -- exit application if connection is denied diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 32600a1..e682787 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,3 +1,6 @@ +-- #REQUIRES comms.lua +-- #REQUIRES modbus.lua + function rtu_init() local self = { discrete_inputs = {}, @@ -85,3 +88,165 @@ function rtu_init() write_holding_reg = write_holding_reg } end + +function rtu_comms(modem, local_port, server_port) + local self = { + seq_num = 0, + txn_id = 0, + modem = modem, + s_port = server_port, + l_port = local_port + } + + -- PRIVATE FUNCTIONS -- + + local _send = function (protocol, msg) + local packet = scada_packet() + packet.make(self.seq_num, protocol, msg) + self.modem.transmit(self.s_port, self.l_port, packet.raw()) + self.seq_num = self.seq_num + 1 + end + + -- PUBLIC FUNCTIONS -- + + -- parse a MODBUS/SCADA 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 = modbus_packet() + m_pkt.receive(s_pkt.data()) + + pkt = { + scada_frame = s_pkt, + modbus_frame = m_pkt + } + -- get as SCADA management packet + elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then + local body = s_pkt.data() + if #body > 1 then + pkt = { + scada_frame = s_pkt, + type = body[1], + length = #body - 1, + body = { table.unpack(body, 2, 1 + #body) } + } + elseif #body == 1 then + pkt = { + scada_frame = s_pkt, + type = body[1], + length = #body - 1, + body = nil + } + else + log._error("Malformed SCADA packet has no length field") + end + else + log._error("Illegal packet type " .. s_pkt.protocol(), true) + end + end + + return pkt + end + + local handle_packet = function(packet, units, ref) + if packet ~= nil then + local protocol = packet.scada_frame.protocol() + + if protocol == PROTOCOLS.MODBUS_TCP then + -- MODBUS instruction + if packet.modbus_frame.unit_id <= #units then + local return_code, response = units.modbus_io.handle_packet(packet.modbus_frame) + _send(response, PROTOCOLS.MODBUS_TCP) + + if not return_code then + log._warning("MODBUS operation failed") + end + else + -- unit ID out of range? + log._error("MODBUS packet requesting non-existent unit") + end + elseif protocol == PROTOCOLS.SCADA_MGMT then + -- SCADA management packet + if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then + -- acknowledgement + ref.linked = true + elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then + -- request for capabilities again + send_advertisement(units) + else + -- not supported + 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) + end + end + end + + -- send capability advertisement + local send_advertisement = function (units) + local advertisement = { + type = SCADA_MGMT_TYPES.RTU_ADVERT, + units = {} + } + + 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 + + if type ~= nil then + if type == RTU_ADVERT_TYPES.REDSTONE then + table.insert(advertisement.units, { + unit = i, + type = type, + index = units[i].index, + reactor = units[i].for_reactor, + rsio = units[i].device + }) + else + table.insert(advertisement.units, { + unit = i, + type = type, + index = units[i].index, + reactor = units[i].for_reactor, + rsio = nil + }) + end + end + end + + _send(advertisement, PROTOCOLS.SCADA_MGMT) + end + + local send_heartbeat = function () + local heartbeat = { + type = SCADA_MGMT_TYPES.RTU_HEARTBEAT + } + + _send(heartbeat, PROTOCOLS.SCADA_MGMT) + end + + return { + parse_packet = parse_packet, + handle_packet = handle_packet, + send_advertisement = send_advertisement, + send_heartbeat = send_heartbeat + } +end diff --git a/rtu/startup.lua b/rtu/startup.lua index ce6adb8..b287a53 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -41,7 +41,7 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local rtu_comms = comms.rtu_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local rtu_comms = rtu.rtu_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) ---------------------------------------- -- determine configuration diff --git a/scada-common/comms.lua b/scada-common/comms.lua index bbcedde..f760352 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -1,5 +1,3 @@ --- #REQUIRES modbus.lua - PROTOCOLS = { MODBUS_TCP = 0, -- our "MODBUS TCP"-esque protocol RPLC = 1, -- reactor PLC protocol @@ -116,375 +114,3 @@ function scada_packet() data = data } end - --- coordinator communications -function coord_comms() - local self = { - reactor_struct_cache = nil - } -end - --- supervisory controller communications -function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_channel) - local self = { - mode = mode, - seq_num = 0, - num_reactors = num_reactors, - modem = modem, - dev_listen = dev_listen, - fo_channel = fo_channel, - sv_channel = sv_channel, - reactor_struct_cache = nil - } -end - -function rtu_comms(modem, local_port, server_port) - local self = { - seq_num = 0, - txn_id = 0, - modem = modem, - s_port = server_port, - l_port = local_port - } - - -- PRIVATE FUNCTIONS -- - - local _send = function (protocol, msg) - local packet = scada_packet() - packet.make(self.seq_num, protocol, msg) - self.modem.transmit(self.s_port, self.l_port, packet.raw()) - self.seq_num = self.seq_num + 1 - end - - -- PUBLIC FUNCTIONS -- - - -- parse a MODBUS/SCADA 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 = modbus_packet() - m_pkt.receive(s_pkt.data()) - - pkt = { - scada_frame = s_pkt, - modbus_frame = m_pkt - } - -- get as SCADA management packet - elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then - local body = s_pkt.data() - if #body > 1 then - pkt = { - scada_frame = s_pkt, - type = body[1], - length = #body - 1, - body = { table.unpack(body, 2, 1 + #body) } - } - elseif #body == 1 then - pkt = { - scada_frame = s_pkt, - type = body[1], - length = #body - 1, - body = nil - } - else - log._error("Malformed SCADA packet has no length field") - end - else - log._error("Illegal packet type " .. s_pkt.protocol(), true) - end - end - - return pkt - end - - local handle_packet = function(packet, units, ref) - if packet ~= nil then - local protocol = packet.scada_frame.protocol() - - if protocol == PROTOCOLS.MODBUS_TCP then - -- MODBUS instruction - if packet.modbus_frame.unit_id <= #units then - local return_code, response = units.modbus_io.handle_packet(packet.modbus_frame) - _send(response, PROTOCOLS.MODBUS_TCP) - - if not return_code then - log._warning("MODBUS operation failed") - end - else - -- unit ID out of range? - log._error("MODBUS packet requesting non-existent unit") - end - elseif protocol == PROTOCOLS.SCADA_MGMT then - -- SCADA management packet - if packet.type == SCADA_MGMT_TYPES.REMOTE_LINKED then - -- acknowledgement - ref.linked = true - elseif packet.type == SCADA_MGMT_TYPES.RTU_ADVERT then - -- request for capabilities again - send_advertisement(units) - else - -- not supported - 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) - end - end - end - - -- send capability advertisement - local send_advertisement = function (units) - local advertisement = { - type = SCADA_MGMT_TYPES.RTU_ADVERT, - units = {} - } - - 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 - - if type ~= nil then - if type == RTU_ADVERT_TYPES.REDSTONE then - table.insert(advertisement.units, { - unit = i, - type = type, - index = units[i].index, - reactor = units[i].for_reactor, - rsio = units[i].device - }) - else - table.insert(advertisement.units, { - unit = i, - type = type, - index = units[i].index, - reactor = units[i].for_reactor, - rsio = nil - }) - end - end - end - - _send(advertisement, PROTOCOLS.SCADA_MGMT) - end - - local send_heartbeat = function () - local heartbeat = { - type = SCADA_MGMT_TYPES.RTU_HEARTBEAT - } - - _send(heartbeat, PROTOCOLS.SCADA_MGMT) - end - - return { - parse_packet = parse_packet, - handle_packet = handle_packet, - send_advertisement = send_advertisement, - send_heartbeat = send_heartbeat - } -end - --- reactor PLC communications -function rplc_comms(id, modem, local_port, server_port, reactor) - local self = { - id = id, - seq_num = 0, - modem = modem, - s_port = server_port, - l_port = local_port, - reactor = reactor, - status_cache = nil - } - - -- 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()) - self.seq_num = self.seq_num + 1 - end - - -- variable reactor status information, excluding heating rate - local _reactor_status = function () - 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(), - - 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() - } - end - - local _update_status_cache = function () - local status = _reactor_status() - local changed = false - - for key, value in pairs(status) do - if value ~= self.status_cache[key] then - changed = true - break - end - end - - if changed then - self.status_cache = status - end - - return changed - end - - -- PUBLIC FUNCTIONS -- - - -- parse an RPLC 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) - - -- get using RPLC protocol format - if s_pkt.is_valid() and s_pkt.protocol() == PROTOCOLS.RPLC then - local body = s_pkt.data() - if #body > 2 then - pkt = { - scada_frame = s_pkt, - id = body[1], - type = body[2], - length = #body - 2, - body = { table.unpack(body, 3, 2 + #body) } - } - end - end - - return pkt - end - - -- handle a linking packet - local handle_link = function (packet) - if packet.type == RPLC_TYPES.LINK_REQ then - return packet.data[1] == RPLC_LINKING.ALLOW - else - return nil - end - end - - -- handle an RPLC packet - local handle_packet = function (packet) - if packet.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive request received, nothing to do except feed watchdog - elseif packet.type == RPLC_TYPES.MEK_STRUCT then - -- request for physical structure - send_struct() - elseif packet.type == RPLC_TYPES.RS_IO_CONNS then - -- request for redstone connections - send_rs_io_conns() - elseif packet.type == RPLC_TYPES.RS_IO_GET then - elseif packet.type == RPLC_TYPES.RS_IO_SET then - elseif packet.type == RPLC_TYPES.MEK_SCRAM then - elseif packet.type == RPLC_TYPES.MEK_ENABLE then - elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then - elseif packet.type == RPLC_TYPES.ISS_GET then - elseif packet.type == RPLC_TYPES.ISS_CLEAR then - end - end - - -- 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) - end - - -- send structure properties (these should not change) - -- (server will cache these) - local send_struct = function () - 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() - } - - local struct_packet = { - id = self.id, - type = RPLC_TYPES.MEK_STRUCT, - mek_data = mek_data - } - - _send(struct_packet) - end - - -- send live status information - -- control_state : acknowledged control state from supervisor - -- overridden : if ISS force disabled reactor - local send_status = function (control_state, overridden) - local mek_data = nil - - if _update_status_cache() then - mek_data = self.status_cache - end - - local sys_status = { - id = self.id, - type = RPLC_TYPES.STATUS, - timestamp = os.time(), - control_state = control_state, - overridden = overridden, - heating_rate = self.reactor.getHeatingRate(), - mek_data = mek_data - } - - _send(sys_status) - end - - return { - parse_packet = parse_packet, - handle_link = handle_link, - handle_packet = handle_packet, - send_link_req = send_link_req, - send_struct = send_struct, - send_status = send_status - } -end From 013656bc4d324aeff9b7af445e0aac2ff6335bf2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Fri, 25 Mar 2022 12:18:33 -0400 Subject: [PATCH 34/63] supervisor code moved around --- supervisor/scada-supervisor.lua | 55 --------------------------- supervisor/startup.lua | 67 ++++++++++++++++++++++++++++++++- supervisor/supervisor.lua | 15 ++++++++ 3 files changed, 80 insertions(+), 57 deletions(-) delete mode 100644 supervisor/scada-supervisor.lua create mode 100644 supervisor/supervisor.lua diff --git a/supervisor/scada-supervisor.lua b/supervisor/scada-supervisor.lua deleted file mode 100644 index 28a7e40..0000000 --- a/supervisor/scada-supervisor.lua +++ /dev/null @@ -1,55 +0,0 @@ --- --- Nuclear Generation Facility SCADA Supervisor --- - -os.loadAPI("scada-common/util.lua") -os.loadAPI("scada-common/comms.lua") -os.loadAPI("supervisor/config.lua") - -local SUPERVISOR_VERSION = "alpha-v0.1" - -local print_ts = util.print_ts - -local modem = peripheral.find("modem") - -print("| SCADA Supervisor - " .. SUPERVISOR_VERSION .. " |") - --- we need a modem -if modem == nil then - print("No modem found, exiting...") - 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 -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 = comms.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) - --- event loop -while true do - local event, param1, param2, param3, param4, param5 = os.pullEvent() - - -- handle event - if event == "timer" and param1 == loop_tick then - -- basic event tick, send keep-alives - elseif event == "modem_message" then - -- got a packet - end -end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 2ee4d40..4ff56a9 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -1,3 +1,66 @@ -- --- Multi-Reactor Controller Server & GUI --- \ No newline at end of file +-- Nuclear Generation Facility SCADA Supervisor +-- + +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("supervisor/config.lua") +os.loadAPI("supervisor/supervisor.lua") + +local SUPERVISOR_VERSION = "alpha-v0.1.0" + +local print_ts = util.print_ts + +ppm.mount_all() + +local modem = ppm.get_device("modem") + +print("| SCADA Supervisor - " .. SUPERVISOR_VERSION .. " |") + +-- we need a modem +if modem == nil then + print("Please connect a modem.") + 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 +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) + +-- event loop +while true do + local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + + -- handle event + if event == "timer" and param1 == loop_tick then + -- basic event tick, send keep-alives + elseif event == "modem_message" then + -- got a packet + elseif event == "terminate" then + -- safe exit + print_ts("[alert] terminated\n") + -- todo: attempt failover, alert hot backup + return + end +end diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua new file mode 100644 index 0000000..5f988a8 --- /dev/null +++ b/supervisor/supervisor.lua @@ -0,0 +1,15 @@ +-- #REQUIRES comms.lua + +-- supervisory controller communications +function superv_comms(mode, num_reactors, modem, dev_listen, fo_channel, sv_channel) + local self = { + mode = mode, + seq_num = 0, + num_reactors = num_reactors, + modem = modem, + dev_listen = dev_listen, + fo_channel = fo_channel, + sv_channel = sv_channel, + reactor_struct_cache = nil + } +end From 36fb4587a15d81ffc87ad55a4c8b5fd3654d6c56 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 2 Apr 2022 08:28:43 -0400 Subject: [PATCH 35/63] consistent packet constructors/receiving --- reactor-plc/plc.lua | 98 ++++++++++++++++++++++++++++++++++++----- rtu/rtu.lua | 38 +++++----------- scada-common/comms.lua | 64 +++++++++++++++++++++++++++ scada-common/modbus.lua | 29 ++++++++---- 4 files changed, 182 insertions(+), 47 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 675bc70..8212875 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -172,6 +172,78 @@ 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 rplc_comms(id, modem, local_port, server_port, reactor) local self = { @@ -249,17 +321,21 @@ function rplc_comms(id, modem, local_port, server_port, reactor) -- parse packet as generic SCADA packet s_pkt.recieve(side, sender, reply_to, message, distance) - -- get using RPLC protocol format - if s_pkt.is_valid() and s_pkt.protocol() == PROTOCOLS.RPLC then - local body = s_pkt.data() - if #body > 2 then - pkt = { - scada_frame = s_pkt, - id = body[1], - type = body[2], - length = #body - 2, - body = { table.unpack(body, 3, 2 + #body) } - } + if s_pkt.is_valid() then + -- get as RPLC packet + if s_pkt.protocol() == PROTOCOLS.RPLC then + local rplc_pkt = 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() + if mgmt_pkt.decode(s_pkt) then + pkt = mgmt_packet.get() + end + else + log._error("illegal packet type " .. s_pkt.protocol(), true) end end diff --git a/rtu/rtu.lua b/rtu/rtu.lua index e682787..a80e3f1 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -121,34 +121,17 @@ function rtu_comms(modem, local_port, server_port) -- get as MODBUS TCP packet if s_pkt.protocol() == PROTOCOLS.MODBUS_TCP then local m_pkt = modbus_packet() - m_pkt.receive(s_pkt.data()) - - pkt = { - scada_frame = s_pkt, - modbus_frame = m_pkt - } + if m_pkt.decode(s_pkt) then + pkt = m_pkt.get() + end -- get as SCADA management packet elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then - local body = s_pkt.data() - if #body > 1 then - pkt = { - scada_frame = s_pkt, - type = body[1], - length = #body - 1, - body = { table.unpack(body, 2, 1 + #body) } - } - elseif #body == 1 then - pkt = { - scada_frame = s_pkt, - type = body[1], - length = #body - 1, - body = nil - } - else - log._error("Malformed SCADA packet has no length field") + local mgmt_pkt = mgmt_packet() + if mgmt_pkt.decode(s_pkt) then + pkt = mgmt_packet.get() end else - log._error("Illegal packet type " .. s_pkt.protocol(), true) + log._error("illegal packet type " .. s_pkt.protocol(), true) end end @@ -161,8 +144,9 @@ function rtu_comms(modem, local_port, server_port) if protocol == PROTOCOLS.MODBUS_TCP then -- MODBUS instruction - if packet.modbus_frame.unit_id <= #units then - local return_code, response = units.modbus_io.handle_packet(packet.modbus_frame) + if packet.unit_id <= #units then + local unit = units[packet.unit_id] + local return_code, response = unit.modbus_io.handle_packet(packet) _send(response, PROTOCOLS.MODBUS_TCP) if not return_code then @@ -186,7 +170,7 @@ function rtu_comms(modem, local_port, server_port) 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 diff --git a/scada-common/comms.lua b/scada-common/comms.lua index f760352..ac0c3c5 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -114,3 +114,67 @@ function scada_packet() data = data } end + +function mgmt_packet() + local self = { + frame = nil, + type = nil, + length = nil, + data = nil + } + + 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.REMOTE_LINKED or + self.type == SCADA_MGMT_TYPES.RTU_ADVERT or + self.type == SCADA_MGMT_TYPES.RTU_HEARTBEAT + end + + -- make a SCADA management packet + local make = function (packet_type, length, data) + self.type = packet_type + self.length = length + self.data = data + end + + -- decode a SCADA management packet from a SCADA frame + local decode = function (frame) + if frame then + self.frame = frame + + if frame.protocol() == comms.PROTOCOLS.SCADA_MGMT then + local data = frame.data() + local ok = #data > 1 + + if ok then + make(data[1], data[2], { table.unpack(data, 3, #data) }) + ok = _scada_type_valid() + end + + return ok + else + log._debug("attempted SCADA_MGMT 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/scada-common/modbus.lua b/scada-common/modbus.lua index de21a2e..9d2899d 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -176,9 +176,8 @@ function modbus_init(rtu_dev) end function modbus_packet() - local MODBUS_TCP = 0 - local self = { + frame = nil, txn_id = txn_id, protocol = protocol, length = length, @@ -187,6 +186,7 @@ function modbus_packet() 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 @@ -196,18 +196,29 @@ function modbus_packet() self.data = data end - local receive = function (raw) - local size_ok = #raw ~= 6 + -- 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(raw[1], raw[2], raw[3], raw[4], raw[5], raw[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 - - return size_ok and self.protocol == MODBUS_TCP end + -- get this packet local get = function () return { + scada_frame = self.frame, txn_id = self.txn_id, protocol = self.protocol, length = self.length, @@ -219,7 +230,7 @@ function modbus_packet() return { make = make, - receive = receive, + decode = decode, get = get } end From a77946ce2ceb5a4a7c9ebb7ded831677123f8daf Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 2 Apr 2022 11:22:44 -0400 Subject: [PATCH 36/63] #1 PLC does not shut down if failed link, repeatedly tries to maintain link as part of main loop --- reactor-plc/plc.lua | 134 ++++++++++++++++++---------------------- reactor-plc/startup.lua | 45 ++++++++------ 2 files changed, 87 insertions(+), 92 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 8212875..bbe2021 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,53 +1,5 @@ -- #REQUIRES comms.lua -function scada_link(plc_comms) - local linked = false - local link_timeout = os.startTimer(5) - - plc_comms.send_link_req() - print_ts("sent link request") - - repeat - local event, p1, p2, p3, p4, p5 = os.pullEvent() - - -- handle event - if event == "timer" and param1 == link_timeout then - -- no response yet - print("...no response"); - elseif event == "modem_message" then - -- server response? cancel timeout - if link_timeout ~= nil then - os.cancelTimer(link_timeout) - end - - local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) - if packet then - -- handle response - local response = plc_comms.handle_link(packet) - if response == nil then - print_ts("invalid link response, bad channel?\n") - break - elseif response == comms.RPLC_LINKING.COLLISION then - print_ts("...reactor PLC ID collision (check config), exiting...\n") - break - elseif response == comms.RPLC_LINKING.ALLOW then - print_ts("...linked!\n") - linked = true - plc_comms.send_rs_io_conns() - plc_comms.send_struct() - plc_comms.send_status() - print_ts("sent initial data\n") - else - print_ts("...denied, exiting...\n") - break - end - end - end - until linked - - return linked -end - -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted -- autonomous from main control @@ -253,7 +205,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor) s_port = server_port, l_port = local_port, reactor = reactor, - status_cache = nil + status_cache = nil, + linked = false } -- PRIVATE FUNCTIONS -- @@ -342,32 +295,60 @@ function rplc_comms(id, modem, local_port, server_port, reactor) return pkt end - -- handle a linking packet - local handle_link = function (packet) - if packet.type == RPLC_TYPES.LINK_REQ then - return packet.data[1] == RPLC_LINKING.ALLOW - else - return nil - end - end - -- handle an RPLC packet local handle_packet = function (packet) - if packet.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive request received, nothing to do except feed watchdog - elseif packet.type == RPLC_TYPES.MEK_STRUCT then - -- request for physical structure - send_struct() - elseif packet.type == RPLC_TYPES.RS_IO_CONNS then - -- request for redstone connections - send_rs_io_conns() - elseif packet.type == RPLC_TYPES.RS_IO_GET then - elseif packet.type == RPLC_TYPES.RS_IO_SET then - elseif packet.type == RPLC_TYPES.MEK_SCRAM then - elseif packet.type == RPLC_TYPES.MEK_ENABLE then - elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then - elseif packet.type == RPLC_TYPES.ISS_GET then - elseif packet.type == RPLC_TYPES.ISS_CLEAR then + if packet ~= nil then + if packet.scada_frame.protocol() == PROTOCOLS.RPLC then + if self.linked then + if packet.type == RPLC_TYPES.KEEP_ALIVE then + -- keep alive request received, nothing to do except feed watchdog + elseif packet.type == RPLC_TYPES.LINK_REQ then + -- link request confirmation + log._debug("received link request response after already being linked") + elseif packet.type == RPLC_TYPES.MEK_STRUCT then + -- request for physical structure + send_struct() + elseif packet.type == RPLC_TYPES.RS_IO_CONNS then + -- request for redstone connections + send_rs_io_conns() + elseif packet.type == RPLC_TYPES.RS_IO_GET then + elseif packet.type == RPLC_TYPES.RS_IO_SET then + elseif packet.type == RPLC_TYPES.MEK_SCRAM then + elseif packet.type == RPLC_TYPES.MEK_ENABLE then + elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then + elseif packet.type == RPLC_TYPES.ISS_GET then + elseif packet.type == RPLC_TYPES.ISS_CLEAR then + 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 + print_ts("...linked!\n") + log._debug("rplc link request approved") + + plc_comms.send_rs_io_conns() + plc_comms.send_struct() + plc_comms.send_status() + + log._debug("sent initial status data") + elseif link_ack == RPLC_LINKING.DENY then + print_ts("...denied, retrying...\n") + 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") + else + print_ts("invalid link response, bad channel? retrying...\n") + log._error("unknown rplc link request response") + end + + self.linked = link_ack == RPLC_LINKING.ALLOW + else + log._("discarding non-link packet before linked") + end + elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + end end end @@ -427,12 +408,17 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _send(sys_status) end + local linked = function () return self.linked end + local unlink = function () self.linked = false end + return { parse_packet = parse_packet, handle_link = handle_link, handle_packet = handle_packet, send_link_req = send_link_req, send_struct = send_struct, - send_status = send_status + send_status = send_status, + linked = linked, + unlink = unlink } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 6875bcb..0554d26 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,13 +14,14 @@ local R_PLC_VERSION = "alpha-v0.1.0" local print_ts = util.print_ts +print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") + +-- mount connected devices ppm.mount_all() local reactor = ppm.get_device("fissionReactor") local modem = ppm.get_device("modem") -print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") - -- we need a reactor and a modem if reactor == nil then print("Fission reactor not found, exiting..."); @@ -44,19 +45,19 @@ end local plc_comms = plc.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) --- attempt server connection --- exit application if connection is denied -if ~plc.scada_link(plc_comms) then - return -end - -- comms watchdog, 3 second timeout local conn_watchdog = watchdog.new_watchdog(3) -- loop clock (10Hz, 2 ticks) --- send status updates at 4Hz (every 5 ticks) local loop_tick = os.startTimer(0.05) -local ticks_to_update = 5 + +-- 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 + +-- start by linking +local ticks_to_update = LINK_TICKS -- runtime variables local control_state = false @@ -70,13 +71,12 @@ while true do -- try to scram reactor if it is still connected if reactor.scram() then - print_ts("[fatal] PLC lost a peripheral: successful SCRAM, now exiting...\n") + print_ts("[fatal] PLC lost a peripheral: successful SCRAM\n") else - print_ts("[fatal] PLC lost a peripheral: failed SCRAM, now exiting...\n") + print_ts("[fatal] PLC lost a peripheral: failed SCRAM\n") end -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? - return end -- check safety (SCRAM occurs if tripped) @@ -87,11 +87,20 @@ while true do -- handle event if event == "timer" and param1 == loop_tick then - -- basic event tick, send updated data if it is time (4Hz) + -- basic event tick, send updated data if it is time (~3.33Hz) + -- iss was already checked (main reason for this tick rate) ticks_to_update = ticks_to_update - 1 - if ticks_to_update == 0 then - plc_comms.send_status(control_state, iss_tripped) - ticks_to_update = 5 + + if plc_comms.linked() then + if ticks_to_update <= 0 then + plc_comms.send_status(control_state, iss_tripped) + 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 elseif event == "modem_message" then -- got a packet @@ -100,9 +109,9 @@ while true do local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) - elseif event == "timer" and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor + plc_comms.unlink() iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then From 7c2d89e70ffc5fd9fc5ac234f1cad5064b389ba3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 2 Apr 2022 11:45:43 -0400 Subject: [PATCH 37/63] allow suppressing of PPM errors --- scada-common/ppm.lua | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 5e19636..6f33117 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -5,7 +5,8 @@ -- local self = { - mounts = {} + mounts = {}, + mute = false } -- wrap peripheral calls with lua protected call @@ -24,13 +25,25 @@ local peri_init = function (device) end else -- function failed - log._error("PPM: protected " .. key .. "() -> " .. result) + if not mute then + log._error("PPM: protected " .. key .. "() -> " .. result) + end return nil end end end end +-- silence error prints +function disable_reporting() + self.mute = true +end + +-- allow error prints +function enable_reporting() + self.mute = false +end + -- mount all available peripherals (clears mounts first) function mount_all() local ifaces = peripheral.getNames() From ed997d53e18192f620747b82b9a2e70228e1ed60 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 2 Apr 2022 11:46:14 -0400 Subject: [PATCH 38/63] #6 PLC retry SCRAM until reactor confirms unpowered --- reactor-plc/plc.lua | 11 +++++++++-- reactor-plc/startup.lua | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index bbe2021..8aaa9b6 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -206,6 +206,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor) l_port = local_port, reactor = reactor, status_cache = nil, + scrammed = false, linked = false } @@ -314,7 +315,11 @@ function rplc_comms(id, modem, local_port, server_port, reactor) elseif packet.type == RPLC_TYPES.RS_IO_GET then elseif packet.type == RPLC_TYPES.RS_IO_SET then elseif packet.type == RPLC_TYPES.MEK_SCRAM then + self.scrammed = true + self.reactor.scram() elseif packet.type == RPLC_TYPES.MEK_ENABLE then + self.scrammed = false + self.reactor.activate() elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then elseif packet.type == RPLC_TYPES.ISS_GET then elseif packet.type == RPLC_TYPES.ISS_CLEAR then @@ -408,7 +413,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _send(sys_status) end - local linked = function () return self.linked end + local is_scrammed = function () return self.scrammed end + local is_linked = function () return self.linked end local unlink = function () self.linked = false end return { @@ -418,7 +424,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor) send_link_req = send_link_req, send_struct = send_struct, send_status = send_status, - linked = linked, + is_scrammed = is_scrammed, + is_linked = is_linked, unlink = unlink } end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 0554d26..8122557 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -61,15 +61,26 @@ local ticks_to_update = LINK_TICKS -- runtime variables local control_state = false +local reactor_scram = true -- treated as latching e-stop -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() + -- 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 reactor_scram and reactor.isPowered() then + reactor.scram() + end + ppm.enable_reporting() + if event == "peripheral_detach" then ppm.handle_unmount(param1) -- try to scram reactor if it is still connected + reactor_scram = true if reactor.scram() then print_ts("[fatal] PLC lost a peripheral: successful SCRAM\n") else @@ -81,6 +92,7 @@ while true do -- check safety (SCRAM occurs if tripped) local iss_status, iss_tripped, iss_first = iss.check() + reactor_scram = reactor_scram or iss_tripped if iss_first then plc_comms.send_iss_alarm(iss_status) end @@ -91,7 +103,7 @@ while true do -- iss was already checked (main reason for this tick rate) ticks_to_update = ticks_to_update - 1 - if plc_comms.linked() then + if plc_comms.is_linked() then if ticks_to_update <= 0 then plc_comms.send_status(control_state, iss_tripped) ticks_to_update = UPDATE_TICKS @@ -109,13 +121,16 @@ while true do local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) + reactor_scram = reactor_scram or plc_comms.is_scrammed() elseif event == "timer" and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor + reactor_scram = true plc_comms.unlink() iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then -- safe exit + reactor_scram = true if reactor.scram() then print_ts("[alert] exiting, reactor disabled\n") else From 34fc625602bce0e45b63b8f18a075d7a55f54376 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sat, 2 Apr 2022 14:43:36 -0400 Subject: [PATCH 39/63] #5 finished implementing PLC packet handler, bugfixes --- reactor-plc/plc.lua | 167 ++++++++++++++++++++++++++++++---------- reactor-plc/startup.lua | 4 +- 2 files changed, 130 insertions(+), 41 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 8aaa9b6..5a5ce4a 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -197,7 +197,7 @@ function rplc_packet() end -- reactor PLC communications -function rplc_comms(id, modem, local_port, server_port, reactor) +function rplc_comms(id, modem, local_port, server_port, reactor, iss) local self = { id = id, seq_num = 0, @@ -205,6 +205,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor) s_port = server_port, l_port = local_port, reactor = reactor, + iss = iss, status_cache = nil, scrammed = false, linked = false @@ -265,6 +266,61 @@ function rplc_comms(id, modem, local_port, server_port, reactor) return changed 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) + end + + -- general ack + local _send_ack = function (type, succeeded) + local ack_data = { + id = self.id, + type = type, + ack = succeeded + } + + _send(ack_data) + end + + -- send structure properties (these should not change) + -- (server will cache these) + local _send_struct = function () + 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() + } + + local struct_packet = { + id = self.id, + type = RPLC_TYPES.MEK_STRUCT, + mek_data = mek_data + } + + _send(struct_packet) + end + + local _send_iss_status = function () + local iss_status = { + id = self.id, + type = RPLC_TYPES.ISS_GET, + status = iss.status() + } + + _send(iss_status) + end + -- PUBLIC FUNCTIONS -- -- parse an RPLC packet @@ -302,27 +358,74 @@ function rplc_comms(id, modem, local_port, server_port, reactor) if packet.scada_frame.protocol() == PROTOCOLS.RPLC then if self.linked then if packet.type == RPLC_TYPES.KEEP_ALIVE then - -- keep alive request received, nothing to do except feed watchdog + -- keep alive request received, echo back + local timestamp = packet.data[1] + local trip_time = os.time() - ts + + if trip_time < 0 then + log._warning("PLC KEEP_ALIVE trip time less than 0 (" .. trip_time .. ")") + elseif trip_time > 1 then + log._warning("PLC KEEP_ALIVE trip time > 1s (" .. trip_time .. ")") + end + + _send_keep_alive_ack() elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation - log._debug("received link request response after already being linked") + log._debug("received unsolicited link request response") + + local link_ack = packet.data[1] + + if link_ack == RPLC_LINKING.ALLOW then + _send_struct() + 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 + 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 + print_ts("received unsolicited link collision, unlinking\n") + log._warning("unsolicited rplc link request collision") + else + print_ts("invalid unsolicited link response\n") + log._error("unsolicited unknown rplc link request response") + end + + self.linked = link_ack == RPLC_LINKING.ALLOW elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure - send_struct() - elseif packet.type == RPLC_TYPES.RS_IO_CONNS then - -- request for redstone connections - send_rs_io_conns() - elseif packet.type == RPLC_TYPES.RS_IO_GET then - elseif packet.type == RPLC_TYPES.RS_IO_SET then + _send_struct() elseif packet.type == RPLC_TYPES.MEK_SCRAM then + -- disable the reactor self.scrammed = true - self.reactor.scram() + _send_ack(packet.type, self.reactor.scram()) elseif packet.type == RPLC_TYPES.MEK_ENABLE then + -- enable the reactor self.scrammed = false - self.reactor.activate() + _send_ack(packet.type, self.reactor.activate()) 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 + + if max_burn_rate is not nil then + if burn_rate > 0 and burn_rate <= max_burn_rate then + success = self.reactor.setBurnRate(burn_rate) + end + end + + _send_ack(packet.type, success) 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() + _send_ack(packet.type, true) + else + log._warning("received unknown RPLC packet type " .. packet.type) end elseif packet.type == RPLC_TYPES.LINK_REQ then -- link request confirmation @@ -332,9 +435,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor) print_ts("...linked!\n") log._debug("rplc link request approved") - plc_comms.send_rs_io_conns() - plc_comms.send_struct() - plc_comms.send_status() + _send_struct() + send_status() log._debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then @@ -367,29 +469,6 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _send(linking_data) end - -- send structure properties (these should not change) - -- (server will cache these) - local send_struct = function () - 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() - } - - local struct_packet = { - id = self.id, - type = RPLC_TYPES.MEK_STRUCT, - mek_data = mek_data - } - - _send(struct_packet) - end - -- send live status information -- control_state : acknowledged control state from supervisor -- overridden : if ISS force disabled reactor @@ -413,17 +492,27 @@ function rplc_comms(id, modem, local_port, server_port, reactor) _send(sys_status) end + local send_iss_alarm = function (cause) + local iss_alarm = { + id = self.id, + type = RPLC_TYPES.ISS_ALARM, + cause = cause, + status = iss.status() + } + + _send(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 return { parse_packet = parse_packet, - handle_link = handle_link, handle_packet = handle_packet, send_link_req = send_link_req, - send_struct = send_struct, send_status = send_status, + send_iss_alarm = send_iss_alarm, is_scrammed = is_scrammed, is_linked = is_linked, unlink = unlink diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 8122557..a85d11f 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -43,7 +43,7 @@ if not modem.isOpen(config.LISTEN_PORT) then modem.open(config.LISTEN_PORT) end -local plc_comms = plc.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local plc_comms = plc.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) -- comms watchdog, 3 second timeout local conn_watchdog = watchdog.new_watchdog(3) @@ -91,7 +91,7 @@ while true do end -- check safety (SCRAM occurs if tripped) - local iss_status, iss_tripped, iss_first = iss.check() + local iss_tripped, iss_status, iss_first = iss.check() reactor_scram = reactor_scram or iss_tripped if iss_first then plc_comms.send_iss_alarm(iss_status) From 02763c9cb312c0104446e8605b55cbb004addb71 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 3 Apr 2022 12:08:22 -0400 Subject: [PATCH 40/63] #4 PLC peripheral disconnect handling and small bugfixes/cleanup --- reactor-plc/plc.lua | 31 ++++++- reactor-plc/startup.lua | 193 ++++++++++++++++++++++++++++------------ scada-common/log.lua | 4 + scada-common/ppm.lua | 2 +- 4 files changed, 170 insertions(+), 60 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5a5ce4a..9a168bb 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -11,6 +11,10 @@ function iss_init(reactor) trip_cause = "" } + local reconnect_reactor = function (reactor) + self.reactor = reactor + end + local check = function () local status = "ok" local was_tripped = self.tripped @@ -110,6 +114,7 @@ function iss_init(reactor) end return { + reconnect_reactor = reconnect_reactor, check = check, trip_timeout = trip_timeout, reset = reset, @@ -197,7 +202,7 @@ function rplc_packet() end -- reactor PLC communications -function rplc_comms(id, modem, local_port, server_port, reactor, iss) +function comms_init(id, modem, local_port, server_port, reactor, iss) local self = { id = id, seq_num = 0, @@ -211,6 +216,11 @@ function rplc_comms(id, modem, local_port, server_port, reactor, iss) linked = false } + -- open modem + if not self.modem.isOpen(self.l_port) then + self.modem.open(self.l_port) + end + -- PRIVATE FUNCTIONS -- local _send = function (msg) @@ -323,6 +333,21 @@ function rplc_comms(id, modem, local_port, server_port, reactor, iss) -- 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 + + -- reconnect a newly connected reactor + local reconnect_reactor = function (reactor) + self.reactor = reactor + end + -- parse an RPLC packet local parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil @@ -410,7 +435,7 @@ function rplc_comms(id, modem, local_port, server_port, reactor, iss) local max_burn_rate = self.reactor.getMaxBurnRate() local success = false - if max_burn_rate is not nil then + if max_burn_rate ~= nil then if burn_rate > 0 and burn_rate <= max_burn_rate then success = self.reactor.setBurnRate(burn_rate) end @@ -508,6 +533,8 @@ function rplc_comms(id, modem, local_port, server_port, reactor, iss) local unlink = function () self.linked = false end return { + reconnect_modem = reconnect_modem, + reconnect_reactor = reconnect_reactor, parse_packet = parse_packet, handle_packet = handle_packet, send_link_req = send_link_req, diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index a85d11f..29522e1 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -22,35 +22,50 @@ ppm.mount_all() local reactor = ppm.get_device("fissionReactor") local modem = ppm.get_device("modem") +local init_ok = true +local control_degraded = { degraded = false, no_reactor = false, no_modem = false } + -- we need a reactor and a modem if reactor == nil then - print("Fission reactor not found, exiting..."); - return -elseif modem == nil then - print("No modem found, disabling reactor and exiting...") + print_ts("Fission reactor not found. Running in a degraded state...\n"); + log._warning("no reactor on startup") + init_ok = false + control_degraded.degraded = true + control_degraded.no_reactor = true +end +if modem == nil then + print_ts("No modem found. Disabling reactor and running in a degraded state...\n") + log._warning("no modem on startup") reactor.scram() - return + init_ok = false + control_degraded.degraded = true + control_degraded.no_modem = true end --- just booting up, no fission allowed (neutrons stay put thanks) -reactor.scram() +::init:: +if init_ok then + -- just booting up, no fission allowed (neutrons stay put thanks) + reactor.scram() --- init internal safety system -local iss = plc.iss_init(reactor) + -- init internal safety system + local iss = plc.iss_init(reactor) + log._debug("iss init") --- start comms -if not modem.isOpen(config.LISTEN_PORT) then - modem.open(config.LISTEN_PORT) + -- start comms + local plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) + log._debug("comms init") + + -- comms watchdog, 3 second timeout + local conn_watchdog = watchdog.new_watchdog(3) + log._debug("conn watchdog started") + + -- loop clock (10Hz, 2 ticks) + local loop_tick = os.startTimer(0.05) + log._debug("loop clock started") +else + log._warning("booted in a degraded state, awaiting peripheral connections...") end -local plc_comms = plc.rplc_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) - --- comms watchdog, 3 second timeout -local conn_watchdog = watchdog.new_watchdog(3) - --- loop clock (10Hz, 2 ticks) -local loop_tick = os.startTimer(0.05) - -- 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 @@ -67,51 +82,112 @@ local reactor_scram = true -- treated as latching e-stop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() - -- 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 reactor_scram and reactor.isPowered() then - reactor.scram() + if 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 reactor_scram and reactor.isPowered() then + reactor.scram() + end + ppm.enable_reporting() end - ppm.enable_reporting() + -- check for peripheral changes before ISS checks if event == "peripheral_detach" then - ppm.handle_unmount(param1) + local device = ppm.handle_unmount(param1) - -- try to scram reactor if it is still connected - reactor_scram = true - if reactor.scram() then - print_ts("[fatal] PLC lost a peripheral: successful SCRAM\n") - else - print_ts("[fatal] PLC lost a peripheral: failed SCRAM\n") + if device.type == "fissionReactor" then + print_ts("reactor disconnected!\n") + log._error("reactor disconnected!") + control_degraded.no_reactor = true + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? + elseif device.type == "modem" then + print_ts("modem disconnected!\n") + log._error("modem disconnected!") + control_degraded.no_modem = true + + if init_ok then + -- try to scram reactor if it is still connected + reactor_scram = true + if reactor.scram() then + print_ts("successful reactor SCRAM\n") + else + print_ts("failed reactor SCRAM\n") + end + end + + control_degraded.degraded = true + end + elseif event == "peripheral" then + local device = ppm.mount(param1) + + if device.type == "fissionReactor" then + -- reconnected reactor + reactor_scram = true + device.scram() + + print_ts("reactor reconnected.\n") + log._info("reactor reconnected.") + control_degraded.no_reactor = false + + if init_ok then + iss.reconnect_reactor(device) + plc_comms.reconnect_reactor(device) + end + + -- determine if we are still in a degraded state + if get_device("modem") not nil then + control_degraded.degraded = false + end + elseif device.type == "modem" then + -- reconnected modem + if init_ok then + plc_comms.reconnect_modem(device) + end + + print_ts("modem reconnected.\n") + log._info("modem reconnected.") + control_degraded.no_modem = false + + -- determine if we are still in a degraded state + if ppm.get_device("fissionReactor") not nil then + control_degraded.degraded = false + end end - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? + if not init_ok and not control_degraded.degraded then + init_ok = false + goto init + end end -- check safety (SCRAM occurs if tripped) - local iss_tripped, iss_status, iss_first = iss.check() - reactor_scram = reactor_scram or iss_tripped - if iss_first then - plc_comms.send_iss_alarm(iss_status) + if not control_degraded.degraded then + local iss_tripped, iss_status, iss_first = iss.check() + reactor_scram = reactor_scram or iss_tripped + if iss_first then + plc_comms.send_iss_alarm(iss_status) + end end -- handle event if event == "timer" and param1 == loop_tick then - -- basic event tick, send updated data if it is time (~3.33Hz) - -- iss was already checked (main reason for this tick rate) - ticks_to_update = ticks_to_update - 1 + if not control_degraded.no_modem then + -- basic event tick, send updated data if it is time (~3.33Hz) + -- iss was already checked (main reason for this tick rate) + ticks_to_update = ticks_to_update - 1 - if plc_comms.is_linked() then - if ticks_to_update <= 0 then - plc_comms.send_status(control_state, iss_tripped) - ticks_to_update = UPDATE_TICKS - end - else - if ticks_to_update <= 0 then - plc_comms.send_link_req() - ticks_to_update = LINK_TICKS + if plc_comms.is_linked() then + if ticks_to_update <= 0 then + plc_comms.send_status(control_state, iss_tripped) + 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 elseif event == "modem_message" then @@ -130,14 +206,17 @@ while true do print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then -- safe exit - reactor_scram = true - if reactor.scram() then - print_ts("[alert] exiting, reactor disabled\n") - else - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? - print_ts("[alert] exiting, reactor failed to disable\n") + if init_ok then + reactor_scram = true + if reactor.scram() then + print_ts("[alert] exiting, reactor disabled\n") + else + -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? + print_ts("[alert] exiting, reactor failed to disable\n") + end end -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? + print_ts("[alert] exited") return end end diff --git a/scada-common/log.lua b/scada-common/log.lua index 3a56919..79296c6 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -34,6 +34,10 @@ function _debug(msg, trace) end end +function _info(msg) + _log("[INF] " .. msg .. "\n") +end + function _warning(msg) _log("[WRN] " .. msg .. "\n") end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 6f33117..9924007 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -90,7 +90,7 @@ function handle_unmount(iface) log._warning("PPM: lost device " .. type .. " mounted to " .. iface) - return self.mounts[iface] + return lost_dev end -- list all available peripherals From 13b0fcf65f83c2a522c72d45423a118d0e61f4dd Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 09:41:06 -0400 Subject: [PATCH 41/63] PLC state code cleanup and bugfixes --- reactor-plc/plc.lua | 7 ++-- reactor-plc/startup.lua | 84 ++++++++++++++++++++++------------------- 2 files changed, 48 insertions(+), 43 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 9a168bb..47f84b5 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -495,9 +495,8 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end -- send live status information - -- control_state : acknowledged control state from supervisor - -- overridden : if ISS force disabled reactor - local send_status = function (control_state, overridden) + -- overridden : if ISS force disabled reactor + local send_status = function (overridden) local mek_data = nil if _update_status_cache() then @@ -508,7 +507,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) id = self.id, type = RPLC_TYPES.STATUS, timestamp = os.time(), - control_state = control_state, + control_state = ~self.scrammed, overridden = overridden, heating_rate = self.reactor.getHeatingRate(), mek_data = mek_data diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 29522e1..65dfe39 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -22,28 +22,38 @@ ppm.mount_all() local reactor = ppm.get_device("fissionReactor") local modem = ppm.get_device("modem") -local init_ok = true -local control_degraded = { degraded = false, no_reactor = false, no_modem = false } +local plc_state = { + init_ok = true, + scram = true, -- treated as latching e-stop, all conditions must be OK to set false + degraded = false, + no_reactor = false, + no_modem = false +} -- we need a reactor and a modem if reactor == nil then print_ts("Fission reactor not found. Running in a degraded state...\n"); log._warning("no reactor on startup") - init_ok = false - control_degraded.degraded = true - control_degraded.no_reactor = true + plc_state.init_ok = false + plc_state.degraded = true + plc_state.no_reactor = true end if modem == nil then - print_ts("No modem found. Disabling reactor and running in a degraded state...\n") + if reactor ~= nil then + print_ts("No modem found. Disabling reactor and running in a degraded state...\n") + reactor.scram() + else + print_ts("No modem found. Running in a degraded state...\n") + end + log._warning("no modem on startup") - reactor.scram() - init_ok = false - control_degraded.degraded = true - control_degraded.no_modem = true + plc_state.init_ok = false + plc_state.degraded = true + plc_state.no_modem = true end ::init:: -if init_ok then +if plc_state.init_ok then -- just booting up, no fission allowed (neutrons stay put thanks) reactor.scram() @@ -74,20 +84,16 @@ local LINK_TICKS = 20 -- start by linking local ticks_to_update = LINK_TICKS --- runtime variables -local control_state = false -local reactor_scram = true -- treated as latching e-stop - -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() - if init_ok then + 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 reactor_scram and reactor.isPowered() then + if plc_state.scram and reactor.isPowered() then reactor.scram() end ppm.enable_reporting() @@ -100,16 +106,16 @@ while true do if device.type == "fissionReactor" then print_ts("reactor disconnected!\n") log._error("reactor disconnected!") - control_degraded.no_reactor = true + plc_state.no_reactor = true -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? elseif device.type == "modem" then print_ts("modem disconnected!\n") log._error("modem disconnected!") - control_degraded.no_modem = true + plc_state.no_modem = true - if init_ok then + if plc_state.init_ok then -- try to scram reactor if it is still connected - reactor_scram = true + plc_state.scram = true if reactor.scram() then print_ts("successful reactor SCRAM\n") else @@ -117,55 +123,55 @@ while true do end end - control_degraded.degraded = true + plc_state.degraded = true end elseif event == "peripheral" then local device = ppm.mount(param1) if device.type == "fissionReactor" then -- reconnected reactor - reactor_scram = true + plc_state.scram = true device.scram() print_ts("reactor reconnected.\n") log._info("reactor reconnected.") - control_degraded.no_reactor = false + plc_state.no_reactor = false - if init_ok then + if plc_state.init_ok then iss.reconnect_reactor(device) plc_comms.reconnect_reactor(device) end -- determine if we are still in a degraded state if get_device("modem") not nil then - control_degraded.degraded = false + plc_state.degraded = false end elseif device.type == "modem" then -- reconnected modem - if init_ok then + if plc_state.init_ok then plc_comms.reconnect_modem(device) end print_ts("modem reconnected.\n") log._info("modem reconnected.") - control_degraded.no_modem = false + plc_state.no_modem = false -- determine if we are still in a degraded state if ppm.get_device("fissionReactor") not nil then - control_degraded.degraded = false + plc_state.degraded = false end end - if not init_ok and not control_degraded.degraded then - init_ok = false + if not plc_state.init_ok and not plc_state.degraded then + plc_state.init_ok = false goto init end end -- check safety (SCRAM occurs if tripped) - if not control_degraded.degraded then + if not plc_state.degraded then local iss_tripped, iss_status, iss_first = iss.check() - reactor_scram = reactor_scram or iss_tripped + plc_state.scram = plc_state.scram or iss_tripped if iss_first then plc_comms.send_iss_alarm(iss_status) end @@ -173,14 +179,14 @@ while true do -- handle event if event == "timer" and param1 == loop_tick then - if not control_degraded.no_modem then + if not plc_state.no_modem then -- basic event tick, send updated data if it is time (~3.33Hz) -- iss was already checked (main reason for this tick rate) ticks_to_update = ticks_to_update - 1 if plc_comms.is_linked() then if ticks_to_update <= 0 then - plc_comms.send_status(control_state, iss_tripped) + plc_comms.send_status(iss_tripped) ticks_to_update = UPDATE_TICKS end else @@ -197,17 +203,17 @@ while true do local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) - reactor_scram = reactor_scram or plc_comms.is_scrammed() + plc_state.scram = plc_state.scram or plc_comms.is_scrammed() elseif event == "timer" and param1 == conn_watchdog.get_timer() then -- haven't heard from server recently? shutdown reactor - reactor_scram = true + plc_state.scram = true plc_comms.unlink() iss.trip_timeout() print_ts("[alert] server timeout, reactor disabled\n") elseif event == "terminate" then -- safe exit - if init_ok then - reactor_scram = true + if plc_state.init_ok then + plc_state.scram = true if reactor.scram() then print_ts("[alert] exiting, reactor disabled\n") else From dbf7377c027e07521cd5e4e6443c02740a4076a3 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 15:56:48 -0400 Subject: [PATCH 42/63] #11 configurable 'networked' setting for PLCs that allows for standalone ISS-only mode --- reactor-plc/config.lua | 2 + reactor-plc/startup.lua | 109 ++++++++++++++++++++++------------------ 2 files changed, 62 insertions(+), 49 deletions(-) diff --git a/reactor-plc/config.lua b/reactor-plc/config.lua index 539946e..25f750c 100644 --- a/reactor-plc/config.lua +++ b/reactor-plc/config.lua @@ -1,3 +1,5 @@ +-- set to false to run in standalone mode (safety regulation only) +NETWORKED = true -- unique reactor ID REACTOR_ID = 1 -- port to send packets TO server diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 65dfe39..dcac721 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -22,6 +22,8 @@ ppm.mount_all() local reactor = ppm.get_device("fissionReactor") local modem = ppm.get_device("modem") +local networked = config.NETWORKED + local plc_state = { init_ok = true, scram = true, -- treated as latching e-stop, all conditions must be OK to set false @@ -38,7 +40,7 @@ if reactor == nil then plc_state.degraded = true plc_state.no_reactor = true end -if modem == nil then +if networked and modem == nil then if reactor ~= nil then print_ts("No modem found. Disabling reactor and running in a degraded state...\n") reactor.scram() @@ -52,37 +54,46 @@ if modem == nil then plc_state.no_modem = true end -::init:: -if plc_state.init_ok then - -- just booting up, no fission allowed (neutrons stay put thanks) - reactor.scram() - - -- init internal safety system - local iss = plc.iss_init(reactor) - log._debug("iss init") - - -- start comms - local plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) - log._debug("comms init") - - -- comms watchdog, 3 second timeout - local conn_watchdog = watchdog.new_watchdog(3) - log._debug("conn watchdog started") - - -- loop clock (10Hz, 2 ticks) - local loop_tick = os.startTimer(0.05) - log._debug("loop clock started") -else - log._warning("booted in a degraded state, awaiting peripheral connections...") -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 --- start by linking -local ticks_to_update = LINK_TICKS +local loop_tick = nil +local ticks_to_update = LINK_TICKS -- start by linking + +-- initialize PLC +::init:: +if plc_state.init_ok then + -- just booting up, no fission allowed (neutrons stay put thanks) + reactor.scram() + + -- init internal safety system + iss = plc.iss_init(reactor) + log._debug("iss init") + + if networked then + -- start comms + plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) + log._debug("comms init") + + -- comms watchdog, 3 second timeout + conn_watchdog = watchdog.new_watchdog(3) + log._debug("conn watchdog started") + else + log._debug("running without networking") + end + + -- loop clock (10Hz, 2 ticks) + loop_tick = os.startTimer(0.05) + log._debug("loop clock started") +else + log._warning("booted in a degraded state, awaiting peripheral connections...") +end -- event loop while true do @@ -93,7 +104,7 @@ while true do -- 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.isPowered() then + if plc_state.degraded or (plc_state.scram and reactor.isPowered()) then reactor.scram() end ppm.enable_reporting() @@ -108,7 +119,7 @@ while true do log._error("reactor disconnected!") plc_state.no_reactor = true -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? - elseif device.type == "modem" then + elseif networked and device.type == "modem" then print_ts("modem disconnected!\n") log._error("modem disconnected!") plc_state.no_modem = true @@ -139,14 +150,16 @@ while true do if plc_state.init_ok then iss.reconnect_reactor(device) - plc_comms.reconnect_reactor(device) + if networked then + plc_comms.reconnect_reactor(device) + end end -- determine if we are still in a degraded state - if get_device("modem") not nil then + if not networked or get_device("modem") not nil then plc_state.degraded = false end - elseif device.type == "modem" then + elseif networked and device.type == "modem" then -- reconnected modem if plc_state.init_ok then plc_comms.reconnect_modem(device) @@ -172,31 +185,29 @@ while true do if not plc_state.degraded then local iss_tripped, iss_status, iss_first = iss.check() plc_state.scram = plc_state.scram or iss_tripped - if iss_first then + if networked and iss_first then plc_comms.send_iss_alarm(iss_status) end end -- handle event - if event == "timer" and param1 == loop_tick then - if not plc_state.no_modem then - -- basic event tick, send updated data if it is time (~3.33Hz) - -- iss was already checked (main reason for this tick rate) - ticks_to_update = ticks_to_update - 1 + if event == "timer" and param1 == loop_tick and networked and not plc_state.no_modem 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) + 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) - ticks_to_update = UPDATE_TICKS - end - else - if ticks_to_update <= 0 then - plc_comms.send_link_req() - ticks_to_update = LINK_TICKS - end + if plc_comms.is_linked() then + if ticks_to_update <= 0 then + plc_comms.send_status(iss_tripped) + 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 - elseif event == "modem_message" then + 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() @@ -204,7 +215,7 @@ while true do local packet = plc_comms.parse_packet(p1, p2, p3, p4, p5) plc_comms.handle_packet(packet) plc_state.scram = plc_state.scram or plc_comms.is_scrammed() - elseif event == "timer" and param1 == conn_watchdog.get_timer() then + elseif event == "timer" and param1 == conn_watchdog.get_timer() and networked then -- haven't heard from server recently? shutdown reactor plc_state.scram = true plc_comms.unlink() From 5b32f838903a8ed6ba8bf31372af1d4c475c8c25 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 16:08:55 -0400 Subject: [PATCH 43/63] writeLine has newline of course.. --- scada-common/log.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scada-common/log.lua b/scada-common/log.lua index 79296c6..fbe2d66 100644 --- a/scada-common/log.lua +++ b/scada-common/log.lua @@ -30,16 +30,16 @@ function _debug(msg, trace) debug.getinfo(2).currentline .. " > " end - _log("[DBG] " .. dbg_info .. msg .. "\n") + _log("[DBG] " .. dbg_info .. msg) end end function _info(msg) - _log("[INF] " .. msg .. "\n") + _log("[INF] " .. msg) end function _warning(msg) - _log("[WRN] " .. msg .. "\n") + _log("[WRN] " .. msg) end function _error(msg, trace) @@ -56,9 +56,9 @@ function _error(msg, trace) debug.getinfo(2).currentline .. " > " end - _log("[ERR] " .. dbg_info .. msg .. "\n") + _log("[ERR] " .. dbg_info .. msg) end function _fatal(msg) - _log("[FTL] " .. msg .. "\n") + _log("[FTL] " .. msg) end From f24b21422905baff4e236e2c1b004563d822b857 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 16:09:29 -0400 Subject: [PATCH 44/63] fixed bugs and removed goto as lua 5.1 does not have goto --- reactor-plc/plc.lua | 4 +-- reactor-plc/startup.lua | 59 ++++++++++++++++++++++------------------- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 47f84b5..12d018e 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -42,7 +42,7 @@ function iss_init(reactor) self.reactor.scram() end - local first_trip = ~was_tripped and self.tripped + local first_trip = not was_tripped and self.tripped return self.tripped, status, first_trip end @@ -507,7 +507,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) id = self.id, type = RPLC_TYPES.STATUS, timestamp = os.time(), - control_state = ~self.scrammed, + control_state = not self.scrammed, overridden = overridden, heating_rate = self.reactor.getHeatingRate(), mek_data = mek_data diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index dcac721..669f777 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -7,10 +7,10 @@ os.loadAPI("scada-common/util.lua") os.loadAPI("scada-common/ppm.lua") os.loadAPI("scada-common/comms.lua") -os.loadAPI("reactor-plc/config.lua") -os.loadAPI("reactor-plc/plc.lua") +os.loadAPI("config.lua") +os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.1.0" +local R_PLC_VERSION = "alpha-v0.1.1" local print_ts = util.print_ts @@ -66,35 +66,38 @@ local LINK_TICKS = 20 local loop_tick = nil local ticks_to_update = LINK_TICKS -- start by linking --- initialize PLC -::init:: -if plc_state.init_ok then - -- just booting up, no fission allowed (neutrons stay put thanks) - reactor.scram() +function init() + if plc_state.init_ok then + -- just booting up, no fission allowed (neutrons stay put thanks) + reactor.scram() - -- init internal safety system - iss = plc.iss_init(reactor) - log._debug("iss init") + -- init internal safety system + iss = plc.iss_init(reactor) + log._debug("iss init") - if networked then - -- start comms - plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) - log._debug("comms init") + if networked then + -- start comms + plc_comms = plc.comms_init(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor, iss) + log._debug("comms init") - -- comms watchdog, 3 second timeout - conn_watchdog = watchdog.new_watchdog(3) - log._debug("conn watchdog started") + -- comms watchdog, 3 second timeout + conn_watchdog = watchdog.new_watchdog(3) + log._debug("conn watchdog started") + else + log._debug("running without networking") + end + + -- loop clock (10Hz, 2 ticks) + loop_tick = os.startTimer(0.05) + log._debug("loop clock started") else - log._debug("running without networking") + log._warning("booted in a degraded state, awaiting peripheral connections...") end - - -- loop clock (10Hz, 2 ticks) - loop_tick = os.startTimer(0.05) - log._debug("loop clock started") -else - log._warning("booted in a degraded state, awaiting peripheral connections...") end +-- initialize PLC +init() + -- event loop while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() @@ -156,7 +159,7 @@ while true do end -- determine if we are still in a degraded state - if not networked or get_device("modem") not nil then + if not networked or get_device("modem") ~= nil then plc_state.degraded = false end elseif networked and device.type == "modem" then @@ -170,14 +173,14 @@ while true do plc_state.no_modem = false -- determine if we are still in a degraded state - if ppm.get_device("fissionReactor") not nil then + if ppm.get_device("fissionReactor") ~= nil then plc_state.degraded = false end end if not plc_state.init_ok and not plc_state.degraded then plc_state.init_ok = false - goto init + init() end end From c47f45ea46a7d4203477eb40bff75a0cf3babf51 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 17:25:56 -0400 Subject: [PATCH 45/63] fixed bad function references in ISS --- reactor-plc/plc.lua | 68 ++++++++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 12d018e..99d2bef 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -15,20 +15,49 @@ function iss_init(reactor) self.reactor = reactor end + local damage_critical = function () + return self.reactor.getDamagePercent() >= 100 + end + + local excess_heated_coolant = function () + return self.reactor.getHeatedCoolantNeeded() == 0 + end + + local excess_waste = function () + return self.reactor.getWasteNeeded() == 0 + end + + local high_temp = function () + -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 + return self.reactor.getTemperature() >= 1200 + end + + local insufficient_fuel = function () + return self.reactor.getFuel() == 0 + end + + local no_coolant = function () + return self.reactor.getCoolantFilledPercentage() < 2 + end + + local timed_out = function () + return self.timed_out + end + local check = function () local status = "ok" local was_tripped = self.tripped -- check system states in order of severity - if self.damage_critical() then + if damage_critical() then status = "dmg_crit" - elseif self.high_temp() then + elseif high_temp() then status = "high_temp" - elseif self.excess_heated_coolant() then + elseif excess_heated_coolant() then status = "heated_coolant_backup" - elseif self.excess_waste() then + elseif excess_waste() then status = "full_waste" - elseif self.insufficient_fuel() then + elseif insufficient_fuel() then status = "no_fuel" elseif self.tripped then status = self.trip_cause @@ -83,35 +112,6 @@ function iss_init(reactor) } end end - - local damage_critical = function () - return self.reactor.getDamagePercent() >= 100 - end - - local excess_heated_coolant = function () - return self.reactor.getHeatedCoolantNeeded() == 0 - end - - local excess_waste = function () - return self.reactor.getWasteNeeded() == 0 - end - - local high_temp = function () - -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - return self.reactor.getTemperature() >= 1200 - end - - local insufficient_fuel = function () - return self.reactor.getFuel() == 0 - end - - local no_coolant = function () - return self.reactor.getCoolantFilledPercentage() < 2 - end - - local timed_out = function () - return self.timed_out - end return { reconnect_reactor = reconnect_reactor, From 895750ea141a7445721adb3d92817af5a916cbac Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 17:28:19 -0400 Subject: [PATCH 46/63] print, println, println_ts --- scada-common/util.lua | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/scada-common/util.lua b/scada-common/util.lua index f6fd611..1d4e11c 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -1,14 +1,33 @@ +-- we are overwriting 'print' so save it first +local _print = print + +-- print +function print(message) + term.write(message) +end + +-- print line +function println(message) + _print(message) +end + -- timestamped print function print_ts(message) term.write(os.date("[%H:%M:%S] ") .. message) end +-- timestamped print line +function println_ts(message) + _print(os.date("[%H:%M:%S] ") .. message) +end + + -- ComputerCraft OS Timer based Watchdog -- triggers a timer event if not fed within 'timeout' seconds function new_watchdog(timeout) local self = { _timeout = timeout, - _wd_timer = os.startTimer(_timeout) + _wd_timer = os.startTimer(timeout) } local get_timer = function () From ba1dd1b50e266ff5a5b40facbb4b1447869dd2a0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 17:29:27 -0400 Subject: [PATCH 47/63] #4 PLC degraded start and reconnects appear to be working now, fixed prints, and bugfixes to PPM --- reactor-plc/startup.lua | 76 +++++++++++++++++++++++++---------------- scada-common/ppm.lua | 32 +++++++++++------ 2 files changed, 68 insertions(+), 40 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 669f777..f6e79be 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -10,11 +10,17 @@ os.loadAPI("scada-common/comms.lua") os.loadAPI("config.lua") os.loadAPI("plc.lua") -local R_PLC_VERSION = "alpha-v0.1.1" +local R_PLC_VERSION = "alpha-v0.1.2" +local print = util.print +local println = util.println local print_ts = util.print_ts +local println_ts = util.println_ts -print(">> Reactor PLC " .. R_PLC_VERSION .. " <<") +log._info("========================================") +log._info("BOOTING reactor-plc.startup " .. R_PLC_VERSION) +log._info("========================================") +println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") -- mount connected devices ppm.mount_all() @@ -34,21 +40,21 @@ local plc_state = { -- we need a reactor and a modem if reactor == nil then - print_ts("Fission reactor not found. Running in a degraded state...\n"); + println("boot> fission reactor not found"); log._warning("no reactor on startup") + plc_state.init_ok = false plc_state.degraded = true plc_state.no_reactor = true end if networked and modem == nil then + println("boot> modem not found") + log._warning("no modem on startup") + if reactor ~= nil then - print_ts("No modem found. Disabling reactor and running in a degraded state...\n") reactor.scram() - else - print_ts("No modem found. Running in a degraded state...\n") end - log._warning("no modem on startup") plc_state.init_ok = false plc_state.degraded = true plc_state.no_modem = true @@ -81,7 +87,7 @@ function init() log._debug("comms init") -- comms watchdog, 3 second timeout - conn_watchdog = watchdog.new_watchdog(3) + conn_watchdog = util.new_watchdog(3) log._debug("conn watchdog started") else log._debug("running without networking") @@ -90,7 +96,10 @@ function init() -- loop clock (10Hz, 2 ticks) loop_tick = os.startTimer(0.05) log._debug("loop clock started") + + println("boot> completed"); else + println("boot> system in degraded state, awaiting devices...") log._warning("booted in a degraded state, awaiting peripheral connections...") end end @@ -107,7 +116,7 @@ while true do -- 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.degraded or (plc_state.scram and reactor.isPowered()) then + if plc_state.scram and reactor.getStatus() then reactor.scram() end ppm.enable_reporting() @@ -118,12 +127,13 @@ while true do local device = ppm.handle_unmount(param1) if device.type == "fissionReactor" then - print_ts("reactor disconnected!\n") + 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 - print_ts("modem disconnected!\n") + println_ts("modem disconnected!") log._error("modem disconnected!") plc_state.no_modem = true @@ -131,44 +141,48 @@ while true do -- try to scram reactor if it is still connected plc_state.scram = true if reactor.scram() then - print_ts("successful reactor SCRAM\n") + println_ts("successful reactor SCRAM") else - print_ts("failed reactor SCRAM\n") + println_ts("failed reactor SCRAM") end end plc_state.degraded = true end elseif event == "peripheral" then - local device = ppm.mount(param1) + local type, device = ppm.mount(param1) - if device.type == "fissionReactor" then + if type == "fissionReactor" then -- reconnected reactor - plc_state.scram = true - device.scram() + reactor = device - print_ts("reactor reconnected.\n") + 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(device) + iss.reconnect_reactor(reactor) if networked then - plc_comms.reconnect_reactor(device) + plc_comms.reconnect_reactor(reactor) end end -- determine if we are still in a degraded state - if not networked or get_device("modem") ~= nil then + if not networked or ppm.get_device("modem") ~= nil then plc_state.degraded = false end - elseif networked and device.type == "modem" then + elseif networked and type == "modem" then -- reconnected modem + modem = device + if plc_state.init_ok then - plc_comms.reconnect_modem(device) + plc_comms.reconnect_modem(modem) end - print_ts("modem reconnected.\n") + println_ts("modem reconnected.") log._info("modem reconnected.") plc_state.no_modem = false @@ -179,7 +193,7 @@ while true do end if not plc_state.init_ok and not plc_state.degraded then - plc_state.init_ok = false + plc_state.init_ok = true init() end end @@ -191,6 +205,8 @@ while true do if networked and iss_first then plc_comms.send_iss_alarm(iss_status) end + elseif plc_state.init_ok then + reactor.scram() end -- handle event @@ -223,20 +239,22 @@ while true do plc_state.scram = true plc_comms.unlink() iss.trip_timeout() - print_ts("[alert] server timeout, reactor disabled\n") + println_ts("server timeout, reactor disabled") + log._warning("server timeout, reactor disabled") elseif event == "terminate" then -- safe exit if plc_state.init_ok then plc_state.scram = true if reactor.scram() then - print_ts("[alert] exiting, reactor disabled\n") + println_ts("reactor disabled") else -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? - print_ts("[alert] exiting, reactor failed to disable\n") + println_ts("exiting, reactor failed to disable") end end -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? - print_ts("[alert] exited") + println_ts("exited") + log._info("terminate requested, exiting") return end end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 9924007..6799d4a 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -53,7 +53,13 @@ function mount_all() for i = 1, #ifaces do local pm_dev = peripheral.wrap(ifaces[i]) peri_init(pm_dev) - self.mounts[ifaces[i]] = { peripheral.getType(ifaces[i]), pm_dev } + + self.mounts[ifaces[i]] = { + type = peripheral.getType(ifaces[i]), + dev = pm_dev + } + + log._debug("PPM: found a " .. self.mounts[ifaces[i]].type) end if #ifaces == 0 then @@ -62,24 +68,28 @@ function mount_all() end -- mount a particular device -function mount(name) +function mount(iface) local ifaces = peripheral.getNames() local pm_dev = nil + local type = nil for i = 1, #ifaces do - if name == peripheral.getType(ifaces[i]) then - pm_dev = peripheral.wrap(ifaces[i]) + if iface == ifaces[i] then + log._debug("PPM: mount(" .. iface .. ") -> found a " .. peripheral.getType(iface)) + + type = peripheral.getType(iface) + pm_dev = peripheral.wrap(iface) peri_init(pm_dev) - self.mounts[ifaces[i]] = { - type = peripheral.getType(ifaces[i]), - device = pm_dev + self.mounts[iface] = { + type = peripheral.getType(iface), + dev = pm_dev } break end end - return pm_dev + return type, pm_dev end -- handle peripheral_detach event @@ -105,7 +115,7 @@ end -- get a mounted peripheral by side/interface function get_periph(iface) - return self.mounts[iface].device + return self.mounts[iface].dev end -- get a mounted peripheral type by side/interface @@ -119,7 +129,7 @@ function get_device(name) for side, data in pairs(self.mounts) do if data.type == name then - device = data.device + device = data.dev break end end @@ -133,7 +143,7 @@ function list_monitors() for side, data in pairs(self.mounts) do if data.type == "monitor" then - monitors[side] = data.device + table.insert(monitors, data.dev) end end From 7e7e98ff6b09ca1234007aedd4270b825437bf29 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 5 Apr 2022 17:58:23 -0400 Subject: [PATCH 48/63] #11 standalone de-asserts SCRAM and resets ISS before check, added prints to ISS, fixed non-networked mode related bugs, cleaned up ISS check call in startup --- reactor-plc/plc.lua | 10 +++++- reactor-plc/startup.lua | 71 ++++++++++++++++++++++++++--------------- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 99d2bef..484a5c7 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -50,14 +50,19 @@ function iss_init(reactor) -- check system states in order of severity if damage_critical() then + log._warning("ISS: damage critical!") status = "dmg_crit" elseif high_temp() then + log._warning("ISS: high temperature!") status = "high_temp" elseif excess_heated_coolant() then + log._warning("ISS: heated coolant backup!") status = "heated_coolant_backup" elseif excess_waste() then + log._warning("ISS: full waste!") status = "full_waste" elseif insufficient_fuel() then + log._warning("ISS: no fuel!") status = "no_fuel" elseif self.tripped then status = self.trip_cause @@ -66,6 +71,7 @@ function iss_init(reactor) end if status ~= "ok" then + log._warning("ISS: reactor SCRAM") self.tripped = true self.trip_cause = status self.reactor.scram() @@ -378,7 +384,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) end -- handle an RPLC packet - local handle_packet = function (packet) + local handle_packet = function (packet, plc_state) if packet ~= nil then if packet.scada_frame.protocol() == PROTOCOLS.RPLC then if self.linked then @@ -424,10 +430,12 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) elseif packet.type == RPLC_TYPES.MEK_SCRAM then -- disable the reactor self.scrammed = true + plc_state.scram = true _send_ack(packet.type, self.reactor.scram()) 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()) elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then -- set the burn rate diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index f6e79be..02da651 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.1.2" +local R_PLC_VERSION = "alpha-v0.1.3" local print = util.print local println = util.println @@ -32,7 +32,7 @@ local networked = config.NETWORKED local plc_state = { init_ok = true, - scram = true, -- treated as latching e-stop, all conditions must be OK to set false + scram = true, degraded = false, no_reactor = false, no_modem = false @@ -69,7 +69,7 @@ local conn_watchdog = nil local UPDATE_TICKS = 3 local LINK_TICKS = 20 -local loop_tick = nil +local loop_clock = nil local ticks_to_update = LINK_TICKS -- start by linking function init() @@ -94,7 +94,7 @@ function init() end -- loop clock (10Hz, 2 ticks) - loop_tick = os.startTimer(0.05) + loop_clock = os.startTimer(0.05) log._debug("loop clock started") println("boot> completed"); @@ -198,43 +198,62 @@ while true do end end - -- check safety (SCRAM occurs if tripped) - if not plc_state.degraded then - local iss_tripped, iss_status, iss_first = iss.check() - plc_state.scram = plc_state.scram or iss_tripped - if networked and iss_first then - plc_comms.send_iss_alarm(iss_status) + -- 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, 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) + if networked then + plc_comms.send_iss_alarm(iss_status) + end + end + else + reactor.scram() end - elseif plc_state.init_ok then - reactor.scram() end -- handle event - if event == "timer" and param1 == loop_tick and networked and not plc_state.no_modem then + 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) - ticks_to_update = ticks_to_update - 1 + 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) - ticks_to_update = UPDATE_TICKS - end - else - if ticks_to_update <= 0 then - plc_comms.send_link_req() - ticks_to_update = LINK_TICKS + if plc_comms.is_linked() then + if ticks_to_update <= 0 then + plc_comms.send_status(iss_tripped) + 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.05) 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.scram = plc_state.scram or plc_comms.is_scrammed() - elseif event == "timer" and param1 == conn_watchdog.get_timer() and networked then + 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() From 03f9284f30ebb2a2c410dd8d4f2deb7edd4ab27c Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 7 Apr 2022 11:12:41 -0400 Subject: [PATCH 49/63] #13 ISS tolerant of failed PPM calls, added comments --- reactor-plc/plc.lua | 69 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 7 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 484a5c7..c5f1bf2 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -2,7 +2,7 @@ -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted --- autonomous from main control +-- autonomous from main SCADA supervisor/coordinator control function iss_init(reactor) local self = { reactor = reactor, @@ -11,39 +11,90 @@ function iss_init(reactor) trip_cause = "" } + -- re-link a reactor after a peripheral re-connect local reconnect_reactor = function (reactor) self.reactor = reactor end + -- check for critical damage local damage_critical = function () - return self.reactor.getDamagePercent() >= 100 + local damage_percent = self.reactor.getDamagePercent() + if damage_percent == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor damage") + return false + else + return damage_percent >= 100 + end end + -- check for heated coolant backup local excess_heated_coolant = function () - return self.reactor.getHeatedCoolantNeeded() == 0 + local hc_needed = self.reactor.getHeatedCoolantNeeded() + if hc_needed == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor heated coolant level") + return false + else + return hc_needed == 0 + end end + -- check for excess waste local excess_waste = function () - return self.reactor.getWasteNeeded() == 0 + local w_needed = self.reactor.getWasteNeeded() + if w_needed == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor waste level") + return false + else + return w_needed == 0 + end end + -- check if the reactor is at a critically high temperature local high_temp = function () -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 - return self.reactor.getTemperature() >= 1200 + local temp = self.reactor.getTemperature() + if temp == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor temperature") + return false + else + return temp >= 1200 + end end + -- check if there is no fuel local insufficient_fuel = function () - return self.reactor.getFuel() == 0 + local fuel = self.reactor.getFuel() + if fuel == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor fuel level") + return false + else + return fuel == 0 + end end + -- check if there is no coolant local no_coolant = function () - return self.reactor.getCoolantFilledPercentage() < 2 + local coolant_filled = self.reactor.getCoolantFilledPercentage() + if coolant_filled == nil then + -- lost the peripheral or terminated, handled later + log._error("ISS: failed to check reactor coolant level") + return false + else + return coolant_filled < 2 + end end + -- if PLC timed out local timed_out = function () return self.timed_out end + -- check all safety conditions local check = function () local status = "ok" local was_tripped = self.tripped @@ -70,6 +121,7 @@ function iss_init(reactor) self.tripped = false end + -- if a new trip occured... if status ~= "ok" then log._warning("ISS: reactor SCRAM") self.tripped = true @@ -82,6 +134,7 @@ function iss_init(reactor) return self.tripped, status, first_trip end + -- report a PLC comms timeout local trip_timeout = function () self.tripped = false self.trip_cause = "timeout" @@ -89,12 +142,14 @@ function iss_init(reactor) self.reactor.scram() end + -- reset the ISS local reset = function () self.timed_out = false self.tripped = false self.trip_cause = "" end + -- get the ISS status local status = function (named) if named then return { From b085baf91bc64673a38e846916013d7639429084 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 7 Apr 2022 11:44:17 -0400 Subject: [PATCH 50/63] #12 specifically get wireless modems --- reactor-plc/startup.lua | 65 ++++++++++++++++++++++++----------------- rtu/startup.lua | 5 ++-- scada-common/ppm.lua | 56 ++++++++++++++++++++++++++++++----- 3 files changed, 89 insertions(+), 37 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 02da651..bd7f49e 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -25,8 +25,8 @@ println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") -- mount connected devices ppm.mount_all() -local reactor = ppm.get_device("fissionReactor") -local modem = ppm.get_device("modem") +local reactor = ppm.get_fission_reactor() +local modem = ppm.get_wireless_modem() local networked = config.NETWORKED @@ -48,8 +48,8 @@ if reactor == nil then plc_state.no_reactor = true end if networked and modem == nil then - println("boot> modem not found") - log._warning("no modem on startup") + println("boot> wireless modem not found") + log._warning("no wireless modem on startup") if reactor ~= nil then reactor.scram() @@ -133,21 +133,28 @@ while true do plc_state.degraded = true -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_PERI_DC) ? elseif networked and device.type == "modem" then - println_ts("modem disconnected!") - log._error("modem disconnected!") - plc_state.no_modem = true + -- 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") - else - println_ts("failed reactor SCRAM") + 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 - end - plc_state.degraded = true + plc_state.degraded = true + else + log._warning("non-comms modem disconnected") + end end elseif event == "peripheral" then local type, device = ppm.mount(param1) @@ -175,20 +182,24 @@ while true do plc_state.degraded = false end elseif networked and type == "modem" then - -- reconnected modem - modem = device + if device.isWireless() then + -- reconnected modem + modem = device - if plc_state.init_ok then - plc_comms.reconnect_modem(modem) - end + if plc_state.init_ok then + plc_comms.reconnect_modem(modem) + end - println_ts("modem reconnected.") - log._info("modem reconnected.") - plc_state.no_modem = false + 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 + -- 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 diff --git a/rtu/startup.lua b/rtu/startup.lua index b287a53..e9635f3 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -30,9 +30,10 @@ local linked = false ppm.mount_all() -- get modem -local modem = ppm.get_device("modem") +local modem = ppm.get_wireless_modem() if modem == nil then - print("No modem found, exiting...") + println("boot> wireless modem not found") + log._warning("no wireless modem on startup") return end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 6799d4a..1775b77 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -4,6 +4,10 @@ -- Protected Peripheral Manager -- +---------------------------- +-- PRIVATE DATA/FUNCTIONS -- +---------------------------- + local self = { mounts = {}, mute = false @@ -34,6 +38,12 @@ local peri_init = function (device) end end +---------------------- +-- PUBLIC FUNCTIONS -- +---------------------- + +-- REPORTING -- + -- silence error prints function disable_reporting() self.mute = true @@ -44,6 +54,8 @@ function enable_reporting() self.mute = false end +-- MOUNTING -- + -- mount all available peripherals (clears mounts first) function mount_all() local ifaces = peripheral.getNames() @@ -103,6 +115,8 @@ function handle_unmount(iface) return lost_dev end +-- GENERAL ACCESSORS -- + -- list all available peripherals function list_avail() return peripheral.getNames() @@ -123,7 +137,20 @@ function get_type(iface) return self.mounts[iface].type end --- get a mounted peripheral by type +-- get all mounted peripherals by type +function get_all_devices(name) + local devices = {} + + for side, data in pairs(self.mounts) do + if data.type == name then + table.insert(devices, data.dev) + end + end + + return devices +end + +-- get a mounted peripheral by type (if multiple, returns the first) function get_device(name) local device = nil @@ -137,15 +164,28 @@ function get_device(name) return device end --- list all connected monitors -function list_monitors() - local monitors = {} +-- SPECIFIC DEVICE ACCESSORS -- - for side, data in pairs(self.mounts) do - if data.type == "monitor" then - table.insert(monitors, data.dev) +-- get the fission reactor (if multiple, returns the first) +function get_fission_reactor() + return get_device("fissionReactor") +end + +-- get the wireless modem (if multiple, returns the first) +function get_wireless_modem() + local w_modem = nil + + for side, device in pairs(self.mounts) do + if device.type == "modem" and device.dev.isWireless() then + w_modem = device.dev + break end end - return monitors + return w_modem +end + +-- list all connected monitors +function list_monitors() + return get_all_devices("monitor") end From 28b1c03e039acaab0a1ed3d807cfd8fe59d4421a Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Thu, 7 Apr 2022 11:45:01 -0400 Subject: [PATCH 51/63] upped version --- 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 bd7f49e..b5b23e0 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.1.3" +local R_PLC_VERSION = "alpha-v0.1.4" local print = util.print local println = util.println From 203d868aeb1017625a128583499661f8d6e979c5 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 11 Apr 2022 11:08:46 -0400 Subject: [PATCH 52/63] RTU print fixes, config fixes, comms init fixes and moved modem open --- rtu/config.lua | 9 ++++++--- rtu/rtu.lua | 5 +++++ rtu/startup.lua | 35 +++++++++++++++++++---------------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/rtu/config.lua b/rtu/config.lua index b06305e..cd9e70c 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -1,7 +1,10 @@ -- #REQUIRES rsio.lua -SCADA_SERVER = 16000 - +-- port to send packets TO server +SERVER_PORT = 16000 +-- port to listen to incoming packets FROM server +LISTEN_PORT = 15001 +-- RTU peripheral devices (named: side/network device name) RTU_DEVICES = { { name = "boiler_0", @@ -14,7 +17,7 @@ RTU_DEVICES = { for_reactor = 1 } } - +-- RTU redstone interface definitions RTU_REDSTONE = { { for_reactor = 1, diff --git a/rtu/rtu.lua b/rtu/rtu.lua index a80e3f1..6bbb1d1 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -98,6 +98,11 @@ function rtu_comms(modem, local_port, server_port) l_port = local_port } + -- open modem + if not self.modem.isOpen(self.l_port) then + self.modem.open(self.l_port) + end + -- PRIVATE FUNCTIONS -- local _send = function (protocol, msg) diff --git a/rtu/startup.lua b/rtu/startup.lua index e9635f3..bde8732 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -17,7 +17,15 @@ os.loadAPI("dev/turbine.lua") local RTU_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 rtu.startup " .. RTU_VERSION) +log._info("========================================") +println(">> RTU " .. RTU_VERSION .. " <<") ---------------------------------------- -- startup @@ -37,12 +45,7 @@ if modem == nil then return end --- start comms -if not modem.isOpen(config.LISTEN_PORT) then - modem.open(config.LISTEN_PORT) -end - -local rtu_comms = rtu.rtu_comms(config.REACTOR_ID, modem, config.LISTEN_PORT, config.SERVER_PORT, reactor) +local rtu_comms = rtu.rtu_comms(modem, config.LISTEN_PORT, config.SERVER_PORT) ---------------------------------------- -- determine configuration @@ -69,8 +72,8 @@ for reactor_idx = 1, #RTU_REDSTONE do end if ~valid then - local message = "invalid redstone configuration at index " .. i - print_ts(message .. "\n") + local message = "init> invalid redstone definition at index " .. i + println_ts(message) log._warning(message) else -- link redstone in RTU @@ -85,13 +88,13 @@ for reactor_idx = 1, #RTU_REDSTONE do rs_rtu.link_ao(config.channel, config.side) else -- should be unreachable code, we already validated channels - log._error("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, config.channel) - log._debug("startup> linked redstone " .. #capabilities .. ": " .. rsio.to_string(config.channel) .. " (" .. config.side .. + log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(config.channel) .. " (" .. config.side .. ") for reactor " .. RTU_REDSTONE[reactor_idx].for_reactor) end end @@ -112,8 +115,8 @@ for i = 1, #RTU_DEVICES do local device = ppm.get_periph(RTU_DEVICES[i].name) if device == nil then - local message = "'" .. RTU_DEVICES[i].name .. "' not found" - print_ts(message .. "\n") + local message = "init> '" .. RTU_DEVICES[i].name .. "' not found" + println_ts(message) log._warning(message) else local type = ppm.get_type(RTU_DEVICES[i].name) @@ -133,8 +136,8 @@ for i = 1, #RTU_DEVICES do rtu_type = "imatrix" rtu_iface = imatrix_rtu(device) else - local message = "device '" .. RTU_DEVICES[i].name .. "' is not a known type (" .. type .. ")" - print_ts(message .. "\n") + local message = "init> device '" .. RTU_DEVICES[i].name .. "' is not a known type (" .. type .. ")" + println_ts(message) log._warning(message) end @@ -149,7 +152,7 @@ for i = 1, #RTU_DEVICES do modbus_io = modbus_init(rtu_iface) }) - log._debug("startup> 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 @@ -188,7 +191,7 @@ while true do -- if linked, stop sending advertisements linked = link_ref.linked elseif event == "terminate" then - print_ts("Exiting...\n") + println_ts("exiting...") return end end From 945b761fc2e06f36f31d6627dc9607bb6cdbebde Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 11 Apr 2022 17:27:57 -0400 Subject: [PATCH 53/63] #2 RTU handle disconnects/reconnects --- rtu/startup.lua | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index bde8732..2caa50d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -170,9 +170,43 @@ while true do local event, param1, param2, param3, param4, param5 = os.pullEventRaw() if event == "peripheral_detach" then - ppm.handle_unmount(param1) + -- handle loss of a device + local device = ppm.handle_unmount(param1) - -- todo: handle unit change + 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 + 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(device) + elseif unit.type == "turbine" then + unit.rtu = turbine_rtu(device) + elseif unit.type == "imatrix" then + unit.rtu = imatrix_rtu(device) + end + + unit.modbus_io = modbus_init(unit.rtu) + + println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) + end + end elseif event == "timer" and param1 == loop_tick then -- period tick, if we are linked send heartbeat, if not send advertisement if linked then From 2a21d7d0be1c8ec4d8b14a200efce10022fbeac8 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Apr 2022 21:12:25 -0400 Subject: [PATCH 54/63] #14, #15 ppm access fault handling, report modbus exceptions, handle ppm faults in PLC/RTU code --- reactor-plc/plc.lua | 65 +++++++++++------- reactor-plc/startup.lua | 6 +- rtu/rtu.lua | 42 +++++++++--- scada-common/modbus.lua | 147 ++++++++++++++++++++++++++++++++-------- scada-common/ppm.lua | 35 +++++++++- 5 files changed, 227 insertions(+), 68 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index c5f1bf2..6843303 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,4 +1,5 @@ -- #REQUIRES comms.lua +-- #REQUIRES ppm.lua -- Internal Safety System -- identifies dangerous states and SCRAMs reactor if warranted @@ -19,7 +20,7 @@ function iss_init(reactor) -- check for critical damage local damage_critical = function () local damage_percent = self.reactor.getDamagePercent() - if damage_percent == nil then + if damage_percent == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor damage") return false @@ -31,7 +32,7 @@ function iss_init(reactor) -- check for heated coolant backup local excess_heated_coolant = function () local hc_needed = self.reactor.getHeatedCoolantNeeded() - if hc_needed == nil then + if hc_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor heated coolant level") return false @@ -43,7 +44,7 @@ function iss_init(reactor) -- check for excess waste local excess_waste = function () local w_needed = self.reactor.getWasteNeeded() - if w_needed == nil then + if w_needed == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor waste level") return false @@ -56,7 +57,7 @@ function iss_init(reactor) local high_temp = function () -- mekanism: MAX_DAMAGE_TEMPERATURE = 1_200 local temp = self.reactor.getTemperature() - if temp == nil then + if temp == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor temperature") return false @@ -68,7 +69,7 @@ function iss_init(reactor) -- check if there is no fuel local insufficient_fuel = function () local fuel = self.reactor.getFuel() - if fuel == nil then + if fuel == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor fuel level") return false @@ -80,7 +81,7 @@ function iss_init(reactor) -- check if there is no coolant local no_coolant = function () local coolant_filled = self.reactor.getCoolantFilledPercentage() - if coolant_filled == nil then + if coolant_filled == ppm.ACCESS_FAULT then -- lost the peripheral or terminated, handled later log._error("ISS: failed to check reactor coolant level") return false @@ -126,7 +127,9 @@ function iss_init(reactor) log._warning("ISS: reactor SCRAM") self.tripped = true self.trip_cause = status - self.reactor.scram() + if self.reactor.scram() == ppm.ACCESS_FAULT then + log._error("ISS: failed reactor SCRAM") + end end local first_trip = not was_tripped and self.tripped @@ -293,6 +296,7 @@ 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 { status = self.reactor.getStatus(), burn_rate = self.reactor.getBurnRate(), @@ -316,17 +320,19 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) hcool_amnt = self.reactor.getHeatedCoolant()['amount'], hcool_need = self.reactor.getHeatedCoolantNeeded(), hcool_fill = self.reactor.getHeatedCoolantFilledPercentage() - } + }, ppm.faulted() end local _update_status_cache = function () - local status = _reactor_status() + local status, faulted = _reactor_status() local changed = false - for key, value in pairs(status) do - if value ~= self.status_cache[key] then - changed = true - break + if not faulted then + for key, value in pairs(status) do + if value ~= self.status_cache[key] then + changed = true + break + end end end @@ -362,6 +368,7 @@ 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 = { heat_cap = self.reactor.getHeatCapacity(), fuel_asm = self.reactor.getFuelAssemblies(), @@ -373,13 +380,17 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) max_burn = self.reactor.getMaxBurnRate() } - local struct_packet = { - id = self.id, - type = RPLC_TYPES.MEK_STRUCT, - mek_data = mek_data - } + if not faulted then + local struct_packet = { + id = self.id, + type = RPLC_TYPES.MEK_STRUCT, + mek_data = mek_data + } - _send(struct_packet) + _send(struct_packet) + else + log._error("failed to send structure: PPM fault") + end end local _send_iss_status = function () @@ -407,6 +418,7 @@ 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() end -- parse an RPLC packet @@ -486,25 +498,25 @@ 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()) + _send_ack(packet.type, self.reactor.scram() == ppm.ACCESS_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()) + _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 - if max_burn_rate ~= nil then + 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) end end - _send_ack(packet.type, success) + _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()) @@ -540,9 +552,10 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) self.linked = link_ack == RPLC_LINKING.ALLOW else - log._("discarding non-link packet before linked") + log._debug("discarding non-link packet before linked") end elseif packet.scada_frame.protocol() == PROTOCOLS.SCADA_MGMT then + -- todo end end end @@ -559,7 +572,8 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) -- send live status information -- overridden : if ISS force disabled reactor - local send_status = function (overridden) + -- degraded : if PLC status is degraded + local send_status = function (overridden, degraded) local mek_data = nil if _update_status_cache() then @@ -572,6 +586,7 @@ function comms_init(id, modem, local_port, server_port, reactor, iss) timestamp = os.time(), control_state = not self.scrammed, overridden = overridden, + degraded = degraded, heating_rate = self.reactor.getHeatingRate(), mek_data = mek_data } diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index b5b23e0..869be2f 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.1.4" +local R_PLC_VERSION = "alpha-v0.1.5" local print = util.print local println = util.println @@ -243,7 +243,7 @@ while true do if plc_comms.is_linked() then if ticks_to_update <= 0 then - plc_comms.send_status(iss_tripped) + plc_comms.send_status(iss_tripped, plc_state.degraded) ticks_to_update = UPDATE_TICKS end else @@ -275,7 +275,7 @@ while true do -- safe exit if plc_state.init_ok then plc_state.scram = true - if reactor.scram() then + if reactor.scram() ~= ppm.ACCESS_FAULT then println_ts("reactor disabled") else -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_LOST_CONTROL) ? diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 6bbb1d1..176b37d 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -1,5 +1,6 @@ -- #REQUIRES comms.lua -- #REQUIRES modbus.lua +-- #REQUIRES ppm.lua function rtu_init() local self = { @@ -10,68 +11,91 @@ function rtu_init() io_count_cache = { 0, 0, 0, 0 } } - local __count_io = function () + 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 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) table.insert(self.discrete_inputs, f) - __count_io() + _count_io() return #self.discrete_inputs end + -- return : value, access fault local read_di = function (di_addr) - return self.discrete_inputs[di_addr]() + ppm.clear_fault() + local value = self.discrete_inputs[di_addr]() + return value, ppm.is_faulted() end -- coils: single bit read-write + -- return : count of coils local connect_coil = function (f_read, f_write) table.insert(self.coils, { read = f_read, write = f_write }) - __count_io() + _count_io() return #self.coils end + -- return : value, access fault local read_coil = function (coil_addr) - return self.coils[coil_addr].read() + 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) + ppm.clear_fault() self.coils[coil_addr].write(value) + return ppm.is_faulted() end -- input registers: multi-bit read-only + -- return : count of input registers local connect_input_reg = function (f) table.insert(self.input_regs, f) - __count_io() + _count_io() return #self.input_regs end + -- return : value, access fault local read_input_reg = function (reg_addr) - return self.coils[reg_addr]() + ppm.clear_fault() + local value = self.coils[reg_addr]() + return value, ppm.is_faulted() end -- holding registers: multi-bit read-write + -- 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 }) - __count_io() + _count_io() return #self.holding_regs end + -- return : value, access fault local read_holding_reg = function (reg_addr) - return self.coils[reg_addr].read() + 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) + ppm.clear_fault() self.coils[reg_addr].write(value) + return ppm.is_faulted() end return { diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index 9d2899d..dc73e64 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -11,6 +11,20 @@ local MODBUS_FCODE = { 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 +} + -- new modbus comms handler object function modbus_init(rtu_dev) local self = { @@ -19,13 +33,22 @@ function modbus_init(rtu_dev) 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 if return_ok then for i = 0, (count - 1) do - readings[i] = self.rtu.read_coil(c_addr_start + i) + readings[i], access_fault = self.rtu.read_coil(c_addr_start + i) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end + else + readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, readings @@ -33,13 +56,22 @@ function modbus_init(rtu_dev) local _2_read_discrete_inputs = function (di_addr_start, count) local readings = {} + local access_fault = false local discrete_inputs, _, _, _ = self.rtu.io_count() local return_ok = (di_addr_start + count) <= discrete_inputs if return_ok then for i = 0, (count - 1) do - readings[i] = self.rtu.read_di(di_addr_start + i) + readings[i], access_fault = self.rtu.read_di(di_addr_start + i) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end + else + readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, readings @@ -47,13 +79,22 @@ function modbus_init(rtu_dev) local _3_read_multiple_holding_registers = function (hr_addr_start, count) local readings = {} + local access_fault = false local _, _, _, hold_regs = self.rtu.io_count() local return_ok = (hr_addr_start + count) <= hold_regs if return_ok then for i = 0, (count - 1) do - readings[i] = self.rtu.read_holding_reg(hr_addr_start + i) + readings[i], access_fault = self.rtu.read_holding_reg(hr_addr_start + i) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end + else + readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, readings @@ -61,93 +102,132 @@ function modbus_init(rtu_dev) local _4_read_input_registers = function (ir_addr_start, count) local readings = {} + local access_fault = false local _, _, input_regs, _ = self.rtu.io_count() local return_ok = (ir_addr_start + count) <= input_regs if return_ok then for i = 0, (count - 1) do - readings[i] = self.rtu.read_input_reg(ir_addr_start + i) + readings[i], access_fault = self.rtu.read_input_reg(ir_addr_start + i) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end + else + readings = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end return return_ok, readings end local _5_write_single_coil = function (c_addr, value) + local response = nil local _, coils, _, _ = self.rtu.io_count() local return_ok = c_addr <= coils - + if return_ok then - self.rtu.write_coil(c_addr, value) + local access_fault = self.rtu.write_coil(c_addr, value) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + end + else + response = MODBUS_EXCODE.ILLEGAL_DATA_ADDR end - return return_ok + return return_ok, response end local _6_write_single_holding_register = function (hr_addr, value) + local response = nil local _, _, _, hold_regs = self.rtu.io_count() local return_ok = hr_addr <= hold_regs if return_ok then - self.rtu.write_holding_reg(hr_addr, value) + local access_fault = self.rtu.write_holding_reg(hr_addr, value) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + end end return return_ok end local _15_write_multiple_coils = function (c_addr_start, values) + local response = nil local _, coils, _, _ = self.rtu.io_count() local count = #values local return_ok = (c_addr_start + count) <= coils if return_ok then for i = 0, (count - 1) do - self.rtu.write_coil(c_addr_start + i, values[i + 1]) + local access_fault = self.rtu.write_coil(c_addr_start + i, values[i + 1]) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end end - return return_ok + return return_ok, response end local _16_write_multiple_holding_registers = function (hr_addr_start, values) + local response = nil local _, _, _, hold_regs = self.rtu.io_count() local count = #values local return_ok = (hr_addr_start + count) <= hold_regs if return_ok then for i = 0, (count - 1) do - self.rtu.write_coil(hr_addr_start + i, values[i + 1]) + local access_fault = self.rtu.write_coil(hr_addr_start + i, values[i + 1]) + + if access_fault then + return_ok = false + readings = MODBUS_EXCODE.SERVER_DEVICE_FAIL + break + end end end - return return_ok + return return_ok, response end local handle_packet = function (packet) local return_code = true - local readings = nil + local response = nil + local reply = packet if #packet.data == 2 then -- handle by function code if packet.func_code == MODBUS_FCODE.READ_COILS then - return_code, readings = _1_read_coils(packet.data[1], packet.data[2]) + return_code, response = _1_read_coils(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.READ_DISCRETE_INPUTS then - return_code, readings = _2_read_discrete_inputs(packet.data[1], packet.data[2]) + 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, readings = _3_read_multiple_holding_registers(packet.data[1], packet.data[2]) + return_code, response = _3_read_multiple_holding_registers(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.READ_INPUT_REGISTERS then - return_code, readings = _4_read_input_registers(packet.data[1], packet.data[2]) + 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 = _5_write_single_coil(packet.data[1], packet.data[2]) + return_code, response = _5_write_single_coil(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.WRITE_SINGLE_HOLD_REG then - return_code = _6_write_single_holding_register(packet.data[1], packet.data[2]) + return_code, response = _6_write_single_holding_register(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_COILS then - return_code = _15_write_multiple_coils(packet.data[1], packet.data[2]) + return_code, response = _15_write_multiple_coils(packet.data[1], packet.data[2]) elseif packet.func_code == MODBUS_FCODE.WRITE_MUL_HOLD_REGS then - return_code = _16_write_multiple_holding_registers(packet.data[1], packet.data[2]) + return_code, response = _16_write_multiple_holding_registers(packet.data[1], packet.data[2]) else -- unknown function return_code = false + response = MODBUS_EXCODE.ILLEGAL_FUNCTION end else -- invalid length @@ -155,19 +235,28 @@ function modbus_init(rtu_dev) end if return_code then - -- response (default is to echo back) - response = packet - if readings ~= nil then - response.length = #readings - response.data = readings + -- default is to echo back + if type(response) == "table" then + reply.length = #response + reply.data = response end else -- echo back with error flag - response = packet - response.func_code = bit.bor(packet.func_code, ERROR_FLAG) + reply.func_code = bit.bor(packet.func_code, MODBUS_FCODE.ERROR_FLAG) + + if type(response) == "nil" then + reply.length = 0 + reply.data = {} + elseif type(response) == "number" then + reply.length = 1 + reply.data = { response } + elseif type(response) == "table" then + reply.length = #response + reply.data = response + end end - return return_code, response + return return_code, reply end return { diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 1775b77..3eb3977 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -4,12 +4,17 @@ -- Protected Peripheral Manager -- +ACCESS_OK = true +ACCESS_FAULT = nil + ---------------------------- -- PRIVATE DATA/FUNCTIONS -- ---------------------------- local self = { mounts = {}, + auto_cf = false, + faulted = false, mute = false } @@ -21,18 +26,22 @@ local peri_init = function (device) 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 true + return ACCESS_OK else return result end else -- function failed + self.faulted = true if not mute then log._error("PPM: protected " .. key .. "() -> " .. result) end - return nil + return ACCESS_FAULT end end end @@ -54,6 +63,28 @@ function enable_reporting() self.mute = false end +-- FAULT MEMORY -- + +-- enable automatically clearing fault flag +function enable_afc() + self.auto_cf = true +end + +-- disable automatically clearing fault flag +function disable_afc() + self.auto_cf = false +end + +-- check fault flag +function is_faulted() + return self.faulted +end + +-- clear fault flag +function clear_fault() + self.faulted = false +end + -- MOUNTING -- -- mount all available peripherals (clears mounts first) From ba5975f29b1a94ee1ca570d703baecd9b22e3b50 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Apr 2022 22:34:31 -0400 Subject: [PATCH 55/63] RTU config fixed missing rsio reference --- rtu/config.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rtu/config.lua b/rtu/config.lua index cd9e70c..71804a4 100644 --- a/rtu/config.lua +++ b/rtu/config.lua @@ -23,19 +23,19 @@ RTU_REDSTONE = { for_reactor = 1, io = { { - channel = RS_IO.WASTE_PO, + channel = rsio.RS_IO.WASTE_PO, side = "top", bundled_color = colors.blue, for_reactor = 1 }, { - channel = RS_IO.WASTE_PU, + channel = rsio.RS_IO.WASTE_PU, side = "top", bundled_color = colors.cyan, for_reactor = 1 }, { - channel = RS_IO.WASTE_AM, + channel = rsio.RS_IO.WASTE_AM, side = "top", bundled_color = colors.purple, for_reactor = 1 From 0c5eb77cbae0bc6bfa4970657baa50462ac1472f Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Apr 2022 22:36:18 -0400 Subject: [PATCH 56/63] fixed some bugs with RTU startup referencing external data/functions --- rtu/startup.lua | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 2caa50d..59bc477 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -11,17 +11,23 @@ os.loadAPI("scada-common/rsio.lua") os.loadAPI("config.lua") os.loadAPI("rtu.lua") +os.loadAPI("dev/redstone.lua") os.loadAPI("dev/boiler.lua") os.loadAPI("dev/imatrix.lua") os.loadAPI("dev/turbine.lua") -local RTU_VERSION = "alpha-v0.1.0" +local RTU_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 +local redstone_rtu = redstone.redstone_rtu +local boiler_rtu = boiler.boiler_rtu +local turbine_rtu = turbine.turbine_rtu +local imatrix_rtu = imatrix.imatrix_rtu + log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) log._info("========================================") @@ -51,10 +57,13 @@ local rtu_comms = rtu.rtu_comms(modem, config.LISTEN_PORT, config.SERVER_PORT) -- determine configuration ---------------------------------------- +local rtu_redstone = config.RTU_REDSTONE +local rtu_devices = config.RTU_DEVICES + -- redstone interfaces -for reactor_idx = 1, #RTU_REDSTONE do +for reactor_idx = 1, #rtu_redstone do local rs_rtu = redstone_rtu() - local io_table = RTU_REDSTONE[reactor_idx].io + local io_table = rtu_redstone[reactor_idx].io local capabilities = {} @@ -71,7 +80,7 @@ for reactor_idx = 1, #RTU_REDSTONE do end end - if ~valid then + if not valid then local message = "init> invalid redstone definition at index " .. i println_ts(message) log._warning(message) @@ -95,7 +104,7 @@ for reactor_idx = 1, #RTU_REDSTONE do table.insert(capabilities, config.channel) log._debug("init> linked redstone " .. #capabilities .. ": " .. rsio.to_string(config.channel) .. " (" .. config.side .. - ") for reactor " .. RTU_REDSTONE[reactor_idx].for_reactor) + ") for reactor " .. rtu_redstone[reactor_idx].for_reactor) end end @@ -103,7 +112,7 @@ for reactor_idx = 1, #RTU_REDSTONE do name = "redstone_io", type = "redstone", index = 1, - reactor = RTU_REDSTONE[reactor_idx].for_reactor, + reactor = rtu_redstone[reactor_idx].for_reactor, device = capabilities, -- use device field for redstone channels rtu = rs_rtu, modbus_io = modbus_init(rs_rtu) @@ -111,15 +120,15 @@ for reactor_idx = 1, #RTU_REDSTONE do end -- mounted peripherals -for i = 1, #RTU_DEVICES do - local device = ppm.get_periph(RTU_DEVICES[i].name) +for i = 1, #rtu_devices do + local device = ppm.get_periph(rtu_devices[i].name) if device == nil then - local message = "init> '" .. RTU_DEVICES[i].name .. "' not found" + local message = "init> '" .. rtu_devices[i].name .. "' not found" println_ts(message) log._warning(message) else - local type = ppm.get_type(RTU_DEVICES[i].name) + local type = ppm.get_type(rtu_devices[i].name) local rtu_iface = nil local rtu_type = "" @@ -136,24 +145,24 @@ for i = 1, #RTU_DEVICES do rtu_type = "imatrix" rtu_iface = imatrix_rtu(device) else - local message = "init> device '" .. RTU_DEVICES[i].name .. "' is not a known type (" .. type .. ")" + local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")" println_ts(message) log._warning(message) end if rtu_iface ~= nil then table.insert(units, { - name = RTU_DEVICES[i].name, + name = rtu_devices[i].name, type = rtu_type, - index = RTU_DEVICES[i].index, - reactor = RTU_DEVICES[i].for_reactor, + index = rtu_devices[i].index, + reactor = rtu_devices[i].for_reactor, device = device, rtu = rtu_iface, modbus_io = modbus_init(rtu_iface) }) - log._debug("init> initialized RTU unit #" .. #units .. ": " .. RTU_DEVICES[i].name .. " (" .. rtu_type .. ") [" .. - RTU_DEVICES[i].index .. "] for reactor " .. RTU_DEVICES[i].for_reactor) + 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 end From 6d6953d7958541494f0ce356418868a91bffb8b6 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Sun, 17 Apr 2022 22:37:09 -0400 Subject: [PATCH 57/63] RTU device inits now correctly use rtu.rtu_init() not rtu_init() --- rtu/dev/boiler.lua | 2 +- rtu/dev/imatrix.lua | 2 +- rtu/dev/redstone.lua | 2 +- rtu/dev/turbine.lua | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rtu/dev/boiler.lua b/rtu/dev/boiler.lua index d76107a..b20cd0f 100644 --- a/rtu/dev/boiler.lua +++ b/rtu/dev/boiler.lua @@ -2,7 +2,7 @@ function boiler_rtu(boiler) local self = { - rtu = rtu_init(), + rtu = rtu.rtu_init(), boiler = boiler } diff --git a/rtu/dev/imatrix.lua b/rtu/dev/imatrix.lua index f87bdbf..39c647b 100644 --- a/rtu/dev/imatrix.lua +++ b/rtu/dev/imatrix.lua @@ -2,7 +2,7 @@ function imatrix_rtu(imatrix) local self = { - rtu = rtu_init(), + rtu = rtu.rtu_init(), imatrix = imatrix } diff --git a/rtu/dev/redstone.lua b/rtu/dev/redstone.lua index da2f7e2..aafa47d 100644 --- a/rtu/dev/redstone.lua +++ b/rtu/dev/redstone.lua @@ -7,7 +7,7 @@ local digital_is_active = rsio.digital_is_active function redstone_rtu() local self = { - rtu = rtu_init() + rtu = rtu.rtu_init() } local rtu_interface = function () diff --git a/rtu/dev/turbine.lua b/rtu/dev/turbine.lua index 4ecc156..d5a6920 100644 --- a/rtu/dev/turbine.lua +++ b/rtu/dev/turbine.lua @@ -2,7 +2,7 @@ function turbine_rtu(turbine) local self = { - rtu = rtu_init(), + rtu = rtu.rtu_init(), turbine = turbine } From a6e1134dc3cf482aed6dc4b04f43b146ebd2ef3e Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 00:10:47 -0400 Subject: [PATCH 58/63] changed modbus init function name, fixed bugs with RTU startup, improved PPM debug prints --- rtu/startup.lua | 10 +++++----- scada-common/modbus.lua | 2 +- scada-common/ppm.lua | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rtu/startup.lua b/rtu/startup.lua index 59bc477..6a21753 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -72,9 +72,9 @@ for reactor_idx = 1, #rtu_redstone do local config = io_table[i] -- verify configuration - if is_valid_channel(config.channel) and is_valid_side(config.side) then + if rsio.is_valid_channel(config.channel) and rsio.is_valid_side(config.side) then if config.bundled_color then - valid = is_color(config.bundled_color) + valid = rsio.is_color(config.bundled_color) else valid = true end @@ -115,7 +115,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_init(rs_rtu) + modbus_io = modbus.new(rs_rtu) }) end @@ -158,7 +158,7 @@ for i = 1, #rtu_devices do reactor = rtu_devices[i].for_reactor, device = device, rtu = rtu_iface, - modbus_io = modbus_init(rtu_iface) + modbus_io = modbus.new(rtu_iface) }) log._debug("init> initialized RTU unit #" .. #units .. ": " .. rtu_devices[i].name .. " (" .. rtu_type .. ") [" .. @@ -211,7 +211,7 @@ while true do unit.rtu = imatrix_rtu(device) end - unit.modbus_io = modbus_init(unit.rtu) + unit.modbus_io = modbus.new(unit.rtu) println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index dc73e64..b192cc9 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -26,7 +26,7 @@ local MODBUS_EXCODE = { } -- new modbus comms handler object -function modbus_init(rtu_dev) +function new(rtu_dev) local self = { rtu = rtu_dev } diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index 3eb3977..f11f0e2 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -102,7 +102,7 @@ function mount_all() dev = pm_dev } - log._debug("PPM: found a " .. self.mounts[ifaces[i]].type) + log._debug("PPM: found a " .. self.mounts[ifaces[i]].type .. " (" .. ifaces[i] .. ")") end if #ifaces == 0 then From 7d9a664d38cfcc44a6fc74b7d2f86a95c769d5c2 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 00:11:23 -0400 Subject: [PATCH 59/63] rsio bugfixes --- scada-common/rsio.lua | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/scada-common/rsio.lua b/scada-common/rsio.lua index 9dbe9ef..f56a8a4 100644 --- a/scada-common/rsio.lua +++ b/scada-common/rsio.lua @@ -86,12 +86,14 @@ function to_string(channel) end function is_valid_channel(channel) - return channel > 0 and channel <= A_T_FLOW_RATE + return channel ~= nil and channel > 0 and channel <= RS_IO.A_T_FLOW_RATE end function is_valid_side(side) - for _, s in pairs(redstone.getSides()) do - if s == side then return true end + if side ~= nil then + for _, s in pairs(rs.getSides()) do + if s == side then return true end + end end return false end From 377cf8e6fc3c2c271123b982501cbf088d43e2ed Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 09:35:08 -0400 Subject: [PATCH 60/63] scope fixes, load comms api, debug prints --- rtu/rtu.lua | 12 ++++++++---- rtu/startup.lua | 10 ++++++++-- scada-common/modbus.lua | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 176b37d..8b83bfd 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -2,6 +2,10 @@ -- #REQUIRES modbus.lua -- #REQUIRES ppm.lua +local PROTOCOLS = comms.PROTOCOLS +local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES +local RTU_ADVERT_TYPES = comms.RTU_ADVERT_TYPES + function rtu_init() local self = { discrete_inputs = {}, @@ -130,7 +134,7 @@ function rtu_comms(modem, local_port, server_port) -- PRIVATE FUNCTIONS -- local _send = function (protocol, msg) - local packet = scada_packet() + local packet = comms.scada_packet() packet.make(self.seq_num, protocol, msg) self.modem.transmit(self.s_port, self.l_port, packet.raw()) self.seq_num = self.seq_num + 1 @@ -141,7 +145,7 @@ function rtu_comms(modem, local_port, server_port) -- parse a MODBUS/SCADA 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.recieve(side, sender, reply_to, message, distance) @@ -149,13 +153,13 @@ 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 = modbus.packet() if m_pkt.decode(s_pkt) then pkt = m_pkt.get() end -- get as SCADA management packet elseif s_pkt.protocol() == PROTOCOLS.SCADA_MGMT then - local mgmt_pkt = mgmt_packet() + local mgmt_pkt = comms.mgmt_packet() if mgmt_pkt.decode(s_pkt) then pkt = mgmt_packet.get() end diff --git a/rtu/startup.lua b/rtu/startup.lua index 6a21753..232cf8f 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -5,6 +5,7 @@ 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("scada-common/rsio.lua") @@ -16,7 +17,7 @@ os.loadAPI("dev/boiler.lua") os.loadAPI("dev/imatrix.lua") os.loadAPI("dev/turbine.lua") -local RTU_VERSION = "alpha-v0.1.1" +local RTU_VERSION = "alpha-v0.1.2" local print = util.print local println = util.println @@ -67,6 +68,8 @@ 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 .. "...") + for i = 1, #io_table do local valid = false local config = io_table[i] @@ -81,7 +84,8 @@ for reactor_idx = 1, #rtu_redstone do end if not valid then - local message = "init> invalid redstone definition at index " .. i + 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) else @@ -117,6 +121,8 @@ for reactor_idx = 1, #rtu_redstone do rtu = rs_rtu, modbus_io = modbus.new(rs_rtu) }) + + log._debug("init> initialized RTU unit #" .. #units .. ": redstone_io (redstone) [1] for reactor " .. rtu_redstone[reactor_idx].for_reactor) end -- mounted peripherals diff --git a/scada-common/modbus.lua b/scada-common/modbus.lua index b192cc9..8a4137f 100644 --- a/scada-common/modbus.lua +++ b/scada-common/modbus.lua @@ -264,7 +264,7 @@ function new(rtu_dev) } end -function modbus_packet() +function packet() local self = { frame = nil, txn_id = txn_id, From 2278469a8b4f92871dea7bea4fb7382e16464bce Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 10:09:44 -0400 Subject: [PATCH 61/63] refactored RTU devices --- rtu/dev/{boiler.lua => boiler_rtu.lua} | 2 +- rtu/dev/{imatrix.lua => imatrix_rtu.lua} | 2 +- rtu/dev/{redstone.lua => redstone_rtu.lua} | 2 +- rtu/dev/{turbine.lua => turbine_rtu.lua} | 2 +- rtu/startup.lua | 29 +++++++++------------- 5 files changed, 16 insertions(+), 21 deletions(-) rename rtu/dev/{boiler.lua => boiler_rtu.lua} (98%) rename rtu/dev/{imatrix.lua => imatrix_rtu.lua} (95%) rename rtu/dev/{redstone.lua => redstone_rtu.lua} (98%) rename rtu/dev/{turbine.lua => turbine_rtu.lua} (97%) diff --git a/rtu/dev/boiler.lua b/rtu/dev/boiler_rtu.lua similarity index 98% rename from rtu/dev/boiler.lua rename to rtu/dev/boiler_rtu.lua index b20cd0f..861a34f 100644 --- a/rtu/dev/boiler.lua +++ b/rtu/dev/boiler_rtu.lua @@ -1,6 +1,6 @@ -- #REQUIRES rtu.lua -function boiler_rtu(boiler) +function new(boiler) local self = { rtu = rtu.rtu_init(), boiler = boiler diff --git a/rtu/dev/imatrix.lua b/rtu/dev/imatrix_rtu.lua similarity index 95% rename from rtu/dev/imatrix.lua rename to rtu/dev/imatrix_rtu.lua index 39c647b..529a1f8 100644 --- a/rtu/dev/imatrix.lua +++ b/rtu/dev/imatrix_rtu.lua @@ -1,6 +1,6 @@ -- #REQUIRES rtu.lua -function imatrix_rtu(imatrix) +function new(imatrix) local self = { rtu = rtu.rtu_init(), imatrix = imatrix diff --git a/rtu/dev/redstone.lua b/rtu/dev/redstone_rtu.lua similarity index 98% rename from rtu/dev/redstone.lua rename to rtu/dev/redstone_rtu.lua index aafa47d..d81cebb 100644 --- a/rtu/dev/redstone.lua +++ b/rtu/dev/redstone_rtu.lua @@ -5,7 +5,7 @@ local digital_read = rsio.digital_read local digital_is_active = rsio.digital_is_active -function redstone_rtu() +function new() local self = { rtu = rtu.rtu_init() } diff --git a/rtu/dev/turbine.lua b/rtu/dev/turbine_rtu.lua similarity index 97% rename from rtu/dev/turbine.lua rename to rtu/dev/turbine_rtu.lua index d5a6920..7584270 100644 --- a/rtu/dev/turbine.lua +++ b/rtu/dev/turbine_rtu.lua @@ -1,6 +1,6 @@ -- #REQUIRES rtu.lua -function turbine_rtu(turbine) +function new(turbine) local self = { rtu = rtu.rtu_init(), turbine = turbine diff --git a/rtu/startup.lua b/rtu/startup.lua index 232cf8f..03d4873 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -12,23 +12,18 @@ os.loadAPI("scada-common/rsio.lua") os.loadAPI("config.lua") os.loadAPI("rtu.lua") -os.loadAPI("dev/redstone.lua") -os.loadAPI("dev/boiler.lua") -os.loadAPI("dev/imatrix.lua") -os.loadAPI("dev/turbine.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.1.2" +local RTU_VERSION = "alpha-v0.1.3" local print = util.print local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local redstone_rtu = redstone.redstone_rtu -local boiler_rtu = boiler.boiler_rtu -local turbine_rtu = turbine.turbine_rtu -local imatrix_rtu = imatrix.imatrix_rtu - log._info("========================================") log._info("BOOTING rtu.startup " .. RTU_VERSION) log._info("========================================") @@ -63,7 +58,7 @@ local rtu_devices = config.RTU_DEVICES -- redstone interfaces for reactor_idx = 1, #rtu_redstone do - local rs_rtu = redstone_rtu() + local rs_rtu = redstone_rtu.new() local io_table = rtu_redstone[reactor_idx].io local capabilities = {} @@ -141,15 +136,15 @@ for i = 1, #rtu_devices do if type == "boiler" then -- boiler multiblock rtu_type = "boiler" - rtu_iface = boiler_rtu(device) + rtu_iface = boiler_rtu.new(device) elseif type == "turbine" then -- turbine multiblock rtu_type = "turbine" - rtu_iface = turbine_rtu(device) + rtu_iface = turbine_rtu.new(device) elseif type == "mekanismMachine" then -- assumed to be an induction matrix multiblock rtu_type = "imatrix" - rtu_iface = imatrix_rtu(device) + rtu_iface = imatrix_rtu.new(device) else local message = "init> device '" .. rtu_devices[i].name .. "' is not a known type (" .. type .. ")" println_ts(message) @@ -210,11 +205,11 @@ while true do unit.device = device if unit.type == "boiler" then - unit.rtu = boiler_rtu(device) + unit.rtu = boiler_rtu.new(device) elseif unit.type == "turbine" then - unit.rtu = turbine_rtu(device) + unit.rtu = turbine_rtu.new(device) elseif unit.type == "imatrix" then - unit.rtu = imatrix_rtu(device) + unit.rtu = imatrix_rtu.new(device) end unit.modbus_io = modbus.new(unit.rtu) From 91079eeb780b9f89482a9985fa1cb8d22d8d636b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 10:21:29 -0400 Subject: [PATCH 62/63] fixed RTU comms bad function calls, fixed loop clock, changed terminate logic/prints --- rtu/rtu.lua | 6 +++--- rtu/startup.lua | 16 +++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 8b83bfd..efa31f2 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -180,7 +180,7 @@ function rtu_comms(modem, local_port, server_port) if packet.unit_id <= #units then local unit = units[packet.unit_id] local return_code, response = unit.modbus_io.handle_packet(packet) - _send(response, PROTOCOLS.MODBUS_TCP) + _send(PROTOCOLS.MODBUS_TCP, response) if not return_code then log._warning("MODBUS operation failed") @@ -249,7 +249,7 @@ function rtu_comms(modem, local_port, server_port) end end - _send(advertisement, PROTOCOLS.SCADA_MGMT) + _send(PROTOCOLS.SCADA_MGMT, advertisement) end local send_heartbeat = function () @@ -257,7 +257,7 @@ function rtu_comms(modem, local_port, server_port) type = SCADA_MGMT_TYPES.RTU_HEARTBEAT } - _send(heartbeat, PROTOCOLS.SCADA_MGMT) + _send(PROTOCOLS.SCADA_MGMT, heartbeat) end return { diff --git a/rtu/startup.lua b/rtu/startup.lua index 03d4873..8f03a35 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.1.3" +local RTU_VERSION = "alpha-v0.1.4" local print = util.print local println = util.println @@ -173,7 +173,7 @@ end ---------------------------------------- -- advertisement/heartbeat clock (every 2 seconds) -local loop_tick = os.startTimer(2) +local loop_clock = os.startTimer(2) -- event loop while true do @@ -217,7 +217,7 @@ while true do println_ts("reconnected the " .. unit.type .. " on interface " .. unit.name) end end - elseif event == "timer" and param1 == loop_tick then + elseif event == "timer" and param1 == loop_clock then -- period tick, if we are linked send heartbeat, if not send advertisement if linked then rtu_comms.send_heartbeat() @@ -225,6 +225,9 @@ 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 } @@ -235,7 +238,10 @@ while true do -- if linked, stop sending advertisements linked = link_ref.linked elseif event == "terminate" then - println_ts("exiting...") - return + log._warning("terminate requested, exiting...") + break end end + +println_ts("exited") +log._info("exited") From 6a5e0243be60f363b4a88590a6e916d291ef05d0 Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Mon, 18 Apr 2022 10:31:24 -0400 Subject: [PATCH 63/63] catch terminations that are caught by PPM --- reactor-plc/startup.lua | 19 +++++++++++++------ rtu/startup.lua | 7 +++++-- scada-common/ppm.lua | 14 ++++++++++++++ 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 869be2f..5692899 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.1.5" +local R_PLC_VERSION = "alpha-v0.1.6" local print = util.print local println = util.println @@ -271,7 +271,12 @@ while true do iss.trip_timeout() println_ts("server timeout, reactor disabled") log._warning("server timeout, reactor disabled") - elseif event == "terminate" then + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then + log._warning("terminate requested, exiting...") + -- safe exit if plc_state.init_ok then plc_state.scram = true @@ -282,9 +287,11 @@ while true do println_ts("exiting, reactor failed to disable") end end - -- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? - println_ts("exited") - log._info("terminate requested, exiting") - return + + break end end + +-- send an alarm: plc_comms.send_alarm(ALARMS.PLC_SHUTDOWN) ? +println_ts("exited") +log._info("exited") diff --git a/rtu/startup.lua b/rtu/startup.lua index 8f03a35..0e2bfaa 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.1.4" +local RTU_VERSION = "alpha-v0.1.5" local print = util.print local println = util.println @@ -237,7 +237,10 @@ while true do -- if linked, stop sending advertisements linked = link_ref.linked - elseif event == "terminate" then + end + + -- check for termination request + if event == "terminate" or ppm.should_terminate() then log._warning("terminate requested, exiting...") break end diff --git a/scada-common/ppm.lua b/scada-common/ppm.lua index f11f0e2..fd92b23 100644 --- a/scada-common/ppm.lua +++ b/scada-common/ppm.lua @@ -15,6 +15,7 @@ local self = { mounts = {}, auto_cf = false, faulted = false, + terminate = false, mute = false } @@ -38,9 +39,15 @@ local peri_init = function (device) else -- function failed self.faulted = true + if not mute then log._error("PPM: protected " .. key .. "() -> " .. result) end + + if result == "Terminated" then + self.terminate = true + end + return ACCESS_FAULT end end @@ -85,6 +92,13 @@ function clear_fault() self.faulted = false end +-- TERMINATION -- + +-- if a caught error was a termination request +function should_terminate() + return self.terminate +end + -- MOUNTING -- -- mount all available peripherals (clears mounts first)