#143 #103 #101 #102 work in progress auto control, added coordinator controls, save/auto load configuration, auto enable/disable on reactor PLC for auto control (untested)

This commit is contained in:
Mikayla Fischler 2023-01-26 18:26:26 -05:00
parent e808ee2be0
commit e9562a140c
17 changed files with 750 additions and 161 deletions

View File

@ -2,10 +2,10 @@ local comms = require("scada-common.comms")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local util = require("scada-common.util")
local process = require("coordinator.process")
local apisessions = require("coordinator.apisessions")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local dialog = require("coordinator.ui.dialog")
@ -20,6 +20,7 @@ local ESTABLISH_ACK = comms.ESTABLISH_ACK
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local FAC_COMMANDS = comms.FAC_COMMANDS
local coordinator = {}
@ -313,11 +314,25 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
return self.sv_linked
end
-- send a facility command
---@param cmd FAC_COMMANDS command
function public.send_fac_command(cmd)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, { cmd })
end
-- send the auto process control configuration with a start command
---@param config coord_auto_config configuration
function public.send_auto_start(config)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.FAC_CMD, {
FAC_COMMANDS.START, config.mode, config.burn_target, config.charge_target, config.gen_target, config.limits
})
end
-- send a unit command
---@param cmd UNIT_COMMANDS command
---@param unit integer unit ID
---@param option any? optional options (like burn rate)
function public.send_command(cmd, unit, option)
---@param option any? optional option options for the optional options (like burn rate) (does option still look like a word?)
function public.send_unit_command(cmd, unit, option)
_send_sv(PROTOCOLS.SCADA_CRDN, SCADA_CRDN_TYPES.UNIT_CMD, { cmd, unit, option })
end
@ -412,6 +427,26 @@ function coordinator.comms(version, modem, sv_port, sv_listen, api_listen, sv_wa
end
elseif packet.type == SCADA_CRDN_TYPES.FAC_CMD then
-- facility command acknowledgement
if packet.length >= 2 then
local cmd = packet.data[1]
local ack = packet.data[2] == true
if cmd == FAC_COMMANDS.SCRAM_ALL then
iocontrol.get_db().facility.scram_ack(ack)
elseif cmd == FAC_COMMANDS.STOP then
iocontrol.get_db().facility.stop_ack(ack)
elseif cmd == FAC_COMMANDS.START then
if packet.length == 7 then
process.start_ack_handle({ table.unpack(packet.data, 2) })
else
log.debug("SCADA_CRDN process start (with configuration) ack echo packet length mismatch")
end
else
log.debug(util.c("received facility command ack with unknown command ", cmd))
end
else
log.debug("SCADA_CRDN facility command ack packet length mismatch")
end
elseif packet.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
-- record builds
if iocontrol.record_unit_builds(packet.data) then

View File

@ -24,9 +24,17 @@ function iocontrol.init(conf, comms)
---@class ioctl_facility
io.facility = {
auto_active = false,
scram = false,
auto_ramping = false,
auto_scram = false,
auto_scram_cause = "ok", ---@type auto_scram_cause
num_units = conf.num_units, ---@type integer
save_cfg_ack = function (success) end, ---@param success boolean
start_ack = function (success) end, ---@param success boolean
stop_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
num_units = conf.num_units, ---@type integer
ps = psil.create(),
induction_ps_tbl = {},
@ -69,7 +77,6 @@ function iocontrol.init(conf, comms)
set_waste = function (mode) process.set_waste(i, mode) end, ---@param mode integer waste processing mode
set_group = function (grp) process.set_group(i, grp) end, ---@param grp integer|0 group ID or 0
set_limit = function (lim) process.set_limit(i, lim) end, ---@param lim number burn rate limit
start_ack = function (success) end, ---@param success boolean
scram_ack = function (success) end, ---@param success boolean
@ -195,7 +202,7 @@ function iocontrol.record_unit_builds(builds)
-- reactor build
if type(build.reactor) == "table" then
unit.reactor_data.mek_struct = build.reactor
unit.reactor_data.mek_struct = build.reactor ---@type mek_struct
for key, val in pairs(unit.reactor_data.mek_struct) do
unit.reactor_ps.publish(key, val)
end
@ -257,11 +264,38 @@ function iocontrol.update_facility_status(status)
else
local fac = io.facility
-- auto control status information
local ctl_status = status[1]
if type(ctl_status) == "table" then
fac.auto_active = ctl_status[1] > 0
fac.auto_ramping = ctl_status[2]
fac.auto_scram = ctl_status[3]
fac.auto_scram_cause = ctl_status[4]
fac.ps.publish("auto_active", fac.auto_active)
fac.ps.publish("auto_ramping", fac.auto_ramping)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("auto_scram_cause", fac.auto_scram_cause)
else
log.debug(log_header .. "control status not a table")
end
-- RTU statuses
local rtu_statuses = status[1]
local rtu_statuses = status[2]
if type(rtu_statuses) == "table" then
-- power statistics
if type(rtu_statuses.power) == "table" then
fac.ps.publish("avg_charge", rtu_statuses.power[1])
fac.ps.publish("avg_inflow", rtu_statuses.power[2])
fac.ps.publish("avg_outflow", rtu_statuses.power[3])
else
log.debug(log_header .. "power statistics list not a table")
end
-- induction matricies statuses
if type(rtu_statuses.induction) == "table" then
for id = 1, #fac.induction_ps_tbl do
@ -328,6 +362,8 @@ function iocontrol.update_unit_statuses(statuses)
log.debug("iocontrol.update_unit_statuses: number of provided unit statuses does not match expected number of units")
return false
else
local burn_rate_sum = 0.0
-- get all unit statuses
for i = 1, #statuses do
local log_header = util.c("iocontrol.update_unit_statuses[unit ", i, "]: ")
@ -369,6 +405,11 @@ function iocontrol.update_unit_statuses(statuses)
unit.reactor_data.rps_status = rps_status ---@type rps_status
unit.reactor_data.mek_status = mek_status ---@type mek_status
-- if status hasn't been received, mek_status = {}
if type(unit.reactor_data.mek_status.act_burn_rate) == "number" then
burn_rate_sum = burn_rate_sum + unit.reactor_data.mek_status.act_burn_rate
end
if unit.reactor_data.mek_status.status then
unit.reactor_ps.publish("computed_status", 5) -- running
else
@ -596,8 +637,8 @@ function iocontrol.update_unit_statuses(statuses)
local auto_ctl_state = status[6]
if type(auto_ctl_state) == "table" then
if #auto_ctl_state == 1 then
unit.reactor_ps.publish("burn_limit", auto_ctl_state[1])
if #auto_ctl_state == 0 then
---@todo
else
log.debug(log_header .. "auto control state length mismatch")
end
@ -606,6 +647,8 @@ function iocontrol.update_unit_statuses(statuses)
end
end
io.facility.ps.publish("burn_sum", burn_rate_sum)
-- update alarm sounder
sounder.eval(io.units)
end

View File

@ -1,16 +1,28 @@
local comms = require("scada-common.comms")
local log = require("scada-common.log")
local types = require("scada-common.types")
local util = require("scada-common.util")
local FAC_COMMANDS = comms.FAC_COMMANDS
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local PROCESS = types.PROCESS
---@class process_controller
local process = {}
local self = {
io = nil, ---@type ioctl
comms = nil ---@type coord_comms
comms = nil, ---@type coord_comms
---@class coord_auto_config
config = {
mode = 0, ---@type PROCESS
burn_target = 0.0,
charge_target = 0.0,
gen_target = 0.0,
limits = {} ---@type table
}
}
--------------------------
@ -24,11 +36,37 @@ function process.init(iocontrol, comms)
self.io = iocontrol
self.comms = comms
for i = 1, self.io.facility.num_units do
self.config.limits[i] = 0.1
end
-- load settings
if not settings.load("/coord.settings") then
log.error("process.init(): failed to load coordinator settings file")
end
local config = settings.get("PROCESS") ---@type coord_auto_config|nil
if type(config) == "table" then
self.config.mode = config.mode
self.config.burn_target = config.burn_target
self.config.charge_target = config.charge_target
self.config.gen_target = config.gen_target
self.config.limits = config.limits
self.io.facility.ps.publish("process_mode", self.config.mode)
self.io.facility.ps.publish("process_burn_target", self.config.burn_target)
self.io.facility.ps.publish("process_charge_target", self.config.charge_target)
self.io.facility.ps.publish("process_gen_target", self.config.gen_target)
for id = 1, math.min(#self.config.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit
unit.reactor_ps.publish("burn_limit", self.config.limits[id])
end
log.info("PROCESS: loaded auto control settings from coord.settings")
end
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
if type(waste_mode) == "table" then
@ -44,7 +82,7 @@ end
---@param id integer unit ID
function process.start(id)
self.io.units[id].control_state = true
self.comms.send_command(UNIT_COMMANDS.START, id)
self.comms.send_unit_command(UNIT_COMMANDS.START, id)
log.debug(util.c("UNIT[", id, "]: START"))
end
@ -52,14 +90,14 @@ end
---@param id integer unit ID
function process.scram(id)
self.io.units[id].control_state = false
self.comms.send_command(UNIT_COMMANDS.SCRAM, id)
self.comms.send_unit_command(UNIT_COMMANDS.SCRAM, id)
log.debug(util.c("UNIT[", id, "]: SCRAM"))
end
-- reset reactor protection system
---@param id integer unit ID
function process.reset_rps(id)
self.comms.send_command(UNIT_COMMANDS.RESET_RPS, id)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_RPS, id)
log.debug(util.c("UNIT[", id, "]: RESET RPS"))
end
@ -67,7 +105,7 @@ end
---@param id integer unit ID
---@param rate number burn rate
function process.set_rate(id, rate)
self.comms.send_command(UNIT_COMMANDS.SET_BURN, id, rate)
self.comms.send_unit_command(UNIT_COMMANDS.SET_BURN, id, rate)
log.debug(util.c("UNIT[", id, "]: SET BURN = ", rate))
end
@ -75,7 +113,7 @@ end
---@param id integer unit ID
---@param mode integer waste mode
function process.set_waste(id, mode)
self.comms.send_command(UNIT_COMMANDS.SET_WASTE, id, mode)
self.comms.send_unit_command(UNIT_COMMANDS.SET_WASTE, id, mode)
log.debug(util.c("UNIT[", id, "]: SET WASTE = ", mode))
local waste_mode = settings.get("WASTE_MODES") ---@type table|nil
@ -96,7 +134,7 @@ end
-- acknowledge all alarms
---@param id integer unit ID
function process.ack_all_alarms(id)
self.comms.send_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALL_ALARMS, id)
log.debug(util.c("UNIT[", id, "]: ACK ALL ALARMS"))
end
@ -104,7 +142,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.ack_alarm(id, alarm)
self.comms.send_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.ACK_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: ACK ALARM ", alarm))
end
@ -112,7 +150,7 @@ end
---@param id integer unit ID
---@param alarm integer alarm ID
function process.reset_alarm(id, alarm)
self.comms.send_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
self.comms.send_unit_command(UNIT_COMMANDS.RESET_ALARM, id, alarm)
log.debug(util.c("UNIT[", id, "]: RESET ALARM ", alarm))
end
@ -120,16 +158,86 @@ end
---@param unit_id integer unit ID
---@param group_id integer|0 group ID or 0 for independent
function process.set_group(unit_id, group_id)
self.comms.send_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id)
self.comms.send_unit_command(UNIT_COMMANDS.SET_GROUP, unit_id, group_id)
log.debug(util.c("UNIT[", unit_id, "]: SET GROUP ", group_id))
end
-- set the burn rate limit
---@param id integer unit ID
---@param limit number burn rate limit
function process.set_limit(id, limit)
self.comms.send_command(UNIT_COMMANDS.SET_LIMIT, id, limit)
log.debug(util.c("UNIT[", id, "]: SET LIMIT = ", limit))
--------------------------
-- AUTO PROCESS CONTROL --
--------------------------
-- facility SCRAM command
function process.fac_scram()
self.comms.send_fac_command(FAC_COMMANDS.SCRAM_ALL)
log.debug("FAC: SCRAM ALL")
end
-- stop automatic process control
function process.stop_auto()
self.comms.send_fac_command(FAC_COMMANDS.STOP)
log.debug("FAC: STOP AUTO")
end
-- start automatic process control
function process.start_auto()
self.comms.send_auto_start(self.config)
log.debug("FAC: START AUTO")
end
-- save process control settings
---@param mode PROCESS control mode
---@param burn_target number burn rate target
---@param charge_target number charge target
---@param gen_target number generation rate target
---@param limits table unit burn rate limits
function process.save(mode, burn_target, charge_target, gen_target, limits)
-- attempt to load settings
if not settings.load("/coord.settings") then
log.warning("process.save(): failed to load coordinator settings file")
end
-- config table
self.config = {
mode = mode,
burn_target = burn_target,
charge_target = charge_target,
gen_target = gen_target,
limits = limits
}
-- save config
settings.set("PROCESS", self.config)
local saved = settings.save("/coord.settings")
if not saved then
log.warning("process.save(): failed to save coordinator settings file")
end
log.debug("saved = " .. util.strval(saved))
self.io.facility.save_cfg_ack(saved)
end
-- handle a start command acknowledgement
---@param response table ack and configuration reply
function process.start_ack_handle(response)
local ack = response[1]
self.config.mode = response[2]
self.config.burn_target = response[3]
self.config.charge_target = response[4]
self.config.gen_target = response[5]
for i = 1, #response[6] do
self.config.limits[i] = response[6][i]
end
self.io.facility.ps.publish("auto_mode", self.config.mode)
self.io.facility.ps.publish("burn_target", self.config.burn_target)
self.io.facility.ps.publish("charge_target", self.config.charge_target)
self.io.facility.ps.publish("gen_target", self.config.gen_target)
self.io.facility.start_ack(ack)
end
--------------------------

View File

@ -1,6 +1,8 @@
local tcd = require("scada-common.tcallbackdsp")
local util = require("scada-common.util")
local iocontrol = require("coordinator.iocontrol")
local process = require("coordinator.process")
local style = require("coordinator.ui.style")
@ -36,29 +38,127 @@ local function new_view(root, x, y)
local facility = iocontrol.get_db().facility
local units = iocontrol.get_db().units
local bw_fg_bg = cpair(colors.black, colors.white)
local bw_fg_bg = cpair(colors.black, colors.white)
local hzd_fg_bg = cpair(colors.white, colors.gray)
local dis_colors = cpair(colors.white, colors.lightGray)
local proc = Div{parent=root,width=60,height=24,x=x,y=y}
local main = Div{parent=root,width=80,height=24,x=x,y=y}
local limits = Div{parent=proc,width=40,height=24,x=30,y=1}
local scram = HazardButton{parent=main,x=1,y=1,text="FAC SCRAM",accent=colors.yellow,dis_colors=dis_colors,callback=process.fac_scram,fg_bg=hzd_fg_bg}
facility.scram_ack = scram.on_response
---------------------
-- process control --
---------------------
local proc = Div{parent=main,width=54,height=24,x=27,y=1}
-----------------------------
-- process control targets --
-----------------------------
local targets = Div{parent=proc,width=31,height=24,x=1,y=1}
local burn_tag = Div{parent=targets,x=1,y=1,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=burn_tag,x=2,y=2,text="Burn Target",width=7,height=2}
local burn_target = Div{parent=targets,x=9,y=1,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local b_target = SpinboxNumeric{parent=burn_target,x=11,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=burn_target,x=18,y=2,text="mB/t"}
local burn_sum = DataIndicator{parent=targets,x=9,y=4,label="",format="%18.1f",value=0,unit="mB/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_burn_target", b_target.set_value)
facility.ps.subscribe("burn_sum", burn_sum.update)
local chg_tag = Div{parent=targets,x=1,y=6,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=chg_tag,x=2,y=2,text="Charge Target",width=7,height=2}
local chg_target = Div{parent=targets,x=9,y=6,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local c_target = SpinboxNumeric{parent=chg_target,x=2,y=1,whole_num_precision=15,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=chg_target,x=18,y=2,text="kFE"}
local cur_charge = DataIndicator{parent=targets,x=9,y=9,label="",format="%19d",value=0,unit="kFE",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_charge_target", c_target.set_value)
facility.induction_ps_tbl[1].subscribe("energy", function (j) cur_charge.update(util.joules_to_fe(j) / 1000) end)
local gen_tag = Div{parent=targets,x=1,y=11,width=8,height=4,fg_bg=cpair(colors.black,colors.purple)}
TextBox{parent=gen_tag,x=2,y=2,text="Gen. Target",width=7,height=2}
local gen_target = Div{parent=targets,x=9,y=11,width=23,height=3,fg_bg=cpair(colors.gray,colors.white)}
local g_target = SpinboxNumeric{parent=gen_target,x=8,y=1,whole_num_precision=9,fractional_precision=0,min=0,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=gen_target,x=18,y=2,text="kFE/t"}
local cur_gen = DataIndicator{parent=targets,x=9,y=14,label="",format="%17d",value=0,unit="kFE/t",commas=true,lu_colors=cpair(colors.black,colors.black),width=23,fg_bg=cpair(colors.black,colors.brown)}
facility.ps.subscribe("process_gen_target", g_target.set_value)
facility.induction_ps_tbl[1].subscribe("last_input", function (j) cur_gen.update(util.joules_to_fe(j) / 1000) end)
-----------------
-- unit limits --
-----------------
local limit_div = Div{parent=proc,width=40,height=19,x=34,y=6}
local rate_limits = {}
for i = 1, facility.num_units do
local unit = units[i] ---@type ioctl_entry
local unit = units[i] ---@type ioctl_unit
local _y = ((i - 1) * 4) + 1
local _y = ((i - 1) * 5) + 1
TextBox{parent=limits,x=1,y=_y+1,text="Unit "..i}
local lim_ctl = Div{parent=limits,x=8,y=_y,width=20,height=3,fg_bg=cpair(colors.gray,colors.white)}
local burn_rate = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,max=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
unit.reactor_ps.subscribe("max_burn", burn_rate.set_max)
unit.reactor_ps.subscribe("burn_limit", burn_rate.set_value)
local unit_tag = Div{parent=limit_div,x=1,y=_y,width=8,height=4,fg_bg=cpair(colors.black,colors.lightBlue)}
TextBox{parent=unit_tag,x=2,y=2,text="Unit "..i.." Limit",width=7,height=2}
local lim_ctl = Div{parent=limit_div,x=9,y=_y,width=14,height=3,fg_bg=cpair(colors.gray,colors.white)}
rate_limits[i] = SpinboxNumeric{parent=lim_ctl,x=2,y=1,whole_num_precision=4,fractional_precision=1,min=0.1,arrow_fg_bg=cpair(colors.gray,colors.white),fg_bg=bw_fg_bg}
TextBox{parent=lim_ctl,x=9,y=2,text="mB/t"}
local set_burn = function () unit.set_limit(burn_rate.get_value()) end
PushButton{parent=lim_ctl,x=14,y=2,text="SAVE",min_width=6,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.white,colors.gray),callback=set_burn}
unit.reactor_ps.subscribe("max_burn", rate_limits[i].set_max)
unit.reactor_ps.subscribe("burn_limit", rate_limits[i].set_value)
local cur_burn = DataIndicator{parent=limit_div,x=9,y=_y+3,label="",format="%7.1f",value=0,unit="mB/t",commas=false,lu_colors=cpair(colors.black,colors.black),width=14,fg_bg=cpair(colors.black,colors.brown)}
unit.reactor_ps.subscribe("act_burn_rate", cur_burn.update)
end
-------------------------
-- controls and status --
-------------------------
local ctl_opts = { "Regulated", "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)
local u_stat = Rectangle{parent=proc,border=border(1,colors.gray,true),thin=true,width=31,height=4,x=1,y=16,fg_bg=bw_fg_bg}
local stat_line_1 = TextBox{parent=u_stat,x=1,y=1,text="UNKNOWN",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=bw_fg_bg}
local stat_line_2 = TextBox{parent=u_stat,x=1,y=2,text="awaiting data",width=31,height=1,alignment=TEXT_ALIGN.CENTER,fg_bg=cpair(colors.gray, colors.white)}
local auto_controls = Div{parent=proc,x=1,y=20,width=31,height=5,fg_bg=cpair(colors.gray,colors.white)}
-- save the automatic process control configuration without starting
local function _save_cfg()
local limits = {}
for i = 1, #rate_limits do limits[i] = rate_limits[i].get_value() end
process.save(mode.get_value(), b_target.get_value(), c_target.get_value(), g_target.get_value(), limits)
end
-- start automatic control after saving process control settings
local function _start_auto()
_save_cfg()
process.start_auto()
end
local save = HazardButton{parent=auto_controls,x=2,y=2,text="SAVE",accent=colors.purple,dis_colors=dis_colors,callback=_save_cfg,fg_bg=hzd_fg_bg}
local start = HazardButton{parent=auto_controls,x=13,y=2,text="START",accent=colors.lightBlue,dis_colors=dis_colors,callback=_start_auto,fg_bg=hzd_fg_bg}
local stop = HazardButton{parent=auto_controls,x=23,y=2,text="STOP",accent=colors.orange,dis_colors=dis_colors,callback=process.stop_auto,fg_bg=hzd_fg_bg}
facility.start_ack = start.on_response
facility.stop_ack = stop.on_response
function facility.save_cfg_ack(ack)
tcd.dispatch(0.2, function () save.on_response(ack) end)
end
end

View File

@ -83,7 +83,7 @@ local function init(monitor)
-- testing
---@fixme remove test code
ColorMap{parent=main,x=2,y=(main.height()-1)}
ColorMap{parent=main,x=132,y=(main.height()-1)}
local audio = Div{parent=main,width=34,height=15,x=95,y=cnc_y_start}

View File

@ -145,9 +145,6 @@ local function hazard_button(args)
---@diagnostic disable-next-line: unused-local
function e.handle_touch(event)
if e.enabled then
-- call the touch callback
args.callback()
-- change text color to indicate clicked
e.window.setTextColor(args.accent)
e.window.setCursorPos(3, 2)
@ -160,6 +157,9 @@ local function hazard_button(args)
-- 1.5 second timeout
tcd.dispatch(1.5, on_timeout)
-- call the touch callback
args.callback()
end
end

View File

@ -13,6 +13,7 @@ local DEVICE_TYPES = comms.DEVICE_TYPES
local ESTABLISH_ACK = comms.ESTABLISH_ACK
local RPLC_TYPES = comms.RPLC_TYPES
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local AUTO_ACK = comms.PLC_AUTO_ACK
local print = util.print
local println = util.println
@ -24,6 +25,8 @@ local println_ts = util.println_ts
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active."
local AUTO_TOGGLE_DELAY_MS = 5000
-- RPS SAFETY CONSTANTS
local MAX_DAMAGE_PERCENT = 90
@ -61,6 +64,7 @@ function plc.rps_init(reactor, is_formed)
reactor = reactor,
state = { false, false, false, false, false, false, false, false, false, false, false, false },
reactor_enabled = false,
enabled_at = 0,
formed = is_formed,
force_disabled = false,
tripped = false,
@ -215,7 +219,7 @@ function plc.rps_init(reactor, is_formed)
self.state[state_keys.sys_fail] = true
end
-- SCRAM the reactor now
-- SCRAM the reactor now (blocks waiting for server tick)
---@return boolean success
function public.scram()
log.info("RPS: reactor SCRAM")
@ -226,11 +230,12 @@ function plc.rps_init(reactor, is_formed)
return false
else
self.reactor_enabled = false
self.last_runtime = util.time_ms() - self.enabled_at
return true
end
end
-- start the reactor
-- start the reactor now (blocks waiting for server tick)
---@return boolean success
function public.activate()
if not self.tripped then
@ -241,6 +246,7 @@ function plc.rps_init(reactor, is_formed)
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true
self.enabled_at = util.time_ms()
return true
end
else
@ -250,6 +256,21 @@ function plc.rps_init(reactor, is_formed)
return false
end
-- automatic control activate/re-activate
---@return boolean success
function public.auto_activate()
-- clear automatic SCRAM if it was the cause
if self.tripped and self.trip_cause == "automatic" then
self.state[state_keys.automatic] = true
self.trip_cause = rps_status_t.ok
self.tripped = false
log.debug("RPS: cleared automatic SCRAM for re-activation")
end
return public.activate()
end
-- check all safety conditions
---@return boolean tripped, rps_status_t trip_status, boolean first_trip
function public.check()
@ -324,7 +345,8 @@ function plc.rps_init(reactor, is_formed)
self.tripped = true
self.trip_cause = status
-- in the case that the reactor is detected to be active, it will be scrammed shortly after this in the main RPS loop if we don't here
-- in the case that the reactor is detected to be active,
-- it will be scrammed shortly after this in the main RPS loop if we don't here
if self.formed then
if not self.force_disabled then
public.scram()
@ -348,6 +370,10 @@ function plc.rps_init(reactor, is_formed)
function public.is_formed() return self.formed end
function public.is_force_disabled() return self.force_disabled end
-- get the runtime of the reactor if active, or the last runtime if disabled
---@return integer runtime time since last enable
function public.get_runtime() return util.trinary(self.reactor_enabled, util.time_ms() - self.enabled_at, self.last_runtime) end
-- reset the RPS
---@param quiet? boolean true to suppress the info log message
function public.reset(quiet)
@ -383,8 +409,8 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
reactor = reactor,
scrammed = false,
linked = false,
status_cache = nil,
resend_build = false,
status_cache = nil,
max_burn_rate = nil
}
@ -532,9 +558,9 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
-- general ack
---@param msg_type RPLC_TYPES
---@param succeeded boolean
local function _send_ack(msg_type, succeeded)
_send(msg_type, { succeeded })
---@param status boolean|integer
local function _send_ack(msg_type, status)
_send(msg_type, { status })
end
-- send structure properties (these should not change, server will cache these)
@ -587,6 +613,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
self.reactor = reactor
self.status_cache = nil
self.resend_build = true
self.max_burn_rate = nil
end
-- unlink from the server
@ -731,7 +758,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
log.debug("sent out structure again, did supervisor miss it?")
elseif packet.type == RPLC_TYPES.MEK_BURN_RATE then
-- set the burn rate
if packet.length == 2 then
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local success = false
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
@ -759,7 +786,7 @@ function plc.comms(id, version, modem, local_port, server_port, reactor, rps, co
_send_ack(packet.type, success)
else
log.debug("RPLC set burn rate packet length mismatch")
log.debug("RPLC set burn rate packet length mismatch or non-numeric burn rate")
end
elseif packet.type == RPLC_TYPES.RPS_ENABLE then
-- enable the reactor
@ -779,6 +806,63 @@ 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.AUTO_BURN_RATE then
-- automatic control requested a new burn rate
if (packet.length == 2) and (type(packet.data[1]) == "number") then
local ack = AUTO_ACK.FAIL
local burn_rate = math.floor(packet.data[1] * 10) / 10
local ramp = packet.data[2]
-- if no known max burn rate, check again
if self.max_burn_rate == nil then
self.max_burn_rate = self.reactor.getMaxBurnRate()
end
-- if we know our max burn rate, update current burn rate setpoint if in range
if self.max_burn_rate ~= ppm.ACCESS_FAULT then
if burn_rate < 0.1 then
if rps.is_active() then
if rps.get_runtime() > AUTO_TOGGLE_DELAY_MS then
-- auto scram to disable
if rps.scram() then
ack = AUTO_ACK.ZERO_DIS_OK
self.auto_last_disable = util.time_ms()
end
else
-- too soon to disable
ack = AUTO_ACK.ZERO_DIS_WAIT
end
else
ack = AUTO_ACK.ZERO_DIS_OK
end
elseif burn_rate <= self.max_burn_rate then
if not rps.is_active() then
-- activate the reactor
if not rps.auto_activate() then
log.debug("automatic reactor activation failed")
end
end
-- if active, set/ramp burn rate
if rps.is_active() then
if ramp then
setpoints.burn_rate_en = true
setpoints.burn_rate = burn_rate
ack = AUTO_ACK.RAMP_SET_OK
else
self.reactor.setBurnRate(burn_rate)
ack = util.trinary(self.reactor.__p_is_faulted(), AUTO_ACK.FAIL, AUTO_ACK.DIRECT_SET_OK)
end
end
else
log.debug(burn_rate .. " rate outside of 0 < x <= " .. self.max_burn_rate)
end
end
_send_ack(packet.type, ack)
else
log.debug("RPLC set automatic burn rate packet length mismatch or non-numeric burn rate")
end
else
log.warning("received unknown RPLC packet type " .. packet.type)
end

View File

@ -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.9.10"
local R_PLC_VERSION = "beta-v0.10.0"
local print = util.print
local println = util.println

View File

@ -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.10"
local RTU_VERSION = "beta-v0.9.11"
local rtu_t = types.rtu_t

View File

@ -12,7 +12,7 @@ local rtu_t = types.rtu_t
local insert = table.insert
comms.version = "1.1.1"
comms.version = "1.1.2"
---@alias PROTOCOLS integer
local PROTOCOLS = {
@ -23,14 +23,6 @@ local PROTOCOLS = {
COORD_API = 4 -- data/control packets for pocket computers to/from coordinators
}
---@alias DEVICE_TYPES integer
local DEVICE_TYPES = {
PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish
SV = 2, -- supervisor device type for establish
CRDN = 3 -- coordinator device type for establish
}
---@alias RPLC_TYPES integer
local RPLC_TYPES = {
STATUS = 0, -- reactor/system status
@ -41,7 +33,8 @@ local RPLC_TYPES = {
RPS_ASCRAM = 5, -- SCRAM reactor (automatic request)
RPS_STATUS = 6, -- RPS status
RPS_ALARM = 7, -- RPS alarm broadcast
RPS_RESET = 8 -- clear RPS trip (if in bad state, will trip immediately)
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
}
---@alias SCADA_MGMT_TYPES integer
@ -53,13 +46,6 @@ local SCADA_MGMT_TYPES = {
RTU_DEV_REMOUNT = 4 -- RTU multiblock possbily changed (formed, unformed) due to PPM remount
}
---@alias ESTABLISH_ACK integer
local ESTABLISH_ACK = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
COLLISION = 2 -- link denied due to existing active link
}
---@alias SCADA_CRDN_TYPES integer
local SCADA_CRDN_TYPES = {
FAC_BUILDS = 0, -- facility RTU builds
@ -70,25 +56,26 @@ local SCADA_CRDN_TYPES = {
UNIT_CMD = 5 -- command a reactor unit
}
---@alias UNIT_COMMANDS integer
local UNIT_COMMANDS = {
SCRAM = 0, -- SCRAM the reactor
START = 1, -- start the reactor
RESET_RPS = 2, -- reset the RPS
SET_BURN = 3, -- set the burn rate
SET_WASTE = 4, -- set the waste processing mode
ACK_ALL_ALARMS = 5, -- ack all active alarms
ACK_ALARM = 6, -- ack a particular alarm
RESET_ALARM = 7, -- reset a particular alarm
SET_GROUP = 8, -- assign this unit to a group
SET_LIMIT = 9 -- set this unit maximum auto burn rate
}
---@alias CAPI_TYPES integer
local CAPI_TYPES = {
ESTABLISH = 0 -- initial greeting
}
---@alias ESTABLISH_ACK integer
local ESTABLISH_ACK = {
ALLOW = 0, -- link approved
DENY = 1, -- link denied
COLLISION = 2 -- link denied due to existing active link
}
---@alias DEVICE_TYPES integer
local DEVICE_TYPES = {
PLC = 0, -- PLC device type for establish
RTU = 1, -- RTU device type for establish
SV = 2, -- supervisor device type for establish
CRDN = 3 -- coordinator device type for establish
}
---@alias RTU_UNIT_TYPES integer
local RTU_UNIT_TYPES = {
REDSTONE = 0, -- redstone I/O
@ -100,16 +87,51 @@ local RTU_UNIT_TYPES = {
ENV_DETECTOR = 6 -- environment detector
}
---@alias PLC_AUTO_ACK integer
local PLC_AUTO_ACK = {
FAIL = 0, -- failed to set burn rate/burn rate invalid
DIRECT_SET_OK = 1, -- successfully set burn rate
RAMP_SET_OK = 2, -- successfully started burn rate ramping
ZERO_DIS_OK = 3, -- successfully disabled reactor with < 0.1 burn rate
ZERO_DIS_WAIT = 4 -- too soon to disable reactor with < 0.1 burn rate
}
---@alias FAC_COMMANDS integer
local FAC_COMMANDS = {
SCRAM_ALL = 0, -- SCRAM all reactors
STOP = 1, -- stop automatic control
START = 2 -- start automatic control
}
---@alias UNIT_COMMANDS integer
local UNIT_COMMANDS = {
SCRAM = 0, -- SCRAM the reactor
START = 1, -- start the reactor
RESET_RPS = 2, -- reset the RPS
SET_BURN = 3, -- set the burn rate
SET_WASTE = 4, -- set the waste processing mode
ACK_ALL_ALARMS = 5, -- ack all active alarms
ACK_ALARM = 6, -- ack a particular alarm
RESET_ALARM = 7, -- reset a particular alarm
SET_GROUP = 8 -- assign this unit to a group
}
comms.PROTOCOLS = PROTOCOLS
comms.DEVICE_TYPES = DEVICE_TYPES
comms.RPLC_TYPES = RPLC_TYPES
comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.SCADA_MGMT_TYPES = SCADA_MGMT_TYPES
comms.SCADA_CRDN_TYPES = SCADA_CRDN_TYPES
comms.UNIT_COMMANDS = UNIT_COMMANDS
comms.CAPI_TYPES = CAPI_TYPES
comms.ESTABLISH_ACK = ESTABLISH_ACK
comms.DEVICE_TYPES = DEVICE_TYPES
comms.RTU_UNIT_TYPES = RTU_UNIT_TYPES
comms.PLC_AUTO_ACK = PLC_AUTO_ACK
comms.UNIT_COMMANDS = UNIT_COMMANDS
comms.FAC_COMMANDS = FAC_COMMANDS
---@alias packet scada_packet|modbus_packet|rplc_packet|mgmt_packet|crdn_packet|capi_packet
---@alias frame modbus_frame|rplc_frame|mgmt_frame|crdn_frame|capi_frame
@ -308,9 +330,10 @@ function comms.rplc_packet()
self.type == RPLC_TYPES.RPS_ENABLE or
self.type == RPLC_TYPES.RPS_SCRAM or
self.type == RPLC_TYPES.RPS_ASCRAM or
self.type == RPLC_TYPES.RPS_ALARM or
self.type == RPLC_TYPES.RPS_STATUS or
self.type == RPLC_TYPES.RPS_RESET
self.type == RPLC_TYPES.RPS_ALARM or
self.type == RPLC_TYPES.RPS_RESET or
self.type == RPLC_TYPES.AUTO_BURN_RATE
end
-- make an RPLC packet

View File

@ -35,6 +35,15 @@ types.TRI_FAIL = {
FULL = 2
}
---@alias PROCESS integer
types.PROCESS = {
INACTIVE = 0,
SIMPLE = 1,
BURN_RATE = 2,
CHARGE = 3,
GEN_RATE = 4
}
---@alias WASTE_MODE integer
types.WASTE_MODE = {
AUTO = 1,
@ -164,6 +173,9 @@ types.ALARM_STATE = {
---| "sys_fail"
---| "force_disabled"
---@alias auto_scram_cause
---| "ok"
---@alias rtu_t string
types.rtu_t = {
redstone = "redstone",

View File

@ -202,6 +202,13 @@ function util.is_int(x)
return type(x) == "number" and x == math.floor(x)
end
-- get the sign of a number
---@param x number value
---@return integer sign (-1 for < 0, 1 otherwise)
function util.sign(x)
return util.trinary(x < 0, -1, 1)
end
-- round a number to an integer
---@return integer rounded
function util.round(x)

View File

@ -11,6 +11,7 @@ local PROTOCOLS = comms.PROTOCOLS
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local SCADA_CRDN_TYPES = comms.SCADA_CRDN_TYPES
local UNIT_COMMANDS = comms.UNIT_COMMANDS
local FAC_COMMANDS = comms.FAC_COMMANDS
local SV_Q_CMDS = svqtypes.SV_Q_CMDS
local SV_Q_DATA = svqtypes.SV_Q_DATA
@ -133,6 +134,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
-- send facility status
local function _send_fac_status()
local status = {
facility.get_control_status(),
facility.get_rtu_statuses()
}
@ -146,9 +148,7 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
for i = 1, #self.units do
local unit = self.units[i] ---@type reactor_unit
local auto_ctl = {
unit.get_control_inf().lim_br10 / 10
}
local auto_ctl = {}
status[unit.get_id()] = {
unit.get_reactor_status(),
@ -208,6 +208,37 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.type == SCADA_CRDN_TYPES.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true
elseif pkt.type == SCADA_CRDN_TYPES.FAC_CMD then
if pkt.length >= 1 then
local cmd = pkt.data[1]
if cmd == FAC_COMMANDS.SCRAM_ALL then
facility.scram_all()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.STOP then
facility.auto_stop()
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMANDS.START then
if pkt.length == 6 then
---@type coord_auto_config
local config = {
mode = pkt.data[2],
burn_target = pkt.data[3],
charge_target = pkt.data[4],
gen_target = pkt.data[5],
limits = pkt.data[6]
}
_send(SCADA_CRDN_TYPES.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
else
log.debug(log_header .. "CRDN auto start (with configuration) packet length mismatch")
end
else
log.debug(log_header .. "CRDN facility command unknown")
end
else
log.debug(log_header .. "CRDN facility command packet length mismatch")
end
elseif pkt.type == SCADA_CRDN_TYPES.UNIT_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.unit_builds = true
@ -234,13 +265,13 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.length == 3 then
self.out_q.push_data(SV_Q_DATA.SET_BURN, data)
else
log.debug(log_header .. "CRDN command unit burn rate missing option")
log.debug(log_header .. "CRDN unit command burn rate missing option")
end
elseif cmd == UNIT_COMMANDS.SET_WASTE then
if pkt.length == 3 then
unit.set_waste(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit set waste missing option")
log.debug(log_header .. "CRDN unit command set waste missing option")
end
elseif cmd == UNIT_COMMANDS.ACK_ALL_ALARMS then
unit.ack_all()
@ -249,36 +280,29 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if pkt.length == 3 then
unit.ack_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit ack alarm missing alarm id")
log.debug(log_header .. "CRDN unit command ack alarm missing alarm id")
end
elseif cmd == UNIT_COMMANDS.RESET_ALARM then
if pkt.length == 3 then
unit.reset_alarm(pkt.data[3])
else
log.debug(log_header .. "CRDN command unit reset alarm missing alarm id")
log.debug(log_header .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMANDS.SET_GROUP then
if pkt.length == 3 then
facility.set_group(unit.get_id(), pkt.data[3])
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN command unit set group missing group id")
end
elseif cmd == UNIT_COMMANDS.SET_LIMIT then
if pkt.length == 3 then
unit.set_burn_limit(pkt.data[3])
_send(SCADA_CRDN_TYPES.UNIT_CMD, { cmd, uid, pkt.data[3] })
else
log.debug(log_header .. "CRDN command unit set limit missing group id")
log.debug(log_header .. "CRDN unit command set group missing group id")
end
else
log.debug(log_header .. "CRDN command unknown")
log.debug(log_header .. "CRDN unit command unknown")
end
else
log.debug(log_header .. "CRDN command unit invalid")
log.debug(log_header .. "CRDN unit command invalid")
end
else
log.debug(log_header .. "CRDN command unit packet length mismatch")
log.debug(log_header .. "CRDN unit command packet length mismatch")
end
else
log.debug(log_header .. "handler received unexpected SCADA_CRDN packet type " .. pkt.type)
@ -331,6 +355,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
self.retry_times.builds_packet = util.time() + RETRY_PERIOD
_send_fac_builds()
_send_unit_builds()
else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
end
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
@ -339,6 +365,8 @@ function coordinator.new_session(id, in_queue, out_queue, facility)
if cmd.key == CRD_S_DATA.CMD_ACK then
local ack = cmd.val ---@type coord_ack
_send(SCADA_CRDN_TYPES.UNIT_CMD, { ack.cmd, ack.unit, ack.ack })
else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
end
end
end

View File

@ -1,12 +1,12 @@
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local rsctl = require("supervisor.session.rsctl")
local unit = require("supervisor.session.unit")
local HEATING_WATER = 20000
local HEATING_SODIUM = 200000
local PROCESS = types.PROCESS
-- 7.14 kJ per blade for 1 mB of fissile fuel<br/>
-- 2856 FE per blade per 1 mB, 285.6 FE per blade per 0.1 mB (minimum)
@ -15,15 +15,6 @@ local POWER_PER_BLADE = util.joules_to_fe(7140)
local MAX_CHARGE = 0.99
local RE_ENABLE_CHARGE = 0.95
---@alias PROCESS integer
local PROCESS = {
INACTIVE = 1,
SIMPLE = 2,
CHARGE = 3,
GEN_RATE = 4,
BURN_RATE = 5
}
local AUTO_SCRAM = {
NONE = 0,
MATRIX_DC = 1,
@ -31,7 +22,7 @@ local AUTO_SCRAM = {
}
local charge_Kp = 1.0
local charge_Ki = 0.0
local charge_Ki = 0.00001
local charge_Kd = 0.0
local rate_Kp = 1.0
@ -41,8 +32,6 @@ local rate_Kd = 0.0
---@class facility_management
local facility = {}
facility.PROCESS_MODES = PROCESS
-- create a new facility management object
---@param num_reactors integer number of reactor units
---@param cooling_conf table cooling configurations of reactor units
@ -54,9 +43,11 @@ function facility.new(num_reactors, cooling_conf)
-- process control
mode = PROCESS.INACTIVE,
last_mode = PROCESS.INACTIVE,
burn_target = 0.0, -- burn rate target for aggregate burn mode
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
charge_target = 0, -- FE charge target
charge_rate = 0, -- FE/t charge rate target
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)
ascram = false,
@ -67,6 +58,7 @@ function facility.new(num_reactors, cooling_conf)
initial_ramp = true,
waiting_on_ramp = false,
accumulator = 0.0,
saturated = false,
last_error = 0.0,
last_time = 0.0,
-- statistics
@ -214,6 +206,8 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then
if self.last_mode == PROCESS.INACTIVE then
local blade_count = 0
self.max_burn_combined = 0.0
for i = 1, #self.prio_defs do
table.sort(self.prio_defs[i],
---@param a reactor_unit
@ -224,6 +218,7 @@ function facility.new(num_reactors, cooling_conf)
for _, u in pairs(self.prio_defs[i]) do
blade_count = blade_count + u.get_db().blade_count
u.a_engage()
self.max_burn_combined = self.max_burn_combined + (u.get_control_inf().lim_br10 / 10.0)
end
end
@ -247,6 +242,18 @@ function facility.new(num_reactors, cooling_conf)
if state_changed then
self.time_start = now
end
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
-- nothing special to do
elseif self.waiting_on_ramp and _all_units_ramped() then
self.waiting_on_ramp = false
self.time_start = now
end
if not self.waiting_on_ramp then
_allocate_burn_rate(self.burn_target, self.initial_ramp)
end
elseif self.mode == PROCESS.CHARGE then
-- target a level of charge
local error = (self.charge_target - avg_charge) / self.charge_conversion
@ -261,23 +268,32 @@ function facility.new(num_reactors, cooling_conf)
end
if not self.waiting_on_ramp then
self.accumulator = self.accumulator + (avg_charge / self.charge_conversion)
if not self.saturated then
self.accumulator = self.accumulator + ((avg_charge / self.charge_conversion) * (now - self.last_time))
end
local runtime = now - self.time_start
local integral = self.accumulator / runtime
local derivative = (error - self.last_error) / (now - self.last_time)
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 D = 0 -- (charge_Kd * derivative)
local setpoint = P + I + D
-- round setpoint -> setpoint rounded (sp_r)
local sp_r = util.round(setpoint * 10.0) / 10.0
log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%d] }",
runtime, avg_charge, error, integral, setpoint, sp_r, P, I, D))
-- clamp at range -> setpoint clamped (sp_c)
local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined))
_allocate_burn_rate(sp_r, self.initial_ramp)
self.saturated = sp_r ~= sp_c
log.debug(util.sprintf("PROC_CHRG[%f] { CHRG[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%d] }",
runtime, avg_charge, error, integral, setpoint, sp_c, P, I, D))
_allocate_burn_rate(sp_c, self.initial_ramp)
if self.initial_ramp then
self.waiting_on_ramp = true
@ -285,7 +301,7 @@ function facility.new(num_reactors, cooling_conf)
end
elseif self.mode == PROCESS.GEN_RATE then
-- target a rate of generation
local error = (self.charge_rate - avg_inflow) / self.charge_conversion
local error = (self.gen_rate_target - avg_inflow) / self.charge_conversion
local setpoint = 0.0
if state_changed then
@ -303,36 +319,32 @@ function facility.new(num_reactors, cooling_conf)
end
if not self.waiting_on_ramp then
self.accumulator = self.accumulator + (avg_inflow / self.charge_conversion)
if not self.saturated then
self.accumulator = self.accumulator + ((avg_inflow / self.charge_conversion) * (now - self.last_time))
end
local runtime = util.time_s() - self.time_start
local integral = self.accumulator / runtime
local derivative = (error - self.last_error) / (now - self.last_time)
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 D = 0 -- (rate_Kd * derivative)
setpoint = P + I + D
-- round setpoint -> setpoint rounded (sp_r)
local sp_r = util.round(setpoint * 10.0) / 10.0
log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_R[%f] <= P[%f] I[%f] D[%f] }",
runtime, avg_inflow, error, integral, setpoint, sp_r, P, I, D))
-- clamp at range -> setpoint clamped (sp_c)
local sp_c = math.max(0, math.min(sp_r, self.max_burn_combined))
_allocate_burn_rate(sp_r, false)
end
elseif self.mode == PROCESS.BURN_RATE then
-- a total aggregate burn rate
if state_changed then
-- nothing special to do
elseif self.waiting_on_ramp and _all_units_ramped() then
self.waiting_on_ramp = false
self.time_start = now
end
self.saturated = sp_r ~= sp_c
if not self.waiting_on_ramp then
_allocate_burn_rate(self.burn_target, self.initial_ramp)
log.debug(util.sprintf("PROC_RATE[%f] { RATE[%f] ERR[%f] INT[%f] => SP[%f] SP_C[%f] <= P[%f] I[%f] D[%f] }",
runtime, avg_inflow, error, integral, setpoint, sp_c, P, I, D))
_allocate_burn_rate(sp_c, false)
end
end
@ -387,6 +399,83 @@ function facility.new(num_reactors, cooling_conf)
end
end
-- COMMANDS --
-- SCRAM all reactor units
function public.scram_all()
for i = 1, #self.units do
local u = self.units[i] ---@type reactor_unit
u.scram()
end
end
-- stop auto control
function public.auto_stop()
self.mode = PROCESS.INACTIVE
end
-- set automatic control configuration and start the process
---@param config coord_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(config)
local ready = false
-- load up current limits
local limits = {}
for i = 1, num_reactors do
local u = self.units[i] ---@type reactor_unit
limits[i] = u.get_control_inf().lim_br10 * 10
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.SIMPLE) then
self.mode_set = config.mode
end
if (type(config.burn_target) == "number") and config.burn_target >= 0.1 then
self.burn_target = config.burn_target
log.debug("SET BURN TARGET " .. config.burn_target)
end
if (type(config.charge_target) == "number") and config.charge_target >= 0 then
self.charge_target = config.charge_target
log.debug("SET CHARGE TARGET " .. config.charge_target)
end
if (type(config.gen_target) == "number") and config.gen_target >= 0 then
self.gen_rate_target = config.gen_target
log.debug("SET RATE TARGET " .. config.gen_target)
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(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
log.debug("SET UNIT " .. i .. " LIMIT " .. limit)
end
end
end
ready = self.mode_set > 0
if (self.mode_set == PROCESS.CHARGE) and (self.charge_target <= 0) then
ready = false
elseif (self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_target <= 0) then
ready = false
elseif (self.mode_set == PROCESS.BURN_RATE) and (self.burn_target <= 0.1) then
ready = false
end
if ready then self.mode = self.mode_set end
end
return { ready, self.mode_set, self.burn_target, self.charge_target, self.gen_rate_target, limits }
end
-- SETTINGS --
-- set the automatic control group of a unit
@ -424,10 +513,27 @@ function facility.new(num_reactors, cooling_conf)
return build
end
-- get automatic process control status
function public.get_control_status()
return {
self.mode,
self.waiting_on_ramp,
self.ascram,
self.ascram_reason
}
end
-- get RTU statuses
function public.get_rtu_statuses()
local status = {}
-- power averages from induction matricies
status.power = {
self.avg_charge,
self.avg_inflow,
self.avg_outflow
}
-- status of induction matricies (including tanks)
status.induction = {}
for i = 1, #self.induction do

View File

@ -10,6 +10,7 @@ local plc = {}
local PROTOCOLS = comms.PROTOCOLS
local RPLC_TYPES = comms.RPLC_TYPES
local SCADA_MGMT_TYPES = comms.SCADA_MGMT_TYPES
local PLC_AUTO_ACK = comms.PLC_AUTO_ACK
local UNIT_COMMANDS = comms.UNIT_COMMANDS
@ -19,8 +20,9 @@ local print_ts = util.print_ts
local println_ts = util.println_ts
-- retry time constants in ms
local INITIAL_WAIT = 1500
local RETRY_PERIOD = 1000
local INITIAL_WAIT = 1500
local INITIAL_AUTO_WAIT = 1000
local RETRY_PERIOD = 1000
local PLC_S_CMDS = {
SCRAM = 1,
@ -440,6 +442,21 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
cmd = UNIT_COMMANDS.RESET_RPS,
ack = ack
})
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
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
elseif ack == PLC_AUTO_ACK.ZERO_DIS_WAIT then
end
else
log.debug(log_header .. "RPLC automatic burn rate ack packet length mismatch")
end
else
log.debug(log_header .. "handler received unsupported RPLC packet type " .. pkt.type)
end
@ -517,6 +534,11 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
---@param engage boolean true to engage the lockout
function public.auto_lock(engage)
self.auto_lock = engage
-- stop retrying a burn rate command
if engage then
self.acks.burn_rate = true
end
end
-- set the burn rate on behalf of automatic control
@ -583,6 +605,8 @@ 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, {})
else
log.warning(log_header .. "unsupported command received in in_queue (this is a bug)")
end
elseif message.qtype == mqueue.TYPE.DATA then
-- instruction with body
@ -613,13 +637,20 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
end
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
-- set automatic burn rate
cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place
if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.commanded_burn_rate = cmd.val
self.acks.burn_rate = false
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
if self.auto_lock then
cmd.val = math.floor(cmd.val * 10) / 10 -- round to 10ths place
if cmd.val > 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.commanded_burn_rate = cmd.val
-- this is only for manual control, only retry auto ramps
self.acks.burn_rate = not self.ramping_rate
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
end
else
log.warning(log_header .. "unsupported data command received in in_queue (this is a bug)")
end
end
end
@ -685,7 +716,12 @@ function plc.new_session(id, for_reactor, in_queue, out_queue)
if not self.acks.burn_rate then
if rtimes.burn_rate_req - util.time() <= 0 then
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
if self.auto_lock then
_send(RPLC_TYPES.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
else
_send(RPLC_TYPES.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
rtimes.burn_rate_req = util.time() + RETRY_PERIOD
end
end

View File

@ -450,6 +450,13 @@ function unit.new(for_reactor, num_boilers, num_turbines)
-- OPERATIONS --
-- queue a command to SCRAM the reactor
function public.scram()
if self.plc_s ~= nil then
self.plc_s.in_queue.push_command(PLC_S_CMDS.SCRAM)
end
end
-- acknowledge all alarms (if possible)
function public.ack_all()
for i = 1, #self.db.alarm_states do

View File

@ -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.6"
local SUPERVISOR_VERSION = "beta-v0.9.7"
local print = util.print
local println = util.println