Compare commits

...

36 Commits

Author SHA1 Message Date
Mikayla
afc38e7e7a Merge pull request #490 from MikaylaFischler/devel
2024.05.15 Release
2024-05-15 17:22:18 -04:00
Mikayla Fischler
41b7a68f3e #487 stop retrying failed disable when needing to enable 2024-05-14 20:10:21 -04:00
Mikayla Fischler
8968ebede3 #486 fixed pcall messages for newer CC versions 2024-05-14 20:00:34 -04:00
Mikayla
50a9168b06 Merge pull request #489 from MikaylaFischler/pocket-alpha-dev
Pocket Update
2024-05-12 18:27:03 -04:00
Mikayla Fischler
76e85da9d5 version increments and small fix 2024-05-12 18:19:21 -04:00
Mikayla Fischler
be560cd532 luacheck fixes 2024-05-12 15:19:01 -04:00
Mikayla Fischler
2b0a536292 #200 pocket RCS overview 2024-05-12 15:14:58 -04:00
Mikayla Fischler
afed6f514d removed stray space in annunciator Coolant Level Low 2024-05-12 15:05:36 -04:00
Mikayla Fischler
3e6a0a8869 #200 updated RPS indicator text 2024-05-12 14:20:48 -04:00
Mikayla Fischler
b99cf19be0 #200 placeholder for alarm page, start of RCS page 2024-05-12 13:36:46 -04:00
Mikayla Fischler
7f009f9c86 #200 RPS view on pocket 2024-05-12 13:28:34 -04:00
Mikayla Fischler
6a8ed311f3 #200 functioning pocket unit overview 2024-05-11 19:19:52 -04:00
Mikayla Fischler
c181142f75 added network details to about app 2024-05-11 15:03:14 -04:00
Mikayla Fischler
0cb964a177 diagnostic disables 2024-05-10 19:18:21 -04:00
Mikayla Fischler
3cd832ca20 sidebar updates 2024-05-10 19:17:52 -04:00
Mikayla Fischler
0c6c7bf9a5 #200 work in progress unit views and sidebar/app updates 2024-05-09 23:05:55 -04:00
Mikayla Fischler
3bfcb1d83c #485 fixed assertion with height auto incrementing y when inheriting height 2024-05-04 13:50:53 -04:00
Mikayla
4fe6792804 Merge pull request #483 from MikaylaFischler/devel
2024.04.30 Release
2024-04-30 20:35:24 -04:00
Mikayla Fischler
25dc47d520 fixed recording bad stats on induction matrix faults 2024-04-30 20:28:07 -04:00
Mikayla Fischler
f958b0e3b7 fixed at max i/o indicator 2024-04-30 20:27:04 -04:00
Mikayla Fischler
f621ff2482 added some value inits and unit labels 2024-04-30 18:54:01 -04:00
Mikayla Fischler
eb45ff899b #455 calculate reactor temp high limit 2024-04-29 22:03:54 -04:00
Mikayla
e91fd2fcaa Merge pull request #482 from MikaylaFischler/412-additional-matrix-integrations
412 additional matrix integrations
2024-04-28 13:08:51 -04:00
Mikayla Fischler
d35b824458 luacheck fix and cleanup 2024-04-28 13:08:16 -04:00
Mikayla Fischler
165d1497f8 reverted test change that got committed 2024-04-28 02:01:40 -04:00
Mikayla Fischler
50bf057ca6 #412 optionally disable SPS at low power 2024-04-28 02:01:21 -04:00
Mikayla Fischler
6f768ef6b3 #469 made ETA tolerant to induction matrix capacity changes 2024-04-28 01:26:44 -04:00
Mikayla Fischler
826086951e return zero on mov_avg compute if no samples 2024-04-27 19:50:35 -04:00
Mikayla Fischler
35bf56663f #469 induction matrix charge ETAs and misc cleanup/updates 2024-04-27 16:27:01 -04:00
Mikayla Fischler
7b8cea4a5c Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 13:57:47 -04:00
Mikayla Fischler
51d4a22532 #478 simplified reactor PLC reactor formed handling 2024-04-21 13:55:39 -04:00
Mikayla Fischler
fb85c2f05b RTU configurator updates for redstone I/O clarity 2024-04-21 13:54:14 -04:00
Mikayla Fischler
712d018806 Merge branch 'devel' into 412-additional-matrix-integrations 2024-04-21 12:09:53 -04:00
Mikayla Fischler
00a8d64a88 fixed coordinator not showing FAIL on unit count mismatch when connecting 2024-04-20 20:38:55 -04:00
Mikayla Fischler
d9efd5b8d2 #412 updates to RSIO for induction matrix low, high, and analog charge level 2024-04-20 16:32:18 -04:00
Mikayla Fischler
a786404092 #476 fixed PPM not initing fault counter in __index handler when a function is found 2024-04-16 15:55:40 -04:00
40 changed files with 1543 additions and 459 deletions

View File

@@ -1449,9 +1449,11 @@ function configurator.configure(start_code, message)
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.gen_mon_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.gen_mon_list()
elseif event == "monitor_resize" then

View File

@@ -348,6 +348,7 @@ function coordinator.comms(version, nic, sv_watchdog)
ok = false
elseif self.sv_config_err then
self.est_task_done(false)
coordinator.log_comms("supervisor unit count does not match coordinator unit count, check configs")
ok = false
elseif (os.clock() - self.est_last) > 1.0 then

View File

@@ -92,6 +92,7 @@ function iocontrol.init(conf, comms, temp_scale)
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
auto_sps_disabled = false,
radiation = types.new_zero_radiation_reading(),
@@ -227,6 +228,8 @@ function iocontrol.init(conf, comms, temp_scale)
---@class ioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = { boilers = {}, turbines = {} },
num_boilers = 0,
num_turbines = 0,
@@ -318,12 +321,14 @@ function iocontrol.init(conf, comms, temp_scale)
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false })
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false })
end
-- create tank tables
@@ -593,7 +598,7 @@ function iocontrol.update_facility_status(status)
local ctl_status = status[1]
if type(ctl_status) == "table" and #ctl_status == 16 then
if type(ctl_status) == "table" and #ctl_status == 17 then
fac.all_sys_ok = ctl_status[1]
fac.auto_ready = ctl_status[2]
@@ -644,9 +649,11 @@ function iocontrol.update_facility_status(status)
fac.auto_current_waste_product = ctl_status[15]
fac.auto_pu_fallback_active = ctl_status[16]
fac.auto_sps_disabled = ctl_status[17]
fac.ps.publish("current_waste_product", fac.auto_current_waste_product)
fac.ps.publish("pu_fallback_active", fac.auto_pu_fallback_active)
fac.ps.publish("sps_disabled_low_power", fac.auto_sps_disabled)
else
log.debug(log_header .. "control status not a table or length mismatch")
valid = false
@@ -663,10 +670,27 @@ function iocontrol.update_facility_status(status)
fac.rtu_count = rtu_statuses.count
-- power statistics
if type(rtu_statuses.power) == "table" then
fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1])
fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2])
fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3])
if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then
local data = fac.induction_data_tbl[1] ---@type imatrix_session_db
local ps = fac.induction_ps_tbl[1] ---@type psil
local chg = tonumber(rtu_statuses.power[1])
local in_f = tonumber(rtu_statuses.power[2])
local out_f = tonumber(rtu_statuses.power[3])
local eta = tonumber(rtu_statuses.power[4])
ps.publish("avg_charge", chg)
ps.publish("avg_inflow", in_f)
ps.publish("avg_outflow", out_f)
ps.publish("eta_ms", eta)
ps.publish("is_charging", in_f > out_f)
ps.publish("is_discharging", out_f > in_f)
if data and data.build then
local cap = util.joules_to_fe(data.build.transfer_cap)
ps.publish("at_max_io", in_f >= cap or out_f >= cap)
end
else
log.debug(log_header .. "power statistics list not a table")
valid = false
@@ -877,6 +901,7 @@ function iocontrol.update_unit_statuses(statuses)
end
if #reactor_status == 0 then
unit.connected = false
unit.unit_ps.publish("computed_status", 1) -- disconnected
elseif #reactor_status == 3 then
local mek_status = reactor_status[1]
@@ -936,6 +961,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish(key, val)
end
end
unit.connected = true
else
log.debug(log_header .. "reactor status length mismatch")
valid = false
@@ -950,7 +977,10 @@ function iocontrol.update_unit_statuses(statuses)
local boil_sum = 0
for id = 1, #unit.boiler_ps_tbl do
if rtu_statuses.boilers[id] == nil then
local connected = rtu_statuses.boilers[id] ~= nil
unit.rtu_hw.boilers[id].connected = connected
if not connected then
-- disconnected
unit.boiler_ps_tbl[id].publish("computed_status", 1)
end
@@ -962,6 +992,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.boiler_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(boiler, data, ps)
unit.rtu_hw.boilers[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted
@@ -993,7 +1024,10 @@ function iocontrol.update_unit_statuses(statuses)
local flow_sum = 0
for id = 1, #unit.turbine_ps_tbl do
if rtu_statuses.turbines[id] == nil then
local connected = rtu_statuses.turbines[id] ~= nil
unit.rtu_hw.turbines[id].connected = connected
if not connected then
-- disconnected
unit.turbine_ps_tbl[id].publish("computed_status", 1)
end
@@ -1005,6 +1039,7 @@ function iocontrol.update_unit_statuses(statuses)
local ps = unit.turbine_ps_tbl[id] ---@type psil
local rtu_faulted = _record_multiblock_status(turbine, data, ps)
unit.rtu_hw.turbines[id].faulted = rtu_faulted
if rtu_faulted then
ps.publish("computed_status", 3) -- faulted

View File

@@ -29,7 +29,8 @@ local self = {
gen_target = 0.0,
limits = {},
waste_product = PRODUCT.PLUTONIUM,
pu_fallback = false
pu_fallback = false,
sps_low_power = false
},
waste_modes = {},
priority_groups = {}
@@ -65,6 +66,7 @@ function process.init(iocontrol, coord_comms)
ctl_proc.limits = config.limits
ctl_proc.waste_product = config.waste_product
ctl_proc.pu_fallback = config.pu_fallback
ctl_proc.sps_low_power = config.sps_low_power
self.io.facility.ps.publish("process_mode", ctl_proc.mode)
self.io.facility.ps.publish("process_burn_target", ctl_proc.burn_target)
@@ -72,6 +74,7 @@ function process.init(iocontrol, coord_comms)
self.io.facility.ps.publish("process_gen_target", ctl_proc.gen_target)
self.io.facility.ps.publish("process_waste_product", ctl_proc.waste_product)
self.io.facility.ps.publish("process_pu_fallback", ctl_proc.pu_fallback)
self.io.facility.ps.publish("process_sps_low_power", ctl_proc.sps_low_power)
for id = 1, math.min(#ctl_proc.limits, self.io.facility.num_units) do
local unit = self.io.units[id] ---@type ioctl_unit
@@ -83,6 +86,7 @@ function process.init(iocontrol, coord_comms)
-- notify supervisor of auto waste config
self.comms.send_fac_command(FAC_COMMAND.SET_WASTE_MODE, ctl_proc.waste_product)
self.comms.send_fac_command(FAC_COMMAND.SET_PU_FB, ctl_proc.pu_fallback)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, ctl_proc.sps_low_power)
end
-- unit waste states
@@ -259,6 +263,18 @@ function process.set_pu_fallback(enabled)
_write_auto_config()
end
-- set automatic process control SPS usage at low power
---@param enabled boolean whether to enable SPS usage at low power
function process.set_sps_low_power(enabled)
self.comms.send_fac_command(FAC_COMMAND.SET_SPS_LP, enabled)
log.debug(util.c("PROCESS: SET SPS LOW POWER ", enabled))
-- update config table and save
self.control_states.process.sps_low_power = enabled
_write_auto_config()
end
-- save process control settings
---@param mode PROCESS control mode
---@param burn_target number burn rate target

View File

@@ -138,21 +138,26 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout)
}
_send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_UNITS then
local data = {}
elseif pkt.type == CRDN_TYPE.API_GET_UNIT then
if pkt.length == 1 and type(pkt.data[1]) == "number" then
local u = db.units[pkt.data[1]] ---@type ioctl_unit
for i = 1, #db.units do
local u = db.units[i] ---@type ioctl_unit
table.insert(data, {
u.unit_id,
u.num_boilers,
u.num_turbines,
u.num_snas,
u.has_tank
})
if u then
local data = {
u.unit_id,
u.connected,
u.rtu_hw,
u.alarms,
u.annunciator,
u.reactor_data,
u.boiler_data_tbl,
u.turbine_data_tbl,
u.tank_data_tbl
}
_send(CRDN_TYPE.API_GET_UNIT, data)
end
end
_send(CRDN_TYPE.API_GET_UNITS, data)
else
log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type)
end

View File

@@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer")
local sounder = require("coordinator.sounder")
local threads = require("coordinator.threads")
local COORDINATOR_VERSION = "v1.4.2"
local COORDINATOR_VERSION = "v1.4.6"
local CHUNK_LOAD_DELAY_S = 30.0

View File

@@ -9,6 +9,7 @@ local Rectangle = require("graphics.elements.rectangle")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local IndicatorLight = require("graphics.elements.indicators.light")
local PowerIndicator = require("graphics.elements.indicators.power")
local StateIndicator = require("graphics.elements.indicators.state")
local VerticalBar = require("graphics.elements.indicators.vbar")
@@ -26,9 +27,13 @@ local ALIGN = core.ALIGN
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
local ind_yel = style.ind_yel
local ind_wht = style.ind_wht
local title = "INDUCTION MATRIX"
if type(id) == "number" then title = title .. id end
@@ -42,45 +47,47 @@ local function new_view(root, x, y, data, ps, id)
local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14}
local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg}
local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg}
status.register(ps, "computed_status", status.update)
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end)
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end)
avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=style.theme.label_fg}
local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg}
local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg}
local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg}
local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg}
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
cells.register(ps, "cells", cells.update)
providers.register(ps, "providers", providers.update)
fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end)
local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht}
local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht}
local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel}
chging.register(ps, "is_charging", chging.update)
dischg.register(ps, "is_discharging", dischg.update)
max_io.register(ps, "at_max_io", max_io.update)
local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4}
local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1}
local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1}
TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg}
TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg}
TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg}
local function calc_saturation(val)
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
@@ -91,6 +98,49 @@ local function new_view(root, x, y, data, ps, id)
charge.register(ps, "energy_fill", charge.update)
in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end)
out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end)
local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_ms", function (eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
eta.set_value(str)
end)
end
return new_view

View File

@@ -341,31 +341,25 @@ local function new_view(root, x, y)
status.register(facility.ps, "current_waste_product", status.update)
local waste_prod = RadioButton{parent=rect,x=2,y=3,options=style.waste.options,callback=process.set_process_waste,radio_colors=cpair(style.theme.accent_dark,style.theme.accent_light),select_color=colors.brown}
local pu_fallback = Checkbox{parent=rect,x=2,y=7,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.green,style.theme.checkbox_bg)}
waste_prod.register(facility.ps, "process_waste_product", waste_prod.set_value)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
local fb_active = IndicatorLight{parent=rect,x=2,y=9,label="Fallback Active",colors=ind_wht}
local fb_active = IndicatorLight{parent=rect,x=2,y=7,label="Fallback Active",colors=ind_wht}
local sps_disabled = IndicatorLight{parent=rect,x=2,y=8,label="SPS Disabled LC",colors=ind_yel}
fb_active.register(facility.ps, "pu_fallback_active", fb_active.update)
sps_disabled.register(facility.ps, "sps_disabled_low_power", sps_disabled.update)
TextBox{parent=rect,x=2,y=11,text="Plutonium Rate",height=1,width=17,fg_bg=style.label}
local pu_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
local pu_fallback = Checkbox{parent=rect,x=2,y=10,label="Pu Fallback",callback=process.set_pu_fallback,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
TextBox{parent=rect,x=2,y=14,text="Polonium Rate",height=1,width=17,fg_bg=style.label}
local po_rate = DataIndicator{parent=rect,x=2,label="",unit="mB/t",format="%12.2f",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
TextBox{parent=rect,x=2,y=12,height=3,text="Switch to Pu when SNAs cannot keep up with waste.",fg_bg=style.label}
TextBox{parent=rect,x=2,y=17,text="Antimatter Rate",height=1,width=17,fg_bg=style.label}
local am_rate = DataIndicator{parent=rect,x=2,label="",unit="\xb5B/t",format="%12d",value=0,lu_colors=lu_cpair,fg_bg=s_field,width=17}
local lc_sps = Checkbox{parent=rect,x=2,y=16,label="Low Charge SPS",callback=process.set_sps_low_power,box_fg_bg=cpair(colors.brown,style.theme.checkbox_bg)}
pu_rate.register(facility.ps, "pu_rate", pu_rate.update)
po_rate.register(facility.ps, "po_rate", po_rate.update)
am_rate.register(facility.ps, "am_rate", am_rate.update)
TextBox{parent=rect,x=2,y=18,height=3,text="Use SPS at low charge, otherwise switches to Po.",fg_bg=style.label}
local sna_count = DataIndicator{parent=rect,x=2,y=20,label="Linked SNAs:",format="%4d",value=0,lu_colors=lu_cpair,width=17}
sna_count.register(facility.ps, "sna_count", sna_count.update)
pu_fallback.register(facility.ps, "process_pu_fallback", pu_fallback.set_value)
lc_sps.register(facility.ps, "process_sps_low_power", lc_sps.set_value)
end
return new_view

View File

@@ -183,7 +183,7 @@ local function init(parent, id)
local rad_wrn = IndicatorLight{parent=annunciator,label="Radiation Warning",colors=ind_yel}
local r_rtrip = IndicatorLight{parent=annunciator,label="RCP Trip",colors=ind_red}
local r_cflow = IndicatorLight{parent=annunciator,label="RCS Flow Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_clow = IndicatorLight{parent=annunciator,label="Coolant Level Low",colors=ind_yel}
local r_temp = IndicatorLight{parent=annunciator,label="Reactor Temp. High",colors=ind_red}
local r_rhdt = IndicatorLight{parent=annunciator,label="Reactor High Delta T",colors=ind_yel}
local r_firl = IndicatorLight{parent=annunciator,label="Fuel Input Rate Low",colors=ind_yel}

View File

@@ -7,7 +7,7 @@ local flasher = require("graphics.flasher")
local core = {}
core.version = "2.2.3"
core.version = "2.2.4"
core.flasher = flasher
core.events = events

View File

@@ -198,6 +198,9 @@ function element.new(args, child_offset_x, child_offset_y)
---@param offset_y integer y offset for mouse events
---@param next_y integer next line if no y was provided
function protected.prepare_template(offset_x, offset_y, next_y)
-- don't auto incrememnt y if inheriting height, that would cause an assertion
next_y = util.trinary(args.height == nil, 1, next_y)
-- record offsets in case there is a reposition
self.offset_x = offset_x
self.offset_y = offset_y

View File

@@ -30,7 +30,7 @@ local function app_button(args)
element.assert(type(args.app_fg_bg) == "table", "app_fg_bg is a required field")
args.height = 4
args.width = 5
args.width = 7
-- create new graphics element base object
local e = element.new(args)
@@ -46,7 +46,7 @@ local function app_button(args)
end
-- draw icon
e.w_set_cur(1, 1)
e.w_set_cur(2, 1)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write("\x9f\x83\x83\x83")
@@ -55,16 +55,16 @@ local function app_button(args)
e.w_write("\x90")
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_set_cur(1, 2)
e.w_set_cur(2, 2)
e.w_write("\x95 ")
e.w_set_fgd(bkg)
e.w_set_bkg(fgd)
e.w_write("\x95")
e.w_set_cur(1, 3)
e.w_set_cur(2, 3)
e.w_write("\x82\x8f\x8f\x8f\x81")
-- write the icon text
e.w_set_cur(3, 2)
e.w_set_cur(4, 2)
e.w_set_fgd(fgd)
e.w_set_bkg(bkg)
e.w_write(args.text)

View File

@@ -8,13 +8,7 @@ local element = require("graphics.element")
local MOUSE_CLICK = core.events.MOUSE_CLICK
---@class sidebar_tab
---@field char string character identifier
---@field color cpair tab colors (fg/bg)
---@class sidebar_args
---@field tabs table sidebar tab options
---@field callback function function to call on tab change
---@field parent graphics_element
---@field id? string element id
---@field x? integer 1 if omitted
@@ -27,21 +21,16 @@ local MOUSE_CLICK = core.events.MOUSE_CLICK
---@param args sidebar_args
---@return graphics_element element, element_id id
local function sidebar(args)
element.assert(type(args.tabs) == "table", "tabs is a required field")
element.assert(#args.tabs > 0, "at least one tab is required")
element.assert(type(args.callback) == "function", "callback is a required field")
args.width = 3
-- create new graphics element base object
local e = element.new(args)
element.assert(e.frame.h >= (#args.tabs * 3), "height insufficent to display all tabs")
-- default to 1st tab
e.value = 1
local was_pressed = false
local tabs = {}
-- show the button state
---@param pressed? boolean if the currently selected tab should appear as actively pressed
@@ -51,10 +40,18 @@ local function sidebar(args)
was_pressed = pressed
pressed_idx = pressed_idx or e.value
for i = 1, #args.tabs do
local tab = args.tabs[i] ---@type sidebar_tab
-- clear
e.w_set_fgd(e.fg_bg.fgd)
e.w_set_bkg(e.fg_bg.bkg)
for y = 1, e.frame.h do
e.w_set_cur(1, y)
e.w_write(" ")
end
local y = ((i - 1) * 3) + 1
-- draw tabs
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
local y = tab.y_start
e.w_set_cur(1, y)
@@ -66,13 +63,29 @@ local function sidebar(args)
e.w_set_bkg(tab.color.bkg)
end
e.w_write(" ")
e.w_set_cur(1, y + 1)
if e.value == i then
e.w_write(" " .. tab.char .. "\x10")
else e.w_write(" " .. tab.char .. " ") end
e.w_set_cur(1, y + 2)
e.w_write(" ")
if tab.tall then
e.w_write(" ")
e.w_set_cur(1, y + 1)
end
e.w_write(tab.label)
if tab.tall then
e.w_set_cur(1, y + 2)
e.w_write(" ")
end
end
end
-- determine which tab was pressed
---@param y integer y coordinate
local function find_tab(y)
for i = 1, #tabs do
local tab = tabs[i] ---@type sidebar_tab
if y >= tab.y_start and y <= tab.y_end then
return i
end
end
end
@@ -81,23 +94,25 @@ local function sidebar(args)
function e.handle_mouse(event)
-- determine what was pressed
if e.enabled then
local cur_idx = math.ceil(event.current.y / 3)
local ini_idx = math.ceil(event.initial.y / 3)
local cur_idx = find_tab(event.current.y)
local ini_idx = find_tab(event.initial.y)
local tab = tabs[cur_idx]
if args.tabs[cur_idx] ~= nil then
-- handle press if a callback was provided
if tab ~= nil and type(tab.callback) == "function" then
if event.type == MOUSE_CLICK.TAP then
e.value = cur_idx
draw(true)
-- show as unpressed in 0.25 seconds
tcd.dispatch(0.25, function () draw(false) end)
args.callback(e.value)
tab.callback()
elseif event.type == MOUSE_CLICK.DOWN then
draw(true, cur_idx)
elseif event.type == MOUSE_CLICK.UP then
if cur_idx == ini_idx and e.in_frame_bounds(event.current.x, event.current.y) then
e.value = cur_idx
draw(false)
args.callback(e.value)
tab.callback()
else draw(false) end
end
elseif event.type == MOUSE_CLICK.UP then
@@ -113,6 +128,35 @@ local function sidebar(args)
draw(false)
end
-- update the sidebar navigation options
---@param items table sidebar entries
function e.on_update(items)
local next_y = 1
tabs = {}
for i = 1, #items do
local item = items[i]
local height = util.trinary(item.tall, 3, 1)
---@class sidebar_tab
local entry = {
y_start = next_y, ---@type integer
y_end = next_y + height - 1, ---@type integer
tall = item.tall, ---@type boolean
label = item.label, ---@type string
color = item.color, ---@type cpair
callback = item.callback ---@type function|nil
}
next_y = next_y + height
tabs[i] = entry
end
draw()
end
-- element redraw
e.redraw = draw

View File

@@ -9,7 +9,7 @@ local element = require("graphics.element")
---@class icon_indicator_args
---@field label string indicator label
---@field states table state color and symbol table
---@field value? integer default state, defaults to 1
---@field value? integer|boolean default state, defaults to 1 (true = 2, false = 1)
---@field min_label_width? integer label length if omitted
---@field parent graphics_element
---@field id? string element id
@@ -33,6 +33,7 @@ local function icon(args)
local e = element.new(args)
e.value = args.value or 1
if e.value == true then e.value = 2 end
-- state blit strings
local state_blit_cmds = {}
@@ -47,8 +48,11 @@ local function icon(args)
end
-- on state change
---@param new_state integer indicator state
---@param new_state integer|boolean indicator state
function e.on_update(new_state)
new_state = new_state or 1
if new_state == true then new_state = 2 end
local blit_cmd = state_blit_cmds[new_state]
e.value = new_state
e.w_set_cur(1, 1)
@@ -56,7 +60,7 @@ local function icon(args)
end
-- set indicator state
---@param val integer indicator state
---@param val integer|boolean indicator state
function e.set_value(val) e.on_update(val) end
-- element redraw

View File

@@ -5,8 +5,10 @@
local log = require("scada-common.log")
local psil = require("scada-common.psil")
local types = require("scada-common.types")
local util = require("scada-common.util")
local ALARM = types.ALARM
local ALARM_STATE = types.ALARM_STATE
---@todo nominal trip time is ping (0ms to 10ms usually)
local WARN_TT = 40
@@ -72,6 +74,10 @@ function iocontrol.alloc_nav()
self.pane = root_pane
end
function io.nav.set_sidebar(sidebar)
self.sidebar = sidebar
end
-- register an app
---@param app_id POCKET_APP_ID app ID
---@param container graphics_element element that contains this app (usually a Div)
@@ -79,18 +85,36 @@ function iocontrol.alloc_nav()
function io.nav.register_app(app_id, container, pane)
---@class pocket_app
local app = {
root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page
loaded = false,
load = nil,
cur_page = nil, ---@type nav_tree_page
pane = pane,
paned_pages = {}
paned_pages = {},
sidebar_items = {}
}
app.load = function () app.loaded = true end
-- delayed set of the pane if it wasn't ready at the start
---@param root_pane graphics_element multipane
function app.set_root_pane(root_pane)
app.pane = root_pane
end
function app.set_sidebar(items)
app.sidebar_items = items
if self.sidebar then self.sidebar.update(items) end
end
-- function to run on initial load into memory
---@param on_load function callback
function app.set_on_load(on_load)
app.load = function ()
on_load()
app.loaded = true
end
end
-- if a pane was provided, this will switch between numbered pages
---@param idx integer page index
function app.switcher(idx)
@@ -107,9 +131,8 @@ function iocontrol.alloc_nav()
---@type nav_tree_page
local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} }
if parent == nil then
app.root = page
if app.cur_page == nil then app.cur_page = page end
if parent == nil and app.cur_page == nil then
app.cur_page = page
end
if type(nav_to) == "number" then
@@ -160,9 +183,16 @@ function iocontrol.alloc_nav()
-- open a given app
---@param app_id POCKET_APP_ID
function io.nav.open_app(app_id)
if self.apps[app_id] then
local app = self.apps[app_id] ---@type pocket_app
if app then
if not app.loaded then app.load() end
self.cur_app = app_id
self.pane.set_value(app_id)
if #app.sidebar_items > 0 then
self.sidebar.update(app.sidebar_items)
end
else
log.debug("tried to open unknown app")
end
@@ -227,6 +257,12 @@ function iocontrol.init_core(comms)
alarm_buttons = {},
tone_indicators = {} -- indicators to update from supervisor tone states
}
-- API access
---@class pocket_ioctl_api
io.api = {
get_unit = function (unit) comms.api__get_unit(unit) end
}
end
-- initialize facility-dependent components of pocket iocontrol
@@ -262,6 +298,16 @@ function iocontrol.init_fac(conf, temp_scale)
auto_ramping = false,
auto_saturated = false,
auto_scram = false,
---@type ascram_status
ascram_status = {
matrix_dc = false,
matrix_fill = false,
crit_alarm = false,
radiation = false,
gen_fault = false
},
---@type WASTE_PRODUCT
auto_current_waste_product = types.WASTE_PRODUCT.PLUTONIUM,
auto_pu_fallback_active = false,
@@ -282,11 +328,188 @@ function iocontrol.init_fac(conf, temp_scale)
env_d_ps = psil.create(),
env_d_data = {}
}
-- create induction and SPS tables (currently only 1 of each is supported)
table.insert(io.facility.induction_ps_tbl, psil.create())
table.insert(io.facility.induction_data_tbl, {})
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- determine tank information
if io.facility.tank_mode == 0 then
io.facility.tank_defs = {}
-- on facility tank mode 0, setup tank defs to match unit tank option
for i = 1, conf.num_units do
io.facility.tank_defs[i] = util.trinary(conf.cooling.r_cool[i].TankConnection, 1, 0)
end
io.facility.tank_list = { table.unpack(io.facility.tank_defs) }
else
-- decode the layout of tanks from the connections definitions
local tank_mode = io.facility.tank_mode
local tank_defs = io.facility.tank_defs
local tank_list = { table.unpack(tank_defs) }
local function calc_fdef(start_idx, end_idx)
local first = 4
for i = start_idx, end_idx do
if io.facility.tank_defs[i] == 2 then
if i < first then first = i end
end
end
return first
end
if tank_mode == 1 then
-- (1) 1 total facility tank (A A A A)
local first_fdef = calc_fdef(1, #tank_defs)
for i = 1, #tank_defs do
if i > first_fdef and tank_defs[i] == 2 then
tank_list[i] = 0
end
end
elseif tank_mode == 2 then
-- (2) 2 total facility tanks (A A A B)
local first_fdef = calc_fdef(1, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (i ~= 4) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 3 then
-- (3) 2 total facility tanks (A A B B)
for _, a in pairs({ 1, 3 }) do
local b = a + 1
if (tank_defs[a] == 2) and (tank_defs[b] == 2) then
tank_list[b] = 0
end
end
elseif tank_mode == 4 then
-- (4) 2 total facility tanks (A B B B)
local first_fdef = calc_fdef(2, #tank_defs)
for i = 1, #tank_defs do
if (i ~= 1) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 5 then
-- (5) 3 total facility tanks (A A B C)
local first_fdef = calc_fdef(1, math.min(2, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 3 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 6 then
-- (6) 3 total facility tanks (A B B C)
local first_fdef = calc_fdef(2, math.min(3, #tank_defs))
for i = 1, #tank_defs do
if (not (i == 1 or i == 4)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
elseif tank_mode == 7 then
-- (7) 3 total facility tanks (A B C C)
local first_fdef = calc_fdef(3, #tank_defs)
for i = 1, #tank_defs do
if (not (i == 1 or i == 2)) and (i > first_fdef) and (tank_defs[i] == 2) then
tank_list[i] = 0
end
end
end
io.facility.tank_list = tank_list
end
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
table.insert(io.facility.tank_ps_tbl, psil.create())
table.insert(io.facility.tank_data_tbl, {})
end
end
-- create unit data structures
io.units = {}
for i = 1, conf.num_units do
---@class pioctl_unit
local entry = {
unit_id = i,
connected = false,
rtu_hw = {},
num_boilers = 0,
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
control_state = false,
burn_rate_cmd = 0.0,
radiation = types.new_zero_radiation_reading(),
sna_peak_rate = 0.0,
sna_max_rate = 0.0,
sna_out_rate = 0.0,
waste_mode = types.WASTE_MODE.MANUAL_PLUTONIUM,
waste_product = types.WASTE_PRODUCT.PLUTONIUM,
-- auto control group
a_group = 0,
---@type alarms
alarms = { ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE, ALARM_STATE.INACTIVE },
annunciator = {}, ---@type annunciator
unit_ps = psil.create(),
reactor_data = {}, ---@type reactor_db
boiler_ps_tbl = {},
boiler_data_tbl = {},
turbine_ps_tbl = {},
turbine_data_tbl = {},
tank_ps_tbl = {},
tank_data_tbl = {}
}
-- on other facility modes, overwrite unit TANK option with facility tank defs
if io.facility.tank_mode ~= 0 then
entry.has_tank = conf.cooling.fac_tank_defs[i] > 0
end
-- create boiler tables
for _ = 1, conf.cooling.r_cool[i].BoilerCount do
table.insert(entry.boiler_ps_tbl, psil.create())
table.insert(entry.boiler_data_tbl, {})
end
-- create turbine tables
for _ = 1, conf.cooling.r_cool[i].TurbineCount do
table.insert(entry.turbine_ps_tbl, psil.create())
table.insert(entry.turbine_data_tbl, {})
end
-- create tank tables
if io.facility.tank_defs[i] == 1 then
table.insert(entry.tank_ps_tbl, psil.create())
table.insert(entry.tank_data_tbl, {})
end
entry.num_boilers = #entry.boiler_data_tbl
entry.num_turbines = #entry.turbine_data_tbl
table.insert(io.units, entry)
end
end
-- set network link state
---@param state POCKET_LINK_STATE
function iocontrol.report_link_state(state)
---@param sv_addr integer? supervisor address if linked
---@param api_addr integer? coordinator address if linked
function iocontrol.report_link_state(state, sv_addr, api_addr)
io.ps.publish("link_state", state)
if state == LINK_STATE.API_LINK_ONLY or state == LINK_STATE.UNLINKED then
@@ -296,6 +519,11 @@ function iocontrol.report_link_state(state)
if state == LINK_STATE.SV_LINK_ONLY or state == LINK_STATE.UNLINKED then
io.ps.publish("crd_conn_quality", 0)
end
if state == LINK_STATE.LINKED then
io.ps.publish("sv_addr", sv_addr)
io.ps.publish("api_addr", api_addr)
end
end
-- determine supervisor connection quality (trip time)
@@ -357,6 +585,188 @@ function iocontrol.record_facility_data(data)
return valid
end
-- update unit status data from API_GET_UNIT
---@param data table
function iocontrol.record_unit_data(data)
if type(data[1]) == "number" and io.units[data[1]] then
local unit = io.units[data[1]] ---@type pioctl_unit
unit.connected = data[2]
unit.rtu_hw = data[3]
unit.alarms = data[4]
--#region Annunciator
unit.annunciator = data[5]
local rcs_disconn, rcs_warn, rcs_hazard = false, false, false
for key, val in pairs(unit.annunciator) do
if key == "BoilerOnline" or key == "TurbineOnline" then
-- split up online arrays
local every = true
for id = 1, #val do
every = every and val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if not every then rcs_disconn = true end
unit.unit_ps.publish("U_" .. key, every)
elseif key == "HeatingRateLow" or key == "WaterLevelLow" then
-- split up array for all boilers
local any = false
for id = 1, #val do
any = any or val[id]
unit.boiler_ps_tbl[id].publish(key, val[id])
end
if key == "HeatingRateLow" and any then
rcs_warn = true
elseif key == "WaterLevelLow" and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
elseif key == "SteamDumpOpen" or key == "TurbineOverSpeed" or key == "GeneratorTrip" or key == "TurbineTrip" then
-- split up array for all turbines
local any = false
for id = 1, #val do
any = any or val[id]
unit.turbine_ps_tbl[id].publish(key, val[id])
end
if key == "GeneratorTrip" and any then
rcs_warn = true
elseif (key == "TurbineOverSpeed" or key == "TurbineTrip") and any then
rcs_hazard = true
end
unit.unit_ps.publish("U_" .. key, any)
else
-- non-table fields
unit.unit_ps.publish(key, val)
end
end
local anc = unit.annunciator
rcs_hazard = rcs_hazard or anc.RCPTrip
rcs_warn = rcs_warn or anc.RCSFlowLow or anc.CoolantLevelLow or anc.RCSFault or anc.MaxWaterReturnFeed or
anc.CoolantFeedMismatch or anc.BoilRateMismatch or anc.SteamFeedMismatch or anc.MaxWaterReturnFeed
local rcs_status = 4
if rcs_hazard then
rcs_status = 2
elseif rcs_warn then
rcs_status = 3
elseif rcs_disconn then
rcs_status = 1
end
unit.unit_ps.publish("U_RCS", rcs_status)
--#endregion
--#region Reactor Data
unit.reactor_data = data[6]
local control_status = 1
local reactor_status = 1
local rps_status = 1
if unit.connected then
-- update RPS status
if unit.reactor_data.rps_tripped then
control_status = 2
rps_status = util.trinary(unit.reactor_data.rps_trip_cause == "manual", 3, 2)
else rps_status = 4 end
-- update reactor/control status
if unit.reactor_data.mek_status.status then
reactor_status = 4
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
else
if unit.reactor_data.no_reactor then
reactor_status = 2
elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then
reactor_status = 3
else
reactor_status = 4
end
end
for key, val in pairs(unit.reactor_data) do
if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.rps_status) == "table" then
for key, val in pairs(unit.reactor_data.rps_status) do
unit.unit_ps.publish(key, val)
end
end
if type(unit.reactor_data.mek_status) == "table" then
for key, val in pairs(unit.reactor_data.mek_status) do
unit.unit_ps.publish(key, val)
end
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.unit_ps.publish("U_ReactorStatus", reactor_status)
unit.unit_ps.publish("U_RPS", rps_status)
--#endregion
unit.boiler_data_tbl = data[7]
for id = 1, #unit.boiler_data_tbl do
local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db
local ps = unit.boiler_ps_tbl[id] ---@type psil
local boiler_status = 1
if unit.rtu_hw.boilers[id].connected then
if unit.rtu_hw.boilers[id].faulted then
boiler_status = 3
elseif boiler.formed then
boiler_status = 4
else
boiler_status = 2
end
end
ps.publish("BoilerStatus", boiler_status)
end
unit.turbine_data_tbl = data[8]
for id = 1, #unit.turbine_data_tbl do
local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db
local ps = unit.turbine_ps_tbl[id] ---@type psil
local turbine_status = 1
if unit.rtu_hw.turbines[id].connected then
if unit.rtu_hw.turbines[id].faulted then
turbine_status = 3
elseif turbine.formed then
turbine_status = 4
else
turbine_status = 2
end
end
ps.publish("TurbineStatus", turbine_status)
end
unit.tank_data_tbl = data[9]
end
end
-- get the IO controller database
function iocontrol.get_db() return io end

View File

@@ -119,6 +119,20 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.api.seq_num = self.api.seq_num + 1
end
-- send an API packet to the coordinator
---@param msg_type CRDN_TYPE
---@param msg table
local function _send_api(msg_type, msg)
local s_pkt = comms.scada_packet()
local pkt = comms.crdn_packet()
pkt.make(msg_type, msg)
s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable())
nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt)
self.api.seq_num = self.api.seq_num + 1
end
-- attempt supervisor connection establishment
local function _send_sv_establish()
_send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT })
@@ -192,7 +206,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
end
else
-- linked, all good!
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
end
end
@@ -215,6 +229,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- coordinator get unit data
function public.api__get_unit(unit)
if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end
end
-- parse a packet
---@param side string
---@param sender integer
@@ -304,7 +323,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
if _check_length(packet, 11) then
iocontrol.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNITS then
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 9) then
iocontrol.record_unit_data(packet.data)
end
else _fail_type(packet) end
else
log.debug("discarding coordinator SCADA_CRDN packet before linked")
@@ -358,7 +380,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.api.addr = src_addr
if self.sv.linked then
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
else
iocontrol.report_link_state(LINK_STATE.API_LINK_ONLY)
end
@@ -497,7 +519,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog)
self.sv.addr = src_addr
if self.api.linked then
iocontrol.report_link_state(LINK_STATE.LINKED)
iocontrol.report_link_state(LINK_STATE.LINKED, self.sv.addr, self.api.addr)
else
iocontrol.report_link_state(LINK_STATE.SV_LINK_ONLY)
end

View File

@@ -18,7 +18,7 @@ local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local POCKET_VERSION = "v0.8.0-alpha"
local POCKET_VERSION = "v0.9.0-alpha"
local println = util.println
local println_ts = util.println_ts

View File

@@ -19,6 +19,8 @@ local function create_pages(root)
db.nav.register_app(iocontrol.APP_ID.DUMMY, main).new_page(nil, function () end)
TextBox{parent=main,text="This app is not implemented yet.",x=1,y=2,alignment=core.ALIGN.CENTER}
TextBox{parent=main,text=" pretend something cool is here \x03",x=1,y=10,alignment=core.ALIGN.CENTER,fg_bg=core.cpair(colors.gray,colors.black)}
end
return create_pages

View File

@@ -3,10 +3,12 @@
--
local comms = require("scada-common.comms")
local lockbox = require("lockbox")
local util = require("scada-common.util")
local lockbox = require("lockbox")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local core = require("graphics.core")
@@ -35,8 +37,9 @@ local function create_pages(root)
local about_app = db.nav.register_app(iocontrol.APP_ID.ABOUT, about_root)
local about_page = about_app.new_page(nil, 1)
local fw_page = about_app.new_page(about_page, 2)
local hw_page = about_app.new_page(about_page, 3)
local nt_page = about_app.new_page(about_page, 2)
local fw_page = about_app.new_page(about_page, 3)
local hw_page = about_app.new_page(about_page, 4)
local about = Div{parent=about_root,x=1,y=2}
@@ -46,8 +49,42 @@ local function create_pages(root)
local btn_active = cpair(colors.white, colors.black)
local label = cpair(colors.lightGray, colors.black)
PushButton{parent=about,x=2,y=3,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
PushButton{parent=about,x=2,y=4,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
PushButton{parent=about,x=2,y=3,text="Network >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=nt_page.nav_to}
PushButton{parent=about,x=2,y=4,text="Firmware >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=fw_page.nav_to}
PushButton{parent=about,x=2,y=5,text="Host Details >",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=hw_page.nav_to}
--#region Network Details
local config = pocket.config
local nt_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=nt_div,y=1,text="Network Details",height=1,alignment=ALIGN.CENTER}
PushButton{parent=nt_div,x=2,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=about_page.nav_to}
TextBox{parent=nt_div,x=2,y=3,text="Pocket Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
---@diagnostic disable-next-line: undefined-field
TextBox{parent=nt_div,x=2,text=util.c(os.getComputerID(),":",config.PKT_Channel),height=1,alignment=ALIGN.LEFT}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Supervisor Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local sv = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Coordinator Address",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local coord = TextBox{parent=nt_div,x=2,text="",height=1,alignment=ALIGN.LEFT}
sv.register(db.ps, "sv_addr", function (addr) sv.set_value(util.c(addr, ":", config.SVR_Channel)) end)
coord.register(db.ps, "api_addr", function (addr) coord.set_value(util.c(addr, ":", config.CRD_Channel)) end)
nt_div.line_break()
TextBox{parent=nt_div,x=2,text="Message Authentication",height=1,alignment=ALIGN.LEFT,fg_bg=label}
local auth = util.trinary(type(config.AuthKey) == "string" and string.len(config.AuthKey) > 0, "HMAC-MD5", "None")
TextBox{parent=nt_div,x=2,text=auth,height=1,alignment=ALIGN.LEFT}
--#endregion
--#region Firmware Versions
local fw_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=fw_div,y=1,text="Firmware Versions",height=1,alignment=ALIGN.CENTER}
@@ -81,6 +118,10 @@ local function create_pages(root)
TextBox{parent=fw_list,x=2,text="Lockbox Version",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=fw_list,x=2,text=lockbox.version,height=1,alignment=ALIGN.LEFT}
--#endregion
--#region Host Versions
local hw_div = Div{parent=about_root,x=1,y=2}
TextBox{parent=hw_div,y=1,text="Host Versions",height=1,alignment=ALIGN.CENTER}
@@ -94,7 +135,9 @@ local function create_pages(root)
TextBox{parent=hw_div,x=2,text="Environment",height=1,alignment=ALIGN.LEFT,fg_bg=label}
TextBox{parent=hw_div,x=2,text=_HOST,height=6,alignment=ALIGN.LEFT}
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,fw_div,hw_div}}
--#endregion
local root_pane = MultiPane{parent=about_root,x=1,y=1,panes={about,nt_div,fw_div,hw_div}}
about_app.set_root_pane(root_pane)
end

View File

@@ -38,7 +38,7 @@ local function init(main)
local db = iocontrol.get_db()
-- window header message
TextBox{parent=main,y=1,text="DEV ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
TextBox{parent=main,y=1,text="WIP ALPHA APP S C ",alignment=ALIGN.LEFT,height=1,fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
@@ -71,10 +71,6 @@ local function init(main)
local page_div = Div{parent=main_pane,x=4,y=1}
local sidebar_tabs = {
{ char = "#", color = cpair(colors.black, colors.green) }
}
home_page(page_div)
unit_page(page_div)
@@ -84,13 +80,13 @@ local function init(main)
assert(#db.nav.get_containers() == iocontrol.APP_ID.NUM_APPS, "app IDs were not sequential or some apps weren't registered")
local page_pane = MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()}
db.nav.set_pane(page_pane)
Sidebar{parent=main_pane,x=1,y=1,tabs=sidebar_tabs,fg_bg=cpair(colors.white,colors.gray),callback=db.nav.open_app}
db.nav.set_pane(MultiPane{parent=page_div,x=1,y=1,panes=db.nav.get_containers()})
db.nav.set_sidebar(Sidebar{parent=main_pane,x=1,y=1,height=18,fg_bg=cpair(colors.white,colors.gray)})
PushButton{parent=main_pane,x=1,y=19,text="\x1b",min_width=3,fg_bg=cpair(colors.white,colors.gray),active_fg_bg=cpair(colors.gray,colors.black),callback=db.nav.nav_up}
db.nav.open_app(iocontrol.APP_ID.ROOT)
--#endregion
end

View File

@@ -39,21 +39,26 @@ local function new_view(root)
local function open(id) db.nav.open_app(id) end
app.set_sidebar({
{ label = " #\x10", tall = true, color = core.cpair(colors.black, colors.green), callback = function () open(APP_ID.ROOT) end }
})
local active_fg_bg = cpair(colors.white,colors.gray)
App{parent=apps_1,x=3,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=10,y=2,text="\x17",title="PRC",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=17,y=2,text="\x15",title="CTL",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=3,y=7,text="\x08",title="DEV",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=10,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=17,y=7,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=3,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=7,text="\x08",title="Devices",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.lightGray),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=12,text="\xb6",title="Guide",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=12,text="?",title="About",callback=function()open(APP_ID.ABOUT)end,app_fg_bg=cpair(colors.black,colors.white),active_fg_bg=active_fg_bg}
TextBox{parent=apps_2,text="Diagnostic Apps",x=1,y=2,height=1,alignment=ALIGN.CENTER}
App{parent=apps_2,x=3,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=10,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=17,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=2,y=4,text="\x0f",title="Alarm",callback=function()open(APP_ID.ALARMS)end,app_fg_bg=cpair(colors.black,colors.red),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=9,y=4,text="\x1e",title="LoopT",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.cyan),active_fg_bg=active_fg_bg}
App{parent=apps_2,x=16,y=4,text="@",title="Comps",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
return main
end

View File

@@ -2,14 +2,56 @@
-- Unit Overview Page
--
local util = require("scada-common.util")
-- local log = require("scada-common.log")
local iocontrol = require("pocket.iocontrol")
local core = require("graphics.core")
local Div = require("graphics.elements.div")
local MultiPane = require("graphics.elements.multipane")
local TextBox = require("graphics.elements.textbox")
local DataIndicator = require("graphics.elements.indicators.data")
local IconIndicator = require("graphics.elements.indicators.icon")
-- local RadIndicator = require("graphics.elements.indicators.rad")
-- local VerticalBar = require("graphics.elements.indicators.vbar")
local PushButton = require("graphics.elements.controls.push_button")
local ALIGN = core.ALIGN
local cpair = core.cpair
local basic_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.yellow), symbol = "\x1e" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
local mode_states = {
{ color = cpair(colors.black, colors.lightGray), symbol = "\x07" },
{ color = cpair(colors.black, colors.red), symbol = "-" },
{ color = cpair(colors.black, colors.green), symbol = "+" },
{ color = cpair(colors.black, colors.purple), symbol = "A" }
}
local emc_ind_s = {
{ color = cpair(colors.black, colors.gray), symbol = "-" },
{ color = cpair(colors.black, colors.white), symbol = "\x07" },
{ color = cpair(colors.black, colors.green), symbol = "+" }
}
local red_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.red), symbol = "-" }
}
local yel_ind_s = {
{ color = cpair(colors.black, colors.lightGray), symbol = "+" },
{ color = cpair(colors.black, colors.yellow), symbol = "-" }
}
-- new unit page view
---@param root graphics_element parent
@@ -19,11 +61,259 @@ local function new_view(root)
local main = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main)
app.new_page(nil, function () end)
TextBox{parent=main,y=2,text="UNITS",height=1,alignment=ALIGN.CENTER}
TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER}
TextBox{parent=main,y=4,text="work in progress",height=1,alignment=ALIGN.CENTER}
TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER}
local btn_fg_bg = cpair(colors.yellow, colors.black)
local btn_active = cpair(colors.white, colors.black)
-- local label = cpair(colors.lightGray, colors.black)
local nav_links = {}
local function set_sidebar(id)
-- local unit = db.units[id] ---@type pioctl_unit
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(iocontrol.APP_ID.ROOT) end },
{ label = "U-" .. id, color = core.cpair(colors.black, colors.yellow), callback = function () app.switcher(id) end },
{ label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = nav_links[id].alarm },
{ label = "RPS", tall = true, color = core.cpair(colors.black, colors.cyan), callback = nav_links[id].rps },
-- { label = " R ", color = core.cpair(colors.black, colors.lightGray), callback = function () end },
{ label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = nav_links[id].rcs },
}
-- for i = 1, unit.num_boilers do
-- table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightBlue), callback = function () end })
-- end
-- for i = 1, unit.num_turbines do
-- table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.white), callback = function () end })
-- end
app.set_sidebar(list)
end
local function load()
local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2}
local panes = {}
local active_unit = 1
-- create all page divs
for _ = 1, db.facility.num_units do
local div = Div{parent=page_div}
table.insert(panes, div)
table.insert(nav_links, {})
end
-- previous unit
local function prev(x)
active_unit = util.trinary(x == 1, db.facility.num_units, x - 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
-- next unit
local function next(x)
active_unit = util.trinary(x == db.facility.num_units, 1, x + 1)
app.switcher(active_unit)
set_sidebar(active_unit)
end
for i = 1, db.facility.num_units do
local u_div = panes[i] ---@type graphics_element
local unit = db.units[i] ---@type pioctl_unit
local u_ps = unit.unit_ps
-- refresh data callback, every 500ms it will re-send the query
local last_update = 0
local function update()
if util.time_ms() - last_update >= 500 then
db.api.get_unit(i)
last_update = util.time_ms()
end
end
--#region Main Unit Overview
local u_page = app.new_page(nil, i)
u_page.tasks = { update }
TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER}
PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end}
PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end}
local type = util.trinary(unit.num_boilers > 0, "Sodium Cooled Reactor", "Boiling Water Reactor")
TextBox{parent=u_div,y=3,text=type,height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
local lu_col = cpair(colors.lightGray, colors.lightGray)
local text_fg = cpair(colors.white, colors._INHERIT)
local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg}
local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states}
rate.register(u_ps, "act_burn_rate", rate.update)
temp.register(u_ps, "temp", temp.update)
ctrl.register(u_ps, "U_ControlStatus", ctrl.update)
u_div.line_break()
local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states}
local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states}
rct.register(u_ps, "U_ReactorStatus", rct.update)
rps.register(u_ps, "U_RPS", rps.update)
u_div.line_break()
local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states}
rcs.register(u_ps, "U_RCS", rcs.update)
for b = 1, unit.num_boilers do
local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states}
blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update)
end
for t = 1, unit.num_turbines do
local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states}
tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update)
end
--#endregion
--#region Alarms Tab
local alm_div = Div{parent=page_div}
table.insert(panes, alm_div)
local alm_page = app.new_page(u_page, #panes)
alm_page.tasks = { update }
nav_links[i].alarm = alm_page.nav_to
TextBox{parent=alm_div,y=1,text="Unit Alarms",height=1,alignment=ALIGN.CENTER}
TextBox{parent=alm_div,y=3,text="work in progress",height=1,alignment=ALIGN.CENTER,fg_bg=cpair(colors.gray,colors.black)}
--#endregion
--#region RPS Tab
local rps_div = Div{parent=page_div}
table.insert(panes, rps_div)
local rps_page = app.new_page(u_page, #panes)
rps_page.tasks = { update }
nav_links[i].rps = rps_page.nav_to
TextBox{parent=rps_div,y=1,text="Protection System",height=1,alignment=ALIGN.CENTER}
local r_trip = IconIndicator{parent=rps_div,y=3,label="RPS Trip",states=basic_states}
r_trip.register(u_ps, "U_RPS", r_trip.update)
local r_mscrm = IconIndicator{parent=rps_div,y=5,label="Manual SCRAM",states=red_ind_s}
local r_ascrm = IconIndicator{parent=rps_div,label="Automatic SCRAM",states=red_ind_s}
local rps_tmo = IconIndicator{parent=rps_div,label="Timeout",states=yel_ind_s}
local rps_flt = IconIndicator{parent=rps_div,label="PPM Fault",states=yel_ind_s}
local rps_sfl = IconIndicator{parent=rps_div,label="Not Formed",states=red_ind_s}
r_mscrm.register(u_ps, "manual", r_mscrm.update)
r_ascrm.register(u_ps, "automatic", r_ascrm.update)
rps_tmo.register(u_ps, "timeout", rps_tmo.update)
rps_flt.register(u_ps, "fault", rps_flt.update)
rps_sfl.register(u_ps, "sys_fail", rps_sfl.update)
rps_div.line_break()
local rps_dmg = IconIndicator{parent=rps_div,label="Reactor Damage Hi",states=red_ind_s}
local rps_tmp = IconIndicator{parent=rps_div,label="Temp. Critical",states=red_ind_s}
local rps_nof = IconIndicator{parent=rps_div,label="Fuel Level Lo",states=yel_ind_s}
local rps_exw = IconIndicator{parent=rps_div,label="Waste Level Hi",states=yel_ind_s}
local rps_loc = IconIndicator{parent=rps_div,label="Coolant Lo Lo",states=yel_ind_s}
local rps_exh = IconIndicator{parent=rps_div,label="Heated Coolant Hi",states=yel_ind_s}
rps_dmg.register(u_ps, "high_dmg", rps_dmg.update)
rps_tmp.register(u_ps, "high_temp", rps_tmp.update)
rps_nof.register(u_ps, "no_fuel", rps_nof.update)
rps_exw.register(u_ps, "ex_waste", rps_exw.update)
rps_loc.register(u_ps, "low_cool", rps_loc.update)
rps_exh.register(u_ps, "ex_hcool", rps_exh.update)
--#endregion
--#region RCS Tab
local rcs_div = Div{parent=page_div}
table.insert(panes, rcs_div)
local rcs_page = app.new_page(u_page, #panes)
rcs_page.tasks = { update }
nav_links[i].rcs = rcs_page.nav_to
TextBox{parent=rcs_div,y=1,text="Coolant System",height=1,alignment=ALIGN.CENTER}
local r_rtrip = IconIndicator{parent=rcs_div,y=3,label="RCP Trip",states=red_ind_s}
local r_cflow = IconIndicator{parent=rcs_div,label="RCS Flow Lo",states=yel_ind_s}
local r_clow = IconIndicator{parent=rcs_div,label="Coolant Level Lo",states=yel_ind_s}
r_rtrip.register(u_ps, "RCPTrip", r_rtrip.update)
r_cflow.register(u_ps, "RCSFlowLow", r_cflow.update)
r_clow.register(u_ps, "CoolantLevelLow", r_clow.update)
local c_flt = IconIndicator{parent=rcs_div,label="RCS HW Fault",states=yel_ind_s}
local c_emg = IconIndicator{parent=rcs_div,label="Emergency Coolant",states=emc_ind_s}
local c_mwrf = IconIndicator{parent=rcs_div,label="Max Water Return",states=yel_ind_s}
c_flt.register(u_ps, "RCSFault", c_flt.update)
c_emg.register(u_ps, "EmergencyCoolant", c_emg.update)
c_mwrf.register(u_ps, "MaxWaterReturnFeed", c_mwrf.update)
-- rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Mismatches",height=1,alignment=ALIGN.CENTER,fg_bg=label}
local c_cfm = IconIndicator{parent=rcs_div,label="Coolant Feed",states=yel_ind_s}
local c_brm = IconIndicator{parent=rcs_div,label="Boil Rate",states=yel_ind_s}
local c_sfm = IconIndicator{parent=rcs_div,label="Steam Feed",states=yel_ind_s}
c_cfm.register(u_ps, "CoolantFeedMismatch", c_cfm.update)
c_brm.register(u_ps, "BoilRateMismatch", c_brm.update)
c_sfm.register(u_ps, "SteamFeedMismatch", c_sfm.update)
rcs_div.line_break()
-- TextBox{parent=rcs_div,text="Aggregate Checks",height=1,alignment=ALIGN.CENTER,fg_bg=label}
if unit.num_boilers > 0 then
local wll = IconIndicator{parent=rcs_div,label="Boiler Water Lo",states=red_ind_s}
local hrl = IconIndicator{parent=rcs_div,label="Heating Rate Lo",states=yel_ind_s}
wll.register(u_ps, "U_WaterLevelLow", wll.update)
hrl.register(u_ps, "U_HeatingRateLow", hrl.update)
end
local tospd = IconIndicator{parent=rcs_div,label="TRB Over Speed",states=red_ind_s}
local gtrip = IconIndicator{parent=rcs_div,label="Generator Trip",states=yel_ind_s}
local ttrip = IconIndicator{parent=rcs_div,label="Turbine Trip",states=red_ind_s}
tospd.register(u_ps, "U_TurbineOverSpeed", tospd.update)
gtrip.register(u_ps, "U_GeneratorTrip", gtrip.update)
ttrip.register(u_ps, "U_TurbineTrip", ttrip.update)
--#endregion
end
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
set_sidebar(active_unit)
end
app.set_on_load(load)
return main
end

View File

@@ -25,8 +25,8 @@ local RPS_LIMITS = const.RPS_LIMITS
-- I sure hope the devs don't change this error message, not that it would have safety implications
-- I wish they didn't change it to be like this
local PCALL_SCRAM_MSG = "pcall: Scram requires the reactor to be active."
local PCALL_START_MSG = "pcall: Reactor is already active."
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active."
---@type plc_config
local config = {}
@@ -307,7 +307,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor SCRAM")
reactor.scram()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_SCRAM_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_SCRAM_MSG) then
log.error("RPS: failed reactor SCRAM")
return false
else
@@ -325,7 +325,7 @@ function plc.rps_init(reactor, is_formed)
log.info("RPS: reactor start")
reactor.activate()
if reactor.__p_is_faulted() and (reactor.__p_last_fault() ~= PCALL_START_MSG) then
if reactor.__p_is_faulted() and not string.find(reactor.__p_last_fault(), PCALL_START_MSG) then
log.error("RPS: failed reactor start")
else
self.reactor_enabled = true

View File

@@ -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.9"
local R_PLC_VERSION = "v1.7.11"
local println = util.println
local println_ts = util.println_ts

View File

@@ -88,56 +88,32 @@ function threads.thread__main(smem, init)
end
end
-- are we now formed after waiting to be formed?
-- check for formed state change
if (not plc_state.reactor_formed) and rps.is_formed() then
-- push a connect event and unmount it from the PPM
local iface = ppm.get_iface(plc_dev.reactor)
if iface then
log.info("unmounting and remounting unformed reactor")
ppm.unmount(plc_dev.reactor)
-- reactor now formed
plc_state.reactor_formed = true
local type, device = ppm.mount(iface)
println_ts("reactor is now formed.")
log.info("reactor is now formed")
if type == "fissionReactorLogicAdapter" and device ~= nil then
-- reconnect reactor
plc_dev.reactor = device
-- SCRAM newly formed reactor
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- we need to assume formed here as we cannot check in this main loop
-- RPS will identify if it isn't and this will get set false later
plc_state.reactor_formed = true
println_ts("reactor reconnected.")
log.info("reactor reconnected")
-- SCRAM newly connected reactor
smem.q.mq_rps.push_command(MQ__RPS_CMD.SCRAM)
-- determine if we are still in a degraded state
if (not networked) or nic.is_connected() then
plc_state.degraded = false
end
rps.reconnect_reactor(plc_dev.reactor)
if networked then
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- partial reset of RPS, specific to becoming formed
rps.reset_formed()
else
-- fully lost the reactor now :(
println_ts("reactor lost (failed reconnect)!")
log.error("reactor lost (failed reconnect)")
plc_state.no_reactor = true
plc_state.degraded = true
end
else
log.error("failed to get interface of previously connected reactor", true)
-- determine if we are still in a degraded state
if (not networked) or nic.is_connected() then
plc_state.degraded = false
end
elseif not rps.is_formed() then
-- partial reset of RPS, specific to becoming formed
-- without this, auto control can't resume on chunk load
rps.reset_formed()
elseif plc_state.reactor_formed and not rps.is_formed() then
-- reactor no longer formed
println_ts("reactor is no longer formed.")
log.info("reactor is no longer formed")
plc_state.reactor_formed = false
plc_state.degraded = true
end
-- update indicators
@@ -227,7 +203,8 @@ function threads.thread__main(smem, init)
plc_comms.reconnect_reactor(plc_dev.reactor)
end
-- partial reset of RPS, specific to becoming formed
-- partial reset of RPS, specific to becoming formed/reconnected
-- without this, auto control can't resume on chunk load
rps.reset_formed()
end
elseif networked and type == "modem" then

View File

@@ -2,6 +2,7 @@
-- Configuration GUI
--
local constants = require("scada-common.constants")
local log = require("scada-common.log")
local ppm = require("scada-common.ppm")
local rsio = require("scada-common.rsio")
@@ -33,45 +34,50 @@ local tri = util.trinary
local cpair = core.cpair
local IO = rsio.IO
local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE
local LEFT = core.ALIGN.LEFT
local CENTER = core.ALIGN.CENTER
local RIGHT = core.ALIGN.RIGHT
-- rsio port descriptions
local PORT_DESC = {
"Facility SCRAM",
"Facility Acknowledge",
"Reactor SCRAM",
"Reactor RPS Reset",
"Reactor Enable",
"Unit Acknowledge",
"Facility Alarm (high prio)",
"Facility Alarm (any)",
"Waste Plutonium Valve",
"Waste Polonium Valve",
"Waste Po Pellets Valve",
"Waste Antimatter Valve",
"Reactor Active",
"Reactor in Auto Control",
"RPS Tripped",
"RPS Auto SCRAM",
"RPS High Damage",
"RPS High Temperature",
"RPS Low Coolant",
"RPS Excess Heated Coolant",
"RPS Excess Waste",
"RPS Insufficient Fuel",
"RPS PLC Fault",
"RPS Supervisor Timeout",
"Unit Alarm",
"Unit Emergency Cool. Valve"
local PORT_DESC_MAP = {
{ IO.F_SCRAM, "Facility SCRAM" },
{ IO.F_ACK, "Facility Acknowledge" },
{ IO.R_SCRAM, "Reactor SCRAM" },
{ IO.R_RESET, "Reactor RPS Reset" },
{ IO.R_ENABLE, "Reactor Enable" },
{ IO.U_ACK, "Unit Acknowledge" },
{ IO.F_ALARM, "Facility Alarm (high prio)" },
{ IO.F_ALARM_ANY, "Facility Alarm (any)" },
{ IO.F_MATRIX_LOW, "Induction Matrix < " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_LOW) .. "%" },
{ IO.F_MATRIX_HIGH, "Induction Matrix > " .. (100 * constants.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH) .. "%" },
{ IO.F_MATRIX_CHG, "Induction Matrix Charge %" },
{ IO.WASTE_PU, "Waste Plutonium Valve" },
{ IO.WASTE_PO, "Waste Polonium Valve" },
{ IO.WASTE_POPL, "Waste Po Pellets Valve" },
{ IO.WASTE_AM, "Waste Antimatter Valve" },
{ IO.R_ACTIVE, "Reactor Active" },
{ IO.R_AUTO_CTRL, "Reactor in Auto Control" },
{ IO.R_SCRAMMED, "RPS Tripped" },
{ IO.R_AUTO_SCRAM, "RPS Auto SCRAM" },
{ IO.R_HIGH_DMG, "RPS High Damage" },
{ IO.R_HIGH_TEMP, "RPS High Temperature" },
{ IO.R_LOW_COOLANT, "RPS Low Coolant" },
{ IO.R_EXCESS_HC, "RPS Excess Heated Coolant" },
{ IO.R_EXCESS_WS, "RPS Excess Waste" },
{ IO.R_INSUFF_FUEL, "RPS Insufficient Fuel" },
{ IO.R_PLC_FAULT, "RPS PLC Fault" },
{ IO.R_PLC_TIMEOUT, "RPS Supervisor Timeout" },
{ IO.U_ALARM, "Unit Alarm" },
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" }
}
-- designation (0 = facility, 1 = unit)
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 }
local PORT_DSGN = { [-1] = 1, 0, 0, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0 }
assert(#PORT_DESC == rsio.NUM_PORTS)
assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
assert(#PORT_DSGN == rsio.NUM_PORTS)
-- changes to the config data/format to let the user know
@@ -442,7 +448,7 @@ local function config_view(display)
TextBox{parent=net_c_3,x=1,y=11,height=1,text="Facility Auth Key"}
local key, _, censor = TextField{parent=net_c_3,x=1,y=12,max_len=64,value=ini_cfg.AuthKey,width=32,height=1,fg_bg=bw_fg_bg}
local function censor_key(enable) censor(util.trinary(enable, "*", nil)) end
local function censor_key(enable) censor(tri(enable, "*", nil)) end
local hide_key = CheckBox{parent=net_c_3,x=34,y=12,label="Hide",box_fg_bg=cpair(colors.lightBlue,colors.black),callback=censor_key}
@@ -555,7 +561,7 @@ local function config_view(display)
PushButton{parent=clr_c_2,x=44,y=14,min_width=6,text="Done",callback=function()clr_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
local function back_from_colors()
main_pane.set_value(util.trinary(tool_ctl.jumped_to_color, 1, 4))
main_pane.set_value(tri(tool_ctl.jumped_to_color, 1, 4))
tool_ctl.jumped_to_color = false
recolor(1)
end
@@ -897,7 +903,7 @@ local function config_view(display)
tool_ctl.p_desc.reposition(1, 8)
tool_ctl.p_desc.set_value("You can connect more than one environment detector for a particular unit or the facility. In that case, the maximum radiation reading from those assigned to that particular unit or the facility will be used for alarms and display.")
elseif type == "inductionPort" or type == "spsPort" then
local dev = util.trinary(type == "inductionPort", "induction matrix", "SPS")
local dev = tri(type == "inductionPort", "induction matrix", "SPS")
tool_ctl.p_idx.hide(true)
tool_ctl.p_unit.hide(true)
tool_ctl.p_prompt.set_value("This is the " .. dev .. " for the facility.")
@@ -923,7 +929,7 @@ local function config_view(display)
tool_ctl.ppm_devs.remove_all()
for name, entry in pairs(mounts) do
if util.table_contains(RTU_DEV_TYPES, entry.type) then
local bkg = util.trinary(alternate, colors.white, colors.lightGray)
local bkg = tri(alternate, colors.white, colors.lightGray)
---@cast entry ppm_entry
local line = Div{parent=tool_ctl.ppm_devs,height=2,fg_bg=cpair(colors.black,bkg)}
@@ -1085,8 +1091,9 @@ local function config_view(display)
local rs_c_4 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_5 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_6 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_c_7 = Div{parent=rs_cfg,x=2,y=4,width=49}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6}}
local rs_pane = MultiPane{parent=rs_cfg,x=1,y=4,panes={rs_c_1,rs_c_2,rs_c_3,rs_c_4,rs_c_5,rs_c_6,rs_c_7}}
TextBox{parent=rs_cfg,x=1,y=2,height=1,text=" Redstone Connections",fg_bg=cpair(colors.black,colors.red)}
@@ -1143,9 +1150,23 @@ local function config_view(display)
text = "You selected the ALL_WASTE shortcut."
else
tool_ctl.rs_cfg_shortcut.hide(true)
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
tool_ctl.rs_cfg_color.show()
text = "You selected " .. rsio.to_string(port) .. " (for "
local io_type = "analog input "
local io_mode = rsio.get_io_mode(port)
local inv = tri(rsio.digital_is_active(port, IO_LVL.LOW) == true, "inverted ", "")
if io_mode == IO_MODE.DIGITAL_IN then
io_type = inv .. "digital input "
elseif io_mode == IO_MODE.DIGITAL_OUT then
io_type = inv .. "digital output "
elseif io_mode == IO_MODE.ANALOG_OUT then
io_type = "analog output "
end
text = "You selected the " .. io_type .. rsio.to_string(port) .. " (for "
if PORT_DSGN[port] == 1 then
text = text .. "a unit)."
tool_ctl.rs_cfg_unit_l.show()
@@ -1167,25 +1188,35 @@ local function config_view(display)
PushButton{parent=all_w_macro,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">ALL_WASTE",callback=function()new_rs(-1)end,fg_bg=cpair(colors.black,colors.green),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=all_w_macro,x=16,y=1,width=5,height=1,text="[n/a]",fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=all_w_macro,x=22,y=1,height=1,text="Create all 4 waste entries",fg_bg=cpair(colors.gray,colors.white)}
for i = 1, rsio.NUM_PORTS do
local name = rsio.to_string(i)
local io_dir = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, "[in]", "[out]")
local btn_color = util.trinary(rsio.get_io_dir(i) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
local p = PORT_DESC_MAP[i][1]
local name = rsio.to_string(p)
local io_dir = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, "[in]", "[out]")
local btn_color = tri(rsio.get_io_dir(p) == rsio.IO_DIR.IN, colors.yellow, colors.lightBlue)
local entry = Div{parent=rs_ports,height=1}
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(i)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
PushButton{parent=entry,x=1,y=1,min_width=14,alignment=LEFT,height=1,text=">"..name,callback=function()new_rs(p)end,fg_bg=cpair(colors.black,btn_color),active_fg_bg=cpair(colors.white,colors.black)}
TextBox{parent=entry,x=16,y=1,width=5,height=1,text=io_dir,fg_bg=cpair(colors.lightGray,colors.white)}
TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC[i],fg_bg=cpair(colors.gray,colors.white)}
TextBox{parent=entry,x=22,y=1,height=1,text=PORT_DESC_MAP[i][2],fg_bg=cpair(colors.gray,colors.white)}
end
PushButton{parent=rs_c_2,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(1)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=1,text=""}
tool_ctl.rs_cfg_selection = TextBox{parent=rs_c_3,x=1,y=1,height=2,text=""}
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=27,y=3,width=7,height=1,text="Unit ID"}
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=27,y=4,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
PushButton{parent=rs_c_3,x=36,y=3,text="What's that?",min_width=14,callback=function()rs_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=3,width=11,height=1,text="Output Side"}
local side = Radio2D{parent=rs_c_3,x=1,y=4,rows=2,columns=3,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
TextBox{parent=rs_c_7,x=1,y=1,height=4,text="(Normal) Digital Input: On if there is a redstone signal, off otherwise\nInverted Digital Input: On without a redstone signal, off otherwise"}
TextBox{parent=rs_c_7,x=1,y=6,height=4,text="(Normal) Digital Output: Redstone signal to 'turn it on', none to 'turn it off'\nInverted Digital Output: No redstone signal to 'turn it on', redstone signal to 'turn it off'"}
TextBox{parent=rs_c_7,x=1,y=11,height=2,text="Analog Input: 0-15 redstone power level input\nAnalog Output: 0-15 scaled redstone power level output"}
PushButton{parent=rs_c_7,x=1,y=14,text="\x1b Back",callback=function()rs_pane.set_value(3)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
tool_ctl.rs_cfg_side_l = TextBox{parent=rs_c_3,x=1,y=4,width=11,height=1,text="Output Side"}
local side = Radio2D{parent=rs_c_3,x=1,y=5,rows=1,columns=6,default=1,options=side_options,radio_colors=cpair(colors.lightGray,colors.black),select_color=colors.red}
tool_ctl.rs_cfg_unit_l = TextBox{parent=rs_c_3,x=25,y=7,width=7,height=1,text="Unit ID"}
tool_ctl.rs_cfg_unit = NumberField{parent=rs_c_3,x=33,y=7,width=10,max_chars=2,min=1,max=4,fg_bg=bw_fg_bg}
local function set_bundled(bundled)
if bundled then tool_ctl.rs_cfg_color.enable() else tool_ctl.rs_cfg_color.disable() end
@@ -1216,10 +1247,10 @@ local function config_view(display)
if port >= 0 then
---@type rtu_rs_definition
local def = {
unit = util.trinary(PORT_DSGN[port] == 1, u, nil),
unit = tri(PORT_DSGN[port] == 1, u, nil),
port = port,
side = side_options_map[side.get_value()],
color = util.trinary(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil)
color = tri(bundled.get_value(), color_options_map[tool_ctl.rs_cfg_color.get_value()], nil)
}
if tool_ctl.rs_cfg_editing == false then
@@ -1233,10 +1264,10 @@ local function config_view(display)
local default_colors = { colors.red, colors.orange, colors.yellow, colors.lime }
for i = 0, 3 do
table.insert(tmp_cfg.Redstone, {
unit = util.trinary(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
unit = tri(PORT_DSGN[IO.WASTE_PU + i] == 1, u, nil),
port = IO.WASTE_PU + i,
side = util.trinary(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = util.trinary(bundled.get_value(), default_colors[i + 1], nil)
side = tri(bundled.get_value(), side_options_map[side.get_value()], default_sides[i + 1]),
color = tri(bundled.get_value(), default_colors[i + 1], nil)
})
end
end
@@ -1289,7 +1320,7 @@ local function config_view(display)
peri_import_list.remove_all()
for _, entry in ipairs(config.RTU_DEVICES) do
local for_facility = entry.for_reactor == 0
local ini_unit = util.trinary(for_facility, nil, entry.for_reactor)
local ini_unit = tri(for_facility, nil, entry.for_reactor)
local def = { name = entry.name, unit = ini_unit, index = entry.index }
local mount = mounts[def.name] ---@type ppm_entry|nil
@@ -1368,7 +1399,7 @@ local function config_view(display)
table.insert(tmp_cfg.Redstone, def)
local name = rsio.to_string(def.port)
local io_dir = util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_dir = tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local conn = def.side
local unit = "facility"
@@ -1431,7 +1462,7 @@ local function config_view(display)
local val = util.strval(raw)
if f[1] == "AuthKey" then val = string.rep("*", string.len(val))
elseif f[1] == "LogMode" then val = util.trinary(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "LogMode" then val = tri(raw == log.MODE.APPEND, "append", "replace")
elseif f[1] == "FrontPanelTheme" then
val = util.strval(themes.fp_theme_name(raw))
elseif f[1] == "ColorMode" then
@@ -1440,7 +1471,7 @@ local function config_view(display)
if val == "nil" then val = "<not set>" end
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
@@ -1554,7 +1585,7 @@ local function config_view(display)
end
tool_ctl.rs_cfg_selection.set_value(text)
tool_ctl.rs_cfg_side_l.set_value(util.trinary(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
tool_ctl.rs_cfg_side_l.set_value(tri(rsio.get_io_dir(def.port) == rsio.IO_DIR.IN, "Input Side", "Output Side"))
side.set_value(side_to_idx(def.side))
bundled.set_value(def.color ~= nil)
tool_ctl.rs_cfg_color.set_value(value)
@@ -1575,7 +1606,7 @@ local function config_view(display)
local def = cfg.Redstone[i] ---@type rtu_rs_definition
local name = rsio.to_string(def.port)
local io_dir = util.trinary(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local io_dir = tri(rsio.get_io_mode(def.port) == rsio.IO_DIR.IN, "\x1a", "\x1b")
local conn = def.side
local unit = util.strval(def.unit or "F")
@@ -1638,9 +1669,11 @@ function configurator.configure(ask_config)
elseif event == "paste" then
display.handle_paste(param1)
elseif event == "peripheral_detach" then
---@diagnostic disable-next-line: discard-returns
ppm.handle_unmount(param1)
tool_ctl.update_peri_list()
elseif event == "peripheral" then
---@diagnostic disable-next-line: discard-returns
ppm.mount(param1)
tool_ctl.update_peri_list()
end

View File

@@ -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.4"
local RTU_VERSION = "v1.9.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_UNIT_HW_STATE = databus.RTU_UNIT_HW_STATE

View File

@@ -17,8 +17,8 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "2.5.0"
comms.api_version = "0.0.1"
comms.version = "2.5.1"
comms.api_version = "0.0.2"
---@enum PROTOCOL
local PROTOCOL = {
@@ -67,7 +67,7 @@ local CRDN_TYPE = {
UNIT_STATUSES = 5, -- state of each of the reactor units
UNIT_CMD = 6, -- command a reactor unit
API_GET_FAC = 7, -- API: get all the facility data
API_GET_UNITS = 8 -- API: get all the reactor unit data
API_GET_UNIT = 8 -- API: get reactor unit data
}
---@enum ESTABLISH_ACK
@@ -97,7 +97,8 @@ local FAC_COMMAND = {
START = 2, -- start automatic process control
ACK_ALL_ALARMS = 3, -- acknowledge all alarms on all units
SET_WASTE_MODE = 4, -- set automatic waste processing mode
SET_PU_FB = 5 -- set plutonium fallback mode
SET_PU_FB = 5, -- set plutonium fallback mode
SET_SPS_LP = 6 -- set SPS at low power mode
}
---@enum UNIT_COMMAND

View File

@@ -29,7 +29,7 @@ local annunc = {}
annunc.RCSFlowLow_H2O = -3.2 -- flow < -3.2 mB/s
annunc.RCSFlowLow_NA = -2.0 -- flow < -2.0 mB/s
annunc.CoolantLevelLow = 0.4 -- fill < 40%
annunc.ReactorTempHigh = 1000 -- temp > 1000K
annunc.OpTempTolerance = 5 -- high temp if >= operational temp + X
annunc.ReactorHighDeltaT = 50 -- rate > 50K/s
annunc.FuelLevelLow = 0.05 -- fill <= 5%
annunc.WasteLevelHigh = 0.80 -- fill >= 80%
@@ -66,6 +66,18 @@ constants.ALARM_LIMITS = alarms
--#endregion
--#region Supervisor Redstone Activation Thresholds
---@class _rs_threshold_constants
local rs = {}
rs.IMATRIX_CHARGE_LOW = 0.05 -- activation threshold (less than) for F_MATRIX_LOW
rs.IMATRIX_CHARGE_HIGH = 0.95 -- activation threshold (greater than) for F_MATRIX_HIGH
constants.RS_THRESHOLDS = rs
--#endregion
--#region Supervisor Constants
-- milliseconds until coolant flow is assumed to be stable enough to enable certain coolant checks
@@ -89,9 +101,11 @@ constants.EXTREME_RADIATION = 100.0
---@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
mek.BASE_BOIL_TEMP = 373.15 -- mekanism: HeatUtils.BASE_BOIL_TEMP
mek.JOULES_PER_MB = 1000000 -- mekanism: energyPerFissionFuel
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

View File

@@ -141,7 +141,9 @@ local function peri_init(iface)
local funcs = peripheral.wrap(iface)
if (type(funcs) == "table") and (type(funcs[key]) == "function") then
-- add this function then return it
self.fault_counts[key] = 0
self.device[key] = protect_peri_function(key, funcs[key])
log.info(util.c("PPM: [@", iface, "] initialized previously undefined field ", key, "()"))
return self.device[key]

View File

@@ -52,6 +52,8 @@ local IO_PORT = {
-- facility
F_ALARM = 7, -- active high, facility-wide alarm (any high priority unit alarm)
F_ALARM_ANY = 8, -- active high, any alarm regardless of priority
F_MATRIX_LOW = 27, -- active high, induction matrix charge low
F_MATRIX_HIGH = 28, -- active high, induction matrix charge high
-- waste
WASTE_PU = 9, -- active low, waste -> plutonium -> pellets route
@@ -75,17 +77,27 @@ local IO_PORT = {
-- unit outputs
U_ALARM = 25, -- active high, unit alarm
U_EMER_COOL = 26 -- active low, emergency coolant control
U_EMER_COOL = 26, -- active low, emergency coolant control
-- analog outputs --
-- facility
F_MATRIX_CHG = 29 -- analog charge level of the induction matrix
}
rsio.IO_LVL = IO_LVL
rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT
rsio.NUM_PORTS = IO_PORT.U_EMER_COOL
rsio.NUM_PORTS = 29
rsio.NUM_DIG_PORTS = 28
rsio.NUM_ANA_PORTS = 1
-- self checks
assert(rsio.NUM_PORTS == (rsio.NUM_DIG_PORTS + rsio.NUM_ANA_PORTS), "port counts inconsistent")
local dup_chk = {}
for _, v in pairs(IO_PORT) do
assert(dup_chk[v] ~= true, "duplicate in port list")
@@ -96,64 +108,45 @@ assert(#dup_chk == rsio.NUM_PORTS, "port list malformed")
--#endregion
--#region Utility Functions
--#region Utility Functions and Attribute Tables
local PORT_NAMES = {
"F_SCRAM",
"F_ACK",
"R_SCRAM",
"R_RESET",
"R_ENABLE",
"U_ACK",
"F_ALARM",
"F_ALARM_ANY",
"WASTE_PU",
"WASTE_PO",
"WASTE_POPL",
"WASTE_AM",
"R_ACTIVE",
"R_AUTO_CTRL",
"R_SCRAMMED",
"R_AUTO_SCRAM",
"R_HIGH_DMG",
"R_HIGH_TEMP",
"R_LOW_COOLANT",
"R_EXCESS_HC",
"R_EXCESS_WS",
"R_INSUFF_FUEL",
"R_PLC_FAULT",
"R_PLC_TIMEOUT",
"U_ALARM",
"U_EMER_COOL"
}
local IO = IO_PORT
-- list of all port names
local PORT_NAMES = {}
for k, v in pairs(IO) do PORT_NAMES[v] = k end
-- list of all port I/O modes
local MODES = {
IO_MODE.DIGITAL_IN, -- F_SCRAM
IO_MODE.DIGITAL_IN, -- F_ACK
IO_MODE.DIGITAL_IN, -- R_SCRAM
IO_MODE.DIGITAL_IN, -- R_RESET
IO_MODE.DIGITAL_IN, -- R_ENABLE
IO_MODE.DIGITAL_IN, -- U_ACK
IO_MODE.DIGITAL_OUT, -- F_ALARM
IO_MODE.DIGITAL_OUT, -- F_ALARM_ANY
IO_MODE.DIGITAL_OUT, -- WASTE_PU
IO_MODE.DIGITAL_OUT, -- WASTE_PO
IO_MODE.DIGITAL_OUT, -- WASTE_POPL
IO_MODE.DIGITAL_OUT, -- WASTE_AM
IO_MODE.DIGITAL_OUT, -- R_ACTIVE
IO_MODE.DIGITAL_OUT, -- R_AUTO_CTRL
IO_MODE.DIGITAL_OUT, -- R_SCRAMMED
IO_MODE.DIGITAL_OUT, -- R_AUTO_SCRAM
IO_MODE.DIGITAL_OUT, -- R_HIGH_DMG
IO_MODE.DIGITAL_OUT, -- R_HIGH_TEMP
IO_MODE.DIGITAL_OUT, -- R_LOW_COOLANT
IO_MODE.DIGITAL_OUT, -- R_EXCESS_HC
IO_MODE.DIGITAL_OUT, -- R_EXCESS_WS
IO_MODE.DIGITAL_OUT, -- R_INSUFF_FUEL
IO_MODE.DIGITAL_OUT, -- R_PLC_FAULT
IO_MODE.DIGITAL_OUT, -- R_PLC_TIMEOUT
IO_MODE.DIGITAL_OUT, -- U_ALARM
IO_MODE.DIGITAL_OUT -- U_EMER_COOL
[IO.F_SCRAM] = IO_MODE.DIGITAL_IN,
[IO.F_ACK] = IO_MODE.DIGITAL_IN,
[IO.R_SCRAM] = IO_MODE.DIGITAL_IN,
[IO.R_RESET] = IO_MODE.DIGITAL_IN,
[IO.R_ENABLE] = IO_MODE.DIGITAL_IN,
[IO.U_ACK] = IO_MODE.DIGITAL_IN,
[IO.F_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.F_ALARM_ANY] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_LOW] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_HIGH] = IO_MODE.DIGITAL_OUT,
[IO.WASTE_PU] = IO_MODE.DIGITAL_OUT,
[IO.WASTE_PO] = IO_MODE.DIGITAL_OUT,
[IO.WASTE_POPL] = IO_MODE.DIGITAL_OUT,
[IO.WASTE_AM] = IO_MODE.DIGITAL_OUT,
[IO.R_ACTIVE] = IO_MODE.DIGITAL_OUT,
[IO.R_AUTO_CTRL] = IO_MODE.DIGITAL_OUT,
[IO.R_SCRAMMED] = IO_MODE.DIGITAL_OUT,
[IO.R_AUTO_SCRAM] = IO_MODE.DIGITAL_OUT,
[IO.R_HIGH_DMG] = IO_MODE.DIGITAL_OUT,
[IO.R_HIGH_TEMP] = IO_MODE.DIGITAL_OUT,
[IO.R_LOW_COOLANT] = IO_MODE.DIGITAL_OUT,
[IO.R_EXCESS_HC] = IO_MODE.DIGITAL_OUT,
[IO.R_EXCESS_WS] = IO_MODE.DIGITAL_OUT,
[IO.R_INSUFF_FUEL] = IO_MODE.DIGITAL_OUT,
[IO.R_PLC_FAULT] = IO_MODE.DIGITAL_OUT,
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
}
assert(rsio.NUM_PORTS == #PORT_NAMES, "port names length incorrect")
@@ -179,74 +172,51 @@ local function _O_ACTIVE_LOW(active) if active then return IO_LVL.LOW else retur
-- I/O mappings to I/O function and I/O mode
local RS_DIO_MAP = {
-- F_SCRAM
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
-- F_ACK
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
[IO.F_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
[IO.F_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- R_SCRAM
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
-- R_RESET
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- R_ENABLE
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
[IO.R_SCRAM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.IN },
[IO.R_RESET] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
[IO.R_ENABLE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- U_ACK
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
[IO.U_ACK] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.IN },
-- F_ALARM
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- F_ALARM_ANY
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.F_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.F_ALARM_ANY] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.F_MATRIX_LOW] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.F_MATRIX_HIGH] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- WASTE_PU
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_PO
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_POPL
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- WASTE_AM
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.WASTE_PU] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.WASTE_PO] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.WASTE_POPL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.WASTE_AM] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
-- R_ACTIVE
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_AUTO_CTRL
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_SCRAMMED
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_AUTO_SCRAM
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_HIGH_DMG
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_HIGH_TEMP
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_LOW_COOLANT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_EXCESS_HC
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_EXCESS_WS
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_INSUFF_FUEL
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_PLC_FAULT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- R_PLC_TIMEOUT
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_ACTIVE] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_AUTO_CTRL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_SCRAMMED] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_AUTO_SCRAM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_HIGH_DMG] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_HIGH_TEMP] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_LOW_COOLANT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_EXCESS_HC] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_EXCESS_WS] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_INSUFF_FUEL] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_PLC_FAULT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- U_ALARM
{ _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
-- U_EMER_COOL
{ _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
[IO.U_ALARM] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, mode = IO_DIR.OUT },
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
}
assert(rsio.NUM_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port
---@nodiscard
---@param port IO_PORT
---@return IO_DIR
function rsio.get_io_dir(port)
if rsio.is_valid_port(port) then return RS_DIO_MAP[port].mode
if rsio.is_valid_port(port) then
return util.trinary(MODES[port] == IO_MODE.DIGITAL_OUT or MODES[port] == IO_MODE.ANALOG_OUT, IO_DIR.OUT, IO_DIR.IN)
else return IO_DIR.IN end
end
@@ -310,6 +280,13 @@ end
--#region Digital I/O
-- check if a port is digital
---@nodiscard
---@param port IO_PORT
function rsio.is_digital(port)
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.DIGITAL_IN or MODES[port] == IO_MODE.DIGITAL_OUT)
end
-- get digital I/O level reading from a redstone boolean input value
---@nodiscard
---@param rs_value boolean raw value from redstone
@@ -330,7 +307,7 @@ function rsio.digital_write(level) return level == IO_LVL.HIGH end
---@param active boolean state to convert to logic level
---@return IO_LVL|false
function rsio.digital_write_active(port, active)
if (not util.is_int(port)) or (port < IO_PORT.F_ALARM) or (port > IO_PORT.U_EMER_COOL) then
if not rsio.is_digital(port) then
return false
else
return RS_DIO_MAP[port]._out(active)
@@ -343,9 +320,7 @@ end
---@param level IO_LVL logic level
---@return boolean|nil state true for active, false for inactive, or nil if invalid port or level provided
function rsio.digital_is_active(port, level)
if not util.is_int(port) then
return nil
elseif level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
if (not rsio.is_digital(port)) or level == IO_LVL.FLOATING or level == IO_LVL.DISCONNECT then
return nil
else
return RS_DIO_MAP[port]._in(level)
@@ -356,6 +331,13 @@ end
--#region Analog I/O
-- check if a port is analog
---@nodiscard
---@param port IO_PORT
function rsio.is_analog(port)
return rsio.is_valid_port(port) and (MODES[port] == IO_MODE.ANALOG_IN or MODES[port] == IO_MODE.ANALOG_OUT)
end
-- read an analog value scaled from min to max
---@nodiscard
---@param rs_value number redstone reading (0 to 15)
@@ -372,7 +354,7 @@ end
---@param value number value to write (from min to max range)
---@param min number minimum of range
---@param max number maximum of range
---@return number rs_value scaled redstone reading (0 to 15)
---@return integer rs_value scaled redstone reading (0 to 15)
function rsio.analog_write(value, min, max)
local scaled_value = (value - min) / (max - min)
return math.floor(scaled_value * 15)

View File

@@ -22,7 +22,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.2.1"
util.version = "1.3.0"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50
@@ -181,8 +181,7 @@ function util.round(x) return math.floor(x + 0.5) end
-- get a new moving average object
---@nodiscard
---@param length integer history length
---@param default number value to fill history with for first call to compute()
function util.mov_avg(length, default)
function util.mov_avg(length)
local data = {}
local index = 1
local last_t = 0 ---@type number|nil
@@ -190,11 +189,15 @@ function util.mov_avg(length, default)
---@class moving_average
local public = {}
-- reset all to a given value
---@param x number value
-- reset all to a given value, or clear all data if no value is given
---@param x number? value
function public.reset(x)
index = 1
data = {}
for _ = 1, length do t_insert(data, x) end
if x then
for _ = 1, length do t_insert(data, x) end
end
end
-- record a new value
@@ -214,12 +217,15 @@ function util.mov_avg(length, default)
---@nodiscard
---@return number average
function public.compute()
local sum = 0
for i = 1, length do sum = sum + data[i] end
return sum / length
end
if #data == 0 then return 0 end
public.reset(default)
local sum = 0
for i = 1, #data do
sum = sum + data[i]
end
return sum / #data
end
return public
end

View File

@@ -120,6 +120,8 @@ function facility.new(config, cooling_conf)
waste_product = WASTE.PLUTONIUM,
current_waste_product = WASTE.PLUTONIUM,
pu_fallback = false,
sps_low_power = false,
disabled_sps = false,
-- alarm tones
tone_states = {},
test_tone_set = false,
@@ -128,9 +130,16 @@ function facility.new(config, cooling_conf)
test_alarm_states = {},
-- statistics
im_stat_init = false,
avg_charge = util.mov_avg(3, 0.0),
avg_inflow = util.mov_avg(6, 0.0),
avg_outflow = util.mov_avg(6, 0.0)
avg_charge = util.mov_avg(3), -- 3 seconds
avg_inflow = util.mov_avg(6), -- 3 seconds
avg_outflow = util.mov_avg(6), -- 3 seconds
-- induction matrix charge delta stats
avg_net = util.mov_avg(60), -- 60 seconds
imtx_last_capacity = 0,
imtx_last_charge = 0,
imtx_last_charge_t = 0,
-- track faulted induction matrix update times to reject
imtx_faulted_times = { 0, 0, 0 }
}
-- create units
@@ -300,23 +309,68 @@ function facility.new(config, cooling_conf)
-- calculate moving averages for induction matrix
if self.induction[1] ~= nil then
local matrix = self.induction[1] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
local matrix = self.induction[1] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
charge_update = db.tanks.last_update
local build_update = db.build.last_update
rate_update = db.state.last_update
charge_update = db.tanks.last_update
local has_data = build_update > 0 and rate_update > 0 and charge_update > 0
if matrix.is_faulted() then
-- a fault occured, cannot reliably update stats
has_data = false
self.im_stat_init = false
self.imtx_faulted_times = { build_update, rate_update, charge_update }
elseif not self.im_stat_init then
-- prevent operation with partially invalid data
-- all fields must have updated since the last fault
has_data = self.imtx_faulted_times[1] < build_update and
self.imtx_faulted_times[2] < rate_update and
self.imtx_faulted_times[3] < charge_update
end
if has_data then
local energy = util.joules_to_fe(db.tanks.energy)
local input = util.joules_to_fe(db.state.last_input)
local output = util.joules_to_fe(db.state.last_output)
if (charge_update > 0) and (rate_update > 0) then
if self.im_stat_init then
self.avg_charge.record(util.joules_to_fe(db.tanks.energy), charge_update)
self.avg_inflow.record(util.joules_to_fe(db.state.last_input), rate_update)
self.avg_outflow.record(util.joules_to_fe(db.state.last_output), rate_update)
self.avg_charge.record(energy, charge_update)
self.avg_inflow.record(input, rate_update)
self.avg_outflow.record(output, rate_update)
if charge_update ~= self.imtx_last_charge_t then
local delta = (energy - self.imtx_last_charge) / (charge_update - self.imtx_last_charge_t)
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
-- if the capacity changed, toss out existing data
if db.build.max_energy ~= self.imtx_last_capacity then
self.imtx_last_capacity = db.build.max_energy
self.avg_net.reset()
else
self.avg_net.record(delta, charge_update)
end
end
else
self.im_stat_init = true
self.avg_charge.reset(util.joules_to_fe(db.tanks.energy))
self.avg_inflow.reset(util.joules_to_fe(db.state.last_input))
self.avg_outflow.reset(util.joules_to_fe(db.state.last_output))
self.avg_charge.reset(energy)
self.avg_inflow.reset(input)
self.avg_outflow.reset(output)
self.avg_net.reset()
self.imtx_last_capacity = db.build.max_energy
self.imtx_last_charge = energy
self.imtx_last_charge_t = charge_update
end
else
-- prevent use by control systems
rate_update = 0
charge_update = 0
end
else
self.im_stat_init = false
@@ -475,7 +529,7 @@ function facility.new(config, cooling_conf)
self.status_text = { "CHARGE MODE", "running control loop" }
log.info("FAC: CHARGE mode starting PID control")
elseif self.last_update ~= charge_update then
elseif self.last_update < charge_update then
-- convert to kFE to make constants not microscopic
local error = util.round((self.charge_setpoint - avg_charge) / 1000) / 1000
@@ -549,7 +603,7 @@ function facility.new(config, cooling_conf)
self.status_text = { "GENERATION MODE", "running control loop" }
log.info("FAC: GEN_RATE process mode initial hold completed, starting PID control")
end
elseif self.last_update ~= rate_update then
elseif self.last_update < rate_update then
-- convert to MFE (in rounded kFE) to make constants not microscopic
local error = util.round((self.gen_rate_setpoint - avg_inflow) / 1000) / 1000
@@ -620,8 +674,7 @@ function facility.new(config, cooling_conf)
local astatus = self.ascram_status
if self.induction[1] ~= nil then
local matrix = self.induction[1] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
local db = self.induction[1].get_db() ---@type imatrix_session_db
-- clear matrix disconnected
if astatus.matrix_dc then
@@ -774,6 +827,15 @@ function facility.new(config, cooling_conf)
self.io_ctl.digital_write(IO.F_ALARM, has_prio_alarm)
self.io_ctl.digital_write(IO.F_ALARM_ANY, has_any_alarm)
-- update induction matrix related outputs
if self.induction[1] ~= nil then
local db = self.induction[1].get_db() ---@type imatrix_session_db
self.io_ctl.digital_write(IO.F_MATRIX_LOW, db.tanks.energy_fill < const.RS_THRESHOLDS.IMATRIX_CHARGE_LOW)
self.io_ctl.digital_write(IO.F_MATRIX_HIGH, db.tanks.energy_fill > const.RS_THRESHOLDS.IMATRIX_CHARGE_HIGH)
self.io_ctl.analog_write(IO.F_MATRIX_CHG, db.tanks.energy_fill, 0, 1)
end
end
--#endregion
@@ -804,9 +866,25 @@ function facility.new(config, cooling_conf)
end
-- update waste product
if self.waste_product == WASTE.PLUTONIUM or (self.pu_fallback and insufficent_po_rate) then
self.current_waste_product = self.waste_product
if (not self.sps_low_power) and (self.waste_product == WASTE.ANTI_MATTER) and (self.induction[1] ~= nil) then
local db = self.induction[1].get_db() ---@type imatrix_session_db
if db.tanks.energy_fill >= 0.15 then
self.disabled_sps = false
elseif self.disabled_sps or ((db.tanks.last_update > 0) and (db.tanks.energy_fill < 0.1)) then
self.disabled_sps = true
self.current_waste_product = WASTE.POLONIUM
end
else
self.disabled_sps = false
end
if self.pu_fallback and insufficent_po_rate then
self.current_waste_product = WASTE.PLUTONIUM
else self.current_waste_product = self.waste_product end
end
-- make sure dynamic tanks are allowing outflow if required
-- set all, rather than trying to determine which is for which (simpler & safer)
@@ -1063,6 +1141,14 @@ function facility.new(config, cooling_conf)
return self.pu_fallback
end
-- enable/disable SPS at low power
---@param enabled boolean requested state
---@return boolean enabled newly set value
function public.set_sps_low_power(enabled)
self.sps_low_power = enabled == true
return self.sps_low_power
end
--#endregion
--#region Diagnostic Testing
@@ -1167,7 +1253,8 @@ function facility.new(config, cooling_conf)
self.status_text[2],
self.group_map,
self.current_waste_product,
(self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM)
self.pu_fallback and (self.current_waste_product == WASTE.PLUTONIUM) and (self.waste_product ~= WASTE.PLUTONIUM),
self.disabled_sps
}
end
@@ -1183,15 +1270,21 @@ function facility.new(config, cooling_conf)
status.power = {
self.avg_charge.compute(),
self.avg_inflow.compute(),
self.avg_outflow.compute()
self.avg_outflow.compute(),
0
}
-- status of induction matricies (including tanks)
status.induction = {}
for i = 1, #self.induction do
local matrix = self.induction[i] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
local matrix = self.induction[i] ---@type unit_session
local db = matrix.get_db() ---@type imatrix_session_db
status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks }
local fe_per_ms = self.avg_net.compute()
local remaining = util.joules_to_fe(util.trinary(fe_per_ms >= 0, db.tanks.energy_need, db.tanks.energy))
status.power[4] = remaining / fe_per_ms
end
-- status of sps

View File

@@ -270,6 +270,12 @@ function coordinator.new_session(id, s_addr, in_queue, out_queue, timeout, facil
else
log.debug(log_header .. "CRDN set pu fallback packet length mismatch")
end
elseif cmd == FAC_COMMAND.SET_SPS_LP then
if pkt.length == 2 then
_send(CRDN_TYPE.FAC_CMD, { cmd, facility.set_sps_low_power(pkt.data[2]) })
else
log.debug(log_header .. "CRDN set sps low power packet length mismatch")
end
else
log.debug(log_header .. "CRDN facility command unknown")
end

View File

@@ -1,4 +1,5 @@
local comms = require("scada-common.comms")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local mqueue = require("scada-common.mqueue")
local types = require("scada-common.types")
@@ -105,6 +106,8 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
formed = false,
rps_tripped = false,
rps_trip_cause = "ok", ---@type rps_trip_cause
max_op_temp_H2O = 1200,
max_op_temp_Na = 1200,
---@class rps_status
rps_status = {
high_dmg = false,
@@ -138,11 +141,11 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
waste = 0,
waste_need = 0,
waste_fill = 0.0,
ccool_type = "?",
ccool_type = types.FLUID.EMPTY_GAS, ---@type fluid
ccool_amnt = 0,
ccool_need = 0,
ccool_fill = 0.0,
hcool_type = "?",
hcool_type = types.FLUID.EMPTY_GAS, ---@type fluid
hcool_amnt = 0,
hcool_need = 0,
hcool_fill = 0.0
@@ -169,6 +172,21 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
---@class plc_session
local public = {}
-- compute maximum expected operational temperatures for high temp warnings
local function _compute_op_temps()
local JOULES_PER_MB = const.mek.JOULES_PER_MB
local BASE_BOIL_TEMP = const.mek.BASE_BOIL_TEMP
local heat_cap = self.sDB.mek_struct.heat_cap
local max_burn = self.sDB.mek_struct.max_burn
self.sDB.max_op_temp_H2O = max_burn * 2 * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
self.sDB.max_op_temp_Na = max_burn * (JOULES_PER_MB * heat_cap ^ -1) + BASE_BOIL_TEMP
log.info(util.sprintf(log_header .. "computed maximum operational temperatures %.3fK (H2O) and %.3fK (Na)",
self.sDB.max_op_temp_H2O, self.sDB.max_op_temp_Na))
end
-- copy in the RPS status
---@param rps_status table
local function _copy_rps_status(rps_status)
@@ -351,6 +369,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
local status = pcall(_copy_struct, pkt.data)
if status then
-- copied in structure data OK
_compute_op_temps()
self.received_struct = true
out_queue.push_data(svqtypes.SV_Q_DATA.PLC_BUILD_CHANGED, reactor_id)
else
@@ -639,6 +658,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
local cmd = message.message
if cmd == PLC_S_CMDS.ENABLE then
-- enable reactor
self.acks.disable = true
if not self.auto_lock then
_send(RPLC_TYPE.RPS_ENABLE, {})
end
@@ -695,6 +715,7 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
self.auto_cmd_token = 0
self.ramping_rate = true
self.acks.burn_rate = false
self.acks.disable = true
self.retry_times.burn_rate_req = util.time() + INITIAL_WAIT
_send(RPLC_TYPE.MEK_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate })
end
@@ -702,13 +723,14 @@ function plc.new_session(id, s_addr, reactor_id, in_queue, out_queue, timeout, f
elseif cmd.key == PLC_S_DATA.AUTO_BURN_RATE then
-- set automatic burn rate
if self.auto_lock then
cmd.val = math.floor(cmd.val * 100) / 100 -- round to 100ths place
cmd.val = math.floor(cmd.val * 100) / 100 -- round to 100ths place
if cmd.val >= 0 and cmd.val <= self.sDB.mek_struct.max_burn then
self.auto_cmd_token = util.time_ms()
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.acks.disable = true
self.retry_times.burn_rate_req = util.time() + INITIAL_AUTO_WAIT
_send(RPLC_TYPE.AUTO_BURN_RATE, { self.commanded_burn_rate, self.ramping_rate, self.auto_cmd_token })

View File

@@ -2,6 +2,8 @@
-- Redstone RTU Session I/O Controller
--
local rsio = require("scada-common.rsio")
local rsctl = {}
-- create a new redstone RTU I/O controller
@@ -16,7 +18,7 @@ function rsctl.new(redstone_rtus)
---@return boolean
function public.is_connected(port)
for i = 1, #redstone_rtus do
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
if db.io[port] ~= nil then return true end
end
@@ -28,8 +30,8 @@ function rsctl.new(redstone_rtus)
---@param value boolean
function public.digital_write(port, value)
for i = 1, #redstone_rtus do
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local io = db.io[port] ---@type rs_db_dig_io|nil
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local io = db.io[port] ---@type rs_db_dig_io|nil
if io ~= nil then io.write(value) end
end
end
@@ -40,12 +42,25 @@ function rsctl.new(redstone_rtus)
---@return boolean|nil
function public.digital_read(port)
for i = 1, #redstone_rtus do
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local io = db.io[port] ---@type rs_db_dig_io|nil
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local io = db.io[port] ---@type rs_db_dig_io|nil
if io ~= nil then return io.read() end
end
end
-- write to an analog redstone port (applies to all RTUs)
---@param port IO_PORT
---@param value number value
---@param min number minimum value for scaling 0 to 15
---@param max number maximum value for scaling 0 to 15
function public.analog_write(port, value, min, max)
for i = 1, #redstone_rtus do
local db = redstone_rtus[i].get_db() ---@type redstone_session_db
local io = db.io[port] ---@type rs_db_ana_io|nil
if io ~= nil then io.write(rsio.analog_write(value, min, max)) end
end
end
return public
end

View File

@@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.3.6"
local SUPERVISOR_VERSION = "v1.3.11"
local println = util.println
local println_ts = util.println_ts

View File

@@ -71,8 +71,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
---@class _unit_self
local self = {
r_id = reactor_id,
plc_s = nil, ---@class plc_session_struct
plc_i = nil, ---@class plc_session
plc_s = nil, ---@type plc_session_struct
plc_i = nil, ---@type plc_session
num_boilers = num_boilers,
num_turbines = num_turbines,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
@@ -147,7 +147,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
},
damage = 0,
temp = 0,
waste = 0
waste = 0,
high_temp_lim = 1150
},
---@class alarm_monitors
alarms = {

View File

@@ -133,7 +133,15 @@ function logic.update_annunciator(self)
self.last_heartbeat = plc_db.last_status_update
end
local flow_low = util.trinary(plc_db.mek_status.ccool_type == types.FLUID.SODIUM, ANNUNC_LIMS.RCSFlowLow_NA, ANNUNC_LIMS.RCSFlowLow_H2O)
local flow_low = ANNUNC_LIMS.RCSFlowLow_H2O
local high_temp = plc_db.max_op_temp_H2O
if plc_db.mek_status.ccool_type == types.FLUID.SODIUM then
flow_low = ANNUNC_LIMS.RCSFlowLow_NA
high_temp = plc_db.max_op_temp_Na
end
self.plc_cache.high_temp_lim = math.min(high_temp + ANNUNC_LIMS.OpTempTolerance, 1200)
-- update other annunciator fields
annunc.ReactorSCRAM = plc_db.rps_tripped
@@ -142,7 +150,7 @@ function logic.update_annunciator(self)
annunc.RCPTrip = plc_db.rps_tripped and (plc_db.rps_status.ex_hcool or plc_db.rps_status.low_cool)
annunc.RCSFlowLow = _get_dt(DT_KEYS.ReactorCCool) < flow_low
annunc.CoolantLevelLow = plc_db.mek_status.ccool_fill < ANNUNC_LIMS.CoolantLevelLow
annunc.ReactorTempHigh = plc_db.mek_status.temp > ANNUNC_LIMS.ReactorTempHigh
annunc.ReactorTempHigh = plc_db.mek_status.temp >= self.plc_cache.high_temp_lim
annunc.ReactorHighDeltaT = _get_dt(DT_KEYS.ReactorTemp) > ANNUNC_LIMS.ReactorHighDeltaT
annunc.FuelInputRateLow = _get_dt(DT_KEYS.ReactorFuel) < -1.0 or plc_db.mek_status.fuel_fill <= ANNUNC_LIMS.FuelLevelLow
annunc.WasteLineOcclusion = _get_dt(DT_KEYS.ReactorWaste) > 1.0 or plc_db.mek_status.waste_fill >= ANNUNC_LIMS.WasteLevelHigh
@@ -542,7 +550,8 @@ function logic.update_alarms(self)
end
-- High Temperature
_update_alarm_state(self, plc_cache.temp >= ALARM_LIMS.HIGH_TEMP, self.alarms.ReactorHighTemp)
local high_temp = math.min(math.max(self.plc_cache.high_temp_lim, 1100), 1199.995)
_update_alarm_state(self, plc_cache.temp >= high_temp, self.alarms.ReactorHighTemp)
-- Waste Leak
_update_alarm_state(self, plc_cache.waste >= 1.0, self.alarms.ReactorWasteLeak)
@@ -718,11 +727,11 @@ function logic.update_status_text(self)
self.status_text[2] = "elevated level of radiation"
end
elseif is_active(self.alarms.ReactorOverTemp) then
self.status_text = { "CORE OVER TEMP", "reactor core temperature >=1200K" }
self.status_text = { "CORE OVER TEMP", "reactor core temp damaging" }
elseif is_active(self.alarms.ReactorWasteLeak) then
self.status_text = { "WASTE LEAK", "radioactive waste leak detected" }
elseif is_active(self.alarms.ReactorHighTemp) then
self.status_text = { "CORE TEMP HIGH", "reactor core temperature >1150K" }
self.status_text = { "CORE TEMP HIGH", "reactor core temperature high" }
elseif is_active(self.alarms.ReactorHighWaste) then
self.status_text = { "WASTE LEVEL HIGH", "waste accumulating in reactor" }
elseif is_active(self.alarms.TurbineTrip) then

View File

@@ -1,16 +1,28 @@
require("/initenv").init_env()
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
local rsio = require("scada-common.rsio")
local util = require("scada-common.util")
local testutils = require("test.testutils")
local IO = rsio.IO
local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE
local print = util.print
local println = util.println
local IO = rsio.IO
local IO_LVL = rsio.IO_LVL
local IO_MODE = rsio.IO_MODE
-- list of inverted digital signals<br>
-- only using the key for a quick lookup, value just can't be nil
local DIG_INV = {
[IO.F_SCRAM] = 0,
[IO.R_SCRAM] = 0,
[IO.WASTE_PU] = 0,
[IO.WASTE_PO] = 0,
[IO.WASTE_POPL] = 0,
[IO.WASTE_AM] = 0,
[IO.U_EMER_COOL] = 0
}
println("starting RSIO tester")
println("")
@@ -50,8 +62,8 @@ testutils.pause()
println(">>> checking invalid ports:")
testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "")
testutils.test_func_nil("rsio.to_string", rsio.to_string, "")
testutils.test_func("rsio.to_string", rsio.to_string, { -1, 100, false }, "UNKNOWN")
testutils.test_func_nil("rsio.to_string", rsio.to_string, "UNKNOWN")
testutils.test_func("rsio.get_io_mode", rsio.get_io_mode, { -1, 100, false }, IO_MODE.ANALOG_IN)
testutils.test_func_nil("rsio.get_io_mode", rsio.get_io_mode, IO_MODE.ANALOG_IN)
@@ -100,46 +112,35 @@ println(">>> checking port I/O:")
print("rsio.digital_is_active(...): ")
-- check input ports
assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.LOW) == true, "IO_F_SCRAM_HIGH")
assert(rsio.digital_is_active(IO.F_SCRAM, IO_LVL.HIGH) == false, "IO_F_SCRAM_LOW")
assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.LOW) == true, "IO_R_SCRAM_HIGH")
assert(rsio.digital_is_active(IO.R_SCRAM, IO_LVL.HIGH) == false, "IO_R_SCRAM_LOW")
assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.LOW) == false, "IO_R_ENABLE_HIGH")
assert(rsio.digital_is_active(IO.R_ENABLE, IO_LVL.HIGH) == true, "IO_R_ENABLE_LOW")
-- check all digital ports
for i = 1, rsio.NUM_PORTS do
if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then
local high = DIG_INV[i] == nil
assert(rsio.digital_is_active(i, IO_LVL.LOW) == not high, "IO_" .. rsio.to_string(i) .. "_LOW")
assert(rsio.digital_is_active(i, IO_LVL.HIGH) == high, "IO_" .. rsio.to_string(i) .. "_HIGH")
end
end
-- non-inputs should always return LOW
assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.LOW) == false, "IO_OUT_READ_LOW")
assert(rsio.digital_is_active(IO.F_ALARM, IO_LVL.HIGH) == false, "IO_OUT_READ_HIGH")
assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.LOW) == nil, "ANA_DIG_READ_LOW")
assert(rsio.digital_is_active(IO.F_MATRIX_CHG, IO_LVL.HIGH) == nil, "ANA_DIG_READ_HIGH")
println("PASS")
-- check output ports
-- check digital write
print("rsio.digital_write(...): ")
print("rsio.digital_write_active(...): ")
-- check output ports
assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.LOW, "IO_F_ALARM_LOW")
assert(rsio.digital_write_active(IO.F_ALARM, true) == IO_LVL.HIGH, "IO_F_ALARM_HIGH")
assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.HIGH, "IO_WASTE_PU_HIGH")
assert(rsio.digital_write_active(IO.WASTE_PU, true) == IO_LVL.LOW, "IO_WASTE_PU_LOW")
assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.HIGH, "IO_WASTE_PO_HIGH")
assert(rsio.digital_write_active(IO.WASTE_PO, true) == IO_LVL.LOW, "IO_WASTE_PO_LOW")
assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.HIGH, "IO_WASTE_POPL_HIGH")
assert(rsio.digital_write_active(IO.WASTE_POPL, true) == IO_LVL.LOW, "IO_WASTE_POPL_LOW")
assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.HIGH, "IO_WASTE_AM_HIGH")
assert(rsio.digital_write_active(IO.WASTE_AM, true) == IO_LVL.LOW, "IO_WASTE_AM_LOW")
-- check all reactor output ports (all are active high)
for i = IO.R_ALARM, (IO.R_PLC_TIMEOUT - IO.R_ALARM + 1) do
assert(rsio.to_string(i) ~= "", "REACTOR_IO_BAD_PORT")
assert(rsio.digital_write_active(i, false) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW")
assert(rsio.digital_write_active(i, true) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH")
-- check all digital ports
for i = 1, rsio.NUM_PORTS do
if rsio.get_io_mode(i) == IO_MODE.DIGITAL_IN or rsio.get_io_mode(i) == IO_MODE.DIGITAL_OUT then
local high = DIG_INV[i] == nil
assert(rsio.digital_write_active(i, not high) == IO_LVL.LOW, "IO_" .. rsio.to_string(i) .. "_LOW")
assert(rsio.digital_write_active(i, high) == IO_LVL.HIGH, "IO_" .. rsio.to_string(i) .. "_HIGH")
end
end
-- non-outputs should always return false
assert(rsio.digital_write_active(IO.F_SCRAM, false) == IO_LVL.LOW, "IO_IN_WRITE_FALSE")
assert(rsio.digital_write_active(IO.F_SCRAM, true) == IO_LVL.LOW, "IO_IN_WRITE_TRUE")
assert(rsio.digital_write_active(IO.F_MATRIX_CHG, true) == false, "ANA_DIG_WRITE_TRUE")
assert(rsio.digital_write_active(IO.F_MATRIX_CHG, false) == false, "ANA_DIG_WRITE_FALSE")
println("PASS")