diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index 40d71d8..de92ef0 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -89,7 +89,7 @@ function coordinator.load_config() if type(config.AuthKey) == "string" then local len = string.len(config.AuthKey) - cfv.assert_eq(len == 0 or len >= 8, true) + cfv.assert(len == 0 or len >= 8) end cfv.assert_type_int(config.LogMode) diff --git a/coordinator/renderer.lua b/coordinator/renderer.lua index 6920845..f2d2418 100644 --- a/coordinator/renderer.lua +++ b/coordinator/renderer.lua @@ -254,6 +254,11 @@ function renderer.close_ui() -- clear unit monitors for _, monitor in ipairs(engine.monitors.unit_displays) do monitor.clear() end + if not engine.disable_flow_view then + -- clear flow monitor + engine.monitors.flow.clear() + end + -- re-draw dmesg engine.dmesg_window.setVisible(true) engine.dmesg_window.redraw() diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 236fa79..0d89a87 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.4.0" +local COORDINATOR_VERSION = "v1.4.2" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/threads.lua b/coordinator/threads.lua index 3b04b5b..e7c2d8c 100644 --- a/coordinator/threads.lua +++ b/coordinator/threads.lua @@ -133,6 +133,7 @@ function threads.thread__main(smem) local ok, start_ui = coord_comms.try_connect() if not ok then crd_state.link_fail = true + crd_state.shutdown = true log_sys("supervisor connection failed, shutting down...") log.fatal("failed to connect to supervisor") break @@ -271,6 +272,13 @@ function threads.thread__render(smem) if msg.qtype == mqueue.TYPE.COMMAND then -- received a command if msg.message == MQ__RENDER_CMD.START_MAIN_UI then + -- stop the UI if it was already started + -- this may occur on a quick supervisor disconnect -> connect + if renderer.ui_ready() then + log_render("closing main UI before executing new request to start") + renderer.close_ui() + end + -- start up the main UI log_render("starting main UI...") diff --git a/coordinator/ui/components/process_ctl.lua b/coordinator/ui/components/process_ctl.lua index fb0c939..430409b 100644 --- a/coordinator/ui/components/process_ctl.lua +++ b/coordinator/ui/components/process_ctl.lua @@ -145,7 +145,7 @@ local function new_view(root, x, y) local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="MFE",commas=true,lu_colors=black,width=23,fg_bg=blk_brn} c_target.register(facility.ps, "process_charge_target", c_target.set_value) - cur_charge.register(facility.induction_ps_tbl[1], "energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000000) end) + cur_charge.register(facility.induction_ps_tbl[1], "avg_charge", function (fe) cur_charge.update(fe / 1000000) end) local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=blk_pur} TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2} diff --git a/reactor-plc/plc.lua b/reactor-plc/plc.lua index b5c299d..fb906a3 100644 --- a/reactor-plc/plc.lua +++ b/reactor-plc/plc.lua @@ -74,7 +74,7 @@ function plc.load_config() if type(config.AuthKey) == "string" then local len = string.len(config.AuthKey) - cfv.assert_eq(len == 0 or len >= 8, true) + cfv.assert(len == 0 or len >= 8) end end diff --git a/reactor-plc/startup.lua b/reactor-plc/startup.lua index d610639..09994cb 100644 --- a/reactor-plc/startup.lua +++ b/reactor-plc/startup.lua @@ -18,7 +18,7 @@ local plc = require("reactor-plc.plc") local renderer = require("reactor-plc.renderer") local threads = require("reactor-plc.threads") -local R_PLC_VERSION = "v1.7.5" +local R_PLC_VERSION = "v1.7.7" local println = util.println local println_ts = util.println_ts diff --git a/reactor-plc/threads.lua b/reactor-plc/threads.lua index 8071fd8..6ccf43e 100644 --- a/reactor-plc/threads.lua +++ b/reactor-plc/threads.lua @@ -662,8 +662,9 @@ function threads.thread__setpoint_control(smem) if (type(cur_burn_rate) == "number") and (setpoints.burn_rate ~= cur_burn_rate) and rps.is_active() then last_burn_sp = setpoints.burn_rate - -- update without ramp if <= 2.5 mB/t change - running = math.abs(setpoints.burn_rate - cur_burn_rate) > 2.5 + -- update without ramp if <= 2.5 mB/t increase + -- no need to ramp down, as the ramp up poses the safety risks + running = (setpoints.burn_rate - cur_burn_rate) > 2.5 if running then log.debug(util.c("SPCTL: starting burn rate ramp from ", cur_burn_rate, " mB/t to ", setpoints.burn_rate, " mB/t")) diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 7e00b98..71cea40 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -60,7 +60,7 @@ function rtu.load_config() if type(config.AuthKey) == "string" then local len = string.len(config.AuthKey) - cfv.assert_eq(len == 0 or len >= 8, true) + cfv.assert(len == 0 or len >= 8) end cfv.assert_type_int(config.LogMode) diff --git a/rtu/startup.lua b/rtu/startup.lua index 4d7ecbd..9470124 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,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 = "v1.9.3" +local RTU_VERSION = "v1.9.4" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE diff --git a/scada-common/constants.lua b/scada-common/constants.lua index eaf6dd3..20925bd 100644 --- a/scada-common/constants.lua +++ b/scada-common/constants.lua @@ -68,19 +68,32 @@ constants.ALARM_LIMITS = alarms --#region Supervisor Constants --- milliseconds until turbine flow is assumed to be stable enough to enable coolant checks -constants.FLOW_STABILITY_DELAY_MS = 15000 +-- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks +constants.FLOW_STABILITY_DELAY_MS = 10000 -- Notes on Radiation -- - background radiation 0.0000001 Sv/h (99.99 nSv/h) -- - "green tint" radiation 0.00001 Sv/h (10 uSv/h) -- - damaging radiation 0.00006 Sv/h (60 uSv/h) -constants.LOW_RADIATION = 0.00001 -constants.HAZARD_RADIATION = 0.00006 -constants.HIGH_RADIATION = 0.001 +constants.LOW_RADIATION = 0.00001 +constants.HAZARD_RADIATION = 0.00006 +constants.HIGH_RADIATION = 0.001 constants.VERY_HIGH_RADIATION = 0.1 -constants.SEVERE_RADIATION = 8.0 -constants.EXTREME_RADIATION = 100.0 +constants.SEVERE_RADIATION = 8.0 +constants.EXTREME_RADIATION = 100.0 + +--#endregion + +--#region Mekanism Configuration Constants + +---@class _mek_constants +local mek = {} + +mek.TURBINE_GAS_PER_TANK = 64000 -- mekanism: turbineGasPerTank +mek.TURBINE_DISPERSER_FLOW = 1280 -- mekanism: turbineDisperserGasFlow +mek.TURBINE_VENT_FLOW = 32000 -- mekanism: turbineVentGasFlow + +constants.mek = mek --#endregion diff --git a/supervisor/configure.lua b/supervisor/configure.lua index cd790e7..8cad070 100644 --- a/supervisor/configure.lua +++ b/supervisor/configure.lua @@ -91,6 +91,7 @@ local tmp_cfg = { CoolingConfig = {}, FacilityTankMode = 0, FacilityTankDefs = {}, + ExtChargeIdling = false, SVR_Channel = nil, ---@type integer PLC_Channel = nil, ---@type integer RTU_Channel = nil, ---@type integer @@ -120,6 +121,7 @@ local fields = { { "CoolingConfig", "Cooling Configuration", {} }, { "FacilityTankMode", "Facility Tank Mode", 0 }, { "FacilityTankDefs", "Facility Tank Definitions", {} }, + { "ExtChargeIdling", "Extended Charge Idling", false }, { "SVR_Channel", "SVR Channel", 16240 }, { "PLC_Channel", "PLC Channel", 16241 }, { "RTU_Channel", "RTU Channel", 16242 }, @@ -222,8 +224,9 @@ local function config_view(display) local svr_c_4 = Div{parent=svr_cfg,x=2,y=4,width=49} local svr_c_5 = Div{parent=svr_cfg,x=2,y=4,width=49} local svr_c_6 = Div{parent=svr_cfg,x=2,y=4,width=49} + local svr_c_7 = Div{parent=svr_cfg,x=2,y=4,width=49} - local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6}} + local svr_pane = MultiPane{parent=svr_cfg,x=1,y=4,panes={svr_c_1,svr_c_2,svr_c_3,svr_c_4,svr_c_5,svr_c_6,svr_c_7}} TextBox{parent=svr_cfg,x=1,y=2,height=1,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)} @@ -329,7 +332,7 @@ local function config_view(display) else tmp_cfg.FacilityTankMode = 0 tmp_cfg.FacilityTankDefs = {} - main_pane.set_value(3) + svr_pane.set_value(7) end end @@ -563,7 +566,7 @@ local function config_view(display) local function submit_mode() tmp_cfg.FacilityTankMode = tank_mode.get_value() - main_pane.set_value(3) + svr_pane.set_value(7) end PushButton{parent=svr_c_5,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(4)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} @@ -577,6 +580,23 @@ local function config_view(display) PushButton{parent=svr_c_6,x=1,y=14,text="\x1b Back",callback=function()svr_pane.set_value(5)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + TextBox{parent=svr_c_7,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."} + TextBox{parent=svr_c_7,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."} + + local ext_idling = CheckBox{parent=svr_c_7,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)} + + local function back_from_idling() + svr_pane.set_value(util.trinary(tmp_cfg.FacilityTankMode == 0, 3, 5)) + end + + local function submit_idling() + tmp_cfg.ExtChargeIdling = ext_idling.get_value() + main_pane.set_value(3) + end + + PushButton{parent=svr_c_7,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + PushButton{parent=svr_c_7,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg} + --#endregion --#region Network diff --git a/supervisor/facility.lua b/supervisor/facility.lua index 43c6cc4..33cd267 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -23,7 +23,7 @@ local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local WASTE_MODE = types.WASTE_MODE local WASTE = types.WASTE_PRODUCT -local IO = rsio.IO +local IO = rsio.IO local DTV_RTU_S_DATA = qtypes.DTV_RTU_S_DATA @@ -50,9 +50,9 @@ local START_STATUS = { BLADE_MISMATCH = 2 } -local charge_Kp = 0.275 +local charge_Kp = 0.15 local charge_Ki = 0.0 -local charge_Kd = 4.5 +local charge_Kd = 0.6 local rate_Kp = 2.45 local rate_Ki = 0.4825 @@ -63,9 +63,9 @@ local facility = {} -- create a new facility management object ---@nodiscard ----@param num_reactors integer number of reactor units +---@param config svr_config supervisor configuration ---@param cooling_conf sv_cooling_conf cooling configurations of reactor units -function facility.new(num_reactors, cooling_conf) +function facility.new(config, cooling_conf) local self = { units = {}, status_text = { "START UP", "initializing..." }, @@ -134,8 +134,8 @@ function facility.new(num_reactors, cooling_conf) } -- create units - for i = 1, num_reactors do - table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount)) + for i = 1, config.UnitCount do + table.insert(self.units, unit.new(i, cooling_conf.r_cool[i].BoilerCount, cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling)) table.insert(self.group_map, 0) end @@ -225,6 +225,14 @@ function facility.new(num_reactors, cooling_conf) return unallocated, false end + -- set idle state of all assigned reactors + ---@param idle boolean idle state + local function _set_idling(idle) + for i = 1, #self.prio_defs do + for _, u in pairs(self.prio_defs[i]) do u.auto_set_idle(idle) end + end + end + -- PUBLIC FUNCTIONS -- ---@class facility @@ -325,10 +333,11 @@ function facility.new(num_reactors, cooling_conf) --#region - local avg_charge = self.avg_charge.compute() - local avg_inflow = self.avg_inflow.compute() + local avg_charge = self.avg_charge.compute() + local avg_inflow = self.avg_inflow.compute() + local avg_outflow = self.avg_outflow.compute() - local now = util.time_s() + local now = os.clock() local state_changed = self.mode ~= self.last_mode local next_mode = self.mode @@ -390,6 +399,7 @@ function facility.new(num_reactors, cooling_conf) -- disable reactors and disengage auto control for _, u in pairs(self.prio_defs[i]) do u.disable() + u.auto_set_idle(false) u.auto_disengage() end end @@ -460,6 +470,9 @@ function facility.new(num_reactors, cooling_conf) self.last_error = 0 self.accumulator = 0 + -- enabling idling on all assigned units + _set_idling(true) + self.status_text = { "CHARGE MODE", "running control loop" } log.info("FAC: CHARGE mode starting PID control") elseif self.last_update ~= charge_update then @@ -475,9 +488,9 @@ function facility.new(num_reactors, cooling_conf) local integral = self.accumulator local derivative = (error - self.last_error) / (now - self.last_time) - local P = (charge_Kp * error) - local I = (charge_Ki * integral) - local D = (charge_Kd * derivative) + local P = charge_Kp * error + local I = charge_Ki * integral + local D = charge_Kd * derivative local output = P + I + D @@ -486,7 +499,12 @@ function facility.new(num_reactors, cooling_conf) self.saturated = output ~= out_c - -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%d] }", + if not config.ExtChargeIdling then + -- stop idling early if the output is zero, we are at or above the setpoint, and are not losing charge + _set_idling(not ((out_c == 0) and (error <= 0) and (avg_outflow <= 0))) + end + + -- log.debug(util.sprintf("CHARGE[%f] { CHRG[%f] ERR[%f] INT[%f] => OUT[%f] OUT_C[%f] <= P[%f] I[%f] D[%f] }", -- runtime, avg_charge, error, integral, output, out_c, P, I, D)) _allocate_burn_rate(out_c, true) @@ -544,9 +562,9 @@ function facility.new(num_reactors, cooling_conf) local integral = self.accumulator local derivative = (error - self.last_error) / (now - self.last_time) - local P = (rate_Kp * error) - local I = (rate_Ki * integral) - local D = (rate_Kd * derivative) + local P = rate_Kp * error + local I = rate_Ki * integral + local D = rate_Kd * derivative -- velocity (rate) (derivative of charge level => rate) feed forward local FF = self.gen_rate_setpoint / self.charge_conversion @@ -936,41 +954,41 @@ function facility.new(num_reactors, cooling_conf) function public.auto_stop() self.mode = PROCESS.INACTIVE end -- set automatic control configuration and start the process - ---@param config coord_auto_config configuration + ---@param auto_cfg coord_auto_config configuration ---@return table response ready state (successfully started) and current configuration (after updating) - function public.auto_start(config) + function public.auto_start(auto_cfg) local charge_scaler = 1000000 -- convert MFE to FE local gen_scaler = 1000 -- convert kFE to FE local ready = false -- load up current limits local limits = {} - for i = 1, num_reactors do + for i = 1, config.UnitCount do local u = self.units[i] ---@type reactor_unit limits[i] = u.get_control_inf().lim_br100 * 100 end -- only allow changes if not running if self.mode == PROCESS.INACTIVE then - if (type(config.mode) == "number") and (config.mode > PROCESS.INACTIVE) and (config.mode <= PROCESS.GEN_RATE) then - self.mode_set = config.mode + if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then + self.mode_set = auto_cfg.mode end - if (type(config.burn_target) == "number") and config.burn_target >= 0.1 then - self.burn_target = config.burn_target + if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then + self.burn_target = auto_cfg.burn_target end - if (type(config.charge_target) == "number") and config.charge_target >= 0 then - self.charge_setpoint = config.charge_target * charge_scaler + if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then + self.charge_setpoint = auto_cfg.charge_target * charge_scaler end - if (type(config.gen_target) == "number") and config.gen_target >= 0 then - self.gen_rate_setpoint = config.gen_target * gen_scaler + if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then + self.gen_rate_setpoint = auto_cfg.gen_target * gen_scaler end - if (type(config.limits) == "table") and (#config.limits == num_reactors) then - for i = 1, num_reactors do - local limit = config.limits[i] + if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then + for i = 1, config.UnitCount do + local limit = auto_cfg.limits[i] if (type(limit) == "number") and (limit >= 0.1) then limits[i] = limit @@ -1010,7 +1028,7 @@ function facility.new(num_reactors, cooling_conf) ---@param unit_id integer unit ID ---@param group integer group ID or 0 for independent function public.set_group(unit_id, group) - if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= num_reactors) and self.mode == PROCESS.INACTIVE then + if (group >= 0 and group <= 4) and (unit_id > 0 and unit_id <= config.UnitCount) and self.mode == PROCESS.INACTIVE then -- remove from old group if previously assigned local old_group = self.group_map[unit_id] if old_group ~= 0 then diff --git a/supervisor/session/svsessions.lua b/supervisor/session/svsessions.lua index e191db6..7300162 100644 --- a/supervisor/session/svsessions.lua +++ b/supervisor/session/svsessions.lua @@ -201,7 +201,7 @@ function svsessions.init(nic, fp_ok, config, cooling_conf) self.nic = nic self.fp_ok = fp_ok self.config = config - self.facility = facility.new(config.UnitCount, cooling_conf) + self.facility = facility.new(config, cooling_conf) end -- find an RTU session by the computer ID diff --git a/supervisor/startup.lua b/supervisor/startup.lua index b63b399..1694ebe 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.5" +local SUPERVISOR_VERSION = "v1.3.6" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/supervisor.lua b/supervisor/supervisor.lua index 3d6d7c7..1d79e72 100644 --- a/supervisor/supervisor.lua +++ b/supervisor/supervisor.lua @@ -26,6 +26,7 @@ function supervisor.load_config() config.CoolingConfig = settings.get("CoolingConfig") config.FacilityTankMode = settings.get("FacilityTankMode") config.FacilityTankDefs = settings.get("FacilityTankDefs") + config.ExtChargeIdling = settings.get("ExtChargeIdling") config.SVR_Channel = settings.get("SVR_Channel") config.PLC_Channel = settings.get("PLC_Channel") @@ -58,6 +59,8 @@ function supervisor.load_config() cfv.assert_type_int(config.FacilityTankMode) cfv.assert_range(config.FacilityTankMode, 0, 8) + cfv.assert_type_bool(config.ExtChargeIdling) + cfv.assert_channel(config.SVR_Channel) cfv.assert_channel(config.PLC_Channel) cfv.assert_channel(config.RTU_Channel) @@ -78,7 +81,7 @@ function supervisor.load_config() if type(config.AuthKey) == "string" then local len = string.len(config.AuthKey) - cfv.assert_eq(len == 0 or len >= 8, true) + cfv.assert(len == 0 or len >= 8) end cfv.assert_type_int(config.LogMode) diff --git a/supervisor/unit.lua b/supervisor/unit.lua index afdf6f3..6fa4d0a 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -8,9 +8,6 @@ local logic = require("supervisor.unitlogic") local plc = require("supervisor.session.plc") local rsctl = require("supervisor.session.rsctl") ----@class reactor_control_unit -local unit = {} - local WASTE_MODE = types.WASTE_MODE local WASTE = types.WASTE_PRODUCT local ALARM = types.ALARM @@ -55,12 +52,22 @@ local AISTATE = { ---@field id ALARM alarm ID ---@field tier integer alarm urgency tier (0 = highest) +-- burn rate to idle at +local IDLE_RATE = 0.01 + +---@class reactor_control_unit +local unit = {} + -- create a new reactor unit ---@nodiscard ---@param reactor_id integer reactor unit number ---@param num_boilers integer number of boilers expected ---@param num_turbines integer number of turbines expected -function unit.new(reactor_id, num_boilers, num_turbines) +---@param ext_idle boolean extended idling mode +function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) + -- time (ms) to idle for auto idling + local IDLE_TIME = util.trinary(ext_idle, 60000, 10000) + ---@class _unit_self local self = { r_id = reactor_id, @@ -83,6 +90,9 @@ function unit.new(reactor_id, num_boilers, num_turbines) emcool_opened = false, -- auto control auto_engaged = false, + auto_idle = false, + auto_idling = false, + auto_idle_start = 0, auto_was_alarmed = false, ramp_target_br100 = 0, -- state tracking @@ -98,6 +108,8 @@ function unit.new(reactor_id, num_boilers, num_turbines) status_text = { "UNKNOWN", "awaiting connection..." }, -- logic for alarms had_reactor = false, + turbine_flow_stable = false, + turbine_stability_data = {}, last_rate_change_ms = 0, ---@type rps_status last_rps_trips = { @@ -251,6 +263,7 @@ function unit.new(reactor_id, num_boilers, num_turbines) table.insert(self.db.annunciator.TurbineOverSpeed, false) table.insert(self.db.annunciator.GeneratorTrip, false) table.insert(self.db.annunciator.TurbineTrip, false) + table.insert(self.turbine_stability_data, { time_state = 0, time_tanks = 0, rotation = 1 }) end -- PRIVATE FUNCTIONS -- @@ -530,6 +543,13 @@ function unit.new(reactor_id, num_boilers, num_turbines) -- re-engage auto lock if it reconnected without it if self.auto_engaged and not self.plc_i.is_auto_locked() then self.plc_i.auto_lock(true) end + + -- stop idling when completed + if self.auto_idling and (((util.time_ms() - self.auto_idle_start) > IDLE_TIME) or not self.auto_idle) then + log.info(util.c("UNIT ", self.r_id, ": completed idling period")) + self.auto_idling = false + self.plc_i.auto_set_burn(0, false) + end end -- update deltas @@ -578,6 +598,23 @@ function unit.new(reactor_id, num_boilers, num_turbines) end end + -- set automatic control idling mode to change behavior when given a burn rate command of zero
+ -- - enabling it will hold the reactor at 0.01 mB/t for a period when commanded zero before disabling + -- - disabling it will stop the reactor when commanded zero + ---@param idle boolean true to enable, false to disable (and stop) + function public.auto_set_idle(idle) + if idle and not self.auto_idle then + self.auto_idling = false + self.auto_idle_start = 0 + end + + if idle ~= self.auto_idle then + log.debug(util.c("UNIT ", self.r_id, ": idling mode changed to ", idle)) + end + + self.auto_idle = idle + end + -- get the actual limit of this unit
-- if it is degraded or not ready, the limit will be 0 ---@nodiscard @@ -597,7 +634,35 @@ function unit.new(reactor_id, num_boilers, num_turbines) if self.auto_engaged then if self.plc_i ~= nil then log.debug(util.c("UNIT ", self.r_id, ": commit br100 of ", self.db.control.br100, " with ramp set to ", ramp)) - self.plc_i.auto_set_burn(self.db.control.br100 / 100, ramp) + + local rate = self.db.control.br100 / 100 + + if self.auto_idle then + if rate <= IDLE_RATE then + if self.auto_idle_start == 0 then + self.auto_idling = true + self.auto_idle_start = util.time_ms() + log.info(util.c("UNIT ", self.r_id, ": started idling at ", IDLE_RATE, " mB/t")) + + rate = IDLE_RATE + elseif (util.time_ms() - self.auto_idle_start) > IDLE_TIME then + if self.auto_idling then + self.auto_idling = false + log.info(util.c("UNIT ", self.r_id, ": completed idling period")) + end + else + log.debug(util.c("UNIT ", self.r_id, ": continuing idle at ", IDLE_RATE, " mB/t")) + + rate = IDLE_RATE + end + else + self.auto_idling = false + self.auto_idle_start = 0 + end + end + + self.plc_i.auto_set_burn(rate, ramp) + if ramp then self.ramp_target_br100 = self.db.control.br100 end end end diff --git a/supervisor/unitlogic.lua b/supervisor/unitlogic.lua index 134652c..ad2b522 100644 --- a/supervisor/unitlogic.lua +++ b/supervisor/unitlogic.lua @@ -39,6 +39,21 @@ local ALARM_LIMS = const.ALARM_LIMITS ---@class unit_logic_extension local logic = {} +-- compute Mekanism's rotation rate for a turbine +---@param turbine turbinev_session_db +local function turbine_rotation(turbine) + local build = turbine.build + + local inner_vol = build.steam_cap / const.mek.TURBINE_GAS_PER_TANK + local disp_rate = (build.dispersers * const.mek.TURBINE_DISPERSER_FLOW) * inner_vol + local vent_rate = build.vents * const.mek.TURBINE_VENT_FLOW + + local max_rate = math.min(disp_rate, vent_rate) + local flow = math.min(max_rate, turbine.tanks.steam.amount) + + return (flow * (turbine.tanks.steam.amount / build.steam_cap)) / max_rate +end + -- update the annunciator ---@param self _unit_self function logic.update_annunciator(self) @@ -81,6 +96,11 @@ function logic.update_annunciator(self) -- some alarms wait until the burn rate has stabilized, so keep track of that if math.abs(_get_dt(DT_KEYS.ReactorBurnR)) > 0 then self.last_rate_change_ms = util.time_ms() + self.turbine_flow_stable = false + + for t = 1, self.num_turbines do + self.turbine_stability_data[t] = { time_state = 0, time_tanks = 0, rotation = 1 } + end end -- record reactor stats @@ -274,6 +294,7 @@ function logic.update_annunciator(self) local total_flow_rate = 0 local total_input_rate = 0 local max_water_return_rate = 0 + local turbines_stable = true -- recompute blade count on the chance that it may have changed self.db.control.blade_count = 0 @@ -282,12 +303,14 @@ function logic.update_annunciator(self) for i = 1, #self.turbines do local session = self.turbines[i] ---@type unit_session local turbine = session.get_db() ---@type turbinev_session_db + local idx = session.get_device_idx() annunc.RCSFault = annunc.RCSFault or (not turbine.formed) or session.is_faulted() + annunc.TurbineOnline[idx] = true -- update ready state - -- - must be formed - -- - must have received build, state, and tanks at least once + -- - must be formed + -- - must have received build, state, and tanks at least once turbines_ready = turbines_ready and turbine.formed and (turbine.build.last_update > 0) and (turbine.state.last_update > 0) and @@ -296,11 +319,56 @@ function logic.update_annunciator(self) total_flow_rate = total_flow_rate + turbine.state.flow_rate total_input_rate = total_input_rate + turbine.state.steam_input_rate max_water_return_rate = max_water_return_rate + turbine.build.max_water_output + self.db.control.blade_count = self.db.control.blade_count + turbine.build.blades - annunc.TurbineOnline[session.get_device_idx()] = true + local last = self.turbine_stability_data[i] + + if (not self.turbine_flow_stable) and (turbine.state.steam_input_rate > 0) then + local rotation = turbine_rotation(turbine) + local rotation_stable = false + + -- see if data updated, and if so, check rotation speed change + -- minimal change indicates the turbine is converging on a flow rate + if last.time_tanks < turbine.tanks.last_update then + if last.time_tanks > 0 then + rotation_stable = math.abs(rotation - last.rotation) < 0.00000003 + end + + last.time_tanks = turbine.tanks.last_update + last.rotation = rotation + end + + -- flow is stable if the flow rate is at the input rate or at the max (±1 mB/t) + local flow_stable = false + if last.time_state < turbine.state.last_update then + if (last.time_state > 0) and (turbine.state.flow_rate > 0) then + flow_stable = math.abs(turbine.state.flow_rate - math.min(turbine.state.steam_input_rate, turbine.build.max_flow_rate)) < 2 + end + + last.time_state = turbine.state.last_update + end + + if rotation_stable then + log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached rotational stability (", rotation, ")")) + end + + if flow_stable then + log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)")) + end + + turbines_stable = turbines_stable and (rotation_stable or flow_stable) + else + last.time_state = 0 + last.time_tanks = 0 + last.rotation = 1 + + turbines_stable = false + end end + self.turbine_flow_stable = self.turbine_flow_stable or turbines_stable + -- check for boil rate mismatch (> 4% error) either between reactor and turbine or boiler and turbine annunc.BoilRateMismatch = math.abs(total_boil_rate - total_input_rate) > (0.04 * total_boil_rate) @@ -508,11 +576,25 @@ function logic.update_alarms(self) local rcs_trans = any_low or any_over or gen_trip or annunc.RCPTrip or annunc.MaxWaterReturnFeed - -- annunciator indicators for these states may not indicate a real issue when: - -- > flow is ramping up right after reactor start - -- > flow is ramping down after reactor shutdown - if ((util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS) and plc_cache.active then - rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch + if plc_cache.active then + -- these conditions may not indicate an issue when flow is changing after a burn rate change + if self.num_boilers == 0 then + if (util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS then + rcs_trans = rcs_trans or annunc.BoilRateMismatch + end + + if self.turbine_flow_stable then + rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.CoolantFeedMismatch or annunc.SteamFeedMismatch + end + else + if (util.time_ms() - self.last_rate_change_ms) > FLOW_STABILITY_DELAY_MS then + rcs_trans = rcs_trans or annunc.RCSFlowLow or annunc.BoilRateMismatch or annunc.CoolantFeedMismatch + end + + if self.turbine_flow_stable then + rcs_trans = rcs_trans or annunc.SteamFeedMismatch + end + end end if _update_alarm_state(self, rcs_trans, self.alarms.RCSTransient) then @@ -666,7 +748,9 @@ function logic.update_status_text(self) elseif annunc.WasteLineOcclusion then self.status_text[2] = "insufficient waste output rate" elseif (util.time_ms() - self.last_rate_change_ms) <= FLOW_STABILITY_DELAY_MS then - self.status_text[2] = "awaiting flow stability" + self.status_text[2] = "awaiting coolant flow stability" + elseif not self.turbine_flow_stable then + self.status_text[2] = "awaiting turbine flow stability" else self.status_text[2] = "system nominal" end