diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 9aa3dbc..bf73e1f 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -228,6 +228,8 @@ function iocontrol.init(conf, comms, temp_scale) ---@class ioctl_unit local entry = { unit_id = i, + connected = false, + rtu_hw = { boilers = {}, turbines = {} }, num_boilers = 0, num_turbines = 0, @@ -319,12 +321,14 @@ function iocontrol.init(conf, comms, temp_scale) for _ = 1, conf.cooling.r_cool[i].BoilerCount do table.insert(entry.boiler_ps_tbl, psil.create()) table.insert(entry.boiler_data_tbl, {}) + table.insert(entry.rtu_hw.boilers, { connected = false, faulted = false }) end -- create turbine tables for _ = 1, conf.cooling.r_cool[i].TurbineCount do table.insert(entry.turbine_ps_tbl, psil.create()) table.insert(entry.turbine_data_tbl, {}) + table.insert(entry.rtu_hw.turbines, { connected = false, faulted = false }) end -- create tank tables @@ -897,6 +901,7 @@ function iocontrol.update_unit_statuses(statuses) end if #reactor_status == 0 then + unit.connected = false unit.unit_ps.publish("computed_status", 1) -- disconnected elseif #reactor_status == 3 then local mek_status = reactor_status[1] @@ -956,6 +961,8 @@ function iocontrol.update_unit_statuses(statuses) unit.unit_ps.publish(key, val) end end + + unit.connected = true else log.debug(log_header .. "reactor status length mismatch") valid = false @@ -970,7 +977,10 @@ function iocontrol.update_unit_statuses(statuses) local boil_sum = 0 for id = 1, #unit.boiler_ps_tbl do - if rtu_statuses.boilers[id] == nil then + local connected = rtu_statuses.boilers[id] ~= nil + unit.rtu_hw.boilers[id].connected = connected + + if not connected then -- disconnected unit.boiler_ps_tbl[id].publish("computed_status", 1) end @@ -982,6 +992,7 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.boiler_ps_tbl[id] ---@type psil local rtu_faulted = _record_multiblock_status(boiler, data, ps) + unit.rtu_hw.boilers[id].faulted = rtu_faulted if rtu_faulted then ps.publish("computed_status", 3) -- faulted @@ -1013,7 +1024,10 @@ function iocontrol.update_unit_statuses(statuses) local flow_sum = 0 for id = 1, #unit.turbine_ps_tbl do - if rtu_statuses.turbines[id] == nil then + local connected = rtu_statuses.turbines[id] ~= nil + unit.rtu_hw.turbines[id].connected = connected + + if not connected then -- disconnected unit.turbine_ps_tbl[id].publish("computed_status", 1) end @@ -1025,6 +1039,7 @@ function iocontrol.update_unit_statuses(statuses) local ps = unit.turbine_ps_tbl[id] ---@type psil local rtu_faulted = _record_multiblock_status(turbine, data, ps) + unit.rtu_hw.turbines[id].faulted = rtu_faulted if rtu_faulted then ps.publish("computed_status", 3) -- faulted diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index 7101297..09ec15c 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -138,21 +138,26 @@ function pocket.new_session(id, s_addr, in_queue, out_queue, timeout) } _send(CRDN_TYPE.API_GET_FAC, data) - elseif pkt.type == CRDN_TYPE.API_GET_UNITS then - local data = {} + elseif pkt.type == CRDN_TYPE.API_GET_UNIT then + if pkt.length == 1 and type(pkt.data[1]) == "number" then + local u = db.units[pkt.data[1]] ---@type ioctl_unit - for i = 1, #db.units do - local u = db.units[i] ---@type ioctl_unit - table.insert(data, { - u.unit_id, - u.num_boilers, - u.num_turbines, - u.num_snas, - u.has_tank - }) + if u then + local data = { + u.unit_id, + u.connected, + u.rtu_hw, + u.alarms, + u.annunciator, + u.reactor_data, + u.boiler_data_tbl, + u.turbine_data_tbl, + u.tank_data_tbl + } + + _send(CRDN_TYPE.API_GET_UNIT, data) + end end - - _send(CRDN_TYPE.API_GET_UNITS, data) else log.debug(log_header .. "handler received unsupported CRDN packet type " .. pkt.type) end diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index 403dde7..c171e9f 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -87,7 +87,6 @@ function iocontrol.alloc_nav() local app = { loaded = false, load = nil, - root = { _p = nil, _c = {}, nav_to = function () end, tasks = {} }, ---@type nav_tree_page cur_page = nil, ---@type nav_tree_page pane = pane, paned_pages = {}, @@ -132,9 +131,8 @@ function iocontrol.alloc_nav() ---@type nav_tree_page local page = { _p = parent, _c = {}, nav_to = function () end, switcher = function () end, tasks = {} } - if parent == nil then - app.root = page - if app.cur_page == nil then app.cur_page = page end + if parent == nil and app.cur_page == nil then + app.cur_page = page end if type(nav_to) == "number" then @@ -259,6 +257,12 @@ function iocontrol.init_core(comms) alarm_buttons = {}, tone_indicators = {} -- indicators to update from supervisor tone states } + + -- API access + ---@class pocket_ioctl_api + io.api = { + get_unit = function (unit) comms.api__get_unit(unit) end + } end -- initialize facility-dependent components of pocket iocontrol @@ -431,6 +435,8 @@ function iocontrol.init_fac(conf, temp_scale) ---@class pioctl_unit local entry = { unit_id = i, + connected = false, + rtu_hw = {}, num_boilers = 0, num_turbines = 0, @@ -579,6 +585,116 @@ function iocontrol.record_facility_data(data) return valid end +-- update unit status data from API_GET_UNIT +---@param data table +function iocontrol.record_unit_data(data) + if type(data[1]) == "number" and io.units[data[1]] then + local unit = io.units[data[1]] ---@type pioctl_unit + + unit.connected = data[2] + unit.rtu_hw = data[3] + unit.alarms = data[4] + unit.annunciator = data[5] + + unit.reactor_data = data[6] + + local control_status = 1 + local reactor_status = 1 + local rps_status = 1 + + if not unit.connected then + -- disconnected + reactor_status = 1 + else + -- update RPS status + if unit.reactor_data.rps_tripped then + control_status = 2 + rps_status = util.trinary(unit.reactor_data.rps_trip_cause ~= "manual", 3, 2) + else rps_status = 4 end + + -- update reactor/control status + if unit.reactor_data.mek_status.status then + reactor_status = 4 + control_status = util.trinary(unit.annunciator.AutoControl, 4, 3) + else + if unit.reactor_data.no_reactor then + reactor_status = 2 + elseif not unit.reactor_data.formed or unit.reactor_data.rps_status.force_dis then + reactor_status = 3 + else + reactor_status = 4 + end + end + + for key, val in pairs(unit.reactor_data) do + if key ~= "rps_status" and key ~= "mek_struct" and key ~= "mek_status" then + unit.unit_ps.publish(key, val) + end + end + + if type(unit.reactor_data.rps_status) == "table" then + for key, val in pairs(unit.reactor_data.rps_status) do + unit.unit_ps.publish(key, val) + end + end + + if type(unit.reactor_data.mek_status) == "table" then + for key, val in pairs(unit.reactor_data.mek_status) do + unit.unit_ps.publish(key, val) + end + end + end + + unit.unit_ps.publish("U_ControlStatus", control_status) + unit.unit_ps.publish("U_ReactorStatus", reactor_status) + unit.unit_ps.publish("U_RPS", rps_status) + + unit.boiler_data_tbl = data[7] + + for id = 1, #unit.boiler_data_tbl do + local boiler = unit.boiler_data_tbl[id] ---@type boilerv_session_db + local ps = unit.boiler_ps_tbl[id] ---@type psil + + local boiler_status = 1 + + if unit.rtu_hw.boilers[id].connected then + if unit.rtu_hw.boilers[id].faulted then + boiler_status = 3 + elseif boiler.formed then + boiler_status = 4 + else + boiler_status = 2 + end + end + + ps.publish("BoilerStatus", boiler_status) + end + + unit.turbine_data_tbl = data[8] + + for id = 1, #unit.turbine_data_tbl do + local turbine = unit.turbine_data_tbl[id] ---@type turbinev_session_db + local ps = unit.turbine_ps_tbl[id] ---@type psil + + local turbine_status = 1 + + if unit.rtu_hw.turbines[id].connected then + if unit.rtu_hw.turbines[id].faulted then + turbine_status = 3 + elseif turbine.formed then + turbine_status = 4 + else + turbine_status = 2 + end + end + + ps.publish("TurbineStatus", turbine_status) + end + + unit.tank_data_tbl = data[9] + end +end + -- get the IO controller database function iocontrol.get_db() return io end diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 7499fd5..0482386 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -119,6 +119,20 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) self.api.seq_num = self.api.seq_num + 1 end + -- send an API packet to the coordinator + ---@param msg_type CRDN_TYPE + ---@param msg table + local function _send_api(msg_type, msg) + local s_pkt = comms.scada_packet() + local pkt = comms.crdn_packet() + + pkt.make(msg_type, msg) + s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_CRDN, pkt.raw_sendable()) + + nic.transmit(config.CRD_Channel, config.PKT_Channel, s_pkt) + self.api.seq_num = self.api.seq_num + 1 + end + -- attempt supervisor connection establishment local function _send_sv_establish() _send_sv(MGMT_TYPE.ESTABLISH, { comms.version, version, DEVICE_TYPE.PKT }) @@ -215,6 +229,11 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if self.sv.linked then _send_sv(MGMT_TYPE.DIAG_ALARM_SET, { id, state }) end end + -- coordinator get unit data + function public.api__get_unit(unit) + if self.api.linked then _send_api(CRDN_TYPE.API_GET_UNIT, { unit }) end + end + -- parse a packet ---@param side string ---@param sender integer @@ -304,7 +323,10 @@ function pocket.comms(version, nic, sv_watchdog, api_watchdog) if _check_length(packet, 11) then iocontrol.record_facility_data(packet.data) end - elseif packet.type == CRDN_TYPE.API_GET_UNITS then + elseif packet.type == CRDN_TYPE.API_GET_UNIT then + if _check_length(packet, 9) then + iocontrol.record_unit_data(packet.data) + end else _fail_type(packet) end else log.debug("discarding coordinator SCADA_CRDN packet before linked") diff --git a/pocket/ui/pages/unit_page.lua b/pocket/ui/pages/unit_page.lua index 53dcce8..f17e16d 100644 --- a/pocket/ui/pages/unit_page.lua +++ b/pocket/ui/pages/unit_page.lua @@ -2,13 +2,15 @@ -- Unit Overview Page -- -local iocontrol = require("pocket.iocontrol") local util = require("scada-common.util") +local log = require("scada-common.log") + +local iocontrol = require("pocket.iocontrol") local core = require("graphics.core") local Div = require("graphics.elements.div") -local MultiPane = require("graphics.elements.multipane") +local MultiPane = require("graphics.elements.multipane") local TextBox = require("graphics.elements.textbox") local AlarmLight = require("graphics.elements.indicators.alight") @@ -29,6 +31,20 @@ local SpinboxNumeric = require("graphics.elements.controls.spinbox_numeric") local ALIGN = core.ALIGN local cpair = core.cpair +local basic_states = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.red), symbol = "-" }, + { color = cpair(colors.black, colors.yellow), symbol = "\x1e" }, + { color = cpair(colors.black, colors.green), symbol = "+" } +} + +local mode_states = { + { color = cpair(colors.black, colors.lightGray), symbol = "\x07" }, + { color = cpair(colors.black, colors.red), symbol = "-" }, + { color = cpair(colors.black, colors.green), symbol = "+" }, + { color = cpair(colors.black, colors.purple), symbol = "A" } +} + -- new unit page view ---@param root graphics_element parent local function new_view(root) @@ -37,29 +53,41 @@ local function new_view(root) local main = Div{parent=root,x=1,y=1} local app = db.nav.register_app(iocontrol.APP_ID.UNITS, main) - app.new_page(nil, function () end) TextBox{parent=main,y=2,text="Units App",height=1,alignment=ALIGN.CENTER} TextBox{parent=main,y=4,text="Loading...",height=1,alignment=ALIGN.CENTER} - local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2} - local btn_fg_bg = cpair(colors.yellow, colors.black) local btn_active = cpair(colors.white, colors.black) local label = cpair(colors.lightGray, colors.black) - local function set_sidebar(unit) - app.set_sidebar({ + local function set_sidebar(id) + local unit = db.units[id] ---@type pioctl_unit + + local list = { { label = " # ", tall = true, color = core.cpair(colors.black, colors.green), callback = function () db.nav.open_app(iocontrol.APP_ID.ROOT) end }, - { label = "U#" .. unit, color = core.cpair(colors.black, colors.yellow), callback = function () end }, - { label = "RPS", color = core.cpair(colors.black, colors.red), callback = function () end }, - { label = "RCS", color = core.cpair(colors.black, colors.blue), callback = function () end }, - { label = " R ", tall = true, color = core.cpair(colors.black, colors.orange), callback = function () end }, - }) + { label = "U-" .. id, color = core.cpair(colors.black, colors.yellow) }, + { label = " \x13 ", color = core.cpair(colors.black, colors.red), callback = function () end }, + { label = " R ", tall = true, color = core.cpair(colors.black, colors.lightGray), callback = function () end }, + { label = "RPS", color = core.cpair(colors.black, colors.cyan), callback = function () end }, + { label = "RCS", tall = true, color = core.cpair(colors.black, colors.blue), callback = function () end }, + } + + for i = 1, unit.num_boilers do + table.insert(list, { label = "B-" .. i, color = core.cpair(colors.black, colors.lightBlue), callback = function () end }) + end + + for i = 1, unit.num_turbines do + table.insert(list, { label = "T-" .. i, color = core.cpair(colors.black, colors.white), callback = function () end }) + end + + app.set_sidebar(list) end local function load() + local page_div = Div{parent=main,x=2,y=2,width=main.get_width()-2} + local u_pages = {} local active_unit = 1 @@ -71,16 +99,17 @@ local function new_view(root) end local u_pane = MultiPane{parent=page_div,x=1,y=1,panes=u_pages} + app.set_root_pane(u_pane) local function prev(x) active_unit = util.trinary(x == 1, db.facility.num_units, x - 1) - u_pane.set_value(active_unit) + app.switcher(active_unit) set_sidebar(active_unit) end local function next(x) active_unit = util.trinary(x == db.facility.num_units, 1, x + 1) - u_pane.set_value(active_unit) + app.switcher(active_unit) set_sidebar(active_unit) end @@ -88,6 +117,16 @@ local function new_view(root) local u_div = u_pages[i] ---@type graphics_element local unit = db.units[i] ---@type pioctl_unit + local last_update = 0 + local function update() + if util.time_ms() - last_update >= 500 then + db.api.get_unit(i) + last_update = util.time_ms() + end + end + + app.new_page(nil, i).tasks = { update } + TextBox{parent=u_div,y=1,text="Reactor Unit #"..i,height=1,alignment=ALIGN.CENTER} PushButton{parent=u_div,x=1,y=1,text="<",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()prev(i)end} PushButton{parent=u_div,x=21,y=1,text=">",fg_bg=btn_fg_bg,active_fg_bg=btn_active,callback=function()next(i)end} @@ -101,46 +140,33 @@ local function new_view(root) local rate = DataIndicator{parent=u_div,y=5,lu_colors=lu_col,label="Rate",unit="mB/t",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg} local temp = DataIndicator{parent=u_div,lu_colors=lu_col,label="Temp",unit="K",format="%10.2f",value=0,commas=true,width=26,fg_bg=text_fg} - local basic_states = { - { color = cpair(colors.black,colors.lightGray), symbol = "\x07" }, - { color = cpair(colors.black,colors.red), symbol = "-" }, - { color = cpair(colors.black,colors.yellow), symbol = "\x1e" }, - { color = cpair(colors.black,colors.green), symbol = "+" } - } - - local mode_states = { - { color = cpair(colors.black,colors.lightGray), symbol = "\x07" }, - { color = cpair(colors.black,colors.red), symbol = "-" }, - { color = cpair(colors.black,colors.green), symbol = "+" }, - { color = cpair(colors.black,colors.purple), symbol = "A" } - } - local ctrl = IconIndicator{parent=u_div,x=1,y=8,label="Control State",states=mode_states} - ctrl.update(i+1) + rate.register(unit.unit_ps, "act_burn_rate", rate.update) + temp.register(unit.unit_ps, "temp", temp.update) + ctrl.register(unit.unit_ps, "U_ControlStatus", ctrl.update) u_div.line_break() local rct = IconIndicator{parent=u_div,x=1,label="Fission Reactor",states=basic_states} local rps = IconIndicator{parent=u_div,x=1,label="Protection System",states=basic_states} + rct.register(unit.unit_ps, "U_ReactorStatus", rct.update) + rps.register(unit.unit_ps, "U_RPS", rps.update) + u_div.line_break() local rcs = IconIndicator{parent=u_div,x=1,label="Coolant System",states=basic_states} for b = 1, unit.num_boilers do local blr = IconIndicator{parent=u_div,x=1,label="Boiler "..b,states=basic_states} - blr.update(b+2) + blr.register(unit.boiler_ps_tbl[b], "BoilerStatus", blr.update) end for t = 1, unit.num_turbines do - local trb = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states} - trb.update(t) + local tbn = IconIndicator{parent=u_div,x=1,label="Turbine "..t,states=basic_states} + tbn.register(unit.turbine_ps_tbl[t], "TurbineStatus", tbn.update) end - - rct.update(4) - rps.update(3) - rcs.update(3) end end diff --git a/scada-common/comms.lua b/scada-common/comms.lua index e107d33..5e364e8 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -18,7 +18,7 @@ local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) comms.version = "2.5.1" -comms.api_version = "0.0.1" +comms.api_version = "0.0.2" ---@enum PROTOCOL local PROTOCOL = { @@ -67,7 +67,7 @@ local CRDN_TYPE = { UNIT_STATUSES = 5, -- state of each of the reactor units UNIT_CMD = 6, -- command a reactor unit API_GET_FAC = 7, -- API: get all the facility data - API_GET_UNITS = 8 -- API: get all the reactor unit data + API_GET_UNIT = 8 -- API: get reactor unit data } ---@enum ESTABLISH_ACK