Merge pull request #599 from MikaylaFischler/devel

Pocket Beta Release
This commit is contained in:
Mikayla 2025-01-27 12:52:32 -05:00 committed by GitHub
commit cf9e26ac8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1043 additions and 269 deletions

View File

@ -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)

View File

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

View File

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

View File

@ -25,10 +25,9 @@ local ALIGN = core.ALIGN
---@param root Container parent
---@param x integer top left x
---@param y integer top left y
---@param data imatrix_session_db matrix data
---@param ps psil ps interface
---@param id number? matrix ID
local function new_view(root, x, y, data, ps, id)
local function new_view(root, x, y, ps, id)
local label_fg = style.theme.label_fg
local text_fg = style.theme.text_fg
local lu_col = style.lu_colors
@ -94,6 +93,7 @@ local function new_view(root, x, y, data, ps, id)
TextBox{parent=rect,text="FILL I/O",x=2,y=20,width=8,fg_bg=label_fg}
local function calc_saturation(val)
local data = db.facility.induction_data_tbl[id or 1]
if (type(data.build) == "table") and (type(data.build.transfer_cap) == "number") and (data.build.transfer_cap > 0) then
return val / data.build.transfer_cap
else return 0 end
@ -105,46 +105,7 @@ local function new_view(root, x, y, data, ps, id)
local eta = TextBox{parent=rect,x=11,y=20,width=20,text="ETA Unknown",alignment=ALIGN.CENTER,fg_bg=style.theme.field_box}
eta.register(ps, "eta_ms", function (eta_ms)
local str, pre = "", util.trinary(eta_ms >= 0, "Full in ", "Empty in ")
local seconds = math.abs(eta_ms) / 1000
local minutes = seconds / 60
local hours = minutes / 60
local days = hours / 24
if math.abs(eta_ms) < 1000 or (eta_ms ~= eta_ms) then
-- really small or NaN
str = "No ETA"
elseif days < 1000 then
days = math.floor(days)
hours = math.floor(hours % 24)
minutes = math.floor(minutes % 60)
seconds = math.floor(seconds % 60)
if days > 0 then
str = days .. "d"
elseif hours > 0 then
str = hours .. "h " .. minutes .. "m"
elseif minutes > 0 then
str = minutes .. "m " .. seconds .. "s"
elseif seconds > 0 then
str = seconds .. "s"
end
str = pre .. str
else
local years = math.floor(days / 365.25)
if years <= 99999999 then
str = pre .. years .. "y"
else
str = pre .. "eras"
end
end
eta.set_value(str)
end)
eta.register(ps, "eta_string", eta.set_value)
end
return new_view

View File

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

View File

@ -94,6 +94,7 @@ function iocontrol.init_core(pkt_comms, nav, cfg)
-- API access
---@class pocket_ioctl_api
io.api = {
get_fac = function () comms.api__get_facility() end,
get_unit = function (unit) comms.api__get_unit(unit) end,
get_ctrl = function () comms.api__get_control() end,
get_proc = function () comms.api__get_process() end,
@ -192,6 +193,14 @@ function iocontrol.init_fac(conf)
table.insert(io.facility.sps_ps_tbl, psil.create())
table.insert(io.facility.sps_data_tbl, {})
-- create facility tank tables
for i = 1, #io.facility.tank_list do
if io.facility.tank_list[i] == 2 then
table.insert(io.facility.tank_ps_tbl, psil.create())
table.insert(io.facility.tank_data_tbl, {})
end
end
-- create unit data structures
io.units = {} ---@type pioctl_unit[]
for i = 1, conf.num_units do

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

@ -10,6 +10,7 @@ local pocket = require("pocket.pocket")
local control_app = require("pocket.ui.apps.control")
local diag_apps = require("pocket.ui.apps.diag_apps")
local dummy_app = require("pocket.ui.apps.dummy_app")
local facil_app = require("pocket.ui.apps.facility")
local guide_app = require("pocket.ui.apps.guide")
local loader_app = require("pocket.ui.apps.loader")
local process_app = require("pocket.ui.apps.process")
@ -45,7 +46,7 @@ local function init(main)
local db = iocontrol.get_db()
-- window header message and connection status
TextBox{parent=main,y=1,text="EARLY ACCESS ALPHA S C ",fg_bg=style.header}
TextBox{parent=main,y=1,text=" S C ",fg_bg=style.header}
local svr_conn = SignalBar{parent=main,y=1,x=22,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
local crd_conn = SignalBar{parent=main,y=1,x=26,compact=true,colors_low_med=cpair(colors.red,colors.yellow),disconnect_color=colors.lightGray,fg_bg=cpair(colors.green,colors.gray)}
@ -65,6 +66,7 @@ local function init(main)
-- create all the apps & pages
home_page(page_div)
unit_app(page_div)
facil_app(page_div)
control_app(page_div)
process_app(page_div)
waste_app(page_div)

View File

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

View File

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

View File

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

View File

@ -46,7 +46,7 @@ local function new_view(root)
local active_fg_bg = cpair(colors.white,colors.gray)
App{parent=apps_1,x=2,y=2,text="U",title="Units",callback=function()open(APP_ID.UNITS)end,app_fg_bg=cpair(colors.black,colors.yellow),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.DUMMY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=2,text="F",title="Facil",callback=function()open(APP_ID.FACILITY)end,app_fg_bg=cpair(colors.black,colors.orange),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=16,y=2,text="\x15",title="Control",callback=function()open(APP_ID.CONTROL)end,app_fg_bg=cpair(colors.black,colors.green),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=2,y=7,text="\x17",title="Process",callback=function()open(APP_ID.PROCESS)end,app_fg_bg=cpair(colors.black,colors.purple),active_fg_bg=active_fg_bg}
App{parent=apps_1,x=9,y=7,text="\x7f",title="Waste",callback=function()open(APP_ID.WASTE)end,app_fg_bg=cpair(colors.black,colors.brown),active_fg_bg=active_fg_bg}

View File

@ -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)
end
-- always queue the command even if busy
log.warning("device busy, discarding new request" .. unit_dbg_tag)
else
-- queue the command if not busy
unit.pkt_queue.push_packet(packet)
end
else
log.warning("cannot perform requested MODBUS operation" .. unit_dbg_tag)
end

View File

@ -31,7 +31,7 @@ local sna_rtu = require("rtu.dev.sna_rtu")
local sps_rtu = require("rtu.dev.sps_rtu")
local turbinev_rtu = require("rtu.dev.turbinev_rtu")
local RTU_VERSION = "v1.10.21"
local RTU_VERSION = "v1.11.0"
local RTU_UNIT_TYPE = types.RTU_UNIT_TYPE
local RTU_HW_STATE = databus.RTU_HW_STATE

View File

@ -17,8 +17,8 @@ local max_distance = nil
local comms = {}
-- protocol/data versions (protocol/data independent changes tracked by util.lua version)
comms.version = "3.0.3"
comms.api_version = "0.0.8"
comms.version = "3.0.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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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