diff --git a/coordinator/iocontrol.lua b/coordinator/iocontrol.lua index 1de337f..7fc2707 100644 --- a/coordinator/iocontrol.lua +++ b/coordinator/iocontrol.lua @@ -663,10 +663,26 @@ function iocontrol.update_facility_status(status) fac.rtu_count = rtu_statuses.count -- power statistics - if type(rtu_statuses.power) == "table" then - fac.induction_ps_tbl[1].publish("avg_charge", rtu_statuses.power[1]) - fac.induction_ps_tbl[1].publish("avg_inflow", rtu_statuses.power[2]) - fac.induction_ps_tbl[1].publish("avg_outflow", rtu_statuses.power[3]) + if type(rtu_statuses.power) == "table" and #rtu_statuses.power == 4 then + local data = fac.induction_data_tbl[1] ---@type imatrix_session_db + local ps = fac.induction_ps_tbl[1] ---@type psil + + local chg = tonumber(rtu_statuses.power[1]) + local in_f = tonumber(rtu_statuses.power[2]) + local out_f = tonumber(rtu_statuses.power[3]) + local eta = tonumber(rtu_statuses.power[4]) + + ps.publish("avg_charge", chg) + ps.publish("avg_inflow", in_f) + ps.publish("avg_outflow", out_f) + ps.publish("eta_ms", eta) + + ps.publish("is_charging", in_f > out_f) + ps.publish("is_discharging", out_f > in_f) + + if data and data.build then + ps.publish("at_max_io", in_f >= data.build.transfer_cap or out_f >= data.build.transfer_cap) + end else log.debug(log_header .. "power statistics list not a table") valid = false diff --git a/coordinator/startup.lua b/coordinator/startup.lua index a50da5b..b3df178 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.4.3" +local COORDINATOR_VERSION = "v1.4.4" local CHUNK_LOAD_DELAY_S = 30.0 diff --git a/coordinator/ui/components/imatrix.lua b/coordinator/ui/components/imatrix.lua index 2b80350..60a745a 100644 --- a/coordinator/ui/components/imatrix.lua +++ b/coordinator/ui/components/imatrix.lua @@ -9,6 +9,7 @@ local Rectangle = require("graphics.elements.rectangle") local TextBox = require("graphics.elements.textbox") local DataIndicator = require("graphics.elements.indicators.data") +local IndicatorLight = require("graphics.elements.indicators.light") local PowerIndicator = require("graphics.elements.indicators.power") local StateIndicator = require("graphics.elements.indicators.state") local VerticalBar = require("graphics.elements.indicators.vbar") @@ -26,9 +27,13 @@ local ALIGN = core.ALIGN ---@param ps psil ps interface ---@param id number? matrix ID local function new_view(root, x, y, data, ps, id) + local label_fg = style.theme.label_fg local text_fg = style.theme.text_fg local lu_col = style.lu_colors + local ind_yel = style.ind_yel + local ind_wht = style.ind_wht + local title = "INDUCTION MATRIX" if type(id) == "number" then title = title .. id end @@ -42,45 +47,47 @@ local function new_view(root, x, y, data, ps, id) local rect = Rectangle{parent=matrix,border=border(1,colors.gray,true),width=33,height=22,x=1,y=3} - local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} - local energy = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local capacity = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local input = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - local output = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - - local avg_chg = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Avg. Chg:",format="%8.2f",value=0,width=26,fg_bg=text_fg} - local avg_in = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="Avg. In: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} - local avg_out = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Avg. Out:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local status = StateIndicator{parent=rect,x=10,y=1,states=style.imatrix.states,value=1,min_width=14} + local capacity = PowerIndicator{parent=rect,x=7,y=3,lu_colors=lu_col,label="Capacity:",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local energy = PowerIndicator{parent=rect,x=7,y=4,lu_colors=lu_col,label="Energy: ",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local avg_chg = PowerIndicator{parent=rect,x=7,y=5,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",value=0,width=26,fg_bg=text_fg} + local input = PowerIndicator{parent=rect,x=7,y=6,lu_colors=lu_col,label="Input: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local avg_in = PowerIndicator{parent=rect,x=7,y=7,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local output = PowerIndicator{parent=rect,x=7,y=8,lu_colors=lu_col,label="Output: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local avg_out = PowerIndicator{parent=rect,x=7,y=9,lu_colors=lu_col,label="\xb7Average:",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} + local trans_cap = PowerIndicator{parent=rect,x=7,y=10,lu_colors=lu_col,label="Max I/O: ",format="%8.2f",rate=true,value=0,width=26,fg_bg=text_fg} status.register(ps, "computed_status", status.update) - energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) capacity.register(ps, "max_energy", function (val) capacity.update(util.joules_to_fe(val)) end) - input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end) - output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end) - + energy.register(ps, "energy", function (val) energy.update(util.joules_to_fe(val)) end) avg_chg.register(ps, "avg_charge", avg_chg.update) + input.register(ps, "last_input", function (val) input.update(util.joules_to_fe(val)) end) avg_in.register(ps, "avg_inflow", avg_in.update) + output.register(ps, "last_output", function (val) output.update(util.joules_to_fe(val)) end) avg_out.register(ps, "avg_outflow", avg_out.update) + trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) - local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill:",unit="%",format="%8.2f",value=0,width=18,fg_bg=text_fg} - - local cells = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg} - local providers = DataIndicator{parent=rect,x=11,y=15,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg} - - TextBox{parent=rect,text="Transfer Capacity",x=11,y=17,height=1,width=17,fg_bg=style.theme.label_fg} - local trans_cap = PowerIndicator{parent=rect,x=19,y=18,lu_colors=lu_col,label="",format="%5.2f",rate=true,value=0,width=12,fg_bg=text_fg} + local fill = DataIndicator{parent=rect,x=11,y=12,lu_colors=lu_col,label="Fill: ",format="%7.2f",unit="%",value=0,width=20,fg_bg=text_fg} + local cells = DataIndicator{parent=rect,x=11,y=13,lu_colors=lu_col,label="Cells: ",format="%7d",value=0,width=18,fg_bg=text_fg} + local providers = DataIndicator{parent=rect,x=11,y=14,lu_colors=lu_col,label="Providers:",format="%7d",value=0,width=18,fg_bg=text_fg} + fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end) cells.register(ps, "cells", cells.update) providers.register(ps, "providers", providers.update) - fill.register(ps, "energy_fill", function (val) fill.update(val * 100) end) - trans_cap.register(ps, "transfer_cap", function (val) trans_cap.update(util.joules_to_fe(val)) end) + + local chging = IndicatorLight{parent=rect,x=11,y=16,label="Charging",colors=ind_wht} + local dischg = IndicatorLight{parent=rect,x=11,y=17,label="Discharging",colors=ind_wht} + local max_io = IndicatorLight{parent=rect,x=11,y=18,label="Max I/O Rate",colors=ind_yel} + + chging.register(ps, "is_charging", chging.update) + dischg.register(ps, "is_discharging", dischg.update) + max_io.register(ps, "at_max_io", max_io.update) local charge = VerticalBar{parent=rect,x=2,y=2,fg_bg=cpair(colors.green,colors.gray),height=17,width=4} local in_cap = VerticalBar{parent=rect,x=7,y=12,fg_bg=cpair(colors.red,colors.gray),height=7,width=1} local out_cap = VerticalBar{parent=rect,x=9,y=12,fg_bg=cpair(colors.blue,colors.gray),height=7,width=1} - TextBox{parent=rect,text="FILL",x=2,y=20,height=1,width=4,fg_bg=text_fg} - TextBox{parent=rect,text="I/O",x=7,y=20,height=1,width=3,fg_bg=text_fg} + TextBox{parent=rect,text="FILL I/O",x=2,y=20,height=1,width=8,fg_bg=label_fg} local function calc_saturation(val) if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then @@ -91,6 +98,49 @@ local function new_view(root, x, y, data, ps, id) charge.register(ps, "energy_fill", charge.update) in_cap.register(ps, "last_input", function (val) in_cap.update(calc_saturation(val)) end) out_cap.register(ps, "last_output", function (val) out_cap.update(calc_saturation(val)) end) + + local eta = TextBox{parent=rect,x=11,y=20,width=20,height=1,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box} + + eta.register(ps, "eta_mss", function (eta_ms) + local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ") + + local seconds = math.abs(eta_ms) / 1000 + local minutes = seconds / 60 + local hours = minutes / 60 + local days = hours / 24 + + if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then + -- really small or NaN + str = "No ETA" + elseif days < 1000 then + days = math.floor(days) + hours = math.floor(hours % 24) + minutes = math.floor(minutes % 60) + seconds = math.floor(seconds % 60) + + if days > 0 then + str = days .. "d" + elseif hours > 0 then + str = hours .. "h " .. minutes .. "m" + elseif minutes > 0 then + str = minutes .. "m " .. seconds .. "s" + elseif seconds > 0 then + str = seconds .. "s" + end + + str = pre .. str + else + local years = math.floor(days / 365.25) + + if years <= 99999999 then + str = pre .. years .. "y" + else + str = pre .. "eras" + end + end + + eta.set_value(str) + end) end return new_view diff --git a/scada-common/comms.lua b/scada-common/comms.lua index fffaaee..cce000c 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -17,7 +17,7 @@ local max_distance = nil local comms = {} -- protocol/data versions (protocol/data independent changes tracked by util.lua version) -comms.version = "2.5.0" +comms.version = "2.5.1" comms.api_version = "0.0.1" ---@enum PROTOCOL diff --git a/scada-common/util.lua b/scada-common/util.lua index 2f92424..d5c85fa 100644 --- a/scada-common/util.lua +++ b/scada-common/util.lua @@ -22,7 +22,7 @@ local t_pack = table.pack local util = {} -- scada-common version -util.version = "1.3.0" +util.version = "1.3.1" util.TICK_TIME_S = 0.05 util.TICK_TIME_MS = 50 @@ -181,8 +181,7 @@ function util.round(x) return math.floor(x + 0.5) end -- get a new moving average object ---@nodiscard ---@param length integer history length ----@param default number value to fill history with for first call to compute() -function util.mov_avg(length, default) +function util.mov_avg(length) local data = {} local index = 1 local last_t = 0 ---@type number|nil @@ -215,12 +214,10 @@ function util.mov_avg(length, default) ---@return number average function public.compute() local sum = 0 - for i = 1, length do sum = sum + data[i] end - return sum / length + for i = 1, #data do sum = sum + data[i] end + return sum / #data end - public.reset(default) - return public end diff --git a/supervisor/facility.lua b/supervisor/facility.lua index d7058a8..0fd0ed9 100644 --- a/supervisor/facility.lua +++ b/supervisor/facility.lua @@ -128,9 +128,13 @@ function facility.new(config, cooling_conf) test_alarm_states = {}, -- statistics im_stat_init = false, - avg_charge = util.mov_avg(3, 0.0), - avg_inflow = util.mov_avg(6, 0.0), - avg_outflow = util.mov_avg(6, 0.0) + avg_charge = util.mov_avg(3), -- 3 seconds + avg_inflow = util.mov_avg(6), -- 3 seconds + avg_outflow = util.mov_avg(6), -- 3 seconds + -- induction matrix charge delta stats + avg_net = util.mov_avg(60), -- 60 seconds + charge_last = 0, + charge_last_t = 0 } -- create units @@ -307,15 +311,32 @@ function facility.new(config, cooling_conf) rate_update = db.state.last_update if (charge_update > 0) and (rate_update > 0) then + local energy = util.joules_to_fe(db.tanks.energy) + local input = util.joules_to_fe(db.state.last_input) + local output = util.joules_to_fe(db.state.last_output) + if self.im_stat_init then - self.avg_charge.record(util.joules_to_fe(db.tanks.energy), charge_update) - self.avg_inflow.record(util.joules_to_fe(db.state.last_input), rate_update) - self.avg_outflow.record(util.joules_to_fe(db.state.last_output), rate_update) + self.avg_charge.record(energy, charge_update) + self.avg_inflow.record(input, rate_update) + self.avg_outflow.record(output, rate_update) + + if charge_update ~= self.charge_last_t then + local delta = (energy - self.charge_last) / (charge_update - self.charge_last_t) + + self.charge_last = energy + self.charge_last_t = charge_update + + self.avg_net.record(delta, charge_update) + end else self.im_stat_init = true - self.avg_charge.reset(util.joules_to_fe(db.tanks.energy)) - self.avg_inflow.reset(util.joules_to_fe(db.state.last_input)) - self.avg_outflow.reset(util.joules_to_fe(db.state.last_output)) + + self.avg_charge.reset(energy) + self.avg_inflow.reset(input) + self.avg_outflow.reset(output) + + self.charge_last = energy + self.charge_last_t = charge_update end end else @@ -1193,15 +1214,21 @@ function facility.new(config, cooling_conf) status.power = { self.avg_charge.compute(), self.avg_inflow.compute(), - self.avg_outflow.compute() + self.avg_outflow.compute(), + 0 } -- status of induction matricies (including tanks) status.induction = {} for i = 1, #self.induction do - local matrix = self.induction[i] ---@type unit_session - local db = matrix.get_db() ---@type imatrix_session_db + local matrix = self.induction[i] ---@type unit_session + local db = matrix.get_db() ---@type imatrix_session_db + status.induction[i] = { matrix.is_faulted(), db.formed, db.state, db.tanks } + + local fe_per_ms = self.avg_net.compute() + local remaining = util.joules_to_fe(util.trinary(fe_per_ms >= 0, db.tanks.energy_need, db.tanks.energy)) + status.power[4] = remaining / fe_per_ms end -- status of sps diff --git a/supervisor/startup.lua b/supervisor/startup.lua index 2a2202c..d15ddc5 100644 --- a/supervisor/startup.lua +++ b/supervisor/startup.lua @@ -21,7 +21,7 @@ local supervisor = require("supervisor.supervisor") local svsessions = require("supervisor.session.svsessions") -local SUPERVISOR_VERSION = "v1.3.7" +local SUPERVISOR_VERSION = "v1.3.8" local println = util.println local println_ts = util.println_ts diff --git a/supervisor/unit.lua b/supervisor/unit.lua index 6fa4d0a..f0dccf2 100644 --- a/supervisor/unit.lua +++ b/supervisor/unit.lua @@ -71,8 +71,8 @@ function unit.new(reactor_id, num_boilers, num_turbines, ext_idle) ---@class _unit_self local self = { r_id = reactor_id, - plc_s = nil, ---@class plc_session_struct - plc_i = nil, ---@class plc_session + plc_s = nil, ---@type plc_session_struct + plc_i = nil, ---@type plc_session num_boilers = num_boilers, num_turbines = num_turbines, types = { DT_KEYS = DT_KEYS, AISTATE = AISTATE },