diff --git a/.gitignore b/.gitignore index 200fed8..17ae161 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ _notes/ +program.sh \ No newline at end of file diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 914f7ee..51399a6 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -28,9 +28,8 @@ function iocontrol.init(conf, comms) auto_ready = false, auto_active = false, auto_ramping = false, + auto_saturated = false, auto_scram = false, - ---@todo not currently used or set - auto_scram_cause = "ok", ---@type auto_scram_cause num_units = conf.num_units, ---@type integer @@ -274,24 +273,26 @@ function iocontrol.update_facility_status(status) local ctl_status = status[1] - if type(ctl_status) == "table" then + if type(ctl_status) == "table" and (#ctl_status == 9) then fac.all_sys_ok = ctl_status[1] fac.auto_ready = ctl_status[2] fac.auto_active = ctl_status[3] > 0 fac.auto_ramping = ctl_status[4] - fac.auto_scram = ctl_status[5] - fac.status_line_1 = ctl_status[6] - fac.status_line_2 = ctl_status[7] + fac.auto_saturated = ctl_status[5] + fac.auto_scram = ctl_status[6] + fac.status_line_1 = ctl_status[7] + fac.status_line_2 = ctl_status[8] fac.ps.publish("all_sys_ok", fac.all_sys_ok) fac.ps.publish("auto_ready", fac.auto_ready) fac.ps.publish("auto_active", fac.auto_active) fac.ps.publish("auto_ramping", fac.auto_ramping) + fac.ps.publish("auto_saturated", fac.auto_saturated) fac.ps.publish("auto_scram", fac.auto_scram) fac.ps.publish("status_line_1", fac.status_line_1) fac.ps.publish("status_line_2", fac.status_line_2) - local group_map = ctl_status[8] + local group_map = ctl_status[9] if (type(group_map) == "table") and (#group_map == fac.num_units) then local names = { "Manual", "Primary", "Secondary", "Tertiary", "Backup" } @@ -302,7 +303,7 @@ function iocontrol.update_facility_status(status) end end else - log.debug(log_header .. "control status not a table") + log.debug(log_header .. "control status not a table or length mismatch") end -- RTU statuses diff --git a/coordinator/startup.lua b/coordinator/startup.lua index d9404f0..74ae70a 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local iocontrol = require("coordinator.iocontrol") local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") -local COORDINATOR_VERSION = "beta-v0.9.0" +local COORDINATOR_VERSION = "beta-v0.9.1" local print = util.print local println = util.println diff --git a/coordinator/ui/components/processctl.lua b/coordinator/ui/components/processctl.lua index 517cbb2..9b9b2a3 100644 --- a/coordinator/ui/components/processctl.lua +++ b/coordinator/ui/components/processctl.lua @@ -62,11 +62,13 @@ local function new_view(root, x, y) local auto_ready = IndicatorLight{parent=main,label="Configured Units Ready",colors=cpair(colors.green,colors.red)} local auto_act = IndicatorLight{parent=main,label="Process Active",colors=cpair(colors.green,colors.gray)} local auto_ramp = IndicatorLight{parent=main,label="Process Ramping",colors=cpair(colors.white,colors.gray),flash=true,period=period.BLINK_250_MS} + local auto_sat = IndicatorLight{parent=main,label="Max Burn Rate",colors=cpair(colors.yellow,colors.gray)} local auto_scram = IndicatorLight{parent=main,label="Automatic SCRAM",colors=cpair(colors.red,colors.gray),flash=true,period=period.BLINK_250_MS} facility.ps.subscribe("auto_ready", auto_ready.update) facility.ps.subscribe("auto_active", auto_act.update) facility.ps.subscribe("auto_ramping", auto_ramp.update) + facility.ps.subscribe("auto_saturated", auto_sat.update) facility.ps.subscribe("auto_scram", auto_scram.update) main.line_break() @@ -176,7 +178,7 @@ local function new_view(root, x, y) -- controls and status -- ------------------------- - local ctl_opts = { "Regulated", "Burn Rate", "Charge Level", "Generation Rate" } + local ctl_opts = { "Monitored Max Burn", "Combined Burn Rate", "Charge Level", "Generation Rate" } local mode = RadioButton{parent=proc,x=34,y=1,options=ctl_opts,callback=function()end,radio_colors=cpair(colors.purple,colors.black),radio_bg=colors.gray} facility.ps.subscribe("process_mode", mode.set_value) diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index 57c8471..a23ad3f 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -387,6 +387,19 @@ function plc.rps_init(reactor, is_formed) if not quiet then log.info("RPS: reset") end end + -- reset the automatic and timeout trip flags, then clear trip if that was the trip cause + function public.auto_reset() + self.state[state_keys.automatic] = false + self.state[state_keys.timeout] = false + + if self.trip_cause == rps_status_t.automatic or self.trip_cause == rps_status_t.timeout then + self.trip_cause = rps_status_t.ok + self.tripped = false + end + + log.info("RPS: auto reset") + end + return public end @@ -808,6 +821,10 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co -- reset the RPS status rps.reset() _send_ack(packet.type, true) + elseif packet.type == RPLC_TYPES.RPS_AUTO_RESET then + -- reset automatic SCRAM and timeout trips + rps.auto_reset() + _send_ack(packet.type, true) elseif packet.type == RPLC_TYPES.AUTO_BURN_RATE then -- automatic control requested a new burn rate if (packet.length == 3) and (type(packet.data[1]) == "number") and (type(packet.data[3]) == "number") then diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index 8ac0de2..207fec6 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -14,7 +14,7 @@ local config = require("reactor-plc.config") local plc = require("reactor-plc.plc") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "beta-v0.10.4" +local R_PLC_VERSION = "beta-v0.10.5" local print = util.print local println = util.println diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 69d2ff1..1c3c29f 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -266,6 +266,7 @@ function threads.thread__main(smem, init) -- this thread cannot be slept because it will miss events (namely "terminate" otherwise) if not plc_state.shutdown then log.info("main thread restarting now...") +---@diagnostic disable-next-line: param-type-mismatch util.push_event("clock_start") end end diff --git a/rtu/startup.lua b/rtu/startup.lua index dec02ab..975784d 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -25,7 +25,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "beta-v0.9.12" +local RTU_VERSION = "beta-v0.9.13" local rtu_t = types.rtu_t diff --git a/scada-common/comms.lua b/scada-common/comms.lua index b1a68f7..1234c31 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -12,7 +12,7 @@ local rtu_t = types.rtu_t local insert = table.insert -comms.version = "1.2.0" +comms.version = "1.3.0" ---@alias PROTOCOLS integer local PROTOCOLS = { @@ -34,7 +34,8 @@ local RPLC_TYPES = { RPS_STATUS = 6, -- RPS status RPS_ALARM = 7, -- RPS alarm broadcast RPS_RESET = 8, -- clear RPS trip (if in bad state, will trip immediately) - AUTO_BURN_RATE = 9 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited + RPS_AUTO_RESET = 9, -- clear RPS trip if it is just a timeout or auto scram + AUTO_BURN_RATE = 10 -- set an automatic burn rate, PLC will respond with status, enable toggle speed limited } ---@alias SCADA_MGMT_TYPES integer @@ -223,12 +224,12 @@ end function comms.modbus_packet() local self = { frame = nil, - raw = nil, - txn_id = nil, - length = nil, - unit_id = nil, - func_code = nil, - data = nil + raw = {}, + txn_id = -1, + length = 0, + unit_id = -1, + func_code = 0, + data = {} } ---@class modbus_packet @@ -312,11 +313,11 @@ end function comms.rplc_packet() local self = { frame = nil, - raw = nil, - id = nil, - type = nil, - length = nil, - body = nil + raw = {}, + id = 0, + type = -1, + length = 0, + data = {} } ---@class rplc_packet @@ -333,6 +334,7 @@ function comms.rplc_packet() self.type == RPLC_TYPES.RPS_STATUS or self.type == RPLC_TYPES.RPS_ALARM or self.type == RPLC_TYPES.RPS_RESET or + self.type == RPLC_TYPES.RPS_AUTO_RESET or self.type == RPLC_TYPES.AUTO_BURN_RATE end @@ -411,10 +413,10 @@ end function comms.mgmt_packet() local self = { frame = nil, - raw = nil, - type = nil, - length = nil, - data = nil + raw = {}, + type = -1, + length = 0, + data = {} } ---@class mgmt_packet @@ -500,10 +502,10 @@ end function comms.crdn_packet() local self = { frame = nil, - raw = nil, - type = nil, - length = nil, - data = nil + raw = {}, + type = -1, + length = 0, + data = {} } ---@class crdn_packet @@ -590,10 +592,10 @@ end function comms.capi_packet() local self = { frame = nil, - raw = nil, - type = nil, - length = nil, - data = nil + raw = {}, + type = -1, + length = 0, + data = {} } ---@class capi_packet diff --git a/scada-common/types.lua b/scada-common/types.lua index d4db1e9..eaa355c 100644 --- a/scada-common/types.lua +++ b/scada-common/types.lua @@ -37,6 +37,8 @@ types.TRI_FAIL = { ---@alias PROCESS integer types.PROCESS = { + UNIT_ALARM_IDLE = -2, + MATRIX_FAULT_IDLE = -1, INACTIVE = 0, SIMPLE = 1, BURN_RATE = 2, @@ -173,9 +175,6 @@ types.ALARM_STATE = { ---| "sys_fail" ---| "force_disabled" ----@alias auto_scram_cause ----| "ok" - ---@alias rtu_t string types.rtu_t = { redstone = "redstone", diff --git a/supervisor/session/facility.lua b/supervisor/session/facility.lua index 3245ac0..9d82477 100644 --- a/supervisor/session/facility.lua +++ b/supervisor/session/facility.lua @@ -12,13 +12,14 @@ local PROCESS = types.PROCESS -- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum) local POWER_PER_BLADE = util.joules_to_fe(7140) -local MAX_CHARGE = 0.99 +local HIGH_CHARGE = 1.0 local RE_ENABLE_CHARGE = 0.95 local AUTO_SCRAM = { NONE = 0, MATRIX_DC = 1, - MATRIX_FILL = 2 + MATRIX_FILL = 2, + CRIT_ALARM = 3 } local charge_Kp = 1.0 @@ -46,6 +47,7 @@ function facility.new(num_reactors, cooling_conf) units_ready = false, mode = PROCESS.INACTIVE, last_mode = PROCESS.INACTIVE, + return_mode = PROCESS.INACTIVE, mode_set = PROCESS.SIMPLE, max_burn_combined = 0.0, -- maximum burn rate to clamp at burn_target = 0.1, -- burn rate target for aggregate burn mode @@ -53,6 +55,7 @@ function facility.new(num_reactors, cooling_conf) gen_rate_target = 0, -- FE/t charge rate target group_map = { 0, 0, 0, 0 }, -- units -> group IDs prio_defs = { {}, {}, {}, {} }, -- priority definitions (each level is a table of units) + at_max_burn = false, ascram = false, ascram_reason = AUTO_SCRAM.NONE, -- closed loop control @@ -102,6 +105,7 @@ function facility.new(num_reactors, cooling_conf) -- split a burn rate among the reactors ---@param burn_rate number burn rate assignment ---@param ramp boolean true to ramp, false to set right away + ---@return integer unallocated local function _allocate_burn_rate(burn_rate, ramp) local unallocated = math.floor(burn_rate * 10) @@ -117,32 +121,38 @@ function facility.new(num_reactors, cooling_conf) splits[#units] = splits[#units] + (unallocated % #units) -- go through all reactor units in this group - for u = 1, #units do - local ctl = units[u].get_control_inf() ---@type unit_control + for id = 1, #units do + local u = units[id] ---@type reactor_unit + + local ctl = u.get_control_inf() + local lim_br10 = u.a_get_effective_limit() + local last = ctl.br10 - if splits[u] <= ctl.lim_br10 then - ctl.br10 = splits[u] + if splits[id] <= lim_br10 then + ctl.br10 = splits[id] else - ctl.br10 = ctl.lim_br10 + ctl.br10 = lim_br10 - if u < #units then - local remaining = #units - u + if id < #units then + local remaining = #units - id split = math.floor(unallocated / remaining) - for x = (u + 1), #units do splits[x] = split end + for x = (id + 1), #units do splits[x] = split end splits[#units] = splits[#units] + (unallocated % remaining) end end - unallocated = unallocated - ctl.br10 + unallocated = math.max(0, unallocated - ctl.br10) - if last ~= ctl.br10 then units[u].a_commit_br10(ramp) end + if last ~= ctl.br10 then + log.debug("unit " .. id .. ": set to " .. ctl.br10 .. " (was " .. last .. ")") + u.a_commit_br10(ramp) + end end end - - -- stop if fully allocated - if unallocated <= 0 then break end end + + return unallocated end -- PUBLIC FUNCTIONS -- @@ -215,10 +225,18 @@ function facility.new(num_reactors, cooling_conf) local now = util.time_s() local state_changed = self.mode ~= self.last_mode + local next_mode = self.mode -- once auto control is started, sort the priority sublists by limits if state_changed then + self.saturated = false + + log.debug("FAC: state changed from " .. self.last_mode .. " to " .. self.mode) + if self.last_mode == PROCESS.INACTIVE then + ---@todo change this to be a reset button + if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then self.ascram = false end + local blade_count = 0 self.max_burn_combined = 0.0 @@ -259,27 +277,31 @@ function facility.new(num_reactors, cooling_conf) self.initial_ramp = false end - if self.mode == PROCESS.INACTIVE then - -- check if we are ready to start when that time comes - self.units_ready = true - for i = 1, #self.prio_defs do - for _, u in pairs(self.prio_defs[i]) do - self.units_ready = self.units_ready and u.get_control_inf().ready - end + -- update unit ready state + self.units_ready = true + for i = 1, #self.prio_defs do + for _, u in pairs(self.prio_defs[i]) do + self.units_ready = self.units_ready and u.get_control_inf().ready end + end + -- perform mode-specific operations + if self.mode == PROCESS.INACTIVE then if self.units_ready then self.status_text = { "IDLE", "control disengaged" } else self.status_text = { "NOT READY", "assigned units not ready" } end elseif self.mode == PROCESS.SIMPLE then - -- run units at their last configured set point + -- run units at their limits if state_changed then self.time_start = now - ---@todo will still need to ramp? + self.saturated = true + self.status_text = { "MONITORED MODE", "running reactors at limit" } log.debug(util.c("FAC: SIMPLE mode first call completed")) end + + _allocate_burn_rate(self.max_burn_combined, true) elseif self.mode == PROCESS.BURN_RATE then -- a total aggregate burn rate if state_changed then @@ -294,7 +316,8 @@ function facility.new(num_reactors, cooling_conf) end if not self.waiting_on_ramp then - _allocate_burn_rate(self.burn_target, self.initial_ramp) + local unallocated = _allocate_burn_rate(self.burn_target, true) + self.saturated = self.burn_target == self.max_burn_combined or unallocated > 0 if self.initial_ramp then self.status_text = { "BURN RATE MODE", "ramping reactors" } @@ -397,16 +420,27 @@ function facility.new(num_reactors, cooling_conf) _allocate_burn_rate(sp_c, false) end + elseif self.mode == PROCESS.MATRIX_FAULT_IDLE then + -- exceeded charge, wait until condition clears + if self.ascram_reason == AUTO_SCRAM.NONE then + next_mode = self.return_mode + log.info("FAC: exiting matrix fault idle state due to fault resolution") + elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then + next_mode = PROCESS.INACTIVE + log.info("FAC: exiting matrix fault idle state due to critical unit alarm") + end + elseif self.mode == PROCESS.UNIT_ALARM_IDLE then + -- do nothing, wait for user to confirm (stop and reset) elseif self.mode ~= PROCESS.INACTIVE then log.error(util.c("FAC: unsupported process mode ", self.mode, ", switching to inactive")) - self.mode = PROCESS.INACTIVE + next_mode = PROCESS.INACTIVE end ------------------------------ -- Evaluate Automatic SCRAM -- ------------------------------ - if self.mode ~= PROCESS.INACTIVE then + if (self.mode ~= PROCESS.INACTIVE) and (self.mode ~= PROCESS.UNIT_ALARM_IDLE) then local scram = false if self.induction[1] ~= nil then @@ -415,37 +449,93 @@ function facility.new(num_reactors, cooling_conf) if self.ascram_reason == AUTO_SCRAM.MATRIX_DC then self.ascram_reason = AUTO_SCRAM.NONE + log.info("FAC: cleared automatic SCRAM trip due to prior induction matrix disconnect") end - if (db.tanks.energy_fill > MAX_CHARGE) or + if (db.tanks.energy_fill >= HIGH_CHARGE) or (self.ascram_reason == AUTO_SCRAM.MATRIX_FILL and db.tanks.energy_fill > RE_ENABLE_CHARGE) then scram = true + if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then + self.return_mode = self.mode + next_mode = PROCESS.MATRIX_FAULT_IDLE + end + if self.ascram_reason == AUTO_SCRAM.NONE then self.ascram_reason = AUTO_SCRAM.MATRIX_FILL end + elseif self.ascram_reason == AUTO_SCRAM.MATRIX_FILL then + log.info("FAC: charge state of induction matrix entered acceptable range <= " .. (RE_ENABLE_CHARGE * 100) .. "%") + self.ascram_reason = AUTO_SCRAM.NONE + end + + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + + if u.has_critical_alarm() then + scram = true + + if self.ascram_reason == AUTO_SCRAM.NONE then + self.ascram_reason = AUTO_SCRAM.CRIT_ALARM + end + + next_mode = PROCESS.UNIT_ALARM_IDLE + + log.info("FAC: emergency exit of process control due to critical unit alarm") + break + end end else scram = true + + if self.mode ~= PROCESS.MATRIX_FAULT_IDLE then + self.return_mode = self.mode + next_mode = PROCESS.MATRIX_FAULT_IDLE + end + if self.ascram_reason == AUTO_SCRAM.NONE then self.ascram_reason = AUTO_SCRAM.MATRIX_DC end end -- SCRAM all units - if not self.ascram and scram then + if (not self.ascram) and scram then for i = 1, #self.prio_defs do for _, u in pairs(self.prio_defs[i]) do u.a_scram() end end - self.ascram = true + if self.ascram_reason == AUTO_SCRAM.MATRIX_DC then + log.info("FAC: automatic SCRAM due to induction matrix disconnection") + self.status_text = { "AUTOMATIC SCRAM", "induction matrix disconnected" } + elseif self.ascram_reason == AUTO_SCRAM.MATRIX_FILL then + log.info("FAC: automatic SCRAM due to induction matrix high charge") + self.status_text = { "AUTOMATIC SCRAM", "induction matrix fill high" } + elseif self.ascram_reason == AUTO_SCRAM.CRIT_ALARM then + log.info("FAC: automatic SCRAM due to critical unit alarm") + self.status_text = { "AUTOMATIC SCRAM", "critical unit alarm tripped" } + else + log.error(util.c("FAC: automatic SCRAM reason (", self.ascram_reason, ") not set to a known value")) + end + end + + self.ascram = scram + + -- clear PLC SCRAM if we should + if not self.ascram then + self.ascram_reason = AUTO_SCRAM.NONE + + for i = 1, #self.units do + local u = self.units[i] ---@type reactor_unit + u.a_cond_rps_reset() + end end end - -- update last mode + -- update last mode and set next mode self.last_mode = self.mode + self.mode = next_mode end -- call the update function of all units in the facility @@ -580,6 +670,7 @@ function facility.new(num_reactors, cooling_conf) self.units_ready, self.mode, self.waiting_on_ramp, + self.at_max_burn or self.saturated, self.ascram, self.status_text[1], self.status_text[2], diff --git a/supervisor/session/plc.lua b/supervisor/session/plc.lua index 3cf6468..45e8bae 100644 --- a/supervisor/session/plc.lua +++ b/supervisor/session/plc.lua @@ -28,7 +28,8 @@ local PLC_S_CMDS = { SCRAM = 1, ASCRAM = 2, ENABLE = 3, - RPS_RESET = 4 + RPS_RESET = 4, + RPS_AUTO_RESET = 5 } local PLC_S_DATA = { @@ -445,18 +446,29 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) cmd = UNIT_COMMANDS.RESET_RPS, ack = ack }) + elseif pkt.type == RPLC_TYPES.RPS_AUTO_RESET then + -- RPS auto control reset acknowledgement + local ack = _get_ack(pkt) + if ack then + self.auto_scram = false + else + log.debug(log_header .. "RPS auto reset failed") + end elseif pkt.type == RPLC_TYPES.AUTO_BURN_RATE then if pkt.length == 1 then local ack = pkt.data[1] - self.acks.burn_rate = ack ~= PLC_AUTO_ACK.FAIL - - ---@todo implement error handling here if ack == PLC_AUTO_ACK.FAIL then - elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK then - elseif ack == PLC_AUTO_ACK.RAMP_SET_OK then - elseif ack == PLC_AUTO_ACK.ZERO_DIS_OK then + self.acks.burn_rate = false + log.debug(log_header .. "RPLC automatic burn rate set fail") + elseif ack == PLC_AUTO_ACK.DIRECT_SET_OK or ack == PLC_AUTO_ACK.RAMP_SET_OK or ack == PLC_AUTO_ACK.ZERO_DIS_OK then + self.acks.burn_rate = true elseif ack == PLC_AUTO_ACK.ZERO_DIS_WAIT then + self.acks.burn_rate = false + log.debug(log_header .. "RPLC automatic burn rate too soon to disable at 0 mB/t") + else + self.acks.burn_rate = false + log.debug(log_header .. "RPLC automatic burn rate ack unknown") end else log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch") @@ -614,6 +626,10 @@ function plc.new_session(id, for_reactor, in_queue, out_queue) self.acks.rps_reset = false self.retry_times.rps_reset_req = util.time() + INITIAL_WAIT _send(RPLC_TYPES.RPS_RESET, {}) + elseif cmd == PLC_S_CMDS.RPS_AUTO_RESET then + if self.auto_scram or self.sDB.rps_status.timeout then + _send(RPLC_TYPES.RPS_AUTO_RESET, {}) + end else log.warning(log_header .. "unsupported command received in in_queue (this is a bug)") end diff --git a/supervisor/session/unit.lua b/supervisor/session/unit.lua index ca2d640..fc22ed6 100644 --- a/supervisor/session/unit.lua +++ b/supervisor/session/unit.lua @@ -375,55 +375,6 @@ function unit.new(for_reactor, num_boilers, num_turbines) --#endregion - -- AUTO CONTROL -- - --#region - - -- engage automatic control - function public.a_engage() - self.db.annunciator.AutoControl = true - if self.plc_i ~= nil then - self.plc_i.auto_lock(true) - end - end - - -- disengage automatic control - function public.a_disengage() - self.db.annunciator.AutoControl = false - if self.plc_i ~= nil then - self.plc_i.auto_lock(false) - self.db.control.br10 = 0 - end - end - - -- set the automatic burn rate based on the last set br10 - ---@param ramp boolean true to ramp to rate, false to set right away - function public.a_commit_br10(ramp) - if self.db.annunciator.AutoControl then - if self.plc_i ~= nil then - self.plc_i.auto_set_burn(self.db.control.br10 / 10, ramp) - - if ramp then self.ramp_target_br10 = self.db.control.br10 end - end - end - end - - -- check if ramping is complete (burn rate is same as target) - ---@return boolean complete - function public.a_ramp_complete() - if self.plc_i ~= nil then - return self.plc_i.is_ramp_complete() or (self.plc_i.get_status().act_burn_rate == 0 and self.db.control.br10 == 0) - else return true end - end - - -- perform an automatic SCRAM - function public.a_scram() - if self.plc_s ~= nil then - self.plc_s.in_queue.push_command(PLC_S_CMDS.ASCRAM) - end - end - - --#endregion - -- UPDATE SESSION -- -- update (iterate) this unit @@ -444,6 +395,32 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- update degraded state for auto control self.db.control.degraded = (#self.boilers ~= num_boilers) or (#self.turbines ~= num_turbines) or (self.plc_i == nil) + -- check boilers formed/faulted + for i = 1, #self.boilers do + local sess = self.boilers[i] ---@type unit_session + local boiler = sess.get_db() ---@type boilerv_session_db + if sess.is_faulted() or not boiler.formed then + self.db.control.degraded = true + end + end + + -- check turbines formed/faulted + for i = 1, #self.turbines do + local sess = self.turbines[i] ---@type unit_session + local turbine = sess.get_db() ---@type turbinev_session_db + if sess.is_faulted() or not turbine.formed then + self.db.control.degraded = true + end + end + + -- check plc formed/faulted + if self.plc_i ~= nil then + local rps = self.plc_i.get_rps() + if rps.fault or rps.sys_fail then + self.db.control.degraded = true + end + end + -- update deltas _dt__compute_all() @@ -457,7 +434,82 @@ function unit.new(for_reactor, num_boilers, num_turbines) logic.update_status_text(self) end + -- AUTO CONTROL OPERATIONS -- + --#region + + -- engage automatic control + function public.a_engage() + self.db.annunciator.AutoControl = true + if self.plc_i ~= nil then + self.plc_i.auto_lock(true) + end + end + + -- disengage automatic control + function public.a_disengage() + self.db.annunciator.AutoControl = false + if self.plc_i ~= nil then + self.plc_i.auto_lock(false) + self.db.control.br10 = 0 + end + end + + -- get the actual limit of this unit + -- + -- if it is degraded or not ready, the limit will be 0 + ---@return integer lim_br10 + function public.a_get_effective_limit() + if not self.db.control.ready or self.db.control.degraded or self.plc_cache.rps_trip then + self.db.control.br10 = 0 + return 0 + else + return self.db.control.lim_br10 + end + end + + -- set the automatic burn rate based on the last set br10 + ---@param ramp boolean true to ramp to rate, false to set right away + function public.a_commit_br10(ramp) + if self.db.annunciator.AutoControl then + if self.plc_i ~= nil then + self.plc_i.auto_set_burn(self.db.control.br10 / 10, ramp) + + if ramp then self.ramp_target_br10 = self.db.control.br10 end + end + end + end + + -- check if ramping is complete (burn rate is same as target) + ---@return boolean complete + function public.a_ramp_complete() + if self.plc_i ~= nil then + return self.plc_i.is_ramp_complete() or + (self.plc_i.get_status().act_burn_rate == 0 and self.db.control.br10 == 0) or + public.a_get_effective_limit() == 0 + else return true end + end + + -- perform an automatic SCRAM + function public.a_scram() + if self.plc_s ~= nil then + self.plc_s.in_queue.push_command(PLC_S_CMDS.ASCRAM) + end + end + + -- queue a command to clear timeout/auto-scram if set + function public.a_cond_rps_reset() + if self.plc_s ~= nil and self.plc_i ~= nil then + local rps = self.plc_i.get_rps() + if rps.timeout or rps.automatic then + self.plc_s.in_queue.push_command(PLC_S_CMDS.RPS_AUTO_RESET) + end + end + end + + --#endregion + -- OPERATIONS -- + --#region -- queue a command to SCRAM the reactor function public.scram() @@ -537,7 +589,21 @@ function unit.new(for_reactor, num_boilers, num_turbines) end end + --#endregion + -- READ STATES/PROPERTIES -- + --#region + + -- check if a critical alarm is tripped + function public.has_critical_alarm() + for _, data in pairs(self.alarms) do + if data.tier == PRIO.CRITICAL and (data.state == AISTATE.TRIPPED or data.state == AISTATE.ACKED) then + return true + end + end + + return false + end -- get build properties of all machines function public.get_build() @@ -622,6 +688,8 @@ function unit.new(for_reactor, num_boilers, num_turbines) -- get the reactor ID function public.get_id() return self.r_id end + --#endregion + return public end diff --git a/supervisor/session/unitlogic.lua b/supervisor/session/unitlogic.lua index 8043a4b..ab96a11 100644 --- a/supervisor/session/unitlogic.lua +++ b/supervisor/session/unitlogic.lua @@ -516,11 +516,7 @@ function logic.update_status_text(self) elseif self.db.annunciator.WasteLineOcclusion then self.status_text[2] = "insufficient waste output rate" elseif (util.time_ms() - self.last_rate_change_ms) <= self.defs.FLOW_STABILITY_DELAY_MS then - if self.num_turbines > 1 then - self.status_text[2] = "turbines spinning up" - else - self.status_text[2] = "turbine spinning up" - end + self.status_text[2] = "awaiting flow stability" else self.status_text[2] = "system nominal" end diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 8b2e4e8..79282c0 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -14,7 +14,7 @@ local svsessions = require("supervisor.session.svsessions") local config = require("supervisor.config") local supervisor = require("supervisor.supervisor") -local SUPERVISOR_VERSION = "beta-v0.9.13" +local SUPERVISOR_VERSION = "beta-v0.9.14" local print = util.print local println = util.println