Compare commits

...

69 Commits

Author SHA1 Message Date
Mikayla
b1ad2084f2 Merge pull request #610 from MikaylaFischler/devel
2025.02.26 Release
2025-02-26 18:52:56 -05:00
Mikayla Fischler
1971153dae configurator summary enhancements 2025-02-26 18:38:21 -05:00
Mikayla Fischler
5fc8912590 #480 fixed aux coolant connection to boilers with emergency coolant 2025-02-26 13:08:58 -05:00
Mikayla
122fa1a7a7 Merge pull request #609 from MikaylaFischler/480-auxiliarybackup-water-control
480 auxiliary backup water control
2025-02-25 16:44:23 -05:00
Mikayla Fischler
2b73196130 #480 updated aux coolant logic 2025-02-25 16:43:03 -05:00
Mikayla Fischler
d45f19c8a6 refactor 2025-02-25 15:32:07 -05:00
Mikayla Fischler
a9f68ce3ea Merge branch 'devel' into 480-auxiliarybackup-water-control 2025-02-25 14:53:44 -05:00
Mikayla Fischler
7ab5ea710f additional supervisor config validations 2025-02-25 14:52:05 -05:00
Mikayla Fischler
de41ee56aa #480 auxiliary water coolant 2025-02-25 14:33:25 -05:00
Mikayla Fischler
99ea59a86b #526 coordinator front panel scale to term size 2025-02-16 13:32:08 -05:00
Mikayla Fischler
234652b886 #526 cleanup 2025-02-16 13:21:00 -05:00
Mikayla Fischler
e37e3ba696 #526 supervisor front panel scale to term size 2025-02-16 13:20:23 -05:00
Mikayla Fischler
20b71bead1 #526 RTU gateway front panel scale to term size 2025-02-16 12:51:10 -05:00
Mikayla Fischler
18d093e72d #526 reactor PLC front panel scale to term size 2025-02-16 12:34:06 -05:00
Mikayla Fischler
21eae4932f #607 updated deny message 2025-02-16 11:54:45 -05:00
Mikayla Fischler
9163fb14c4 RTU gateway version increment 2025-02-16 11:45:26 -05:00
Mikayla Fischler
02db01524c Merge branch 'devel' of github.com:MikaylaFischler/cc-mek-scada into devel 2025-02-16 11:44:44 -05:00
Mikayla Fischler
e0d1eb3445 #608 fixed front panel network lights 2025-02-16 11:44:30 -05:00
Mikayla Fischler
7c22c172d5 #607 deny reactor PLC with index out of range 2025-02-16 11:43:32 -05:00
Mikayla
7b29702000 #480 auxiliary coolant control logic 2025-02-11 22:42:52 +00:00
Mikayla
425a6c8775 #480 added auxiliary coolant redstone output 2025-02-11 22:42:07 +00:00
Mikayla
eafcd89aba updated SNA RTU note after #564's changes 2025-02-11 22:40:17 +00:00
Mikayla Fischler
016cd988e1 #564 improved SNA statistic clarity 2025-02-09 16:17:37 -05:00
Mikayla
06a8e3d9ca Merge pull request #603 from MikaylaFischler/589-reboot-recovery
589 reboot recovery
2025-02-09 15:25:48 -05:00
Mikayla Fischler
5f22069ce1 #589 cleanup and fixes 2025-02-09 14:19:06 -05:00
Mikayla Fischler
ecdaf78ed0 #589 moved boot recovery to facility update file 2025-02-09 13:48:20 -05:00
Mikayla Fischler
3b2fb00285 cleanup 2025-02-09 13:37:22 -05:00
Mikayla Fischler
54167e2113 #589 only scram reactor on plc boot if networked 2025-02-09 13:13:18 -05:00
Mikayla Fischler
22cdbc8638 #589 supervisor control reboot recovery 2025-02-09 13:07:36 -05:00
Mikayla Fischler
556331f75b better unit ready check 2025-02-09 13:07:01 -05:00
Mikayla Fischler
40cb9f599a #602 only auto reset units that should be 2025-02-09 13:06:44 -05:00
Mikayla Fischler
cab3427c70 #601 only reset on timeout once per unit per supervisor boot 2025-02-09 12:10:13 -05:00
Mikayla Fischler
4e31b33b09 #601 reset RPS if the triggering condition is a timeout on PLC session establish 2025-02-09 11:59:03 -05:00
Mikayla Fischler
f32855084e #589 WIP reboot recovery 2025-02-08 22:20:00 -05:00
Mikayla
b3cf40a01a #589 initial attempt at reboot recovery 2025-02-08 20:35:04 +00:00
Mikayla
cf9e26ac8f Merge pull request #599 from MikaylaFischler/devel
Pocket Beta Release
2025-01-27 12:52:32 -05:00
Mikayla
cbc84c5998 Merge pull request #598 from MikaylaFischler/559-modbus-device-busy-unrecoverable
559 modbus device busy unrecoverable
2025-01-27 11:49:50 -05:00
Mikayla Fischler
869e67710f #559 supervisor bugfix 2025-01-26 14:49:44 -05:00
Mikayla Fischler
1b9d3d3f23 Merge branch 'devel' into 559-modbus-device-busy-unrecoverable 2025-01-26 12:05:42 -05:00
Mikayla
0a060b656c Merge pull request #595 from MikaylaFischler/pocket-alpha-dev
Pocket Alpha
2025-01-20 17:18:01 -05:00
Mikayla Fischler
c859c22964 cleanup 2025-01-20 17:01:49 -05:00
Mikayla Fischler
3767c0f8d9 luacheck fixes and coordinator version bump 2025-01-20 16:26:41 -05:00
Mikayla Fischler
fbebc2a021 prep for beta 2025-01-20 16:24:18 -05:00
Mikayla Fischler
afd6800be6 updated pocket version 2025-01-20 15:40:00 -05:00
Mikayla Fischler
baba2e1411 #557 facility app data and fixes 2025-01-20 15:38:53 -05:00
Mikayla Fischler
127c878794 #557 facility app ui design complete 2025-01-20 12:21:51 -05:00
Mikayla
767b54c3e6 #557 facility tank overview page 2025-01-15 22:49:55 +00:00
Mikayla Fischler
1c57fc1fe3 #557 work on facility app 2025-01-11 11:57:28 -05:00
Mikayla Fischler
2d83de8b88 moved ETA string generation to icontrol 2025-01-11 11:57:06 -05:00
Mikayla Fischler
4a4234c8c8 #557 ui improvements 2025-01-10 22:52:27 -05:00
Mikayla
eb197e7fdd updated dynamic tank page to indicate which tank it is 2025-01-09 23:51:02 +00:00
Mikayla
78b0e1bf24 #557 facility app and induction matrix updates 2025-01-09 23:50:47 +00:00
Mikayla Fischler
cbc004a6c7 #557 induction matrix page updates 2025-01-08 22:49:05 -05:00
Mikayla Fischler
d05abf6e00 #557 facility app and sps page fixes 2025-01-08 21:54:45 -05:00
Mikayla
813e30bcde Merge branch 'devel' into pocket-alpha-dev 2025-01-09 00:17:22 +00:00
Mikayla
fb139949f8 fix to induction matrix transfer bars not rescaling with capacity changes 2025-01-09 00:17:00 +00:00
Mikayla
2fdc9feea7 #557 work on induction matrix page 2025-01-09 00:15:12 +00:00
Mikayla
eabb065d17 #557 ui cleanup on sps page 2025-01-09 00:14:49 +00:00
Mikayla
fb221a566c #557 facility app bug fix 2025-01-09 00:14:28 +00:00
Mikayla Fischler
cd4caf0163 #559 supervisor updates to handle busy errors 2025-01-08 19:07:53 -05:00
Mikayla Fischler
1190fe2dd5 #559 discard modbus messages if busy 2025-01-08 19:04:38 -05:00
Mikayla
4cb6f9ca0f #557 work on message data 2025-01-07 23:21:48 +00:00
Mikayla
872082b970 #557 sps page 2025-01-07 23:21:29 +00:00
Mikayla Fischler
071df9e431 #557 include matrix page 2025-01-05 15:08:41 -05:00
Mikayla Fischler
ae85cfc579 #557 start of induction matrix and sps pages 2025-01-05 15:05:01 -05:00
Mikayla Fischler
1dece587b2 cleanup 2025-01-05 14:40:36 -05:00
Mikayla Fischler
01c5d62f38 #557 skeleton of facility app with some pages 2025-01-05 14:39:16 -05:00
Mikayla
c6a5d487e0 comment updates and refactors 2025-01-04 15:33:57 +00:00
Mikayla
ba4a5aa85e #557 work on facility app 2025-01-04 15:33:37 +00:00
65 changed files with 1737 additions and 502 deletions

View File

@@ -550,7 +550,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -380,6 +380,18 @@ function coordinator.comms(version, nic, sv_watchdog)
_send_sv(PROTOCOL.SCADA_MGMT, MGMT_TYPE.CLOSE, {})
end
-- send the resume ready state to the supervisor
---@param mode PROCESS process control mode
---@param burn_target number burn rate target
---@param charge_target number charge level target
---@param gen_target number generation rate target
---@param limits number[] unit burn rate limits
function public.send_ready(mode, burn_target, charge_target, gen_target, limits)
_send_sv(PROTOCOL.SCADA_CRDN, CRDN_TYPE.PROCESS_READY, {
mode, burn_target, charge_target, gen_target, limits
})
end
-- send a facility command
---@param cmd FAC_COMMAND command
---@param option any? optional option options for the optional options (like waste mode)

View File

@@ -164,6 +164,7 @@ function iocontrol.init(conf, comms, temp_scale, energy_scale)
num_turbines = 0,
num_snas = 0,
has_tank = conf.cooling.r_cool[i].TankConnection,
aux_coolant = conf.cooling.aux_coolant[i],
status_lines = { "", "" },
@@ -495,6 +496,49 @@ end
--#region Statuses
-- generate the text string for the induction matrix charge/discharge ETA
---@param eta_ms number eta in milliseconds
local function gen_eta_text(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
return str
end
-- record and publish multiblock status data
---@param entry any
---@param data imatrix_session_db|sps_session_db|dynamicv_session_db|turbinev_session_db|boilerv_session_db
@@ -616,6 +660,7 @@ function iocontrol.update_facility_status(status)
ps.publish("avg_inflow", in_f)
ps.publish("avg_outflow", out_f)
ps.publish("eta_ms", eta)
ps.publish("eta_string", gen_eta_text(eta or 0))
ps.publish("is_charging", in_f > out_f)
ps.publish("is_discharging", out_f > in_f)
@@ -1170,7 +1215,7 @@ function iocontrol.update_unit_statuses(statuses)
local valve_states = status[6]
if type(valve_states) == "table" then
if #valve_states == 5 then
if #valve_states == 6 then
unit.unit_ps.publish("V_pu_conn", valve_states[1] > 0)
unit.unit_ps.publish("V_pu_state", valve_states[1] == 2)
unit.unit_ps.publish("V_po_conn", valve_states[2] > 0)
@@ -1181,6 +1226,8 @@ function iocontrol.update_unit_statuses(statuses)
unit.unit_ps.publish("V_am_state", valve_states[4] == 2)
unit.unit_ps.publish("V_emc_conn", valve_states[5] > 0)
unit.unit_ps.publish("V_emc_state", valve_states[5] == 2)
unit.unit_ps.publish("V_aux_conn", valve_states[6] > 0)
unit.unit_ps.publish("V_aux_state", valve_states[6] == 2)
else
log.debug(log_header .. "valve states length mismatch")
valid = false

View File

@@ -139,6 +139,11 @@ function process.init(iocontrol, coord_comms)
log.info("PROCESS: loaded priority groups settings")
end
-- report to the supervisor all initial configuration data has been sent
-- startup resume can occur if needed
local p = ctl_proc
pctl.comms.send_ready(p.mode, p.burn_target, p.charge_target, p.gen_target, p.limits)
end
-- create a handle to process control for usage of commands that get acknowledgements

View File

@@ -137,7 +137,7 @@ function renderer.try_start_fp()
if not engine.fp_ready then
-- show front panel view on terminal
status, msg = pcall(function ()
engine.ui.front_panel = DisplayBox{window=term.native(),fg_bg=style.fp.root}
engine.ui.front_panel = DisplayBox{window=term.current(),fg_bg=style.fp.root}
panel_view(engine.ui.front_panel, #engine.monitors.unit_displays)
end)

View File

@@ -260,11 +260,52 @@ function pocket.new_session(id, s_addr, i_seq_num, in_queue, out_queue, timeout)
{ fac.auto_ready, fac.auto_active, fac.auto_ramping, fac.auto_saturated },
{ fac.auto_current_waste_product, fac.auto_pu_fallback_active },
util.table_len(fac.tank_data_tbl),
fac.induction_data_tbl[1] ~= nil,
fac.sps_data_tbl[1] ~= nil,
fac.induction_data_tbl[1] ~= nil, ---@fixme this means nothing
fac.sps_data_tbl[1] ~= nil ---@fixme this means nothing
}
_send(CRDN_TYPE.API_GET_FAC, data)
elseif pkt.type == CRDN_TYPE.API_GET_FAC_DTL then
local fac = db.facility
local mtx_sps = fac.induction_ps_tbl[1]
local units = {}
local tank_statuses = {}
for i = 1, #db.units do
local u = db.units[i]
units[i] = { u.connected, u.annunciator, u.reactor_data, u.tank_data_tbl }
for t = 1, #u.tank_ps_tbl do table.insert(tank_statuses, u.tank_ps_tbl[t].get("computed_status")) end
end
for i = 1, #fac.tank_ps_tbl do table.insert(tank_statuses, fac.tank_ps_tbl[i].get("computed_status")) end
local matrix_data = {
mtx_sps.get("eta_string"),
mtx_sps.get("avg_charge"),
mtx_sps.get("avg_inflow"),
mtx_sps.get("avg_outflow"),
mtx_sps.get("is_charging"),
mtx_sps.get("is_discharging"),
mtx_sps.get("at_max_io")
}
local data = {
fac.all_sys_ok,
fac.rtu_count,
fac.auto_scram,
fac.ascram_status,
tank_statuses,
fac.tank_data_tbl,
fac.induction_ps_tbl[1].get("computed_status") or types.IMATRIX_STATE.OFFLINE,
fac.induction_data_tbl[1],
matrix_data,
fac.sps_ps_tbl[1].get("computed_status") or types.SPS_STATE.OFFLINE,
fac.sps_data_tbl[1],
units
}
_send(CRDN_TYPE.API_GET_FAC_DTL, 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]]

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.6.2"
local COORDINATOR_VERSION = "v1.6.11"
local CHUNK_LOAD_DELAY_S = 30.0

View File

@@ -25,10 +25,9 @@ local ALIGN = core.ALIGN
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param data imatrix_session_db matrix data
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id)
local function new_view(root, x, y, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
@@ -94,6 +93,7 @@ local function new_view(root, x, y, data, ps, id)
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val)
local data = db.facility.induction_data_tbl[id or 1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap
else return 0 end
@@ -105,46 +105,7 @@ local function new_view(root, x, y, data, ps, id)
local eta = TextBox{parent=rect,x=11,y=20,width=20,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)
eta.register(ps, "eta_string", eta.set_value)
end
return new_view

View File

@@ -28,6 +28,8 @@ local function init(parent, id)
local ps = iocontrol.get_db().fp.ps
local term_w, _ = term.getSize()
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=s_hi_bright}
@@ -43,9 +45,9 @@ local function init(parent, id)
local pkt_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pkt_fw_v.register(ps, ps_prefix .. "fw", pkt_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pkt_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
pkt_rtt.register(ps, ps_prefix .. "rtt", pkt_rtt.update)
pkt_rtt.register(ps, ps_prefix .. "rtt_color", pkt_rtt.recolor)

View File

@@ -59,7 +59,7 @@ local function make(parent, x, y, wide, unit_id)
local tank_conns = facility.tank_conns
local tank_types = facility.tank_fluid_types
local v_start = 1 + ((unit.unit_id - 1) * 5)
local v_start = 1 + ((unit.unit_id - 1) * 6)
local prv_start = 1 + ((unit.unit_id - 1) * 3)
local v_fields = { "pu", "po", "pl", "am" }
local v_names = {
@@ -94,11 +94,21 @@ local function make(parent, x, y, wide, unit_id)
if unit.num_boilers > 0 then
table.insert(rc_pipes, pipe(0, 1, _wide(28, 19), 1, colors.lightBlue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(28, 19), 3, colors.orange, true))
table.insert(rc_pipes, pipe(_wide(46 ,39), 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46,39), 3, _wide(72,58), 3, colors.white, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(_wide(46, 39), 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
local em_water = facility.tank_fluid_types[facility.tank_conns[unit_id]] == COOLANT_TYPE.WATER
local offset = util.trinary(unit.has_tank and em_water, 3, 0)
table.insert(rc_pipes, pipe(_wide(51, 41) + offset, 0, _wide(51, 41) + offset, 0, colors.blue, true))
end
else
table.insert(rc_pipes, pipe(0, 1, _wide(72,58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72,58), 3, colors.white, true))
table.insert(rc_pipes, pipe(0, 1, _wide(72, 58), 1, colors.blue, true))
table.insert(rc_pipes, pipe(0, 3, _wide(72, 58), 3, colors.white, true))
if unit.aux_coolant then
table.insert(rc_pipes, pipe(8, 0, 8, 0, colors.blue, true))
end
end
if unit.has_tank then
@@ -222,17 +232,21 @@ local function make(parent, x, y, wide, unit_id)
_machine(_wide(116, 94), 6, "SPENT WASTE \x1b")
TextBox{parent=waste,x=_wide(30,25),y=3,text="SNAs [Po]",alignment=ALIGN.CENTER,width=19,fg_bg=wh_gray}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=7,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_po = Rectangle{parent=waste,x=_wide(30,25),y=4,border=border(1,colors.gray,true),width=19,height=8,thin=true,fg_bg=style.theme.highlight_box_bright}
local sna_act = IndicatorLight{parent=sna_po,label="ACTIVE",colors=ind_grn}
local sna_cnt = DataIndicator{parent=sna_po,x=12,y=1,lu_colors=lu_c_d,label="CNT",unit="",format="%2d",value=0,width=7}
local sna_pk = DataIndicator{parent=sna_po,y=3,lu_colors=lu_c_d,label="PEAK",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="MAX",unit="mB/t",format="%8.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="IN",unit="mB/t",format="%9.2f",value=0,width=17}
TextBox{parent=sna_po,y=3,text="PEAK\x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
TextBox{parent=sna_po,text="MAX \x1a",width=5,fg_bg=cpair(style.theme.label_dark,colors._INHERIT)}
local sna_pk = DataIndicator{parent=sna_po,x=6,y=3,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_o = DataIndicator{parent=sna_po,x=6,lu_colors=lu_c_d,label="",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_max_i = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aMAX",unit="mB/t",format="%7.2f",value=0,width=17}
local sna_in = DataIndicator{parent=sna_po,lu_colors=lu_c_d,label="\x1aIN",unit="mB/t",format="%8.2f",value=0,width=17}
sna_act.register(unit.unit_ps, "po_rate", function (r) sna_act.update(r > 0) end)
sna_cnt.register(unit.unit_ps, "sna_count", sna_cnt.update)
sna_pk.register(unit.unit_ps, "sna_peak_rate", sna_pk.update)
sna_max.register(unit.unit_ps, "sna_max_rate", sna_max.update)
sna_max_o.register(unit.unit_ps, "sna_max_rate", sna_max_o.update)
sna_max_i.register(unit.unit_ps, "sna_max_rate", function (r) sna_max_i.update(r * 10) end)
sna_in.register(unit.unit_ps, "sna_in", sna_in.update)
return root

View File

@@ -286,7 +286,7 @@ local function init(main)
TextBox{parent=main,x=12,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", i * 5),colors=style.ind_grn}
local conn = IndicatorLight{parent=main,x=9,y=vy+1,label=util.sprintf("PV%02d-EMC", (i * 6) - 1),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=9,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_emc_conn", conn.update)
@@ -294,6 +294,35 @@ local function init(main)
end
end
------------------------------
-- auxiliary coolant valves --
------------------------------
for i = 1, facility.num_units do
if units[i].aux_coolant then
local vx
local vy = 3 + y_ofs(i)
if #emcool_pipes == 0 then
vx = util.trinary(units[i].num_boilers == 0, 36, 79)
else
local em_water = tank_types[tank_conns[i]] == COOLANT_TYPE.WATER
vx = util.trinary(units[i].num_boilers == 0, 58, util.trinary(units[i].has_tank and em_water, 94, 91))
end
PipeNetwork{parent=main,x=vx-6,y=vy,pipes={pipe(0,1,9,0,colors.blue,true)},bg=style.theme.bg}
TextBox{parent=main,x=vx,y=vy,text="\x10\x11",fg_bg=text_col,width=2}
TextBox{parent=main,x=vx+5,y=vy,text="\x1b",fg_bg=cpair(colors.blue,text_col.bkg),width=1}
local conn = IndicatorLight{parent=main,x=vx-3,y=vy+1,label=util.sprintf("PV%02d-AUX", i * 6),colors=style.ind_grn}
local open = IndicatorLight{parent=main,x=vx-3,y=vy+2,label="OPEN",colors=style.ind_wht}
conn.register(units[i].unit_ps, "V_aux_conn", conn.update)
open.register(units[i].unit_ps, "V_aux_state", open.update)
end
end
-------------------
-- dynamic tanks --
-------------------

View File

@@ -39,6 +39,8 @@ local led_grn = style.led_grn
local function init(panel, num_units)
local ps = iocontrol.get_db().fp.ps
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA COORDINATOR",alignment=ALIGN.CENTER,fg_bg=style.fp_theme.header}
local page_div = Div{parent=panel,x=1,y=3}
@@ -61,7 +63,7 @@ local function init(panel, num_units)
local modem = LED{parent=system,label="MODEM",colors=led_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.fp_ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.fp_ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(ps, "link_state", network.update)
else
@@ -131,9 +133,9 @@ local function init(panel, num_units)
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -145,7 +147,7 @@ local function init(panel, num_units)
-- API page
local api_page = Div{parent=page_div,x=1,y=1,hidden=true}
local api_list = ListBox{parent=api_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local api_list = ListBox{parent=api_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=api_list,height=1} -- padding
-- assemble page panes

View File

@@ -88,7 +88,7 @@ local function init(main)
util.nop()
imatrix(main, 131, cnc_bottom_align_start, facility.induction_data_tbl[1], facility.induction_ps_tbl[1])
imatrix(main, 131, cnc_bottom_align_start, facility.induction_ps_tbl[1])
end
return init

View File

@@ -385,7 +385,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -94,6 +94,7 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
-- API access
---@class pocket_ioctl_api
io.api = {
get_fac = function () comms.api__get_facility() end,
get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end,
@@ -192,6 +193,14 @@ function iocontrol.init_fac(conf)
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- 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 = {} ---@type pioctl_unit[]
for i = 1, conf.num_units do

View File

@@ -12,6 +12,8 @@ local ALARM_STATE = types.ALARM_STATE
local BLR_STATE = types.BOILER_STATE
local TRB_STATE = types.TURBINE_STATE
local TNK_STATE = types.TANK_STATE
local MTX_STATE = types.IMATRIX_STATE
local SPS_STATE = types.SPS_STATE
local io ---@type pocket_ioctl
local iorx = {} ---@class iorx
@@ -55,6 +57,11 @@ local function _record_multiblock_status(faulted, data, ps)
ps.publish("formed", data.formed)
ps.publish("faulted", faulted)
---@todo revisit this
if data.build then
for key, val in pairs(data.build) do ps.publish(key, val) end
end
for key, val in pairs(data.state) do ps.publish(key, val) end
for key, val in pairs(data.tanks) do ps.publish(key, val) end
end
@@ -647,10 +654,171 @@ function iorx.record_waste_data(data)
fac.ps.publish("po_am_rate", fac.waste_stats[5])
fac.ps.publish("spent_waste_rate", fac.waste_stats[6])
fac.ps.publish("sps_computed_status", f_data[8])
fac.sps_ps_tbl[1].publish("SPSStateStatus", f_data[8])
fac.ps.publish("sps_process_rate", f_data[9])
end
-- update facility app with facility and unit data from API_GET_FAC_DTL
---@param data table
function iorx.record_fac_detail_data(data)
local fac = io.facility
local tank_statuses = data[5]
local next_t_stat = 1
-- annunciator
fac.all_sys_ok = data[1]
fac.rtu_count = data[2]
fac.auto_scram = data[3]
fac.ascram_status = data[4]
fac.ps.publish("all_sys_ok", fac.all_sys_ok)
fac.ps.publish("rtu_count", fac.rtu_count)
fac.ps.publish("auto_scram", fac.auto_scram)
fac.ps.publish("as_matrix_fault", fac.ascram_status.matrix_fault)
fac.ps.publish("as_matrix_fill", fac.ascram_status.matrix_fill)
fac.ps.publish("as_crit_alarm", fac.ascram_status.crit_alarm)
fac.ps.publish("as_radiation", fac.ascram_status.radiation)
fac.ps.publish("as_gen_fault", fac.ascram_status.gen_fault)
-- unit data
local units = data[12]
for i = 1, io.facility.num_units do
local unit = io.units[i]
local u_rx = units[i]
unit.connected = u_rx[1]
unit.annunciator = u_rx[2]
unit.reactor_data = u_rx[3]
local control_status = 1
if unit.connected then
if unit.reactor_data.rps_tripped then control_status = 2 end
if unit.reactor_data.mek_status.status then
control_status = util.trinary(unit.annunciator.AutoControl, 4, 3)
end
end
unit.unit_ps.publish("U_ControlStatus", control_status)
unit.tank_data_tbl = u_rx[4]
for id = 1, #unit.tank_data_tbl do
local tank = unit.tank_data_tbl[id]
local ps = unit.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
end
-- facility dynamic tank data
fac.tank_data_tbl = data[6]
for id = 1, #fac.tank_data_tbl do
local tank = fac.tank_data_tbl[id]
local ps = fac.tank_ps_tbl[id]
local c_stat = tank_statuses[next_t_stat]
local tank_status = 1
if c_stat ~= TNK_STATE.OFFLINE then
if c_stat == TNK_STATE.FAULT then
tank_status = 3
elseif tank.formed then
tank_status = 4
else
tank_status = 2
end
_record_multiblock_status(c_stat == TNK_STATE.FAULT, tank, ps)
end
ps.publish("DynamicTankStatus", tank_status)
ps.publish("DynamicTankStateStatus", c_stat)
next_t_stat = next_t_stat + 1
end
-- induction matrix data
fac.induction_data_tbl[1] = data[8]
local matrix = fac.induction_data_tbl[1]
local m_ps = fac.induction_ps_tbl[1]
local m_stat = data[7]
local mtx_status = 1
if m_stat ~= MTX_STATE.OFFLINE then
if m_stat == MTX_STATE.FAULT then
mtx_status = 3
elseif matrix.formed then
mtx_status = 4
else
mtx_status = 2
end
_record_multiblock_status(m_stat == MTX_STATE.FAULT, matrix, m_ps)
end
m_ps.publish("InductionMatrixStatus", mtx_status)
m_ps.publish("InductionMatrixStateStatus", m_stat)
m_ps.publish("eta_string", data[9][1])
m_ps.publish("avg_charge", data[9][2])
m_ps.publish("avg_inflow", data[9][3])
m_ps.publish("avg_outflow", data[9][4])
m_ps.publish("is_charging", data[9][5])
m_ps.publish("is_discharging", data[9][6])
m_ps.publish("at_max_io", data[9][7])
-- sps data
fac.sps_data_tbl[1] = data[11]
local sps = fac.sps_data_tbl[1]
local s_ps = fac.sps_ps_tbl[1]
local s_stat = data[10]
local sps_status = 1
if s_stat ~= SPS_STATE.OFFLINE then
if s_stat == SPS_STATE.FAULT then
sps_status = 3
elseif sps.formed then
sps_status = 4
else
sps_status = 2
end
_record_multiblock_status(s_stat == SPS_STATE.FAULT, sps, s_ps)
end
s_ps.publish("SPSStatus", sps_status)
s_ps.publish("SPSStateStatus", s_stat)
end
return function (io_obj)
io = io_obj
return iorx

View File

@@ -88,16 +88,17 @@ local APP_ID = {
LOADER = 2,
-- main app pages
UNITS = 3,
CONTROL = 4,
PROCESS = 5,
WASTE = 6,
GUIDE = 7,
ABOUT = 8,
FACILITY = 4,
CONTROL = 5,
PROCESS = 6,
WASTE = 7,
GUIDE = 8,
ABOUT = 9,
-- diagnostic app pages
ALARMS = 9,
ALARMS = 10,
-- other
DUMMY = 10,
NUM_APPS = 10
DUMMY = 11,
NUM_APPS = 11
}
pocket.APP_ID = APP_ID
@@ -553,6 +554,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end
end
-- coordinator get facility app data
function public.api__get_facility()
if self.api.linked then _send_api(CRDN_TYPE.API_GET_FAC_DTL, {}) 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
@@ -729,6 +735,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
if _check_length(packet, 11) then
iocontrol.rx.record_facility_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_FAC_DTL then
if _check_length(packet, 12) then
iocontrol.rx.record_fac_detail_data(packet.data)
end
elseif packet.type == CRDN_TYPE.API_GET_UNIT then
if _check_length(packet, 12) and type(packet.data[1]) == "number" and iocontrol.get_db().units[packet.data[1]] then
iocontrol.rx.record_unit_data(packet.data)
@@ -903,7 +913,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.tone_buttons[i] ~= nil then
@@ -922,7 +932,7 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog, nav)
local ready = packet.data[1]
local states = packet.data[2]
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not ready"))
diag.tone_test.ready_warn.set_value(util.trinary(ready, "", "system not idle"))
for i = 1, #states do
if diag.tone_test.alarm_buttons[i] ~= nil then

View File

@@ -20,7 +20,7 @@ local pocket = require("pocket.pocket")
local renderer = require("pocket.renderer")
local threads = require("pocket.threads")
local POCKET_VERSION = "v0.12.13-alpha"
local POCKET_VERSION = "v0.13.1-beta"
local println = util.println
local println_ts = util.println_ts

View File

@@ -1,5 +1,5 @@
--
-- Unit Control Page
-- Facility & Unit Control App
--
local types = require("scada-common.types")

258
pocket/ui/apps/facility.lua Normal file
View File

@@ -0,0 +1,258 @@
--
-- Facility Overview App
--
local util = require("scada-common.util")
local iocontrol = require("pocket.iocontrol")
local pocket = require("pocket.pocket")
local style = require("pocket.ui.style")
local dyn_tank = require("pocket.ui.pages.dynamic_tank")
local facility_sps = require("pocket.ui.pages.facility_sps")
local induction_mtx = require("pocket.ui.pages.facility_matrix")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local MultiPane = require("graphics.elements.MultiPane")
local TextBox = require("graphics.elements.TextBox")
local WaitingAnim = require("graphics.elements.animations.Waiting")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local lu_col = style.label_unit_pair
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s
local yel_ind_s = style.icon_states.yel_ind_s
local grn_ind_s = style.icon_states.grn_ind_s
-- new unit page view
---@param root Container parent
local function new_view(root)
local db = iocontrol.get_db()
local frame = Div{parent=root,x=1,y=1}
local app = db.nav.register_app(APP_ID.FACILITY, frame, nil, false, true)
local load_div = Div{parent=frame,x=1,y=1}
local main = Div{parent=frame,x=1,y=1}
TextBox{parent=load_div,y=12,text="Loading...",alignment=ALIGN.CENTER}
WaitingAnim{parent=load_div,x=math.floor(main.get_width()/2)-1,y=8,fg_bg=cpair(colors.orange,colors._INHERIT)}
local load_pane = MultiPane{parent=main,x=1,y=1,panes={load_div,main}}
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
local tank_page_navs = {}
local page_div = nil ---@type Div|nil
-- load the app (create the elements)
local function load()
local fac = db.facility
local f_ps = fac.ps
page_div = Div{parent=main,y=2,width=main.get_width()}
local panes = {} ---@type Div[]
-- 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_fac()
last_update = util.time_ms()
end
end
--#region facility overview
local main_pane = Div{parent=page_div}
local f_div = Div{parent=main_pane,x=2,width=main.get_width()-2}
table.insert(panes, main_pane)
local fac_page = app.new_page(nil, #panes)
fac_page.tasks = { update }
TextBox{parent=f_div,y=1,text="Facility",alignment=ALIGN.CENTER}
local mtx_state = IconIndicator{parent=f_div,y=3,label="Matrix Status",states=basic_states}
local sps_state = IconIndicator{parent=f_div,label="SPS Status",states=basic_states}
mtx_state.register(fac.induction_ps_tbl[1], "InductionMatrixStatus", mtx_state.update)
sps_state.register(fac.sps_ps_tbl[1], "SPSStatus", sps_state.update)
TextBox{parent=f_div,y=6,text="RTU Gateways",fg_bg=label_fg_bg}
local rtu_count = DataIndicator{parent=f_div,x=19,y=6,label="",format="%3d",value=0,lu_colors=lu_col,width=3}
rtu_count.register(f_ps, "rtu_count", rtu_count.update)
TextBox{parent=f_div,y=8,text="Induction Matrix",alignment=ALIGN.CENTER}
local eta = TextBox{parent=f_div,x=1,y=10,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=cpair(colors.white,colors.gray)}
eta.register(fac.induction_ps_tbl[1], "eta_string", eta.set_value)
TextBox{parent=f_div,y=12,text="Unit Statuses",alignment=ALIGN.CENTER}
f_div.line_break()
for i = 1, fac.num_units do
local ctrl = IconIndicator{parent=f_div,label="U"..i.." Control State",states=mode_states}
ctrl.register(db.units[i].unit_ps, "U_ControlStatus", ctrl.update)
end
--#endregion
--#region facility annunciator
local a_pane = Div{parent=page_div}
local a_div = Div{parent=a_pane,x=2,width=main.get_width()-2}
table.insert(panes, a_pane)
local annunc_page = app.new_page(nil, #panes)
annunc_page.tasks = { update }
TextBox{parent=a_div,y=1,text="Annunciator",alignment=ALIGN.CENTER}
local all_ok = IconIndicator{parent=a_div,y=3,label="Units Online",states=grn_ind_s}
local ind_mat = IconIndicator{parent=a_div,label="Induction Matrix",states=grn_ind_s}
local sps = IconIndicator{parent=a_div,label="SPS Connected",states=grn_ind_s}
all_ok.register(f_ps, "all_sys_ok", all_ok.update)
ind_mat.register(fac.induction_ps_tbl[1], "InductionMatrixStateStatus", function (status) ind_mat.update(status > 1) end)
sps.register(fac.sps_ps_tbl[1], "SPSStateStatus", function (status) sps.update(status > 1) end)
a_div.line_break()
local auto_scram = IconIndicator{parent=a_div,label="Automatic SCRAM",states=red_ind_s}
local matrix_flt = IconIndicator{parent=a_div,label="Ind. Matrix Fault",states=yel_ind_s}
local matrix_fill = IconIndicator{parent=a_div,label="Matrix Charge Hi",states=red_ind_s}
local unit_crit = IconIndicator{parent=a_div,label="Unit Crit. Alarm",states=red_ind_s}
local fac_rad_h = IconIndicator{parent=a_div,label="FAC Radiation Hi",states=red_ind_s}
local gen_fault = IconIndicator{parent=a_div,label="Gen Control Fault",states=yel_ind_s}
auto_scram.register(f_ps, "auto_scram", auto_scram.update)
matrix_flt.register(f_ps, "as_matrix_fault", matrix_flt.update)
matrix_fill.register(f_ps, "as_matrix_fill", matrix_fill.update)
unit_crit.register(f_ps, "as_crit_alarm", unit_crit.update)
fac_rad_h.register(f_ps, "as_radiation", fac_rad_h.update)
gen_fault.register(f_ps, "as_gen_fault", gen_fault.update)
--#endregion
--#region induction matrix
local mtx_page_nav = induction_mtx(app, panes, Div{parent=page_div}, fac.induction_ps_tbl[1], update)
--#endregion
--#region SPS
local sps_page_nav = facility_sps(app, panes, Div{parent=page_div}, fac.sps_ps_tbl[1], update)
--#endregion
--#region facility tank pages
local t_pane = Div{parent=page_div}
local t_div = Div{parent=t_pane,x=2,width=main.get_width()-2}
table.insert(panes, t_pane)
local tank_page = app.new_page(nil, #panes)
tank_page.tasks = { update }
TextBox{parent=t_div,y=1,text="Facility Tanks",alignment=ALIGN.CENTER}
local f_tank_id = 1
for t = 1, #fac.tank_list do
if fac.tank_list[t] == 1 then
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Unit Tank "..t.." (U-"..t..")",states=basic_states}
tank.register(db.units[t].tank_ps_tbl[1], "DynamicTankStatus", tank.update)
TextBox{parent=t_div,x=5,text="\x07 Unit "..t,fg_bg=label_fg_bg}
elseif fac.tank_list[t] == 2 then
tank_page_navs[f_tank_id] = dyn_tank(app, nil, panes, Div{parent=page_div}, t, fac.tank_ps_tbl[f_tank_id], update)
t_div.line_break()
local tank = IconIndicator{parent=t_div,x=1,label="Fac. Tank "..f_tank_id.." (F-"..f_tank_id..")",states=basic_states}
tank.register(fac.tank_ps_tbl[f_tank_id], "DynamicTankStatus", tank.update)
local connections = ""
for i = 1, #fac.tank_conns do
if fac.tank_conns[i] == t then
if connections ~= "" then
connections = connections .. "\n\x07 Unit " .. i
else
connections = "\x07 Unit " .. i
end
end
end
TextBox{parent=t_div,x=5,text=connections,fg_bg=label_fg_bg}
f_tank_id = f_tank_id + 1
end
end
--#endregion
-- setup multipane
local f_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(f_pane)
-- setup sidebar
local list = {
{ label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home },
{ label = "FAC", tall = true, color = core.cpair(colors.black, colors.orange), callback = fac_page.nav_to },
{ label = "ANN", color = core.cpair(colors.black, colors.yellow), callback = annunc_page.nav_to },
{ label = "MTX", color = core.cpair(colors.black, colors.white), callback = mtx_page_nav },
{ label = "SPS", color = core.cpair(colors.black, colors.purple), callback = sps_page_nav },
{ label = "TNK", tall = true, color = core.cpair(colors.black, colors.blue), callback = tank_page.nav_to }
}
for i = 1, #fac.tank_data_tbl do
table.insert(list, { label = "F-" .. i, color = core.cpair(colors.black, colors.lightGray), callback = tank_page_navs[i] })
end
app.set_sidebar(list)
-- done, show the app
load_pane.set_value(2)
end
-- delete the elements and switch back to the loading screen
local function unload()
if page_div then
page_div.delete()
page_div = nil
end
app.set_sidebar({ { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = db.nav.go_home } })
app.delete_pages()
-- show loading screen
load_pane.set_value(1)
end
app.set_load(load)
app.set_unload(unload)
return main
end
return new_view

View File

@@ -1,5 +1,5 @@
--
-- Process Control Page
-- Process Control App
--
local types = require("scada-common.types")

View File

@@ -1,5 +1,5 @@
--
-- Unit Overview Page
-- Unit Overview App
--
local util = require("scada-common.util")
@@ -33,9 +33,8 @@ local cpair = core.cpair
local APP_ID = pocket.APP_ID
-- local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local basic_states = style.icon_states.basic_states
local mode_states = style.icon_states.mode_states
local red_ind_s = style.icon_states.red_ind_s

View File

@@ -1,5 +1,5 @@
--
-- Waste Control Page
-- Waste Control App
--
local util = require("scada-common.util")
@@ -33,9 +33,7 @@ local APP_ID = pocket.APP_ID
local label_fg_bg = style.label
local text_fg = style.text_fg
local lu_col = style.label_unit_pair
local yel_ind_s = style.icon_states.yel_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
@@ -249,7 +247,7 @@ local function new_view(root)
local sps_status = StateIndicator{parent=s_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
sps_status.register(f_ps, "sps_computed_status", sps_status.update)
sps_status.register(db.facility.sps_ps_tbl[1], "SPSStateStatus", sps_status.update)
TextBox{parent=s_div,y=5,text="Input Rate",width=10,fg_bg=label_fg_bg}
local sps_in = DataIndicator{parent=s_div,label="",format="%16.2f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
@@ -264,8 +262,8 @@ local function new_view(root)
--#endregion
-- setup multipane
local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(u_pane)
local w_pane = MultiPane{parent=page_div,x=1,y=1,panes=panes}
app.set_root_pane(w_pane)
-- setup sidebar

View File

@@ -10,6 +10,7 @@ local pocket = require("pocket.pocket")
local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local facil_app = require("pocket.ui.apps.facility")
local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local process_app = require("pocket.ui.apps.process")
@@ -45,7 +46,7 @@ local function init(main)
local db = iocontrol.get_db()
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
TextBox{parent=main,y=1,text=" S C ",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)}
@@ -65,6 +66,7 @@ local function init(main)
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
facil_app(page_div)
control_app(page_div)
process_app(page_div)
waste_app(page_div)

View File

@@ -18,6 +18,7 @@ local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local CONTAINER_MODE = types.CONTAINER_MODE
local COOLANT_TYPE = types.COOLANT_TYPE
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
@@ -31,7 +32,7 @@ local mode_ind_s = {
-- create a dynamic tank view for the unit or facility app
---@param app pocket_app
---@param page nav_tree_page
---@param page nav_tree_page|nil parent page, if applicable
---@param panes Div[]
---@param tank_pane Div
---@param tank_id integer global facility tank ID (as used for tank list, etc)
@@ -46,22 +47,35 @@ return function (app, page, panes, tank_pane, tank_id, ps, update)
local tank_page = app.new_page(page, #panes)
tank_page.tasks = { update }
TextBox{parent=tank_div,y=1,text="Dyn Tank",width=9}
local status = StateIndicator{parent=tank_div,x=10,y=1,states=style.dtank.states,value=1,min_width=12}
local tank_assign = ""
local f_tank_count = 0
for i = 1, #fac.tank_list do
local is_fac = fac.tank_list[i] == 2
if is_fac then f_tank_count = f_tank_count + 1 end
if i == tank_id then
tank_assign = util.trinary(is_fac, "F-" .. f_tank_count, "U-" .. i)
break
end
end
TextBox{parent=tank_div,y=1,text="Dynamic Tank "..tank_assign,alignment=ALIGN.CENTER}
local status = StateIndicator{parent=tank_div,x=5,y=3,states=style.dtank.states,value=1,min_width=12}
status.register(ps, "DynamicTankStateStatus", status.update)
TextBox{parent=tank_div,y=3,text="Fill",width=10,fg_bg=label}
local tank_pcnt = DataIndicator{parent=tank_div,x=14,y=3,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_fg}
TextBox{parent=tank_div,y=5,text="Fill",width=10,fg_bg=label}
local tank_pcnt = DataIndicator{parent=tank_div,x=14,y=5,label="",format="%5.2f",value=100,unit="%",lu_colors=lu_col,width=8,fg_bg=text_fg}
local tank_amnt = DataIndicator{parent=tank_div,label="",format="%18d",value=0,commas=true,unit="mB",lu_colors=lu_col,width=21,fg_bg=text_fg}
local is_water = fac.tank_fluid_types[tank_id] == COOLANT_TYPE.WATER
TextBox{parent=tank_div,y=6,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=label}
local level = HorizontalBar{parent=tank_div,y=7,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=21}
TextBox{parent=tank_div,y=8,text=util.trinary(is_water,"Water","Sodium").." Level",width=12,fg_bg=label}
local level = HorizontalBar{parent=tank_div,y=9,bar_fg_bg=cpair(util.trinary(is_water,colors.blue,colors.lightBlue),colors.gray),height=1,width=21}
TextBox{parent=tank_div,y=9,text="Tank Fill Mode",width=14,fg_bg=label}
local can_fill = IconIndicator{parent=tank_div,y=10,label="Fill",states=mode_ind_s}
local can_empty = IconIndicator{parent=tank_div,y=11,label="Empty",states=mode_ind_s}
TextBox{parent=tank_div,y=11,text="Tank Fill Mode",width=14,fg_bg=label}
local can_fill = IconIndicator{parent=tank_div,y=12,label="Fill",states=mode_ind_s}
local can_empty = IconIndicator{parent=tank_div,y=13,label="Empty",states=mode_ind_s}
local function _can_fill(mode)
can_fill.update((mode == CONTAINER_MODE.BOTH) or (mode == CONTAINER_MODE.FILL))

View File

@@ -0,0 +1,121 @@
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local IconIndicator = require("graphics.elements.indicators.IconIndicator")
local PowerIndicator = require("graphics.elements.indicators.PowerIndicator")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
local yel_ind_s = style.icon_states.yel_ind_s
local wht_ind_s = style.icon_states.wht_ind_s
-- create an induction matrix view for the facility app
---@param app pocket_app
---@param panes Div[]
---@param matrix_pane Div
---@param ps psil
---@param update function
return function (app, panes, matrix_pane, ps, update)
local db = iocontrol.get_db()
local fac = db.facility
local mtx_div = Div{parent=matrix_pane,x=2,width=matrix_pane.get_width()-2}
table.insert(panes, mtx_div)
local matrix_page = app.new_page(nil, #panes)
matrix_page.tasks = { update }
TextBox{parent=mtx_div,y=1,text="Induction Matrix",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=mtx_div,x=5,y=3,states=style.imatrix.states,value=1,min_width=12}
status.register(ps, "InductionMatrixStateStatus", status.update)
TextBox{parent=mtx_div,text="Chg",y=5,fg_bg=label}
local chg_bar = HorizontalBar{parent=mtx_div,x=5,y=5,height=1,fg_bg=cpair(colors.green,colors.gray)}
TextBox{parent=mtx_div,text="In",y=7,fg_bg=label}
local in_bar = HorizontalBar{parent=mtx_div,x=5,y=7,height=1,fg_bg=cpair(colors.blue,colors.gray)}
TextBox{parent=mtx_div,text="Out",y=9,fg_bg=label}
local out_bar = HorizontalBar{parent=mtx_div,x=5,y=9,height=1,fg_bg=cpair(colors.red,colors.gray)}
local function calc_saturation(val)
local data = fac.induction_data_tbl[1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap
else return 0 end
end
chg_bar.register(ps, "energy_fill", chg_bar.update)
in_bar.register(ps, "last_input", function (val) in_bar.update(calc_saturation(val)) end)
out_bar.register(ps, "last_output", function (val) out_bar.update(calc_saturation(val)) end)
local energy = PowerIndicator{parent=mtx_div,y=11,lu_colors=lu_col,label="Chg: ",unit=db.energy_label,format="%8.2f",value=0,width=21,fg_bg=text_fg}
local avg_chg = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",value=0,width=21,fg_bg=text_fg}
local input = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="In: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local avg_in = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local output = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="Out: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
local avg_out = PowerIndicator{parent=mtx_div,lu_colors=lu_col,label="\xb7Avg: ",unit=db.energy_label,format="%8.2f",rate=true,value=0,width=21,fg_bg=text_fg}
energy.register(ps, "energy", function (val) energy.update(db.energy_convert(val)) end)
avg_chg.register(ps, "avg_charge", avg_chg.update)
input.register(ps, "last_input", function (val) input.update(db.energy_convert(val)) end)
avg_in.register(ps, "avg_inflow", avg_in.update)
output.register(ps, "last_output", function (val) output.update(db.energy_convert(val)) end)
avg_out.register(ps, "avg_outflow", avg_out.update)
local mtx_ext_div = Div{parent=matrix_pane,x=2,width=matrix_pane.get_width()-2}
table.insert(panes, mtx_ext_div)
local mtx_ext_page = app.new_page(matrix_page, #panes)
mtx_ext_page.tasks = { update }
PushButton{parent=mtx_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=mtx_ext_page.nav_to}
PushButton{parent=mtx_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=matrix_page.nav_to}
TextBox{parent=mtx_ext_div,y=1,text="More Matrix Info",alignment=ALIGN.CENTER}
local chging = IconIndicator{parent=mtx_ext_div,y=3,label="Charging",states=wht_ind_s}
local dischg = IconIndicator{parent=mtx_ext_div,y=4,label="Discharging",states=wht_ind_s}
TextBox{parent=mtx_ext_div,text="Energy Fill",x=1,y=6,width=13,fg_bg=label}
local fill = DataIndicator{parent=mtx_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
chging.register(ps, "is_charging", chging.update)
dischg.register(ps, "is_discharging", dischg.update)
fill.register(ps, "energy_fill", function (x) fill.update(x * 100) end)
local max_io = IconIndicator{parent=mtx_ext_div,y=8,label="Max I/O Rate",states=yel_ind_s}
TextBox{parent=mtx_ext_div,text="Input Util.",x=1,y=10,width=13,fg_bg=label}
local in_util = DataIndicator{parent=mtx_ext_div,x=14,y=10,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
TextBox{parent=mtx_ext_div,text="Output Util.",x=1,y=11,width=13,fg_bg=label}
local out_util = DataIndicator{parent=mtx_ext_div,x=14,y=11,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
max_io.register(ps, "at_max_io", max_io.update)
in_util.register(ps, "last_input", function (x) in_util.update(calc_saturation(x) * 100) end)
out_util.register(ps, "last_output", function (x) out_util.update(calc_saturation(x) * 100) end)
TextBox{parent=mtx_ext_div,text="Capacity ("..db.energy_label..")",x=1,y=13,fg_bg=label}
local capacity = DataIndicator{parent=mtx_ext_div,y=14,lu_colors=lu_col,label="",unit="",format="%21d",value=0,width=21,fg_bg=text_fg}
TextBox{parent=mtx_ext_div,text="Max In/Out ("..db.energy_label.."/t)",x=1,y=15,fg_bg=label}
local trans_cap = DataIndicator{parent=mtx_ext_div,y=16,lu_colors=lu_col,label="",unit="",format="%21d",rate=true,value=0,width=21,fg_bg=text_fg}
capacity.register(ps, "max_energy", function (val) capacity.update(db.energy_convert(val)) end)
trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(db.energy_convert(val)) end)
return matrix_page.nav_to
end

View File

@@ -0,0 +1,84 @@
local iocontrol = require("pocket.iocontrol")
local style = require("pocket.ui.style")
local core = require("graphics.core")
local Div = require("graphics.elements.Div")
local TextBox = require("graphics.elements.TextBox")
local PushButton = require("graphics.elements.controls.PushButton")
local DataIndicator = require("graphics.elements.indicators.DataIndicator")
local HorizontalBar = require("graphics.elements.indicators.HorizontalBar")
local StateIndicator = require("graphics.elements.indicators.StateIndicator")
local ALIGN = core.ALIGN
local cpair = core.cpair
local label = style.label
local lu_col = style.label_unit_pair
local text_fg = style.text_fg
-- create an SPS view in the facility app
---@param app pocket_app
---@param panes Div[]
---@param sps_pane Div
---@param ps psil
---@param update function
return function (app, panes, sps_pane, ps, update)
local db = iocontrol.get_db()
local sps_div = Div{parent=sps_pane,x=2,width=sps_pane.get_width()-2}
table.insert(panes, sps_div)
local sps_page = app.new_page(nil, #panes)
sps_page.tasks = { update }
TextBox{parent=sps_div,y=1,text="Facility SPS",alignment=ALIGN.CENTER}
local status = StateIndicator{parent=sps_div,x=5,y=3,states=style.sps.states,value=1,min_width=12}
status.register(ps, "SPSStateStatus", status.update)
TextBox{parent=sps_div,text="Po",y=5,fg_bg=label}
local po_bar = HorizontalBar{parent=sps_div,x=4,y=5,fg_bg=cpair(colors.cyan,colors.gray),height=1}
TextBox{parent=sps_div,text="AM",y=7,fg_bg=label}
local am_bar = HorizontalBar{parent=sps_div,x=4,y=7,fg_bg=cpair(colors.purple,colors.gray),height=1}
po_bar.register(ps, "input_fill", po_bar.update)
am_bar.register(ps, "output_fill", am_bar.update)
TextBox{parent=sps_div,y=9,text="Input Rate",width=10,fg_bg=label}
local input_rate = DataIndicator{parent=sps_div,label="",format="%16.2f",value=0,unit="mB/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
TextBox{parent=sps_div,y=12,text="Production Rate",width=15,fg_bg=label}
local proc_rate = DataIndicator{parent=sps_div,label="",format="%16d",value=0,unit="\xb5B/t",lu_colors=lu_col,width=21,fg_bg=text_fg}
proc_rate.register(ps, "process_rate", function (r) proc_rate.update(r * 1000) end)
input_rate.register(db.facility.ps, "po_am_rate", input_rate.update)
local sps_ext_div = Div{parent=sps_pane,x=2,width=sps_pane.get_width()-2}
table.insert(panes, sps_ext_div)
local sps_ext_page = app.new_page(sps_page, #panes)
sps_ext_page.tasks = { update }
PushButton{parent=sps_div,x=9,y=18,text="MORE",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_ext_page.nav_to}
PushButton{parent=sps_ext_div,x=9,y=18,text="BACK",min_width=6,fg_bg=cpair(colors.lightGray,colors.gray),active_fg_bg=cpair(colors.gray,colors.lightGray),callback=sps_page.nav_to}
TextBox{parent=sps_ext_div,y=1,text="More SPS Info",alignment=ALIGN.CENTER}
TextBox{parent=sps_ext_div,text="Polonium",x=1,y=3,width=13,fg_bg=label}
local input_p = DataIndicator{parent=sps_ext_div,x=14,y=3,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local input_amnt = DataIndicator{parent=sps_ext_div,x=1,y=4,lu_colors=lu_col,label="",unit="mB",format="%18.0f",value=0,commas=true,width=21,fg_bg=text_fg}
input_p.register(ps, "input_fill", function (x) input_p.update(x * 100) end)
input_amnt.register(ps, "input", function (x) input_amnt.update(x.amount) end)
TextBox{parent=sps_ext_div,text="Antimatter",x=1,y=6,width=15,fg_bg=label}
local output_p = DataIndicator{parent=sps_ext_div,x=14,y=6,lu_colors=lu_col,label="",unit="%",format="%6.2f",value=0,width=8,fg_bg=text_fg}
local output_amnt = DataIndicator{parent=sps_ext_div,x=1,y=7,lu_colors=lu_col,label="",unit="\xb5B",format="%18.3f",value=0,commas=true,width=21,fg_bg=text_fg}
output_p.register(ps, "output_fill", function (x) output_p.update(x * 100) end)
output_amnt.register(ps, "output", function (x) output_amnt.update(x.amount) end)
return sps_page.nav_to
end

View File

@@ -46,7 +46,7 @@ local function new_view(root)
local active_fg_bg = cpair(colors.white,colors.gray)
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=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.FACILITY)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.CONTROL)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.PROCESS)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.WASTE)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}

View File

@@ -592,7 +592,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, style, exit)
local c = util.trinary(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -40,6 +40,8 @@ local function init(panel)
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
local header = TextBox{parent=panel,y=1,text="FISSION REACTOR PLC - UNIT ?",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
header.register(databus.ps, "unit_id", function (id) header.set_value(util.c("FISSION REACTOR PLC - UNIT ", id)) end)
@@ -60,7 +62,7 @@ local function init(panel)
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update)
else
@@ -121,7 +123,7 @@ local function init(panel)
-- status & controls
--
local status = Div{parent=panel,width=19,height=18,x=17,y=3}
local status = Div{parent=panel,width=term_w-32,height=18,x=17,y=3}
local active = LED{parent=status,x=2,width=12,label="RCT ACTIVE",colors=ind_grn}
@@ -131,14 +133,15 @@ local function init(panel)
emer_cool.register(databus.ps, "emer_cool", emer_cool.update)
end
local status_trip_rct = Rectangle{parent=status,width=20,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local status_trip = Div{parent=status_trip_rct,width=18,height=1,fg_bg=s_hi_box}
local status_trip_rct = Rectangle{parent=status,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local status_trip = Div{parent=status_trip_rct,height=1,fg_bg=s_hi_box}
local scram = LED{parent=status_trip,width=10,label="RPS TRIP",colors=ind_red,flash=true,period=flasher.PERIOD.BLINK_250_MS}
local controls_rct = Rectangle{parent=status,width=17,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local controls = Div{parent=controls_rct,width=15,height=1,fg_bg=s_hi_box}
PushButton{parent=controls,x=1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
local controls_rct = Rectangle{parent=status,width=status.get_width()-2,height=3,x=1,border=border(1,s_hi_box.bkg,true),even_inner=true}
local controls = Div{parent=controls_rct,width=controls_rct.get_width()-2,height=1,fg_bg=s_hi_box}
local button_padding = math.floor((controls.get_width() - 14) / 3)
PushButton{parent=controls,x=button_padding+1,y=1,min_width=7,text="SCRAM",callback=databus.rps_scram,fg_bg=cpair(colors.black,colors.red),active_fg_bg=cpair(colors.black,colors.red_off)}
PushButton{parent=controls,x=(2*button_padding)+9,y=1,min_width=7,text="RESET",callback=databus.rps_reset,fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=cpair(colors.black,colors.yellow_off)}
active.register(databus.ps, "reactor_active", active.update)
scram.register(databus.ps, "rps_scram", scram.update)
@@ -147,9 +150,9 @@ local function init(panel)
-- about footer
--
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -158,7 +161,7 @@ local function init(panel)
-- rps list
--
local rps = Rectangle{parent=panel,width=16,height=16,x=36,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
local rps = Rectangle{parent=panel,width=16,height=16,x=term_w-15,y=3,border=border(1,s_hi_box.bkg),thin=true,fg_bg=s_hi_box}
local rps_man = LED{parent=rps,label="MANUAL",colors=ind_red}
local rps_auto = LED{parent=rps,label="AUTOMATIC",colors=ind_red}
local rps_tmo = LED{parent=rps,label="TIMEOUT",colors=ind_red}

View File

@@ -23,8 +23,7 @@ local AUTO_ACK = comms.PLC_AUTO_ACK
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
-- specific errors thrown when scram/start is used that still count as success
local PCALL_SCRAM_MSG = "Scram requires the reactor to be active."
local PCALL_START_MSG = "Reactor is already active."

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.8.14"
local R_PLC_VERSION = "v1.8.19"
local println = util.println
local println_ts = util.println_ts
@@ -169,12 +169,12 @@ local function main()
-- PLC init<br>
--- EVENT_CONSUMER: this function consumes events
local function init()
-- just booting up, no fission allowed (neutrons stay put thanks)
if (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
-- scram on boot if networked, otherwise leave the reactor be
if __shared_memory.networked and (not plc_state.no_reactor) and plc_state.reactor_formed and smem_dev.reactor.getStatus() then
smem_dev.reactor.scram()
end
-- front panel time!
-- setup front panel
if not renderer.ui_ready() then
local message
plc_state.fp_ok, message = renderer.try_start_ui(config.FrontPanelTheme, config.ColorMode)

View File

@@ -149,7 +149,7 @@ function peripherals.create(tool_ctl, main_pane, cfg_sys, peri_cfg, style)
reposition("This SNA is for reactor unit # .", 46, 1, 31, 4, 7)
self.p_idx.hide()
self.p_assign_btn.hide(true)
self.p_desc_ext.set_value("Before adding lots of SNAs: multiply the \"PEAK\" rate on the flow monitor (after connecting at least 1 SNA) by 10 to get the mB/t of waste that they can process. Enough SNAs to provide 2x to 3x of your max burn rate should be a good margin to catch up after night or cloudy weather. Too many devices (such as SNAs) on one RTU can cause lag.")
self.p_desc_ext.set_value("Warning: too many devices on one RTU Gateway can cause lag. Note that 10x the \"PEAK\x1a\" rate on the flow monitor gives you the mB/t of waste that the SNA(s) can process. Enough SNAs to provide 2x to 3x of that unit's max burn rate should be a good margin to catch up after night or cloudy weather.")
elseif type == "dynamicValve" then
reposition("This is the below system's # dynamic tank.", 29, 4, 17, 6, 8)
self.p_assign_btn.show()

View File

@@ -74,11 +74,12 @@ local PORT_DESC_MAP = {
{ 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" }
{ IO.U_EMER_COOL, "Unit Emergency Cool. Valve" },
{ IO.U_AUX_COOL, "Unit Auxiliary 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, 0, 0, 0 }
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, 1 }
assert(#PORT_DESC_MAP == rsio.NUM_PORTS)
assert(#PORT_DSGN == rsio.NUM_PORTS)

View File

@@ -646,7 +646,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, ext, style)
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -35,13 +35,15 @@ local UNIT_TYPE_LABELS = { "UNKNOWN", "REDSTONE", "BOILER", "TURBINE", "DYNAMIC
local function init(panel, units)
local disabled_fg = style.fp.disabled_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="RTU GATEWAY",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
--
-- system indicators
--
local system = Div{parent=panel,width=14,height=18,x=2,y=3}
local system = Div{parent=panel,width=14,height=term_h-5,x=2,y=3}
local on = LED{parent=system,label="STATUS",colors=cpair(colors.green,colors.red)}
local heartbeat = LED{parent=system,label="HEARTBEAT",colors=ind_grn}
@@ -53,7 +55,7 @@ local function init(panel, units)
local modem = LED{parent=system,label="MODEM",colors=ind_grn}
if not style.colorblind then
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.orange,colors.yellow,style.ind_bkg}}
local network = RGBLED{parent=system,label="NETWORK",colors={colors.green,colors.red,colors.yellow,colors.orange,style.ind_bkg}}
network.update(types.PANEL_LINK_STATE.DISCONNECTED)
network.register(databus.ps, "link_state", network.update)
else
@@ -100,17 +102,17 @@ local function init(panel, units)
local comp_id = util.sprintf("(%d)", os.getComputerID())
TextBox{parent=system,x=9,y=4,width=6,text=comp_id,fg_bg=disabled_fg}
TextBox{parent=system,x=1,y=14,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=14,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
TextBox{parent=system,y=term_h-5,text="SPEAKERS",width=8,fg_bg=style.fp.text_fg}
local speaker_count = DataIndicator{parent=system,x=10,y=term_h-5,label="",format="%3d",value=0,width=3,fg_bg=style.theme.field_box}
speaker_count.register(databus.ps, "speaker_count", speaker_count.update)
--
-- about label
--
local about = Div{parent=panel,width=15,height=3,x=1,y=18,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=panel,width=15,height=2,y=term_h-1,fg_bg=disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -119,10 +121,10 @@ local function init(panel, units)
-- unit status list
--
local threads = Div{parent=panel,width=8,height=18,x=17,y=3}
local threads = Div{parent=panel,width=8,height=term_h-3,x=17,y=3}
-- display up to 16 units
local list_length = math.min(#units, 16)
-- display as many units as we can with 1 line of padding above and below
local list_length = math.min(#units, term_h - 3)
-- show routine statuses
for i = 1, list_length do
@@ -131,7 +133,7 @@ local function init(panel, units)
rt_unit.register(databus.ps, "routine__unit_" .. i, rt_unit.update)
end
local unit_hw_statuses = Div{parent=panel,height=18,x=25,y=3}
local unit_hw_statuses = Div{parent=panel,height=term_h-3,x=25,y=3}
-- show hardware statuses
for i = 1, list_length do
@@ -150,7 +152,7 @@ local function init(panel, units)
-- assignment (unit # or facility)
local for_unit = util.trinary(unit.reactor == 0, "\x1a FACIL ", "\x1a UNIT " .. unit.reactor)
TextBox{parent=unit_hw_statuses,y=i,x=19,text=for_unit,fg_bg=disabled_fg}
TextBox{parent=unit_hw_statuses,y=i,x=term_w-32,text=for_unit,fg_bg=disabled_fg}
end
end

View File

@@ -481,16 +481,14 @@ function rtu.comms(version, nic, conn_watchdog)
-- check validity then pass off to unit comms thread
return_code, reply = unit.modbus_io.check_request(packet)
if return_code then
-- check if there are more than 3 active transactions
-- still queue the packet, but this may indicate a problem
-- check if there are more than 3 active transactions, which will be treated as busy
if unit.pkt_queue.length() > 3 then
reply = modbus.reply__srv_device_busy(packet)
log.debug("queueing new request with " .. unit.pkt_queue.length() ..
" transactions already in the queue" .. unit_dbg_tag)
log.warning("device busy, discarding new request" .. unit_dbg_tag)
else
-- queue the command if not busy
unit.pkt_queue.push_packet(packet)
end
-- always queue the command even if busy
unit.pkt_queue.push_packet(packet)
else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag)
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.10.21"
local RTU_VERSION = "v1.11.6"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_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 = "3.0.3"
comms.api_version = "0.0.8"
comms.version = "3.0.5"
comms.api_version = "0.0.9"
---@enum PROTOCOL
local PROTOCOL = {
@@ -60,17 +60,19 @@ local MGMT_TYPE = {
---@enum CRDN_TYPE
local CRDN_TYPE = {
INITIAL_BUILDS = 0, -- initial, complete builds packet to the coordinator
FAC_BUILDS = 1, -- facility RTU builds
FAC_STATUS = 2, -- state of facility and facility devices
FAC_CMD = 3, -- faility command
UNIT_BUILDS = 4, -- build of each reactor unit (reactor + RTUs)
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_UNIT = 8, -- API: get reactor unit data
API_GET_CTRL = 9, -- API: get data for the control app
API_GET_PROC = 10, -- API: get data for the process app
API_GET_WASTE = 11 -- API: get data for the waste app
PROCESS_READY = 1, -- process init is complete + last set of info for supervisor startup recovery
FAC_BUILDS = 2, -- facility RTU builds
FAC_STATUS = 3, -- state of facility and facility devices
FAC_CMD = 4, -- faility command
UNIT_BUILDS = 5, -- build of each reactor unit (reactor + RTUs)
UNIT_STATUSES = 6, -- state of each of the reactor units
UNIT_CMD = 7, -- command a reactor unit
API_GET_FAC = 8, -- API: get the facility general data
API_GET_FAC_DTL = 9, -- API: get (detailed) data for the facility app
API_GET_UNIT = 10, -- API: get reactor unit data
API_GET_CTRL = 11, -- API: get data for the control app
API_GET_PROC = 12, -- API: get data for the process app
API_GET_WASTE = 13 -- API: get data for the waste app
}
---@enum ESTABLISH_ACK

View File

@@ -72,6 +72,8 @@ 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
rs.AUX_COOL_ENABLE = 0.60 -- actiation threshold (less than or equal) for U_AUX_COOL
rs.AUX_COOL_DISABLE = 1.00 -- deactivation threshold (greater than or equal) for U_AUX_COOL
constants.RS_THRESHOLDS = rs

View File

@@ -78,6 +78,7 @@ local IO_PORT = {
-- unit outputs
U_ALARM = 25, -- active high, unit alarm
U_EMER_COOL = 26, -- active low, emergency coolant control
U_AUX_COOL = 30, -- active low, auxiliary coolant control
-- analog outputs --
@@ -90,8 +91,8 @@ rsio.IO_DIR = IO_DIR
rsio.IO_MODE = IO_MODE
rsio.IO = IO_PORT
rsio.NUM_PORTS = 29
rsio.NUM_DIG_PORTS = 28
rsio.NUM_PORTS = 30
rsio.NUM_DIG_PORTS = 29
rsio.NUM_ANA_PORTS = 1
-- self checks
@@ -149,6 +150,7 @@ local MODES = {
[IO.R_PLC_TIMEOUT] = IO_MODE.DIGITAL_OUT,
[IO.U_ALARM] = IO_MODE.DIGITAL_OUT,
[IO.U_EMER_COOL] = IO_MODE.DIGITAL_OUT,
[IO.U_AUX_COOL] = IO_MODE.DIGITAL_OUT,
[IO.F_MATRIX_CHG] = IO_MODE.ANALOG_OUT
}
@@ -208,10 +210,11 @@ local RS_DIO_MAP = {
[IO.R_PLC_TIMEOUT] = { _in = _I_ACTIVE_HIGH, _out = _O_ACTIVE_HIGH, 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 }
[IO.U_EMER_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT },
[IO.U_AUX_COOL] = { _in = _I_ACTIVE_LOW, _out = _O_ACTIVE_LOW, mode = IO_DIR.OUT }
}
assert(rsio.NUM_DIG_PORTS == #RS_DIO_MAP, "RS_DIO_MAP length incorrect")
assert(rsio.NUM_DIG_PORTS == util.table_len(RS_DIO_MAP), "RS_DIO_MAP length incorrect")
-- get the I/O direction of a port
---@nodiscard

View File

@@ -24,7 +24,7 @@ local t_pack = table.pack
local util = {}
-- scada-common version
util.version = "1.4.10"
util.version = "1.4.12"
util.TICK_TIME_S = 0.05
util.TICK_TIME_MS = 50

View File

@@ -185,8 +185,9 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
local fac_c_6 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_7 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_8 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_c_9 = Div{parent=fac_cfg,x=2,y=4,width=49}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7, fac_c_8}}
local fac_pane = MultiPane{parent=fac_cfg,x=1,y=4,panes={fac_c_1,fac_c_2,fac_c_3,fac_c_4,fac_c_5,fac_c_6,fac_c_7,fac_c_8,fac_c_9}}
TextBox{parent=fac_cfg,x=1,y=2,text=" Facility Configuration",fg_bg=cpair(colors.black,colors.yellow)}
@@ -205,10 +206,18 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
nu_error.hide(true)
tmp_cfg.UnitCount = count
local confs = tool_ctl.cooling_elems
if count >= 2 then confs[2].line.show() else confs[2].line.hide(true) end
if count >= 3 then confs[3].line.show() else confs[3].line.hide(true) end
if count == 4 then confs[4].line.show() else confs[4].line.hide(true) end
local c_confs = tool_ctl.cooling_elems
local a_confs = tool_ctl.aux_cool_elems
for i = 2, 4 do
if count >= i then
c_confs[i].line.show()
a_confs[i].line.show()
else
c_confs[i].line.hide(true)
a_confs[i].line.hide(true)
end
end
fac_pane.set_value(2)
else nu_error.show() end
@@ -285,6 +294,14 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
else elem.div.hide(true) end
end
if not any_has_tank then
tmp_cfg.FacilityTankMode = 0
tmp_cfg.FacilityTankDefs = {}
tmp_cfg.FacilityTankList = {}
tmp_cfg.FacilityTankConns = {}
tmp_cfg.TankFluidTypes = {}
end
if any_has_tank then fac_pane.set_value(3) else main_pane.set_value(3) end
end
end
@@ -672,25 +689,48 @@ function facility.create(tool_ctl, main_pane, cfg_sys, fac_cfg, style)
PushButton{parent=fac_c_7,x=1,y=14,text="\x1b Back",callback=back_from_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_7,x=44,y=14,text="Next \x1a",callback=submit_tank_fluids,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Auxiliary Coolant
TextBox{parent=fac_c_8,height=5,text="Auxiliary water coolant can be enabled for units to provide extra water during turbine ramp-up. For water cooled reactors, this goes to the reactor. For sodium cooled reactors, water goes to the boiler."}
for i = 1, 4 do
local line = Div{parent=fac_c_8,x=1,y=7+i,height=1}
TextBox{parent=line,text="Unit "..i.." -",width=8}
local aux_cool = Checkbox{parent=line,x=10,y=1,label="Has Auxiliary Coolant",default=ini_cfg.AuxiliaryCoolant[i],box_fg_bg=cpair(colors.yellow,colors.black)}
tool_ctl.aux_cool_elems[i] = { line = line, enable = aux_cool }
end
local function submit_aux_cool()
tmp_cfg.AuxiliaryCoolant = {}
for i = 1, tmp_cfg.UnitCount do
tmp_cfg.AuxiliaryCoolant[i] = tool_ctl.aux_cool_elems[i].enable.get_value()
end
fac_pane.set_value(9)
end
PushButton{parent=fac_c_8,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(7)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_aux_cool,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion
--#region Extended Idling
TextBox{parent=fac_c_8,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
TextBox{parent=fac_c_8,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
TextBox{parent=fac_c_9,height=6,text="Charge control provides automatic control to maintain an induction matrix charge level. In order to have smoother control, reactors that were activated will be held on at 0.01 mB/t for a short period before allowing them to turn off. This minimizes overshooting the charge target."}
TextBox{parent=fac_c_9,y=8,height=3,text="You can extend this to a full minute to minimize reactors flickering on/off, but there may be more overshoot of the target."}
local ext_idling = Checkbox{parent=fac_c_8,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
local function back_from_idling()
fac_pane.set_value(tri(tmp_cfg.FacilityTankMode == 0, 3, 7))
end
local ext_idling = Checkbox{parent=fac_c_9,x=1,y=12,label="Enable Extended Idling",default=ini_cfg.ExtChargeIdling,box_fg_bg=cpair(colors.yellow,colors.black)}
local function submit_idling()
tmp_cfg.ExtChargeIdling = ext_idling.get_value()
main_pane.set_value(3)
end
PushButton{parent=fac_c_8,x=1,y=14,text="\x1b Back",callback=back_from_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_8,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_9,x=1,y=14,text="\x1b Back",callback=function()fac_pane.set_value(8)end,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
PushButton{parent=fac_c_9,x=44,y=14,text="Next \x1a",callback=submit_idling,fg_bg=nav_fg_bg,active_fg_bg=btn_act_fg_bg}
--#endregion

View File

@@ -402,6 +402,10 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
try_set(tool_ctl.tank_elems[i].tank_opt, ini_cfg.FacilityTankDefs[i])
end
for i = 1, #ini_cfg.AuxiliaryCoolant do
try_set(tool_ctl.aux_cool_elems[i].enable, ini_cfg.AuxiliaryCoolant[i])
end
tool_ctl.en_fac_tanks.set_value(ini_cfg.FacilityTankMode > 0)
tool_ctl.view_cfg.enable()
@@ -588,7 +592,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
end
if val == "" then val = "no facility tanks" end
elseif f[1] == "FacilityTankMode" and raw == 0 then val = "0 (n/a, unit mode)"
elseif f[1] == "FacilityTankMode" and raw == 0 then val = "no facility tanks"
elseif f[1] == "FacilityTankDefs" and type(cfg.FacilityTankDefs) == "table" then
local tank_name_list = { table.unpack(cfg.FacilityTankList) } ---@type (string|integer)[]
local next_f = 1
@@ -625,6 +629,13 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
val = ""
local count = 0
for idx = 1, #tank_list do
if tank_list[idx] > 0 then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #tank_list do
local prefix = "?"
local fluid = "water"
@@ -642,11 +653,28 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
fluid = "sodium"
end
val = val .. tri(val == "", "", "\n") .. util.sprintf(" \x07 tank %s - %s", prefix, fluid)
val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "tank %s - %s", prefix, fluid)
end
end
if val == "" then val = "no emergency coolant tanks" end
elseif f[1] == "AuxiliaryCoolant" then
val = ""
local count = 0
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then count = count + 1 end
end
local bullet = tri(count < 2, "", " \x07 ")
for idx = 1, #cfg.AuxiliaryCoolant do
if cfg.AuxiliaryCoolant[idx] then
val = val .. tri(val == "", "", "\n") .. util.sprintf(bullet .. "unit %d", idx)
end
end
if val == "" then val = "no auxiliary coolant" end
end
if not skip then
@@ -655,7 +683,7 @@ function system.create(tool_ctl, main_pane, cfg_sys, divs, fac_pane, style, exit
local c = tri(alternate, g_lg_fg_bg, cpair(colors.gray,colors.white))
alternate = not alternate
if string.len(val) > val_max_w then
if (string.len(val) > val_max_w) or string.find(val, "\n") then
local lines = util.strwrap(val, inner_width)
height = #lines + 1
end

View File

@@ -72,7 +72,8 @@ local tool_ctl = {
load_legacy = nil, ---@type function
cooling_elems = {}, ---@type { line: Div, turbines: NumberField, boilers: NumberField, tank: Checkbox }[]
tank_elems = {} ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
tank_elems = {}, ---@type { div: Div, tank_opt: Radio2D, no_tank: TextBox }[]
aux_cool_elems = {} ---@type { line: Div, enable: Checkbox }[]
}
---@class svr_config
@@ -84,6 +85,7 @@ local tmp_cfg = {
FacilityTankList = {}, ---@type integer[] list of tanks by slot (0 = none or covered by an above tank, 1 = unit tank, 2 = facility tank)
FacilityTankConns = {}, ---@type integer[] map of unit tank connections (indicies are units, values are tank indicies in the tank list)
TankFluidTypes = {}, ---@type integer[] which type of fluid each tank in the tank list should be containing
AuxiliaryCoolant = {}, ---@type boolean[] if a unit has auxiliary coolant
ExtChargeIdling = false,
SVR_Channel = nil, ---@type integer
PLC_Channel = nil, ---@type integer
@@ -117,6 +119,7 @@ local fields = {
{ "FacilityTankList", "Facility Tank List", {} }, -- hidden
{ "FacilityTankConns", "Facility Tank Connections", {} }, -- hidden
{ "TankFluidTypes", "Tank Fluid Types", {} },
{ "AuxiliaryCoolant", "Auxiliary Water Coolant", {} },
{ "ExtChargeIdling", "Extended Charge Idling", false },
{ "SVR_Channel", "SVR Channel", 16240 },
{ "PLC_Channel", "PLC Channel", 16241 },

View File

@@ -31,6 +31,17 @@ local START_STATUS = {
BLADE_MISMATCH = 2
}
---@enum RECOVERY_STATE
local RCV_STATE = {
INACTIVE = 0,
PRIMED = 1,
RUNNING = 2,
STOPPED = 3
}
local CHARGE_SCALER = 1000000 -- convert MFE to FE
local GEN_SCALER = 1000 -- convert kFE to FE
---@class facility_management
local facility = {}
@@ -41,7 +52,7 @@ function facility.new(config)
---@class _facility_self
local self = {
units = {}, ---@type reactor_unit[]
types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS },
types = { AUTO_SCRAM = AUTO_SCRAM, START_STATUS = START_STATUS, RCV_STATE = RCV_STATE },
status_text = { "START UP", "initializing..." },
all_sys_ok = false,
allow_testing = false,
@@ -53,7 +64,8 @@ function facility.new(config)
fac_tank_defs = config.FacilityTankDefs,
fac_tank_list = config.FacilityTankList,
fac_tank_conns = config.FacilityTankConns,
tank_fluid_types = config.TankFluidTypes
tank_fluid_types = config.TankFluidTypes,
aux_coolant = config.AuxiliaryCoolant
},
-- rtus
rtu_gw_conn_count = 0,
@@ -66,12 +78,15 @@ function facility.new(config)
-- redstone I/O control
io_ctl = nil, ---@type rs_controller
-- process control
recovery = RCV_STATE.INACTIVE, ---@type RECOVERY_STATE
recovery_boot_state = nil, ---@type sv_boot_state|nil
last_unit_states = {}, ---@type boolean[]
units_ready = false,
mode = PROCESS.INACTIVE,
last_mode = PROCESS.INACTIVE,
return_mode = PROCESS.INACTIVE,
mode_set = PROCESS.MAX_BURN,
start_fail = START_STATUS.OK,
mode = PROCESS.INACTIVE, ---@type PROCESS
last_mode = PROCESS.INACTIVE, ---@type PROCESS
return_mode = PROCESS.INACTIVE, ---@type PROCESS
mode_set = PROCESS.MAX_BURN, ---@type PROCESS
start_fail = START_STATUS.OK, ---@type START_STATUS
max_burn_combined = 0.0, -- maximum burn rate to clamp at
burn_target = 0.1, -- burn rate target for aggregate burn mode
charge_setpoint = 0, -- FE charge target setpoint
@@ -101,8 +116,8 @@ function facility.new(config)
last_error = 0.0,
last_time = 0.0,
-- waste processing
waste_product = WASTE.PLUTONIUM,
current_waste_product = WASTE.PLUTONIUM,
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
current_waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
pu_fallback = false,
sps_low_power = false,
disabled_sps = false,
@@ -126,14 +141,16 @@ function facility.new(config)
imtx_faulted_times = { 0, 0, 0 }
}
--#region SETUP
-- provide self to facility update functions
local f_update = fac_update(self)
-- create units
for i = 1, config.UnitCount do
table.insert(self.units,
unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling))
table.insert(self.units, unit.new(i, self.cooling_conf.r_cool[i].BoilerCount, self.cooling_conf.r_cool[i].TurbineCount, config.ExtChargeIdling, self.cooling_conf.aux_coolant[i]))
table.insert(self.group_map, AUTO_GROUP.MANUAL)
table.insert(self.last_unit_states, false)
end
-- list for RTU session management
@@ -149,6 +166,70 @@ function facility.new(config)
table.insert(self.test_tone_states, false)
end
-- init next boot state
settings.set("LastProcessState", PROCESS.INACTIVE)
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("FAC: failed to save initial control state into supervisor settings file")
end
--#endregion
-- PRIVATE FUNCTIONS --
-- check an auto process control configuration and save it if its valid (does not start the process)
---@param auto_cfg start_auto_config configuration
---@return boolean ready, number[] unit_limits
local function _auto_check_and_save(auto_cfg)
local ready = false
-- load up current limits
local limits = {}
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
self.mode_set = auto_cfg.mode
end
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
self.charge_setpoint = auto_cfg.charge_target * CHARGE_SCALER
end
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
self.gen_rate_setpoint = auto_cfg.gen_target * GEN_SCALER
end
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
for i = 1, config.UnitCount do
local limit = auto_cfg.limits[i]
if (type(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
end
return ready, limits
end
-- PUBLIC FUNCTIONS --
---@class facility
@@ -239,6 +320,9 @@ function facility.new(config)
-- update (iterate) the facility management
function public.update()
-- run reboot recovery routine if needed
f_update.boot_recovery()
-- run process control and evaluate automatic SCRAM
f_update.pre_auto()
f_update.auto_control(config.ExtChargeIdling)
@@ -267,6 +351,50 @@ function facility.new(config)
--#endregion
--#region Startup Recovery
-- on exit, use this to clear the boot state so we don't resume when exiting cleanly
function public.clear_boot_state()
settings.unset("LastProcessState")
settings.unset("LastUnitStates")
if not settings.save("/supervisor.settings") then
log.warning("facility.clear_boot_state(): failed to save supervisor settings file")
else
log.debug("FAC: cleared boot state on exit")
end
end
-- initialize facility resume boot recovery
---@param state sv_boot_state|nil
function public.boot_recovery_init(state)
if self.recovery == RCV_STATE.INACTIVE and state then
self.recovery_boot_state = state
self.recovery = RCV_STATE.PRIMED
log.info("FAC: startup resume ready")
end
end
-- attempt facility resume boot recovery
---@param auto_cfg start_auto_config configuration
function public.boot_recovery_start(auto_cfg)
if self.recovery == RCV_STATE.PRIMED then
self.recovery = util.trinary(_auto_check_and_save(auto_cfg), RCV_STATE.RUNNING, RCV_STATE.STOPPED)
log.info(util.c("FAC: startup resume ", util.trinary(self.recovery == RCV_STATE.RUNNING, "started", "failed")))
else self.recovery = RCV_STATE.STOPPED end
end
-- used on certain coordinator commands to end reboot recovery (remain in current operational state)
function public.cancel_recovery()
if self.recovery == RCV_STATE.RUNNING then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: process startup resume cancelled by user operation")
end
end
--#endregion
--#region Commands
-- SCRAM all reactor units
@@ -290,59 +418,13 @@ function facility.new(config)
function public.auto_stop() self.mode = PROCESS.INACTIVE end
-- set automatic control configuration and start the process
---@param auto_cfg sys_auto_config configuration
---@param auto_cfg start_auto_config configuration
---@return table response ready state (successfully started) and current configuration (after updating)
function public.auto_start(auto_cfg)
local charge_scaler = 1000000 -- convert MFE to FE
local gen_scaler = 1000 -- convert kFE to FE
local ready = false
local ready, limits = _auto_check_and_save(auto_cfg)
-- load up current limits
local limits = {}
for i = 1, config.UnitCount do
limits[i] = self.units[i].get_control_inf().lim_br100 * 100
end
-- only allow changes if not running
if self.mode == PROCESS.INACTIVE then
if (type(auto_cfg.mode) == "number") and (auto_cfg.mode > PROCESS.INACTIVE) and (auto_cfg.mode <= PROCESS.GEN_RATE) then
self.mode_set = auto_cfg.mode
end
if (type(auto_cfg.burn_target) == "number") and auto_cfg.burn_target >= 0.1 then
self.burn_target = auto_cfg.burn_target
end
if (type(auto_cfg.charge_target) == "number") and auto_cfg.charge_target >= 0 then
self.charge_setpoint = auto_cfg.charge_target * charge_scaler
end
if (type(auto_cfg.gen_target) == "number") and auto_cfg.gen_target >= 0 then
self.gen_rate_setpoint = auto_cfg.gen_target * gen_scaler
end
if (type(auto_cfg.limits) == "table") and (#auto_cfg.limits == config.UnitCount) then
for i = 1, config.UnitCount do
local limit = auto_cfg.limits[i]
if (type(limit) == "number") and (limit >= 0.1) then
limits[i] = limit
self.units[i].set_burn_limit(limit)
end
end
end
ready = self.mode_set > 0
if ((self.mode_set == PROCESS.CHARGE) and (self.charge_setpoint <= 0)) or
((self.mode_set == PROCESS.GEN_RATE) and (self.gen_rate_setpoint <= 0)) or
((self.mode_set == PROCESS.BURN_RATE) and (self.burn_target < 0.1)) then
ready = false
end
ready = ready and self.units_ready
if ready then self.mode = self.mode_set end
if ready and self.units_ready then
self.mode = self.mode_set
end
log.debug(util.c("FAC: process start ", util.trinary(ready, "accepted", "rejected")))
@@ -351,8 +433,8 @@ function facility.new(config)
ready,
self.mode_set,
self.burn_target,
self.charge_setpoint / charge_scaler,
self.gen_rate_setpoint / gen_scaler,
self.charge_setpoint / CHARGE_SCALER,
self.gen_rate_setpoint / GEN_SCALER,
limits
}
end

View File

@@ -1,17 +1,21 @@
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local audio = require("scada-common.audio")
local const = require("scada-common.constants")
local log = require("scada-common.log")
local rsio = require("scada-common.rsio")
local types = require("scada-common.types")
local util = require("scada-common.util")
local qtypes = require("supervisor.session.rtu.qtypes")
local plc = require("supervisor.session.plc")
local svsessions = require("supervisor.session.svsessions")
local qtypes = require("supervisor.session.rtu.qtypes")
local TONE = audio.TONE
local ALARM = types.ALARM
local PRIO = types.ALARM_PRIORITY
local ALARM_STATE = types.ALARM_STATE
local AUTO_GROUP = types.AUTO_GROUP
local CONTAINER_MODE = types.CONTAINER_MODE
local PROCESS = types.PROCESS
local PROCESS_NAMES = types.PROCESS_NAMES
@@ -131,6 +135,54 @@ end
--#region PUBLIC FUNCTIONS
-- run reboot recovery routine if needed
function update.boot_recovery()
local RCV_STATE = self.types.RCV_STATE
-- attempt reboot recovery if in progress
if self.recovery == RCV_STATE.RUNNING then
local was_inactive = self.recovery_boot_state.mode == PROCESS.INACTIVE or self.recovery_boot_state.mode == PROCESS.SYSTEM_ALARM_IDLE
-- try to start auto control
if self.recovery_boot_state.mode ~= nil and self.units_ready then
if not was_inactive then
self.mode = self.mode_set
log.info("FAC: process startup resume initiated")
end
self.recovery_boot_state.mode = nil
end
local recovered = self.recovery_boot_state.mode == nil or was_inactive
-- restore manual control reactors
for i = 1, #self.units do
local u = self.units[i]
if self.recovery_boot_state.unit_states[i] and self.group_map[i] == AUTO_GROUP.MANUAL then
recovered = false
if u.get_control_inf().ready then
local plc_s = svsessions.get_reactor_session(i)
if plc_s ~= nil then
plc_s.in_queue.push_command(plc.PLC_S_CMDS.ENABLE)
log.info("FAC: startup resume enabling manually controlled reactor unit #" .. i)
-- only execute once
self.recovery_boot_state.unit_states[i] = nil
end
end
end
end
if recovered then
self.recovery = RCV_STATE.STOPPED
self.recovery_boot_state = nil
log.info("FAC: startup resume sequence completed")
end
end
end
-- automatic control pre-update logic
function update.pre_auto()
-- unlink RTU sessions if they are closed
@@ -243,6 +295,11 @@ function update.auto_control(ExtChargeIdling)
log.debug(util.c("FAC: state changed from ", PROCESS_NAMES[self.last_mode + 1], " to ", PROCESS_NAMES[self.mode + 1]))
settings.set("LastProcessState", self.mode)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.auto_control(): failed to save supervisor settings file")
end
if (self.last_mode == PROCESS.INACTIVE) or (self.last_mode == PROCESS.GEN_RATE_FAULT_IDLE) then
self.start_fail = START_STATUS.OK
@@ -642,15 +699,16 @@ function update.auto_safety()
self.ascram_reason = AUTO_SCRAM.NONE
-- reset PLC RPS trips if we should
for i = 1, #self.units do
local u = self.units[i]
u.auto_cond_rps_reset()
for i = 1, #self.prio_defs do
for _, u in pairs(self.prio_defs[i]) do
u.auto_cond_rps_reset()
end
end
end
end
end
-- update last mode and set next mode
-- update last mode, set next mode, and update saved state as needed
function update.post_auto()
self.last_mode = self.mode
self.mode = next_mode
@@ -792,6 +850,7 @@ end
function update.unit_mgmt()
local insufficent_po_rate = false
local need_emcool = false
local write_state = false
for i = 1, #self.units do
local u = self.units[i]
@@ -807,6 +866,21 @@ function update.unit_mgmt()
if (self.cooling_conf.fac_tank_mode > 0) and u.is_emer_cool_tripped() and (self.cooling_conf.fac_tank_defs[i] == 2) then
need_emcool = true
end
-- check for enabled state changes to save
if self.last_unit_states[i] ~= u.is_reactor_enabled() then
self.last_unit_states[i] = u.is_reactor_enabled()
write_state = true
end
end
-- record unit control states
if write_state then
settings.set("LastUnitStates", self.last_unit_states)
if not settings.save("/supervisor.settings") then
log.warning("facility_update.unit_mgmt(): failed to save supervisor settings file")
end
end
-- update waste product

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg
local term_w, _ = term.getSize()
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
@@ -40,9 +42,9 @@ local function init(parent, id)
local pdg_fw_v = TextBox{parent=entry,x=14,y=2,text=" ------- ",width=20,fg_bg=label_fg}
pdg_fw_v.register(databus.ps, ps_prefix .. "fw", pdg_fw_v.set_value)
TextBox{parent=entry,x=35,y=2,text="RTT:",width=4}
local pdg_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-16,y=2,text="RTT:",width=4}
local pdg_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
pdg_rtt.register(databus.ps, ps_prefix .. "rtt", pdg_rtt.update)
pdg_rtt.register(databus.ps, ps_prefix .. "rtt_color", pdg_rtt.recolor)

View File

@@ -25,6 +25,8 @@ local function init(parent, id)
local label_fg = style.fp.label_fg
local term_w, _ = term.getSize()
-- root div
local root = Div{parent=parent,x=2,y=2,height=4,width=parent.get_width()-2}
local entry = Div{parent=root,x=2,y=1,height=3,fg_bg=style.theme.highlight_box_bright}
@@ -40,13 +42,13 @@ local function init(parent, id)
local unit_count = DataIndicator{parent=entry,x=17,y=2,label="",unit="",format="%2d",value=0,width=2,fg_bg=style.fp.label_d_fg}
unit_count.register(databus.ps, ps_prefix .. "units", unit_count.set_value)
TextBox{parent=entry,x=21,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=25,y=2,text=" ------- ",width=9,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-30,y=2,text="FW:",width=3}
local rtu_fw_v = TextBox{parent=entry,x=term_w-26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
rtu_fw_v.register(databus.ps, ps_prefix .. "fw", rtu_fw_v.set_value)
TextBox{parent=entry,x=36,y=2,text="RTT:",width=4}
local rtu_rtt = DataIndicator{parent=entry,x=40,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=46,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-15,y=2,text="RTT:",width=4}
local rtu_rtt = DataIndicator{parent=entry,x=term_w-11,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=entry,x=term_w-5,y=2,text="ms",width=4,fg_bg=label_fg}
rtu_rtt.register(databus.ps, ps_prefix .. "rtt", rtu_rtt.update)
rtu_rtt.register(databus.ps, ps_prefix .. "rtt_color", rtu_rtt.recolor)

View File

@@ -41,6 +41,8 @@ local function init(panel)
local label_fg = style.fp.label_fg
local label_d_fg = style.fp.label_d_fg
local term_w, term_h = term.getSize()
TextBox{parent=panel,y=1,text="SCADA SUPERVISOR",alignment=ALIGN.CENTER,fg_bg=style.theme.header}
local page_div = Div{parent=panel,x=1,y=3}
@@ -73,9 +75,9 @@ local function init(panel)
-- about footer
--
local about = Div{parent=main_page,width=15,height=3,x=1,y=16,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,x=1,y=1,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,x=1,y=2,text="NT: v00.00.00"}
local about = Div{parent=main_page,width=15,height=2,y=term_h-3,fg_bg=style.fp.disabled_fg}
local fw_v = TextBox{parent=about,text="FW: v00.00.00"}
local comms_v = TextBox{parent=about,text="NT: v00.00.00"}
fw_v.register(databus.ps, "version", function (version) fw_v.set_value(util.c("FW: ", version)) end)
comms_v.register(databus.ps, "comms_version", function (version) comms_v.set_value(util.c("NT: v", version)) end)
@@ -87,7 +89,7 @@ local function init(panel)
-- plc sessions page
local plc_page = Div{parent=page_div,x=1,y=1,hidden=true}
local plc_list = Div{parent=plc_page,x=2,y=2,width=49}
local plc_list = Div{parent=plc_page,x=2,y=2,width=term_w-2}
for i = 1, supervisor.config.UnitCount do
local ps_prefix = "plc_" .. i .. "_"
@@ -103,13 +105,13 @@ local function init(panel)
local plc_addr = TextBox{parent=plc_entry,x=17,y=2,text=" --- ",width=5,fg_bg=label_d_fg}
plc_addr.register(databus.ps, ps_prefix .. "addr", plc_addr.set_value)
TextBox{parent=plc_entry,x=23,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=27,y=2,text=" ------- ",width=9,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-28,y=2,text="FW:",width=3}
local plc_fw_v = TextBox{parent=plc_entry,x=term_w-24,y=2,text=" ------- ",width=9,fg_bg=label_fg}
plc_fw_v.register(databus.ps, ps_prefix .. "fw", plc_fw_v.set_value)
TextBox{parent=plc_entry,x=37,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=42,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-14,y=2,text="RTT:",width=4}
local plc_rtt = DataIndicator{parent=plc_entry,x=term_w-9,y=2,label="",unit="",format="%4d",value=0,width=4,fg_bg=label_fg}
TextBox{parent=plc_entry,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
plc_rtt.register(databus.ps, ps_prefix .. "rtt", plc_rtt.update)
plc_rtt.register(databus.ps, ps_prefix .. "rtt_color", plc_rtt.recolor)
@@ -119,13 +121,13 @@ local function init(panel)
-- rtu sessions page
local rtu_page = Div{parent=page_div,x=1,y=1,hidden=true}
local rtu_list = ListBox{parent=rtu_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local rtu_list = ListBox{parent=rtu_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=cpair(colors.black,colors.ivory),nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=rtu_list,height=1} -- padding
-- coordinator session page
local crd_page = Div{parent=page_div,x=1,y=1,hidden=true}
local crd_box = Div{parent=crd_page,x=2,y=2,width=49,height=4,fg_bg=s_hi_bright}
local crd_box = Div{parent=crd_page,x=2,y=2,width=term_w-2,height=4,fg_bg=s_hi_bright}
local crd_conn = LED{parent=crd_box,x=2,y=2,label="CONNECTION",colors=cpair(colors.green_hc,colors.green_off)}
crd_conn.register(databus.ps, "crd_conn", crd_conn.update)
@@ -138,27 +140,27 @@ local function init(panel)
local crd_fw_v = TextBox{parent=crd_box,x=26,y=2,text=" ------- ",width=9,fg_bg=label_fg}
crd_fw_v.register(databus.ps, "crd_fw", crd_fw_v.set_value)
TextBox{parent=crd_box,x=36,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=41,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=47,y=2,text="ms",width=4,fg_bg=label_fg}
TextBox{parent=crd_box,x=term_w-15,y=2,text="RTT:",width=4}
local crd_rtt = DataIndicator{parent=crd_box,x=term_w-10,y=2,label="",unit="",format="%5d",value=0,width=5,fg_bg=label_fg}
TextBox{parent=crd_box,x=term_w-4,y=2,text="ms",width=4,fg_bg=label_fg}
crd_rtt.register(databus.ps, "crd_rtt", crd_rtt.update)
crd_rtt.register(databus.ps, "crd_rtt_color", crd_rtt.recolor)
-- pocket sessions page
local pkt_page = Div{parent=page_div,x=1,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local pkt_page = Div{parent=page_div,y=1,hidden=true}
local pdg_list = ListBox{parent=pkt_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=pdg_list,height=1} -- padding
-- RTU device ID check/diagnostics page
local chk_page = Div{parent=page_div,x=1,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,x=1,y=1,height=17,width=51,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local chk_page = Div{parent=page_div,y=1,hidden=true}
local chk_list = ListBox{parent=chk_page,y=1,height=term_h-2,width=term_w,scroll_height=1000,fg_bg=style.fp.text_fg,nav_fg_bg=cpair(colors.gray,colors.lightGray),nav_active=cpair(colors.black,colors.gray)}
local _ = Div{parent=chk_list,height=1} -- padding
-- info page
local info_page = Div{parent=page_div,x=1,y=1,hidden=true}
local info_page = Div{parent=page_div,y=1,hidden=true}
local info = Div{parent=info_page,height=6,x=2,y=2}
TextBox{parent=info,text="SVR \x1a Supervisor Status"}
@@ -168,7 +170,7 @@ local function init(panel)
TextBox{parent=info,text="PKT \x1a Pocket Connections"}
TextBox{parent=info,text="DEV \x1a RTU Device/Configuration Alerts"}
local notes = Div{parent=info_page,width=49,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
local notes = Div{parent=info_page,width=term_w-2,height=8,x=2,y=9,fg_bg=style.fp.disabled_fg}
TextBox{parent=notes,text="The DEV tab will show missing devices and devices that connected with incorrect information. Missing entries will indicate how the configuration should be, duplicate entries will indicate what is a duplicate, and out-of-range entries will indicate the invalid entry. An out-of-range example is a #2 turbine when you should only have 1 turbine for that unit."}

View File

@@ -234,6 +234,23 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if pkt.type == CRDN_TYPE.INITIAL_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.builds = true
elseif pkt.type == CRDN_TYPE.PROCESS_READY then
if pkt.length == 5 then
-- coordinator has sent all initial process data, power-on recovery is now possible
---@type start_auto_config
local config = {
mode = pkt.data[1],
burn_target = pkt.data[2],
charge_target = pkt.data[3],
gen_target = pkt.data[4],
limits = pkt.data[5]
}
facility.boot_recovery_start(config)
else
log.debug(log_tag .. "CRDN process ready packet length mismatch")
end
elseif pkt.type == CRDN_TYPE.FAC_BUILDS then
-- acknowledgement to coordinator receiving builds
self.acks.fac_builds = true
@@ -243,8 +260,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
if cmd == FAC_COMMAND.SCRAM_ALL then
facility.scram_all()
facility.cancel_recovery()
_send(CRDN_TYPE.FAC_CMD, { cmd, true })
elseif cmd == FAC_COMMAND.STOP then
facility.cancel_recovery()
local was_active = facility.auto_is_active()
if was_active then
@@ -253,15 +273,16 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
_send(CRDN_TYPE.FAC_CMD, { cmd, was_active })
elseif cmd == FAC_COMMAND.START then
facility.cancel_recovery()
if pkt.length == 6 then
---@type sys_auto_config
---@diagnostic disable-next-line: missing-fields
---@class start_auto_config
local config = {
mode = pkt.data[2],
burn_target = pkt.data[3],
charge_target = pkt.data[4],
gen_target = pkt.data[5],
limits = pkt.data[6]
mode = pkt.data[2], ---@type PROCESS
burn_target = pkt.data[3], ---@type number
charge_target = pkt.data[4], ---@type number
gen_target = pkt.data[5], ---@type number
limits = pkt.data[6] ---@type number[]
}
_send(CRDN_TYPE.FAC_CMD, { cmd, table.unpack(facility.auto_start(config)) })
@@ -313,8 +334,11 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
local manual = facility.get_group(uid) == AUTO_GROUP.MANUAL
if cmd == UNIT_COMMAND.SCRAM then
facility.cancel_recovery()
out_queue.push_data(SV_Q_DATA.SCRAM, data)
elseif cmd == UNIT_COMMAND.START then
facility.cancel_recovery()
if manual then
out_queue.push_data(SV_Q_DATA.START, data)
else
@@ -324,6 +348,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
elseif cmd == UNIT_COMMAND.RESET_RPS then
out_queue.push_data(SV_Q_DATA.RESET_RPS, data)
elseif cmd == UNIT_COMMAND.SET_BURN then
facility.cancel_recovery()
if pkt.length == 3 then
if manual then
out_queue.push_data(SV_Q_DATA.SET_BURN, data)
@@ -354,6 +380,8 @@ function coordinator.new_session(id, s_addr, i_seq_num, in_queue, out_queue, tim
log.debug(log_tag .. "CRDN unit command reset alarm missing alarm id")
end
elseif cmd == UNIT_COMMAND.SET_GROUP then
facility.cancel_recovery()
if (pkt.length == 3) and (type(pkt.data[3]) == "number") and
(pkt.data[3] >= AUTO_GROUP.MANUAL) and (pkt.data[3] <= AUTO_GROUP.BACKUP) then
facility.set_group(unit.get_id(), pkt.data[3])

View File

@@ -53,15 +53,15 @@ local PERIODICS = {
---@param in_queue mqueue in message queue
---@param out_queue mqueue out message queue
---@param timeout number communications timeout
---@param initial_reset boolean[] initial PLC reset on timeout flags, indexed by reactor_id
---@param fp_ok boolean if the front panel UI is running
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, fp_ok)
function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue, timeout, initial_reset, fp_ok)
-- print a log message to the terminal as long as the UI isn't running
local function println(message) if not fp_ok then util.println_ts(message) end end
local log_tag = "plc_session(" .. id .. "): "
local self = {
commanded_state = false,
commanded_burn_rate = 0.0,
auto_cmd_token = 0,
ramping_rate = false,
@@ -72,6 +72,7 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
connected = true,
received_struct = false,
received_status_cache = false,
received_rps_status = false,
conn_watchdog = util.new_watchdog(timeout),
last_rtt = 0,
-- periodic messages
@@ -381,6 +382,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, pkt.data)
if status then
-- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout status sent")
end
end
else
-- error copying RPS status data
log.error(log_tag .. "failed to parse RPS status packet data")
@@ -394,6 +405,16 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
local status = pcall(_copy_rps_status, { true, table.unpack(pkt.data) })
if status then
-- copied in RPS status data OK
self.received_rps_status = true
-- try initial reset if needed
if initial_reset[reactor_id] then
initial_reset[reactor_id] = false
if self.sDB.rps_trip_cause == "timeout" then
_send(RPLC_TYPE.RPS_AUTO_RESET, {})
log.debug(log_tag .. "initial RPS reset on timeout alarm sent")
end
end
else
-- error copying RPS status data
log.error(log_tag .. "failed to parse RPS alarm status data")
@@ -487,6 +508,10 @@ function plc.new_session(id, s_addr, i_seq_num, reactor_id, in_queue, out_queue,
---@nodiscard
function public.get_db() return self.sDB end
-- check if the reactor structure, status, and RPS status have been received
---@nodiscard
function public.check_received_all_data() return self.received_struct and self.received_status_cache and self.received_rps_status end
-- check if ramping is completed by first verifying auto command token ack
---@nodiscard
function public.is_ramp_complete()

View File

@@ -105,27 +105,39 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 12 (start = 1, count = 12)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 12 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 12 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 13 through 15 (start = 13, count = 3)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 13, 3 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 13, 3 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 16 through 27 (start = 16, count = 12)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 16, 12 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -210,26 +222,12 @@ function boilerv.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -42,6 +42,8 @@ local PERIODICS = {
TANKS = 500
}
local WRITE_BUSY_WAIT = 1000
-- create a new dynamicv rtu session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -63,6 +65,8 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
mode_cmd = nil, ---@type container_mode|nil
resend_mode = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
@@ -101,45 +105,77 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
-- increment the container mode
local function _inc_cont_mode()
-- set mode command
if self.mode_cmd == "BOTH" then self.mode_cmd = "FILL"
elseif self.mode_cmd == "FILL" then self.mode_cmd = "EMPTY"
elseif self.mode_cmd == "EMPTY" then self.mode_cmd = "BOTH"
end
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
if self.session.send_request(TXN_TYPES.INC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- decrement the container mode
local function _dec_cont_mode()
-- set mode command
if self.mode_cmd == "BOTH" then self.mode_cmd = "EMPTY"
elseif self.mode_cmd == "EMPTY" then self.mode_cmd = "FILL"
elseif self.mode_cmd == "FILL" then self.mode_cmd = "BOTH"
end
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
if self.session.send_request(TXN_TYPES.DEC_CONT, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 , WRITE_BUSY_WAIT}) == false then
self.resend_mode = false
end
end
-- set the container mode
---@param mode container_mode
local function _set_cont_mode(mode)
self.mode_cmd = mode
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
if self.session.send_request(TXN_TYPES.SET_CONT, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }, WRITE_BUSY_WAIT) == false then
self.resend_mode = false
end
end
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 7 (start = 1, count = 7)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 7 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read holding register 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_MUL_HOLD_REGS, { 1, 1 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 8 through 9 (start = 8, count = 2)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 8, 2 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -182,6 +218,10 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
if m_pkt.length == 1 then
self.db.state.last_update = util.time_ms()
self.db.state.container_mode = m_pkt.data[1]
if self.mode_cmd == nil then
self.mode_cmd = self.db.state.container_mode
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
@@ -247,30 +287,22 @@ function dynamicv.new(session_id, unit_id, advert, out_queue)
end
end
-- try to resend mode if needed
if self.resend_mode then
self.resend_mode = false
_set_cont_mode(self.mode_cmd)
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -58,9 +58,12 @@ function envd.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the radiation readings of the device
local function _request_radiation()
---@param time_now integer
local function _request_radiation(time_now)
-- read input registers 1 and 2 (start = 1, count = 2)
self.session.send_request(TXN_TYPES.RAD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 })
if self.session.send_request(TXN_TYPES.RAD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 }) ~= false then
self.periodics.next_rad_req = time_now + PERIODICS.RAD
end
end
-- PUBLIC FUNCTIONS --
@@ -90,10 +93,7 @@ function envd.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_rad_req <= time_now then
_request_radiation()
self.periodics.next_rad_req = time_now + PERIODICS.RAD
end
if self.periodics.next_rad_req <= time_now then _request_radiation(time_now) end
self.session.post_update()
end

View File

@@ -89,27 +89,39 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 9 (start = 1, count = 9)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input register 10 through 11 (start = 10, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 2 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 2 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 12 through 15 (start = 12, count = 3)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 12, 3 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 12, 3 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -181,26 +193,12 @@ function imatrix.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -80,21 +80,30 @@ function sna.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 2 (start = 1, count = 2)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 2 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 3 through 4 (start = 3, count = 2)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 3, 2 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 3, 2 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 5 through 10 (start = 5, count = 6)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 5, 6 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 5, 6 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -152,20 +161,9 @@ function sna.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
self.session.post_update()
end

View File

@@ -94,27 +94,39 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- PRIVATE FUNCTIONS --
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 9 (start = 1, count = 9)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 9 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input register 10 (start = 10, count = 1)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 1 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 10, 1 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 11 through 19 (start = 11, count = 9)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 11, 9 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -191,26 +203,12 @@ function sps.new(session_id, unit_id, advert, out_queue)
-- update this runner
---@param time_now integer milliseconds
function public.update(time_now)
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -42,6 +42,8 @@ local PERIODICS = {
TANKS = 1000
}
local WRITE_BUSY_WAIT = 1000
-- create a new turbinev rtu session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -63,6 +65,8 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
local self = {
session = unit_session.new(session_id, unit_id, advert, out_queue, log_tag, TXN_TAGS),
has_build = false,
mode_cmd = nil, ---@type dumping_mode|nil
resend_mode = false,
periodics = {
next_formed_req = 0,
next_build_req = 0,
@@ -116,45 +120,77 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
-- increment the dumping mode
local function _inc_dump_mode()
-- set mode command
if self.mode_cmd == "IDLE" then self.mode_cmd = "DUMPING_EXCESS"
elseif self.mode_cmd == "DUMPING_EXCESS" then self.mode_cmd = "DUMPING"
elseif self.mode_cmd == "DUMPING" then self.mode_cmd = "IDLE"
end
-- write coil 1 with unused value 0
self.session.send_request(TXN_TYPES.INC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 })
if self.session.send_request(TXN_TYPES.INC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 1, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- decrement the dumping mode
local function _dec_dump_mode()
-- set mode command
if self.mode_cmd == "IDLE" then self.mode_cmd = "DUMPING"
elseif self.mode_cmd == "DUMPING_EXCESS" then self.mode_cmd = "IDLE"
elseif self.mode_cmd == "DUMPING" then self.mode_cmd = "DUMPING_EXCESS"
end
-- write coil 2 with unused value 0
self.session.send_request(TXN_TYPES.DEC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 })
if self.session.send_request(TXN_TYPES.DEC_DUMP, MODBUS_FCODE.WRITE_SINGLE_COIL, { 2, 0 }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- set the dumping mode
---@param mode dumping_mode
local function _set_dump_mode(mode)
self.mode_cmd = mode
-- write holding register 1
self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode })
if self.session.send_request(TXN_TYPES.SET_DUMP, MODBUS_FCODE.WRITE_SINGLE_HOLD_REG, { 1, mode }, WRITE_BUSY_WAIT) == false then
self.resend_mode = true
end
end
-- query if the multiblock is formed
local function _request_formed()
---@param time_now integer
local function _request_formed(time_now)
-- read discrete input 1 (start = 1, count = 1)
self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 })
if self.session.send_request(TXN_TYPES.FORMED, MODBUS_FCODE.READ_DISCRETE_INPUTS, { 1, 1 }) ~= false then
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
end
-- query the build of the device
local function _request_build()
---@param time_now integer
local function _request_build(time_now)
-- read input registers 1 through 15 (start = 1, count = 15)
self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 15 })
if self.session.send_request(TXN_TYPES.BUILD, MODBUS_FCODE.READ_INPUT_REGS, { 1, 15 }) ~= false then
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
end
-- query the state of the device
local function _request_state()
---@param time_now integer
local function _request_state(time_now)
-- read input registers 16 through 19 (start = 16, count = 4)
self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 16, 4 })
if self.session.send_request(TXN_TYPES.STATE, MODBUS_FCODE.READ_INPUT_REGS, { 16, 4 }) ~= false then
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
end
-- query the tanks of the device
local function _request_tanks()
---@param time_now integer
local function _request_tanks(time_now)
-- read input registers 20 through 25 (start = 20, count = 6)
self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 20, 6 })
if self.session.send_request(TXN_TYPES.TANKS, MODBUS_FCODE.READ_INPUT_REGS, { 20, 6 }) ~= false then
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
end
-- PUBLIC FUNCTIONS --
@@ -208,6 +244,10 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
self.db.state.prod_rate = m_pkt.data[2]
self.db.state.steam_input_rate = m_pkt.data[3]
self.db.state.dumping_mode = m_pkt.data[4]
if self.mode_cmd == nil then
self.mode_cmd = self.db.state.dumping_mode
end
else
log.debug(log_tag .. "MODBUS transaction reply length mismatch (" .. TXN_TAGS[txn_type] .. ")")
end
@@ -277,30 +317,22 @@ function turbinev.new(session_id, unit_id, advert, out_queue)
end
end
-- try to resend mode if needed
if self.resend_mode then
self.resend_mode = false
_set_dump_mode(self.mode_cmd)
end
time_now = util.time()
-- handle periodics
if self.periodics.next_formed_req <= time_now then
_request_formed()
self.periodics.next_formed_req = time_now + PERIODICS.FORMED
end
if self.periodics.next_formed_req <= time_now then _request_formed(time_now) end
if self.db.formed then
if not self.has_build and self.periodics.next_build_req <= time_now then
_request_build()
self.periodics.next_build_req = time_now + PERIODICS.BUILD
end
if self.periodics.next_state_req <= time_now then
_request_state()
self.periodics.next_state_req = time_now + PERIODICS.STATE
end
if self.periodics.next_tanks_req <= time_now then
_request_tanks()
self.periodics.next_tanks_req = time_now + PERIODICS.TANKS
end
if not self.has_build and self.periodics.next_build_req <= time_now then _request_build(time_now) end
if self.periodics.next_state_req <= time_now then _request_state(time_now) end
if self.periodics.next_tanks_req <= time_now then _request_tanks(time_now) end
end
self.session.post_update()

View File

@@ -22,6 +22,8 @@ local RTU_US_DATA = {
unit_session.RTU_US_CMDS = RTU_US_CMDS
unit_session.RTU_US_DATA = RTU_US_DATA
local DEFAULT_BUSY_WAIT = 3000
-- create a new unit session runner
---@nodiscard
---@param session_id integer RTU gateway session ID
@@ -36,7 +38,8 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
reactor = advert.reactor,
transaction_controller = txnctrl.new(),
connected = true,
device_fail = false
device_fail = false,
last_busy = 0
}
---@class _unit_session
@@ -53,14 +56,21 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
---@param txn_type integer transaction type
---@param f_code MODBUS_FCODE function code
---@param register_param (number|string)[] register range or register and values
---@return integer txn_id transaction ID of this transaction
function protected.send_request(txn_type, f_code, register_param)
local m_pkt = comms.modbus_packet()
local txn_id = self.transaction_controller.create(txn_type)
---@param busy_wait integer|nil milliseconds to wait (>0), or uses the default
---@return integer|false txn_id transaction ID of this transaction or false if not sent due to being busy
function protected.send_request(txn_type, f_code, register_param, busy_wait)
local txn_id = false ---@type integer|false
m_pkt.make(txn_id, unit_id, f_code, register_param)
busy_wait = busy_wait or DEFAULT_BUSY_WAIT
out_queue.push_packet(m_pkt)
if (util.time_ms() - self.last_busy) >= busy_wait then
local m_pkt = comms.modbus_packet()
txn_id = self.transaction_controller.create(txn_type)
m_pkt.make(txn_id, unit_id, f_code, register_param)
out_queue.push_packet(m_pkt)
end
return txn_id
end
@@ -99,9 +109,9 @@ function unit_session.new(session_id, unit_id, advert, out_queue, log_tag, txn_t
-- will have to wait on reply, renew the transaction
self.transaction_controller.renew(m_pkt.txn_id, txn_type)
elseif ex == MODBUS_EXCODE.SERVER_DEVICE_BUSY then
-- will have to wait on reply, renew the transaction
self.transaction_controller.renew(m_pkt.txn_id, txn_type)
log.debug(log_tag .. "MODBUS: device busy" .. txn_tag)
-- will have to try again later
self.last_busy = util.time_ms()
log.warning(log_tag .. "MODBUS: device busy" .. txn_tag)
elseif ex == MODBUS_EXCODE.NEG_ACKNOWLEDGE then
-- general failure
log.error(log_tag .. "MODBUS: negative acknowledge (bad request)" .. txn_tag)

View File

@@ -45,6 +45,7 @@ local self = {
fp_ok = false,
config = nil, ---@type svr_config
facility = nil, ---@type facility|nil
plc_ini_reset = {},
-- lists of connected sessions
---@diagnostic disable: missing-fields
sessions = {
@@ -391,6 +392,7 @@ function svsessions.init(nic, fp_ok, config, facility)
conns.tanks[1] = true
end
self.plc_ini_reset[i] = true
self.dev_dbg.connected.units[i] = conns
end
end
@@ -486,7 +488,7 @@ function svsessions.establish_plc_session(source_addr, i_seq_num, for_reactor, v
local id = self.next_ids.plc
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.fp_ok)
plc_s.instance = plc.new_session(id, source_addr, i_seq_num, for_reactor, plc_s.in_queue, plc_s.out_queue, self.config.PLC_Timeout, self.plc_ini_reset, self.fp_ok)
table.insert(self.sessions.plc, plc_s)
local units = self.facility.get_units()

View File

@@ -10,6 +10,7 @@ local log = require("scada-common.log")
local network = require("scada-common.network")
local ppm = require("scada-common.ppm")
local tcd = require("scada-common.tcd")
local types = require("scada-common.types")
local util = require("scada-common.util")
local core = require("graphics.core")
@@ -22,7 +23,7 @@ local supervisor = require("supervisor.supervisor")
local svsessions = require("supervisor.session.svsessions")
local SUPERVISOR_VERSION = "v1.6.1"
local SUPERVISOR_VERSION = "v1.6.8"
local println = util.println
local println_ts = util.println_ts
@@ -72,6 +73,21 @@ if config.FacilityTankMode > 0 then
cfv.assert_type_int(def)
cfv.assert_range(def, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank definition for reactor unit " .. i)
local entry = config.FacilityTankList[i]
cfv.assert_type_int(entry)
cfv.assert_range(entry, 0, 2)
assert(cfv.valid(), "startup> invalid facility tank list entry for tank " .. i)
local conn = config.FacilityTankConns[i]
cfv.assert_type_int(conn)
cfv.assert_range(conn, 0, #config.FacilityTankDefs)
assert(cfv.valid(), "startup> invalid facility tank connection for reactor unit " .. i)
local type = config.TankFluidTypes[i]
cfv.assert_type_int(type)
cfv.assert_range(type, 0, types.COOLANT_TYPE.SODIUM)
assert(cfv.valid(), "startup> invalid tank fluid type for tank " .. i)
end
end
@@ -147,6 +163,9 @@ local function main()
-- halve the rate heartbeat LED flash
local heartbeat_toggle = true
-- init startup recovery
sv_facility.boot_recovery_init(supervisor.boot_state)
-- event loop
while true do
local event, param1, param2, param3, param4, param5 = util.pull_event()
@@ -237,6 +256,8 @@ local function main()
end
end
sv_facility.clear_boot_state()
renderer.close_ui()
util.println_ts("exited")

View File

@@ -19,10 +19,24 @@ local config = {}
supervisor.config = config
-- load the supervisor configuration
-- control state from last unexpected shutdown
supervisor.boot_state = nil ---@type sv_boot_state|nil
-- load the supervisor configuration and startup state
function supervisor.load_config()
if not settings.load("/supervisor.settings") then return false end
---@class sv_boot_state
local boot_state = {
mode = settings.get("LastProcessState"), ---@type PROCESS
unit_states = settings.get("LastUnitStates") ---@type boolean[]
}
-- only record boot state if likely valid
if type(boot_state.mode) == "number" and type(boot_state.unit_states) == "table" then
supervisor.boot_state = boot_state
end
config.UnitCount = settings.get("UnitCount")
config.CoolingConfig = settings.get("CoolingConfig")
config.FacilityTankMode = settings.get("FacilityTankMode")
@@ -30,6 +44,7 @@ function supervisor.load_config()
config.FacilityTankList = settings.get("FacilityTankList")
config.FacilityTankConns = settings.get("FacilityTankConns")
config.TankFluidTypes = settings.get("TankFluidTypes")
config.AuxiliaryCoolant = settings.get("AuxiliaryCoolant")
config.ExtChargeIdling = settings.get("ExtChargeIdling")
config.SVR_Channel = settings.get("SVR_Channel")
@@ -64,6 +79,7 @@ function supervisor.load_config()
cfv.assert_type_table(config.FacilityTankList)
cfv.assert_type_table(config.FacilityTankConns)
cfv.assert_type_table(config.TankFluidTypes)
cfv.assert_type_table(config.AuxiliaryCoolant)
cfv.assert_range(config.FacilityTankMode, 0, 8)
cfv.assert_type_bool(config.ExtChargeIdling)
@@ -246,20 +262,32 @@ function supervisor.comms(_version, nic, fp_ok, facility)
-- PLC linking request
if packet.length == 4 and type(packet.data[4]) == "number" then
local reactor_id = packet.data[4]
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
-- check ID validity
if reactor_id < 1 or reactor_id > config.UnitCount then
-- reactor index out of range
if last_ack ~= ESTABLISH_ACK.DENY then
log.warning(util.c("PLC_ESTABLISH: denied assignment ", reactor_id, " outside of configured unit count ", config.UnitCount))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
_send_establish(packet.scada_frame, ESTABLISH_ACK.DENY)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
-- try to establish the session
local plc_id = svsessions.establish_plc_session(src_addr, i_seq_num, reactor_id, firmware_v)
if plc_id == false then
-- reactor already has a PLC assigned
if last_ack ~= ESTABLISH_ACK.COLLISION then
log.warning(util.c("PLC_ESTABLISH: assignment collision with reactor ", reactor_id))
end
_send_establish(packet.scada_frame, ESTABLISH_ACK.COLLISION)
else
-- got an ID; assigned to a reactor successfully
println(util.c("PLC (", firmware_v, ") [@", src_addr, "] \xbb reactor ", reactor_id, " connected"))
log.info(util.c("PLC_ESTABLISH: PLC (", firmware_v, ") [@", src_addr, "] reactor unit ", reactor_id, " PLC connected with session ID ", plc_id))
_send_establish(packet.scada_frame, ESTABLISH_ACK.ALLOW)
end
end
else
log.debug("PLC_ESTABLISH: packet length mismatch/bad parameter type")

View File

@@ -66,7 +66,8 @@ local unit = {}
---@param num_boilers integer number of boilers expected
---@param num_turbines integer number of turbines expected
---@param ext_idle boolean extended idling mode
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
---@param aux_coolant boolean if this unit has auxiliary coolant
function unit.new(reactor_id, num_boilers, num_turbines, ext_idle, aux_coolant)
-- time (ms) to idle for auto idling
local IDLE_TIME = util.trinary(ext_idle, 60000, 10000)
@@ -79,6 +80,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
plc_i = nil, ---@type plc_session
num_boilers = num_boilers,
num_turbines = num_turbines,
aux_coolant = aux_coolant,
types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },
-- rtus
rtu_list = {}, ---@type unit_session[][]
@@ -92,7 +94,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
io_ctl = nil, ---@type rs_controller
---@diagnostic disable-next-line: missing-fields
valves = {}, ---@type unit_valves
emcool_opened = false,
em_cool_opened = false,
aux_cool_opened = false,
-- auto control
auto_engaged = false,
auto_idle = false,
@@ -111,6 +114,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
damage_est_last = 0,
waste_product = WASTE.PLUTONIUM, ---@type WASTE_PRODUCT
status_text = { "UNKNOWN", "awaiting connection..." },
enable_aux_cool = false,
-- logic for alarms
had_reactor = false,
turbine_flow_stable = false,
@@ -373,6 +377,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
local waste_po = _make_valve_iface(IO.WASTE_POPL)
local waste_sps = _make_valve_iface(IO.WASTE_AM)
local emer_cool = _make_valve_iface(IO.U_EMER_COOL)
local aux_cool = _make_valve_iface(IO.U_AUX_COOL)
---@class unit_valves
self.valves = {
@@ -380,7 +385,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
waste_sna = waste_sna,
waste_po = waste_po,
waste_sps = waste_sps,
emer_cool = emer_cool
emer_cool = emer_cool,
aux_cool = aux_cool
}
-- route reactor waste for a given waste product
@@ -606,7 +612,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
if #self.redstone > 0 then
logic.handle_redstone(self)
elseif not self.plc_cache.rps_trip then
self.emcool_opened = false
self.em_cool_opened = false
end
end
@@ -724,7 +730,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- queue a command to clear timeout/auto-scram if set
function public.auto_cond_rps_reset()
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.emcool_opened) then
if self.plc_s ~= nil and self.plc_i ~= nil and (not self.auto_was_alarmed) and (not self.em_cool_opened) then
local rps = self.plc_i.get_rps()
if rps.timeout or rps.automatic then
self.plc_i.auto_lock(true) -- if it timed out/restarted, auto lock was lost, so re-lock it
@@ -840,6 +846,12 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
return false
end
-- check the active state of the reactor (if connected)
---@nodiscard
function public.is_reactor_enabled()
if self.plc_i ~= nil then return self.plc_i.get_status().status else return false end
end
-- check if the reactor is connected, is stopped, the RPS is not tripped, and no alarms are active
---@nodiscard
function public.is_safe_idle()
@@ -859,7 +871,7 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
-- check if emergency coolant activation has been tripped
---@nodiscard
function public.is_emer_cool_tripped() return self.emcool_opened end
function public.is_emer_cool_tripped() return self.em_cool_opened end
-- get build properties of machines
--
@@ -1053,7 +1065,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle)
v.waste_sna.check(),
v.waste_po.check(),
v.waste_sps.check(),
v.emer_cool.check()
v.emer_cool.check(),
v.aux_cool.check()
}
end

View File

@@ -35,6 +35,7 @@ local FLOW_STABILITY_DELAY_MS = const.FLOW_STABILITY_DELAY_MS
local ANNUNC_LIMS = const.ANNUNCIATOR_LIMITS
local ALARM_LIMS = const.ALARM_LIMITS
local RS_THRESH = const.RS_THRESHOLDS
---@class unit_logic_extension
local logic = {}
@@ -54,6 +55,10 @@ function logic.update_annunciator(self)
-- variables for boiler, or reactor if no boilers used
local total_boil_rate = 0.0
-- auxiliary coolant control
local need_aux_cool = false
local dis_aux_cool = true
--#region Reactor
annunc.AutoControl = self.auto_engaged
@@ -67,11 +72,10 @@ function logic.update_annunciator(self)
local plc_db = self.plc_i.get_db()
-- update ready state
-- - can't be tripped
-- - must have received status at least once
-- - must have received struct at least once
plc_ready = plc_db.formed and (not plc_db.no_reactor) and (not plc_db.rps_tripped) and
(next(self.plc_i.get_status()) ~= nil) and (next(self.plc_i.get_struct()) ~= nil)
-- - must be connected to a formed reactor
-- - can't have a tripped RPS
-- - must have received status, struct, and RPS status at least once
plc_ready = plc_db.formed and (not plc_db.no_reactor) and (not plc_db.rps_tripped) and self.plc_i.check_received_all_data()
-- update auto control limit
if (plc_db.mek_struct.max_burn > 0) and ((self.db.control.lim_br100 / 100) > plc_db.mek_struct.max_burn) then
@@ -149,6 +153,9 @@ function logic.update_annunciator(self)
-- if no boilers, use reactor heating rate to check for boil rate mismatch
if num_boilers == 0 then
total_boil_rate = plc_db.mek_status.heating_rate
need_aux_cool = plc_db.mek_status.ccool_fill <= RS_THRESH.AUX_COOL_ENABLE
dis_aux_cool = plc_db.mek_status.ccool_fill >= RS_THRESH.AUX_COOL_DISABLE
end
else
self.plc_cache.ok = false
@@ -216,6 +223,9 @@ function logic.update_annunciator(self)
annunc.BoilerOnline[idx] = true
annunc.WaterLevelLow[idx] = boiler.tanks.water_fill < ANNUNC_LIMS.WaterLevelLow
need_aux_cool = need_aux_cool or (boiler.tanks.water_fill <= RS_THRESH.AUX_COOL_ENABLE)
dis_aux_cool = dis_aux_cool and (boiler.tanks.water_fill >= RS_THRESH.AUX_COOL_DISABLE)
end
-- check heating rate low
@@ -342,11 +352,11 @@ function logic.update_annunciator(self)
end
if rotation_stable then
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached rotational stability (", rotation, ")"))
log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reached rotational stability (", rotation, ")"))
end
if flow_stable then
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)"))
log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reached flow stability (", turbine.state.flow_rate, " mB/t)"))
end
turbines_stable = turbines_stable and (rotation_stable or flow_stable)
@@ -358,7 +368,7 @@ function logic.update_annunciator(self)
turbines_stable = false
log.debug(util.c("UNIT ", self.r_id, ": turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)"))
log.debug(util.c("UNIT ", self.r_id, " turbine ", idx, " reset stability (new rate ", turbine.state.steam_input_rate, " != ", last.input_rate," mB/t)"))
end
last.input_rate = turbine.state.steam_input_rate
@@ -407,6 +417,12 @@ function logic.update_annunciator(self)
-- update auto control ready state for this unit
self.db.control.ready = plc_ready and boilers_ready and turbines_ready
-- update auxiliary coolant command
if plc_ready then
self.enable_aux_cool = self.plc_i.get_db().mek_status.status and
(self.enable_aux_cool or need_aux_cool) and not (dis_aux_cool and self.turbine_flow_stable)
else self.enable_aux_cool = false end
end
-- update an alarm state given conditions
@@ -729,7 +745,7 @@ function logic.update_status_text(self)
self.status_text = { "RCS TRANSIENT", "check coolant system" }
-- elseif is_active(self.alarms.RPSTransient) then
-- RPS status handled when checking reactor status
elseif self.emcool_opened then
elseif self.em_cool_opened then
self.status_text = { "EMERGENCY COOLANT OPENED", "reset RPS to close valve" }
-- connection dependent states
elseif self.plc_i ~= nil then
@@ -887,7 +903,7 @@ function logic.handle_redstone(self)
(annunc.CoolantLevelLow or (boiler_water_low and rps.ex_hcool)) and
is_active(self.alarms.ReactorOverTemp))
if enable_emer_cool and not self.emcool_opened then
if enable_emer_cool and not self.em_cool_opened then
log.debug(util.c(">> Emergency Coolant Enable Detail Report (Unit ", self.r_id, ") <<"))
log.debug(util.c("| CoolantLevelLow[", annunc.CoolantLevelLow, "] CoolantLevelLowLow[", rps.low_cool, "] ExcessHeatedCoolant[", rps.ex_hcool, "]"))
log.debug(util.c("| ReactorOverTemp[", AISTATE_NAMES[self.alarms.ReactorOverTemp.state], "]"))
@@ -911,13 +927,13 @@ function logic.handle_redstone(self)
end
end
if annunc.EmergencyCoolant > 1 and self.emcool_opened then
if annunc.EmergencyCoolant > 1 and self.em_cool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve closed"))
log.info(util.c("UNIT ", self.r_id, " turbines set to not dump steam"))
end
self.emcool_opened = false
elseif enable_emer_cool or self.emcool_opened then
self.em_cool_opened = false
elseif enable_emer_cool or self.em_cool_opened then
-- set turbines to dump excess steam
for i = 1, #self.turbines do
local session = self.turbines[i]
@@ -938,16 +954,33 @@ function logic.handle_redstone(self)
end
end
if annunc.EmergencyCoolant > 1 and not self.emcool_opened then
if annunc.EmergencyCoolant > 1 and not self.em_cool_opened then
log.info(util.c("UNIT ", self.r_id, " emergency coolant valve opened"))
log.info(util.c("UNIT ", self.r_id, " turbines set to dump excess steam"))
end
self.emcool_opened = true
self.em_cool_opened = true
end
-- set valve state always
if self.emcool_opened then self.valves.emer_cool.open() else self.valves.emer_cool.close() end
if self.em_cool_opened then self.valves.emer_cool.open() else self.valves.emer_cool.close() end
-----------------------
-- Auxiliary Coolant --
-----------------------
if self.aux_coolant then
if self.enable_aux_cool and (not self.aux_cool_opened) then
log.info(util.c("UNIT ", self.r_id, " auxiliary coolant valve opened"))
self.aux_cool_opened = true
elseif (not self.enable_aux_cool) and self.aux_cool_opened then
log.info(util.c("UNIT ", self.r_id, " auxiliary coolant valve closed"))
self.aux_cool_opened = false
end
-- set valve state always
if self.aux_cool_opened then self.valves.aux_cool.open() else self.valves.aux_cool.close() end
end
end
return logic