From faac421b63b6be16578e96df60f4d56998715c5b Mon Sep 17 00:00:00 2001 From: Mikayla Fischler Date: Tue, 10 May 2022 21:49:14 -0400 Subject: [PATCH] #47 reactor plc docs and bugfixes --- reactor-plc/plc.lua | 148 ++++++++++++++----------- reactor-plc/startup.lua | 12 ++- reactor-plc/threads.lua | 234 +++++++++++++++++++++------------------- 3 files changed, 219 insertions(+), 175 deletions(-) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 5b84c7f..04d63fd 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -1,3 +1,4 @@ +---@diagnostic disable: redefined-local local comms = require("scada-common.comms") local log = require("scada-common.log") local ppm = require("scada-common.ppm") @@ -18,9 +19,11 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts --- Reactor Protection System --- identifies dangerous states and SCRAMs reactor if warranted --- autonomous from main SCADA supervisor/coordinator control +--- RPS: Reactor Protection System +--- +--- identifies dangerous states and SCRAMs reactor if warranted +--- +--- autonomous from main SCADA supervisor/coordinator control plc.rps_init = function (reactor) local state_keys = { dmg_crit = 1, @@ -42,6 +45,9 @@ plc.rps_init = function (reactor) trip_cause = "" } + ---@class rps + local public = {} + -- PRIVATE FUNCTIONS -- -- set reactor access fault flag @@ -136,22 +142,28 @@ plc.rps_init = function (reactor) -- PUBLIC FUNCTIONS -- -- re-link a reactor after a peripheral re-connect - local reconnect_reactor = function (reactor) + public.reconnect_reactor = function (reactor) self.reactor = reactor end - -- report a PLC comms timeout - local trip_timeout = function () + -- trip for lost peripheral + public.trip_fault = function () + _set_fault() + end + + -- trip for a PLC comms timeout + public.trip_timeout = function () self.state[state_keys.timed_out] = true end -- manually SCRAM the reactor - local trip_manual = function () + public.trip_manual = function () self.state[state_keys.manual] = true end -- SCRAM the reactor now - local scram = function () + ---@return boolean success + public.scram = function () log.info("RPS: reactor SCRAM") self.reactor.scram() @@ -165,7 +177,8 @@ plc.rps_init = function (reactor) end -- start the reactor - local activate = function () + ---@return boolean success + public.activate = function () if not self.tripped then log.info("RPS: reactor start") @@ -182,7 +195,8 @@ plc.rps_init = function (reactor) end -- check all safety conditions - local check = function () + ---@return boolean tripped, rps_status_t trip_status, boolean first_trip + public.check = function () local status = rps_status_t.ok local was_tripped = self.tripped local first_trip = false @@ -237,38 +251,37 @@ plc.rps_init = function (reactor) self.tripped = true self.trip_cause = status - scram() + public.scram() end return self.tripped, status, first_trip end - -- get the RPS status - local status = function () return self.state end - local is_tripped = function () return self.tripped end - local is_active = function () return self.reactor_enabled end + public.status = function () return self.state end + public.is_tripped = function () return self.tripped end + public.is_active = function () return self.reactor_enabled end -- reset the RPS - local reset = function () + public.reset = function () self.tripped = false self.trip_cause = rps_status_t.ok + + for i = 1, #self.state do + self.state[i] = false + end end - return { - reconnect_reactor = reconnect_reactor, - trip_timeout = trip_timeout, - trip_manual = trip_manual, - scram = scram, - activate = activate, - check = check, - status = status, - is_tripped = is_tripped, - is_active = is_active, - reset = reset - } + return public end --- reactor PLC communications +-- Reactor PLC Communications +---@param id integer +---@param modem table +---@param local_port integer +---@param server_port integer +---@param reactor table +---@param rps rps +---@param conn_watchdog watchdog plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_watchdog) local self = { id = id, @@ -286,6 +299,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat max_burn_rate = nil } + ---@class plc_comms + local public = {} + -- open modem if not self.modem.isOpen(self.l_port) then self.modem.open(self.l_port) @@ -293,6 +309,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- PRIVATE FUNCTIONS -- + -- send an RPLC packet + ---@param msg_type RPLC_TYPES + ---@param msg string local _send = function (msg_type, msg) local s_pkt = comms.scada_packet() local r_pkt = comms.rplc_packet() @@ -304,6 +323,9 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat self.seq_num = self.seq_num + 1 end + -- send a SCADA management packet + ---@param msg_type SCADA_MGMT_TYPES + ---@param msg string local _send_mgmt = function (msg_type, msg) local s_pkt = comms.scada_packet() local m_pkt = comms.mgmt_packet() @@ -316,6 +338,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- variable reactor status information, excluding heating rate + ---@return table data_table, boolean faulted local _reactor_status = function () local coolant = nil local hcoolant = nil @@ -373,6 +396,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat return data_table, self.reactor.__p_is_faulted() end + -- update the status cache if changed + ---@return boolean changed local _update_status_cache = function () local status, faulted = _reactor_status() local changed = false @@ -398,11 +423,14 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- keep alive ack + ---@param srv_time integer local _send_keep_alive_ack = function (srv_time) _send(SCADA_MGMT_TYPES.KEEP_ALIVE, { srv_time, util.time() }) end -- general ack + ---@param msg_type RPLC_TYPES + ---@param succeeded boolean local _send_ack = function (msg_type, succeeded) _send(msg_type, { succeeded }) end @@ -434,7 +462,8 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat -- PUBLIC FUNCTIONS -- -- reconnect a newly connected modem - local reconnect_modem = function (modem) + ---@param modem table + public.reconnect_modem = function (modem) self.modem = modem -- open modem @@ -444,32 +473,34 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- reconnect a newly connected reactor - local reconnect_reactor = function (reactor) + ---@param reactor table + public.reconnect_reactor = function (reactor) self.reactor = reactor self.status_cache = nil end -- unlink from the server - local unlink = function () + public.unlink = function () self.linked = false self.r_seq_num = nil self.status_cache = nil end -- close the connection to the server - local close = function () + public.close = function () self.conn_watchdog.cancel() - unlink() + public.unlink() _send_mgmt(SCADA_MGMT_TYPES.CLOSE, {}) end -- attempt to establish link with supervisor - local send_link_req = function () + public.send_link_req = function () _send(RPLC_TYPES.LINK_REQ, { self.id }) end -- send live status information - local send_status = function (degraded) + ---@param degraded boolean + public.send_status = function (degraded) if self.linked then local mek_data = nil @@ -495,14 +526,15 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- send reactor protection system status - local send_rps_status = function () + public.send_rps_status = function () if self.linked then _send(RPLC_TYPES.RPS_STATUS, rps.status()) end end -- send reactor protection system alarm - local send_rps_alarm = function (cause) + ---@param cause rps_status_t + public.send_rps_alarm = function (cause) if self.linked then local rps_alarm = { cause, @@ -514,7 +546,13 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- parse an RPLC packet - local parse_packet = function(side, sender, reply_to, message, distance) + ---@param side string + ---@param sender integer + ---@param reply_to integer + ---@param message any + ---@param distance integer + ---@return rplc_frame|mgmt_frame|nil packet + public.parse_packet = function(side, sender, reply_to, message, distance) local pkt = nil local s_pkt = comms.scada_packet() @@ -543,7 +581,10 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end -- handle an RPLC packet - local handle_packet = function (packet, plc_state, setpoints) + ---@param packet rplc_frame|mgmt_frame + ---@param plc_state plc_state + ---@param setpoints setpoints + public.handle_packet = function (packet, plc_state, setpoints) if packet ~= nil then -- check sequence number if self.r_seq_num == nil then @@ -573,7 +614,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat if link_ack == RPLC_LINKING.ALLOW then self.status_cache = nil _send_struct() - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("re-sent initial status data") elseif link_ack == RPLC_LINKING.DENY then println_ts("received unsolicited link denial, unlinking") @@ -593,7 +634,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat elseif packet.type == RPLC_TYPES.STATUS then -- request of full status, clear cache first self.status_cache = nil - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("sent out status cache again, did supervisor miss it?") elseif packet.type == RPLC_TYPES.MEK_STRUCT then -- request for physical structure @@ -659,7 +700,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat self.status_cache = nil _send_struct() - send_status(plc_state.degraded) + public.send_status(plc_state.degraded) log.debug("sent initial status data") elseif link_ack == RPLC_LINKING.DENY then @@ -700,7 +741,7 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat elseif packet.type == SCADA_MGMT_TYPES.CLOSE then -- handle session close self.conn_watchdog.cancel() - unlink() + public.unlink() println_ts("server connection closed by remote host") log.warning("server connection closed by remote host") else @@ -713,23 +754,10 @@ plc.comms = function (id, modem, local_port, server_port, reactor, rps, conn_wat end end - local is_scrammed = function () return self.scrammed end - local is_linked = function () return self.linked end + public.is_scrammed = function () return self.scrammed end + public.is_linked = function () return self.linked end - return { - reconnect_modem = reconnect_modem, - reconnect_reactor = reconnect_reactor, - unlink = unlink, - close = close, - send_link_req = send_link_req, - send_status = send_status, - send_rps_status = send_rps_status, - send_rps_alarm = send_rps_alarm, - parse_packet = parse_packet, - handle_packet = handle_packet, - is_scrammed = is_scrammed, - is_linked = is_linked - } + return public end return plc diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 1082afd..9d79a9c 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -29,11 +29,13 @@ println(">> Reactor PLC " .. R_PLC_VERSION .. " <<") ppm.mount_all() -- shared memory across threads +---@class plc_shared_memory local __shared_memory = { -- networked setting - networked = config.NETWORKED, + networked = config.NETWORKED, ---@type boolean -- PLC system state flags + ---@class plc_state plc_state = { init_ok = true, shutdown = false, @@ -42,6 +44,8 @@ local __shared_memory = { no_modem = false }, + -- control setpoints + ---@class setpoints setpoints = { burn_rate_en = false, burn_rate = 0.0 @@ -55,9 +59,9 @@ local __shared_memory = { -- system objects plc_sys = { - rps = nil, - plc_comms = nil, - conn_watchdog = nil + rps = nil, ---@type rps + plc_comms = nil, ---@type plc_comms + conn_watchdog = nil ---@type watchdog }, -- message queues diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 7231f2a..9531792 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -10,8 +10,6 @@ local println = util.println local print_ts = util.print_ts local println_ts = util.println_ts -local psleep = util.psleep - local MAIN_CLOCK = 1 -- (1Hz, 20 ticks) local RPS_SLEEP = 250 -- (250ms, 5 ticks) local COMMS_SLEEP = 150 -- (150ms, 3 ticks) @@ -30,6 +28,8 @@ local MQ__COMM_CMD = { } -- main thread +---@param smem plc_shared_memory +---@param init function threads.thread__main = function (smem, init) -- execute thread local exec = function () @@ -47,7 +47,7 @@ threads.thread__main = function (smem, init) local plc_dev = smem.plc_dev local rps = smem.plc_sys.rps local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog ---@type watchdog + local conn_watchdog = smem.plc_sys.conn_watchdog -- event loop while true do @@ -187,6 +187,7 @@ threads.thread__main = function (smem, init) end -- RPS operation thread +---@param smem plc_shared_memory threads.thread__rps = function (smem) -- execute thread local exec = function () @@ -224,6 +225,7 @@ threads.thread__rps = function (smem) -- if we tried to SCRAM but failed, keep trying -- in that case, SCRAM won't be called until it reconnects (this is the expected use of this check) +---@diagnostic disable-next-line: need-check-nil if not plc_state.no_reactor and rps.is_tripped() and reactor.getStatus() then rps.scram() end @@ -249,26 +251,28 @@ threads.thread__rps = function (smem) while rps_queue.ready() and not plc_state.shutdown do local msg = rps_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - if plc_state.init_ok then - if msg.message == MQ__RPS_CMD.SCRAM then - -- SCRAM - rps.scram() - elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then - -- lost peripheral(s) - rps.trip_degraded() - elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then - -- watchdog tripped - rps.trip_timeout() - println_ts("server timeout") - log.warning("server timeout") + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if plc_state.init_ok then + if msg.message == MQ__RPS_CMD.SCRAM then + -- SCRAM + rps.scram() + elseif msg.message == MQ__RPS_CMD.DEGRADED_SCRAM then + -- lost peripheral(s) + rps.trip_fault() + elseif msg.message == MQ__RPS_CMD.TRIP_TIMEOUT then + -- watchdog tripped + rps.trip_timeout() + println_ts("server timeout") + log.warning("server timeout") + end end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet end - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet end -- quick yield @@ -301,6 +305,7 @@ threads.thread__rps = function (smem) end -- communications sender thread +---@param smem plc_shared_memory threads.thread__comms_tx = function (smem) -- execute thread local exec = function () @@ -320,17 +325,19 @@ threads.thread__comms_tx = function (smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - if msg.message == MQ__COMM_CMD.SEND_STATUS then - -- send PLC/RPS status - plc_comms.send_status(plc_state.degraded) - plc_comms.send_rps_status() + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + if msg.message == MQ__COMM_CMD.SEND_STATUS then + -- send PLC/RPS status + plc_comms.send_status(plc_state.degraded) + plc_comms.send_rps_status() + end + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet end - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet end -- quick yield @@ -352,22 +359,20 @@ threads.thread__comms_tx = function (smem) end -- communications handler thread +---@param smem plc_shared_memory threads.thread__comms_rx = function (smem) -- execute thread local exec = function () log.debug("comms rx thread start") -- load in from shared memory - local plc_state = smem.plc_state - local setpoints = smem.setpoints - local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps - local plc_comms = smem.plc_sys.plc_comms - local conn_watchdog = smem.plc_sys.conn_watchdog + local plc_state = smem.plc_state + local setpoints = smem.setpoints + local plc_comms = smem.plc_sys.plc_comms - local comms_queue = smem.q.mq_comms_rx + local comms_queue = smem.q.mq_comms_rx - local last_update = util.time() + local last_update = util.time() -- thread loop while true do @@ -375,16 +380,17 @@ threads.thread__comms_rx = function (smem) while comms_queue.ready() and not plc_state.shutdown do local msg = comms_queue.pop() - if msg.qtype == mqueue.TYPE.COMMAND then - -- received a command - elseif msg.qtype == mqueue.TYPE.DATA then - -- received data - elseif msg.qtype == mqueue.TYPE.PACKET then - -- received a packet - -- handle the packet (setpoints passed to update burn rate setpoint) - -- (plc_state passed to check if degraded) - -- (conn_watchdog passed to allow feeding the watchdog) - plc_comms.handle_packet(msg.message, setpoints, plc_state, conn_watchdog) + if msg ~= nil then + if msg.qtype == mqueue.TYPE.COMMAND then + -- received a command + elseif msg.qtype == mqueue.TYPE.DATA then + -- received data + elseif msg.qtype == mqueue.TYPE.PACKET then + -- received a packet + -- handle the packet (setpoints passed to update burn rate setpoint) + -- (plc_state passed to check if degraded) + plc_comms.handle_packet(msg.message, setpoints, plc_state) + end end -- quick yield @@ -406,84 +412,90 @@ threads.thread__comms_rx = function (smem) end -- apply setpoints +---@param smem plc_shared_memory threads.thread__setpoint_control = function (smem) -- execute thread local exec = function () log.debug("setpoint control thread start") -- load in from shared memory - local plc_state = smem.plc_state - local setpoints = smem.setpoints - local plc_dev = smem.plc_dev - local rps = smem.plc_sys.rps + local plc_state = smem.plc_state + local setpoints = smem.setpoints + local plc_dev = smem.plc_dev + local rps = smem.plc_sys.rps - local last_update = util.time() - local running = false + local last_update = util.time() + local running = false - local last_sp_burn = 0 + local last_sp_burn = 0.0 + + -- do not use the actual elapsed time, it could spike + -- we do not want to have big jumps as that is what we are trying to avoid in the first place + local min_elapsed_s = SP_CTRL_SLEEP / 1000.0 -- thread loop while true do local reactor = plc_dev.reactor - -- check if we should start ramping - if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then - if rps.is_active() then - if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then - -- update without ramp if <= 5 mB/t change - log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") - reactor.setBurnRate(setpoints.burn_rate) - else - log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") - running = true - end - - last_sp_burn = setpoints.burn_rate - else - last_sp_burn = 0 - end - end - - -- only check I/O if active to save on processing time - if running then - -- do not use the actual elapsed time, it could spike - -- we do not want to have big jumps as that is what we are trying to avoid in the first place - local min_elapsed_s = SP_CTRL_SLEEP / 1000.0 - - -- clear so we can later evaluate if we should keep running - running = false - - -- adjust burn rate (setpoints.burn_rate) - if setpoints.burn_rate_en then + if plc_state.init_ok and not plc_state.no_reactor then + -- check if we should start ramping + if setpoints.burn_rate_en and setpoints.burn_rate ~= last_sp_burn then if rps.is_active() then - local current_burn_rate = reactor.getBurnRate() - - -- we yielded, check enable again - if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then - -- calculate new burn rate - local new_burn_rate = current_burn_rate - - if setpoints.burn_rate > current_burn_rate then - -- need to ramp up - local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate > setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate - end - else - -- need to ramp down - local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) - if new_burn_rate < setpoints.burn_rate then - new_burn_rate = setpoints.burn_rate - end - end - - -- set the burn rate - reactor.setBurnRate(new_burn_rate) - - running = running or (new_burn_rate ~= setpoints.burn_rate) + if math.abs(setpoints.burn_rate - last_sp_burn) <= 5 then + -- update without ramp if <= 5 mB/t change + log.debug("setting burn rate directly to " .. setpoints.burn_rate .. "mB/t") +---@diagnostic disable-next-line: need-check-nil + reactor.setBurnRate(setpoints.burn_rate) + else + log.debug("starting burn rate ramp from " .. last_sp_burn .. "mB/t to " .. setpoints.burn_rate .. "mB/t") + running = true end + + last_sp_burn = setpoints.burn_rate else - last_sp_burn = 0 + last_sp_burn = 0.0 + end + end + + -- only check I/O if active to save on processing time + if running then + -- clear so we can later evaluate if we should keep running + running = false + + -- adjust burn rate (setpoints.burn_rate) + if setpoints.burn_rate_en then + if rps.is_active() then +---@diagnostic disable-next-line: need-check-nil + local current_burn_rate = reactor.getBurnRate() + + -- we yielded, check enable again + if setpoints.burn_rate_en and (current_burn_rate ~= ppm.ACCESS_FAULT) and (current_burn_rate ~= setpoints.burn_rate) then + -- calculate new burn rate + local new_burn_rate = current_burn_rate + + if setpoints.burn_rate > current_burn_rate then + -- need to ramp up + local new_burn_rate = current_burn_rate + (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate > setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + else + -- need to ramp down + local new_burn_rate = current_burn_rate - (BURN_RATE_RAMP_mB_s * min_elapsed_s) + if new_burn_rate < setpoints.burn_rate then + new_burn_rate = setpoints.burn_rate + end + end + + -- set the burn rate +---@diagnostic disable-next-line: need-check-nil + reactor.setBurnRate(new_burn_rate) + + running = running or (new_burn_rate ~= setpoints.burn_rate) + end + else + last_sp_burn = 0.0 + end end end end