diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 07479e0b..95a3e6f2 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -495,6 +495,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 +659,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) diff --git a/coordinator/session/pocket.lua b/coordinator/session/pocket.lua index c8b77de0..a12870a3 100644 --- a/coordinator/session/pocket.lua +++ b/coordinator/session/pocket.lua @@ -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]] diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 6fb924b6..d3413e33 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -19,7 +19,7 @@ local renderer = require("coordinator.renderer") local sounder = require("coordinator.sounder") local threads = require("coordinator.threads") -local COORDINATOR_VERSION = "v1.6.2" +local COORDINATOR_VERSION = "v1.6.4" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 186f7bda..c6fd21db 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -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 diff --git a/coordinator/ui/layout/main_view.lua b/coordinator/ui/layout/main_view.lua index ebc9cc35..b1369d14 100644 --- a/coordinator/ui/layout/main_view.lua +++ b/coordinator/ui/layout/main_view.lua @@ -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 diff --git a/pocket/iocontrol.lua b/pocket/iocontrol.lua index e606b07f..53783292 100644 --- a/pocket/iocontrol.lua +++ b/pocket/iocontrol.lua @@ -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 diff --git a/pocket/iorx.lua b/pocket/iorx.lua index 0aac073a..76fcd9bb 100644 --- a/pocket/iorx.lua +++ b/pocket/iorx.lua @@ -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 diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 8233c786..7bfe6691 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -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 diff --git a/pocket/startup.lua b/pocket/startup.lua index b4e1a9be..636c6ef6 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -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.0-beta" local println = util.println local println_ts = util.println_ts diff --git a/pocket/ui/apps/control.lua b/pocket/ui/apps/control.lua index 57cabc87..6732c93e 100644 --- a/pocket/ui/apps/control.lua +++ b/pocket/ui/apps/control.lua @@ -1,5 +1,5 @@ -- --- Unit Control Page +-- Facility & Unit Control App -- local types = require("scada-common.types") diff --git a/pocket/ui/apps/facility.lua b/pocket/ui/apps/facility.lua new file mode 100644 index 00000000..65b25e33 --- /dev/null +++ b/pocket/ui/apps/facility.lua @@ -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 diff --git a/pocket/ui/apps/process.lua b/pocket/ui/apps/process.lua index 593e74c6..6f04d60a 100644 --- a/pocket/ui/apps/process.lua +++ b/pocket/ui/apps/process.lua @@ -1,5 +1,5 @@ -- --- Process Control Page +-- Process Control App -- local types = require("scada-common.types") diff --git a/pocket/ui/apps/unit.lua b/pocket/ui/apps/unit.lua index 2f916d31..304608ee 100644 --- a/pocket/ui/apps/unit.lua +++ b/pocket/ui/apps/unit.lua @@ -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 diff --git a/pocket/ui/apps/waste.lua b/pocket/ui/apps/waste.lua index 24e62aee..8037d824 100644 --- a/pocket/ui/apps/waste.lua +++ b/pocket/ui/apps/waste.lua @@ -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 diff --git a/pocket/ui/main.lua b/pocket/ui/main.lua index 99b6ab3a..588cf0b1 100644 --- a/pocket/ui/main.lua +++ b/pocket/ui/main.lua @@ -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) diff --git a/pocket/ui/pages/dynamic_tank.lua b/pocket/ui/pages/dynamic_tank.lua index 356f7e61..17221a62 100644 --- a/pocket/ui/pages/dynamic_tank.lua +++ b/pocket/ui/pages/dynamic_tank.lua @@ -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)) diff --git a/pocket/ui/pages/facility_matrix.lua b/pocket/ui/pages/facility_matrix.lua new file mode 100644 index 00000000..77640b11 --- /dev/null +++ b/pocket/ui/pages/facility_matrix.lua @@ -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 diff --git a/pocket/ui/pages/facility_sps.lua b/pocket/ui/pages/facility_sps.lua new file mode 100644 index 00000000..1de23ca0 --- /dev/null +++ b/pocket/ui/pages/facility_sps.lua @@ -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 diff --git a/pocket/ui/pages/home_page.lua b/pocket/ui/pages/home_page.lua index 345c9ce3..ada6e927 100644 --- a/pocket/ui/pages/home_page.lua +++ b/pocket/ui/pages/home_page.lua @@ -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} diff --git a/rtu/rtu.lua b/rtu/rtu.lua index 06178ad0..2fce5418 100644 --- a/rtu/rtu.lua +++ b/rtu/rtu.lua @@ -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 diff --git a/rtu/startup.lua b/rtu/startup.lua index f43996c6..c957ac9b 100644 --- a/rtu/startup.lua +++ b/rtu/startup.lua @@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu") local sps_rtu = require("rtu.dev.sps_rtu") local turbinev_rtu = require("rtu.dev.turbinev_rtu") -local RTU_VERSION = "v1.10.21" +local RTU_VERSION = "v1.11.0" local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE local RTU_HW_STATE = databus.RTU_HW_STATE diff --git a/scada-common/comms.lua b/scada-common/comms.lua index a3e40a81..a8d0014c 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -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.4" +comms.api_version = "0.0.9" ---@enum PROTOCOL local PROTOCOL = { @@ -66,11 +66,12 @@ local CRDN_TYPE = { 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 + API_GET_FAC = 7, -- API: get the facility general data + API_GET_FAC_DTL = 8, -- API: get (detailed) data for the facility app + API_GET_UNIT = 9, -- API: get reactor unit data + API_GET_CTRL = 10, -- API: get data for the control app + API_GET_PROC = 11, -- API: get data for the process app + API_GET_WASTE = 12 -- API: get data for the waste app } ---@enum ESTABLISH_ACK diff --git a/supervisor/session/rtu/boilerv.lua b/supervisor/session/rtu/boilerv.lua index a1a1a999..11b5e39b 100644 --- a/supervisor/session/rtu/boilerv.lua +++ b/supervisor/session/rtu/boilerv.lua @@ -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() diff --git a/supervisor/session/rtu/dynamicv.lua b/supervisor/session/rtu/dynamicv.lua index 0c06d7ba..16ba84ba 100644 --- a/supervisor/session/rtu/dynamicv.lua +++ b/supervisor/session/rtu/dynamicv.lua @@ -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() diff --git a/supervisor/session/rtu/envd.lua b/supervisor/session/rtu/envd.lua index 269975a7..ef36fadb 100644 --- a/supervisor/session/rtu/envd.lua +++ b/supervisor/session/rtu/envd.lua @@ -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 diff --git a/supervisor/session/rtu/imatrix.lua b/supervisor/session/rtu/imatrix.lua index aa7a9845..5fea7aa6 100644 --- a/supervisor/session/rtu/imatrix.lua +++ b/supervisor/session/rtu/imatrix.lua @@ -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() diff --git a/supervisor/session/rtu/sna.lua b/supervisor/session/rtu/sna.lua index 7e0fc340..f46fa5c9 100644 --- a/supervisor/session/rtu/sna.lua +++ b/supervisor/session/rtu/sna.lua @@ -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 diff --git a/supervisor/session/rtu/sps.lua b/supervisor/session/rtu/sps.lua index 1dacd61e..20968210 100644 --- a/supervisor/session/rtu/sps.lua +++ b/supervisor/session/rtu/sps.lua @@ -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() diff --git a/supervisor/session/rtu/turbinev.lua b/supervisor/session/rtu/turbinev.lua index 3581884b..fa9fb6ad 100644 --- a/supervisor/session/rtu/turbinev.lua +++ b/supervisor/session/rtu/turbinev.lua @@ -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() diff --git a/supervisor/session/rtu/unit_session.lua b/supervisor/session/rtu/unit_session.lua index 632890b1..8c51ccb5 100644 --- a/supervisor/session/rtu/unit_session.lua +++ b/supervisor/session/rtu/unit_session.lua @@ -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 + + busy_wait = busy_wait or DEFAULT_BUSY_WAIT + + 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) + m_pkt.make(txn_id, unit_id, f_code, register_param) - out_queue.push_packet(m_pkt) + 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) diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 870792e5..32651da3 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -22,7 +22,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.6.1" +local SUPERVISOR_VERSION = "v1.6.2" local println = util.println local println_ts = util.println_ts